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;
}
}