f294255f10
- Add IEventRepository/IResultRepository.GetManyAsync to kill N+1 lookups at 6 sites (backtest, outcome eval, both bet-journal paths, anomaly browsing, results selection); guarded by a Received(1).GetManyAsync test. - Add EventRepository.QueryAsync to push date+sport filtering to SQL (was load-whole-range-then-filter); search/sort stay in-memory for Cyrillic order. - Add AnomalyRepository.CountSinceAsync (unread badge) + ListByDateRangeAsync (feed date filter); add Event/Snapshot count methods for the dashboard. - Add composite indexes IX_Snapshots_EventCode_CapturedAt and _EventCode_Source_CapturedAt via a new migration + model snapshot. - Introduce SqliteDateText as the single source of the O-format date encoding shared by Mapping (read/write) and the repositories' range predicates. - Fix LiveOddsPoller cadence drift (budget sleep against cycle time); make DetectAnomalies dedup O(1) per event; add Event.Title to dedup the title join. Tests adapted to the batched GetManyAsync via a TestFixtures bridge.
108 lines
4.3 KiB
C#
108 lines
4.3 KiB
C#
using Marathon.Application.Abstractions;
|
|
using Marathon.Application.Configuration;
|
|
using Marathon.Domain.Entities;
|
|
using Marathon.Domain.Enums;
|
|
using Marathon.Domain.ValueObjects;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
|
|
namespace Marathon.Application.Tests.UseCases;
|
|
|
|
/// <summary>
|
|
/// Shared factory helpers for domain objects used across use-case tests.
|
|
/// </summary>
|
|
internal static class TestFixtures
|
|
{
|
|
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
|
|
|
/// <summary>
|
|
/// Bridges the legacy per-id <c>GetAsync</c> stubs to the batched
|
|
/// <c>GetManyAsync</c> the use cases now call: each requested id is resolved
|
|
/// through whatever <c>GetAsync</c> was configured to return for it. Lets the
|
|
/// existing per-id <c>.Returns(...)</c> setups keep working unchanged.
|
|
/// </summary>
|
|
public static void BridgeGetMany(IEventRepository events)
|
|
{
|
|
events.GetManyAsync(Arg.Any<IReadOnlyCollection<EventId>>(), Arg.Any<CancellationToken>())
|
|
.Returns(ci =>
|
|
{
|
|
var ids = ci.Arg<IReadOnlyCollection<EventId>>();
|
|
var dict = new Dictionary<EventId, Event>();
|
|
foreach (var id in ids.Distinct())
|
|
{
|
|
var ev = events.GetAsync(id, CancellationToken.None).GetAwaiter().GetResult();
|
|
if (ev is not null) dict[id] = ev;
|
|
}
|
|
return (IReadOnlyDictionary<EventId, Event>)dict;
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc cref="BridgeGetMany(IEventRepository)"/>
|
|
public static void BridgeGetMany(IResultRepository results)
|
|
{
|
|
results.GetManyAsync(Arg.Any<IReadOnlyCollection<EventId>>(), Arg.Any<CancellationToken>())
|
|
.Returns(ci =>
|
|
{
|
|
var ids = ci.Arg<IReadOnlyCollection<EventId>>();
|
|
var dict = new Dictionary<EventId, EventResult>();
|
|
foreach (var id in ids.Distinct())
|
|
{
|
|
var r = results.GetAsync(id, CancellationToken.None).GetAwaiter().GetResult();
|
|
if (r is not null) dict[id] = r;
|
|
}
|
|
return (IReadOnlyDictionary<EventId, EventResult>)dict;
|
|
});
|
|
}
|
|
|
|
/// <summary>Creates a minimal valid <see cref="Event"/> with the given event ID string.</summary>
|
|
public static Event MakeEvent(string eventIdValue = "12345678")
|
|
{
|
|
return new Event(
|
|
Id: new EventId(eventIdValue),
|
|
Sport: new SportCode(6),
|
|
CountryCode: "BY",
|
|
LeagueId: "league-1",
|
|
Category: "Group A",
|
|
ScheduledAt: new DateTimeOffset(2026, 5, 10, 18, 0, 0, MoscowOffset),
|
|
Side1Name: "Team A",
|
|
Side2Name: "Team B");
|
|
}
|
|
|
|
/// <summary>Creates a minimal valid <see cref="OddsSnapshot"/> for the given event.</summary>
|
|
public static OddsSnapshot MakeSnapshot(EventId eventId, OddsSource source = OddsSource.PreMatch)
|
|
{
|
|
var bets = new List<Bet>
|
|
{
|
|
new Bet(MatchScope.Instance, BetType.Win, Side.Side1, value: null, new OddsRate(1.85m)),
|
|
new Bet(MatchScope.Instance, BetType.Win, Side.Side2, value: null, new OddsRate(2.10m)),
|
|
};
|
|
|
|
return new OddsSnapshot(eventId, DateTimeOffset.UtcNow, source, bets);
|
|
}
|
|
|
|
/// <summary>Creates a minimal valid <see cref="EventResult"/> for the given event ID.</summary>
|
|
public static EventResult MakeResult(EventId eventId)
|
|
{
|
|
return new EventResult(eventId, 2, 1, Side.Side1, DateTimeOffset.UtcNow);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IOptionsMonitor{TOptions}"/> that always returns the given
|
|
/// throttle. Use 1 for sequential test behaviour, higher values to exercise fan-out.
|
|
/// </summary>
|
|
public static IOptionsMonitor<ScrapingThrottle> Throttle(int maxConcurrentRequests = 1) =>
|
|
new StaticOptionsMonitor<ScrapingThrottle>(new ScrapingThrottle
|
|
{
|
|
MaxConcurrentRequests = maxConcurrentRequests,
|
|
});
|
|
|
|
private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T> where T : class
|
|
{
|
|
private readonly T _value;
|
|
public StaticOptionsMonitor(T value) => _value = value;
|
|
public T CurrentValue => _value;
|
|
public T Get(string? name) => _value;
|
|
public IDisposable? OnChange(Action<T, string?> listener) => null;
|
|
}
|
|
}
|