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); } }