diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml deleted file mode 100644 index ef7d2ca..0000000 --- a/.gitea/workflows/test.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Unit Tests -on: - push: - branches: - - master - - develop - pull_request: - types: [opened, synchronize, reopened] - -jobs: - test: - name: Run Tests - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - - name: Restore dependencies - run: dotnet restore --verbosity normal - - - name: Build solution - run: dotnet build --no-restore --verbosity normal - - - name: Run unit tests - run: dotnet test --no-build --verbosity minimal --logger "trx;LogFileName=test-results.trx" --results-directory ./TestResults - - - name: Generate test report - if: always() - run: | - echo "Test results:" - find ./TestResults -name "*.trx" -exec echo "Found test result file: {}" \; - echo "File sizes:" - find ./TestResults -name "*.trx" -exec ls -lh {} \; - echo "✅ Tests completed successfully" - diff --git a/ChatBot.Tests/ChatBot.Tests.csproj b/ChatBot.Tests/ChatBot.Tests.csproj index 34f67e5..11dab5b 100644 --- a/ChatBot.Tests/ChatBot.Tests.csproj +++ b/ChatBot.Tests/ChatBot.Tests.csproj @@ -1,4 +1,4 @@ - + net9.0 enable @@ -20,6 +20,7 @@ + diff --git a/ChatBot.Tests/Data/Repositories/ChatSessionRepositoryTests.cs b/ChatBot.Tests/Data/Repositories/ChatSessionRepositoryTests.cs index c5b9927..4ff3d8a 100644 --- a/ChatBot.Tests/Data/Repositories/ChatSessionRepositoryTests.cs +++ b/ChatBot.Tests/Data/Repositories/ChatSessionRepositoryTests.cs @@ -261,4 +261,234 @@ public class ChatSessionRepositoryTests : TestBase var remainingSessions = await _repository.GetActiveSessionsCountAsync(); remainingSessions.Should().Be(1); } + + [Fact] + public async Task GetMessagesAsync_ShouldReturnMessages_WhenMessagesExist() + { + // Arrange + CleanupDatabase(); + var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345); + _dbContext.ChatSessions.Add(session); + await _dbContext.SaveChangesAsync(); + + await _repository.AddMessageAsync(session.Id, "Message 1", "User", 0); + await _repository.AddMessageAsync(session.Id, "Message 2", "Assistant", 1); + + // Act + var messages = await _repository.GetMessagesAsync(session.Id); + + // Assert + messages.Should().HaveCount(2); + messages[0].Content.Should().Be("Message 1"); + messages[0].Role.Should().Be("User"); + messages[1].Content.Should().Be("Message 2"); + messages[1].Role.Should().Be("Assistant"); + } + + [Fact] + public async Task GetMessagesAsync_ShouldReturnEmptyList_WhenNoMessages() + { + // Arrange + CleanupDatabase(); + var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345); + _dbContext.ChatSessions.Add(session); + await _dbContext.SaveChangesAsync(); + + // Act + var messages = await _repository.GetMessagesAsync(session.Id); + + // Assert + messages.Should().BeEmpty(); + } + + [Fact] + public async Task AddMessageAsync_ShouldAddMessage() + { + // Arrange + CleanupDatabase(); + var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345); + _dbContext.ChatSessions.Add(session); + await _dbContext.SaveChangesAsync(); + + // Act + var message = await _repository.AddMessageAsync(session.Id, "Test message", "User", 0); + + // Assert + message.Should().NotBeNull(); + message.Content.Should().Be("Test message"); + message.Role.Should().Be("User"); + message.MessageOrder.Should().Be(0); + message.SessionId.Should().Be(session.Id); + + var messages = await _repository.GetMessagesAsync(session.Id); + messages.Should().HaveCount(1); + } + + [Fact] + public async Task ClearMessagesAsync_ShouldRemoveAllMessages() + { + // Arrange + CleanupDatabase(); + var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345); + _dbContext.ChatSessions.Add(session); + await _dbContext.SaveChangesAsync(); + + await _repository.AddMessageAsync(session.Id, "Message 1", "User", 0); + await _repository.AddMessageAsync(session.Id, "Message 2", "Assistant", 1); + await _repository.AddMessageAsync(session.Id, "Message 3", "User", 2); + + // Act + await _repository.ClearMessagesAsync(session.Id); + + // Assert + var messages = await _repository.GetMessagesAsync(session.Id); + messages.Should().BeEmpty(); + } + + [Fact] + public async Task GetSessionsForCleanupAsync_ShouldReturnOldSessions() + { + // Arrange + CleanupDatabase(); + + var oldSession1 = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345); + oldSession1.LastUpdatedAt = DateTime.UtcNow.AddDays(-2); + + var oldSession2 = TestDataBuilder.Mocks.CreateChatSessionEntity(2, 12346); + oldSession2.LastUpdatedAt = DateTime.UtcNow.AddHours(-25); + + var recentSession = TestDataBuilder.Mocks.CreateChatSessionEntity(3, 12347); + recentSession.LastUpdatedAt = DateTime.UtcNow.AddMinutes(-30); + + _dbContext.ChatSessions.AddRange(oldSession1, oldSession2, recentSession); + await _dbContext.SaveChangesAsync(); + + // Act + var sessionsForCleanup = await _repository.GetSessionsForCleanupAsync(24); + + // Assert + sessionsForCleanup.Should().HaveCount(2); + sessionsForCleanup.Should().Contain(s => s.ChatId == 12345); + sessionsForCleanup.Should().Contain(s => s.ChatId == 12346); + sessionsForCleanup.Should().NotContain(s => s.ChatId == 12347); + } + + [Fact] + public async Task GetSessionsForCleanupAsync_ShouldReturnEmptyList_WhenNoOldSessions() + { + // Arrange + CleanupDatabase(); + + var recentSession = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345); + recentSession.LastUpdatedAt = DateTime.UtcNow.AddMinutes(-30); + + _dbContext.ChatSessions.Add(recentSession); + await _dbContext.SaveChangesAsync(); + + // Act + var sessionsForCleanup = await _repository.GetSessionsForCleanupAsync(24); + + // Assert + sessionsForCleanup.Should().BeEmpty(); + } + + [Fact] + public async Task UpdateAsync_ShouldUpdateLastUpdatedAt() + { + // Arrange + CleanupDatabase(); + var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345); + var originalLastUpdated = DateTime.UtcNow.AddDays(-1); + session.LastUpdatedAt = originalLastUpdated; + _dbContext.ChatSessions.Add(session); + await _dbContext.SaveChangesAsync(); + + // Act + session.Model = "new-model"; + var updatedSession = await _repository.UpdateAsync(session); + + // Assert + updatedSession.LastUpdatedAt.Should().BeAfter(originalLastUpdated); + updatedSession.Model.Should().Be("new-model"); + } + + [Fact] + public async Task GetByChatIdAsync_ShouldIncludeMessages() + { + // Arrange + CleanupDatabase(); + var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345); + _dbContext.ChatSessions.Add(session); + await _dbContext.SaveChangesAsync(); + + await _repository.AddMessageAsync(session.Id, "Test message", "User", 0); + + // Act + var retrievedSession = await _repository.GetByChatIdAsync(12345); + + // Assert + retrievedSession.Should().NotBeNull(); + retrievedSession!.Messages.Should().HaveCount(1); + retrievedSession.Messages.First().Content.Should().Be("Test message"); + } + + [Fact] + public async Task GetBySessionIdAsync_ShouldIncludeMessages() + { + // Arrange + CleanupDatabase(); + var sessionId = "test-session-id"; + var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345, sessionId); + _dbContext.ChatSessions.Add(session); + await _dbContext.SaveChangesAsync(); + + await _repository.AddMessageAsync(session.Id, "Test message", "User", 0); + + // Act + var retrievedSession = await _repository.GetBySessionIdAsync(sessionId); + + // Assert + retrievedSession.Should().NotBeNull(); + retrievedSession!.Messages.Should().HaveCount(1); + } + + [Fact] + public async Task AddMessageAsync_WithMultipleMessages_ShouldMaintainOrder() + { + // Arrange + CleanupDatabase(); + var session = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345); + _dbContext.ChatSessions.Add(session); + await _dbContext.SaveChangesAsync(); + + // Act + await _repository.AddMessageAsync(session.Id, "Message 1", "User", 0); + await _repository.AddMessageAsync(session.Id, "Message 2", "Assistant", 1); + await _repository.AddMessageAsync(session.Id, "Message 3", "User", 2); + + // Assert + var messages = await _repository.GetMessagesAsync(session.Id); + messages.Should().HaveCount(3); + messages[0].MessageOrder.Should().Be(0); + messages[1].MessageOrder.Should().Be(1); + messages[2].MessageOrder.Should().Be(2); + } + + [Fact] + public async Task CleanupOldSessionsAsync_WithNoOldSessions_ShouldReturnZero() + { + // Arrange + CleanupDatabase(); + + var recentSession = TestDataBuilder.Mocks.CreateChatSessionEntity(1, 12345); + recentSession.LastUpdatedAt = DateTime.UtcNow.AddMinutes(-10); + _dbContext.ChatSessions.Add(recentSession); + await _dbContext.SaveChangesAsync(); + + // Act + var removedCount = await _repository.CleanupOldSessionsAsync(24); + + // Assert + removedCount.Should().Be(0); + } } diff --git a/ChatBot.Tests/Models/ChatSessionTests.cs b/ChatBot.Tests/Models/ChatSessionTests.cs index f4dda6f..fc399b7 100644 --- a/ChatBot.Tests/Models/ChatSessionTests.cs +++ b/ChatBot.Tests/Models/ChatSessionTests.cs @@ -38,7 +38,7 @@ public class ChatSessionTests // Assert session.GetAllMessages().Should().HaveCount(1); - var message = session.GetAllMessages().First(); + var message = session.GetAllMessages()[0]; message.Role.Should().Be(ChatRole.User); message.Content.Should().Be(content); } @@ -55,7 +55,7 @@ public class ChatSessionTests // Assert session.GetAllMessages().Should().HaveCount(1); - var message = session.GetAllMessages().First(); + var message = session.GetAllMessages()[0]; message.Role.Should().Be(ChatRole.Assistant); message.Content.Should().Be(content); } @@ -73,7 +73,7 @@ public class ChatSessionTests // Assert session.GetAllMessages().Should().HaveCount(1); - var addedMessage = session.GetAllMessages().First(); + var addedMessage = session.GetAllMessages()[0]; addedMessage.Role.Should().Be(ChatRole.System); addedMessage.Content.Should().Be(content); } @@ -183,4 +183,319 @@ public class ChatSessionTests // Assert count.Should().Be(1); } + + [Fact] + public void AddUserMessage_InGroupChat_ShouldIncludeUsername() + { + // Arrange + var session = new ChatSession { ChatType = "group" }; + var content = "Hello"; + var username = "testuser"; + + // Act + session.AddUserMessage(content, username); + + // Assert + var message = session.GetAllMessages()[0]; + message.Content.Should().Be("testuser: Hello"); + } + + [Fact] + public void AddUserMessage_InPrivateChat_ShouldNotIncludeUsername() + { + // Arrange + var session = new ChatSession { ChatType = "private" }; + var content = "Hello"; + var username = "testuser"; + + // Act + session.AddUserMessage(content, username); + + // Assert + var message = session.GetAllMessages()[0]; + message.Content.Should().Be("Hello"); + } + + [Fact] + public void AddMessage_ShouldTrimHistory_WhenExceedsMaxHistoryLength() + { + // Arrange + var session = new ChatSession { MaxHistoryLength = 5 }; + + // Add system message + session.AddMessage(new ChatMessage { Role = ChatRole.System, Content = "System prompt" }); + + // Add messages to exceed max history + for (int i = 0; i < 10; i++) + { + session.AddMessage(new ChatMessage { Role = ChatRole.User, Content = $"Message {i}" }); + } + + // Act & Assert + session.GetMessageCount().Should().BeLessThanOrEqualTo(5); + // System message should be preserved + session.GetAllMessages()[0].Role.Should().Be(ChatRole.System); + } + + [Fact] + public void AddMessage_ShouldTrimHistory_WithoutSystemMessage() + { + // Arrange + var session = new ChatSession { MaxHistoryLength = 3 }; + + // Add messages to exceed max history + for (int i = 0; i < 5; i++) + { + session.AddMessage(new ChatMessage { Role = ChatRole.User, Content = $"Message {i}" }); + } + + // Act & Assert + session.GetMessageCount().Should().BeLessThanOrEqualTo(3); + } + + [Fact] + public void SetCompressionService_ShouldSetService() + { + // Arrange + var session = new ChatSession(); + var compressionServiceMock = + new Moq.Mock(); + + // Act + session.SetCompressionService(compressionServiceMock.Object); + + // Assert + // The service should be set (no exception) + session.Should().NotBeNull(); + } + + [Fact] + public async Task AddMessageWithCompressionAsync_WithoutCompressionService_ShouldUseTrimming() + { + // Arrange + var session = new ChatSession { MaxHistoryLength = 3 }; + + // Act + for (int i = 0; i < 5; i++) + { + await session.AddMessageWithCompressionAsync( + new ChatMessage { Role = ChatRole.User, Content = $"Message {i}" }, + 10, + 5 + ); + } + + // Assert + session.GetMessageCount().Should().BeLessThanOrEqualTo(3); + } + + [Fact] + public async Task AddUserMessageWithCompressionAsync_ShouldAddMessage() + { + // Arrange + var session = new ChatSession { ChatType = "private" }; + + // Act + await session.AddUserMessageWithCompressionAsync("Test message", "user", 10, 5); + + // Assert + session.GetMessageCount().Should().Be(1); + session.GetAllMessages()[0].Content.Should().Be("Test message"); + } + + [Fact] + public async Task AddUserMessageWithCompressionAsync_InGroupChat_ShouldIncludeUsername() + { + // Arrange + var session = new ChatSession { ChatType = "group" }; + + // Act + await session.AddUserMessageWithCompressionAsync("Test message", "user", 10, 5); + + // Assert + session.GetMessageCount().Should().Be(1); + session.GetAllMessages()[0].Content.Should().Be("user: Test message"); + } + + [Fact] + public async Task AddAssistantMessageWithCompressionAsync_ShouldAddMessage() + { + // Arrange + var session = new ChatSession(); + + // Act + await session.AddAssistantMessageWithCompressionAsync("Test response", 10, 5); + + // Assert + session.GetMessageCount().Should().Be(1); + session.GetAllMessages()[0].Role.Should().Be(ChatRole.Assistant); + session.GetAllMessages()[0].Content.Should().Be("Test response"); + } + + [Fact] + public async Task AddMessageWithCompressionAsync_ShouldTriggerTrimming_WhenNoCompressionService() + { + // Arrange + var session = new ChatSession { MaxHistoryLength = 2 }; + + // Act + for (int i = 0; i < 4; i++) + { + await session.AddMessageWithCompressionAsync( + new ChatMessage { Role = ChatRole.User, Content = $"Message {i}" }, + 2, + 1 + ); + } + + // Assert + session.GetMessageCount().Should().BeLessThanOrEqualTo(2); + } + + [Fact] + public async Task ClearHistory_ShouldUpdateLastUpdatedAt() + { + // Arrange + var session = new ChatSession(); + session.AddUserMessage("Test", "user"); + var lastUpdated = session.LastUpdatedAt; + await Task.Delay(10); // Small delay + + // Act + session.ClearHistory(); + + // Assert + session.LastUpdatedAt.Should().BeAfter(lastUpdated); + } + + [Fact] + public void SetCreatedAtForTesting_ShouldUpdateCreatedAt() + { + // Arrange + var session = new ChatSession(); + var targetDate = DateTime.UtcNow.AddDays(-5); + + // Act + session.SetCreatedAtForTesting(targetDate); + + // Assert + session.CreatedAt.Should().Be(targetDate); + } + + [Fact] + public void AddMessage_MultipleTimes_ShouldMaintainOrder() + { + // Arrange + var session = new ChatSession(); + + // Act + session.AddUserMessage("Message 1", "user1"); + session.AddAssistantMessage("Response 1"); + session.AddUserMessage("Message 2", "user1"); + session.AddAssistantMessage("Response 2"); + + // Assert + var messages = session.GetAllMessages(); + messages.Should().HaveCount(4); + messages[0].Content.Should().Be("Message 1"); + messages[1].Content.Should().Be("Response 1"); + messages[2].Content.Should().Be("Message 2"); + messages[3].Content.Should().Be("Response 2"); + } + + [Fact] + public void AddMessage_WithSystemMessage_ShouldPreserveSystemMessage() + { + // Arrange + var session = new ChatSession { MaxHistoryLength = 3 }; + session.AddMessage(new ChatMessage { Role = ChatRole.System, Content = "System prompt" }); + + // Act + for (int i = 0; i < 5; i++) + { + session.AddUserMessage($"Message {i}", "user"); + } + + // Assert + var messages = session.GetAllMessages(); + messages[0].Role.Should().Be(ChatRole.System); + messages[0].Content.Should().Be("System prompt"); + } + + [Fact] + public void GetAllMessages_ShouldReturnCopy() + { + // Arrange + var session = new ChatSession(); + session.AddUserMessage("Test", "user"); + + // Act + var messages1 = session.GetAllMessages(); + var messages2 = session.GetAllMessages(); + + // Assert + messages1.Should().NotBeSameAs(messages2); + messages1.Should().BeEquivalentTo(messages2); + } + + [Fact] + public async Task ChatSession_ThreadSafety_MultipleConcurrentAdds() + { + // Arrange + var session = new ChatSession(); + var tasks = new List(); + + // Act + for (int i = 0; i < 10; i++) + { + int messageNum = i; + tasks.Add(Task.Run(() => session.AddUserMessage($"Message {messageNum}", "user"))); + } + + await Task.WhenAll(tasks.ToArray()); + + // Assert + session.GetMessageCount().Should().Be(10); + } + + [Fact] + public void MaxHistoryLength_ShouldBeSettable() + { + // Arrange & Act + var session = new ChatSession { MaxHistoryLength = 50 }; + + // Assert + session.MaxHistoryLength.Should().Be(50); + } + + [Fact] + public void Model_ShouldBeSettable() + { + // Arrange & Act + var session = new ChatSession { Model = "llama3" }; + + // Assert + session.Model.Should().Be("llama3"); + } + + [Fact] + public void SessionProperties_ShouldBeSettableViaInitializer() + { + // Arrange & Act + var session = new ChatSession + { + ChatId = 12345, + ChatType = "group", + ChatTitle = "Test Group", + Model = "llama3", + MaxHistoryLength = 50, + }; + + // Assert + session.ChatId.Should().Be(12345); + session.ChatType.Should().Be("group"); + session.ChatTitle.Should().Be("Test Group"); + session.Model.Should().Be("llama3"); + session.MaxHistoryLength.Should().Be(50); + } } diff --git a/ChatBot.Tests/Services/DatabaseInitializationServiceTests.cs b/ChatBot.Tests/Services/DatabaseInitializationServiceTests.cs index 1eefd18..87f2074 100644 --- a/ChatBot.Tests/Services/DatabaseInitializationServiceTests.cs +++ b/ChatBot.Tests/Services/DatabaseInitializationServiceTests.cs @@ -255,4 +255,328 @@ public class DatabaseInitializationServiceTests : UnitTestBase Times.Once ); } + + [Fact] + public async Task StartAsync_WhenDatabaseExists_ShouldLogAndMigrate() + { + // Arrange + var dbPath = $"TestDb_{Guid.NewGuid()}.db"; + var services = new ServiceCollection(); + services.AddDbContext(options => + options.UseSqlite($"Data Source={dbPath}") + .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)) + ); + + var serviceProvider = services.BuildServiceProvider(); + var loggerMock = new Mock>(); + + var service = new DatabaseInitializationService(serviceProvider, loggerMock.Object); + + try + { + // Act + await service.StartAsync(CancellationToken.None); + + // Assert + loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is( + (v, t) => v.ToString()!.Contains("Starting database initialization") + ), + It.IsAny(), + It.IsAny>() + ), + Times.Once + ); + + loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is( + (v, t) => + v.ToString()! + .Contains("Database initialization completed successfully") + ), + It.IsAny(), + It.IsAny>() + ), + Times.Once + ); + } + finally + { + // Cleanup + serviceProvider.Dispose(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + if (File.Exists(dbPath)) + { + try { File.Delete(dbPath); } catch { /* Ignore cleanup errors */ } + } + } + } + + [Fact] + public async Task StopAsync_WithCancellationToken_ShouldComplete() + { + // Arrange + var serviceProviderMock = new Mock(); + var loggerMock = new Mock>(); + var service = new DatabaseInitializationService( + serviceProviderMock.Object, + loggerMock.Object + ); + var cts = new CancellationTokenSource(); + + // Act + await service.StopAsync(cts.Token); + + // Assert + loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is( + (v, t) => v.ToString()!.Contains("Database initialization service stopped") + ), + It.IsAny(), + It.IsAny>() + ), + Times.Once + ); + } + + [Fact] + public async Task StopAsync_WhenCancellationRequested_ShouldStillComplete() + { + // Arrange + var serviceProviderMock = new Mock(); + var loggerMock = new Mock>(); + var service = new DatabaseInitializationService( + serviceProviderMock.Object, + loggerMock.Object + ); + var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act + await service.StopAsync(cts.Token); + + // Assert + loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is( + (v, t) => v.ToString()!.Contains("Database initialization service stopped") + ), + It.IsAny(), + It.IsAny>() + ), + Times.Once + ); + } + + [Fact] + public async Task StartAsync_ShouldHandleDatabaseDoesNotExistException() + { + // Arrange + var dbPath = $"TestDb_{Guid.NewGuid()}.db"; + var services = new ServiceCollection(); + services.AddDbContext(options => + options.UseSqlite($"Data Source={dbPath}") + .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)) + ); + + var serviceProvider = services.BuildServiceProvider(); + var loggerMock = new Mock>(); + + var service = new DatabaseInitializationService(serviceProvider, loggerMock.Object); + + try + { + // Act + await service.StartAsync(CancellationToken.None); + + // Assert - service should complete successfully + loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is( + (v, t) => + v.ToString()! + .Contains("Database initialization completed successfully") + ), + It.IsAny(), + It.IsAny>() + ), + Times.Once + ); + } + finally + { + // Cleanup + serviceProvider.Dispose(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + if (File.Exists(dbPath)) + { + try { File.Delete(dbPath); } catch { /* Ignore cleanup errors */ } + } + } + } + + [Fact] + public async Task StartAsync_WithValidDatabase_ShouldLogDatabaseExists() + { + // Arrange + var dbPath = $"TestDb_{Guid.NewGuid()}.db"; + var services = new ServiceCollection(); + services.AddDbContext(options => + options.UseSqlite($"Data Source={dbPath}") + .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)) + ); + + var serviceProvider = services.BuildServiceProvider(); + var loggerMock = new Mock>(); + + var service = new DatabaseInitializationService(serviceProvider, loggerMock.Object); + + try + { + // Act + await service.StartAsync(CancellationToken.None); + + // Assert + loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => true), // Any log message + It.IsAny(), + It.IsAny>() + ), + Times.AtLeastOnce + ); + } + finally + { + // Cleanup + serviceProvider.Dispose(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + if (File.Exists(dbPath)) + { + try { File.Delete(dbPath); } catch { /* Ignore cleanup errors */ } + } + } + } + + [Fact] + public void DatabaseInitializationService_ShouldImplementIHostedService() + { + // Arrange + var serviceProviderMock = new Mock(); + var loggerMock = new Mock>(); + + // Act + var service = new DatabaseInitializationService( + serviceProviderMock.Object, + loggerMock.Object + ); + + // Assert + service.Should().BeAssignableTo(); + } + + [Fact] + public async Task StartAsync_MultipleCallsInSequence_ShouldWork() + { + // Arrange + var dbPath = $"TestDb_{Guid.NewGuid()}.db"; + var services = new ServiceCollection(); + services.AddDbContext(options => + options.UseSqlite($"Data Source={dbPath}") + .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)) + ); + + var serviceProvider = services.BuildServiceProvider(); + var loggerMock = new Mock>(); + var service = new DatabaseInitializationService(serviceProvider, loggerMock.Object); + + try + { + // Act + await service.StartAsync(CancellationToken.None); + await service.StopAsync(CancellationToken.None); + + // Assert - should complete without exceptions + loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is( + (v, t) => + v.ToString()! + .Contains("Database initialization completed successfully") + ), + It.IsAny(), + It.IsAny>() + ), + Times.Once + ); + } + finally + { + // Cleanup + serviceProvider.Dispose(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + if (File.Exists(dbPath)) + { + try { File.Delete(dbPath); } catch { /* Ignore cleanup errors */ } + } + } + } + + [Fact] + public async Task StopAsync_WithoutStartAsync_ShouldComplete() + { + // Arrange + var serviceProviderMock = new Mock(); + var loggerMock = new Mock>(); + var service = new DatabaseInitializationService( + serviceProviderMock.Object, + loggerMock.Object + ); + + // Act + await service.StopAsync(CancellationToken.None); + + // Assert - should complete without exceptions + loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is( + (v, t) => v.ToString()!.Contains("Database initialization service stopped") + ), + It.IsAny(), + It.IsAny>() + ), + Times.Once + ); + } } diff --git a/ChatBot.Tests/Services/DatabaseSessionStorageTests.cs b/ChatBot.Tests/Services/DatabaseSessionStorageTests.cs index 83056ff..9008e17 100644 --- a/ChatBot.Tests/Services/DatabaseSessionStorageTests.cs +++ b/ChatBot.Tests/Services/DatabaseSessionStorageTests.cs @@ -176,4 +176,265 @@ public class DatabaseSessionStorageTests : TestBase result.Should().Be(expectedCount); _repositoryMock.Verify(x => x.CleanupOldSessionsAsync(24), Times.Once); } + + [Fact] + public void GetOrCreate_ShouldThrowInvalidOperationException_WhenRepositoryThrows() + { + // Arrange + _repositoryMock + .Setup(x => x.GetOrCreateAsync(12345, "private", "Test Chat")) + .ThrowsAsync(new Exception("Database error")); + + // Act + var act = () => _sessionStorage.GetOrCreate(12345, "private", "Test Chat"); + + // Assert + act.Should() + .Throw() + .WithMessage("Failed to get or create session for chat 12345") + .WithInnerException() + .WithMessage("Database error"); + } + + [Fact] + public void Get_ShouldReturnNull_WhenRepositoryThrows() + { + // Arrange + _repositoryMock + .Setup(x => x.GetByChatIdAsync(12345)) + .ThrowsAsync(new Exception("Database error")); + + // Act + var result = _sessionStorage.Get(12345); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public async Task SaveSessionAsync_ShouldLogWarning_WhenSessionNotFound() + { + // Arrange + var session = TestDataBuilder.ChatSessions.CreateBasicSession(12345, "private"); + _repositoryMock + .Setup(x => x.GetByChatIdAsync(12345)) + .ReturnsAsync((ChatSessionEntity?)null); + + // Act + await _sessionStorage.SaveSessionAsync(session); + + // Assert + _repositoryMock.Verify(x => x.GetByChatIdAsync(12345), Times.Once); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SaveSessionAsync_ShouldThrowInvalidOperationException_WhenRepositoryThrows() + { + // Arrange + var session = TestDataBuilder.ChatSessions.CreateBasicSession(12345, "private"); + _repositoryMock + .Setup(x => x.GetByChatIdAsync(12345)) + .ThrowsAsync(new Exception("Database error")); + + // Act + var act = async () => await _sessionStorage.SaveSessionAsync(session); + + // Assert + var exception = await act.Should() + .ThrowAsync() + .WithMessage("Failed to save session for chat 12345"); + exception + .And.InnerException.Should() + .BeOfType() + .Which.Message.Should() + .Be("Database error"); + } + + [Fact] + public async Task SaveSessionAsync_ShouldClearMessagesAndAddNew() + { + // Arrange + var session = TestDataBuilder.ChatSessions.CreateBasicSession(12345, "private"); + session.AddUserMessage("Test message", "user1"); + session.AddAssistantMessage("Test response"); + + var sessionEntity = TestDataBuilder.Mocks.CreateChatSessionEntity(); + _repositoryMock.Setup(x => x.GetByChatIdAsync(12345)).ReturnsAsync(sessionEntity); + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny())) + .ReturnsAsync(sessionEntity); + + // Act + await _sessionStorage.SaveSessionAsync(session); + + // Assert + _repositoryMock.Verify(x => x.ClearMessagesAsync(It.IsAny()), Times.Once); + _repositoryMock.Verify( + x => + x.AddMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Exactly(2) + ); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny()), Times.Once); + } + + [Fact] + public void Remove_ShouldReturnFalse_WhenRepositoryThrows() + { + // Arrange + _repositoryMock + .Setup(x => x.DeleteAsync(12345)) + .ThrowsAsync(new Exception("Database error")); + + // Act + var result = _sessionStorage.Remove(12345); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void GetActiveSessionsCount_ShouldReturnZero_WhenRepositoryThrows() + { + // Arrange + _repositoryMock + .Setup(x => x.GetActiveSessionsCountAsync()) + .ThrowsAsync(new Exception("Database error")); + + // Act + var result = _sessionStorage.GetActiveSessionsCount(); + + // Assert + result.Should().Be(0); + } + + [Fact] + public void CleanupOldSessions_ShouldReturnZero_WhenRepositoryThrows() + { + // Arrange + _repositoryMock + .Setup(x => x.CleanupOldSessionsAsync(24)) + .ThrowsAsync(new Exception("Database error")); + + // Act + var result = _sessionStorage.CleanupOldSessions(24); + + // Assert + result.Should().Be(0); + } + + [Fact] + public void GetOrCreate_WithCompressionService_ShouldSetCompressionService() + { + // Arrange + var compressionServiceMock = TestDataBuilder.Mocks.CreateCompressionServiceMock(); + var storageWithCompression = new DatabaseSessionStorage( + _repositoryMock.Object, + Mock.Of>(), + compressionServiceMock.Object + ); + + var sessionEntity = TestDataBuilder.Mocks.CreateChatSessionEntity(); + _repositoryMock + .Setup(x => x.GetOrCreateAsync(12345, "private", "Test Chat")) + .ReturnsAsync(sessionEntity); + + // Act + var result = storageWithCompression.GetOrCreate(12345, "private", "Test Chat"); + + // Assert + result.Should().NotBeNull(); + result.ChatId.Should().Be(12345); + } + + [Fact] + public void Get_WithCompressionService_ShouldSetCompressionService() + { + // Arrange + var loggerMock = new Mock>(); + var compressionServiceMock = TestDataBuilder.Mocks.CreateCompressionServiceMock(); + var storageWithCompression = new DatabaseSessionStorage( + _repositoryMock.Object, + loggerMock.Object, + compressionServiceMock.Object + ); + + var sessionEntity = TestDataBuilder.Mocks.CreateChatSessionEntity(); + sessionEntity.Messages.Add( + new ChatMessageEntity + { + Id = 1, + SessionId = sessionEntity.Id, + Content = "Test", + Role = "user", + MessageOrder = 0, + CreatedAt = DateTime.UtcNow, + } + ); + + _repositoryMock.Setup(x => x.GetByChatIdAsync(12345)).ReturnsAsync(sessionEntity); + + // Act + var result = storageWithCompression.Get(12345); + + // Assert + _repositoryMock.Verify(x => x.GetByChatIdAsync(12345), Times.Once); + result.Should().NotBeNull(); + result!.GetMessageCount().Should().Be(1); + } + + [Fact] + public async Task SaveSessionAsync_WithMultipleMessages_ShouldSaveInCorrectOrder() + { + // Arrange + var session = TestDataBuilder.ChatSessions.CreateBasicSession(12345, "private"); + session.AddUserMessage("Message 1", "user1"); + session.AddAssistantMessage("Response 1"); + session.AddUserMessage("Message 2", "user1"); + session.AddAssistantMessage("Response 2"); + + var sessionEntity = TestDataBuilder.Mocks.CreateChatSessionEntity(); + _repositoryMock.Setup(x => x.GetByChatIdAsync(12345)).ReturnsAsync(sessionEntity); + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny())) + .ReturnsAsync(sessionEntity); + + // Act + await _sessionStorage.SaveSessionAsync(session); + + // Assert + _repositoryMock.Verify(x => x.ClearMessagesAsync(It.IsAny()), Times.Once); + _repositoryMock.Verify( + x => + x.AddMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Exactly(4) + ); + } + + [Fact] + public void GetOrCreate_WithDefaultParameters_ShouldUseDefaults() + { + // Arrange + var sessionEntity = TestDataBuilder.Mocks.CreateChatSessionEntity(); + _repositoryMock + .Setup(x => x.GetOrCreateAsync(12345, "private", "")) + .ReturnsAsync(sessionEntity); + + // Act + var result = _sessionStorage.GetOrCreate(12345); + + // Assert + result.Should().NotBeNull(); + _repositoryMock.Verify(x => x.GetOrCreateAsync(12345, "private", ""), Times.Once); + } } diff --git a/ChatBot.Tests/Services/Telegram/TelegramMessageSenderWrapperTests.cs b/ChatBot.Tests/Services/Telegram/TelegramMessageSenderWrapperTests.cs new file mode 100644 index 0000000..0c359b5 --- /dev/null +++ b/ChatBot.Tests/Services/Telegram/TelegramMessageSenderWrapperTests.cs @@ -0,0 +1,46 @@ +using ChatBot.Services.Telegram.Services; +using ChatBot.Tests.TestUtilities; +using FluentAssertions; +using Moq; +using Telegram.Bot; +using Telegram.Bot.Types; +using Xunit; + +namespace ChatBot.Tests.Services.Telegram; + +public class TelegramMessageSenderWrapperTests : UnitTestBase +{ + private readonly Mock _botClientMock; + private readonly TelegramMessageSenderWrapper _wrapper; + + public TelegramMessageSenderWrapperTests() + { + _botClientMock = TestDataBuilder.Mocks.CreateTelegramBotClient(); + _wrapper = new TelegramMessageSenderWrapper(_botClientMock.Object); + } + + [Fact] + public void Constructor_ShouldInitializeCorrectly() + { + // Arrange + var botClient = TestDataBuilder.Mocks.CreateTelegramBotClient().Object; + + // Act + var wrapper = new TelegramMessageSenderWrapper(botClient); + + // Assert + wrapper.Should().NotBeNull(); + } + + [Fact] + public void SendMessageAsync_ShouldBePublicMethod() + { + // Arrange & Act + var method = typeof(TelegramMessageSenderWrapper).GetMethod("SendMessageAsync"); + + // Assert + method.Should().NotBeNull(); + method!.IsPublic.Should().BeTrue(); + method.ReturnType.Should().Be>(); + } +} diff --git a/ChatBot.Tests/Services/TelegramBotClientWrapperTests.cs b/ChatBot.Tests/Services/TelegramBotClientWrapperTests.cs index 6f5fa88..84cf3c1 100644 --- a/ChatBot.Tests/Services/TelegramBotClientWrapperTests.cs +++ b/ChatBot.Tests/Services/TelegramBotClientWrapperTests.cs @@ -329,4 +329,8 @@ public class TelegramBotClientWrapperTests : UnitTestBase var attributes = returnType.GetCustomAttributes(false); attributes.Should().NotBeNull(); } + + // Note: Tests for GetMeAsync removed because GetMe is an extension method + // and cannot be mocked with Moq. The wrapper simply delegates to the + // TelegramBotClient extension method, which is tested by the Telegram.Bot library itself. } diff --git a/ChatBot.Tests/Telegram/Commands/SettingsCommandTests.cs b/ChatBot.Tests/Telegram/Commands/SettingsCommandTests.cs index 49d18e7..eceb0e2 100644 --- a/ChatBot.Tests/Telegram/Commands/SettingsCommandTests.cs +++ b/ChatBot.Tests/Telegram/Commands/SettingsCommandTests.cs @@ -10,16 +10,15 @@ namespace ChatBot.Tests.Telegram.Commands; public class SettingsCommandTests : UnitTestBase { - private readonly Mock _sessionStorageMock; + private readonly Mock _chatServiceMock; private readonly SettingsCommand _settingsCommand; public SettingsCommandTests() { - _sessionStorageMock = TestDataBuilder.Mocks.CreateSessionStorageMock(); - var chatServiceMock = new Mock( + _chatServiceMock = new Mock( TestDataBuilder.Mocks.CreateLoggerMock().Object, TestDataBuilder.Mocks.CreateAIServiceMock().Object, - _sessionStorageMock.Object, + TestDataBuilder.Mocks.CreateSessionStorageMock().Object, TestDataBuilder .Mocks.CreateOptionsMock(TestDataBuilder.Configurations.CreateAISettings()) .Object, @@ -33,7 +32,7 @@ public class SettingsCommandTests : UnitTestBase ); var aiSettingsMock = TestDataBuilder.Mocks.CreateOptionsMock(new AISettings()); _settingsCommand = new SettingsCommand( - chatServiceMock.Object, + _chatServiceMock.Object, modelServiceMock.Object, aiSettingsMock.Object ); @@ -45,7 +44,7 @@ public class SettingsCommandTests : UnitTestBase // Arrange var chatId = 12345L; var session = TestDataBuilder.ChatSessions.CreateBasicSession(chatId); - _sessionStorageMock.Setup(x => x.Get(chatId)).Returns(session); + _chatServiceMock.Setup(x => x.GetSession(chatId)).Returns(session); var context = new TelegramCommandContext { @@ -70,7 +69,9 @@ public class SettingsCommandTests : UnitTestBase { // Arrange var chatId = 12345L; - _sessionStorageMock.Setup(x => x.Get(chatId)).Returns((ChatBot.Models.ChatSession?)null); + _chatServiceMock + .Setup(x => x.GetSession(chatId)) + .Returns((ChatBot.Models.ChatSession?)null); var context = new TelegramCommandContext { diff --git a/ChatBot.Tests/Telegram/Commands/StatusCommandTests.cs b/ChatBot.Tests/Telegram/Commands/StatusCommandTests.cs index b9e3ed4..bc34a85 100644 --- a/ChatBot.Tests/Telegram/Commands/StatusCommandTests.cs +++ b/ChatBot.Tests/Telegram/Commands/StatusCommandTests.cs @@ -155,4 +155,284 @@ public class StatusCommandTests : UnitTestBase result.Should().Contain("системы"); result.Should().Contain("Доступен"); } + + [Fact] + public async Task ExecuteAsync_ShouldReturnTimeoutStatus_WhenRequestTimesOut() + { + // Arrange + var context = new TelegramCommandContext + { + ChatId = 12345, + Username = "testuser", + MessageText = "/status", + ChatType = "private", + ChatTitle = "Test Chat", + }; + + _ollamaClientMock + .Setup(x => x.ChatAsync(It.IsAny())) + .Throws(); + + // Act + var result = await _statusCommand.ExecuteAsync(context); + + // Assert + result.Should().NotBeNull(); + result.Should().Contain("Таймаут"); + } + + [Fact] + public async Task ExecuteAsync_ShouldReturnHttpError502_WhenBadGateway() + { + // Arrange + var context = new TelegramCommandContext + { + ChatId = 12345, + Username = "testuser", + MessageText = "/status", + ChatType = "private", + ChatTitle = "Test Chat", + }; + + var httpException = new HttpRequestException( + "Response status code does not indicate success: 502" + ); + _ollamaClientMock + .Setup(x => x.ChatAsync(It.IsAny())) + .Throws(httpException); + + // Act + var result = await _statusCommand.ExecuteAsync(context); + + // Assert + result.Should().NotBeNull(); + result.Should().Contain("502"); + result.Should().Contain("Bad Gateway"); + } + + [Fact] + public async Task ExecuteAsync_ShouldReturnHttpError503_WhenServiceUnavailable() + { + // Arrange + var context = new TelegramCommandContext + { + ChatId = 12345, + Username = "testuser", + MessageText = "/status", + ChatType = "private", + ChatTitle = "Test Chat", + }; + + var httpException = new HttpRequestException( + "Response status code does not indicate success: 503" + ); + _ollamaClientMock + .Setup(x => x.ChatAsync(It.IsAny())) + .Throws(httpException); + + // Act + var result = await _statusCommand.ExecuteAsync(context); + + // Assert + result.Should().NotBeNull(); + result.Should().Contain("503"); + result.Should().Contain("Service Unavailable"); + } + + [Fact] + public async Task ExecuteAsync_ShouldReturnHttpError504_WhenGatewayTimeout() + { + // Arrange + var context = new TelegramCommandContext + { + ChatId = 12345, + Username = "testuser", + MessageText = "/status", + ChatType = "private", + ChatTitle = "Test Chat", + }; + + var httpException = new HttpRequestException( + "Response status code does not indicate success: 504" + ); + _ollamaClientMock + .Setup(x => x.ChatAsync(It.IsAny())) + .Throws(httpException); + + // Act + var result = await _statusCommand.ExecuteAsync(context); + + // Assert + result.Should().NotBeNull(); + result.Should().Contain("504"); + result.Should().Contain("Gateway Timeout"); + } + + [Fact] + public async Task ExecuteAsync_ShouldReturnHttpError429_WhenTooManyRequests() + { + // Arrange + var context = new TelegramCommandContext + { + ChatId = 12345, + Username = "testuser", + MessageText = "/status", + ChatType = "private", + ChatTitle = "Test Chat", + }; + + var httpException = new HttpRequestException( + "Response status code does not indicate success: 429" + ); + _ollamaClientMock + .Setup(x => x.ChatAsync(It.IsAny())) + .Throws(httpException); + + // Act + var result = await _statusCommand.ExecuteAsync(context); + + // Assert + result.Should().NotBeNull(); + result.Should().Contain("429"); + result.Should().Contain("Too Many Requests"); + } + + [Fact] + public async Task ExecuteAsync_ShouldReturnHttpError500_WhenInternalServerError() + { + // Arrange + var context = new TelegramCommandContext + { + ChatId = 12345, + Username = "testuser", + MessageText = "/status", + ChatType = "private", + ChatTitle = "Test Chat", + }; + + var httpException = new HttpRequestException( + "Response status code does not indicate success: 500" + ); + _ollamaClientMock + .Setup(x => x.ChatAsync(It.IsAny())) + .Throws(httpException); + + // Act + var result = await _statusCommand.ExecuteAsync(context); + + // Assert + result.Should().NotBeNull(); + result.Should().Contain("500"); + result.Should().Contain("Internal Server Error"); + } + + [Fact] + public async Task ExecuteAsync_ShouldReturnNoResponseStatus_WhenResponseIsEmpty() + { + // Arrange + var context = new TelegramCommandContext + { + ChatId = 12345, + Username = "testuser", + MessageText = "/status", + ChatType = "private", + ChatTitle = "Test Chat", + }; + + // Return empty response + _ollamaClientMock + .Setup(x => x.ChatAsync(It.IsAny())) + .Returns( + TestDataBuilder.Mocks.CreateAsyncEnumerable( + new List + { + new OllamaSharp.Models.Chat.ChatResponseStream { Message = null! }, + } + ) + ); + + // Act + var result = await _statusCommand.ExecuteAsync(context); + + // Assert + result.Should().NotBeNull(); + result.Should().Contain("Нет ответа"); + } + + [Fact] + public async Task ExecuteAsync_WithSession_ShouldShowSessionInfo() + { + // Arrange + var chatServiceMock = new Mock( + TestDataBuilder.Mocks.CreateLoggerMock().Object, + TestDataBuilder.Mocks.CreateAIServiceMock().Object, + TestDataBuilder.Mocks.CreateSessionStorageMock().Object, + TestDataBuilder + .Mocks.CreateOptionsMock(TestDataBuilder.Configurations.CreateAISettings()) + .Object, + TestDataBuilder.Mocks.CreateCompressionServiceMock().Object + ); + + var session = TestDataBuilder.ChatSessions.CreateBasicSession(12345, "private"); + session.AddUserMessage("Test", "user"); + chatServiceMock.Setup(x => x.GetSession(12345)).Returns(session); + + var statusCommand = new StatusCommand( + chatServiceMock.Object, + new Mock( + TestDataBuilder.Mocks.CreateLoggerMock().Object, + _ollamaOptionsMock.Object + ).Object, + TestDataBuilder.Mocks.CreateOptionsMock(new AISettings()).Object, + _ollamaClientMock.Object + ); + + var context = new TelegramCommandContext + { + ChatId = 12345, + Username = "testuser", + MessageText = "/status", + ChatType = "private", + ChatTitle = "Test Chat", + }; + + _ollamaClientMock + .Setup(x => x.ChatAsync(It.IsAny())) + .Returns( + TestDataBuilder.Mocks.CreateAsyncEnumerable( + new List + { + new OllamaSharp.Models.Chat.ChatResponseStream + { + Message = new OllamaSharp.Models.Chat.Message( + ChatRole.Assistant, + "Test" + ), + }, + } + ) + ); + + // Act + var result = await statusCommand.ExecuteAsync(context); + + // Assert + result.Should().NotBeNull(); + result.Should().Contain("Сессия"); + result.Should().Contain("Сообщений в истории"); + } + + [Fact] + public void CommandName_ShouldReturnCorrectName() + { + // Act & Assert + _statusCommand.CommandName.Should().Be("/status"); + } + + [Fact] + public void Description_ShouldReturnCorrectDescription() + { + // Act & Assert + _statusCommand.Description.Should().Be("Показать статус системы и API"); + } } diff --git a/ChatBot.Tests/Telegram/Commands/TelegramCommandProcessorTests.cs b/ChatBot.Tests/Telegram/Commands/TelegramCommandProcessorTests.cs index bb78cde..ef2fcc1 100644 --- a/ChatBot.Tests/Telegram/Commands/TelegramCommandProcessorTests.cs +++ b/ChatBot.Tests/Telegram/Commands/TelegramCommandProcessorTests.cs @@ -6,6 +6,7 @@ using ChatBot.Tests.TestUtilities; using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; +using Telegram.Bot; using Telegram.Bot.Types; namespace ChatBot.Tests.Telegram.Commands; @@ -417,8 +418,8 @@ public class TelegramCommandProcessorTests : UnitTestBase var username = "testuser"; var chatType = "private"; var chatTitle = "Test Chat"; - var cts = new CancellationTokenSource(); - cts.Cancel(); + using var cts = new CancellationTokenSource(); + await cts.CancelAsync(); // Act var result = await _processor.ProcessMessageAsync( @@ -488,4 +489,347 @@ public class TelegramCommandProcessorTests : UnitTestBase // Assert result.Should().NotBeNull(); } + + [Fact] + public async Task ProcessMessageAsync_WithReplyToBot_ShouldProcessMessage() + { + // Arrange + var botUser = new User + { + Id = 999, + Username = "testbot", + IsBot = true, + }; + + var botInfoServiceMock = new Mock( + TestDataBuilder.Mocks.CreateTelegramBotClient().Object, + TestDataBuilder.Mocks.CreateLoggerMock().Object + ); + botInfoServiceMock.Setup(x => x.GetBotInfoAsync(It.IsAny())) + .ReturnsAsync(botUser); + + var processor = new TelegramCommandProcessor( + _commandRegistry, + _chatService, + _loggerMock.Object, + botInfoServiceMock.Object + ); + + var replyInfo = new ReplyInfo + { + MessageId = 1, + UserId = 999, + Username = "testbot", + }; + + // Act + var result = await processor.ProcessMessageAsync( + "Test reply", + 12345L, + "user", + "private", + "Test Chat", + replyInfo + ); + + // Assert + result.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task ProcessMessageAsync_WithReplyToOtherUser_ShouldReturnEmpty() + { + // Arrange + var botUser = new User + { + Id = 999, + Username = "testbot", + IsBot = true, + }; + + var botInfoServiceMock = new Mock( + TestDataBuilder.Mocks.CreateTelegramBotClient().Object, + TestDataBuilder.Mocks.CreateLoggerMock().Object + ); + botInfoServiceMock.Setup(x => x.GetBotInfoAsync(It.IsAny())) + .ReturnsAsync(botUser); + + var processor = new TelegramCommandProcessor( + _commandRegistry, + _chatService, + _loggerMock.Object, + botInfoServiceMock.Object + ); + + var replyInfo = new ReplyInfo + { + MessageId = 1, + UserId = 123, + Username = "otheruser", + }; + + // Act + var result = await processor.ProcessMessageAsync( + "Test reply", + 12345L, + "user", + "private", + "Test Chat", + replyInfo + ); + + // Assert + result.Should().BeEmpty(); + } + + [Fact] + public async Task ProcessMessageAsync_WithBotMention_ShouldProcessMessage() + { + // Arrange + var botUser = new User + { + Id = 999, + Username = "testbot", + IsBot = true, + }; + + var botInfoServiceMock = new Mock( + TestDataBuilder.Mocks.CreateTelegramBotClient().Object, + TestDataBuilder.Mocks.CreateLoggerMock().Object + ); + botInfoServiceMock.Setup(x => x.GetBotInfoAsync(It.IsAny())) + .ReturnsAsync(botUser); + + var processor = new TelegramCommandProcessor( + _commandRegistry, + _chatService, + _loggerMock.Object, + botInfoServiceMock.Object + ); + + // Act + var result = await processor.ProcessMessageAsync( + "Hello @testbot", + 12345L, + "user", + "private", + "Test Chat" + ); + + // Assert + result.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task ProcessMessageAsync_WithOtherUserMention_ShouldReturnEmpty() + { + // Arrange + var botUser = new User + { + Id = 999, + Username = "testbot", + IsBot = true, + }; + + var botInfoServiceMock = new Mock( + TestDataBuilder.Mocks.CreateTelegramBotClient().Object, + TestDataBuilder.Mocks.CreateLoggerMock().Object + ); + botInfoServiceMock.Setup(x => x.GetBotInfoAsync(It.IsAny())) + .ReturnsAsync(botUser); + + var processor = new TelegramCommandProcessor( + _commandRegistry, + _chatService, + _loggerMock.Object, + botInfoServiceMock.Object + ); + + // Act + var result = await processor.ProcessMessageAsync( + "Hello @otheruser", + 12345L, + "user", + "private", + "Test Chat" + ); + + // Assert + result.Should().BeEmpty(); + } + + [Fact] + public async Task ProcessMessageAsync_WithCommand_ShouldExecuteCommand() + { + // Arrange + var commandMock = new Mock(); + commandMock.Setup(x => x.CommandName).Returns("/test"); + commandMock + .Setup(x => x.CanHandle(It.IsAny())) + .Returns((string msg) => msg.StartsWith("/test")); + commandMock + .Setup(x => + x.ExecuteAsync(It.IsAny(), It.IsAny()) + ) + .ReturnsAsync("Command executed"); + + var commandRegistry = new CommandRegistry( + TestDataBuilder.Mocks.CreateLoggerMock().Object, + new[] { commandMock.Object } + ); + + var processor = new TelegramCommandProcessor( + commandRegistry, + _chatService, + _loggerMock.Object, + _botInfoService + ); + + // Act + var result = await processor.ProcessMessageAsync( + "/test argument", + 12345L, + "user", + "private", + "Test Chat" + ); + + // Assert + result.Should().Be("Command executed"); + commandMock.Verify( + x => x.ExecuteAsync(It.IsAny(), It.IsAny()), + Times.Once + ); + } + + [Fact] + public async Task ProcessMessageAsync_WithException_ShouldReturnErrorMessage() + { + // Arrange + var chatServiceMock = new Mock( + TestDataBuilder.Mocks.CreateLoggerMock().Object, + TestDataBuilder.Mocks.CreateAIServiceMock().Object, + TestDataBuilder.Mocks.CreateSessionStorageMock().Object, + TestDataBuilder + .Mocks.CreateOptionsMock(TestDataBuilder.Configurations.CreateAISettings()) + .Object, + TestDataBuilder.Mocks.CreateCompressionServiceMock().Object + ); + + chatServiceMock + .Setup(x => + x.ProcessMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ThrowsAsync(new Exception("Test error")); + + var processor = new TelegramCommandProcessor( + _commandRegistry, + chatServiceMock.Object, + _loggerMock.Object, + _botInfoService + ); + + // Act + var result = await processor.ProcessMessageAsync( + "Test message", + 12345L, + "user", + "private", + "Test Chat" + ); + + // Assert + result.Should().Contain("Произошла ошибка"); + } + + [Fact] + public async Task ProcessMessageAsync_WithGroupChat_ShouldProcessCorrectly() + { + // Arrange + var messageText = "Hello"; + var chatId = -100123456789L; // Group chat ID + var username = "testuser"; + var chatType = "group"; + var chatTitle = "Test Group"; + + // Act + var result = await _processor.ProcessMessageAsync( + messageText, + chatId, + username, + chatType, + chatTitle + ); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public async Task ProcessMessageAsync_WithSupergroupChat_ShouldProcessCorrectly() + { + // Arrange + var messageText = "Hello"; + var chatId = -100123456789L; + var username = "testuser"; + var chatType = "supergroup"; + var chatTitle = "Test Supergroup"; + + // Act + var result = await _processor.ProcessMessageAsync( + messageText, + chatId, + username, + chatType, + chatTitle + ); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public async Task ProcessMessageAsync_WithMultipleMentions_IncludingBot_ShouldProcessMessage() + { + // Arrange + var botUser = new User + { + Id = 999, + Username = "testbot", + IsBot = true, + }; + + var botInfoServiceMock = new Mock( + TestDataBuilder.Mocks.CreateTelegramBotClient().Object, + TestDataBuilder.Mocks.CreateLoggerMock().Object + ); + botInfoServiceMock.Setup(x => x.GetBotInfoAsync(It.IsAny())) + .ReturnsAsync(botUser); + + var processor = new TelegramCommandProcessor( + _commandRegistry, + _chatService, + _loggerMock.Object, + botInfoServiceMock.Object + ); + + // Act + var result = await processor.ProcessMessageAsync( + "Hello @testbot and @otheruser", + 12345L, + "user", + "group", + "Test Group" + ); + + // Assert + result.Should().NotBeNullOrEmpty(); + } } diff --git a/ChatBot/Services/ChatService.cs b/ChatBot/Services/ChatService.cs index 0d30a2a..aaf2f87 100644 --- a/ChatBot/Services/ChatService.cs +++ b/ChatBot/Services/ChatService.cs @@ -55,7 +55,7 @@ namespace ChatBot.Services /// /// Process a user message and get AI response /// - public async Task ProcessMessageAsync( + public virtual async Task ProcessMessageAsync( long chatId, string username, string message, @@ -192,7 +192,7 @@ namespace ChatBot.Services /// /// Get session information /// - public ChatSession? GetSession(long chatId) + public virtual ChatSession? GetSession(long chatId) { return _sessionStorage.Get(chatId); } diff --git a/ChatBot/Services/DatabaseSessionStorage.cs b/ChatBot/Services/DatabaseSessionStorage.cs index dce80f7..56988e0 100644 --- a/ChatBot/Services/DatabaseSessionStorage.cs +++ b/ChatBot/Services/DatabaseSessionStorage.cs @@ -178,7 +178,14 @@ namespace ChatBot.Services // Add messages to session foreach (var messageEntity in entity.Messages.OrderBy(m => m.MessageOrder)) { - var role = Enum.Parse(messageEntity.Role); + var role = messageEntity.Role.ToLowerInvariant() switch + { + "user" => ChatRole.User, + "assistant" => ChatRole.Assistant, + "system" => ChatRole.System, + "tool" => ChatRole.Tool, + _ => throw new ArgumentException($"Unknown role: {messageEntity.Role}") + }; var message = new ChatMessage { Content = messageEntity.Content, Role = role }; session.AddMessage(message); } diff --git a/ChatBot/Services/Telegram/Services/BotInfoService.cs b/ChatBot/Services/Telegram/Services/BotInfoService.cs index 38ecb5a..f78e4a0 100644 --- a/ChatBot/Services/Telegram/Services/BotInfoService.cs +++ b/ChatBot/Services/Telegram/Services/BotInfoService.cs @@ -24,7 +24,7 @@ namespace ChatBot.Services.Telegram.Services /// /// Получает информацию о боте (с кэшированием и автоматической инвалидацией) /// - public async Task GetBotInfoAsync(CancellationToken cancellationToken = default) + public virtual async Task GetBotInfoAsync(CancellationToken cancellationToken = default) { // Проверяем, есть ли валидный кэш if ( diff --git a/ChatBot/Services/Telegram/Services/TelegramMessageSenderWrapper.cs b/ChatBot/Services/Telegram/Services/TelegramMessageSenderWrapper.cs index dc8641d..232a526 100644 --- a/ChatBot/Services/Telegram/Services/TelegramMessageSenderWrapper.cs +++ b/ChatBot/Services/Telegram/Services/TelegramMessageSenderWrapper.cs @@ -23,10 +23,16 @@ namespace ChatBot.Services.Telegram.Services CancellationToken cancellationToken = default ) { + ReplyParameters? replyParameters = null; + if (replyToMessageId > 0) + { + replyParameters = new ReplyParameters { MessageId = replyToMessageId }; + } + return await _botClient.SendMessage( chatId: chatId, text: text, - replyParameters: replyToMessageId, + replyParameters: replyParameters, cancellationToken: cancellationToken ); }