Files
Playa/tests/Volcanic.AgenticCore.Telegram.Tests/Support/Updates.cs

247 lines
11 KiB
C#

using Microsoft.Extensions.Hosting;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
namespace Volcanic.AgenticCore.Telegram.Tests.Support;
public static class Updates
{
public const long AllowedUser = 1001;
public const long Stranger = 666;
private static int _nextUpdateId;
private static int _nextMessageId = 1;
public static Update Text(string text, long userId = AllowedUser, long? chatId = null, ChatType chatType = ChatType.Private) => new()
{
Id = Interlocked.Increment(ref _nextUpdateId),
Message = new Message
{
Id = Interlocked.Increment(ref _nextMessageId),
From = new User { Id = userId, FirstName = "Test" },
Chat = new Chat { Id = chatId ?? userId, Type = chatType },
Text = text,
Date = DateTime.UtcNow,
},
};
public static Update Photo(string? caption = null, long userId = AllowedUser) => new()
{
Id = Interlocked.Increment(ref _nextUpdateId),
Message = new Message
{
Id = Interlocked.Increment(ref _nextMessageId),
From = new User { Id = userId, FirstName = "Test" },
Chat = new Chat { Id = userId, Type = ChatType.Private },
Photo = [new PhotoSize { FileId = "photo", FileUniqueId = "photo-u", Width = 10, Height = 10 }],
Caption = caption,
Date = DateTime.UtcNow,
},
};
public const long GroupId = -1001234567890;
/// <summary>A text message in a group (a supergroup unless <paramref name="type"/> says otherwise).</summary>
public static Update GroupText(string text, long userId = Stranger, long groupId = GroupId, string title = "Dev Team", ChatType type = ChatType.Supergroup,
string firstName = "Alice") => new()
{
Id = Interlocked.Increment(ref _nextUpdateId),
Message = new Message
{
Id = Interlocked.Increment(ref _nextMessageId),
From = new User { Id = userId, FirstName = firstName },
Chat = new Chat { Id = groupId, Type = type, Title = title },
Text = text,
Date = DateTime.UtcNow,
},
};
/// <summary>The bot's own membership in a group changed (added by <paramref name="byUser"/>, or removed).</summary>
public static Update BotMembership(ChatMemberStatus status, long byUser = AllowedUser, long groupId = GroupId, string title = "Dev Team") => new()
{
Id = Interlocked.Increment(ref _nextUpdateId),
MyChatMember = new ChatMemberUpdated
{
Chat = new Chat { Id = groupId, Type = ChatType.Supergroup, Title = title },
From = new User { Id = byUser, FirstName = "Adder" },
Date = DateTime.UtcNow,
OldChatMember = new ChatMemberLeft { User = new User { Id = BotUserId, IsBot = true, FirstName = "Fake" } },
NewChatMember = status switch
{
ChatMemberStatus.Administrator => new ChatMemberAdministrator { User = new User { Id = BotUserId, IsBot = true, FirstName = "Fake" } },
ChatMemberStatus.Kicked => new ChatMemberBanned { User = new User { Id = BotUserId, IsBot = true, FirstName = "Fake" } },
ChatMemberStatus.Left => new ChatMemberLeft { User = new User { Id = BotUserId, IsBot = true, FirstName = "Fake" } },
_ => new ChatMemberMember { User = new User { Id = BotUserId, IsBot = true, FirstName = "Fake" } },
},
},
};
public static Update WithId(int id) => new() { Id = id, Message = Text("#" + id).Message };
/// <summary>The fake bot's user id (<see cref="FakeBotClient.BotId"/>).</summary>
public const long BotUserId = 42;
private static Message NewMessage(int? id, long userId = AllowedUser) => new()
{
Id = id ?? Interlocked.Increment(ref _nextMessageId),
From = new User { Id = userId, FirstName = "Test" },
Chat = new Chat { Id = userId, Type = ChatType.Private },
Date = DateTime.UtcNow,
};
private static Update Wrap(Message message) => new() { Id = Interlocked.Increment(ref _nextUpdateId), Message = message };
/// <summary>An earlier message sent by the bot, for <see cref="Reply"/> and <see cref="Quote"/>.</summary>
public static Message BotMessage(string text, int? id = null)
{
var m = NewMessage(id);
m.From = new User { Id = BotUserId, IsBot = true, FirstName = "Fake", Username = "fake_bot" };
m.Text = text;
return m;
}
/// <summary>A text message replying to <paramref name="replyTo"/>.</summary>
public static Update Reply(string text, Message replyTo, int? id = null)
{
var m = NewMessage(id);
m.Text = text;
m.ReplyToMessage = replyTo;
return Wrap(m);
}
/// <summary>A text message replying to <paramref name="replyTo"/> with a selected <paramref name="quote"/>.</summary>
public static Update Quote(string text, Message replyTo, string quote, int? id = null)
{
var update = Reply(text, replyTo, id);
update.Message!.Quote = new TextQuote { Text = quote, Position = 0 };
return update;
}
/// <summary>A text message replying to a message from another chat (Telegram's "reply in another chat").</summary>
public static Update ExternalReply(string text, MessageOrigin origin, string? quote = null, int? id = null)
{
var m = NewMessage(id);
m.Text = text;
m.ExternalReply = new ExternalReplyInfo { Origin = origin };
if (quote is not null) m.Quote = new TextQuote { Text = quote, Position = 0 };
return Wrap(m);
}
/// <summary>A forwarded text message.</summary>
public static Update Forward(string text, MessageOrigin origin, int? id = null)
{
var m = NewMessage(id);
m.Text = text;
m.ForwardOrigin = origin;
return Wrap(m);
}
/// <summary>One photo of an album (media group <paramref name="groupId"/>), with its own file id.</summary>
public static Update AlbumPhoto(string groupId, string? caption = null, MessageOrigin? forwardedFrom = null, int? id = null)
{
var m = NewMessage(id);
m.MediaGroupId = groupId;
m.Photo = [new PhotoSize { FileId = $"photo-{m.Id}", FileUniqueId = $"photo-{m.Id}-u", Width = 10, Height = 10 }];
m.Caption = caption;
m.ForwardOrigin = forwardedFrom;
return Wrap(m);
}
public static Update StickerMessage(string emoji = "👍")
{
var m = NewMessage(null);
m.Sticker = new Sticker { FileId = "sticker", FileUniqueId = "sticker-u", Type = StickerType.Regular, Width = 1, Height = 1, Emoji = emoji };
return Wrap(m);
}
// Media helpers end in "Message": a member named Document/Video/Voice/Audio would hide the Telegram type in this class.
/// <summary>A document (file id <paramref name="fileId"/>, scripted in <see cref="FakeBotClient.Files"/>).</summary>
public static Update DocumentMessage(string? fileName, string? mimeType, long? size, string? caption = null, string fileId = "doc")
{
var m = NewMessage(null);
m.Document = new Document { FileId = fileId, FileUniqueId = fileId + "-u", FileName = fileName, MimeType = mimeType, FileSize = size };
m.Caption = caption;
return Wrap(m);
}
/// <summary>A video (MIME <c>video/mp4</c>).</summary>
public static Update VideoMessage(string? fileName, long? size, string? caption = null, string fileId = "video")
{
var m = NewMessage(null);
m.Video = new Video { FileId = fileId, FileUniqueId = fileId + "-u", Width = 1, Height = 1, Duration = 1, FileName = fileName, MimeType = "video/mp4", FileSize = size };
m.Caption = caption;
return Wrap(m);
}
/// <summary>A voice note (MIME <c>audio/ogg</c>).</summary>
public static Update VoiceMessage(long? size = 3, string? caption = null, string fileId = "voice")
{
var m = NewMessage(null);
m.Voice = new Voice { FileId = fileId, FileUniqueId = fileId + "-u", Duration = 2, MimeType = "audio/ogg", FileSize = size };
m.Caption = caption;
return Wrap(m);
}
/// <summary>An audio file.</summary>
public static Update AudioMessage(string? fileName, string? mimeType, long? size, string? caption = null, string fileId = "audio")
{
var m = NewMessage(null);
m.Audio = new Audio { FileId = fileId, FileUniqueId = fileId + "-u", Duration = 60, FileName = fileName, MimeType = mimeType, FileSize = size };
m.Caption = caption;
return Wrap(m);
}
/// <summary>A channel post origin, optionally signed by its author.</summary>
public static MessageOriginChannel Channel(string title, string? signature = null) => new()
{
Chat = new Chat { Id = -100, Type = ChatType.Channel, Title = title },
MessageId = 1,
AuthorSignature = signature,
Date = DateTime.UtcNow,
};
/// <summary>A visible user origin. Named <c>UserOrigin</c> so it doesn't hide the <see cref="User"/> type in this class.</summary>
public static MessageOriginUser UserOrigin(string username, string first, string? last = null) => new()
{
SenderUser = new User { Id = 7, FirstName = first, LastName = last, Username = username },
Date = DateTime.UtcNow,
};
public static Update Callback(string data, long chatId, long userId = AllowedUser) => new()
{
Id = Interlocked.Increment(ref _nextUpdateId),
CallbackQuery = new CallbackQuery
{
Id = Guid.NewGuid().ToString("N"),
From = new User { Id = userId, FirstName = "Test" },
Message = new Message { Id = 1, Chat = new Chat { Id = chatId, Type = ChatType.Private }, Date = DateTime.UtcNow },
Data = data,
ChatInstance = "ci",
},
};
}
public static class Wait
{
public static async Task UntilAsync(Func<bool> condition, string what, int timeoutMs = 5000)
{
var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
while (!condition())
{
if (DateTime.UtcNow > deadline) throw new TimeoutException($"Timed out waiting for {what}");
await Task.Delay(10);
}
}
}
public sealed class FakeLifetime : IHostApplicationLifetime
{
private readonly CancellationTokenSource _stopping = new();
public CancellationToken ApplicationStarted => CancellationToken.None;
public CancellationToken ApplicationStopping => _stopping.Token;
public CancellationToken ApplicationStopped => CancellationToken.None;
public bool StopRequested => _stopping.IsCancellationRequested;
public void StopApplication() => _stopping.Cancel();
}