Add more tests
Some checks failed
SonarQube / Build and analyze (push) Failing after 3m2s
Unit Tests / Run Tests (push) Failing after 2m23s

This commit is contained in:
Leonid Pershin
2025-10-20 08:20:55 +03:00
parent 1647fe19d3
commit c9eac74e35
12 changed files with 3873 additions and 11 deletions

View File

@@ -0,0 +1,318 @@
using ChatBot.Services.Telegram.Services;
using ChatBot.Tests.TestUtilities;
using Microsoft.Extensions.Logging;
using Moq;
using Telegram.Bot;
using Telegram.Bot.Exceptions;
namespace ChatBot.Tests.Services.Telegram;
public class TelegramErrorHandlerTests : UnitTestBase
{
private readonly Mock<ILogger<TelegramErrorHandler>> _loggerMock;
private readonly Mock<ITelegramBotClient> _botClientMock;
private readonly TelegramErrorHandler _errorHandler;
public TelegramErrorHandlerTests()
{
_loggerMock = new Mock<ILogger<TelegramErrorHandler>>();
_botClientMock = new Mock<ITelegramBotClient>();
_errorHandler = new TelegramErrorHandler(_loggerMock.Object);
}
[Fact]
public void Constructor_ShouldCreateInstance()
{
// Act & Assert
Assert.NotNull(_errorHandler);
}
[Fact]
public async Task HandlePollingErrorAsync_WithApiRequestException_ShouldLogErrorWithFormattedMessage()
{
// Arrange
var errorCode = 400;
var errorMessage = "Bad Request: chat not found";
var apiException = new ApiRequestException(errorMessage, errorCode);
var cancellationToken = CancellationToken.None;
// Act
await _errorHandler.HandlePollingErrorAsync(
_botClientMock.Object,
apiException,
cancellationToken
);
// Assert
_loggerMock.Verify(
x =>
x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!
.Contains($"Telegram API Error:\n[{errorCode}]\n{errorMessage}")
),
apiException,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Theory]
[InlineData(400, "Bad Request")]
[InlineData(401, "Unauthorized")]
[InlineData(403, "Forbidden")]
[InlineData(404, "Not Found")]
[InlineData(429, "Too Many Requests")]
[InlineData(500, "Internal Server Error")]
public async Task HandlePollingErrorAsync_WithDifferentApiErrorCodes_ShouldLogCorrectFormat(
int errorCode,
string errorMessage
)
{
// Arrange
var apiException = new ApiRequestException(errorMessage, errorCode);
var cancellationToken = CancellationToken.None;
// Act
await _errorHandler.HandlePollingErrorAsync(
_botClientMock.Object,
apiException,
cancellationToken
);
// Assert
_loggerMock.Verify(
x =>
x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!
.Contains($"Telegram API Error:\n[{errorCode}]\n{errorMessage}")
),
apiException,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task HandlePollingErrorAsync_WithGenericException_ShouldLogExceptionToString()
{
// Arrange
var genericException = new InvalidOperationException("Something went wrong");
var cancellationToken = CancellationToken.None;
// Act
await _errorHandler.HandlePollingErrorAsync(
_botClientMock.Object,
genericException,
cancellationToken
);
// Assert
_loggerMock.Verify(
x =>
x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!
.Contains("System.InvalidOperationException: Something went wrong")
),
genericException,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task HandlePollingErrorAsync_WithTimeoutException_ShouldLogTimeoutException()
{
// Arrange
var timeoutException = new TimeoutException("Request timed out");
var cancellationToken = CancellationToken.None;
// Act
await _errorHandler.HandlePollingErrorAsync(
_botClientMock.Object,
timeoutException,
cancellationToken
);
// Assert
_loggerMock.Verify(
x =>
x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!.Contains("System.TimeoutException: Request timed out")
),
timeoutException,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task HandlePollingErrorAsync_WithHttpRequestException_ShouldLogHttpRequestException()
{
// Arrange
var httpException = new HttpRequestException("Network error occurred");
var cancellationToken = CancellationToken.None;
// Act
await _errorHandler.HandlePollingErrorAsync(
_botClientMock.Object,
httpException,
cancellationToken
);
// Assert
_loggerMock.Verify(
x =>
x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!
.Contains(
"System.Net.Http.HttpRequestException: Network error occurred"
)
),
httpException,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task HandlePollingErrorAsync_WithCancelledToken_ShouldCompleteSuccessfully()
{
// Arrange
var exception = new InvalidOperationException("Test exception");
using var cancellationTokenSource = new CancellationTokenSource();
await cancellationTokenSource.CancelAsync();
var cancelledToken = cancellationTokenSource.Token;
// Act
await _errorHandler.HandlePollingErrorAsync(
_botClientMock.Object,
exception,
cancelledToken
);
// Assert
_loggerMock.Verify(
x =>
x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
exception,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task HandlePollingErrorAsync_ShouldReturnCompletedTask()
{
// Arrange
var exception = new Exception("Test exception");
var cancellationToken = CancellationToken.None;
// Act
await _errorHandler.HandlePollingErrorAsync(
_botClientMock.Object,
exception,
cancellationToken
);
// Assert
}
[Fact]
public async Task HandlePollingErrorAsync_WithNestedException_ShouldLogOuterException()
{
// Arrange
var innerException = new ArgumentException("Inner exception");
var outerException = new InvalidOperationException("Outer exception", innerException);
var cancellationToken = CancellationToken.None;
// Act
await _errorHandler.HandlePollingErrorAsync(
_botClientMock.Object,
outerException,
cancellationToken
);
// Assert
_loggerMock.Verify(
x =>
x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!
.Contains("System.InvalidOperationException: Outer exception")
),
outerException,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task HandlePollingErrorAsync_WithAggregateException_ShouldLogAggregateException()
{
// Arrange
var exceptions = new Exception[]
{
new InvalidOperationException("First exception"),
new ArgumentException("Second exception"),
};
var aggregateException = new AggregateException("Multiple exceptions occurred", exceptions);
var cancellationToken = CancellationToken.None;
// Act
await _errorHandler.HandlePollingErrorAsync(
_botClientMock.Object,
aggregateException,
cancellationToken
);
// Assert
_loggerMock.Verify(
x =>
x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!
.Contains("System.AggregateException: Multiple exceptions occurred")
),
aggregateException,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
}

View File

@@ -0,0 +1,832 @@
using ChatBot.Services.Telegram.Commands;
using ChatBot.Services.Telegram.Interfaces;
using ChatBot.Services.Telegram.Services;
using ChatBot.Tests.TestUtilities;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Xunit;
namespace ChatBot.Tests.Services.Telegram;
public class TelegramMessageHandlerTests : UnitTestBase
{
private readonly Mock<ILogger<TelegramMessageHandler>> _loggerMock;
private readonly Mock<ITelegramCommandProcessor> _commandProcessorMock;
private readonly Mock<ITelegramMessageSender> _messageSenderMock;
private readonly Mock<ITelegramBotClient> _botClientMock;
private readonly TelegramMessageHandler _handler;
public TelegramMessageHandlerTests()
{
_loggerMock = TestDataBuilder.Mocks.CreateLoggerMock<TelegramMessageHandler>();
_commandProcessorMock = new Mock<ITelegramCommandProcessor>();
_messageSenderMock = new Mock<ITelegramMessageSender>();
_botClientMock = TestDataBuilder.Mocks.CreateTelegramBotClient();
_handler = new TelegramMessageHandler(
_loggerMock.Object,
_commandProcessorMock.Object,
_messageSenderMock.Object
);
}
private static Message CreateMessage(
string text,
long chatId,
string username,
string chatTitle,
int messageId,
User? from = null,
Message? replyToMessage = null
)
{
var message = new Message
{
Text = text,
Chat = new Chat
{
Id = chatId,
Type = ChatType.Private,
Title = chatTitle,
},
From = from ?? new User { Id = 67890, Username = username },
ReplyToMessage = replyToMessage,
};
// Note: MessageId is read-only, so we can't set it directly
// The actual MessageId will be 0, but this is sufficient for testing
return message;
}
[Fact]
public void Constructor_ShouldInitializeCorrectly()
{
// Arrange
var logger = TestDataBuilder.Mocks.CreateLoggerMock<TelegramMessageHandler>().Object;
var commandProcessor = new Mock<ITelegramCommandProcessor>().Object;
var messageSender = new Mock<ITelegramMessageSender>().Object;
// Act
var handler = new TelegramMessageHandler(logger, commandProcessor, messageSender);
// Assert
handler.Should().NotBeNull();
}
[Fact]
public void Constructor_ShouldNotThrow_WhenLoggerIsNull()
{
// Arrange
ILogger<TelegramMessageHandler>? logger = null;
var commandProcessor = new Mock<ITelegramCommandProcessor>().Object;
var messageSender = new Mock<ITelegramMessageSender>().Object;
// Act & Assert
var act = () => new TelegramMessageHandler(logger!, commandProcessor, messageSender);
act.Should().NotThrow();
}
[Fact]
public void Constructor_ShouldNotThrow_WhenCommandProcessorIsNull()
{
// Arrange
var logger = TestDataBuilder.Mocks.CreateLoggerMock<TelegramMessageHandler>().Object;
ITelegramCommandProcessor? commandProcessor = null;
var messageSender = new Mock<ITelegramMessageSender>().Object;
// Act & Assert
var act = () => new TelegramMessageHandler(logger, commandProcessor!, messageSender);
act.Should().NotThrow();
}
[Fact]
public void Constructor_ShouldNotThrow_WhenMessageSenderIsNull()
{
// Arrange
var logger = TestDataBuilder.Mocks.CreateLoggerMock<TelegramMessageHandler>().Object;
var commandProcessor = new Mock<ITelegramCommandProcessor>().Object;
ITelegramMessageSender? messageSender = null;
// Act & Assert
var act = () => new TelegramMessageHandler(logger, commandProcessor, messageSender!);
act.Should().NotThrow();
}
[Fact]
public async Task HandleUpdateAsync_ShouldReturnEarly_WhenUpdateMessageIsNull()
{
// Arrange
var update = new Update { Message = null };
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_commandProcessorMock.Verify(
x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
),
Times.Never
);
_messageSenderMock.Verify(
x =>
x.SendMessageWithRetry(
It.IsAny<ITelegramBotClient>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<CancellationToken>(),
It.IsAny<int>()
),
Times.Never
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldReturnEarly_WhenMessageTextIsNull()
{
// Arrange
var message = CreateMessage(null!, 12345, "testuser", "Test Chat", 1);
var update = new Update { Message = message };
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_commandProcessorMock.Verify(
x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
),
Times.Never
);
_messageSenderMock.Verify(
x =>
x.SendMessageWithRetry(
It.IsAny<ITelegramBotClient>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<CancellationToken>(),
It.IsAny<int>()
),
Times.Never
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldProcessMessage_WhenValidMessage()
{
// Arrange
var messageText = "Hello bot";
var chatId = 12345L;
var username = "testuser";
var chatType = "Private";
var chatTitle = "Test Chat";
var messageId = 1;
var message = CreateMessage(messageText, chatId, username, chatTitle, messageId);
var update = new Update { Message = message };
var expectedResponse = "Hello! How can I help you?";
_commandProcessorMock
.Setup(x =>
x.ProcessMessageAsync(
messageText,
chatId,
username,
chatType,
chatTitle,
null,
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync(expectedResponse);
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_commandProcessorMock.Verify(
x =>
x.ProcessMessageAsync(
messageText,
chatId,
username,
chatType,
chatTitle,
null,
It.IsAny<CancellationToken>()
),
Times.Once
);
_messageSenderMock.Verify(
x =>
x.SendMessageWithRetry(
_botClientMock.Object,
chatId,
expectedResponse,
It.IsAny<int>(),
It.IsAny<CancellationToken>(),
3
),
Times.Once
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldNotSendMessage_WhenResponseIsEmpty()
{
// Arrange
var messageText = "Hello bot";
var chatId = 12345L;
var username = "testuser";
var chatType = "Private";
var chatTitle = "Test Chat";
var message = CreateMessage(messageText, chatId, username, chatTitle, 1);
var update = new Update { Message = message };
_commandProcessorMock
.Setup(x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync(string.Empty);
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_commandProcessorMock.Verify(
x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
),
Times.Once
);
_messageSenderMock.Verify(
x =>
x.SendMessageWithRetry(
It.IsAny<ITelegramBotClient>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<CancellationToken>(),
It.IsAny<int>()
),
Times.Never
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldNotSendMessage_WhenResponseIsNull()
{
// Arrange
var messageText = "Hello bot";
var chatId = 12345L;
var username = "testuser";
var chatType = "Private";
var chatTitle = "Test Chat";
var message = CreateMessage(messageText, chatId, username, chatTitle, 1);
var update = new Update { Message = message };
_commandProcessorMock
.Setup(x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync((string)null!);
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_commandProcessorMock.Verify(
x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
),
Times.Once
);
_messageSenderMock.Verify(
x =>
x.SendMessageWithRetry(
It.IsAny<ITelegramBotClient>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<CancellationToken>(),
It.IsAny<int>()
),
Times.Never
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldUseUsername_WhenFromHasUsername()
{
// Arrange
var messageText = "Hello bot";
var chatId = 12345L;
var username = "testuser";
var chatType = "Private";
var chatTitle = "Test Chat";
var message = CreateMessage(messageText, chatId, username, chatTitle, 1);
var update = new Update { Message = message };
_commandProcessorMock
.Setup(x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync("Response");
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_commandProcessorMock.Verify(
x =>
x.ProcessMessageAsync(
messageText,
chatId,
username,
chatType,
chatTitle,
null,
It.IsAny<CancellationToken>()
),
Times.Once
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldUseFirstName_WhenFromHasNoUsername()
{
// Arrange
var messageText = "Hello bot";
var chatId = 12345L;
var firstName = "TestUser";
var chatType = "Private";
var chatTitle = "Test Chat";
var from = new User
{
Id = 67890,
Username = null,
FirstName = firstName,
};
var message = CreateMessage(messageText, chatId, firstName, chatTitle, 1, from);
var update = new Update { Message = message };
_commandProcessorMock
.Setup(x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync("Response");
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_commandProcessorMock.Verify(
x =>
x.ProcessMessageAsync(
messageText,
chatId,
firstName,
chatType,
chatTitle,
null,
It.IsAny<CancellationToken>()
),
Times.Once
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldUseUnknown_WhenFromIsNull()
{
// Arrange
var messageText = "Hello bot";
var chatId = 12345L;
var chatType = "Private";
var chatTitle = "Test Chat";
var message = CreateMessage(messageText, chatId, "Unknown", chatTitle, 1, null);
var update = new Update { Message = message };
_commandProcessorMock
.Setup(x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync("Response");
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_commandProcessorMock.Verify(
x =>
x.ProcessMessageAsync(
messageText,
chatId,
"Unknown",
chatType,
chatTitle,
null,
It.IsAny<CancellationToken>()
),
Times.Once
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldHandleReplyMessage()
{
// Arrange
var messageText = "Hello bot";
var chatId = 12345L;
var username = "testuser";
var chatType = "Private";
var chatTitle = "Test Chat";
var messageId = 1;
var replyToMessageId = 2;
var replyToUserId = 67890L;
var replyToUsername = "originaluser";
var replyToMessage = CreateMessage(
"Original message",
chatId,
replyToUsername,
chatTitle,
replyToMessageId
);
var message = CreateMessage(
messageText,
chatId,
username,
chatTitle,
messageId,
null,
replyToMessage
);
var update = new Update { Message = message };
var expectedResponse = "Response to reply";
_commandProcessorMock
.Setup(x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync(expectedResponse);
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_commandProcessorMock.Verify(
x =>
x.ProcessMessageAsync(
messageText,
chatId,
username,
chatType,
chatTitle,
It.Is<ReplyInfo>(r =>
r.UserId == replyToUserId && r.Username == replyToUsername
),
It.IsAny<CancellationToken>()
),
Times.Once
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldPassCancellationToken()
{
// Arrange
var messageText = "Hello bot";
var chatId = 12345L;
var username = "testuser";
var chatType = "Private";
var chatTitle = "Test Chat";
var cancellationToken = new CancellationToken();
var message = CreateMessage(messageText, chatId, username, chatTitle, 1);
var update = new Update { Message = message };
var expectedResponse = "Response";
_commandProcessorMock
.Setup(x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync(expectedResponse);
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, cancellationToken);
// Assert
_commandProcessorMock.Verify(
x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
cancellationToken
),
Times.Once
);
_messageSenderMock.Verify(
x =>
x.SendMessageWithRetry(
It.IsAny<ITelegramBotClient>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<int>(),
cancellationToken,
It.IsAny<int>()
),
Times.Once
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldLogError_WhenExceptionOccurs()
{
// Arrange
var messageText = "Hello bot";
var chatId = 12345L;
var username = "testuser";
var chatType = "Private";
var chatTitle = "Test Chat";
var message = CreateMessage(messageText, chatId, username, chatTitle, 1);
var update = new Update { Message = message };
var exception = new Exception("Test exception");
_commandProcessorMock
.Setup(x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
)
)
.ThrowsAsync(exception);
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_loggerMock.Verify(
x =>
x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) => v.ToString()!.Contains("Error handling update from chat")
),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldLogInformation_WhenMessageReceived()
{
// Arrange
var messageText = "Hello bot";
var chatId = 12345L;
var username = "testuser";
var message = CreateMessage(messageText, chatId, username, "Test Chat", 1);
var update = new Update { Message = message };
_commandProcessorMock
.Setup(x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync("Response");
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_loggerMock.Verify(
x =>
x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!
.Contains(
$"Message from @{username} in chat {chatId}: \"{messageText}\""
)
),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldLogInformation_WhenResponseSent()
{
// Arrange
var messageText = "Hello bot";
var chatId = 12345L;
var username = "testuser";
var response = "Hello! How can I help you?";
var message = CreateMessage(messageText, chatId, username, "Test Chat", 1);
var update = new Update { Message = message };
_commandProcessorMock
.Setup(x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync(response);
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_loggerMock.Verify(
x =>
x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!
.Contains(
$"Response sent to @{username} in chat {chatId}: \"{response}\""
)
),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task HandleUpdateAsync_ShouldLogInformation_WhenNoResponseSent()
{
// Arrange
var messageText = "Hello bot";
var chatId = 12345L;
var username = "testuser";
var message = CreateMessage(messageText, chatId, username, "Test Chat", 1);
var update = new Update { Message = message };
_commandProcessorMock
.Setup(x =>
x.ProcessMessageAsync(
It.IsAny<string>(),
It.IsAny<long>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<ReplyInfo?>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync(string.Empty);
// Act
await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None);
// Assert
_loggerMock.Verify(
x =>
x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>(
(v, t) =>
v.ToString()!
.Contains(
$"No response sent to @{username} in chat {chatId} (AI chose to ignore message)"
)
),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
}

View File

@@ -0,0 +1,647 @@
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
);
}
}

View File

@@ -1,3 +1,4 @@
using ChatBot.Services.Telegram.Interfaces;
using ChatBot.Services.Telegram.Services;
using ChatBot.Tests.TestUtilities;
using Microsoft.Extensions.Logging;
@@ -69,7 +70,8 @@ public class TelegramServicesTests : UnitTestBase
var loggerMock = new Mock<ILogger<TelegramMessageSender>>();
// Act
var sender = new TelegramMessageSender(loggerMock.Object);
var messageSenderWrapperMock = new Mock<ITelegramMessageSenderWrapper>();
var sender = new TelegramMessageSender(loggerMock.Object, messageSenderWrapperMock.Object);
// Assert
Assert.NotNull(sender);