diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 6b7904b..7d1b95e 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -11,8 +11,8 @@ Tracked as a Gitea milestone: [E2E Automation](https://git.codeanddice.ru/toutsu |-------|-------|--------| | #144 | initData / Login Widget helper for mock Telegram auth | ✅ Done | | #145 | Playwright tests for Blazor dashboard with mocked Telegram auth | ✅ Done | -| #146 | Telegram user client (MTProto) | 🚧 In progress | -| #147 | Automate group creation and bot invitation | ⏳ Planned | +| #146 | Telegram user client (MTProto) | ✅ Done | +| #147 | Automate group creation and bot invitation | 🚧 In progress | | #148 | Scenario: /newsession from creation to publication | ⏳ Planned | | #149 | Join/leave, waitlist, reschedule and notification scenarios | ⏳ Planned | | #150 | Dashboard display and editing verification | ⏳ Planned | @@ -36,6 +36,7 @@ tests/e2e/ ├── GmRelay.E2E.Runner.csproj # C# console runner using WTelegramClient (MTProto) ├── Program.cs # Entry point for quick manual checks ├── TelegramUserClient.cs # Reusable MTProto user client wrapper + ├── GroupSetupScenario.cs # Create group + invite bot + verify /start ├── RunnerConfig.cs # Configuration model ├── .env.example # Required environment variables ├── .gitignore # Ignore .env and session files @@ -85,7 +86,7 @@ python tests/e2e/helpers/test_telegram_init_data.py ## Run the MTProto user client runner -The runner logs in to a real Telegram user account, creates a supergroup, and invites the test bot. +The runner logs in to a real Telegram user account, creates a supergroup, invites the test bot, sends `/start`, waits for a reply, and deletes the group. 1. Copy the example environment file and fill in real values: ```bash @@ -115,6 +116,8 @@ The runner logs in to a real Telegram user account, creates a supergroup, and in - Login as a Telegram user. - Create a supergroup (`Channels_CreateChannel` with `megagroup: true`). - Resolve a bot by username and invite it to the group. +- Send `/start` to the bot inside the group and wait for any reply. +- Delete the test supergroup after the scenario (cleanup). - Send messages/commands and read recent messages. - Wait for a bot reply. diff --git a/tests/e2e/runner/GroupSetupScenario.cs b/tests/e2e/runner/GroupSetupScenario.cs new file mode 100644 index 0000000..3f60a61 --- /dev/null +++ b/tests/e2e/runner/GroupSetupScenario.cs @@ -0,0 +1,59 @@ +namespace GmRelay.E2E.Runner; + +/// +/// Automates the first step of every Telegram E2E scenario: +/// create a supergroup, invite the GmRelay bot, and verify the bot responds to /start. +/// +public sealed class GroupSetupScenario +{ + private readonly TelegramUserClient _client; + private readonly RunnerConfig _config; + + public GroupSetupScenario(TelegramUserClient client, RunnerConfig config) + { + _client = client; + _config = config; + } + + public async Task RunAsync(CancellationToken cancellationToken = default) + { + var group = await _client.CreateGroupAsync( + $"GmRelay E2E {DateTime.UtcNow:yyyyMMdd-HHmmss}", + "Automated test group for GmRelay E2E suite.", + cancellationToken); + + Console.WriteLine($"[scenario] created group id={group.Id} title='{group.Title}'"); + + await _client.InviteBotToGroupAsync(group, _config.BotUsername, cancellationToken); + Console.WriteLine($"[scenario] invited @{_config.BotUsername}"); + + await _client.SendCommandAsync(group, "start", cancellationToken); + var reply = await _client.WaitForBotReplyAsync( + group, + containsText: null, + timeout: TimeSpan.FromSeconds(30), + cancellationToken); + + if (reply is null) + throw new InvalidOperationException("Bot did not reply to /start in the group."); + + Console.WriteLine($"[scenario] bot replied to /start (msg id={reply.id})"); + + return new ScenarioResult(group, reply.id); + } + + public async Task CleanupAsync(ScenarioResult result, CancellationToken cancellationToken = default) + { + try + { + await _client.DeleteGroupAsync(result.Group, cancellationToken); + Console.WriteLine($"[scenario] deleted group id={result.Group.Id}"); + } + catch (Exception ex) + { + Console.WriteLine($"[scenario] warning: failed to delete group id={result.Group.Id}: {ex.Message}"); + } + } +} + +public sealed record ScenarioResult(ChatGroup Group, int LastBotMessageId); diff --git a/tests/e2e/runner/Program.cs b/tests/e2e/runner/Program.cs index bc5547f..db835d6 100644 --- a/tests/e2e/runner/Program.cs +++ b/tests/e2e/runner/Program.cs @@ -12,16 +12,29 @@ var config = new RunnerConfig BotToken = Environment.GetEnvironmentVariable("TELEGRAM_BOT_TOKEN")!, }; -using var runner = new TelegramUserClient(config); -await runner.ConnectAsync(); +using var client = new TelegramUserClient(config); +await client.ConnectAsync(); -Console.WriteLine("Connected as Telegram user. Creating test group..."); -var group = await runner.CreateGroupAsync("GmRelay E2E Test Group"); -Console.WriteLine($"Created group id={group.Id} title='{group.Title}'"); +var scenario = new GroupSetupScenario(client, config); +ScenarioResult? result = null; -await runner.InviteBotToGroupAsync(group, config.BotUsername); -Console.WriteLine($"Invited @{config.BotUsername} to the group"); +try +{ + result = await scenario.RunAsync(); + Console.WriteLine("Scenario completed successfully."); +} +catch (Exception ex) +{ + Console.WriteLine($"Scenario failed: {ex.Message}"); + throw; +} +finally +{ + if (result is not null) + { + await scenario.CleanupAsync(result); + } -// Keep the process alive briefly so the session is persisted. -await Task.Delay(TimeSpan.FromSeconds(2)); -Console.WriteLine("Done."); + // Keep the session file written to disk. + await Task.Delay(TimeSpan.FromSeconds(2)); +} diff --git a/tests/e2e/runner/TelegramUserClient.cs b/tests/e2e/runner/TelegramUserClient.cs index 863daf4..6ccb8a5 100644 --- a/tests/e2e/runner/TelegramUserClient.cs +++ b/tests/e2e/runner/TelegramUserClient.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using TL; using WTelegram; @@ -7,6 +8,7 @@ public sealed class TelegramUserClient : IDisposable { private readonly Client _client; private readonly RunnerConfig _config; + private readonly ConcurrentDictionary _knownChannels = new(); public TelegramUserClient(RunnerConfig config) { @@ -22,11 +24,11 @@ public sealed class TelegramUserClient : IDisposable public async Task CreateGroupAsync(string title, string about = "", CancellationToken cancellationToken = default) { - // Creating a megagroup gives us an UpdatesBase with Chats immediately. var updates = await _client.Channels_CreateChannel(title, about, megagroup: true); var channel = updates.Chats.Values.OfType().FirstOrDefault() ?? throw new InvalidOperationException("Failed to create a supergroup."); + _knownChannels[channel.id] = channel; return new ChatGroup(channel.id, channel.title); } @@ -44,6 +46,13 @@ public sealed class TelegramUserClient : IDisposable await _client.Channels_InviteToChannel(channel, new InputUserBase[] { inputUser }); } + public async Task DeleteGroupAsync(ChatGroup group, CancellationToken cancellationToken = default) + { + var channel = await ResolveChannelAsync(group.Id, cancellationToken); + await _client.Channels_DeleteChannel(channel); + _knownChannels.TryRemove(group.Id, out _); + } + public async Task SendMessageAsync(ChatGroup group, string text, CancellationToken cancellationToken = default) { var channel = await ResolveChannelAsync(group.Id, cancellationToken); @@ -102,12 +111,19 @@ public sealed class TelegramUserClient : IDisposable private async Task ResolveChannelAsync(long channelId, CancellationToken cancellationToken = default) { + if (_knownChannels.TryGetValue(channelId, out var cached)) + return cached; + var dialogs = await _client.Messages_GetAllDialogs(); var channel = dialogs.chats.Values .OfType() .FirstOrDefault(c => c.id == channelId); - return channel ?? throw new InvalidOperationException($"Could not resolve channel {channelId}."); + if (channel is null) + throw new InvalidOperationException($"Could not resolve channel {channelId}."); + + _knownChannels[channelId] = channel; + return channel; } }