using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Notifications;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Notifications;
using Xunit;
namespace TeleWave.Application.Tests.Notifications;
///
/// Разговор с ботом. Главное свойство — подписаться можно только по одноразовому коду: рассылка
/// уходит в личные чаты, и «кто нашёл бота, тот и подписался» здесь означало бы чужие оповещения.
///
public class TelegramBotTests
{
private static readonly DateTimeOffset Now = new(2026, 8, 1, 12, 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 TelegramUpdate Message(long chatId, string text) =>
new(1, chatId, text, "viewer", null, null, null);
private static TelegramUpdate Callback(long chatId, string data) =>
new(2, chatId, null, "viewer", "cb-1", data, 55);
[Fact]
public async Task Start_WithoutCode_DoesNotSubscribeAnyone()
{
var fixture = new TestDb();
var api = Substitute.For();
await using var db = fixture.New();
await new TelegramBotService(db, api).HandleAsync(
Settings(),
Message(100, "/start"),
Now,
CancellationToken.None
);
Assert.Empty(db.TelegramSubscribers);
// Незнакомцу объясняют, где взять код, а не молчат в ответ.
await api.Received(1)
.SendMessageAsync(
Arg.Any(),
100,
Arg.Is(s => s.Contains("код")),
null,
Arg.Any()
);
}
[Fact]
public async Task Start_WithCode_LinksChatToUserAndBurnsCode()
{
var fixture = new TestDb();
var userId = Guid.NewGuid();
var code = TelegramLinkCode.Issue("abc123", userId, Now);
var channel = Channel.Create("Мультреалити", "mult", Now);
channel.UpdateSettings(channel.Name, isEnabled: true, null);
await using (var seed = fixture.New())
{
seed.TelegramLinkCodes.Add(code);
seed.Channels.Add(channel);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
await new TelegramBotService(db, Substitute.For()).HandleAsync(
Settings(),
Message(100, "/start abc123"),
Now,
CancellationToken.None
);
var subscriber = Assert.Single(db.TelegramSubscribers);
Assert.Equal(userId, subscriber.UserId);
Assert.Equal(100, subscriber.ChatId);
// Код одноразовый: ссылка оседает в истории браузера и в чате.
Assert.NotNull(db.TelegramLinkCodes.Single().UsedAt);
}
[Fact]
public async Task Start_WithUsedCode_IsRefused()
{
var fixture = new TestDb();
var code = TelegramLinkCode.Issue("abc123", Guid.NewGuid(), Now);
code.MarkUsed(Now);
await using (var seed = fixture.New())
{
seed.TelegramLinkCodes.Add(code);
await seed.SaveChangesAsync(CancellationToken.None);
}
await using var db = fixture.New();
await new TelegramBotService(db, Substitute.For()).HandleAsync(
Settings(),
Message(100, "/start abc123"),
Now,
CancellationToken.None
);
Assert.Empty(db.TelegramSubscribers);
}
[Fact]
public async Task Callback_TogglesSubscriptionBothWays()
{
var fixture = new TestDb();
var channel = Channel.Create("Мультреалити", "mult", Now);
channel.UpdateSettings(channel.Name, isEnabled: true, null);
var subscriber = TelegramSubscriber.Create(100, Guid.NewGuid(), "viewer", Now);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.TelegramSubscribers.Add(subscriber);
await seed.SaveChangesAsync(CancellationToken.None);
}
var data = $"{channel.Id}:{TelegramNotificationKind.ShowStart}";
await using (var db = fixture.New())
{
await new TelegramBotService(db, Substitute.For()).HandleAsync(
Settings(),
Callback(100, data),
Now,
CancellationToken.None
);
}
await using (var check = fixture.New())
{
Assert.Single(check.TelegramSubscriptions);
}
await using (var db = fixture.New())
{
await new TelegramBotService(db, Substitute.For()).HandleAsync(
Settings(),
Callback(100, data),
Now,
CancellationToken.None
);
}
await using var verify = fixture.New();
// Повторное нажатие выключает: кнопка — тумблер, а не «добавить ещё одну подписку».
Assert.Empty(verify.TelegramSubscriptions);
}
[Fact]
public async Task Callback_FromUnknownChat_IsRefused()
{
var fixture = new TestDb();
var api = Substitute.For();
await using var db = fixture.New();
await new TelegramBotService(db, api).HandleAsync(
Settings(),
Callback(100, $"{Guid.NewGuid()}:{TelegramNotificationKind.ShowStart}"),
Now,
CancellationToken.None
);
Assert.Empty(db.TelegramSubscriptions);
}
}