Add Telegram bot integration and related features
Implemented Telegram bot functionality, including settings management, subscriber tracking, and link generation for user interaction. Updated the backend to support new Telegram-related services and database entities. Enhanced the frontend to display Telegram options and allow users to open a chat with the bot. Localization strings were added for both English and Russian to support the new features. This integration aims to improve user engagement through Telegram notifications and interactions.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
using NSubstitute;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Notifications;
|
||||
using TeleWave.Application.Tests.Support;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
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);
|
||||
|
||||
[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 = $"{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_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, $"{Guid.NewGuid()}:{TelegramNotificationKind.ShowStart}"),
|
||||
Now,
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
Assert.Empty(db.TelegramSubscriptions);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user