Files
TeleWave/backend/tests/TeleWave.Application.Tests/Notifications/TelegramBotTests.cs
T
Leonid Pershin 8a439f1351
ci / build-backend (push) Successful in 2m38s
ci / build-frontend (push) Successful in 58s
ci / tests (push) Successful in 4m27s
ci / sonar (push) Successful in 9m26s
Enhance TelegramBotService with schedule handling and subscription toggling
Updated the TelegramBotService to support new actions for managing subscriptions and retrieving channel schedules. Introduced constants for action types and improved the callback data structure for better clarity. Added a method to send channel schedules, consolidating upcoming program information into a user-friendly format. Updated related tests to ensure proper functionality of the new features and adjusted documentation to reflect changes in button actions.
2026-07-31 08:16:37 +03:00

255 lines
9.0 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;
await api.SendMessageAsync(
Arg.Any<TelegramSettings>(),
Arg.Any<long>(),
Arg.Do<string>(text => sent = text),
Arg.Any<IReadOnlyList<IReadOnlyList<TelegramButton>>?>(),
Arg.Any<CancellationToken>()
);
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);
Assert.Contains("Футурама", sent, StringComparison.Ordinal);
// Две подряд идущие серии Симпсонов дают одну строку, а не две.
Assert.Equal(1, sent!.Split("Симпсоны").Length - 1);
}
[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);
}
}