648 lines
20 KiB
C#
648 lines
20 KiB
C#
using ChatBot.Services.Telegram.Interfaces;
|
|
using ChatBot.Services.Telegram.Services;
|
|
using ChatBot.Tests.TestUtilities;
|
|
using Microsoft.Extensions.Logging;
|
|
using Moq;
|
|
using Telegram.Bot;
|
|
using Telegram.Bot.Exceptions;
|
|
using Telegram.Bot.Types;
|
|
|
|
namespace ChatBot.Tests.Services.Telegram;
|
|
|
|
public class TelegramMessageSenderTests : UnitTestBase
|
|
{
|
|
private readonly Mock<ILogger<TelegramMessageSender>> _loggerMock;
|
|
private readonly Mock<ITelegramBotClient> _botClientMock;
|
|
private readonly Mock<ITelegramMessageSenderWrapper> _messageSenderWrapperMock;
|
|
private readonly TelegramMessageSender _messageSender;
|
|
|
|
public TelegramMessageSenderTests()
|
|
{
|
|
_loggerMock = new Mock<ILogger<TelegramMessageSender>>();
|
|
_botClientMock = new Mock<ITelegramBotClient>();
|
|
_messageSenderWrapperMock = new Mock<ITelegramMessageSenderWrapper>();
|
|
_messageSender = new TelegramMessageSender(
|
|
_loggerMock.Object,
|
|
_messageSenderWrapperMock.Object
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_ShouldCreateInstance()
|
|
{
|
|
// Act & Assert
|
|
Assert.NotNull(_messageSender);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessageWithRetry_ShouldSendMessageSuccessfully()
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = "Test message";
|
|
var replyToMessageId = 67890;
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_messageSenderWrapperMock
|
|
.Setup(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ReturnsAsync(new Message());
|
|
|
|
// Act
|
|
await _messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancellationToken
|
|
);
|
|
|
|
// Assert
|
|
_messageSenderWrapperMock.Verify(
|
|
x =>
|
|
x.SendMessageAsync(
|
|
It.Is<long>(id => id == chatId),
|
|
It.Is<string>(t => t == text),
|
|
It.Is<int>(r => r == replyToMessageId),
|
|
It.Is<CancellationToken>(ct => ct == cancellationToken)
|
|
),
|
|
Times.Once
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessageWithRetry_WithCustomMaxRetries_ShouldUseCustomValue()
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = "Test message";
|
|
var replyToMessageId = 67890;
|
|
var maxRetries = 5;
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_messageSenderWrapperMock
|
|
.Setup(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ReturnsAsync(new Message());
|
|
|
|
// Act
|
|
await _messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancellationToken,
|
|
maxRetries
|
|
);
|
|
|
|
// Assert
|
|
_messageSenderWrapperMock.Verify(
|
|
x =>
|
|
x.SendMessageAsync(
|
|
It.Is<long>(id => id == chatId),
|
|
It.Is<string>(t => t == text),
|
|
It.Is<int>(r => r == replyToMessageId),
|
|
It.Is<CancellationToken>(ct => ct == cancellationToken)
|
|
),
|
|
Times.Once
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessageWithRetry_WithRateLimitError_ShouldRetryAndSucceed()
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = "Test message";
|
|
var replyToMessageId = 67890;
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
var rateLimitException = new ApiRequestException("Rate limit exceeded", 429);
|
|
|
|
_messageSenderWrapperMock
|
|
.SetupSequence(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ThrowsAsync(rateLimitException)
|
|
.ReturnsAsync(new Message());
|
|
|
|
// Act
|
|
await _messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancellationToken
|
|
);
|
|
|
|
// Assert
|
|
_messageSenderWrapperMock.Verify(
|
|
x =>
|
|
x.SendMessageAsync(
|
|
It.Is<long>(id => id == chatId),
|
|
It.Is<string>(t => t == text),
|
|
It.Is<int>(r => r == replyToMessageId),
|
|
It.Is<CancellationToken>(ct => ct == cancellationToken)
|
|
),
|
|
Times.Exactly(2)
|
|
);
|
|
|
|
_loggerMock.Verify(
|
|
x =>
|
|
x.Log(
|
|
LogLevel.Warning,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>(
|
|
(v, t) => v.ToString()!.Contains("Rate limit exceeded (429)")
|
|
),
|
|
It.IsAny<Exception>(),
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
|
),
|
|
Times.Once
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessageWithRetry_WithRateLimitErrorMaxRetries_ShouldThrowException()
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = "Test message";
|
|
var replyToMessageId = 67890;
|
|
var maxRetries = 2;
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
var rateLimitException = new ApiRequestException("Rate limit exceeded", 429);
|
|
|
|
_messageSenderWrapperMock
|
|
.Setup(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ThrowsAsync(rateLimitException);
|
|
|
|
// Act & Assert
|
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
_messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancellationToken,
|
|
maxRetries
|
|
)
|
|
);
|
|
|
|
Assert.Contains(
|
|
"Failed to send message after 2 attempts due to rate limiting",
|
|
exception.Message
|
|
);
|
|
|
|
_messageSenderWrapperMock.Verify(
|
|
x =>
|
|
x.SendMessageAsync(
|
|
It.Is<long>(id => id == chatId),
|
|
It.Is<string>(t => t == text),
|
|
It.Is<int>(r => r == replyToMessageId),
|
|
It.Is<CancellationToken>(ct => ct == cancellationToken)
|
|
),
|
|
Times.Exactly(maxRetries)
|
|
);
|
|
|
|
_loggerMock.Verify(
|
|
x =>
|
|
x.Log(
|
|
LogLevel.Error,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>(
|
|
(v, t) =>
|
|
v.ToString()!
|
|
.Contains(
|
|
"Failed to send message after 2 attempts due to rate limiting"
|
|
)
|
|
),
|
|
It.IsAny<Exception>(),
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
|
),
|
|
Times.Once
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessageWithRetry_WithGenericException_ShouldThrowInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = "Test message";
|
|
var replyToMessageId = 67890;
|
|
var cancellationToken = CancellationToken.None;
|
|
var originalException = new HttpRequestException("Network error");
|
|
|
|
_messageSenderWrapperMock
|
|
.Setup(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ThrowsAsync(originalException);
|
|
|
|
// Act & Assert
|
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
_messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancellationToken
|
|
)
|
|
);
|
|
|
|
Assert.Contains("Failed to send message to chat 12345 after 1 attempts", exception.Message);
|
|
Assert.Equal(originalException, exception.InnerException);
|
|
|
|
_loggerMock.Verify(
|
|
x =>
|
|
x.Log(
|
|
LogLevel.Error,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>(
|
|
(v, t) =>
|
|
v.ToString()!
|
|
.Contains(
|
|
"Unexpected error sending message to chat 12345 on attempt 1"
|
|
)
|
|
),
|
|
originalException,
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
|
),
|
|
Times.Once
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessageWithRetry_WithApiRequestExceptionNon429_ShouldThrowInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = "Test message";
|
|
var replyToMessageId = 67890;
|
|
var cancellationToken = CancellationToken.None;
|
|
var apiException = new ApiRequestException("Bad Request", 400);
|
|
|
|
_messageSenderWrapperMock
|
|
.Setup(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ThrowsAsync(apiException);
|
|
|
|
// Act & Assert
|
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
_messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancellationToken
|
|
)
|
|
);
|
|
|
|
Assert.Contains("Failed to send message to chat 12345 after 1 attempts", exception.Message);
|
|
Assert.Equal(apiException, exception.InnerException);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessageWithRetry_WithCancelledToken_ShouldHandleCancellation()
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = "Test message";
|
|
var replyToMessageId = 67890;
|
|
using var cancellationTokenSource = new CancellationTokenSource();
|
|
await cancellationTokenSource.CancelAsync();
|
|
var cancelledToken = cancellationTokenSource.Token;
|
|
|
|
_messageSenderWrapperMock
|
|
.Setup(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ReturnsAsync(new Message());
|
|
|
|
// Act
|
|
await _messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancelledToken
|
|
);
|
|
|
|
// Assert
|
|
_messageSenderWrapperMock.Verify(
|
|
x =>
|
|
x.SendMessageAsync(
|
|
It.Is<long>(id => id == chatId),
|
|
It.Is<string>(t => t == text),
|
|
It.Is<int>(r => r == replyToMessageId),
|
|
It.Is<CancellationToken>(ct => ct == cancelledToken)
|
|
),
|
|
Times.Once
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessageWithRetry_WithEmptyText_ShouldSendEmptyMessage()
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = "";
|
|
var replyToMessageId = 0;
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_messageSenderWrapperMock
|
|
.Setup(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ReturnsAsync(new Message());
|
|
|
|
// Act
|
|
await _messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancellationToken
|
|
);
|
|
|
|
// Assert
|
|
_messageSenderWrapperMock.Verify(
|
|
x =>
|
|
x.SendMessageAsync(
|
|
It.Is<long>(id => id == chatId),
|
|
It.Is<string>(t => t == text),
|
|
It.Is<int>(r => r == replyToMessageId),
|
|
It.Is<CancellationToken>(ct => ct == cancellationToken)
|
|
),
|
|
Times.Once
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessageWithRetry_WithLongText_ShouldSendLongMessage()
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = new string('A', 4096); // Very long message
|
|
var replyToMessageId = 67890;
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_messageSenderWrapperMock
|
|
.Setup(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ReturnsAsync(new Message());
|
|
|
|
// Act
|
|
await _messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancellationToken
|
|
);
|
|
|
|
// Assert
|
|
_messageSenderWrapperMock.Verify(
|
|
x =>
|
|
x.SendMessageAsync(
|
|
It.Is<long>(id => id == chatId),
|
|
It.Is<string>(t => t == text),
|
|
It.Is<int>(r => r == replyToMessageId),
|
|
It.Is<CancellationToken>(ct => ct == cancellationToken)
|
|
),
|
|
Times.Once
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessageWithRetry_WithSpecialCharacters_ShouldSendMessageWithSpecialChars()
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = "Test message with special chars: !@#$%^&*()_+-=[]{}|;':\",./<>?`~";
|
|
var replyToMessageId = 67890;
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_messageSenderWrapperMock
|
|
.Setup(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ReturnsAsync(new Message());
|
|
|
|
// Act
|
|
await _messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancellationToken
|
|
);
|
|
|
|
// Assert
|
|
_messageSenderWrapperMock.Verify(
|
|
x =>
|
|
x.SendMessageAsync(
|
|
It.Is<long>(id => id == chatId),
|
|
It.Is<string>(t => t == text),
|
|
It.Is<int>(r => r == replyToMessageId),
|
|
It.Is<CancellationToken>(ct => ct == cancellationToken)
|
|
),
|
|
Times.Once
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessageWithRetry_WithUnicodeText_ShouldSendUnicodeMessage()
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = "Test message with unicode: 🚀 Hello 世界 🌍 Привет";
|
|
var replyToMessageId = 67890;
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_messageSenderWrapperMock
|
|
.Setup(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ReturnsAsync(new Message());
|
|
|
|
// Act
|
|
await _messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancellationToken
|
|
);
|
|
|
|
// Assert
|
|
_messageSenderWrapperMock.Verify(
|
|
x =>
|
|
x.SendMessageAsync(
|
|
It.Is<long>(id => id == chatId),
|
|
It.Is<string>(t => t == text),
|
|
It.Is<int>(r => r == replyToMessageId),
|
|
It.Is<CancellationToken>(ct => ct == cancellationToken)
|
|
),
|
|
Times.Once
|
|
);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(3)]
|
|
[InlineData(5)]
|
|
[InlineData(10)]
|
|
public async Task SendMessageWithRetry_WithDifferentMaxRetries_ShouldRespectMaxRetries(
|
|
int maxRetries
|
|
)
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = "Test message";
|
|
var replyToMessageId = 67890;
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_messageSenderWrapperMock
|
|
.Setup(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ReturnsAsync(new Message());
|
|
|
|
// Act
|
|
await _messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancellationToken,
|
|
maxRetries
|
|
);
|
|
|
|
// Assert
|
|
_messageSenderWrapperMock.Verify(
|
|
x =>
|
|
x.SendMessageAsync(
|
|
It.Is<long>(id => id == chatId),
|
|
It.Is<string>(t => t == text),
|
|
It.Is<int>(r => r == replyToMessageId),
|
|
It.Is<CancellationToken>(ct => ct == cancellationToken)
|
|
),
|
|
Times.Once
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessageWithRetry_WithNegativeMaxRetries_ShouldUseDefaultValue()
|
|
{
|
|
// Arrange
|
|
var chatId = 12345L;
|
|
var text = "Test message";
|
|
var replyToMessageId = 67890;
|
|
var maxRetries = -1; // Invalid value
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_messageSenderWrapperMock
|
|
.Setup(x =>
|
|
x.SendMessageAsync(
|
|
It.IsAny<long>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<int>(),
|
|
It.IsAny<CancellationToken>()
|
|
)
|
|
)
|
|
.ReturnsAsync(new Message());
|
|
|
|
// Act
|
|
await _messageSender.SendMessageWithRetry(
|
|
_botClientMock.Object,
|
|
chatId,
|
|
text,
|
|
replyToMessageId,
|
|
cancellationToken,
|
|
maxRetries
|
|
);
|
|
|
|
// Assert
|
|
_messageSenderWrapperMock.Verify(
|
|
x =>
|
|
x.SendMessageAsync(
|
|
It.Is<long>(id => id == chatId),
|
|
It.Is<string>(t => t == text),
|
|
It.Is<int>(r => r == replyToMessageId),
|
|
It.Is<CancellationToken>(ct => ct == cancellationToken)
|
|
),
|
|
Times.Once
|
|
);
|
|
}
|
|
}
|