686550d697
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).
197 lines
7.5 KiB
C#
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");
|
|
}
|
|
}
|