using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Notifications;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using TeleWave.Domain.Notifications;
using Xunit;
namespace TeleWave.Application.Tests.Notifications;
///
/// Рассылка по ленте. Проверяется то, из-за чего бот становится спамом: оповещение шлётся
/// на смену шоу, а не на каждую серию, и только тем, кто подписан на этот канал.
///
public class TelegramNotifierTests
{
private static readonly DateTimeOffset Now = new(2026, 8, 1, 20, 0, 0, TimeSpan.Zero);
private static TelegramSettings Settings()
{
var settings = TelegramSettings.CreateDefault();
settings.Update(
new TelegramSettingsUpdate(
true,
TelegramTransport.Polling,
"123:ABC",
null,
TelegramProxyKind.None,
null,
null,
null,
null
)
);
return settings;
}
private static ScheduleEntry Program(
Guid channelId,
Guid showId,
DateTimeOffset start,
int minutes
) =>
ScheduleEntry.FromSlot(
channelId,
Guid.NewGuid(),
ScheduleEntryKind.Program,
start,
start.AddMinutes(minutes),
new ScheduleEntryOrigin(showId, 0, null, null, null),
0
);
[Fact]
public async Task Notifies_OnlySubscribersOfTheChannel_AndOnlyOnShowChange()
{
var fixture = new TestDb();
var channel = Channel.Create("Мультреалити", "mult", Now);
var other = Channel.Create("Второй", "second", Now);
var show = Show.Create("Футурама", ShowKind.Series);
var another = Show.Create("Симпсоны", ShowKind.Series);
var subscriber = TelegramSubscriber.Create(100, Guid.NewGuid(), "viewer", Now);
subscriber.Toggle(channel.Id, TelegramNotificationKind.ShowStart);
var stranger = TelegramSubscriber.Create(200, Guid.NewGuid(), "other", Now);
stranger.Toggle(other.Id, TelegramNotificationKind.ShowStart);
await using (var seed = fixture.New())
{
seed.Channels.AddRange(channel, other);
seed.Shows.AddRange(show, another);
seed.TelegramSubscribers.AddRange(subscriber, stranger);
// Предыдущая программа того же шоу: следующая серия сменой не считается.
seed.ScheduleEntries.Add(Program(channel.Id, show.Id, Now.AddMinutes(-30), 25));
seed.ScheduleEntries.Add(Program(channel.Id, show.Id, Now.AddMinutes(-2), 25));
await seed.SaveChangesAsync(CancellationToken.None);
}
var api = Substitute.For();
var settings = Settings();
settings.MarkNotifiedUntil(Now.AddMinutes(-5));
await using var db = fixture.New();
var sent = await new TelegramNotifier(db, api).RunAsync(
settings,
Now,
CancellationToken.None
);
Assert.Equal(0, sent);
await api.DidNotReceive()
.SendMessageAsync(
Arg.Any(),
Arg.Any(),
Arg.Any(),
Arg.Any>?>(),
Arg.Any()
);
}
[Fact]
public async Task Notifies_WhenShowChanges()
{
var fixture = new TestDb();
var channel = Channel.Create("Мультреалити", "mult", Now);
var previous = Show.Create("Симпсоны", ShowKind.Series);
var started = Show.Create("Футурама", ShowKind.Series);
var subscriber = TelegramSubscriber.Create(100, Guid.NewGuid(), "viewer", Now);
subscriber.Toggle(channel.Id, TelegramNotificationKind.ShowStart);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.Shows.AddRange(previous, started);
seed.TelegramSubscribers.Add(subscriber);
seed.ScheduleEntries.Add(Program(channel.Id, previous.Id, Now.AddMinutes(-30), 25));
seed.ScheduleEntries.Add(Program(channel.Id, started.Id, Now.AddMinutes(-1), 25));
await seed.SaveChangesAsync(CancellationToken.None);
}
var api = Substitute.For();
var settings = Settings();
settings.MarkNotifiedUntil(Now.AddMinutes(-5));
await using var db = fixture.New();
var sent = await new TelegramNotifier(db, api).RunAsync(
settings,
Now,
CancellationToken.None
);
Assert.Equal(1, sent);
await api.Received(1)
.SendMessageAsync(
Arg.Any(),
100,
// Канал назван в самом сообщении: их у зрителя несколько, и «сейчас в эфире»
// без названия не отвечает на вопрос, куда переключаться.
// Предикат NSubstitute принимает аргумент как допускающий null — само сообщение
// объявлено обязательным, потому и разыменовываем без проверки.
Arg.Is(s => s!.Contains("Футурама") && s.Contains("Мультреалити")),
null,
Arg.Any()
);
// Граница разобранного двигается всегда — иначе следующий тик оповестил бы о том же.
Assert.Equal(Now, settings.NotifiedUntil);
}
[Fact]
public async Task Notifies_UpNext_TellsWhenAndWhere()
{
var fixture = new TestDb();
var channel = Channel.Create("Мультреалити", "mult", Now);
var soon = Show.Create("Гриффины", ShowKind.Series);
var subscriber = TelegramSubscriber.Create(100, Guid.NewGuid(), "viewer", Now);
subscriber.Toggle(channel.Id, TelegramNotificationKind.UpNext);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.Shows.Add(soon);
seed.TelegramSubscribers.Add(subscriber);
seed.ScheduleEntries.Add(Program(channel.Id, soon.Id, Now.AddMinutes(4), 25));
await seed.SaveChangesAsync(CancellationToken.None);
}
var api = Substitute.For();
var settings = Settings();
// Окно предупреждения — свои пять минут: разобранное кончается на Now, а смотрим до Now+5.
settings.MarkNotifiedUntil(Now.AddMinutes(-5));
await using var db = fixture.New();
var sent = await new TelegramNotifier(db, api).RunAsync(
settings,
Now,
CancellationToken.None
);
Assert.Equal(1, sent);
await api.Received(1)
.SendMessageAsync(
Arg.Any(),
100,
Arg.Is(s =>
s!.Contains("Гриффины") && s.Contains("4 мин") && s.Contains("Мультреалити")
),
null,
Arg.Any()
);
}
[Fact]
public async Task Notifies_ReplacingItsPreviousAlert()
{
var fixture = new TestDb();
var channel = Channel.Create("Мультреалити", "mult", Now);
var previous = Show.Create("Симпсоны", ShowKind.Series);
var started = Show.Create("Футурама", ShowKind.Series);
var subscriber = TelegramSubscriber.Create(100, Guid.NewGuid(), "viewer", Now);
subscriber.Toggle(channel.Id, TelegramNotificationKind.ShowStart);
subscriber.SetAlertMessage(777);
// Программа, запрошенная зрителем, — не оповещение: рассылка её трогать не должна.
subscriber.SetNoticeMessage(555);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.Shows.AddRange(previous, started);
seed.TelegramSubscribers.Add(subscriber);
seed.ScheduleEntries.Add(Program(channel.Id, previous.Id, Now.AddMinutes(-30), 25));
seed.ScheduleEntries.Add(Program(channel.Id, started.Id, Now.AddMinutes(-1), 25));
await seed.SaveChangesAsync(CancellationToken.None);
}
var api = Substitute.For();
api.SendMessageAsync(
Arg.Any(),
Arg.Any(),
Arg.Any(),
Arg.Any>?>(),
Arg.Any()
)
.Returns(4242L);
var settings = Settings();
settings.MarkNotifiedUntil(Now.AddMinutes(-5));
await using (var db = fixture.New())
{
await new TelegramNotifier(db, api).RunAsync(settings, Now, CancellationToken.None);
await db.SaveChangesAsync(CancellationToken.None);
}
// Вчерашнее «сейчас в эфире» — уже неправда: старое снимаем, новое запоминаем.
await api.Received(1)
.DeleteMessageAsync(
Arg.Any(),
100,
777,
Arg.Any()
);
await using var check = fixture.New();
var stored = check.TelegramSubscribers.Single();
Assert.Equal(4242L, stored.AlertMessageId);
Assert.Equal(555L, stored.NoticeMessageId);
}
}