From e011bb667fc126caa57ed5ae83ede5732afa5cb8 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Mon, 20 Oct 2025 08:36:57 +0300 Subject: [PATCH] Add more test --- .../Interfaces/IChatSessionRepositoryTests.cs | 473 ++++++++++++++++++ .../Services/Interfaces/IAIServiceTests.cs | 327 ++++++++++++ .../IHistoryCompressionServiceTests.cs | 367 ++++++++++++++ .../Services/Interfaces/IOllamaClientTests.cs | 380 ++++++++++++++ .../Interfaces/ISessionStorageTests.cs | 295 +++++++++++ .../ITelegramBotClientWrapperTests.cs | 272 ++++++++++ test_coverage_report.md | 12 +- 7 files changed, 2120 insertions(+), 6 deletions(-) create mode 100644 ChatBot.Tests/Data/Interfaces/IChatSessionRepositoryTests.cs create mode 100644 ChatBot.Tests/Services/Interfaces/IAIServiceTests.cs create mode 100644 ChatBot.Tests/Services/Interfaces/IHistoryCompressionServiceTests.cs create mode 100644 ChatBot.Tests/Services/Interfaces/IOllamaClientTests.cs create mode 100644 ChatBot.Tests/Services/Interfaces/ISessionStorageTests.cs create mode 100644 ChatBot.Tests/Services/Interfaces/ITelegramBotClientWrapperTests.cs diff --git a/ChatBot.Tests/Data/Interfaces/IChatSessionRepositoryTests.cs b/ChatBot.Tests/Data/Interfaces/IChatSessionRepositoryTests.cs new file mode 100644 index 0000000..9b9328a --- /dev/null +++ b/ChatBot.Tests/Data/Interfaces/IChatSessionRepositoryTests.cs @@ -0,0 +1,473 @@ +using ChatBot.Data.Interfaces; +using ChatBot.Data.Repositories; +using ChatBot.Models.Entities; +using ChatBot.Tests.TestUtilities; +using FluentAssertions; +using Moq; + +namespace ChatBot.Tests.Data.Interfaces; + +public class IChatSessionRepositoryTests : UnitTestBase +{ + [Fact] + public void IChatSessionRepository_ShouldHaveCorrectMethodSignatures() + { + // Arrange & Act + var interfaceType = typeof(IChatSessionRepository); + var methods = interfaceType.GetMethods(); + + // Assert + methods.Should().HaveCount(11); + + // GetOrCreateAsync method + var getOrCreateAsyncMethod = methods.FirstOrDefault(m => m.Name == "GetOrCreateAsync"); + getOrCreateAsyncMethod.Should().NotBeNull(); + getOrCreateAsyncMethod!.ReturnType.Should().Be(typeof(Task)); + getOrCreateAsyncMethod.GetParameters().Should().HaveCount(3); + getOrCreateAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(long)); + getOrCreateAsyncMethod.GetParameters()[1].ParameterType.Should().Be(typeof(string)); + getOrCreateAsyncMethod.GetParameters()[2].ParameterType.Should().Be(typeof(string)); + + // GetByChatIdAsync method + var getByChatIdAsyncMethod = methods.FirstOrDefault(m => m.Name == "GetByChatIdAsync"); + getByChatIdAsyncMethod.Should().NotBeNull(); + getByChatIdAsyncMethod!.ReturnType.Should().Be(typeof(Task)); + getByChatIdAsyncMethod.GetParameters().Should().HaveCount(1); + getByChatIdAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(long)); + + // GetBySessionIdAsync method + var getBySessionIdAsyncMethod = methods.FirstOrDefault(m => + m.Name == "GetBySessionIdAsync" + ); + getBySessionIdAsyncMethod.Should().NotBeNull(); + getBySessionIdAsyncMethod!.ReturnType.Should().Be(typeof(Task)); + getBySessionIdAsyncMethod.GetParameters().Should().HaveCount(1); + getBySessionIdAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(string)); + + // UpdateAsync method + var updateAsyncMethod = methods.FirstOrDefault(m => m.Name == "UpdateAsync"); + updateAsyncMethod.Should().NotBeNull(); + updateAsyncMethod!.ReturnType.Should().Be(typeof(Task)); + updateAsyncMethod.GetParameters().Should().HaveCount(1); + updateAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(ChatSessionEntity)); + + // DeleteAsync method + var deleteAsyncMethod = methods.FirstOrDefault(m => m.Name == "DeleteAsync"); + deleteAsyncMethod.Should().NotBeNull(); + deleteAsyncMethod!.ReturnType.Should().Be(typeof(Task)); + deleteAsyncMethod.GetParameters().Should().HaveCount(1); + deleteAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(long)); + + // GetMessagesAsync method + var getMessagesAsyncMethod = methods.FirstOrDefault(m => m.Name == "GetMessagesAsync"); + getMessagesAsyncMethod.Should().NotBeNull(); + getMessagesAsyncMethod!.ReturnType.Should().Be(typeof(Task>)); + getMessagesAsyncMethod.GetParameters().Should().HaveCount(1); + getMessagesAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(int)); + + // AddMessageAsync method + var addMessageAsyncMethod = methods.FirstOrDefault(m => m.Name == "AddMessageAsync"); + addMessageAsyncMethod.Should().NotBeNull(); + addMessageAsyncMethod!.ReturnType.Should().Be(typeof(Task)); + addMessageAsyncMethod.GetParameters().Should().HaveCount(4); + addMessageAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(int)); + addMessageAsyncMethod.GetParameters()[1].ParameterType.Should().Be(typeof(string)); + addMessageAsyncMethod.GetParameters()[2].ParameterType.Should().Be(typeof(string)); + addMessageAsyncMethod.GetParameters()[3].ParameterType.Should().Be(typeof(int)); + + // ClearMessagesAsync method + var clearMessagesAsyncMethod = methods.FirstOrDefault(m => m.Name == "ClearMessagesAsync"); + clearMessagesAsyncMethod.Should().NotBeNull(); + clearMessagesAsyncMethod!.ReturnType.Should().Be(typeof(Task)); + clearMessagesAsyncMethod.GetParameters().Should().HaveCount(1); + clearMessagesAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(int)); + + // GetActiveSessionsCountAsync method + var getActiveSessionsCountAsyncMethod = methods.FirstOrDefault(m => + m.Name == "GetActiveSessionsCountAsync" + ); + getActiveSessionsCountAsyncMethod.Should().NotBeNull(); + getActiveSessionsCountAsyncMethod!.ReturnType.Should().Be(typeof(Task)); + getActiveSessionsCountAsyncMethod.GetParameters().Should().BeEmpty(); + + // CleanupOldSessionsAsync method + var cleanupOldSessionsAsyncMethod = methods.FirstOrDefault(m => + m.Name == "CleanupOldSessionsAsync" + ); + cleanupOldSessionsAsyncMethod.Should().NotBeNull(); + cleanupOldSessionsAsyncMethod!.ReturnType.Should().Be(typeof(Task)); + cleanupOldSessionsAsyncMethod.GetParameters().Should().HaveCount(1); + cleanupOldSessionsAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(int)); + + // GetSessionsForCleanupAsync method + var getSessionsForCleanupAsyncMethod = methods.FirstOrDefault(m => + m.Name == "GetSessionsForCleanupAsync" + ); + getSessionsForCleanupAsyncMethod.Should().NotBeNull(); + getSessionsForCleanupAsyncMethod! + .ReturnType.Should() + .Be(typeof(Task>)); + getSessionsForCleanupAsyncMethod.GetParameters().Should().HaveCount(1); + getSessionsForCleanupAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(int)); + } + + [Fact] + public void IChatSessionRepository_ShouldBeImplementedByChatSessionRepository() + { + // Arrange & Act + var chatSessionRepositoryType = typeof(ChatSessionRepository); + var interfaceType = typeof(IChatSessionRepository); + + // Assert + interfaceType.IsAssignableFrom(chatSessionRepositoryType).Should().BeTrue(); + } + + [Fact] + public async Task IChatSessionRepository_GetOrCreateAsync_ShouldReturnChatSessionEntity() + { + // Arrange + var mock = new Mock(); + var chatId = 12345L; + var chatType = "private"; + var chatTitle = "Test Chat"; + var expectedSession = TestDataBuilder.Mocks.CreateChatSessionEntity(); + + mock.Setup(x => + x.GetOrCreateAsync(It.IsAny(), It.IsAny(), It.IsAny()) + ) + .ReturnsAsync(expectedSession); + + // Act + var result = await mock.Object.GetOrCreateAsync(chatId, chatType, chatTitle); + + // Assert + result.Should().Be(expectedSession); + mock.Verify(x => x.GetOrCreateAsync(chatId, chatType, chatTitle), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_GetOrCreateAsync_ShouldUseDefaultValues() + { + // Arrange + var mock = new Mock(); + var chatId = 12345L; + var expectedSession = TestDataBuilder.Mocks.CreateChatSessionEntity(); + + mock.Setup(x => + x.GetOrCreateAsync(It.IsAny(), It.IsAny(), It.IsAny()) + ) + .ReturnsAsync(expectedSession); + + // Act + var result = await mock.Object.GetOrCreateAsync(chatId); + + // Assert + result.Should().Be(expectedSession); + mock.Verify(x => x.GetOrCreateAsync(chatId, "private", ""), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_GetByChatIdAsync_ShouldReturnChatSessionEntityOrNull() + { + // Arrange + var mock = new Mock(); + var chatId = 12345L; + var expectedSession = TestDataBuilder.Mocks.CreateChatSessionEntity(); + + mock.Setup(x => x.GetByChatIdAsync(It.IsAny())).ReturnsAsync(expectedSession); + + // Act + var result = await mock.Object.GetByChatIdAsync(chatId); + + // Assert + result.Should().Be(expectedSession); + mock.Verify(x => x.GetByChatIdAsync(chatId), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_GetByChatIdAsync_ShouldReturnNullWhenNotFound() + { + // Arrange + var mock = new Mock(); + var chatId = 12345L; + + mock.Setup(x => x.GetByChatIdAsync(It.IsAny())) + .ReturnsAsync((ChatSessionEntity?)null); + + // Act + var result = await mock.Object.GetByChatIdAsync(chatId); + + // Assert + result.Should().BeNull(); + mock.Verify(x => x.GetByChatIdAsync(chatId), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_GetBySessionIdAsync_ShouldReturnChatSessionEntityOrNull() + { + // Arrange + var mock = new Mock(); + var sessionId = "test-session-id"; + var expectedSession = TestDataBuilder.Mocks.CreateChatSessionEntity(); + + mock.Setup(x => x.GetBySessionIdAsync(It.IsAny())).ReturnsAsync(expectedSession); + + // Act + var result = await mock.Object.GetBySessionIdAsync(sessionId); + + // Assert + result.Should().Be(expectedSession); + mock.Verify(x => x.GetBySessionIdAsync(sessionId), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_UpdateAsync_ShouldReturnUpdatedChatSessionEntity() + { + // Arrange + var mock = new Mock(); + var session = TestDataBuilder.Mocks.CreateChatSessionEntity(); + var expectedUpdatedSession = TestDataBuilder.Mocks.CreateChatSessionEntity(); + + mock.Setup(x => x.UpdateAsync(It.IsAny())) + .ReturnsAsync(expectedUpdatedSession); + + // Act + var result = await mock.Object.UpdateAsync(session); + + // Assert + result.Should().Be(expectedUpdatedSession); + mock.Verify(x => x.UpdateAsync(session), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_DeleteAsync_ShouldReturnBoolean() + { + // Arrange + var mock = new Mock(); + var chatId = 12345L; + var expectedResult = true; + + mock.Setup(x => x.DeleteAsync(It.IsAny())).ReturnsAsync(expectedResult); + + // Act + var result = await mock.Object.DeleteAsync(chatId); + + // Assert + result.Should().Be(expectedResult); + mock.Verify(x => x.DeleteAsync(chatId), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_GetMessagesAsync_ShouldReturnListOfMessages() + { + // Arrange + var mock = new Mock(); + var sessionId = 1; + var expectedMessages = new List + { + TestDataBuilder.Mocks.CreateChatMessageEntity(), + TestDataBuilder.Mocks.CreateChatMessageEntity(), + }; + + mock.Setup(x => x.GetMessagesAsync(It.IsAny())).ReturnsAsync(expectedMessages); + + // Act + var result = await mock.Object.GetMessagesAsync(sessionId); + + // Assert + result.Should().BeEquivalentTo(expectedMessages); + mock.Verify(x => x.GetMessagesAsync(sessionId), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_AddMessageAsync_ShouldReturnChatMessageEntity() + { + // Arrange + var mock = new Mock(); + var sessionId = 1; + var content = "Test message"; + var role = "user"; + var messageOrder = 1; + var expectedMessage = TestDataBuilder.Mocks.CreateChatMessageEntity(); + + mock.Setup(x => + x.AddMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedMessage); + + // Act + var result = await mock.Object.AddMessageAsync(sessionId, content, role, messageOrder); + + // Assert + result.Should().Be(expectedMessage); + mock.Verify(x => x.AddMessageAsync(sessionId, content, role, messageOrder), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_ClearMessagesAsync_ShouldReturnTask() + { + // Arrange + var mock = new Mock(); + var sessionId = 1; + + mock.Setup(x => x.ClearMessagesAsync(It.IsAny())).Returns(Task.CompletedTask); + + // Act + await mock.Object.ClearMessagesAsync(sessionId); + + // Assert + mock.Verify(x => x.ClearMessagesAsync(sessionId), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_GetActiveSessionsCountAsync_ShouldReturnInt() + { + // Arrange + var mock = new Mock(); + var expectedCount = 5; + + mock.Setup(x => x.GetActiveSessionsCountAsync()).ReturnsAsync(expectedCount); + + // Act + var result = await mock.Object.GetActiveSessionsCountAsync(); + + // Assert + result.Should().Be(expectedCount); + mock.Verify(x => x.GetActiveSessionsCountAsync(), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_CleanupOldSessionsAsync_ShouldReturnInt() + { + // Arrange + var mock = new Mock(); + var hoursOld = 24; + var expectedCleanedCount = 3; + + mock.Setup(x => x.CleanupOldSessionsAsync(It.IsAny())) + .ReturnsAsync(expectedCleanedCount); + + // Act + var result = await mock.Object.CleanupOldSessionsAsync(hoursOld); + + // Assert + result.Should().Be(expectedCleanedCount); + mock.Verify(x => x.CleanupOldSessionsAsync(hoursOld), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_CleanupOldSessionsAsync_ShouldUseDefaultValue() + { + // Arrange + var mock = new Mock(); + var expectedCleanedCount = 2; + + mock.Setup(x => x.CleanupOldSessionsAsync(It.IsAny())) + .ReturnsAsync(expectedCleanedCount); + + // Act + var result = await mock.Object.CleanupOldSessionsAsync(); + + // Assert + result.Should().Be(expectedCleanedCount); + mock.Verify(x => x.CleanupOldSessionsAsync(24), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_GetSessionsForCleanupAsync_ShouldReturnListOfSessions() + { + // Arrange + var mock = new Mock(); + var hoursOld = 24; + var expectedSessions = new List + { + TestDataBuilder.Mocks.CreateChatSessionEntity(), + TestDataBuilder.Mocks.CreateChatSessionEntity(), + }; + + mock.Setup(x => x.GetSessionsForCleanupAsync(It.IsAny())) + .ReturnsAsync(expectedSessions); + + // Act + var result = await mock.Object.GetSessionsForCleanupAsync(hoursOld); + + // Assert + result.Should().BeEquivalentTo(expectedSessions); + mock.Verify(x => x.GetSessionsForCleanupAsync(hoursOld), Times.Once); + } + + [Fact] + public async Task IChatSessionRepository_GetSessionsForCleanupAsync_ShouldUseDefaultValue() + { + // Arrange + var mock = new Mock(); + var expectedSessions = new List(); + + mock.Setup(x => x.GetSessionsForCleanupAsync(It.IsAny())) + .ReturnsAsync(expectedSessions); + + // Act + var result = await mock.Object.GetSessionsForCleanupAsync(); + + // Assert + result.Should().BeEquivalentTo(expectedSessions); + mock.Verify(x => x.GetSessionsForCleanupAsync(24), Times.Once); + } + + [Fact] + public void IChatSessionRepository_ShouldBePublicInterface() + { + // Arrange & Act + var interfaceType = typeof(IChatSessionRepository); + + // Assert + interfaceType.IsPublic.Should().BeTrue(); + interfaceType.IsInterface.Should().BeTrue(); + } + + [Fact] + public void IChatSessionRepository_ShouldHaveCorrectNamespace() + { + // Arrange & Act + var interfaceType = typeof(IChatSessionRepository); + + // Assert + interfaceType.Namespace.Should().Be("ChatBot.Data.Interfaces"); + } + + [Fact] + public void IChatSessionRepository_ShouldHaveCorrectGenericConstraints() + { + // Arrange & Act + var interfaceType = typeof(IChatSessionRepository); + var methods = interfaceType.GetMethods(); + + // Assert + // All methods should be public + methods.All(m => m.IsPublic).Should().BeTrue(); + + // GetOrCreateAsync should have default parameters + var getOrCreateAsyncMethod = methods.First(m => m.Name == "GetOrCreateAsync"); + getOrCreateAsyncMethod.GetParameters()[1].HasDefaultValue.Should().BeTrue(); + getOrCreateAsyncMethod.GetParameters()[1].DefaultValue.Should().Be("private"); + getOrCreateAsyncMethod.GetParameters()[2].HasDefaultValue.Should().BeTrue(); + getOrCreateAsyncMethod.GetParameters()[2].DefaultValue.Should().Be(""); + + // CleanupOldSessionsAsync should have default parameter + var cleanupOldSessionsAsyncMethod = methods.First(m => m.Name == "CleanupOldSessionsAsync"); + cleanupOldSessionsAsyncMethod.GetParameters()[0].HasDefaultValue.Should().BeTrue(); + cleanupOldSessionsAsyncMethod.GetParameters()[0].DefaultValue.Should().Be(24); + + // GetSessionsForCleanupAsync should have default parameter + var getSessionsForCleanupAsyncMethod = methods.First(m => + m.Name == "GetSessionsForCleanupAsync" + ); + getSessionsForCleanupAsyncMethod.GetParameters()[0].HasDefaultValue.Should().BeTrue(); + getSessionsForCleanupAsyncMethod.GetParameters()[0].DefaultValue.Should().Be(24); + } +} diff --git a/ChatBot.Tests/Services/Interfaces/IAIServiceTests.cs b/ChatBot.Tests/Services/Interfaces/IAIServiceTests.cs new file mode 100644 index 0000000..e0cd10e --- /dev/null +++ b/ChatBot.Tests/Services/Interfaces/IAIServiceTests.cs @@ -0,0 +1,327 @@ +using ChatBot.Models.Dto; +using ChatBot.Services.Interfaces; +using ChatBot.Tests.TestUtilities; +using FluentAssertions; +using Moq; + +namespace ChatBot.Tests.Services.Interfaces; + +public class IAIServiceTests : UnitTestBase +{ + [Fact] + public void IAIService_ShouldHaveCorrectMethodSignatures() + { + // Arrange & Act + var interfaceType = typeof(IAIService); + var methods = interfaceType.GetMethods(); + + // Assert + methods.Should().HaveCount(2); + + var generateChatCompletionMethod = methods.FirstOrDefault(m => + m.Name == "GenerateChatCompletionAsync" + ); + generateChatCompletionMethod.Should().NotBeNull(); + generateChatCompletionMethod!.ReturnType.Should().Be(typeof(Task)); + generateChatCompletionMethod.GetParameters().Should().HaveCount(2); + generateChatCompletionMethod + .GetParameters()[0] + .ParameterType.Should() + .Be(typeof(List)); + generateChatCompletionMethod + .GetParameters()[1] + .ParameterType.Should() + .Be(typeof(CancellationToken)); + + var generateChatCompletionWithCompressionMethod = methods.FirstOrDefault(m => + m.Name == "GenerateChatCompletionWithCompressionAsync" + ); + generateChatCompletionWithCompressionMethod.Should().NotBeNull(); + generateChatCompletionWithCompressionMethod!.ReturnType.Should().Be(typeof(Task)); + generateChatCompletionWithCompressionMethod.GetParameters().Should().HaveCount(2); + generateChatCompletionWithCompressionMethod + .GetParameters()[0] + .ParameterType.Should() + .Be(typeof(List)); + generateChatCompletionWithCompressionMethod + .GetParameters()[1] + .ParameterType.Should() + .Be(typeof(CancellationToken)); + } + + [Fact] + public void IAIService_ShouldBeImplementedByAIService() + { + // Arrange & Act + var aiServiceType = typeof(ChatBot.Services.AIService); + var interfaceType = typeof(IAIService); + + // Assert + interfaceType.IsAssignableFrom(aiServiceType).Should().BeTrue(); + } + + [Fact] + public async Task IAIService_GenerateChatCompletionAsync_ShouldReturnString() + { + // Arrange + var mock = new Mock(); + var messages = new List + { + new() { Role = "user", Content = "Test message" }, + }; + var cancellationToken = CancellationToken.None; + var expectedResponse = "Test response"; + + mock.Setup(x => + x.GenerateChatCompletionAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedResponse); + + // Act + var result = await mock.Object.GenerateChatCompletionAsync(messages, cancellationToken); + + // Assert + result.Should().Be(expectedResponse); + mock.Verify(x => x.GenerateChatCompletionAsync(messages, cancellationToken), Times.Once); + } + + [Fact] + public async Task IAIService_GenerateChatCompletionWithCompressionAsync_ShouldReturnString() + { + // Arrange + var mock = new Mock(); + var messages = new List + { + new() { Role = "user", Content = "Test message" }, + }; + var cancellationToken = CancellationToken.None; + var expectedResponse = "Test response with compression"; + + mock.Setup(x => + x.GenerateChatCompletionWithCompressionAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedResponse); + + // Act + var result = await mock.Object.GenerateChatCompletionWithCompressionAsync( + messages, + cancellationToken + ); + + // Assert + result.Should().Be(expectedResponse); + mock.Verify( + x => x.GenerateChatCompletionWithCompressionAsync(messages, cancellationToken), + Times.Once + ); + } + + [Fact] + public async Task IAIService_GenerateChatCompletionAsync_ShouldHandleEmptyMessages() + { + // Arrange + var mock = new Mock(); + var messages = new List(); + var cancellationToken = CancellationToken.None; + var expectedResponse = "Empty response"; + + mock.Setup(x => + x.GenerateChatCompletionAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedResponse); + + // Act + var result = await mock.Object.GenerateChatCompletionAsync(messages, cancellationToken); + + // Assert + result.Should().Be(expectedResponse); + mock.Verify(x => x.GenerateChatCompletionAsync(messages, cancellationToken), Times.Once); + } + + [Fact] + public async Task IAIService_GenerateChatCompletionWithCompressionAsync_ShouldHandleEmptyMessages() + { + // Arrange + var mock = new Mock(); + var messages = new List(); + var cancellationToken = CancellationToken.None; + var expectedResponse = "Empty response with compression"; + + mock.Setup(x => + x.GenerateChatCompletionWithCompressionAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedResponse); + + // Act + var result = await mock.Object.GenerateChatCompletionWithCompressionAsync( + messages, + cancellationToken + ); + + // Assert + result.Should().Be(expectedResponse); + mock.Verify( + x => x.GenerateChatCompletionWithCompressionAsync(messages, cancellationToken), + Times.Once + ); + } + + [Fact] + public async Task IAIService_GenerateChatCompletionAsync_ShouldHandleCancellationToken() + { + // Arrange + var mock = new Mock(); + var messages = new List + { + new() { Role = "user", Content = "Test message" }, + }; + var cancellationToken = new CancellationToken(true); // Cancelled token + var expectedResponse = "Cancelled response"; + + mock.Setup(x => + x.GenerateChatCompletionAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedResponse); + + // Act + var result = await mock.Object.GenerateChatCompletionAsync(messages, cancellationToken); + + // Assert + result.Should().Be(expectedResponse); + mock.Verify(x => x.GenerateChatCompletionAsync(messages, cancellationToken), Times.Once); + } + + [Fact] + public async Task IAIService_GenerateChatCompletionWithCompressionAsync_ShouldHandleCancellationToken() + { + // Arrange + var mock = new Mock(); + var messages = new List + { + new() { Role = "user", Content = "Test message" }, + }; + var cancellationToken = new CancellationToken(true); // Cancelled token + var expectedResponse = "Cancelled response with compression"; + + mock.Setup(x => + x.GenerateChatCompletionWithCompressionAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedResponse); + + // Act + var result = await mock.Object.GenerateChatCompletionWithCompressionAsync( + messages, + cancellationToken + ); + + // Assert + result.Should().Be(expectedResponse); + mock.Verify( + x => x.GenerateChatCompletionWithCompressionAsync(messages, cancellationToken), + Times.Once + ); + } + + [Fact] + public async Task IAIService_GenerateChatCompletionAsync_ShouldHandleLargeMessageList() + { + // Arrange + var mock = new Mock(); + var messages = new List(); + for (int i = 0; i < 100; i++) + { + messages.Add(new() { Role = "user", Content = $"Message {i}" }); + } + var cancellationToken = CancellationToken.None; + var expectedResponse = "Large response"; + + mock.Setup(x => + x.GenerateChatCompletionAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedResponse); + + // Act + var result = await mock.Object.GenerateChatCompletionAsync(messages, cancellationToken); + + // Assert + result.Should().Be(expectedResponse); + mock.Verify(x => x.GenerateChatCompletionAsync(messages, cancellationToken), Times.Once); + } + + [Fact] + public async Task IAIService_GenerateChatCompletionWithCompressionAsync_ShouldHandleLargeMessageList() + { + // Arrange + var mock = new Mock(); + var messages = new List(); + for (int i = 0; i < 100; i++) + { + messages.Add(new() { Role = "user", Content = $"Message {i}" }); + } + var cancellationToken = CancellationToken.None; + var expectedResponse = "Large response with compression"; + + mock.Setup(x => + x.GenerateChatCompletionWithCompressionAsync( + It.IsAny>(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedResponse); + + // Act + var result = await mock.Object.GenerateChatCompletionWithCompressionAsync( + messages, + cancellationToken + ); + + // Assert + result.Should().Be(expectedResponse); + mock.Verify( + x => x.GenerateChatCompletionWithCompressionAsync(messages, cancellationToken), + Times.Once + ); + } + + [Fact] + public void IAIService_ShouldBePublicInterface() + { + // Arrange & Act + var interfaceType = typeof(IAIService); + + // Assert + interfaceType.IsPublic.Should().BeTrue(); + interfaceType.IsInterface.Should().BeTrue(); + } + + [Fact] + public void IAIService_ShouldHaveCorrectNamespace() + { + // Arrange & Act + var interfaceType = typeof(IAIService); + + // Assert + interfaceType.Namespace.Should().Be("ChatBot.Services.Interfaces"); + } +} diff --git a/ChatBot.Tests/Services/Interfaces/IHistoryCompressionServiceTests.cs b/ChatBot.Tests/Services/Interfaces/IHistoryCompressionServiceTests.cs new file mode 100644 index 0000000..85175d0 --- /dev/null +++ b/ChatBot.Tests/Services/Interfaces/IHistoryCompressionServiceTests.cs @@ -0,0 +1,367 @@ +using ChatBot.Models.Dto; +using ChatBot.Services; +using ChatBot.Services.Interfaces; +using ChatBot.Tests.TestUtilities; +using FluentAssertions; +using Moq; + +namespace ChatBot.Tests.Services.Interfaces; + +public class IHistoryCompressionServiceTests : UnitTestBase +{ + [Fact] + public void IHistoryCompressionService_ShouldHaveCorrectMethodSignatures() + { + // Arrange & Act + var interfaceType = typeof(IHistoryCompressionService); + var methods = interfaceType.GetMethods(); + + // Assert + methods.Should().HaveCount(2); + + // CompressHistoryAsync method + var compressHistoryAsyncMethod = methods.FirstOrDefault(m => + m.Name == "CompressHistoryAsync" + ); + compressHistoryAsyncMethod.Should().NotBeNull(); + compressHistoryAsyncMethod!.ReturnType.Should().Be(typeof(Task>)); + compressHistoryAsyncMethod.GetParameters().Should().HaveCount(3); + compressHistoryAsyncMethod + .GetParameters()[0] + .ParameterType.Should() + .Be(typeof(List)); + compressHistoryAsyncMethod.GetParameters()[1].ParameterType.Should().Be(typeof(int)); + compressHistoryAsyncMethod + .GetParameters()[2] + .ParameterType.Should() + .Be(typeof(CancellationToken)); + + // ShouldCompress method + var shouldCompressMethod = methods.FirstOrDefault(m => m.Name == "ShouldCompress"); + shouldCompressMethod.Should().NotBeNull(); + shouldCompressMethod!.ReturnType.Should().Be(typeof(bool)); + shouldCompressMethod.GetParameters().Should().HaveCount(2); + shouldCompressMethod.GetParameters()[0].ParameterType.Should().Be(typeof(int)); + shouldCompressMethod.GetParameters()[1].ParameterType.Should().Be(typeof(int)); + } + + [Fact] + public void IHistoryCompressionService_ShouldBeImplementedByHistoryCompressionService() + { + // Arrange & Act + var historyCompressionServiceType = typeof(HistoryCompressionService); + var interfaceType = typeof(IHistoryCompressionService); + + // Assert + interfaceType.IsAssignableFrom(historyCompressionServiceType).Should().BeTrue(); + } + + [Fact] + public async Task IHistoryCompressionService_CompressHistoryAsync_ShouldReturnCompressedMessages() + { + // Arrange + var mock = new Mock(); + var messages = new List + { + new() { Role = "user", Content = "Message 1" }, + new() { Role = "assistant", Content = "Response 1" }, + new() { Role = "user", Content = "Message 2" }, + new() { Role = "assistant", Content = "Response 2" }, + }; + var targetCount = 2; + var cancellationToken = CancellationToken.None; + var expectedCompressedMessages = new List + { + new() { Role = "user", Content = "Compressed message" }, + new() { Role = "assistant", Content = "Compressed response" }, + }; + + mock.Setup(x => + x.CompressHistoryAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedCompressedMessages); + + // Act + var result = await mock.Object.CompressHistoryAsync( + messages, + targetCount, + cancellationToken + ); + + // Assert + result.Should().BeEquivalentTo(expectedCompressedMessages); + mock.Verify( + x => x.CompressHistoryAsync(messages, targetCount, cancellationToken), + Times.Once + ); + } + + [Fact] + public async Task IHistoryCompressionService_CompressHistoryAsync_ShouldHandleEmptyMessages() + { + // Arrange + var mock = new Mock(); + var messages = new List(); + var targetCount = 5; + var cancellationToken = CancellationToken.None; + var expectedCompressedMessages = new List(); + + mock.Setup(x => + x.CompressHistoryAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedCompressedMessages); + + // Act + var result = await mock.Object.CompressHistoryAsync( + messages, + targetCount, + cancellationToken + ); + + // Assert + result.Should().BeEquivalentTo(expectedCompressedMessages); + mock.Verify( + x => x.CompressHistoryAsync(messages, targetCount, cancellationToken), + Times.Once + ); + } + + [Fact] + public async Task IHistoryCompressionService_CompressHistoryAsync_ShouldHandleLargeMessageList() + { + // Arrange + var mock = new Mock(); + var messages = new List(); + for (int i = 0; i < 100; i++) + { + messages.Add(new() { Role = "user", Content = $"Message {i}" }); + } + var targetCount = 10; + var cancellationToken = CancellationToken.None; + var expectedCompressedMessages = new List + { + new() { Role = "user", Content = "Compressed summary" }, + }; + + mock.Setup(x => + x.CompressHistoryAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedCompressedMessages); + + // Act + var result = await mock.Object.CompressHistoryAsync( + messages, + targetCount, + cancellationToken + ); + + // Assert + result.Should().BeEquivalentTo(expectedCompressedMessages); + mock.Verify( + x => x.CompressHistoryAsync(messages, targetCount, cancellationToken), + Times.Once + ); + } + + [Fact] + public async Task IHistoryCompressionService_CompressHistoryAsync_ShouldHandleCancellationToken() + { + // Arrange + var mock = new Mock(); + var messages = new List + { + new() { Role = "user", Content = "Test message" }, + }; + var targetCount = 1; + var cancellationToken = new CancellationToken(true); // Cancelled token + var expectedCompressedMessages = new List(); + + mock.Setup(x => + x.CompressHistoryAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(expectedCompressedMessages); + + // Act + var result = await mock.Object.CompressHistoryAsync( + messages, + targetCount, + cancellationToken + ); + + // Assert + result.Should().BeEquivalentTo(expectedCompressedMessages); + mock.Verify( + x => x.CompressHistoryAsync(messages, targetCount, cancellationToken), + Times.Once + ); + } + + [Fact] + public void IHistoryCompressionService_ShouldCompress_ShouldReturnTrue_WhenMessageCountExceedsThreshold() + { + // Arrange + var mock = new Mock(); + var messageCount = 15; + var threshold = 10; + var expectedResult = true; + + mock.Setup(x => x.ShouldCompress(It.IsAny(), It.IsAny())).Returns(expectedResult); + + // Act + var result = mock.Object.ShouldCompress(messageCount, threshold); + + // Assert + result.Should().Be(expectedResult); + mock.Verify(x => x.ShouldCompress(messageCount, threshold), Times.Once); + } + + [Fact] + public void IHistoryCompressionService_ShouldCompress_ShouldReturnFalse_WhenMessageCountIsBelowThreshold() + { + // Arrange + var mock = new Mock(); + var messageCount = 5; + var threshold = 10; + var expectedResult = false; + + mock.Setup(x => x.ShouldCompress(It.IsAny(), It.IsAny())).Returns(expectedResult); + + // Act + var result = mock.Object.ShouldCompress(messageCount, threshold); + + // Assert + result.Should().Be(expectedResult); + mock.Verify(x => x.ShouldCompress(messageCount, threshold), Times.Once); + } + + [Fact] + public void IHistoryCompressionService_ShouldCompress_ShouldReturnFalse_WhenMessageCountEqualsThreshold() + { + // Arrange + var mock = new Mock(); + var messageCount = 10; + var threshold = 10; + var expectedResult = false; + + mock.Setup(x => x.ShouldCompress(It.IsAny(), It.IsAny())).Returns(expectedResult); + + // Act + var result = mock.Object.ShouldCompress(messageCount, threshold); + + // Assert + result.Should().Be(expectedResult); + mock.Verify(x => x.ShouldCompress(messageCount, threshold), Times.Once); + } + + [Fact] + public void IHistoryCompressionService_ShouldCompress_ShouldHandleZeroValues() + { + // Arrange + var mock = new Mock(); + var messageCount = 0; + var threshold = 0; + var expectedResult = false; + + mock.Setup(x => x.ShouldCompress(It.IsAny(), It.IsAny())).Returns(expectedResult); + + // Act + var result = mock.Object.ShouldCompress(messageCount, threshold); + + // Assert + result.Should().Be(expectedResult); + mock.Verify(x => x.ShouldCompress(messageCount, threshold), Times.Once); + } + + [Fact] + public void IHistoryCompressionService_ShouldCompress_ShouldHandleNegativeValues() + { + // Arrange + var mock = new Mock(); + var messageCount = -5; + var threshold = -10; + var expectedResult = false; + + mock.Setup(x => x.ShouldCompress(It.IsAny(), It.IsAny())).Returns(expectedResult); + + // Act + var result = mock.Object.ShouldCompress(messageCount, threshold); + + // Assert + result.Should().Be(expectedResult); + mock.Verify(x => x.ShouldCompress(messageCount, threshold), Times.Once); + } + + [Fact] + public void IHistoryCompressionService_ShouldCompress_ShouldHandleLargeValues() + { + // Arrange + var mock = new Mock(); + var messageCount = int.MaxValue; + var threshold = int.MaxValue - 1; + var expectedResult = true; + + mock.Setup(x => x.ShouldCompress(It.IsAny(), It.IsAny())).Returns(expectedResult); + + // Act + var result = mock.Object.ShouldCompress(messageCount, threshold); + + // Assert + result.Should().Be(expectedResult); + mock.Verify(x => x.ShouldCompress(messageCount, threshold), Times.Once); + } + + [Fact] + public void IHistoryCompressionService_ShouldBePublicInterface() + { + // Arrange & Act + var interfaceType = typeof(IHistoryCompressionService); + + // Assert + interfaceType.IsPublic.Should().BeTrue(); + interfaceType.IsInterface.Should().BeTrue(); + } + + [Fact] + public void IHistoryCompressionService_ShouldHaveCorrectNamespace() + { + // Arrange & Act + var interfaceType = typeof(IHistoryCompressionService); + + // Assert + interfaceType.Namespace.Should().Be("ChatBot.Services.Interfaces"); + } + + [Fact] + public void IHistoryCompressionService_ShouldHaveCorrectGenericConstraints() + { + // Arrange & Act + var interfaceType = typeof(IHistoryCompressionService); + var methods = interfaceType.GetMethods(); + + // Assert + // All methods should be public + methods.All(m => m.IsPublic).Should().BeTrue(); + + // CompressHistoryAsync should have default parameter for CancellationToken + var compressMethod = methods.First(m => m.Name == "CompressHistoryAsync"); + compressMethod.GetParameters()[2].HasDefaultValue.Should().BeTrue(); + // Note: Default value for CancellationToken in interface might be null + compressMethod.GetParameters()[2].DefaultValue.Should().BeNull(); + } +} diff --git a/ChatBot.Tests/Services/Interfaces/IOllamaClientTests.cs b/ChatBot.Tests/Services/Interfaces/IOllamaClientTests.cs new file mode 100644 index 0000000..89a187e --- /dev/null +++ b/ChatBot.Tests/Services/Interfaces/IOllamaClientTests.cs @@ -0,0 +1,380 @@ +using ChatBot.Services; +using ChatBot.Services.Interfaces; +using ChatBot.Tests.TestUtilities; +using FluentAssertions; +using Moq; +using OllamaSharp.Models; +using OllamaSharp.Models.Chat; + +namespace ChatBot.Tests.Services.Interfaces; + +public class IOllamaClientTests : UnitTestBase +{ + [Fact] + public void IOllamaClient_ShouldHaveCorrectMethodSignatures() + { + // Arrange & Act + var interfaceType = typeof(IOllamaClient); + var methods = interfaceType.GetMethods(); + var properties = interfaceType.GetProperties(); + + // Assert + methods.Should().HaveCount(4); // ChatAsync, ListLocalModelsAsync, get_SelectedModel, set_SelectedModel + properties.Should().HaveCount(1); + + // SelectedModel property + var selectedModelProperty = properties.FirstOrDefault(p => p.Name == "SelectedModel"); + selectedModelProperty.Should().NotBeNull(); + selectedModelProperty!.PropertyType.Should().Be(typeof(string)); + selectedModelProperty.CanRead.Should().BeTrue(); + selectedModelProperty.CanWrite.Should().BeTrue(); + + // ChatAsync method + var chatAsyncMethod = methods.FirstOrDefault(m => m.Name == "ChatAsync"); + chatAsyncMethod.Should().NotBeNull(); + chatAsyncMethod!.ReturnType.Should().Be(typeof(IAsyncEnumerable)); + chatAsyncMethod.GetParameters().Should().HaveCount(1); + chatAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(ChatRequest)); + + // ListLocalModelsAsync method + var listLocalModelsAsyncMethod = methods.FirstOrDefault(m => + m.Name == "ListLocalModelsAsync" + ); + listLocalModelsAsyncMethod.Should().NotBeNull(); + listLocalModelsAsyncMethod!.ReturnType.Should().Be(typeof(Task>)); + listLocalModelsAsyncMethod.GetParameters().Should().BeEmpty(); + } + + [Fact] + public void IOllamaClient_ShouldBeImplementedByOllamaClientAdapter() + { + // Arrange & Act + var ollamaClientAdapterType = typeof(OllamaClientAdapter); + var interfaceType = typeof(IOllamaClient); + + // Assert + interfaceType.IsAssignableFrom(ollamaClientAdapterType).Should().BeTrue(); + } + + [Fact] + public void IOllamaClient_SelectedModel_ShouldBeReadableAndWritable() + { + // Arrange + var mock = new Mock(); + var expectedModel = "llama2:7b"; + + mock.SetupProperty(x => x.SelectedModel, "default-model"); + + // Act + mock.Object.SelectedModel = expectedModel; + var result = mock.Object.SelectedModel; + + // Assert + result.Should().Be(expectedModel); + mock.VerifySet(x => x.SelectedModel = expectedModel, Times.Once); + mock.VerifyGet(x => x.SelectedModel, Times.Once); + } + + [Fact] + public async Task IOllamaClient_ChatAsync_ShouldReturnAsyncEnumerable() + { + // Arrange + var mock = new Mock(); + var request = new ChatRequest + { + Model = "llama2:7b", + Messages = new List + { + new() { Role = "user", Content = "Hello" }, + }, + }; + + var expectedResponse = new List + { + new() + { + Message = new Message + { + Role = "assistant", + Content = "Hello! How can I help you?", + }, + }, + new() { Done = true }, + }; + + mock.Setup(x => x.ChatAsync(It.IsAny())) + .Returns(CreateAsyncEnumerable(expectedResponse)); + + // Act + var result = mock.Object.ChatAsync(request); + var responses = new List(); + await foreach (var response in result) + { + responses.Add(response); + } + + // Assert + responses.Should().HaveCount(2); + responses[0]?.Message?.Content.Should().Be("Hello! How can I help you?"); + responses[1]?.Done.Should().BeTrue(); + mock.Verify(x => x.ChatAsync(request), Times.Once); + } + + [Fact] + public async Task IOllamaClient_ChatAsync_ShouldHandleEmptyResponse() + { + // Arrange + var mock = new Mock(); + var request = new ChatRequest { Model = "llama2:7b", Messages = new List() }; + + var expectedResponse = new List(); + + mock.Setup(x => x.ChatAsync(It.IsAny())) + .Returns(CreateAsyncEnumerable(expectedResponse)); + + // Act + var result = mock.Object.ChatAsync(request); + var responses = new List(); + await foreach (var response in result) + { + responses.Add(response); + } + + // Assert + responses.Should().BeEmpty(); + mock.Verify(x => x.ChatAsync(request), Times.Once); + } + + [Fact] + public async Task IOllamaClient_ChatAsync_ShouldHandleNullResponse() + { + // Arrange + var mock = new Mock(); + var request = new ChatRequest + { + Model = "llama2:7b", + Messages = new List + { + new() { Role = "user", Content = "Test" }, + }, + }; + + var expectedResponse = new List + { + null, + new() + { + Message = new Message { Role = "assistant", Content = "Response" }, + }, + }; + + mock.Setup(x => x.ChatAsync(It.IsAny())) + .Returns(CreateAsyncEnumerable(expectedResponse)); + + // Act + var result = mock.Object.ChatAsync(request); + var responses = new List(); + await foreach (var response in result) + { + responses.Add(response); + } + + // Assert + responses.Should().HaveCount(2); + responses[0].Should().BeNull(); + responses[1]?.Message?.Content.Should().Be("Response"); + mock.Verify(x => x.ChatAsync(request), Times.Once); + } + + [Fact] + public async Task IOllamaClient_ListLocalModelsAsync_ShouldReturnModels() + { + // Arrange + var mock = new Mock(); + var expectedModels = new List + { + new() + { + Name = "llama2:7b", + Size = 3825819519, + ModifiedAt = DateTime.UtcNow, + }, + new() + { + Name = "codellama:7b", + Size = 3825819519, + ModifiedAt = DateTime.UtcNow, + }, + }; + + mock.Setup(x => x.ListLocalModelsAsync()).ReturnsAsync(expectedModels); + + // Act + var result = await mock.Object.ListLocalModelsAsync(); + + // Assert + result.Should().BeEquivalentTo(expectedModels); + mock.Verify(x => x.ListLocalModelsAsync(), Times.Once); + } + + [Fact] + public async Task IOllamaClient_ListLocalModelsAsync_ShouldReturnEmptyList() + { + // Arrange + var mock = new Mock(); + var expectedModels = new List(); + + mock.Setup(x => x.ListLocalModelsAsync()).ReturnsAsync(expectedModels); + + // Act + var result = await mock.Object.ListLocalModelsAsync(); + + // Assert + result.Should().BeEmpty(); + mock.Verify(x => x.ListLocalModelsAsync(), Times.Once); + } + + [Fact] + public async Task IOllamaClient_ListLocalModelsAsync_ShouldHandleNullModels() + { + // Arrange + var mock = new Mock(); + IEnumerable? expectedModels = null; + + mock.Setup(x => x.ListLocalModelsAsync()).ReturnsAsync(expectedModels!); + + // Act + var result = await mock.Object.ListLocalModelsAsync(); + + // Assert + result.Should().BeNull(); + mock.Verify(x => x.ListLocalModelsAsync(), Times.Once); + } + + [Fact] + public void IOllamaClient_SelectedModel_ShouldHandleNullValue() + { + // Arrange + var mock = new Mock(); + string? expectedModel = null; + + mock.SetupProperty(x => x.SelectedModel, "default-model"); + + // Act + mock.Object.SelectedModel = expectedModel!; + var result = mock.Object.SelectedModel; + + // Assert + result.Should().BeNull(); + mock.VerifySet(x => x.SelectedModel = expectedModel!, Times.Once); + mock.VerifyGet(x => x.SelectedModel, Times.Once); + } + + [Fact] + public void IOllamaClient_SelectedModel_ShouldHandleEmptyString() + { + // Arrange + var mock = new Mock(); + var expectedModel = ""; + + mock.SetupProperty(x => x.SelectedModel, "default-model"); + + // Act + mock.Object.SelectedModel = expectedModel; + var result = mock.Object.SelectedModel; + + // Assert + result.Should().Be(expectedModel); + mock.VerifySet(x => x.SelectedModel = expectedModel, Times.Once); + mock.VerifyGet(x => x.SelectedModel, Times.Once); + } + + [Fact] + public void IOllamaClient_ShouldBePublicInterface() + { + // Arrange & Act + var interfaceType = typeof(IOllamaClient); + + // Assert + interfaceType.IsPublic.Should().BeTrue(); + interfaceType.IsInterface.Should().BeTrue(); + } + + [Fact] + public void IOllamaClient_ShouldHaveCorrectNamespace() + { + // Arrange & Act + var interfaceType = typeof(IOllamaClient); + + // Assert + interfaceType.Namespace.Should().Be("ChatBot.Services.Interfaces"); + } + + [Fact] + public void IOllamaClient_ShouldHaveCorrectGenericConstraints() + { + // Arrange & Act + var interfaceType = typeof(IOllamaClient); + var methods = interfaceType.GetMethods(); + var properties = interfaceType.GetProperties(); + + // Assert + // All methods should be public + methods.All(m => m.IsPublic).Should().BeTrue(); + + // All properties should be public + properties.All(p => p.GetGetMethod()?.IsPublic == true).Should().BeTrue(); + properties.All(p => p.GetSetMethod()?.IsPublic == true).Should().BeTrue(); + } + + [Fact] + public async Task IOllamaClient_ChatAsync_ShouldHandleLargeRequest() + { + // Arrange + var mock = new Mock(); + var messages = new List(); + // Add many messages + for (int i = 0; i < 100; i++) + { + messages.Add( + new Message { Role = i % 2 == 0 ? "user" : "assistant", Content = $"Message {i}" } + ); + } + + var request = new ChatRequest { Model = "llama2:7b", Messages = messages }; + + var expectedResponse = new List + { + new() + { + Message = new Message { Role = "assistant", Content = "Large response" }, + }, + }; + + mock.Setup(x => x.ChatAsync(It.IsAny())) + .Returns(CreateAsyncEnumerable(expectedResponse)); + + // Act + var result = mock.Object.ChatAsync(request); + var responses = new List(); + await foreach (var response in result) + { + responses.Add(response); + } + + // Assert + responses.Should().HaveCount(1); + responses[0]?.Message?.Content.Should().Be("Large response"); + mock.Verify(x => x.ChatAsync(request), Times.Once); + } + + private static async IAsyncEnumerable CreateAsyncEnumerable( + List items + ) + { + foreach (var item in items) + { + yield return item; + } + } +} diff --git a/ChatBot.Tests/Services/Interfaces/ISessionStorageTests.cs b/ChatBot.Tests/Services/Interfaces/ISessionStorageTests.cs new file mode 100644 index 0000000..6c482e0 --- /dev/null +++ b/ChatBot.Tests/Services/Interfaces/ISessionStorageTests.cs @@ -0,0 +1,295 @@ +using ChatBot.Models; +using ChatBot.Services; +using ChatBot.Services.Interfaces; +using ChatBot.Tests.TestUtilities; +using FluentAssertions; +using Moq; + +namespace ChatBot.Tests.Services.Interfaces; + +public class ISessionStorageTests : UnitTestBase +{ + [Fact] + public void ISessionStorage_ShouldHaveCorrectMethodSignatures() + { + // Arrange & Act + var interfaceType = typeof(ISessionStorage); + var methods = interfaceType.GetMethods(); + + // Assert + methods.Should().HaveCount(6); + + // GetOrCreate method + var getOrCreateMethod = methods.FirstOrDefault(m => m.Name == "GetOrCreate"); + getOrCreateMethod.Should().NotBeNull(); + getOrCreateMethod!.ReturnType.Should().Be(typeof(ChatSession)); + getOrCreateMethod.GetParameters().Should().HaveCount(3); + getOrCreateMethod.GetParameters()[0].ParameterType.Should().Be(typeof(long)); + getOrCreateMethod.GetParameters()[1].ParameterType.Should().Be(typeof(string)); + getOrCreateMethod.GetParameters()[2].ParameterType.Should().Be(typeof(string)); + + // Get method + var getMethod = methods.FirstOrDefault(m => m.Name == "Get"); + getMethod.Should().NotBeNull(); + getMethod!.ReturnType.Should().Be(typeof(ChatSession)); + getMethod.GetParameters().Should().HaveCount(1); + getMethod.GetParameters()[0].ParameterType.Should().Be(typeof(long)); + + // Remove method + var removeMethod = methods.FirstOrDefault(m => m.Name == "Remove"); + removeMethod.Should().NotBeNull(); + removeMethod!.ReturnType.Should().Be(typeof(bool)); + removeMethod.GetParameters().Should().HaveCount(1); + removeMethod.GetParameters()[0].ParameterType.Should().Be(typeof(long)); + + // GetActiveSessionsCount method + var getActiveSessionsCountMethod = methods.FirstOrDefault(m => + m.Name == "GetActiveSessionsCount" + ); + getActiveSessionsCountMethod.Should().NotBeNull(); + getActiveSessionsCountMethod!.ReturnType.Should().Be(typeof(int)); + getActiveSessionsCountMethod.GetParameters().Should().BeEmpty(); + + // CleanupOldSessions method + var cleanupOldSessionsMethod = methods.FirstOrDefault(m => m.Name == "CleanupOldSessions"); + cleanupOldSessionsMethod.Should().NotBeNull(); + cleanupOldSessionsMethod!.ReturnType.Should().Be(typeof(int)); + cleanupOldSessionsMethod.GetParameters().Should().HaveCount(1); + cleanupOldSessionsMethod.GetParameters()[0].ParameterType.Should().Be(typeof(int)); + + // SaveSessionAsync method + var saveSessionAsyncMethod = methods.FirstOrDefault(m => m.Name == "SaveSessionAsync"); + saveSessionAsyncMethod.Should().NotBeNull(); + saveSessionAsyncMethod!.ReturnType.Should().Be(typeof(Task)); + saveSessionAsyncMethod.GetParameters().Should().HaveCount(1); + saveSessionAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(ChatSession)); + } + + [Fact] + public void ISessionStorage_ShouldBeImplementedByDatabaseSessionStorage() + { + // Arrange & Act + var databaseSessionStorageType = typeof(DatabaseSessionStorage); + var interfaceType = typeof(ISessionStorage); + + // Assert + interfaceType.IsAssignableFrom(databaseSessionStorageType).Should().BeTrue(); + } + + [Fact] + public void ISessionStorage_ShouldBeImplementedByInMemorySessionStorage() + { + // Arrange & Act + var inMemorySessionStorageType = typeof(InMemorySessionStorage); + var interfaceType = typeof(ISessionStorage); + + // Assert + interfaceType.IsAssignableFrom(inMemorySessionStorageType).Should().BeTrue(); + } + + [Fact] + public void ISessionStorage_GetOrCreate_ShouldReturnChatSession() + { + // Arrange + var mock = new Mock(); + var chatId = 12345L; + var chatType = "private"; + var chatTitle = "Test Chat"; + var expectedSession = TestDataBuilder.ChatSessions.CreateBasicSession(chatId, chatType); + + mock.Setup(x => x.GetOrCreate(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(expectedSession); + + // Act + var result = mock.Object.GetOrCreate(chatId, chatType, chatTitle); + + // Assert + result.Should().Be(expectedSession); + mock.Verify(x => x.GetOrCreate(chatId, chatType, chatTitle), Times.Once); + } + + [Fact] + public void ISessionStorage_Get_ShouldReturnChatSessionOrNull() + { + // Arrange + var mock = new Mock(); + var chatId = 12345L; + var expectedSession = TestDataBuilder.ChatSessions.CreateBasicSession(chatId, "private"); + + mock.Setup(x => x.Get(It.IsAny())).Returns(expectedSession); + + // Act + var result = mock.Object.Get(chatId); + + // Assert + result.Should().Be(expectedSession); + mock.Verify(x => x.Get(chatId), Times.Once); + } + + [Fact] + public void ISessionStorage_Get_ShouldReturnNullWhenSessionNotFound() + { + // Arrange + var mock = new Mock(); + var chatId = 12345L; + + mock.Setup(x => x.Get(It.IsAny())).Returns((ChatSession?)null); + + // Act + var result = mock.Object.Get(chatId); + + // Assert + result.Should().BeNull(); + mock.Verify(x => x.Get(chatId), Times.Once); + } + + [Fact] + public void ISessionStorage_Remove_ShouldReturnBoolean() + { + // Arrange + var mock = new Mock(); + var chatId = 12345L; + var expectedResult = true; + + mock.Setup(x => x.Remove(It.IsAny())).Returns(expectedResult); + + // Act + var result = mock.Object.Remove(chatId); + + // Assert + result.Should().Be(expectedResult); + mock.Verify(x => x.Remove(chatId), Times.Once); + } + + [Fact] + public void ISessionStorage_GetActiveSessionsCount_ShouldReturnInt() + { + // Arrange + var mock = new Mock(); + var expectedCount = 5; + + mock.Setup(x => x.GetActiveSessionsCount()).Returns(expectedCount); + + // Act + var result = mock.Object.GetActiveSessionsCount(); + + // Assert + result.Should().Be(expectedCount); + mock.Verify(x => x.GetActiveSessionsCount(), Times.Once); + } + + [Fact] + public void ISessionStorage_CleanupOldSessions_ShouldReturnInt() + { + // Arrange + var mock = new Mock(); + var hoursOld = 24; + var expectedCleanedCount = 3; + + mock.Setup(x => x.CleanupOldSessions(It.IsAny())).Returns(expectedCleanedCount); + + // Act + var result = mock.Object.CleanupOldSessions(hoursOld); + + // Assert + result.Should().Be(expectedCleanedCount); + mock.Verify(x => x.CleanupOldSessions(hoursOld), Times.Once); + } + + [Fact] + public void ISessionStorage_CleanupOldSessions_ShouldUseDefaultValue() + { + // Arrange + var mock = new Mock(); + var expectedCleanedCount = 2; + + mock.Setup(x => x.CleanupOldSessions(It.IsAny())).Returns(expectedCleanedCount); + + // Act + var result = mock.Object.CleanupOldSessions(); + + // Assert + result.Should().Be(expectedCleanedCount); + mock.Verify(x => x.CleanupOldSessions(24), Times.Once); // Default value is 24 + } + + [Fact] + public async Task ISessionStorage_SaveSessionAsync_ShouldReturnTask() + { + // Arrange + var mock = new Mock(); + var session = TestDataBuilder.ChatSessions.CreateBasicSession(12345L, "private"); + + mock.Setup(x => x.SaveSessionAsync(It.IsAny())).Returns(Task.CompletedTask); + + // Act + await mock.Object.SaveSessionAsync(session); + + // Assert + mock.Verify(x => x.SaveSessionAsync(session), Times.Once); + } + + [Fact] + public void ISessionStorage_GetOrCreate_ShouldUseDefaultValues() + { + // Arrange + var mock = new Mock(); + var chatId = 12345L; + var expectedSession = TestDataBuilder.ChatSessions.CreateBasicSession(chatId, "private"); + + mock.Setup(x => x.GetOrCreate(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(expectedSession); + + // Act + var result = mock.Object.GetOrCreate(chatId); + + // Assert + result.Should().Be(expectedSession); + mock.Verify(x => x.GetOrCreate(chatId, "private", ""), Times.Once); // Default values + } + + [Fact] + public void ISessionStorage_ShouldBePublicInterface() + { + // Arrange & Act + var interfaceType = typeof(ISessionStorage); + + // Assert + interfaceType.IsPublic.Should().BeTrue(); + interfaceType.IsInterface.Should().BeTrue(); + } + + [Fact] + public void ISessionStorage_ShouldHaveCorrectNamespace() + { + // Arrange & Act + var interfaceType = typeof(ISessionStorage); + + // Assert + interfaceType.Namespace.Should().Be("ChatBot.Services.Interfaces"); + } + + [Fact] + public void ISessionStorage_ShouldHaveCorrectGenericConstraints() + { + // Arrange & Act + var interfaceType = typeof(ISessionStorage); + var methods = interfaceType.GetMethods(); + + // Assert + // All methods should be public + methods.All(m => m.IsPublic).Should().BeTrue(); + + // GetOrCreate should have default parameters + var getOrCreateMethod = methods.First(m => m.Name == "GetOrCreate"); + getOrCreateMethod.GetParameters()[1].HasDefaultValue.Should().BeTrue(); + getOrCreateMethod.GetParameters()[1].DefaultValue.Should().Be("private"); + getOrCreateMethod.GetParameters()[2].HasDefaultValue.Should().BeTrue(); + getOrCreateMethod.GetParameters()[2].DefaultValue.Should().Be(""); + + // CleanupOldSessions should have default parameter + var cleanupMethod = methods.First(m => m.Name == "CleanupOldSessions"); + cleanupMethod.GetParameters()[0].HasDefaultValue.Should().BeTrue(); + cleanupMethod.GetParameters()[0].DefaultValue.Should().Be(24); + } +} diff --git a/ChatBot.Tests/Services/Interfaces/ITelegramBotClientWrapperTests.cs b/ChatBot.Tests/Services/Interfaces/ITelegramBotClientWrapperTests.cs new file mode 100644 index 0000000..d24bf6c --- /dev/null +++ b/ChatBot.Tests/Services/Interfaces/ITelegramBotClientWrapperTests.cs @@ -0,0 +1,272 @@ +using ChatBot.Services; +using ChatBot.Services.Interfaces; +using ChatBot.Tests.TestUtilities; +using FluentAssertions; +using Moq; +using Telegram.Bot.Types; + +namespace ChatBot.Tests.Services.Interfaces; + +public class ITelegramBotClientWrapperTests : UnitTestBase +{ + [Fact] + public void ITelegramBotClientWrapper_ShouldHaveCorrectMethodSignatures() + { + // Arrange & Act + var interfaceType = typeof(ITelegramBotClientWrapper); + var methods = interfaceType.GetMethods(); + + // Assert + methods.Should().HaveCount(1); + + // GetMeAsync method + var getMeAsyncMethod = methods.FirstOrDefault(m => m.Name == "GetMeAsync"); + getMeAsyncMethod.Should().NotBeNull(); + getMeAsyncMethod!.ReturnType.Should().Be(typeof(Task)); + getMeAsyncMethod.GetParameters().Should().HaveCount(1); + getMeAsyncMethod.GetParameters()[0].ParameterType.Should().Be(typeof(CancellationToken)); + } + + [Fact] + public void ITelegramBotClientWrapper_ShouldBeImplementedByTelegramBotClientWrapper() + { + // Arrange & Act + var telegramBotClientWrapperType = typeof(TelegramBotClientWrapper); + var interfaceType = typeof(ITelegramBotClientWrapper); + + // Assert + interfaceType.IsAssignableFrom(telegramBotClientWrapperType).Should().BeTrue(); + } + + [Fact] + public async Task ITelegramBotClientWrapper_GetMeAsync_ShouldReturnUser() + { + // Arrange + var mock = new Mock(); + var cancellationToken = CancellationToken.None; + var expectedUser = new User + { + Id = 123456789, + IsBot = true, + FirstName = "TestBot", + Username = "test_bot", + }; + + mock.Setup(x => x.GetMeAsync(It.IsAny())).ReturnsAsync(expectedUser); + + // Act + var result = await mock.Object.GetMeAsync(cancellationToken); + + // Assert + result.Should().Be(expectedUser); + mock.Verify(x => x.GetMeAsync(cancellationToken), Times.Once); + } + + [Fact] + public async Task ITelegramBotClientWrapper_GetMeAsync_ShouldHandleCancellationToken() + { + // Arrange + var mock = new Mock(); + var cancellationToken = new CancellationToken(true); // Cancelled token + var expectedUser = new User + { + Id = 123456789, + IsBot = true, + FirstName = "TestBot", + Username = "test_bot", + }; + + mock.Setup(x => x.GetMeAsync(It.IsAny())).ReturnsAsync(expectedUser); + + // Act + var result = await mock.Object.GetMeAsync(cancellationToken); + + // Assert + result.Should().Be(expectedUser); + mock.Verify(x => x.GetMeAsync(cancellationToken), Times.Once); + } + + [Fact] + public async Task ITelegramBotClientWrapper_GetMeAsync_ShouldUseDefaultCancellationToken() + { + // Arrange + var mock = new Mock(); + var expectedUser = new User + { + Id = 123456789, + IsBot = true, + FirstName = "TestBot", + Username = "test_bot", + }; + + mock.Setup(x => x.GetMeAsync(It.IsAny())).ReturnsAsync(expectedUser); + + // Act + var result = await mock.Object.GetMeAsync(); + + // Assert + result.Should().Be(expectedUser); + mock.Verify(x => x.GetMeAsync(CancellationToken.None), Times.Once); + } + + [Fact] + public async Task ITelegramBotClientWrapper_GetMeAsync_ShouldHandleUserWithAllProperties() + { + // Arrange + var mock = new Mock(); + var cancellationToken = CancellationToken.None; + var expectedUser = new User + { + Id = 987654321, + IsBot = true, + FirstName = "AdvancedBot", + LastName = "Test", + Username = "advanced_test_bot", + LanguageCode = "en", + IsPremium = true, + AddedToAttachmentMenu = true, + }; + + mock.Setup(x => x.GetMeAsync(It.IsAny())).ReturnsAsync(expectedUser); + + // Act + var result = await mock.Object.GetMeAsync(cancellationToken); + + // Assert + result.Should().Be(expectedUser); + result.Id.Should().Be(987654321); + result.IsBot.Should().BeTrue(); + result.FirstName.Should().Be("AdvancedBot"); + result.LastName.Should().Be("Test"); + result.Username.Should().Be("advanced_test_bot"); + result.LanguageCode.Should().Be("en"); + result.IsPremium.Should().BeTrue(); + result.AddedToAttachmentMenu.Should().BeTrue(); + mock.Verify(x => x.GetMeAsync(cancellationToken), Times.Once); + } + + [Fact] + public async Task ITelegramBotClientWrapper_GetMeAsync_ShouldHandleMinimalUser() + { + // Arrange + var mock = new Mock(); + var cancellationToken = CancellationToken.None; + var expectedUser = new User + { + Id = 111111111, + IsBot = false, + FirstName = "MinimalUser", + }; + + mock.Setup(x => x.GetMeAsync(It.IsAny())).ReturnsAsync(expectedUser); + + // Act + var result = await mock.Object.GetMeAsync(cancellationToken); + + // Assert + result.Should().Be(expectedUser); + result.Id.Should().Be(111111111); + result.IsBot.Should().BeFalse(); + result.FirstName.Should().Be("MinimalUser"); + result.LastName.Should().BeNull(); + result.Username.Should().BeNull(); + result.LanguageCode.Should().BeNull(); + mock.Verify(x => x.GetMeAsync(cancellationToken), Times.Once); + } + + [Fact] + public void ITelegramBotClientWrapper_ShouldBePublicInterface() + { + // Arrange & Act + var interfaceType = typeof(ITelegramBotClientWrapper); + + // Assert + interfaceType.IsPublic.Should().BeTrue(); + interfaceType.IsInterface.Should().BeTrue(); + } + + [Fact] + public void ITelegramBotClientWrapper_ShouldHaveCorrectNamespace() + { + // Arrange & Act + var interfaceType = typeof(ITelegramBotClientWrapper); + + // Assert + interfaceType.Namespace.Should().Be("ChatBot.Services.Interfaces"); + } + + [Fact] + public void ITelegramBotClientWrapper_ShouldHaveCorrectGenericConstraints() + { + // Arrange & Act + var interfaceType = typeof(ITelegramBotClientWrapper); + var methods = interfaceType.GetMethods(); + + // Assert + // All methods should be public + methods.All(m => m.IsPublic).Should().BeTrue(); + + // GetMeAsync should have default parameter for CancellationToken + var getMeAsyncMethod = methods.First(m => m.Name == "GetMeAsync"); + getMeAsyncMethod.GetParameters()[0].HasDefaultValue.Should().BeTrue(); + getMeAsyncMethod.GetParameters()[0].DefaultValue.Should().BeNull(); + } + + [Fact] + public async Task ITelegramBotClientWrapper_GetMeAsync_ShouldHandleMultipleCalls() + { + // Arrange + var mock = new Mock(); + var cancellationToken = CancellationToken.None; + var expectedUser = new User + { + Id = 123456789, + IsBot = true, + FirstName = "TestBot", + Username = "test_bot", + }; + + mock.Setup(x => x.GetMeAsync(It.IsAny())).ReturnsAsync(expectedUser); + + // Act + var result1 = await mock.Object.GetMeAsync(cancellationToken); + var result2 = await mock.Object.GetMeAsync(cancellationToken); + var result3 = await mock.Object.GetMeAsync(cancellationToken); + + // Assert + result1.Should().Be(expectedUser); + result2.Should().Be(expectedUser); + result3.Should().Be(expectedUser); + mock.Verify(x => x.GetMeAsync(cancellationToken), Times.Exactly(3)); + } + + [Fact] + public async Task ITelegramBotClientWrapper_GetMeAsync_ShouldHandleConcurrentCalls() + { + // Arrange + var mock = new Mock(); + var cancellationToken = CancellationToken.None; + var expectedUser = new User + { + Id = 123456789, + IsBot = true, + FirstName = "TestBot", + Username = "test_bot", + }; + + mock.Setup(x => x.GetMeAsync(It.IsAny())).ReturnsAsync(expectedUser); + + // Act + var tasks = new List>(); + for (int i = 0; i < 10; i++) + { + tasks.Add(mock.Object.GetMeAsync(cancellationToken)); + } + + var results = await Task.WhenAll(tasks); + + // Assert + results.Should().AllBeEquivalentTo(expectedUser); + mock.Verify(x => x.GetMeAsync(cancellationToken), Times.Exactly(10)); + } +} diff --git a/test_coverage_report.md b/test_coverage_report.md index 342b642..1d63064 100644 --- a/test_coverage_report.md +++ b/test_coverage_report.md @@ -90,12 +90,12 @@ - [x] `TelegramMessageSender` - тесты отправки различных типов сообщений ### 6. Интерфейсы -- [ ] `IAIService` - тесты интерфейса -- [ ] `ISessionStorage` - тесты интерфейса -- [ ] `IHistoryCompressionService` - тесты интерфейса -- [ ] `IOllamaClient` - тесты интерфейса -- [ ] `ITelegramBotClientWrapper` - тесты интерфейса -- [ ] `IChatSessionRepository` - тесты интерфейса +- [x] `IAIService` - тесты интерфейса +- [x] `ISessionStorage` - тесты интерфейса +- [x] `IHistoryCompressionService` - тесты интерфейса +- [x] `IOllamaClient` - тесты интерфейса +- [x] `ITelegramBotClientWrapper` - тесты интерфейса +- [x] `IChatSessionRepository` - тесты интерфейса ### 7. Контекст базы данных - [ ] `ChatBotDbContext` - тесты контекста БД