346 lines
12 KiB
C#
346 lines
12 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));
|
|
}
|
|
}
|