feat(e2e): #147 group creation and bot invitation scenario
- Add GroupSetupScenario: create supergroup, invite GmRelay bot, send /start, wait for reply, then delete the group - Extend TelegramUserClient with DeleteGroupAsync and channel cache - Update Program.cs to run the scenario with cleanup in finally - Update README status table and runner documentation Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
namespace GmRelay.E2E.Runner;
|
||||
|
||||
/// <summary>
|
||||
/// Automates the first step of every Telegram E2E scenario:
|
||||
/// create a supergroup, invite the GmRelay bot, and verify the bot responds to /start.
|
||||
/// </summary>
|
||||
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<ScenarioResult> 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);
|
||||
+23
-10
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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<long, Channel> _knownChannels = new();
|
||||
|
||||
public TelegramUserClient(RunnerConfig config)
|
||||
{
|
||||
@@ -22,11 +24,11 @@ public sealed class TelegramUserClient : IDisposable
|
||||
|
||||
public async Task<ChatGroup> 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<Channel>().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<Channel> 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<Channel>()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user