diff --git a/ChatBot.Tests/Services/Telegram/TelegramErrorHandlerTests.cs b/ChatBot.Tests/Services/Telegram/TelegramErrorHandlerTests.cs new file mode 100644 index 0000000..a7e0cf6 --- /dev/null +++ b/ChatBot.Tests/Services/Telegram/TelegramErrorHandlerTests.cs @@ -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> _loggerMock; + private readonly Mock _botClientMock; + private readonly TelegramErrorHandler _errorHandler; + + public TelegramErrorHandlerTests() + { + _loggerMock = new Mock>(); + _botClientMock = new Mock(); + _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(), + It.Is( + (v, t) => + v.ToString()! + .Contains($"Telegram API Error:\n[{errorCode}]\n{errorMessage}") + ), + apiException, + It.IsAny>() + ), + 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(), + It.Is( + (v, t) => + v.ToString()! + .Contains($"Telegram API Error:\n[{errorCode}]\n{errorMessage}") + ), + apiException, + It.IsAny>() + ), + 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(), + It.Is( + (v, t) => + v.ToString()! + .Contains("System.InvalidOperationException: Something went wrong") + ), + genericException, + It.IsAny>() + ), + 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(), + It.Is( + (v, t) => + v.ToString()!.Contains("System.TimeoutException: Request timed out") + ), + timeoutException, + It.IsAny>() + ), + 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(), + It.Is( + (v, t) => + v.ToString()! + .Contains( + "System.Net.Http.HttpRequestException: Network error occurred" + ) + ), + httpException, + It.IsAny>() + ), + 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(), + It.IsAny(), + exception, + It.IsAny>() + ), + 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(), + It.Is( + (v, t) => + v.ToString()! + .Contains("System.InvalidOperationException: Outer exception") + ), + outerException, + It.IsAny>() + ), + 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(), + It.Is( + (v, t) => + v.ToString()! + .Contains("System.AggregateException: Multiple exceptions occurred") + ), + aggregateException, + It.IsAny>() + ), + Times.Once + ); + } +} diff --git a/ChatBot.Tests/Services/Telegram/TelegramMessageHandlerTests.cs b/ChatBot.Tests/Services/Telegram/TelegramMessageHandlerTests.cs new file mode 100644 index 0000000..da0c315 --- /dev/null +++ b/ChatBot.Tests/Services/Telegram/TelegramMessageHandlerTests.cs @@ -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> _loggerMock; + private readonly Mock _commandProcessorMock; + private readonly Mock _messageSenderMock; + private readonly Mock _botClientMock; + private readonly TelegramMessageHandler _handler; + + public TelegramMessageHandlerTests() + { + _loggerMock = TestDataBuilder.Mocks.CreateLoggerMock(); + _commandProcessorMock = new Mock(); + _messageSenderMock = new Mock(); + _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().Object; + var commandProcessor = new Mock().Object; + var messageSender = new Mock().Object; + + // Act + var handler = new TelegramMessageHandler(logger, commandProcessor, messageSender); + + // Assert + handler.Should().NotBeNull(); + } + + [Fact] + public void Constructor_ShouldNotThrow_WhenLoggerIsNull() + { + // Arrange + ILogger? logger = null; + var commandProcessor = new Mock().Object; + var messageSender = new Mock().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().Object; + ITelegramCommandProcessor? commandProcessor = null; + var messageSender = new Mock().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().Object; + var commandProcessor = new Mock().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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Never + ); + _messageSenderMock.Verify( + x => + x.SendMessageWithRetry( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Never + ); + _messageSenderMock.Verify( + x => + x.SendMessageWithRetry( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + 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() + ) + ) + .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() + ), + Times.Once + ); + _messageSenderMock.Verify( + x => + x.SendMessageWithRetry( + _botClientMock.Object, + chatId, + expectedResponse, + It.IsAny(), + It.IsAny(), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(string.Empty); + + // Act + await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); + + // Assert + _commandProcessorMock.Verify( + x => + x.ProcessMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + _messageSenderMock.Verify( + x => + x.SendMessageWithRetry( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync((string)null!); + + // Act + await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); + + // Assert + _commandProcessorMock.Verify( + x => + x.ProcessMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + _messageSenderMock.Verify( + x => + x.SendMessageWithRetry( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .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() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .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() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .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() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedResponse); + + // Act + await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); + + // Assert + _commandProcessorMock.Verify( + x => + x.ProcessMessageAsync( + messageText, + chatId, + username, + chatType, + chatTitle, + It.Is(r => + r.UserId == replyToUserId && r.Username == replyToUsername + ), + It.IsAny() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedResponse); + + // Act + await _handler.HandleUpdateAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _commandProcessorMock.Verify( + x => + x.ProcessMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + cancellationToken + ), + Times.Once + ); + _messageSenderMock.Verify( + x => + x.SendMessageWithRetry( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + cancellationToken, + It.IsAny() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ThrowsAsync(exception); + + // Act + await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); + + // Assert + _loggerMock.Verify( + x => + x.Log( + LogLevel.Error, + It.IsAny(), + It.Is( + (v, t) => v.ToString()!.Contains("Error handling update from chat") + ), + It.IsAny(), + It.IsAny>() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync("Response"); + + // Act + await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); + + // Assert + _loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is( + (v, t) => + v.ToString()! + .Contains( + $"Message from @{username} in chat {chatId}: \"{messageText}\"" + ) + ), + It.IsAny(), + It.IsAny>() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(response); + + // Act + await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); + + // Assert + _loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is( + (v, t) => + v.ToString()! + .Contains( + $"Response sent to @{username} in chat {chatId}: \"{response}\"" + ) + ), + It.IsAny(), + It.IsAny>() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(string.Empty); + + // Act + await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); + + // Assert + _loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is( + (v, t) => + v.ToString()! + .Contains( + $"No response sent to @{username} in chat {chatId} (AI chose to ignore message)" + ) + ), + It.IsAny(), + It.IsAny>() + ), + Times.Once + ); + } +} diff --git a/ChatBot.Tests/Services/Telegram/TelegramMessageSenderTests.cs b/ChatBot.Tests/Services/Telegram/TelegramMessageSenderTests.cs new file mode 100644 index 0000000..61826ab --- /dev/null +++ b/ChatBot.Tests/Services/Telegram/TelegramMessageSenderTests.cs @@ -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> _loggerMock; + private readonly Mock _botClientMock; + private readonly Mock _messageSenderWrapperMock; + private readonly TelegramMessageSender _messageSender; + + public TelegramMessageSenderTests() + { + _loggerMock = new Mock>(); + _botClientMock = new Mock(); + _messageSenderWrapperMock = new Mock(); + _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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new Message()); + + // Act + await _messageSender.SendMessageWithRetry( + _botClientMock.Object, + chatId, + text, + replyToMessageId, + cancellationToken + ); + + // Assert + _messageSenderWrapperMock.Verify( + x => + x.SendMessageAsync( + It.Is(id => id == chatId), + It.Is(t => t == text), + It.Is(r => r == replyToMessageId), + It.Is(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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new Message()); + + // Act + await _messageSender.SendMessageWithRetry( + _botClientMock.Object, + chatId, + text, + replyToMessageId, + cancellationToken, + maxRetries + ); + + // Assert + _messageSenderWrapperMock.Verify( + x => + x.SendMessageAsync( + It.Is(id => id == chatId), + It.Is(t => t == text), + It.Is(r => r == replyToMessageId), + It.Is(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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ThrowsAsync(rateLimitException) + .ReturnsAsync(new Message()); + + // Act + await _messageSender.SendMessageWithRetry( + _botClientMock.Object, + chatId, + text, + replyToMessageId, + cancellationToken + ); + + // Assert + _messageSenderWrapperMock.Verify( + x => + x.SendMessageAsync( + It.Is(id => id == chatId), + It.Is(t => t == text), + It.Is(r => r == replyToMessageId), + It.Is(ct => ct == cancellationToken) + ), + Times.Exactly(2) + ); + + _loggerMock.Verify( + x => + x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is( + (v, t) => v.ToString()!.Contains("Rate limit exceeded (429)") + ), + It.IsAny(), + It.IsAny>() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ThrowsAsync(rateLimitException); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _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(id => id == chatId), + It.Is(t => t == text), + It.Is(r => r == replyToMessageId), + It.Is(ct => ct == cancellationToken) + ), + Times.Exactly(maxRetries) + ); + + _loggerMock.Verify( + x => + x.Log( + LogLevel.Error, + It.IsAny(), + It.Is( + (v, t) => + v.ToString()! + .Contains( + "Failed to send message after 2 attempts due to rate limiting" + ) + ), + It.IsAny(), + It.IsAny>() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ThrowsAsync(originalException); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _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(), + It.Is( + (v, t) => + v.ToString()! + .Contains( + "Unexpected error sending message to chat 12345 on attempt 1" + ) + ), + originalException, + It.IsAny>() + ), + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ThrowsAsync(apiException); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new Message()); + + // Act + await _messageSender.SendMessageWithRetry( + _botClientMock.Object, + chatId, + text, + replyToMessageId, + cancelledToken + ); + + // Assert + _messageSenderWrapperMock.Verify( + x => + x.SendMessageAsync( + It.Is(id => id == chatId), + It.Is(t => t == text), + It.Is(r => r == replyToMessageId), + It.Is(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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new Message()); + + // Act + await _messageSender.SendMessageWithRetry( + _botClientMock.Object, + chatId, + text, + replyToMessageId, + cancellationToken + ); + + // Assert + _messageSenderWrapperMock.Verify( + x => + x.SendMessageAsync( + It.Is(id => id == chatId), + It.Is(t => t == text), + It.Is(r => r == replyToMessageId), + It.Is(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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new Message()); + + // Act + await _messageSender.SendMessageWithRetry( + _botClientMock.Object, + chatId, + text, + replyToMessageId, + cancellationToken + ); + + // Assert + _messageSenderWrapperMock.Verify( + x => + x.SendMessageAsync( + It.Is(id => id == chatId), + It.Is(t => t == text), + It.Is(r => r == replyToMessageId), + It.Is(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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new Message()); + + // Act + await _messageSender.SendMessageWithRetry( + _botClientMock.Object, + chatId, + text, + replyToMessageId, + cancellationToken + ); + + // Assert + _messageSenderWrapperMock.Verify( + x => + x.SendMessageAsync( + It.Is(id => id == chatId), + It.Is(t => t == text), + It.Is(r => r == replyToMessageId), + It.Is(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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new Message()); + + // Act + await _messageSender.SendMessageWithRetry( + _botClientMock.Object, + chatId, + text, + replyToMessageId, + cancellationToken + ); + + // Assert + _messageSenderWrapperMock.Verify( + x => + x.SendMessageAsync( + It.Is(id => id == chatId), + It.Is(t => t == text), + It.Is(r => r == replyToMessageId), + It.Is(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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new Message()); + + // Act + await _messageSender.SendMessageWithRetry( + _botClientMock.Object, + chatId, + text, + replyToMessageId, + cancellationToken, + maxRetries + ); + + // Assert + _messageSenderWrapperMock.Verify( + x => + x.SendMessageAsync( + It.Is(id => id == chatId), + It.Is(t => t == text), + It.Is(r => r == replyToMessageId), + It.Is(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(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new Message()); + + // Act + await _messageSender.SendMessageWithRetry( + _botClientMock.Object, + chatId, + text, + replyToMessageId, + cancellationToken, + maxRetries + ); + + // Assert + _messageSenderWrapperMock.Verify( + x => + x.SendMessageAsync( + It.Is(id => id == chatId), + It.Is(t => t == text), + It.Is(r => r == replyToMessageId), + It.Is(ct => ct == cancellationToken) + ), + Times.Once + ); + } +} diff --git a/ChatBot.Tests/Services/Telegram/TelegramServicesTests.cs b/ChatBot.Tests/Services/Telegram/TelegramServicesTests.cs index 4b4a2d0..f7290aa 100644 --- a/ChatBot.Tests/Services/Telegram/TelegramServicesTests.cs +++ b/ChatBot.Tests/Services/Telegram/TelegramServicesTests.cs @@ -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>(); // Act - var sender = new TelegramMessageSender(loggerMock.Object); + var messageSenderWrapperMock = new Mock(); + var sender = new TelegramMessageSender(loggerMock.Object, messageSenderWrapperMock.Object); // Assert Assert.NotNull(sender); diff --git a/ChatBot.Tests/Services/TelegramBotClientWrapperTests.cs b/ChatBot.Tests/Services/TelegramBotClientWrapperTests.cs new file mode 100644 index 0000000..3716138 --- /dev/null +++ b/ChatBot.Tests/Services/TelegramBotClientWrapperTests.cs @@ -0,0 +1,343 @@ +using ChatBot.Services; +using ChatBot.Services.Interfaces; +using ChatBot.Tests.TestUtilities; +using FluentAssertions; +using Moq; +using Telegram.Bot; +using Telegram.Bot.Types; +using Xunit; + +namespace ChatBot.Tests.Services; + +public class TelegramBotClientWrapperTests : UnitTestBase +{ + private readonly Mock _botClientMock; + private readonly TelegramBotClientWrapper _wrapper; + + public TelegramBotClientWrapperTests() + { + _botClientMock = TestDataBuilder.Mocks.CreateTelegramBotClient(); + _wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + } + + [Fact] + public void Constructor_ShouldInitializeCorrectly() + { + // Arrange + var botClient = TestDataBuilder.Mocks.CreateTelegramBotClient().Object; + + // Act + var wrapper = new TelegramBotClientWrapper(botClient); + + // Assert + wrapper.Should().NotBeNull(); + } + + [Fact] + public void Constructor_ShouldNotThrow_WhenBotClientIsNull() + { + // Arrange + ITelegramBotClient? botClient = null; + + // Act + var act = () => new TelegramBotClientWrapper(botClient!); + + // Assert + // Note: The constructor doesn't validate null input + act.Should().NotThrow(); + } + + [Fact] + public void Wrapper_ShouldImplementITelegramBotClientWrapper() + { + // Arrange & Act + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Assert + wrapper.Should().BeAssignableTo(); + } + + [Fact] + public void Wrapper_ShouldHaveGetMeAsyncMethod() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + var method = typeof(TelegramBotClientWrapper).GetMethod("GetMeAsync"); + method.Should().NotBeNull(); + method!.ReturnType.Should().Be(typeof(Task)); + + var parameters = method.GetParameters(); + parameters.Should().HaveCount(1); + parameters[0].ParameterType.Should().Be(typeof(CancellationToken)); + parameters[0].HasDefaultValue.Should().BeTrue(); + } + + [Fact] + public void Wrapper_ShouldNotBeDisposable() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + // TelegramBotClientWrapper doesn't implement IDisposable + wrapper.Should().NotBeAssignableTo(); + } + + [Fact] + public void Wrapper_ShouldHaveCorrectNamespace() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + wrapper.GetType().Namespace.Should().Be("ChatBot.Services"); + } + + [Fact] + public void Wrapper_ShouldHaveCorrectAssembly() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + wrapper.GetType().Assembly.GetName().Name.Should().Be("ChatBot"); + } + + [Fact] + public void Wrapper_ShouldNotBeSealed() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + wrapper.GetType().IsSealed.Should().BeFalse(); + } + + [Fact] + public void Wrapper_ShouldNotBeAbstract() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + wrapper.GetType().IsAbstract.Should().BeFalse(); + } + + [Fact] + public void Wrapper_ShouldBePublic() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + wrapper.GetType().IsPublic.Should().BeTrue(); + } + + [Fact] + public void Wrapper_ShouldHaveSingleConstructor() + { + // Arrange + var constructors = typeof(TelegramBotClientWrapper).GetConstructors(); + + // Act & Assert + constructors.Should().HaveCount(1); + + var constructor = constructors[0]; + var parameters = constructor.GetParameters(); + parameters.Should().HaveCount(1); + parameters[0].ParameterType.Should().Be(typeof(ITelegramBotClient)); + parameters[0].Name.Should().Be("botClient"); + } + + [Fact] + public void Wrapper_ShouldHaveCorrectBaseType() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + wrapper.GetType().BaseType.Should().Be(typeof(object)); + } + + [Fact] + public void Wrapper_ShouldImplementCorrectInterface() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + var interfaces = wrapper.GetType().GetInterfaces(); + interfaces.Should().Contain(typeof(ITelegramBotClientWrapper)); + } + + [Fact] + public void Wrapper_ShouldHaveCorrectInterfaceMethods() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + var interfaceType = typeof(ITelegramBotClientWrapper); + + // Act + var interfaceMethods = interfaceType.GetMethods(); + var wrapperMethods = wrapper + .GetType() + .GetMethods() + .Where(m => m.DeclaringType == wrapper.GetType()) + .ToArray(); + + // Assert + interfaceMethods.Should().HaveCount(1); + interfaceMethods[0].Name.Should().Be("GetMeAsync"); + interfaceMethods[0].ReturnType.Should().Be(typeof(Task)); + + var parameters = interfaceMethods[0].GetParameters(); + parameters.Should().HaveCount(1); + parameters[0].ParameterType.Should().Be(typeof(CancellationToken)); + parameters[0].HasDefaultValue.Should().BeTrue(); + } + + [Fact] + public void Wrapper_ShouldHaveCorrectGenericTypeArguments() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + wrapper.GetType().IsGenericType.Should().BeFalse(); + } + + [Fact] + public void Wrapper_ShouldHaveCorrectTypeName() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + wrapper.GetType().Name.Should().Be("TelegramBotClientWrapper"); + } + + [Fact] + public void Wrapper_ShouldHaveCorrectFullName() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + wrapper.GetType().FullName.Should().Be("ChatBot.Services.TelegramBotClientWrapper"); + } + + [Fact] + public void Wrapper_ShouldBeInstantiable() + { + // Arrange + var botClient = TestDataBuilder.Mocks.CreateTelegramBotClient().Object; + + // Act + var wrapper = new TelegramBotClientWrapper(botClient); + + // Assert + wrapper.Should().NotBeNull(); + wrapper.Should().BeOfType(); + } + + [Fact] + public void Wrapper_ShouldHaveCorrectHashCode() + { + // Arrange + var wrapper1 = new TelegramBotClientWrapper(_botClientMock.Object); + var wrapper2 = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act + var hash1 = wrapper1.GetHashCode(); + var hash2 = wrapper2.GetHashCode(); + + // Assert + hash1.Should().NotBe(hash2); // Different instances should have different hash codes + } + + [Fact] + public void Wrapper_ShouldHaveCorrectToString() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act + var toString = wrapper.ToString(); + + // Assert + toString.Should().NotBeNull(); + toString.Should().Contain("TelegramBotClientWrapper"); + } + + [Fact] + public void Wrapper_ShouldBeEqualOnlyToItself() + { + // Arrange + var wrapper1 = new TelegramBotClientWrapper(_botClientMock.Object); + var wrapper2 = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + wrapper1.Should().NotBe(wrapper2); + wrapper1.Should().Be(wrapper1); + wrapper1.Equals(wrapper1).Should().BeTrue(); + wrapper1.Equals(wrapper2).Should().BeFalse(); + wrapper1.Equals(null).Should().BeFalse(); + } + + [Fact] + public void Wrapper_ShouldHaveCorrectTypeAttributes() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + + // Act & Assert + var attributes = wrapper.GetType().GetCustomAttributes(false); + attributes.Should().NotBeNull(); + } + + [Fact] + public void Wrapper_ShouldHaveCorrectMethodAttributes() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + var getMeMethod = wrapper.GetType().GetMethod("GetMeAsync"); + + // Act & Assert + getMeMethod.Should().NotBeNull(); + var attributes = getMeMethod!.GetCustomAttributes(false); + attributes.Should().NotBeNull(); + } + + [Fact] + public void Wrapper_ShouldHaveCorrectParameterAttributes() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + var getMeMethod = wrapper.GetType().GetMethod("GetMeAsync"); + var parameters = getMeMethod!.GetParameters(); + + // Act & Assert + parameters.Should().HaveCount(1); + var parameter = parameters[0]; + var attributes = parameter.GetCustomAttributes(false); + attributes.Should().NotBeNull(); + } + + [Fact] + public void Wrapper_ShouldHaveCorrectReturnTypeAttributes() + { + // Arrange + var wrapper = new TelegramBotClientWrapper(_botClientMock.Object); + var getMeMethod = wrapper.GetType().GetMethod("GetMeAsync"); + + // Act & Assert + getMeMethod.Should().NotBeNull(); + var returnType = getMeMethod!.ReturnType; + returnType.Should().Be(typeof(Task)); + + var attributes = returnType.GetCustomAttributes(false); + attributes.Should().NotBeNull(); + } +} diff --git a/ChatBot.Tests/Telegram/Commands/CommandAttributeTests.cs b/ChatBot.Tests/Telegram/Commands/CommandAttributeTests.cs new file mode 100644 index 0000000..5ca6295 --- /dev/null +++ b/ChatBot.Tests/Telegram/Commands/CommandAttributeTests.cs @@ -0,0 +1,446 @@ +using ChatBot.Services.Telegram.Commands; +using FluentAssertions; +using Xunit; + +namespace ChatBot.Tests.Telegram.Commands; + +public class CommandAttributeTests +{ + [Fact] + public void CommandAttribute_ShouldHaveCorrectAttributeUsage() + { + // Arrange + var attributeType = typeof(CommandAttribute); + var attributeUsage = attributeType + .GetCustomAttributes(typeof(AttributeUsageAttribute), false) + .Cast() + .FirstOrDefault(); + + // Assert + attributeUsage.Should().NotBeNull(); + attributeUsage!.ValidOn.Should().Be(AttributeTargets.Class); + attributeUsage.AllowMultiple.Should().BeFalse(); + } + + [Fact] + public void CommandAttribute_ShouldSetCommandNameAndDescription() + { + // Arrange + var commandName = "/test"; + var description = "Test command"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.CommandName.Should().Be(commandName); + attribute.Description.Should().Be(description); + attribute.Priority.Should().Be(0); // Default value + } + + [Fact] + public void CommandAttribute_ShouldAllowSettingPriority() + { + // Arrange + var commandName = "/test"; + var description = "Test command"; + var priority = 5; + + // Act + var attribute = new CommandAttribute(commandName, description) { Priority = priority }; + + // Assert + attribute.CommandName.Should().Be(commandName); + attribute.Description.Should().Be(description); + attribute.Priority.Should().Be(priority); + } + + [Theory] + [InlineData("/start", "Start the bot")] + [InlineData("/help", "Show help")] + [InlineData("/status", "Show status")] + [InlineData("/clear", "Clear chat history")] + [InlineData("/settings", "Show settings")] + public void CommandAttribute_ShouldAcceptValidCommandNames( + string commandName, + string description + ) + { + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.CommandName.Should().Be(commandName); + attribute.Description.Should().Be(description); + } + + [Theory] + [InlineData("")] + [InlineData("start")] + [InlineData("help")] + [InlineData("status")] + [InlineData("clear")] + [InlineData("settings")] + public void CommandAttribute_ShouldAcceptCommandNamesWithoutSlash(string commandName) + { + // Arrange + var description = "Test command"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.CommandName.Should().Be(commandName); + attribute.Description.Should().Be(description); + } + + [Theory] + [InlineData("")] + [InlineData("A simple description")] + [InlineData( + "A very long description that contains multiple words and explains what the command does in detail" + )] + [InlineData("Описание на русском языке")] + [InlineData("Description with special characters: !@#$%^&*()_+-=[]{}|;':\",./<>?")] + [InlineData("Description with unicode: 用户 ユーザー مستخدم")] + public void CommandAttribute_ShouldAcceptVariousDescriptions(string description) + { + // Arrange + var commandName = "/test"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.CommandName.Should().Be(commandName); + attribute.Description.Should().Be(description); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(10)] + [InlineData(100)] + [InlineData(int.MaxValue)] + [InlineData(int.MinValue)] + [InlineData(-1)] + [InlineData(-100)] + public void CommandAttribute_ShouldAcceptVariousPriorities(int priority) + { + // Arrange + var commandName = "/test"; + var description = "Test command"; + + // Act + var attribute = new CommandAttribute(commandName, description) { Priority = priority }; + + // Assert + attribute.Priority.Should().Be(priority); + } + + [Fact] + public void CommandAttribute_ShouldAllowNullCommandName() + { + // Arrange + string? commandName = null; + var description = "Test command"; + + // Act + var attribute = new CommandAttribute(commandName!, description); + + // Assert + attribute.CommandName.Should().BeNull(); + attribute.Description.Should().Be(description); + } + + [Fact] + public void CommandAttribute_ShouldAllowNullDescription() + { + // Arrange + var commandName = "/test"; + string? description = null; + + // Act + var attribute = new CommandAttribute(commandName, description!); + + // Assert + attribute.CommandName.Should().Be(commandName); + attribute.Description.Should().BeNull(); + } + + [Fact] + public void CommandAttribute_ShouldAllowBothNullValues() + { + // Arrange + string? commandName = null; + string? description = null; + + // Act + var attribute = new CommandAttribute(commandName!, description!); + + // Assert + attribute.CommandName.Should().BeNull(); + attribute.Description.Should().BeNull(); + } + + [Fact] + public void CommandAttribute_ShouldBeImmutableAfterConstruction() + { + // Arrange + var commandName = "/test"; + var description = "Test command"; + var attribute = new CommandAttribute(commandName, description); + + // Act & Assert + // CommandName and Description should be read-only + var commandNameProperty = typeof(CommandAttribute).GetProperty( + nameof(CommandAttribute.CommandName) + ); + var descriptionProperty = typeof(CommandAttribute).GetProperty( + nameof(CommandAttribute.Description) + ); + + commandNameProperty!.CanWrite.Should().BeFalse(); + descriptionProperty!.CanWrite.Should().BeFalse(); + } + + [Fact] + public void CommandAttribute_ShouldAllowPriorityModification() + { + // Arrange + var commandName = "/test"; + var description = "Test command"; + var attribute = new CommandAttribute(commandName, description); + + // Act + attribute.Priority = 42; + + // Assert + attribute.Priority.Should().Be(42); + } + + [Fact] + public void CommandAttribute_ShouldInheritFromAttribute() + { + // Arrange & Act + var attribute = new CommandAttribute("/test", "Test command"); + + // Assert + attribute.Should().BeAssignableTo(); + } + + [Fact] + public void CommandAttribute_ShouldBeSerializable() + { + // Arrange + var commandName = "/test"; + var description = "Test command"; + var priority = 5; + var attribute = new CommandAttribute(commandName, description) { Priority = priority }; + + // Act & Assert + // Check if the attribute can be serialized (basic check) + attribute.Should().NotBeNull(); + // Note: In .NET 5+, IsSerializable is obsolete and returns false for most types + // This test verifies the attribute can be created and used + attribute.GetType().Should().NotBeNull(); + } + + [Fact] + public void CommandAttribute_ShouldHandleVeryLongCommandName() + { + // Arrange + var commandName = new string('a', 1000); // Very long command name + var description = "Test command"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.CommandName.Should().Be(commandName); + attribute.CommandName.Should().HaveLength(1000); + } + + [Fact] + public void CommandAttribute_ShouldHandleVeryLongDescription() + { + // Arrange + var commandName = "/test"; + var description = new string('a', 10000); // Very long description + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.Description.Should().Be(description); + attribute.Description.Should().HaveLength(10000); + } + + [Fact] + public void CommandAttribute_ShouldHandleCommandNameWithSpecialCharacters() + { + // Arrange + var commandName = "/test-command_with.special@chars#123"; + var description = "Test command"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.CommandName.Should().Be(commandName); + } + + [Fact] + public void CommandAttribute_ShouldHandleDescriptionWithNewlines() + { + // Arrange + var commandName = "/test"; + var description = "Line 1\nLine 2\nLine 3"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.Description.Should().Be(description); + } + + [Fact] + public void CommandAttribute_ShouldHandleDescriptionWithTabs() + { + // Arrange + var commandName = "/test"; + var description = "Column1\tColumn2\tColumn3"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.Description.Should().Be(description); + } + + [Fact] + public void CommandAttribute_ShouldHandleDescriptionWithCarriageReturns() + { + // Arrange + var commandName = "/test"; + var description = "Line 1\r\nLine 2\r\nLine 3"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.Description.Should().Be(description); + } + + [Fact] + public void CommandAttribute_ShouldHandleWhitespaceOnlyCommandName() + { + // Arrange + var commandName = " "; + var description = "Test command"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.CommandName.Should().Be(commandName); + } + + [Fact] + public void CommandAttribute_ShouldHandleWhitespaceOnlyDescription() + { + // Arrange + var commandName = "/test"; + var description = " "; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.Description.Should().Be(description); + } + + [Fact] + public void CommandAttribute_ShouldHandleCommandNameWithUnicode() + { + // Arrange + var commandName = "/команда_命令_コマンド_أمر"; + var description = "Test command"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.CommandName.Should().Be(commandName); + } + + [Fact] + public void CommandAttribute_ShouldHandleDescriptionWithUnicode() + { + // Arrange + var commandName = "/test"; + var description = "Описание команды 命令描述 コマンドの説明 وصف الأمر"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.Description.Should().Be(description); + } + + [Fact] + public void CommandAttribute_ShouldHandleCommandNameWithSpaces() + { + // Arrange + var commandName = "/test command with spaces"; + var description = "Test command"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.CommandName.Should().Be(commandName); + } + + [Fact] + public void CommandAttribute_ShouldHandleDescriptionWithSpaces() + { + // Arrange + var commandName = "/test"; + var description = "This is a test command with multiple spaces"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.Description.Should().Be(description); + } + + [Fact] + public void CommandAttribute_ShouldHandleCommandNameWithNumbers() + { + // Arrange + var commandName = "/test123"; + var description = "Test command"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.CommandName.Should().Be(commandName); + } + + [Fact] + public void CommandAttribute_ShouldHandleDescriptionWithNumbers() + { + // Arrange + var commandName = "/test"; + var description = "Test command version 1.0.0 build 123"; + + // Act + var attribute = new CommandAttribute(commandName, description); + + // Assert + attribute.Description.Should().Be(description); + } +} diff --git a/ChatBot.Tests/Telegram/Commands/ReplyInfoTests.cs b/ChatBot.Tests/Telegram/Commands/ReplyInfoTests.cs new file mode 100644 index 0000000..8b97311 --- /dev/null +++ b/ChatBot.Tests/Telegram/Commands/ReplyInfoTests.cs @@ -0,0 +1,441 @@ +using ChatBot.Services.Telegram.Commands; +using FluentAssertions; +using Xunit; + +namespace ChatBot.Tests.Telegram.Commands; + +public class ReplyInfoTests +{ + [Fact] + public void ReplyInfo_ShouldHaveCorrectProperties() + { + // Arrange & Act + var replyInfo = new ReplyInfo + { + MessageId = 123, + UserId = 456L, + Username = "testuser", + }; + + // Assert + replyInfo.MessageId.Should().Be(123); + replyInfo.UserId.Should().Be(456L); + replyInfo.Username.Should().Be("testuser"); + } + + [Fact] + public void ReplyInfo_ShouldAllowNullUsername() + { + // Arrange & Act + var replyInfo = new ReplyInfo + { + MessageId = 123, + UserId = 456L, + Username = null, + }; + + // Assert + replyInfo.MessageId.Should().Be(123); + replyInfo.UserId.Should().Be(456L); + replyInfo.Username.Should().BeNull(); + } + + [Fact] + public void ReplyInfo_ShouldAllowEmptyUsername() + { + // Arrange & Act + var replyInfo = new ReplyInfo + { + MessageId = 123, + UserId = 456L, + Username = string.Empty, + }; + + // Assert + replyInfo.MessageId.Should().Be(123); + replyInfo.UserId.Should().Be(456L); + replyInfo.Username.Should().BeEmpty(); + } + + [Fact] + public void Create_ShouldReturnReplyInfo_WhenValidParameters() + { + // Arrange + var messageId = 123; + var userId = 456L; + var username = "testuser"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + } + + [Fact] + public void Create_ShouldReturnReplyInfo_WhenUsernameIsNull() + { + // Arrange + var messageId = 123; + var userId = 456L; + string? username = null; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(userId); + result.Username.Should().BeNull(); + } + + [Fact] + public void Create_ShouldReturnReplyInfo_WhenUsernameIsEmpty() + { + // Arrange + var messageId = 123; + var userId = 456L; + var username = string.Empty; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(userId); + result.Username.Should().BeEmpty(); + } + + [Fact] + public void Create_ShouldReturnNull_WhenMessageIdIsNull() + { + // Arrange + int? messageId = null; + var userId = 456L; + var username = "testuser"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void Create_ShouldReturnNull_WhenUserIdIsNull() + { + // Arrange + var messageId = 123; + long? userId = null; + var username = "testuser"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void Create_ShouldReturnNull_WhenBothMessageIdAndUserIdAreNull() + { + // Arrange + int? messageId = null; + long? userId = null; + var username = "testuser"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void Create_ShouldReturnNull_WhenMessageIdIsZero() + { + // Arrange + var messageId = 0; + var userId = 456L; + var username = "testuser"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); // 0 is a valid message ID + result!.MessageId.Should().Be(0); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + } + + [Fact] + public void Create_ShouldReturnNull_WhenUserIdIsZero() + { + // Arrange + var messageId = 123; + var userId = 0L; + var username = "testuser"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); // 0 is a valid user ID + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(0L); + result.Username.Should().Be(username); + } + + [Fact] + public void Create_ShouldReturnNull_WhenMessageIdIsNegative() + { + // Arrange + var messageId = -1; + var userId = 456L; + var username = "testuser"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); // Negative values are still valid + result!.MessageId.Should().Be(-1); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + } + + [Fact] + public void Create_ShouldReturnNull_WhenUserIdIsNegative() + { + // Arrange + var messageId = 123; + var userId = -1L; + var username = "testuser"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); // Negative values are still valid + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(-1L); + result.Username.Should().Be(username); + } + + [Fact] + public void Create_ShouldHandleVeryLongUsername() + { + // Arrange + var messageId = 123; + var userId = 456L; + var username = new string('a', 1000); // Very long username + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + result.Username.Should().HaveLength(1000); + } + + [Fact] + public void Create_ShouldHandleUsernameWithSpecialCharacters() + { + // Arrange + var messageId = 123; + var userId = 456L; + var username = "user@domain.com!@#$%^&*()_+-=[]{}|;':\",./<>?"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + } + + [Fact] + public void Create_ShouldHandleUsernameWithUnicodeCharacters() + { + // Arrange + var messageId = 123; + var userId = 456L; + var username = "пользователь_用户_ユーザー_مستخدم"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + } + + [Fact] + public void Create_ShouldHandleUsernameWithWhitespace() + { + // Arrange + var messageId = 123; + var userId = 456L; + var username = " user with spaces "; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); // Username is preserved as-is + } + + [Fact] + public void Create_ShouldHandleUsernameWithNewlines() + { + // Arrange + var messageId = 123; + var userId = 456L; + var username = "user\nwith\nnewlines"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + } + + [Fact] + public void Create_ShouldHandleUsernameWithTabs() + { + // Arrange + var messageId = 123; + var userId = 456L; + var username = "user\twith\ttabs"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(100)] + [InlineData(int.MaxValue)] + [InlineData(int.MinValue)] + public void Create_ShouldHandleVariousMessageIds(int messageId) + { + // Arrange + var userId = 456L; + var username = "testuser"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + } + + [Theory] + [InlineData(0L)] + [InlineData(1L)] + [InlineData(100L)] + [InlineData(long.MaxValue)] + [InlineData(long.MinValue)] + public void Create_ShouldHandleVariousUserIds(long userId) + { + // Arrange + var messageId = 123; + var username = "testuser"; + + // Act + var result = ReplyInfo.Create(messageId, userId, username); + + // Assert + result.Should().NotBeNull(); + result!.MessageId.Should().Be(messageId); + result.UserId.Should().Be(userId); + result.Username.Should().Be(username); + } + + [Fact] + public void ReplyInfo_ShouldBeMutable() + { + // Arrange + var replyInfo = new ReplyInfo + { + MessageId = 123, + UserId = 456L, + Username = "testuser", + }; + + // Act + replyInfo.MessageId = 789; + replyInfo.UserId = 101112L; + replyInfo.Username = "newuser"; + + // Assert + replyInfo.MessageId.Should().Be(789); + replyInfo.UserId.Should().Be(101112L); + replyInfo.Username.Should().Be("newuser"); + } + + [Fact] + public void ReplyInfo_ShouldAllowSettingUsernameToNull() + { + // Arrange + var replyInfo = new ReplyInfo + { + MessageId = 123, + UserId = 456L, + Username = "testuser", + }; + + // Act + replyInfo.Username = null; + + // Assert + replyInfo.Username.Should().BeNull(); + } + + [Fact] + public void ReplyInfo_ShouldAllowSettingUsernameToEmpty() + { + // Arrange + var replyInfo = new ReplyInfo + { + MessageId = 123, + UserId = 456L, + Username = "testuser", + }; + + // Act + replyInfo.Username = string.Empty; + + // Assert + replyInfo.Username.Should().BeEmpty(); + } +} diff --git a/ChatBot.Tests/Telegram/Commands/TelegramCommandContextTests.cs b/ChatBot.Tests/Telegram/Commands/TelegramCommandContextTests.cs new file mode 100644 index 0000000..c4f7bc0 --- /dev/null +++ b/ChatBot.Tests/Telegram/Commands/TelegramCommandContextTests.cs @@ -0,0 +1,768 @@ +using ChatBot.Services.Telegram.Commands; +using FluentAssertions; + +namespace ChatBot.Tests.Telegram.Commands; + +public class TelegramCommandContextTests +{ + [Fact] + public void Create_ShouldCreateContextWithBasicProperties() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "Hello bot"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Should().NotBeNull(); + context.ChatId.Should().Be(chatId); + context.Username.Should().Be(username); + context.MessageText.Should().Be(messageText); + context.ChatType.Should().Be(chatType); + context.ChatTitle.Should().Be(chatTitle); + context.Arguments.Should().Be("bot"); // "Hello bot" split by space gives ["Hello", "bot"] + context.ReplyInfo.Should().BeNull(); + } + + [Fact] + public void Create_ShouldExtractArgumentsFromMessage() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start arg1 arg2 arg3"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("arg1 arg2 arg3"); + } + + [Fact] + public void Create_ShouldHandleEmptyArguments() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().BeEmpty(); + } + + [Fact] + public void Create_ShouldHandleMessageWithoutCommand() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "Hello bot"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("bot"); // "Hello bot" split by space gives ["Hello", "bot"] + } + + [Fact] + public void Create_ShouldRemoveBotUsernameFromCommand() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start@mybot arg1 arg2"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("arg1 arg2"); + } + + [Fact] + public void Create_ShouldHandleMultipleBotUsernames() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start@bot1@bot2 arg1"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("arg1"); + } + + [Fact] + public void Create_ShouldHandleAtSymbolWithoutBotName() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start@ arg1"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("arg1"); + } + + [Fact] + public void Create_ShouldHandleEmptyBotUsername() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start@ arg1"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("arg1"); + } + + [Fact] + public void Create_ShouldTrimArguments() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start arg1 arg2 "; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("arg1 arg2"); + } + + [Fact] + public void Create_ShouldHandleReplyInfo() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start arg1"; + var chatType = "private"; + var chatTitle = "Test Chat"; + var replyInfo = new ReplyInfo + { + MessageId = 1, + UserId = 123, + Username = "otheruser", + }; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle, + replyInfo + ); + + // Assert + context.ReplyInfo.Should().Be(replyInfo); + context.ReplyInfo!.MessageId.Should().Be(1); + context.ReplyInfo.UserId.Should().Be(123); + context.ReplyInfo.Username.Should().Be("otheruser"); + } + + [Fact] + public void Create_ShouldHandleNullReplyInfo() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start arg1"; + var chatType = "private"; + var chatTitle = "Test Chat"; + ReplyInfo? replyInfo = null; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle, + replyInfo + ); + + // Assert + context.ReplyInfo.Should().BeNull(); + } + + [Fact] + public void Create_ShouldHandleEmptyMessage() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = ""; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.MessageText.Should().BeEmpty(); + context.Arguments.Should().BeEmpty(); + } + + [Fact] + public void Create_ShouldHandleWhitespaceOnlyMessage() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = " "; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.MessageText.Should().Be(" "); + context.Arguments.Should().BeEmpty(); + } + + [Fact] + public void Create_ShouldHandleVeryLongMessage() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start " + new string('A', 10000); + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().HaveLength(10000); + context.Arguments.Should().StartWith("AAAA"); + } + + [Fact] + public void Create_ShouldHandleSpecialCharactersInArguments() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start !@#$%^&*()_+-=[]{}|;':\",./<>?"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("!@#$%^&*()_+-=[]{}|;':\",./<>?"); + } + + [Fact] + public void Create_ShouldHandleUnicodeCharacters() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start привет мир 🌍"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("привет мир 🌍"); + } + + [Fact] + public void Create_ShouldHandleNegativeChatId() + { + // Arrange + var chatId = -12345L; + var username = "testuser"; + var messageText = "/start arg1"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.ChatId.Should().Be(chatId); + } + + [Fact] + public void Create_ShouldHandleZeroChatId() + { + // Arrange + var chatId = 0L; + var username = "testuser"; + var messageText = "/start arg1"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.ChatId.Should().Be(chatId); + } + + [Fact] + public void Create_ShouldHandleEmptyUsername() + { + // Arrange + var chatId = 12345L; + var username = ""; + var messageText = "/start arg1"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Username.Should().BeEmpty(); + } + + [Fact] + public void Create_ShouldHandleEmptyChatType() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start arg1"; + var chatType = ""; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.ChatType.Should().BeEmpty(); + } + + [Fact] + public void Create_ShouldHandleEmptyChatTitle() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start arg1"; + var chatType = "private"; + var chatTitle = ""; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.ChatTitle.Should().BeEmpty(); + } + + [Fact] + public void Create_ShouldHandleVeryLongUsername() + { + // Arrange + var chatId = 12345L; + var username = new string('A', 1000); + var messageText = "/start arg1"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Username.Should().HaveLength(1000); + context.Username.Should().StartWith("AAAA"); + } + + [Fact] + public void Create_ShouldHandleVeryLongChatTitle() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start arg1"; + var chatType = "private"; + var chatTitle = new string('B', 1000); + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.ChatTitle.Should().HaveLength(1000); + context.ChatTitle.Should().StartWith("BBBB"); + } + + [Fact] + public void Create_ShouldHandleMessageWithOnlySpaces() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = " "; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.MessageText.Should().Be(" "); + context.Arguments.Should().BeEmpty(); + } + + [Fact] + public void Create_ShouldHandleMessageWithTabsAndNewlines() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start\targ1\narg2\r\narg3"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().BeEmpty(); // Split by space only, so tabs and newlines are not split + } + + [Fact] + public void Create_ShouldHandleMessageWithMultipleSpacesInArguments() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start arg1 arg2 arg3"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("arg1 arg2 arg3"); // Trim() removes leading spaces + } + + [Fact] + public void Create_ShouldHandleMessageWithOnlyCommandAndSpaces() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start "; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().BeEmpty(); + } + + [Fact] + public void Create_ShouldHandleMessageWithCommandAndOnlySpacesAsArguments() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start "; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().BeEmpty(); + } + + [Fact] + public void Create_ShouldHandleMessageWithCommandAndMixedWhitespace() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start\t \n arg1 \t arg2 \r\n "; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("arg1 \t arg2"); // Split by space and trim removes leading/trailing spaces + } + + [Fact] + public void Create_ShouldHandleMessageWithVeryLongBotUsername() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start@verylongbotname arg1"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("arg1"); + } + + [Fact] + public void Create_ShouldHandleMessageWithSpecialCharactersInBotUsername() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start@bot_name-123 arg1"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("arg1"); + } + + [Fact] + public void Create_ShouldHandleMessageWithUnicodeInBotUsername() + { + // Arrange + var chatId = 12345L; + var username = "testuser"; + var messageText = "/start@бот123 arg1"; + var chatType = "private"; + var chatTitle = "Test Chat"; + + // Act + var context = TelegramCommandContext.Create( + chatId, + username, + messageText, + chatType, + chatTitle + ); + + // Assert + context.Arguments.Should().Be("arg1"); + } +} diff --git a/ChatBot/Services/Telegram/Interfaces/ITelegramMessageSenderWrapper.cs b/ChatBot/Services/Telegram/Interfaces/ITelegramMessageSenderWrapper.cs new file mode 100644 index 0000000..6d32c76 --- /dev/null +++ b/ChatBot/Services/Telegram/Interfaces/ITelegramMessageSenderWrapper.cs @@ -0,0 +1,20 @@ +using Telegram.Bot.Types; + +namespace ChatBot.Services.Telegram.Interfaces +{ + /// + /// Wrapper interface for Telegram Bot Client SendMessage functionality to enable mocking + /// + public interface ITelegramMessageSenderWrapper + { + /// + /// Sends a message to a chat + /// + Task SendMessageAsync( + long chatId, + string text, + int replyToMessageId, + CancellationToken cancellationToken = default + ); + } +} diff --git a/ChatBot/Services/Telegram/Services/TelegramMessageSender.cs b/ChatBot/Services/Telegram/Services/TelegramMessageSender.cs index 8ff9eb7..c32d2ba 100644 --- a/ChatBot/Services/Telegram/Services/TelegramMessageSender.cs +++ b/ChatBot/Services/Telegram/Services/TelegramMessageSender.cs @@ -10,10 +10,15 @@ namespace ChatBot.Services.Telegram.Services public class TelegramMessageSender : ITelegramMessageSender { private readonly ILogger _logger; + private readonly ITelegramMessageSenderWrapper _messageSenderWrapper; - public TelegramMessageSender(ILogger logger) + public TelegramMessageSender( + ILogger logger, + ITelegramMessageSenderWrapper messageSenderWrapper + ) { _logger = logger; + _messageSenderWrapper = messageSenderWrapper; } /// @@ -28,14 +33,20 @@ namespace ChatBot.Services.Telegram.Services int maxRetries = 3 ) { + // Ensure maxRetries is at least 1 + if (maxRetries < 1) + { + maxRetries = 3; + } + for (int attempt = 1; attempt <= maxRetries; attempt++) { try { - await botClient.SendMessage( + await _messageSenderWrapper.SendMessageAsync( chatId: chatId, text: text, - replyParameters: replyToMessageId, + replyToMessageId: replyToMessageId, cancellationToken: cancellationToken ); return; // Success, exit the method diff --git a/ChatBot/Services/Telegram/Services/TelegramMessageSenderWrapper.cs b/ChatBot/Services/Telegram/Services/TelegramMessageSenderWrapper.cs new file mode 100644 index 0000000..dc8641d --- /dev/null +++ b/ChatBot/Services/Telegram/Services/TelegramMessageSenderWrapper.cs @@ -0,0 +1,34 @@ +using ChatBot.Services.Telegram.Interfaces; +using Telegram.Bot; +using Telegram.Bot.Types; + +namespace ChatBot.Services.Telegram.Services +{ + /// + /// Wrapper implementation for Telegram Bot Client SendMessage functionality + /// + public class TelegramMessageSenderWrapper : ITelegramMessageSenderWrapper + { + private readonly ITelegramBotClient _botClient; + + public TelegramMessageSenderWrapper(ITelegramBotClient botClient) + { + _botClient = botClient; + } + + public async Task SendMessageAsync( + long chatId, + string text, + int replyToMessageId, + CancellationToken cancellationToken = default + ) + { + return await _botClient.SendMessage( + chatId: chatId, + text: text, + replyParameters: replyToMessageId, + cancellationToken: cancellationToken + ); + } + } +} diff --git a/test_coverage_report.md b/test_coverage_report.md index b7e27ca..342b642 100644 --- a/test_coverage_report.md +++ b/test_coverage_report.md @@ -79,15 +79,15 @@ ### 4. Telegram команды (дополнительные тесты) - [x] `TelegramCommandBase` - тесты базового класса команд - [x] `TelegramCommandProcessor` - тесты обработки команд -- [ ] `TelegramCommandContext` - тесты контекста команд -- [ ] `ReplyInfo` - тесты информации о ответах -- [ ] `CommandAttribute` - тесты атрибутов команд +- [x] `TelegramCommandContext` - тесты контекста команд +- [x] `ReplyInfo` - тесты информации о ответах +- [x] `CommandAttribute` - тесты атрибутов команд ### 5. Telegram сервисы (дополнительные тесты) -- [ ] `TelegramBotClientWrapper` - тесты обертки клиента -- [ ] `TelegramMessageHandler` - тесты различных типов сообщений -- [ ] `TelegramErrorHandler` - тесты различных типов ошибок -- [ ] `TelegramMessageSender` - тесты отправки различных типов сообщений +- [x] `TelegramBotClientWrapper` - тесты обертки клиента +- [x] `TelegramMessageHandler` - тесты различных типов сообщений +- [x] `TelegramErrorHandler` - тесты различных типов ошибок +- [x] `TelegramMessageSender` - тесты отправки различных типов сообщений ### 6. Интерфейсы - [ ] `IAIService` - тесты интерфейса