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