Files
IBKRTrader/tests/IBKRTrader.Tests/Modules/CapitolTradesScraperTests.cs
T
RichardandClaude Opus 5 8b9b993d1d L1b/2: Kultur und Dateisystem plattformunabhaengig machen
Kultur - Ausgaben und Parsen haengen nicht mehr am Host:

- PdfExporter formatierte Betraege mit ToString("N2") ohne Formatanbieter,
  also CurrentCulture. Auf dem deutschen Desktop "1.234,56", in einem
  Container mit LANG=C "1,234.56" - dieselbe Zahl, fuer einen Leser eine
  andere. Fuer ein ausdruecklich pruefbares Dokument jetzt fest de-DE.
- CapitolTradesScraper.ParseDate nutzte DateOnly.TryParse ohne
  Formatanbieter. Das ist nicht theoretisch: gemessen wurde aus
  "2026-08-04" unter th-TH das Jahr 1483 (buddhistischer Kalender), unter
  fa-IR das Jahr 2647 (persischer Kalender), unter ar-SA schlug das Parsen
  ganz fehl. de-DE und en-US kommen mit ISO klar - genau deshalb faellt so
  etwas auf dem Entwicklungsrechner nie auf. Jetzt TryParseExact mit
  InvariantCulture; ein Formatwechsel der Quelle faellt damit auf, statt
  still ein falsches Datum zu erzeugen. Regressionstest ueber vier Kulturen.
- IBKRGatewayService baute den Query-Parameter mit .ToString().ToLower()
  (Tuerkisch-I) - jetzt fest "true"/"false".

PDF-Schriften: PDFsharp 6 loest auf Nicht-Windows-Plattformen nichts von
selbst auf, "Segoe UI" gibt es dort nicht - der Export waere zur Laufzeit
gescheitert. Neuer DocumentFontResolver: unter Windows bleibt die Plattform
zustaendig (unveraenderte Optik), auf Linux wird eine freie Systemschrift
gesucht (DejaVu/Liberation/Noto/FreeSans). Bewusst keine Schrift im Repo -
das erspart eine Lizenzfrage; fehlt sie, nennt die Fehlermeldung das zu
installierende Paket.

BackupWorker:
- Suchte "mysqldump.exe" in C:\Program Files\... und splittete PATH mit ';'.
  Auf Linux ist das Trennzeichen ':' - der gesamte PATH waere als ein
  Eintrag gelesen worden. Jetzt Path.PathSeparator, plattformabhaengige
  Suchpfade und zusaetzlich "mariadb-dump" (MariaDB hat mysqldump ab 10.5
  umbenannt).
- Das DB-Passwort stand als Kommandozeilenargument im Prozessbaum. Unter
  Linux ist /proc/<pid>/cmdline fuer jeden lokalen Nutzer lesbar - das waere
  eine neue Offenlegung gewesen, die es unter Windows so nicht gab. Jetzt
  ueber MYSQL_PWD, nur an den Kindprozess vererbt. Argumente einzeln statt
  als Zeichenkette (kein Quoting-Problem bei Pfaden mit Leerzeichen).

Verifiziert: 188 Tests gruen (+5), Build 0 Fehler/0 Warnungen, Core + 3
Module + Tests bauen fuer linux-x64, --smoke-ui konstruiert alle 7 Fenster.

Offen aus L1b und nach L2 verschoben: IAppPaths (Logs/Backups/settings.json/
master.key liegen neben der Binaerdatei; unter /opt ist das nicht schreibbar).
Gehoert zum Daemon, wo die Pfade tatsaechlich gebraucht werden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:58:10 +02:00

94 lines
3.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Globalization;
using FluentAssertions;
using IBKRTrader.Core.Logging;
using IBKRTrader.Modules.CongressTrading.Scraper;
namespace IBKRTrader.Tests.Modules;
[Trait("cat", "unit")]
public class CapitolTradesScraperTests
{
private static string LoadFixture()
{
var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", "ct_raw.html");
File.Exists(path).Should().BeTrue($"Fixture fehlt: {path}");
return File.ReadAllText(path);
}
[Fact]
public void ParseTradesFromHtml_ExtractsTradesFromRscStream()
{
var scraper = new CapitolTradesScraper(new LoggingService());
var (trades, memberIds) = scraper.ParseTradesFromHtml(LoadFixture());
trades.Should().NotBeEmpty("die Fixture enthält einen RSC-Datenstrom mit Trades");
memberIds.Should().NotBeEmpty();
var first = trades[0];
first.TradeId.Should().NotBeNullOrWhiteSpace();
first.TradeType.Should().NotBeNullOrWhiteSpace();
}
[Fact]
public void ParseTradesFromHtml_MapsMemberSnapshotConsistently()
{
var scraper = new CapitolTradesScraper(new LoggingService());
var (trades, memberIds) = scraper.ParseTradesFromHtml(LoadFixture());
// Jeder Member-Snapshot muss eine BioId haben, die auch im MemberBioIds-Set liegt.
foreach (var trade in trades.Where(t => t.MemberSnapshot is not null))
{
trade.MemberSnapshot!.BioId.Should().NotBeNullOrWhiteSpace();
memberIds.Should().Contain(trade.MemberBioId);
}
}
[Theory]
[InlineData("de-DE")] // Tag.Monat.Jahr
[InlineData("th-TH")] // buddhistischer Kalender
[InlineData("fa-IR")] // persischer Kalender
[InlineData("ar-SA")] // Hidschri-Kalender
[InlineData("")] // Invariant so verhält sich ein Container ohne LANG
public void ParseTradesFromHtml_LiefertDieselbenDaten_UnabhaengigVonDerHostKultur(string culture)
{
// Die Datumsfelder kamen ueber DateOnly.TryParse OHNE Formatanbieter herein, also mit der
// Kultur des Rechners. Bei Kulturen mit eigenem Kalender ist das nicht theoretisch:
// aus "2026-08-04" wurde unter th-TH das Jahr 1483 und unter fa-IR das Jahr 2647,
// unter ar-SA schlug das Parsen ganz fehl. de-DE und en-US kommen mit ISO klar - genau
// deshalb faellt so ein Fehler auf dem Entwicklungsrechner nie auf.
var previous = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture);
var (trades, _) = new CapitolTradesScraper(new LoggingService()).ParseTradesFromHtml(LoadFixture());
trades.Should().NotBeEmpty();
trades.Should().Contain(t => t.TradeDate != null,
"die Fixture enthaelt Handelsdaten im Format yyyy-MM-dd");
// Alle geparsten Daten muessen plausibel sein ein kulturbedingter Tag/Monat-Dreher
// faellt hier auf, weil Tage > 12 sonst gar nicht parsen wuerden.
foreach (var t in trades.Where(t => t.TradeDate != null))
t.TradeDate!.Value.Year.Should().BeInRange(2000, 2100);
}
finally
{
CultureInfo.CurrentCulture = previous;
}
}
[Fact]
public void ParseTradesFromHtml_EmptyHtml_ReturnsNoTrades()
{
var scraper = new CapitolTradesScraper(new LoggingService());
var (trades, memberIds) = scraper.ParseTradesFromHtml("<html><body>nichts</body></html>");
trades.Should().BeEmpty();
memberIds.Should().BeEmpty();
}
}