Files
TeleWave/backend/tests/TeleWave.Application.Tests/Notifications/TelegramBotTests.cs
T
Leonid Pershin 08f56e14c2
ci / build-backend (push) Successful in 4m34s
ci / build-frontend (push) Successful in 1m7s
ci / tests (push) Successful in 4m22s
ci / sonar (push) Successful in 8m49s
Refactor TelegramBotService to streamline message sending process
Updated the TelegramBotService to replace the SendMessageAsync method with a new NoticeAsync method, improving clarity and functionality. Adjusted related tests to ensure proper handling of message IDs and maintain consistency in message management. This change enhances the overall user experience by simplifying the message notification process.
2026-07-31 08:28:42 +03:00

323 lines
12 KiB
C#

using NSubstitute;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Notifications;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Notifications;
using Xunit;
namespace TeleWave.Application.Tests.Notifications;
/// <summary>
/// Разговор с ботом. Главное свойство — подписаться можно только по одноразовому коду: рассылка
/// уходит в личные чаты, и «кто нашёл бота, тот и подписался» здесь означало бы чужие оповещения.
/// </summary>
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);
private static ScheduleEntry Program(
Guid channelId,
Guid showId,
DateTimeOffset start,
int minutes
) =>
ScheduleEntry.FromSlot(
channelId,
Guid.NewGuid(),
ScheduleEntryKind.Program,
start,
start.AddMinutes(minutes),
new ScheduleEntryOrigin(showId, 0, null, null, null)
);
[Fact]
public async Task Start_WithoutCode_DoesNotSubscribeAnyone()
{
var fixture = new TestDb();
var api = Substitute.For<ITelegramApi>();
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<TelegramSettings>(),
100,
Arg.Is<string>(s => s.Contains("код")),
null,
Arg.Any<CancellationToken>()
);
}
[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<ITelegramApi>()).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<ITelegramApi>()).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 = $"sub:{channel.Id}:{TelegramNotificationKind.ShowStart}";
await using (var db = fixture.New())
{
await new TelegramBotService(db, Substitute.For<ITelegramApi>()).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<ITelegramApi>()).HandleAsync(
Settings(),
Callback(100, data),
Now,
CancellationToken.None
);
}
await using var verify = fixture.New();
// Повторное нажатие выключает: кнопка — тумблер, а не «добавить ещё одну подписку».
Assert.Empty(verify.TelegramSubscriptions);
}
[Fact]
public async Task Callback_Schedule_SendsUpcomingProgrammes()
{
// Кнопка-заголовок канала показывает программу. Подряд идущие серии одного шоу — это
// один блок: зритель спрашивает «что дальше», а не «в каком порядке лежат файлы».
var fixture = new TestDb();
var channel = Channel.Create("Мультреалити", "mult", Now);
channel.UpdateSettings(channel.Name, isEnabled: true, null);
var first = Show.Create("Симпсоны", ShowKind.Series);
var second = Show.Create("Футурама", ShowKind.Series);
var subscriber = TelegramSubscriber.Create(100, Guid.NewGuid(), "viewer", Now);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.Shows.AddRange(first, second);
seed.TelegramSubscribers.Add(subscriber);
seed.ScheduleEntries.AddRange(
Program(channel.Id, first.Id, Now.AddMinutes(-5), 25),
Program(channel.Id, first.Id, Now.AddMinutes(20), 25),
Program(channel.Id, second.Id, Now.AddMinutes(45), 25)
);
await seed.SaveChangesAsync(CancellationToken.None);
}
var api = Substitute.For<ITelegramApi>();
string? sent = null;
api.SendMessageAsync(
Arg.Any<TelegramSettings>(),
Arg.Any<long>(),
Arg.Do<string>(text => sent = text),
Arg.Any<IReadOnlyList<IReadOnlyList<TelegramButton>>?>(),
Arg.Any<CancellationToken>()
)
.Returns(4242L);
await using var db = fixture.New();
await new TelegramBotService(db, api).HandleAsync(
Settings(),
Callback(100, $"epg:{channel.Id}"),
Now,
CancellationToken.None
);
Assert.NotNull(sent);
Assert.Contains("Симпсоны", sent, StringComparison.Ordinal);
// Программа — одноразовое сообщение: её идентификатор запомнен, чтобы снять при следующем
// показе. Без этого чат копит одинаковые программы на разное время.
await using (var check = fixture.New())
{
Assert.Equal(4242L, check.TelegramSubscribers.Single().NoticeMessageId);
}
Assert.Contains("Футурама", sent, StringComparison.Ordinal);
// Две подряд идущие серии Симпсонов дают одну строку, а не две.
Assert.Equal(1, sent!.Split("Симпсоны").Length - 1);
}
[Fact]
public async Task Menu_ReplacesThePreviousOne()
{
// Два меню в чате — это две панели с разными галочками, и одна из них врёт. Старое снимаем.
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 api = Substitute.For<ITelegramApi>();
api.SendMessageAsync(
Arg.Any<TelegramSettings>(),
Arg.Any<long>(),
Arg.Any<string>(),
Arg.Any<IReadOnlyList<IReadOnlyList<TelegramButton>>?>(),
Arg.Any<CancellationToken>()
)
.Returns(777L);
await using (var db = fixture.New())
{
await new TelegramBotService(db, api).HandleAsync(
Settings(),
Message(100, "/start"),
Now,
CancellationToken.None
);
}
await using (var check = fixture.New())
{
Assert.Equal(777L, check.TelegramSubscribers.Single().MenuMessageId);
}
await using (var db = fixture.New())
{
await new TelegramBotService(db, api).HandleAsync(
Settings(),
Message(100, "/start"),
Now,
CancellationToken.None
);
}
// Второй заход снял предыдущее меню, а не оставил его висеть.
await api.Received(1)
.DeleteMessageAsync(
Arg.Any<TelegramSettings>(),
100,
777L,
Arg.Any<CancellationToken>()
);
}
[Fact]
public async Task Callback_FromUnknownChat_IsRefused()
{
var fixture = new TestDb();
var api = Substitute.For<ITelegramApi>();
await using var db = fixture.New();
await new TelegramBotService(db, api).HandleAsync(
Settings(),
Callback(100, $"sub:{Guid.NewGuid()}:{TelegramNotificationKind.ShowStart}"),
Now,
CancellationToken.None
);
Assert.Empty(db.TelegramSubscriptions);
}
}