005d4e794a
- INotificationSink + AnomalyNotification (Application) and a testable GetPendingAnomalyNotificationsUseCase (date+score filter, event-title join, oldest-first). 4 use-case tests. - TelegramNotificationSink posts to the Bot API via HttpClient (no SDK dependency); no-ops with a warning when unconfigured and never logs the token. - AnomalyNotificationDispatcher BackgroundService: startup-baselined marker advanced past the newest sent (gap- and dup-free); idles until Notifications:Enabled. - Wire options + named client + sink + dispatcher in InfrastructureModule. Add a secret-free Notifications section + steam-move tuning to appsettings.json (bot token + chat id go in appsettings.Local.json only).
96 lines
3.6 KiB
C#
96 lines
3.6 KiB
C#
using FluentAssertions;
|
|
using Marathon.Application.Abstractions;
|
|
using Marathon.Application.UseCases;
|
|
using Marathon.Domain.Entities;
|
|
using Marathon.Domain.Enums;
|
|
using Marathon.Domain.ValueObjects;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NSubstitute;
|
|
|
|
namespace Marathon.Application.Tests.UseCases;
|
|
|
|
public sealed class GetPendingAnomalyNotificationsUseCaseTests
|
|
{
|
|
private readonly IAnomalyRepository _anomalies = Substitute.For<IAnomalyRepository>();
|
|
private readonly IEventRepository _events = Substitute.For<IEventRepository>();
|
|
|
|
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
|
private static readonly DateTimeOffset BaseTime = new(2026, 5, 10, 18, 0, 0, MoscowOffset);
|
|
|
|
public GetPendingAnomalyNotificationsUseCaseTests()
|
|
{
|
|
// Event titles are resolved via the batched GetManyAsync; route it through
|
|
// the per-id GetAsync stubs each test configures.
|
|
TestFixtures.BridgeGetMany(_events);
|
|
}
|
|
|
|
private GetPendingAnomalyNotificationsUseCase CreateSut() =>
|
|
new(_anomalies, _events, NullLogger<GetPendingAnomalyNotificationsUseCase>.Instance);
|
|
|
|
private static Anomaly MakeAnomaly(EventId id, decimal score, DateTimeOffset detectedAt) =>
|
|
new(Guid.NewGuid(), id, detectedAt, AnomalyKind.SuspensionFlip, score, "{}");
|
|
|
|
private static Event MakeEvent(EventId id) =>
|
|
new(id, new SportCode(11), "BY", "L1", "Cat", BaseTime, "Team A", "Team B");
|
|
|
|
private void StubAnomalies(params Anomaly[] anomalies) =>
|
|
_anomalies.ListByDateRangeAsync(
|
|
Arg.Any<DateTimeOffset?>(), Arg.Any<DateTimeOffset?>(), Arg.Any<CancellationToken>())
|
|
.Returns(anomalies.ToList().AsReadOnly());
|
|
|
|
[Fact]
|
|
public async Task Should_ExcludeAnomaliesBelowMinScore()
|
|
{
|
|
var id = new EventId("11111111");
|
|
StubAnomalies(
|
|
MakeAnomaly(id, 0.30m, BaseTime),
|
|
MakeAnomaly(id, 0.70m, BaseTime.AddMinutes(1)));
|
|
_events.GetAsync(id, Arg.Any<CancellationToken>()).Returns(MakeEvent(id));
|
|
|
|
var result = await CreateSut().ExecuteAsync(BaseTime.AddHours(-1), minScore: 0.45m, CancellationToken.None);
|
|
|
|
result.Should().ContainSingle();
|
|
result[0].Score.Should().Be(0.70m);
|
|
result[0].EventTitle.Should().Be("Team A vs Team B");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_OrderOldestFirst()
|
|
{
|
|
var id1 = new EventId("11111111");
|
|
var id2 = new EventId("22222222");
|
|
StubAnomalies(
|
|
MakeAnomaly(id2, 0.60m, BaseTime.AddMinutes(5)),
|
|
MakeAnomaly(id1, 0.50m, BaseTime));
|
|
_events.GetAsync(id1, Arg.Any<CancellationToken>()).Returns(MakeEvent(id1));
|
|
_events.GetAsync(id2, Arg.Any<CancellationToken>()).Returns(MakeEvent(id2));
|
|
|
|
var result = await CreateSut().ExecuteAsync(BaseTime.AddHours(-1), 0.45m, CancellationToken.None);
|
|
|
|
result.Select(n => n.DetectedAt).Should().BeInAscendingOrder();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_FallBackToEventId_When_EventMissing()
|
|
{
|
|
var id = new EventId("orphan00");
|
|
StubAnomalies(MakeAnomaly(id, 0.55m, BaseTime));
|
|
_events.GetAsync(id, Arg.Any<CancellationToken>()).Returns((Event?)null);
|
|
|
|
var result = await CreateSut().ExecuteAsync(BaseTime.AddHours(-1), 0.45m, CancellationToken.None);
|
|
|
|
result.Should().ContainSingle();
|
|
result[0].EventTitle.Should().Be("orphan00");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_ReturnEmpty_When_NothingQualifies()
|
|
{
|
|
StubAnomalies(Array.Empty<Anomaly>());
|
|
|
|
var result = await CreateSut().ExecuteAsync(BaseTime, 0.45m, CancellationToken.None);
|
|
|
|
result.Should().BeEmpty();
|
|
}
|
|
}
|