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.
255 lines
9.0 KiB
C#
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);
|
|
}
|
|
}
|