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