Files
ChatBot/ChatBot.Tests/Services/Telegram/TelegramMessageSenderTests.cs
Leonid Pershin c9eac74e35
Some checks failed
SonarQube / Build and analyze (push) Failing after 3m2s
Unit Tests / Run Tests (push) Failing after 2m23s
Add more tests
2025-10-20 08:20:55 +03:00

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