Files
maraphon-app/tests/Marathon.Infrastructure.Tests/Scraping/EventOddsParserTests.cs
T
alexei.dolgolyov 686550d697 fix(initial-implementation): resolve P2/P3 cross-phase build issues
Three minimal fixes to make Marathon.sln build with 0/0:

1. Marathon.Infrastructure.csproj — add InternalsVisibleTo for
   Marathon.Infrastructure.Tests so test code can reference internal
   repository and exporter classes (Phase 2 issue blocking Phase 3 tests).
2. EventOddsParserTests.cs — add 'using Marathon.Domain.ValueObjects' so
   MatchScope/PeriodScope resolve.
3. RoundTripTests.cs — add 'using Microsoft.EntityFrameworkCore' so the
   ExecuteSqlRawAsync extension method on DatabaseFacade resolves.

Phase 5's anticipated LocalizationOptions / Serilog issues were already
resolved by its agent before being killed — no changes needed there.

Build status: 0 warnings, 0 errors.
Test status: Domain 96/96, UI 11/11, Infrastructure 42/77 (35 failing —
parser fixture issues + a real DateTimeOffset bug; reviewer will assess).
2026-05-05 11:35:42 +03:00

197 lines
7.5 KiB
C#

using FluentAssertions;
using Marathon.Domain.Enums;
using Marathon.Domain.ValueObjects;
using Marathon.Infrastructure.Scraping.Parsers;
using Microsoft.Extensions.Logging.Abstractions;
namespace Marathon.Infrastructure.Tests.Scraping;
public sealed class EventOddsParserTests
{
private static string FixturePath(string filename) => Path.Combine(
AppContext.BaseDirectory,
"Fixtures", "marathonbet", filename);
private readonly EventOddsParser _sut;
public EventOddsParserTests()
{
var serverTime = new ServerTimeProvider(NullLogger<ServerTimeProvider>.Instance);
var periodMapper = new PeriodScopeMapper(basketballQuarterMode: false);
_sut = new EventOddsParser(
serverTime,
periodMapper,
NullLogger<EventOddsParser>.Instance);
}
// ── Football fixture ───────────────────────────────────────────────────
[Fact]
public async Task ParseAsync_FootballFixture_ReturnsNonNullSnapshot()
{
var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html"));
var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch);
snapshot.Should().NotBeNull();
}
[Fact]
public async Task ParseAsync_FootballFixture_SnapshotEventIdMatches()
{
var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html"));
var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch);
snapshot!.EventId.Value.Should().Be("26456117");
}
[Fact]
public async Task ParseAsync_FootballFixture_MatchWin1Extracted()
{
var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html"));
var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch);
var win1 = snapshot!.Bets.FirstOrDefault(b =>
b.Scope is MatchScope && b.Type == BetType.Win && b.Side == Side.Side1);
win1.Should().NotBeNull("Match Win-1 bet must be present");
win1!.Rate.Value.Should().Be(1.65m);
}
[Fact]
public async Task ParseAsync_FootballFixture_MatchDrawExtracted()
{
var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html"));
var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch);
var draw = snapshot!.Bets.FirstOrDefault(b =>
b.Scope is MatchScope && b.Type == BetType.Draw);
draw.Should().NotBeNull("Match Draw bet must be present");
draw!.Rate.Value.Should().Be(4.1m);
}
[Fact]
public async Task ParseAsync_FootballFixture_MatchWin2Extracted()
{
var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html"));
var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch);
var win2 = snapshot!.Bets.FirstOrDefault(b =>
b.Scope is MatchScope && b.Type == BetType.Win && b.Side == Side.Side2);
win2.Should().NotBeNull("Match Win-2 bet must be present");
win2!.Rate.Value.Should().Be(5.7m);
}
[Fact]
public async Task ParseAsync_FootballFixture_HandicapBetsExtracted()
{
var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html"));
var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch);
var fora1 = snapshot!.Bets.FirstOrDefault(b =>
b.Scope is MatchScope && b.Type == BetType.WinFora && b.Side == Side.Side1);
var fora2 = snapshot.Bets.FirstOrDefault(b =>
b.Scope is MatchScope && b.Type == BetType.WinFora && b.Side == Side.Side2);
fora1.Should().NotBeNull("Handicap Side1 must be present");
fora2.Should().NotBeNull("Handicap Side2 must be present");
fora1!.Value!.Value.Should().Be(-1.0m);
fora1.Rate.Value.Should().Be(2.04m);
fora2!.Value!.Value.Should().Be(1.0m);
fora2.Rate.Value.Should().Be(1.82m);
}
[Fact]
public async Task ParseAsync_FootballFixture_TotalBetsExtracted()
{
var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html"));
var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch);
var totalLess = snapshot!.Bets.FirstOrDefault(b =>
b.Scope is MatchScope && b.Type == BetType.Total && b.Side == Side.Less);
var totalMore = snapshot.Bets.FirstOrDefault(b =>
b.Scope is MatchScope && b.Type == BetType.Total && b.Side == Side.More);
totalLess.Should().NotBeNull("Total Less must be present");
totalMore.Should().NotBeNull("Total More must be present");
totalLess!.Value!.Value.Should().Be(2.5m);
totalMore!.Value!.Value.Should().Be(2.5m);
}
[Fact]
public async Task ParseAsync_FootballFixture_Period1WinExtracted()
{
var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html"));
var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch);
var p1Win1 = snapshot!.Bets.FirstOrDefault(b =>
b.Scope is PeriodScope { Number: 1 } && b.Type == BetType.Win && b.Side == Side.Side1);
var p1Draw = snapshot.Bets.FirstOrDefault(b =>
b.Scope is PeriodScope { Number: 1 } && b.Type == BetType.Draw);
var p1Win2 = snapshot.Bets.FirstOrDefault(b =>
b.Scope is PeriodScope { Number: 1 } && b.Type == BetType.Win && b.Side == Side.Side2);
p1Win1.Should().NotBeNull("Period-1 Win-1 must be present for football");
p1Draw.Should().NotBeNull("Period-1 Draw must be present for football");
p1Win2.Should().NotBeNull("Period-1 Win-2 must be present for football");
}
[Fact]
public async Task ParseAsync_FootballFixture_SourceIsStamped()
{
var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html"));
var snapshot = await _sut.ParseAsync(html, OddsSource.Live);
snapshot!.Source.Should().Be(OddsSource.Live);
}
// ── Basketball fixture ─────────────────────────────────────────────────
[Fact]
public async Task ParseAsync_BasketballFixture_MatchWin1WithNoDrawExtracted()
{
var html = await File.ReadAllTextAsync(FixturePath("event-basketball-sample.html"));
var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch);
snapshot.Should().NotBeNull();
var win1 = snapshot!.Bets.FirstOrDefault(b =>
b.Scope is MatchScope && b.Type == BetType.Win && b.Side == Side.Side1);
var draw = snapshot.Bets.FirstOrDefault(b =>
b.Scope is MatchScope && b.Type == BetType.Draw);
win1.Should().NotBeNull("Basketball Match Win-1 must be present");
draw.Should().BeNull("Basketball (OT market) has no Draw outcome");
}
[Fact]
public async Task ParseAsync_BasketballFixture_Period1WinsExtracted()
{
var html = await File.ReadAllTextAsync(FixturePath("event-basketball-sample.html"));
var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch);
var p1Win1 = snapshot!.Bets.FirstOrDefault(b =>
b.Scope is PeriodScope { Number: 1 } && b.Type == BetType.Win && b.Side == Side.Side1);
var p1Win2 = snapshot.Bets.FirstOrDefault(b =>
b.Scope is PeriodScope { Number: 1 } && b.Type == BetType.Win && b.Side == Side.Side2);
p1Win1.Should().NotBeNull("Basketball Period-1 Win-1 must be present");
p1Win2.Should().NotBeNull("Basketball Period-1 Win-2 must be present");
}
}