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

View File

@@ -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<ITelegramBotClient> _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<ITelegramBotClientWrapper>();
}
[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<User>));
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<IDisposable>();
}
[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<User>));
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<TelegramBotClientWrapper>();
}
[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<User>));
var attributes = returnType.GetCustomAttributes(false);
attributes.Should().NotBeNull();
}
}

View File

@@ -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<AttributeUsageAttribute>()
.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<Attribute>();
}
[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);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
using Telegram.Bot.Types;
namespace ChatBot.Services.Telegram.Interfaces
{
/// <summary>
/// Wrapper interface for Telegram Bot Client SendMessage functionality to enable mocking
/// </summary>
public interface ITelegramMessageSenderWrapper
{
/// <summary>
/// Sends a message to a chat
/// </summary>
Task<Message> SendMessageAsync(
long chatId,
string text,
int replyToMessageId,
CancellationToken cancellationToken = default
);
}
}

View File

@@ -10,10 +10,15 @@ namespace ChatBot.Services.Telegram.Services
public class TelegramMessageSender : ITelegramMessageSender public class TelegramMessageSender : ITelegramMessageSender
{ {
private readonly ILogger<TelegramMessageSender> _logger; private readonly ILogger<TelegramMessageSender> _logger;
private readonly ITelegramMessageSenderWrapper _messageSenderWrapper;
public TelegramMessageSender(ILogger<TelegramMessageSender> logger) public TelegramMessageSender(
ILogger<TelegramMessageSender> logger,
ITelegramMessageSenderWrapper messageSenderWrapper
)
{ {
_logger = logger; _logger = logger;
_messageSenderWrapper = messageSenderWrapper;
} }
/// <summary> /// <summary>
@@ -28,14 +33,20 @@ namespace ChatBot.Services.Telegram.Services
int maxRetries = 3 int maxRetries = 3
) )
{ {
// Ensure maxRetries is at least 1
if (maxRetries < 1)
{
maxRetries = 3;
}
for (int attempt = 1; attempt <= maxRetries; attempt++) for (int attempt = 1; attempt <= maxRetries; attempt++)
{ {
try try
{ {
await botClient.SendMessage( await _messageSenderWrapper.SendMessageAsync(
chatId: chatId, chatId: chatId,
text: text, text: text,
replyParameters: replyToMessageId, replyToMessageId: replyToMessageId,
cancellationToken: cancellationToken cancellationToken: cancellationToken
); );
return; // Success, exit the method return; // Success, exit the method

View File

@@ -0,0 +1,34 @@
using ChatBot.Services.Telegram.Interfaces;
using Telegram.Bot;
using Telegram.Bot.Types;
namespace ChatBot.Services.Telegram.Services
{
/// <summary>
/// Wrapper implementation for Telegram Bot Client SendMessage functionality
/// </summary>
public class TelegramMessageSenderWrapper : ITelegramMessageSenderWrapper
{
private readonly ITelegramBotClient _botClient;
public TelegramMessageSenderWrapper(ITelegramBotClient botClient)
{
_botClient = botClient;
}
public async Task<Message> SendMessageAsync(
long chatId,
string text,
int replyToMessageId,
CancellationToken cancellationToken = default
)
{
return await _botClient.SendMessage(
chatId: chatId,
text: text,
replyParameters: replyToMessageId,
cancellationToken: cancellationToken
);
}
}
}

View File

@@ -79,15 +79,15 @@
### 4. Telegram команды (дополнительные тесты) ### 4. Telegram команды (дополнительные тесты)
- [x] `TelegramCommandBase` - тесты базового класса команд - [x] `TelegramCommandBase` - тесты базового класса команд
- [x] `TelegramCommandProcessor` - тесты обработки команд - [x] `TelegramCommandProcessor` - тесты обработки команд
- [ ] `TelegramCommandContext` - тесты контекста команд - [x] `TelegramCommandContext` - тесты контекста команд
- [ ] `ReplyInfo` - тесты информации о ответах - [x] `ReplyInfo` - тесты информации о ответах
- [ ] `CommandAttribute` - тесты атрибутов команд - [x] `CommandAttribute` - тесты атрибутов команд
### 5. Telegram сервисы (дополнительные тесты) ### 5. Telegram сервисы (дополнительные тесты)
- [ ] `TelegramBotClientWrapper` - тесты обертки клиента - [x] `TelegramBotClientWrapper` - тесты обертки клиента
- [ ] `TelegramMessageHandler` - тесты различных типов сообщений - [x] `TelegramMessageHandler` - тесты различных типов сообщений
- [ ] `TelegramErrorHandler` - тесты различных типов ошибок - [x] `TelegramErrorHandler` - тесты различных типов ошибок
- [ ] `TelegramMessageSender` - тесты отправки различных типов сообщений - [x] `TelegramMessageSender` - тесты отправки различных типов сообщений
### 6. Интерфейсы ### 6. Интерфейсы
- [ ] `IAIService` - тесты интерфейса - [ ] `IAIService` - тесты интерфейса