fix(initial-implementation): close P2/P3/P5 review blockers — 185/185 tests green

Combined-batch reviewer flagged three real blockers + two test-infra
issues across the parallel P2/P3/P5 batch. All resolved:

PHASE 3 — DateTimeOffset UTC-kind constructor (3 sites)
  EventListingParserBase.cs:39, EventOddsParser.cs:72, ResultsParser.cs:104
  Replaced `new DateTimeOffset(DateTimeOffset.UtcNow.UtcDateTime, MoscowOffset)`
  (throws ArgumentException because UtcDateTime has Kind=Utc) with
  `DateTimeOffset.UtcNow.ToOffset(MoscowOffset)`.

PHASE 2 — EF string.Compare not translatable (3 sites)
  EventRepository.cs:34, SnapshotRepository.cs:46, ExcelExporter.cs:35
  Replaced `string.Compare(col, str, StringComparison.Ordinal)` with
  `col.CompareTo(str)` so EF Core's SQLite provider can translate the
  expression. Semantics unchanged (SQLite default collation = BINARY = ordinal).

PHASE 3 — ServerTimeProvider regex misses JSON-quoted key
  Regex `serverTime\s*:\s*"..."` only matched bare-key form. Updated to
  `"?serverTime"?\s*:\s*"..."` so the JSON-quoted form (the actual
  marathonbet.by production format) is matched.

PHASE 3 — fixture: orphan <td> elements stripped by HTML5 parser
  tests/.../Fixtures/marathonbet/event-football-sample.html — wrapped
  the <td> blocks in a proper <table><tbody><tr> hierarchy so AngleSharp
  preserves them and `td.Closest("td")` succeeds in the parser.

PHASE 2 — InMemoryDbFixture shared state across parallel tests
  All fixture instances used `Data Source=marathon_tests` causing xUnit's
  parallel-within-class runs to contaminate each other's data. Each fixture
  now uses a Guid-suffixed unique data source name.

PLAN.md — P2/P3/P5 rows updated to  Done with batch commit reference.

Test status:
  Domain.Tests:         96/96 
  Application.Tests:     1/1  
  Infrastructure.Tests: 77/77 
  UI.Tests:             11/11 
  TOTAL:               185/185 
Build: 0 warnings, 0 errors.

Deferred to later phases (per reviewer 🟡 / 🔵 notes):
- SnapshotRepository.GetAsync(Guid) uses lossy GetHashCode workaround;
  Phase 4 to fix or remove from interface.
- Excel Sport name column writes string.Empty (need lookup join in Phase 6).
- PeriodScopeMapper football n>2 falls through to "Quarter" token;
  guarded by MaxPeriods today, but defensive cleanup at Phase 9.
- Settings.razor duplicate m-rise-5 class on Localization section.
This commit is contained in:
2026-05-05 12:09:44 +03:00
parent 686550d697
commit c4d87b59d6
10 changed files with 79 additions and 58 deletions
@@ -29,46 +29,56 @@
</table>
</div>
<!-- Match Result (1x2) -->
<td data-market-type="RESULT">
<span data-selection-price="1.65" data-selection-key="26456117@Match_Result.1">1.65</span>
</td>
<td data-market-type="RESULT">
<span data-selection-price="4.1" data-selection-key="26456117@Match_Result.draw">4.10</span>
</td>
<td data-market-type="RESULT">
<span data-selection-price="5.7" data-selection-key="26456117@Match_Result.3">5.70</span>
</td>
<!-- Handicap -->
<td data-market-type="HANDICAP">
(-1.0)<br/>
<span data-selection-price="2.04" data-selection-key="26456117@To_Win_Match_With_Handicap.HB_H">2.04</span>
</td>
<td data-market-type="HANDICAP">
(+1.0)<br/>
<span data-selection-price="1.82" data-selection-key="26456117@To_Win_Match_With_Handicap.HB_A">1.82</span>
</td>
<!-- Total Goals -->
<td data-market-type="TOTAL">
(2.5)<br/>
<span data-selection-price="1.92" data-selection-key="26456117@Total_Goals.Under_2.5">1.92</span>
</td>
<td data-market-type="TOTAL">
(2.5)<br/>
<span data-selection-price="1.92" data-selection-key="26456117@Total_Goals.Over_2.5">1.92</span>
</td>
<!-- Period 1 (1st Half) Result -->
<span data-selection-price="1.80" data-selection-key="26456117@Result_-_1st_Half.RN_H">1.80</span>
<span data-selection-price="3.60" data-selection-key="26456117@Result_-_1st_Half.RN_D">3.60</span>
<span data-selection-price="4.20" data-selection-key="26456117@Result_-_1st_Half.RN_A">4.20</span>
<!-- Period 2 (2nd Half) Result -->
<span data-selection-price="2.10" data-selection-key="26456117@Result_-_2nd_Half.RN_H">2.10</span>
<span data-selection-price="2.80" data-selection-key="26456117@Result_-_2nd_Half.RN_D">2.80</span>
<span data-selection-price="3.50" data-selection-key="26456117@Result_-_2nd_Half.RN_A">3.50</span>
<table>
<tbody>
<tr>
<!-- Match Result (1x2) -->
<td data-market-type="RESULT">
<span data-selection-price="1.65" data-selection-key="26456117@Match_Result.1">1.65</span>
</td>
<td data-market-type="RESULT">
<span data-selection-price="4.1" data-selection-key="26456117@Match_Result.draw">4.10</span>
</td>
<td data-market-type="RESULT">
<span data-selection-price="5.7" data-selection-key="26456117@Match_Result.3">5.70</span>
</td>
</tr>
<tr>
<!-- Handicap -->
<td data-market-type="HANDICAP">
(-1.0)<br/>
<span data-selection-price="2.04" data-selection-key="26456117@To_Win_Match_With_Handicap.HB_H">2.04</span>
</td>
<td data-market-type="HANDICAP">
(+1.0)<br/>
<span data-selection-price="1.82" data-selection-key="26456117@To_Win_Match_With_Handicap.HB_A">1.82</span>
</td>
</tr>
<tr>
<!-- Total Goals -->
<td data-market-type="TOTAL">
(2.5)<br/>
<span data-selection-price="1.92" data-selection-key="26456117@Total_Goals.Under_2.5">1.92</span>
</td>
<td data-market-type="TOTAL">
(2.5)<br/>
<span data-selection-price="1.92" data-selection-key="26456117@Total_Goals.Over_2.5">1.92</span>
</td>
</tr>
<tr>
<!-- Period 1 (1st Half) Result -->
<td><span data-selection-price="1.80" data-selection-key="26456117@Result_-_1st_Half.RN_H">1.80</span></td>
<td><span data-selection-price="3.60" data-selection-key="26456117@Result_-_1st_Half.RN_D">3.60</span></td>
<td><span data-selection-price="4.20" data-selection-key="26456117@Result_-_1st_Half.RN_A">4.20</span></td>
</tr>
<tr>
<!-- Period 2 (2nd Half) Result -->
<td><span data-selection-price="2.10" data-selection-key="26456117@Result_-_2nd_Half.RN_H">2.10</span></td>
<td><span data-selection-price="2.80" data-selection-key="26456117@Result_-_2nd_Half.RN_D">2.80</span></td>
<td><span data-selection-price="3.50" data-selection-key="26456117@Result_-_2nd_Half.RN_A">3.50</span></td>
</tr>
</tbody>
</table>
</body>
</html>
@@ -17,13 +17,19 @@ public sealed class InMemoryDbFixture : IDisposable
public InMemoryDbFixture()
{
// Keep a single connection open so the in-memory DB is not dropped between
// DbContext operations. Cache=Shared ensures the same DB is reused.
_keepAliveConnection = new SqliteConnection("Data Source=marathon_tests;Mode=Memory;Cache=Shared");
// Each fixture instance gets its own in-memory database so xUnit's per-test
// isolation isn't violated when tests run in parallel within a class
// (the previous shared "marathon_tests" name caused state contamination).
var dbName = $"marathon_tests_{Guid.NewGuid():N}";
var connectionString = $"Data Source={dbName};Mode=Memory;Cache=Shared";
// Keep a single connection open so the named in-memory DB lives until the
// fixture is disposed.
_keepAliveConnection = new SqliteConnection(connectionString);
_keepAliveConnection.Open();
var options = new DbContextOptionsBuilder<MarathonDbContext>()
.UseSqlite("Data Source=marathon_tests;Mode=Memory;Cache=Shared")
.UseSqlite(connectionString)
.Options;
DbContext = new MarathonDbContext(options);