using NSubstitute; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Notifications; using TeleWave.Application.Notifications.Settings; using TeleWave.Application.Tests.Support; using TeleWave.Domain.Notifications; using TeleWave.Infrastructure.Persistence; using Xunit; namespace TeleWave.Application.Tests.Notifications; /// /// Настройки бота: сохранение проверяет связь делом и приводит режим доставки в соответствие. /// Главное свойство — админка никогда не видит секретов, только признак «задан». /// public class TelegramSettingsTests { private const string Token = "123:ABC"; [Fact] public async Task Update_WithPolling_RemovesWebhookAndRemembersBotName() { var fixture = new TestDb(); var api = Api("telewave_bot"); await using var db = fixture.New(); var result = await Handler(db, api).Handle(Command(token: Token), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal("telewave_bot", result.Value.BotUsername); Assert.Equal("https://t.me/telewave_bot", result.Value.BotLink); Assert.True(result.Value.HasToken); Assert.NotNull(result.Value.LastContactAt); Assert.Null(result.Value.LastError); // Пока стоит вебхук, Telegram не отдаёт обновления опросом — снимаем его явно. await api.Received(1) .DeleteWebhookAsync(Arg.Any(), Arg.Any()); await api.DidNotReceive() .SetWebhookAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Update_WithWebhook_GeneratesSecretOnce() { var fixture = new TestDb(); var api = Api("telewave_bot"); await using var db = fixture.New(); var handler = Handler(db, api); var command = Command( token: Token, transport: TelegramTransport.Webhook, webhookUrl: "https://tv.example.com/api/telegram/webhook" ); Assert.True((await handler.Handle(command, CancellationToken.None)).IsSuccess); await db.SaveChangesAsync(CancellationToken.None); var secret = db.TelegramSettings.Single().WebhookSecret; Assert.False(string.IsNullOrWhiteSpace(secret)); // Повторное сохранение секрет не меняет: он уже стоит на стороне Telegram, и смена // на каждое нажатие «Сохранить» рвала бы приём обновлений до следующего setWebhook. Assert.True((await handler.Handle(command, CancellationToken.None)).IsSuccess); Assert.Equal(secret, db.TelegramSettings.Single().WebhookSecret); await api.Received(2) .SetWebhookAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Update_WhenTelegramFails_KeepsWhatAdminTypedAndReportsWhy() { var fixture = new TestDb(); var api = Substitute.For(); api.GetMeAsync(Arg.Any(), Arg.Any()) .Returns(_ => throw new InvalidOperationException("Unauthorized")); await using (var db = fixture.New()) { var result = await Handler(db, api) .Handle(Command(token: Token, proxyHost: "127.0.0.1"), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal("Telegram.Unreachable", result.Error.Code); } // Настройки при этом сохранены: иначе админ набирал бы форму заново из-за одной опечатки. await using var check = fixture.New(); var stored = check.TelegramSettings.Single(); Assert.Equal("127.0.0.1", stored.ProxyHost); Assert.Equal("Unauthorized", stored.LastError); } [Fact] public async Task Update_WithoutToken_DoesNotEnableTheBot() { var fixture = new TestDb(); var api = Api("telewave_bot"); await using var db = fixture.New(); var result = await Handler(db, api).Handle(Command(token: null), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal("Telegram.NotConfigured", result.Error.Code); await api.DidNotReceive() .GetMeAsync(Arg.Any(), Arg.Any()); } [Theory] [InlineData( TelegramTransport.Webhook, null, TelegramProxyKind.None, null, "Telegram.WebhookUrlRequired" )] [InlineData( TelegramTransport.Polling, null, TelegramProxyKind.Socks5, null, "Telegram.ProxyHostRequired" )] public async Task Update_WithHalfFilledMode_IsRejectedBeforeAnyCall( TelegramTransport transport, string? webhookUrl, TelegramProxyKind proxyKind, string? proxyHost, string expected ) { var fixture = new TestDb(); var api = Api("telewave_bot"); await using var db = fixture.New(); var result = await Handler(db, api) .Handle( Command( token: Token, transport: transport, webhookUrl: webhookUrl, proxyKind: proxyKind, proxyHost: proxyHost ), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(expected, result.Error.Code); await api.DidNotReceive() .GetMeAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Update_WhileDisabled_SavesWithoutTouchingTelegram() { var fixture = new TestDb(); var api = Api("telewave_bot"); await using var db = fixture.New(); var result = await Handler(db, api) .Handle(Command(token: Token, isEnabled: false), CancellationToken.None); // Выключенный бот в сеть не ходит: проверять нечего, а лишний запрос через прокси // ждал бы таймаута на каждом сохранении. Assert.True(result.IsSuccess); Assert.False(result.Value.IsEnabled); Assert.Null(result.Value.BotLink); await api.DidNotReceive() .GetMeAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Get_WithoutRow_ReturnsDefaultsAndCreatesNothing() { var fixture = new TestDb(); await using var db = fixture.New(); var result = await new GetTelegramSettingsQueryHandler( new TelegramSettingsService(db) ).Handle(new GetTelegramSettingsQuery(), CancellationToken.None); Assert.True(result.IsSuccess); Assert.False(result.Value.IsEnabled); Assert.False(result.Value.HasToken); Assert.Equal(TelegramTransport.Polling, result.Value.Transport); Assert.Equal(0, result.Value.Subscribers); // Чтение не должно заводить строку: GET в базу не пишет. Assert.Empty(db.TelegramSettings); } [Fact] public async Task Get_CountsLiveSubscribersAndHidesSecrets() { var fixture = new TestDb(); var settings = TelegramSettings.CreateDefault(); settings.Update( new TelegramSettingsUpdate( true, TelegramTransport.Polling, Token, null, TelegramProxyKind.Socks5, "127.0.0.1", 1080, "user", "secret" ) ); settings.SetBotUsername("telewave_bot"); var live = TelegramSubscriber.Create(100, Guid.NewGuid(), "viewer", DateTimeOffset.UtcNow); var stopped = TelegramSubscriber.Create(101, Guid.NewGuid(), "gone", DateTimeOffset.UtcNow); stopped.Stop(); await using (var seed = fixture.New()) { seed.TelegramSettings.Add(settings); seed.TelegramSubscribers.AddRange(live, stopped); await seed.SaveChangesAsync(CancellationToken.None); } await using var db = fixture.New(); var result = await new GetTelegramSettingsQueryHandler( new TelegramSettingsService(db) ).Handle(new GetTelegramSettingsQuery(), CancellationToken.None); var dto = result.Value; // Ушедший в «стоп» в счётчик не идёт: админ смотрит, скольким реально уходит рассылка. Assert.Equal(1, dto.Subscribers); Assert.True(dto.HasToken); Assert.True(dto.HasProxyPassword); Assert.Equal("user", dto.ProxyUsername); Assert.Equal(1080, dto.ProxyPort); } [Fact] public void BotLink_NeedsBothNameAndEnabledFlag() { var settings = TelegramSettings.CreateDefault(); Assert.Null(TelegramSettingsService.BotLink(settings)); settings.SetBotUsername("telewave_bot"); // Имя есть, но бот выключен — кнопки у зрителя быть не должно. Assert.Null(TelegramSettingsService.BotLink(settings)); settings.Update( new TelegramSettingsUpdate( true, TelegramTransport.Polling, Token, null, TelegramProxyKind.None, null, null, null, null ) ); Assert.Equal("https://t.me/telewave_bot", TelegramSettingsService.BotLink(settings)); } private static ITelegramApi Api(string username) { var api = Substitute.For(); api.GetMeAsync(Arg.Any(), Arg.Any()) .Returns(new TelegramBotInfo(1, username)); return api; } private static UpdateTelegramSettingsCommandHandler Handler( AppDbContext db, ITelegramApi api ) => new(db, new TelegramSettingsService(db), api); private static UpdateTelegramSettingsCommand Command( string? token, bool isEnabled = true, TelegramTransport transport = TelegramTransport.Polling, string? webhookUrl = null, TelegramProxyKind proxyKind = TelegramProxyKind.None, string? proxyHost = null ) => new( isEnabled, transport, token, webhookUrl, proxyKind, proxyHost, proxyHost is null ? null : 1080, null, null ); }