Files
Luna/Luna.Channels.Tests/TelegramChannelTests.cs
T
2026-04-04 05:01:51 +02:00

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