Updated the ITelegramApi interface to include a method for deleting messages and modified SendMessageAsync to return the message ID. Enhanced the TelegramBotService to manage message visibility by replacing previous messages with new ones, ensuring a cleaner user experience. Introduced properties in TelegramSubscriber to track the latest menu and notice message IDs. Updated related tests to verify the new message management behavior and adjusted documentation to reflect these changes.
316 lines
11 KiB
C#
316 lines
11 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 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);
|
|
}
|
|
}
|