using NSubstitute; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Notifications.Link; using TeleWave.Application.Notifications.Settings; using TeleWave.Application.Notifications.Subscribers; using TeleWave.Application.Tests.Support; using TeleWave.Domain.Broadcast; using TeleWave.Domain.Notifications; using Xunit; namespace TeleWave.Application.Tests.Notifications; /// /// Что видит и делает админ вокруг бота: выдача ссылки зрителю и список подписчиков. Ссылка — /// единственная дверь в подписку, поэтому проверяем, что старые коды при выдаче новой гаснут. /// public class TelegramAdminTests { private static readonly DateTimeOffset T0 = new(2026, 8, 1, 12, 0, 0, TimeSpan.Zero); [Fact] public async Task Link_BurnsPreviousCodesOfTheSameUser() { var fixture = new TestDb(); var userId = Guid.NewGuid(); var stale = TelegramLinkCode.Issue("old", userId, T0); var foreign = TelegramLinkCode.Issue("alien", Guid.NewGuid(), T0); await using (var seed = fixture.New()) { seed.TelegramSettings.Add(Configured()); seed.TelegramLinkCodes.AddRange(stale, foreign); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var result = await new IssueTelegramLinkCommandHandler( db, new TelegramSettingsService(db), CurrentUser(userId) ).Handle(new IssueTelegramLinkCommand(), CancellationToken.None); await db.SaveChangesAsync(CancellationToken.None); Assert.True(result.IsSuccess); Assert.StartsWith("https://t.me/telewave_bot?start=", result.Value.Url); await using var check = fixture.New(); var codes = check.TelegramLinkCodes.ToList(); // Своя старая ссылка гасится — две живые двери в один аккаунт не нужны; чужая не трогается. Assert.NotNull(codes.Single(c => c.Code == "old").UsedAt); Assert.Null(codes.Single(c => c.Code == "alien").UsedAt); var issued = codes.Single(c => c.UserId == userId && c.UsedAt is null); Assert.EndsWith(issued.Code, result.Value.Url); Assert.Equal(TelegramLinkCode.Lifetime, issued.ExpiresAt - issued.CreatedAt); } [Fact] public async Task Link_WithoutConfiguredBot_IsRefused() { var fixture = new TestDb(); await using var db = fixture.New(); var result = await new IssueTelegramLinkCommandHandler( db, new TelegramSettingsService(db), CurrentUser(Guid.NewGuid()) ).Handle(new IssueTelegramLinkCommand(), CancellationToken.None); // Ссылку не из чего собрать: имя бота приходит только после успешной проверки токена. Assert.False(result.IsSuccess); Assert.Equal("Telegram.NotConfigured", result.Error.Code); Assert.Empty(db.TelegramLinkCodes); } [Fact] public async Task Link_WithoutSignedInViewer_IsRefused() { var fixture = new TestDb(); await using (var seed = fixture.New()) { seed.TelegramSettings.Add(Configured()); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var anonymous = Substitute.For(); anonymous.UserId.Returns((Guid?)null); var result = await new IssueTelegramLinkCommandHandler( db, new TelegramSettingsService(db), anonymous ).Handle(new IssueTelegramLinkCommand(), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Empty(db.TelegramLinkCodes); } [Fact] public async Task Subscribers_ShowChannelsAndUserNamesNewestFirst() { var fixture = new TestDb(); var channel = Channel.Create("Мультреалити", "mult", T0); var userId = Guid.NewGuid(); var active = TelegramSubscriber.Create(100, userId, "viewer", T0); active.Touch(T0.AddHours(2)); active.Toggle(channel.Id, TelegramNotificationKind.ShowStart); active.Toggle(channel.Id, TelegramNotificationKind.UpNext); // Канал удалили, а подписка осталась висеть — это должно быть видно, а не пропасть. active.Toggle(Guid.NewGuid(), TelegramNotificationKind.ShowStart); var older = TelegramSubscriber.Create(101, Guid.NewGuid(), "gone", T0.AddHours(-1)); await using (var seed = fixture.New()) { seed.Channels.Add(channel); seed.TelegramSubscribers.AddRange(active, older); await seed.SaveChangesAsync(CancellationToken.None); } var identity = Substitute.For(); identity .GetProfileAsync(userId, Arg.Any()) .Returns(new CurrentUserProfile(userId, "leonid", "user", false)); await using var db = fixture.New(); var result = await new ListTelegramSubscribersQueryHandler(db, identity).Handle( new ListTelegramSubscribersQuery(), CancellationToken.None ); var subscribers = result.Value; Assert.Equal([100L, 101L], subscribers.Select(s => s.ChatId).ToArray()); Assert.Equal("leonid", subscribers[0].UserName); // Учётку без профиля показываем как есть: чат жив, а пользователя уже нет. Assert.Null(subscribers[1].UserName); var live = subscribers[0].Channels.Single(c => c.ChannelId == channel.Id); Assert.Equal("Мультреалити", live.ChannelName); Assert.Equal(["ShowStart", "UpNext"], live.Kinds); Assert.Equal( "—", subscribers[0].Channels.Single(c => c.ChannelId != channel.Id).ChannelName ); } private static TelegramSettings Configured() { var settings = TelegramSettings.CreateDefault(); settings.Update( new TelegramSettingsUpdate( true, TelegramTransport.Polling, "123:ABC", null, TelegramProxyKind.None, null, null, null, null ) ); settings.SetBotUsername("telewave_bot"); return settings; } private static ICurrentUser CurrentUser(Guid userId) { var currentUser = Substitute.For(); currentUser.UserId.Returns(userId); return currentUser; } }