Updated the Channel and related DTOs to incorporate an optional IconImageId property, allowing for the specification of a channel icon for external IPTV players. Modified the UpdateChannelSettings command and its handler to accommodate this new property. Adjusted frontend components to manage the icon image, including selection and display functionality. Updated localization files to support new UI elements related to the IPTV icon. This enhancement improves the user experience by providing a visual representation of channels in IPTV applications.
325 lines
12 KiB
C#
325 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,
|
|
// Предикат NSubstitute принимает аргумент как допускающий null — само сообщение
|
|
// объявлено обязательным, потому и разыменовываем без проверки.
|
|
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, 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, 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, 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, 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);
|
|
}
|
|
}
|