ec78c0cdf5
Introduce Luna.Identity project with user identity service, pairing code generation, and channel linking. Channels (Telegram, CLI) now verify link status before routing messages and prompt users to pair via the web interface. WebChannel handles /link <channel-type> <code> commands to complete the pairing flow. Fix inverted expiration check in IsPairingCodeExpired that caused codes to be treated as expired immediately after creation.
346 lines
13 KiB
C#
346 lines
13 KiB
C#
// using Luna.Channels.Abstractions;
|
|
// using Luna.Channels.Telegram;
|
|
// using Luna.Configuration;
|
|
// using Luna.Shared;
|
|
// using Microsoft.Extensions.Logging;
|
|
// using Moq;
|
|
// using Telegram.Bot;
|
|
// using Telegram.Bot.Requests;
|
|
// using Telegram.Bot.Types;
|
|
//
|
|
// namespace Luna.Channels.Tests;
|
|
//
|
|
// [TestFixture]
|
|
// [Category("Unit")]
|
|
// public class TelegramChannelTests
|
|
// {
|
|
// private Mock<ITelegramBotClient> mockBotClient = null!;
|
|
// private Mock<ILogger<TelegramChannel>> mockLogger = null!;
|
|
// private TelegramOptions options = null!;
|
|
// private TelegramChannel channel = null!;
|
|
//
|
|
// [SetUp]
|
|
// public void SetUp()
|
|
// {
|
|
// mockBotClient = new Mock<ITelegramBotClient>();
|
|
// mockLogger = new Mock<ILogger<TelegramChannel>>();
|
|
// options = new TelegramOptions { BotToken = "test-bot-token:ABCDEF" };
|
|
// channel = new TelegramChannel("100", mockBotClient.Object, mockLogger.Object, options);
|
|
// }
|
|
//
|
|
// [TearDown]
|
|
// public void TearDown()
|
|
// {
|
|
// channel.Dispose();
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void ChannelType_Returns_Telegram()
|
|
// {
|
|
// Assert.That(channel.ChannelType, Is.EqualTo("telegram"));
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void ChannelId_IsSet_From_Constructor()
|
|
// {
|
|
// Assert.That(channel.ChannelId, Is.EqualTo("100"));
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void ChannelId_IsUnique_Per_Instance()
|
|
// {
|
|
// var channel1 = new TelegramChannel("111", mockBotClient.Object, mockLogger.Object, options);
|
|
// var channel2 = new TelegramChannel("222", mockBotClient.Object, mockLogger.Object, options);
|
|
//
|
|
// Assert.That(channel1.ChannelId, Is.Not.EqualTo(channel2.ChannelId));
|
|
//
|
|
// channel1.Dispose();
|
|
// channel2.Dispose();
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void DisplayName_Contains_Telegram()
|
|
// {
|
|
// Assert.That(channel.DisplayName, Does.Contain("Telegram"));
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void IsConnected_Is_True_Initially()
|
|
// {
|
|
// Assert.That(channel.IsConnected, Is.True);
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void IsConnected_Is_False_After_Dispose()
|
|
// {
|
|
// channel.Dispose();
|
|
//
|
|
// Assert.That(channel.IsConnected, Is.False);
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public async Task SendMessageAsync_Calls_BotClient_SendMessage()
|
|
// {
|
|
// mockBotClient
|
|
// .Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
|
|
// .ReturnsAsync(new Message { Id = 1, Chat = new Chat { Id = 100 } });
|
|
//
|
|
// var message = new ChannelMessage(
|
|
// Guid.NewGuid().ToString(), "100", "42", "Alice", "Hello!", DateTimeOffset.UtcNow);
|
|
//
|
|
// await channel.SendMessageAsync(message);
|
|
//
|
|
// mockBotClient.Verify(
|
|
// c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()),
|
|
// Times.Once);
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public async Task SendMessageAsync_Handles_Network_Error_Without_Crash()
|
|
// {
|
|
// mockBotClient
|
|
// .Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
|
|
// .ThrowsAsync(new HttpRequestException("Network error"));
|
|
//
|
|
// var message = new ChannelMessage(
|
|
// Guid.NewGuid().ToString(), "100", "42", "Alice", "Hello!", DateTimeOffset.UtcNow);
|
|
//
|
|
// Assert.DoesNotThrowAsync(async () => await channel.SendMessageAsync(message));
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void RaiseMessageReceived_Fires_Event_With_Correct_Mapping()
|
|
// {
|
|
// ChannelMessageReceivedEventArgs? captured = null;
|
|
// channel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
|
|
//
|
|
// var message = new ChannelMessage(
|
|
// Guid.NewGuid().ToString(), "999", "77", "Bob", "Hello Luna", DateTimeOffset.UtcNow);
|
|
//
|
|
// channel.RaiseMessageReceived(message);
|
|
//
|
|
// Assert.That(captured, Is.Not.Null);
|
|
// Assert.That(captured!.Message.ConversationId, Is.EqualTo("999"));
|
|
// Assert.That(captured.Message.SenderId, Is.EqualTo("77"));
|
|
// Assert.That(captured.Message.SenderName, Is.EqualTo("Bob"));
|
|
// Assert.That(captured.Message.Content, Is.EqualTo("Hello Luna"));
|
|
// Assert.That(captured.Channel, Is.SameAs(channel));
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void RaiseMessageReceived_Maps_SenderName_And_Timestamp()
|
|
// {
|
|
// var sentAt = new DateTimeOffset(2025, 6, 1, 12, 0, 0, TimeSpan.Zero);
|
|
//
|
|
// ChannelMessageReceivedEventArgs? captured = null;
|
|
// channel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
|
|
//
|
|
// var message = new ChannelMessage(
|
|
// Guid.NewGuid().ToString(), "123", "55", "Carol", "Hi", sentAt);
|
|
//
|
|
// channel.RaiseMessageReceived(message);
|
|
//
|
|
// Assert.That(captured, Is.Not.Null);
|
|
// Assert.That(captured!.Message.SenderName, Is.EqualTo("Carol"));
|
|
// Assert.That(captured.Message.Timestamp, Is.EqualTo(sentAt));
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public async Task SendStreamingMessageAsync_Sends_Initial_Then_Edits_In_Place()
|
|
// {
|
|
// mockBotClient
|
|
// .Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
|
|
// .ReturnsAsync(new Message { Id = 10, Chat = new Chat { Id = 100 } });
|
|
//
|
|
// mockBotClient
|
|
// .Setup(c => c.SendRequest(It.IsAny<EditMessageTextRequest>(), It.IsAny<CancellationToken>()))
|
|
// .ReturnsAsync(new Message { Id = 10, Chat = new Chat { Id = 100 } });
|
|
//
|
|
// var updates = new List<ChatStreamUpdate>
|
|
// {
|
|
// new(ChatStreamUpdateType.Text, "Hello ", null, false),
|
|
// new(ChatStreamUpdateType.Text, "World", null, false),
|
|
// new(ChatStreamUpdateType.Complete, null, null, true),
|
|
// };
|
|
//
|
|
// await channel.SendStreamingMessageAsync(updates.ToAsyncEnumerable());
|
|
//
|
|
// // First chunk sends a message, second chunk edits it
|
|
// mockBotClient.Verify(
|
|
// c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()),
|
|
// Times.Once);
|
|
//
|
|
// mockBotClient.Verify(
|
|
// c => c.SendRequest(It.IsAny<EditMessageTextRequest>(), It.IsAny<CancellationToken>()),
|
|
// Times.AtLeastOnce);
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public async Task SendStreamingMessageAsync_Handles_4096_Char_Limit_By_Chunking()
|
|
// {
|
|
// var longContent = new string('A', 4097);
|
|
// var updates = new List<ChatStreamUpdate>
|
|
// {
|
|
// new(ChatStreamUpdateType.Text, longContent, null, false),
|
|
// new(ChatStreamUpdateType.Complete, null, null, true),
|
|
// };
|
|
//
|
|
// mockBotClient
|
|
// .Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
|
|
// .ReturnsAsync(new Message { Id = 20, Chat = new Chat { Id = 100 } });
|
|
//
|
|
// mockBotClient
|
|
// .Setup(c => c.SendRequest(It.IsAny<EditMessageTextRequest>(), It.IsAny<CancellationToken>()))
|
|
// .ReturnsAsync(new Message { Id = 20, Chat = new Chat { Id = 100 } });
|
|
//
|
|
// await channel.SendStreamingMessageAsync(updates.ToAsyncEnumerable());
|
|
//
|
|
// // Content exceeds 4096 — needs at least 2 SendMessage calls (initial + overflow)
|
|
// mockBotClient.Verify(
|
|
// c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()),
|
|
// Times.AtLeast(2));
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public async Task SendStreamingMessageAsync_Handles_IsComplete_True_With_Final_Edit()
|
|
// {
|
|
// mockBotClient
|
|
// .Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
|
|
// .ReturnsAsync(new Message { Id = 30, Chat = new Chat { Id = 100 } });
|
|
//
|
|
// mockBotClient
|
|
// .Setup(c => c.SendRequest(It.IsAny<EditMessageTextRequest>(), It.IsAny<CancellationToken>()))
|
|
// .ReturnsAsync(new Message { Id = 30, Chat = new Chat { Id = 100 } });
|
|
//
|
|
// var updates = new List<ChatStreamUpdate>
|
|
// {
|
|
// new(ChatStreamUpdateType.Text, "Final answer", null, false),
|
|
// new(ChatStreamUpdateType.Complete, null, null, true),
|
|
// };
|
|
//
|
|
// await channel.SendStreamingMessageAsync(updates.ToAsyncEnumerable());
|
|
//
|
|
// // Initial message sent
|
|
// mockBotClient.Verify(
|
|
// c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()),
|
|
// Times.Once);
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void AllowedUserIds_Non_Empty_Blocks_Unauthorized_User()
|
|
// {
|
|
// var restrictedOptions = new TelegramOptions
|
|
// {
|
|
// BotToken = "test-token",
|
|
// AllowedUserIds = ["111", "222"]
|
|
// };
|
|
// var restrictedChannel = new TelegramChannel(
|
|
// "500", mockBotClient.Object, mockLogger.Object, restrictedOptions);
|
|
//
|
|
// ChannelMessageReceivedEventArgs? captured = null;
|
|
// restrictedChannel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
|
|
//
|
|
// var message = new ChannelMessage(
|
|
// Guid.NewGuid().ToString(), "500", "999", "Hacker", "attack", DateTimeOffset.UtcNow);
|
|
//
|
|
// restrictedChannel.RaiseMessageReceived(message);
|
|
//
|
|
// Assert.That(captured, Is.Null, "Message from unauthorized user should be silently ignored");
|
|
//
|
|
// restrictedChannel.Dispose();
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void AllowedUserIds_Non_Empty_Allows_Authorized_User()
|
|
// {
|
|
// var restrictedOptions = new TelegramOptions
|
|
// {
|
|
// BotToken = "test-token",
|
|
// AllowedUserIds = ["111", "222"]
|
|
// };
|
|
// var restrictedChannel = new TelegramChannel(
|
|
// "500", mockBotClient.Object, mockLogger.Object, restrictedOptions);
|
|
//
|
|
// ChannelMessageReceivedEventArgs? captured = null;
|
|
// restrictedChannel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
|
|
//
|
|
// var message = new ChannelMessage(
|
|
// Guid.NewGuid().ToString(), "500", "111", "TrustedUser", "hello", DateTimeOffset.UtcNow);
|
|
//
|
|
// restrictedChannel.RaiseMessageReceived(message);
|
|
//
|
|
// Assert.That(captured, Is.Not.Null, "Message from authorized user should be accepted");
|
|
//
|
|
// restrictedChannel.Dispose();
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void AllowedUserIds_Empty_Accepts_All_Users()
|
|
// {
|
|
// var openOptions = new TelegramOptions
|
|
// {
|
|
// BotToken = "test-token",
|
|
// AllowedUserIds = []
|
|
// };
|
|
// var openChannel = new TelegramChannel(
|
|
// "600", mockBotClient.Object, mockLogger.Object, openOptions);
|
|
//
|
|
// ChannelMessageReceivedEventArgs? captured = null;
|
|
// openChannel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
|
|
//
|
|
// var message = new ChannelMessage(
|
|
// Guid.NewGuid().ToString(), "600", "12345", "Anyone", "hello", DateTimeOffset.UtcNow);
|
|
//
|
|
// openChannel.RaiseMessageReceived(message);
|
|
//
|
|
// Assert.That(captured, Is.Not.Null, "Message should be accepted when AllowedUserIds is empty");
|
|
//
|
|
// openChannel.Dispose();
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void Malformed_Message_With_Null_Text_Is_Skipped()
|
|
// {
|
|
// ChannelMessageReceivedEventArgs? captured = null;
|
|
// channel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
|
|
//
|
|
// var message = new ChannelMessage(
|
|
// Guid.NewGuid().ToString(), "700", "88", "Ghost", null!, DateTimeOffset.UtcNow);
|
|
//
|
|
// channel.RaiseMessageReceived(message);
|
|
//
|
|
// Assert.That(captured, Is.Null, "Message with null text should be silently skipped");
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void Malformed_Message_With_Empty_Text_Is_Skipped()
|
|
// {
|
|
// ChannelMessageReceivedEventArgs? captured = null;
|
|
// channel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
|
|
//
|
|
// var message = new ChannelMessage(
|
|
// Guid.NewGuid().ToString(), "700", "88", "Ghost", "", DateTimeOffset.UtcNow);
|
|
//
|
|
// channel.RaiseMessageReceived(message);
|
|
//
|
|
// Assert.That(captured, Is.Null, "Message with empty text should be silently skipped");
|
|
// }
|
|
//
|
|
// [Test]
|
|
// public void Dispose_Fires_ConnectionStateChanged()
|
|
// {
|
|
// ChannelConnectionEventArgs? captured = null;
|
|
// channel.ConnectionStateChanged += (_, args) =>
|
|
// {
|
|
// captured = args;
|
|
// return Task.CompletedTask;
|
|
// };
|
|
//
|
|
// channel.Dispose();
|
|
//
|
|
// Assert.That(captured, Is.Not.Null);
|
|
// Assert.That(captured!.IsConnected, Is.False);
|
|
// Assert.That(captured.Channel, Is.SameAs(channel));
|
|
// }
|
|
// }
|