add tests

This commit is contained in:
Leonid Pershin
2025-10-17 05:47:18 +03:00
parent f7e3024e7e
commit 03eb0f22a2
41 changed files with 4001 additions and 30 deletions

View File

@@ -0,0 +1,209 @@
using System.Linq;
using ChatBot.Common.Constants;
using ChatBot.Models.Configuration;
using ChatBot.Models.Dto;
using ChatBot.Services;
using ChatBot.Services.Interfaces;
using ChatBot.Tests.TestUtilities;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using OllamaSharp.Models.Chat;
namespace ChatBot.Tests.Services;
public class AIServiceTests : UnitTestBase
{
private readonly Mock<ILogger<AIService>> _loggerMock;
private readonly Mock<ModelService> _modelServiceMock;
private readonly Mock<IOllamaClient> _ollamaClientMock;
private readonly Mock<SystemPromptService> _systemPromptServiceMock;
private readonly Mock<IHistoryCompressionService> _compressionServiceMock;
private readonly AISettings _aiSettings;
private readonly AIService _aiService;
public AIServiceTests()
{
_loggerMock = TestDataBuilder.Mocks.CreateLoggerMock<AIService>();
var ollamaSettings = TestDataBuilder.Configurations.CreateOllamaSettings();
var ollamaOptionsMock = TestDataBuilder.Mocks.CreateOptionsMock(ollamaSettings);
_modelServiceMock = new Mock<ModelService>(
Mock.Of<ILogger<ModelService>>(),
ollamaOptionsMock.Object
);
_ollamaClientMock = TestDataBuilder.Mocks.CreateOllamaClientMock();
_systemPromptServiceMock = new Mock<SystemPromptService>(
Mock.Of<ILogger<SystemPromptService>>(),
TestDataBuilder
.Mocks.CreateOptionsMock(TestDataBuilder.Configurations.CreateAISettings())
.Object
);
_compressionServiceMock = TestDataBuilder.Mocks.CreateCompressionServiceMock();
_aiSettings = TestDataBuilder.Configurations.CreateAISettings();
var optionsMock = TestDataBuilder.Mocks.CreateOptionsMock(_aiSettings);
_aiService = new AIService(
_loggerMock.Object,
_modelServiceMock.Object,
_ollamaClientMock.Object,
optionsMock.Object,
_systemPromptServiceMock.Object,
_compressionServiceMock.Object
);
}
[Fact]
public async Task GenerateChatCompletionAsync_ShouldReturnResponse_WhenSuccessful()
{
// Arrange
var messages = TestDataBuilder.ChatMessages.CreateMessageHistory(2);
var expectedResponse = "Test AI response";
var model = "llama3.2";
_modelServiceMock.Setup(x => x.GetCurrentModel()).Returns(model);
_systemPromptServiceMock.Setup(x => x.GetSystemPromptAsync()).ReturnsAsync("System prompt");
var responseBuilder = new System.Text.StringBuilder();
responseBuilder.Append(expectedResponse);
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Returns(
TestDataBuilder.Mocks.CreateAsyncEnumerable(
new List<OllamaSharp.Models.Chat.ChatResponseStream>
{
new OllamaSharp.Models.Chat.ChatResponseStream
{
Message = new Message(ChatRole.Assistant, expectedResponse),
},
}
)
);
// Act
var result = await _aiService.GenerateChatCompletionAsync(messages);
// Assert
result.Should().Be(expectedResponse);
_ollamaClientMock.Verify(
x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()),
Times.Once
);
}
[Fact]
public async Task GenerateChatCompletionAsync_ShouldThrowException_WhenOllamaClientThrows()
{
// Arrange
var messages = TestDataBuilder.ChatMessages.CreateMessageHistory(2);
var model = "llama3.2";
_modelServiceMock.Setup(x => x.GetCurrentModel()).Returns(model);
_systemPromptServiceMock.Setup(x => x.GetSystemPromptAsync()).ReturnsAsync("System prompt");
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Throws(new Exception("Ollama client error"));
// Act & Assert
var result = await _aiService.GenerateChatCompletionAsync(messages);
result.Should().Be(AIResponseConstants.DefaultErrorMessage);
}
[Fact]
public async Task GenerateChatCompletionWithCompressionAsync_ShouldUseCompression_WhenEnabled()
{
// Arrange
var messages = TestDataBuilder.ChatMessages.CreateMessageHistory(10);
var expectedResponse = "Test AI response with compression";
var model = "llama3.2";
_modelServiceMock.Setup(x => x.GetCurrentModel()).Returns(model);
_systemPromptServiceMock.Setup(x => x.GetSystemPromptAsync()).ReturnsAsync("System prompt");
_compressionServiceMock.Setup(x => x.ShouldCompress(20, 10)).Returns(true);
_compressionServiceMock
.Setup(x =>
x.CompressHistoryAsync(
It.IsAny<List<ChatMessage>>(),
5,
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync(messages.TakeLast(5).ToList());
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Returns(
TestDataBuilder.Mocks.CreateAsyncEnumerable(
new List<OllamaSharp.Models.Chat.ChatResponseStream>
{
new OllamaSharp.Models.Chat.ChatResponseStream
{
Message = new Message(ChatRole.Assistant, expectedResponse),
},
}
)
);
// Act
var result = await _aiService.GenerateChatCompletionWithCompressionAsync(messages);
// Assert
result.Should().Be(expectedResponse);
_compressionServiceMock.Verify(x => x.ShouldCompress(20, 10), Times.Once);
_compressionServiceMock.Verify(
x =>
x.CompressHistoryAsync(
It.IsAny<List<ChatMessage>>(),
5,
It.IsAny<CancellationToken>()
),
Times.Once
);
}
[Fact]
public async Task GenerateChatCompletionWithCompressionAsync_ShouldNotUseCompression_WhenNotNeeded()
{
// Arrange
var messages = TestDataBuilder.ChatMessages.CreateMessageHistory(3);
var expectedResponse = "Test AI response without compression";
var model = "llama3.2";
_modelServiceMock.Setup(x => x.GetCurrentModel()).Returns(model);
_systemPromptServiceMock.Setup(x => x.GetSystemPromptAsync()).ReturnsAsync("System prompt");
_compressionServiceMock.Setup(x => x.ShouldCompress(6, 10)).Returns(false);
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Returns(
TestDataBuilder.Mocks.CreateAsyncEnumerable(
new List<OllamaSharp.Models.Chat.ChatResponseStream>
{
new OllamaSharp.Models.Chat.ChatResponseStream
{
Message = new Message(ChatRole.Assistant, expectedResponse),
},
}
)
);
// Act
var result = await _aiService.GenerateChatCompletionWithCompressionAsync(messages);
// Assert
result.Should().Be(expectedResponse);
_compressionServiceMock.Verify(x => x.ShouldCompress(6, 10), Times.Once);
_compressionServiceMock.Verify(
x =>
x.CompressHistoryAsync(
It.IsAny<List<ChatMessage>>(),
It.IsAny<int>(),
It.IsAny<CancellationToken>()
),
Times.Never
);
}
}

View File

@@ -0,0 +1,321 @@
using ChatBot.Models;
using ChatBot.Models.Configuration;
using ChatBot.Models.Dto;
using ChatBot.Services;
using ChatBot.Services.Interfaces;
using ChatBot.Tests.TestUtilities;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
namespace ChatBot.Tests.Services;
public class ChatServiceTests : UnitTestBase
{
private readonly Mock<ILogger<ChatService>> _loggerMock;
private readonly Mock<IAIService> _aiServiceMock;
private readonly Mock<ISessionStorage> _sessionStorageMock;
private readonly Mock<IHistoryCompressionService> _compressionServiceMock;
private readonly AISettings _aiSettings;
private readonly ChatService _chatService;
public ChatServiceTests()
{
_loggerMock = TestDataBuilder.Mocks.CreateLoggerMock<ChatService>();
_aiServiceMock = TestDataBuilder.Mocks.CreateAIServiceMock();
_sessionStorageMock = TestDataBuilder.Mocks.CreateSessionStorageMock();
_compressionServiceMock = TestDataBuilder.Mocks.CreateCompressionServiceMock();
_aiSettings = TestDataBuilder.Configurations.CreateAISettings();
var optionsMock = TestDataBuilder.Mocks.CreateOptionsMock(_aiSettings);
_chatService = new ChatService(
_loggerMock.Object,
_aiServiceMock.Object,
_sessionStorageMock.Object,
optionsMock.Object,
_compressionServiceMock.Object
);
}
[Fact]
public void GetOrCreateSession_ShouldCreateNewSession_WhenSessionDoesNotExist()
{
// Arrange
var chatId = 12345L;
var chatType = "private";
var chatTitle = "Test Chat";
// Act
var session = _chatService.GetOrCreateSession(chatId, chatType, chatTitle);
// Assert
session.Should().NotBeNull();
session.ChatId.Should().Be(chatId);
session.ChatType.Should().Be(chatType);
session.ChatTitle.Should().Be(chatTitle);
_sessionStorageMock.Verify(x => x.GetOrCreate(chatId, chatType, chatTitle), Times.Once);
}
[Fact]
public void GetOrCreateSession_ShouldSetCompressionService_WhenCompressionIsEnabled()
{
// Arrange
var chatId = 12345L;
_aiSettings.EnableHistoryCompression = true;
// Act
var session = _chatService.GetOrCreateSession(chatId);
// Assert
session.Should().NotBeNull();
_sessionStorageMock.Verify(x => x.GetOrCreate(chatId, "private", ""), Times.Once);
}
[Fact]
public async Task ProcessMessageAsync_ShouldProcessMessageSuccessfully_WhenValidInput()
{
// Arrange
var chatId = 12345L;
var username = "testuser";
var message = "Hello, bot!";
var expectedResponse = "Hello! How can I help you?";
_aiServiceMock
.Setup(x =>
x.GenerateChatCompletionWithCompressionAsync(
It.IsAny<List<ChatBot.Models.Dto.ChatMessage>>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync(expectedResponse);
// Act
var result = await _chatService.ProcessMessageAsync(chatId, username, message);
// Assert
result.Should().Be(expectedResponse);
_sessionStorageMock.Verify(
x => x.SaveSessionAsync(It.IsAny<ChatBot.Models.ChatSession>()),
Times.AtLeastOnce
);
_aiServiceMock.Verify(
x =>
x.GenerateChatCompletionWithCompressionAsync(
It.IsAny<List<ChatBot.Models.Dto.ChatMessage>>(),
It.IsAny<CancellationToken>()
),
Times.Once
);
}
[Fact]
public async Task ProcessMessageAsync_ShouldReturnEmptyString_WhenAIResponseIsEmptyMarker()
{
// Arrange
var chatId = 12345L;
var username = "testuser";
var message = "Hello, bot!";
var emptyResponse = "{empty}";
_aiServiceMock
.Setup(x =>
x.GenerateChatCompletionWithCompressionAsync(
It.IsAny<List<ChatBot.Models.Dto.ChatMessage>>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync(emptyResponse);
// Act
var result = await _chatService.ProcessMessageAsync(chatId, username, message);
// Assert
result.Should().BeEmpty();
}
[Fact]
public async Task ProcessMessageAsync_ShouldReturnErrorMessage_WhenAIResponseIsNull()
{
// Arrange
var chatId = 12345L;
var username = "testuser";
var message = "Hello, bot!";
_aiServiceMock
.Setup(x =>
x.GenerateChatCompletionWithCompressionAsync(
It.IsAny<List<ChatBot.Models.Dto.ChatMessage>>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync((string)null!);
// Act
var result = await _chatService.ProcessMessageAsync(chatId, username, message);
// Assert
result.Should().Be("Извините, произошла ошибка при генерации ответа.");
}
[Fact]
public async Task ProcessMessageAsync_ShouldHandleException_WhenErrorOccurs()
{
// Arrange
var chatId = 12345L;
var username = "testuser";
var message = "Hello, bot!";
_aiServiceMock
.Setup(x =>
x.GenerateChatCompletionWithCompressionAsync(
It.IsAny<List<ChatBot.Models.Dto.ChatMessage>>(),
It.IsAny<CancellationToken>()
)
)
.ThrowsAsync(new Exception("Test exception"));
// Act
var result = await _chatService.ProcessMessageAsync(chatId, username, message);
// Assert
result.Should().Be("Извините, произошла ошибка при обработке вашего сообщения.");
// Verify that error was logged
// In a real test, we would verify the logger calls
}
[Fact]
public async Task UpdateSessionParametersAsync_ShouldUpdateSession_WhenSessionExists()
{
// Arrange
var chatId = 12345L;
var newModel = "llama3.2";
var session = TestDataBuilder.ChatSessions.CreateBasicSession(chatId);
_sessionStorageMock.Setup(x => x.Get(chatId)).Returns(session);
// Act
await _chatService.UpdateSessionParametersAsync(chatId, newModel);
// Assert
session.Model.Should().Be(newModel);
_sessionStorageMock.Verify(x => x.SaveSessionAsync(session), Times.Once);
}
[Fact]
public async Task UpdateSessionParametersAsync_ShouldNotUpdate_WhenSessionDoesNotExist()
{
// Arrange
var chatId = 12345L;
var newModel = "llama3.2";
_sessionStorageMock.Setup(x => x.Get(chatId)).Returns((ChatBot.Models.ChatSession?)null);
// Act
await _chatService.UpdateSessionParametersAsync(chatId, newModel);
// Assert
_sessionStorageMock.Verify(
x => x.SaveSessionAsync(It.IsAny<ChatBot.Models.ChatSession>()),
Times.Never
);
}
[Fact]
public async Task ClearHistoryAsync_ShouldClearHistory_WhenSessionExists()
{
// Arrange
var chatId = 12345L;
var session = TestDataBuilder.ChatSessions.CreateSessionWithMessages(chatId, 5);
_sessionStorageMock.Setup(x => x.Get(chatId)).Returns(session);
// Act
await _chatService.ClearHistoryAsync(chatId);
// Assert
session.GetMessageCount().Should().Be(0);
_sessionStorageMock.Verify(x => x.SaveSessionAsync(session), Times.Once);
}
[Fact]
public void GetSession_ShouldReturnSession_WhenSessionExists()
{
// Arrange
var chatId = 12345L;
var session = TestDataBuilder.ChatSessions.CreateBasicSession(chatId);
_sessionStorageMock.Setup(x => x.Get(chatId)).Returns(session);
// Act
var result = _chatService.GetSession(chatId);
// Assert
result.Should().Be(session);
}
[Fact]
public void GetSession_ShouldReturnNull_WhenSessionDoesNotExist()
{
// Arrange
var chatId = 12345L;
_sessionStorageMock.Setup(x => x.Get(chatId)).Returns((ChatBot.Models.ChatSession?)null);
// Act
var result = _chatService.GetSession(chatId);
// Assert
result.Should().BeNull();
}
[Fact]
public void RemoveSession_ShouldReturnTrue_WhenSessionExists()
{
// Arrange
var chatId = 12345L;
_sessionStorageMock.Setup(x => x.Remove(chatId)).Returns(true);
// Act
var result = _chatService.RemoveSession(chatId);
// Assert
result.Should().BeTrue();
_sessionStorageMock.Verify(x => x.Remove(chatId), Times.Once);
}
[Fact]
public void GetActiveSessionsCount_ShouldReturnCorrectCount()
{
// Arrange
var expectedCount = 5;
_sessionStorageMock.Setup(x => x.GetActiveSessionsCount()).Returns(expectedCount);
// Act
var result = _chatService.GetActiveSessionsCount();
// Assert
result.Should().Be(expectedCount);
}
[Fact]
public void CleanupOldSessions_ShouldReturnCleanedCount()
{
// Arrange
var hoursOld = 24;
var expectedCleaned = 3;
_sessionStorageMock.Setup(x => x.CleanupOldSessions(hoursOld)).Returns(expectedCleaned);
// Act
var result = _chatService.CleanupOldSessions(hoursOld);
// Assert
result.Should().Be(expectedCleaned);
_sessionStorageMock.Verify(x => x.CleanupOldSessions(hoursOld), Times.Once);
}
}

View File

@@ -0,0 +1,182 @@
using ChatBot.Data;
using ChatBot.Data.Interfaces;
using ChatBot.Data.Repositories;
using ChatBot.Models;
using ChatBot.Models.Entities;
using ChatBot.Services;
using ChatBot.Tests.TestUtilities;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moq;
namespace ChatBot.Tests.Services;
public class DatabaseSessionStorageTests : TestBase
{
private ChatBotDbContext _dbContext = null!;
private DatabaseSessionStorage _sessionStorage = null!;
private Mock<IChatSessionRepository> _repositoryMock = null!;
public DatabaseSessionStorageTests()
{
SetupServices();
}
protected override void ConfigureServices(IServiceCollection services)
{
// Add in-memory database
services.AddDbContext<ChatBotDbContext>(options =>
options.UseInMemoryDatabase("TestDatabase")
);
// Add mocked repository
_repositoryMock = new Mock<IChatSessionRepository>();
services.AddSingleton(_repositoryMock.Object);
// Add logger
services.AddSingleton(Mock.Of<ILogger<DatabaseSessionStorage>>());
// Add session storage
services.AddScoped<DatabaseSessionStorage>();
}
protected override void SetupServices()
{
base.SetupServices();
_dbContext = ServiceProvider.GetRequiredService<ChatBotDbContext>();
_sessionStorage = ServiceProvider.GetRequiredService<DatabaseSessionStorage>();
// Ensure database is created
_dbContext.Database.EnsureCreated();
}
[Fact]
public void GetOrCreate_ShouldReturnExistingSession_WhenSessionExists()
{
// Arrange
var existingSession = TestDataBuilder.Mocks.CreateChatSessionEntity();
_repositoryMock
.Setup(x => x.GetOrCreateAsync(12345, "private", "Test Chat"))
.ReturnsAsync(existingSession);
// Act
var result = _sessionStorage.GetOrCreate(12345, "private", "Test Chat");
// Assert
result.Should().NotBeNull();
result.ChatId.Should().Be(12345);
_repositoryMock.Verify(x => x.GetOrCreateAsync(12345, "private", "Test Chat"), Times.Once);
}
[Fact]
public void Get_ShouldReturnSession_WhenSessionExists()
{
// Arrange
var sessionEntity = TestDataBuilder.Mocks.CreateChatSessionEntity();
_repositoryMock.Setup(x => x.GetByChatIdAsync(12345)).ReturnsAsync(sessionEntity);
// Act
var result = _sessionStorage.Get(12345);
// Assert
result.Should().NotBeNull();
result.ChatId.Should().Be(12345);
_repositoryMock.Verify(x => x.GetByChatIdAsync(12345), Times.Once);
}
[Fact]
public void Get_ShouldReturnNull_WhenSessionDoesNotExist()
{
// Arrange
_repositoryMock
.Setup(x => x.GetByChatIdAsync(12345))
.ReturnsAsync((ChatSessionEntity?)null);
// Act
var result = _sessionStorage.Get(12345);
// Assert
result.Should().BeNull();
_repositoryMock.Verify(x => x.GetByChatIdAsync(12345), Times.Once);
}
[Fact]
public async Task SaveSessionAsync_ShouldUpdateSession_WhenSessionExists()
{
// Arrange
var session = TestDataBuilder.ChatSessions.CreateBasicSession(12345, "private");
var sessionEntity = TestDataBuilder.Mocks.CreateChatSessionEntity();
_repositoryMock.Setup(x => x.GetByChatIdAsync(12345)).ReturnsAsync(sessionEntity);
_repositoryMock
.Setup(x => x.UpdateAsync(It.IsAny<ChatSessionEntity>()))
.ReturnsAsync(sessionEntity);
// Act
await _sessionStorage.SaveSessionAsync(session);
// Assert
_repositoryMock.Verify(x => x.UpdateAsync(It.IsAny<ChatSessionEntity>()), Times.Once);
}
[Fact]
public void Remove_ShouldReturnTrue_WhenSessionExists()
{
// Arrange
_repositoryMock.Setup(x => x.DeleteAsync(12345)).ReturnsAsync(true);
// Act
var result = _sessionStorage.Remove(12345);
// Assert
result.Should().BeTrue();
_repositoryMock.Verify(x => x.DeleteAsync(12345), Times.Once);
}
[Fact]
public void Remove_ShouldReturnFalse_WhenSessionDoesNotExist()
{
// Arrange
_repositoryMock.Setup(x => x.DeleteAsync(12345)).ReturnsAsync(false);
// Act
var result = _sessionStorage.Remove(12345);
// Assert
result.Should().BeFalse();
_repositoryMock.Verify(x => x.DeleteAsync(12345), Times.Once);
}
[Fact]
public void GetActiveSessionsCount_ShouldReturnCorrectCount()
{
// Arrange
var expectedCount = 5;
_repositoryMock.Setup(x => x.GetActiveSessionsCountAsync()).ReturnsAsync(expectedCount);
// Act
var result = _sessionStorage.GetActiveSessionsCount();
// Assert
result.Should().Be(expectedCount);
_repositoryMock.Verify(x => x.GetActiveSessionsCountAsync(), Times.Once);
}
[Fact]
public void CleanupOldSessions_ShouldReturnCorrectCount()
{
// Arrange
var expectedCount = 3;
_repositoryMock.Setup(x => x.CleanupOldSessionsAsync(24)).ReturnsAsync(expectedCount);
// Act
var result = _sessionStorage.CleanupOldSessions(24);
// Assert
result.Should().Be(expectedCount);
_repositoryMock.Verify(x => x.CleanupOldSessionsAsync(24), Times.Once);
}
}

View File

@@ -0,0 +1,101 @@
using System.Linq;
using ChatBot.Models.Configuration;
using ChatBot.Services.HealthChecks;
using ChatBot.Services.Interfaces;
using ChatBot.Tests.TestUtilities;
using FluentAssertions;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using OllamaSharp.Models.Chat;
namespace ChatBot.Tests.Services.HealthChecks;
public class OllamaHealthCheckTests : UnitTestBase
{
private readonly Mock<ILogger<OllamaHealthCheck>> _loggerMock;
private readonly Mock<IOllamaClient> _ollamaClientMock;
private readonly Mock<IOptions<OllamaSettings>> _optionsMock;
private readonly OllamaHealthCheck _healthCheck;
public OllamaHealthCheckTests()
{
_loggerMock = TestDataBuilder.Mocks.CreateLoggerMock<OllamaHealthCheck>();
_ollamaClientMock = TestDataBuilder.Mocks.CreateOllamaClientMock();
var ollamaSettings = TestDataBuilder.Configurations.CreateOllamaSettings();
_optionsMock = TestDataBuilder.Mocks.CreateOptionsMock(ollamaSettings);
_healthCheck = new OllamaHealthCheck(_ollamaClientMock.Object, _loggerMock.Object);
}
[Fact]
public async Task CheckHealthAsync_ShouldReturnHealthy_WhenOllamaResponds()
{
// Arrange
_ollamaClientMock
.Setup(x => x.ListLocalModelsAsync())
.ReturnsAsync(
new List<OllamaSharp.Models.Model>
{
new OllamaSharp.Models.Model { Name = "llama3.2" },
}
);
// Act
var context = new HealthCheckContext();
var result = await _healthCheck.CheckHealthAsync(context);
// Assert
result.Status.Should().Be(HealthStatus.Healthy);
}
[Fact]
public async Task CheckHealthAsync_ShouldReturnUnhealthy_WhenOllamaThrowsException()
{
// Arrange
_ollamaClientMock
.Setup(x => x.ListLocalModelsAsync())
.ThrowsAsync(new Exception("Ollama unavailable"));
// Act
var context = new HealthCheckContext();
var result = await _healthCheck.CheckHealthAsync(context);
// Assert
result.Status.Should().Be(HealthStatus.Unhealthy);
}
[Fact]
public async Task CheckHealthAsync_ShouldReturnUnhealthy_WhenOllamaReturnsEmptyResponse()
{
// Arrange
_ollamaClientMock
.Setup(x => x.ListLocalModelsAsync())
.ReturnsAsync(new List<OllamaSharp.Models.Model>());
// Act
var context = new HealthCheckContext();
var result = await _healthCheck.CheckHealthAsync(context);
// Assert
result.Status.Should().Be(HealthStatus.Unhealthy);
}
[Fact]
public async Task CheckHealthAsync_ShouldReturnUnhealthy_WhenOllamaReturnsNullMessage()
{
// Arrange
_ollamaClientMock
.Setup(x => x.ListLocalModelsAsync())
.ReturnsAsync((IEnumerable<OllamaSharp.Models.Model>)null!);
// Act
var context = new HealthCheckContext();
var result = await _healthCheck.CheckHealthAsync(context);
// Assert
result.Status.Should().Be(HealthStatus.Unhealthy);
}
}

View File

@@ -0,0 +1,101 @@
using ChatBot.Models.Configuration;
using ChatBot.Services.HealthChecks;
using ChatBot.Services.Interfaces;
using ChatBot.Tests.TestUtilities;
using FluentAssertions;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Telegram.Bot.Types;
namespace ChatBot.Tests.Services.HealthChecks;
public class TelegramBotHealthCheckTests : UnitTestBase
{
private readonly Mock<ILogger<TelegramBotHealthCheck>> _loggerMock;
private readonly Mock<ITelegramBotClientWrapper> _telegramBotClientWrapperMock;
private readonly TelegramBotHealthCheck _healthCheck;
public TelegramBotHealthCheckTests()
{
_loggerMock = TestDataBuilder.Mocks.CreateLoggerMock<TelegramBotHealthCheck>();
_telegramBotClientWrapperMock = new Mock<ITelegramBotClientWrapper>();
_healthCheck = new TelegramBotHealthCheck(
_telegramBotClientWrapperMock.Object,
_loggerMock.Object
);
}
[Fact]
public async Task CheckHealthAsync_ShouldReturnHealthy_WhenTelegramResponds()
{
// Arrange
var botInfo = TestDataBuilder.Mocks.CreateTelegramBot();
_telegramBotClientWrapperMock
.Setup(x => x.GetMeAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(botInfo);
// Act
var context = new HealthCheckContext();
var result = await _healthCheck.CheckHealthAsync(context);
// Assert
result.Status.Should().Be(HealthStatus.Healthy);
}
[Fact]
public async Task CheckHealthAsync_ShouldReturnUnhealthy_WhenTelegramThrowsException()
{
// Arrange
_telegramBotClientWrapperMock
.Setup(x => x.GetMeAsync(It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("Telegram unavailable"));
// Act
var context = new HealthCheckContext();
var result = await _healthCheck.CheckHealthAsync(context);
// Assert
result.Status.Should().Be(HealthStatus.Unhealthy);
}
[Fact]
public async Task CheckHealthAsync_ShouldReturnUnhealthy_WhenTelegramReturnsNull()
{
// Arrange
_telegramBotClientWrapperMock
.Setup(x => x.GetMeAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync((User)null!);
// Act
var context = new HealthCheckContext();
var result = await _healthCheck.CheckHealthAsync(context);
// Assert
result.Status.Should().Be(HealthStatus.Unhealthy);
}
[Fact]
public async Task CheckHealthAsync_ShouldReturnUnhealthy_WhenTelegramReturnsInvalidBot()
{
// Arrange
var invalidBot = new User
{
Id = 0, // Invalid bot ID
Username = null,
};
_telegramBotClientWrapperMock
.Setup(x => x.GetMeAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(invalidBot);
// Act
var context = new HealthCheckContext();
var result = await _healthCheck.CheckHealthAsync(context);
// Assert
result.Status.Should().Be(HealthStatus.Unhealthy);
}
}

View File

@@ -0,0 +1,224 @@
using System.Linq;
using ChatBot.Models.Configuration;
using ChatBot.Models.Dto;
using ChatBot.Services;
using ChatBot.Services.Interfaces;
using ChatBot.Tests.TestUtilities;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using OllamaSharp.Models.Chat;
namespace ChatBot.Tests.Services;
public class HistoryCompressionServiceTests : UnitTestBase
{
private readonly Mock<ILogger<HistoryCompressionService>> _loggerMock;
private readonly Mock<IOllamaClient> _ollamaClientMock;
private readonly AISettings _aiSettings;
private readonly HistoryCompressionService _compressionService;
public HistoryCompressionServiceTests()
{
_loggerMock = TestDataBuilder.Mocks.CreateLoggerMock<HistoryCompressionService>();
_ollamaClientMock = TestDataBuilder.Mocks.CreateOllamaClientMock();
_aiSettings = TestDataBuilder.Configurations.CreateAISettings();
var optionsMock = TestDataBuilder.Mocks.CreateOptionsMock(_aiSettings);
_compressionService = new HistoryCompressionService(
_loggerMock.Object,
optionsMock.Object,
_ollamaClientMock.Object
);
}
[Fact]
public void ShouldCompress_ShouldReturnTrue_WhenMessageCountExceedsThreshold()
{
// Arrange
var messageCount = 15;
var threshold = 10;
// Act
var result = _compressionService.ShouldCompress(messageCount, threshold);
// Assert
result.Should().BeTrue();
}
[Fact]
public void ShouldCompress_ShouldReturnFalse_WhenMessageCountIsBelowThreshold()
{
// Arrange
var messageCount = 5;
var threshold = 10;
// Act
var result = _compressionService.ShouldCompress(messageCount, threshold);
// Assert
result.Should().BeFalse();
}
[Fact]
public void ShouldCompress_ShouldReturnFalse_WhenMessageCountEqualsThreshold()
{
// Arrange
var messageCount = 10;
var threshold = 10;
// Act
var result = _compressionService.ShouldCompress(messageCount, threshold);
// Assert
result.Should().BeFalse();
}
[Fact]
public async Task CompressHistoryAsync_ShouldReturnCompressedMessages_WhenSuccessful()
{
// Arrange
var messages = TestDataBuilder.ChatMessages.CreateMessageHistory(10);
var targetCount = 5;
var expectedResponse = "Compressed summary of previous messages";
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Returns(
TestDataBuilder.Mocks.CreateAsyncEnumerable(
new List<OllamaSharp.Models.Chat.ChatResponseStream>
{
new OllamaSharp.Models.Chat.ChatResponseStream
{
Message = new Message(ChatRole.Assistant, expectedResponse),
},
}
)
);
// Act
var result = await _compressionService.CompressHistoryAsync(messages, targetCount);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(7); // 2 compressed messages + 5 recent messages
result.Should().Contain(m => m.Role == ChatRole.User && m.Content.Contains("[Сжато:"));
result.Should().Contain(m => m.Role == ChatRole.Assistant && m.Content.Contains("[Сжато:"));
result.Should().Contain(m => m.Role == ChatRole.User && m.Content == "User message 9");
result
.Should()
.Contain(m => m.Role == ChatRole.Assistant && m.Content == "Assistant response 9");
}
[Fact]
public async Task CompressHistoryAsync_ShouldFallbackToSimpleTrimming_WhenOllamaClientThrows()
{
// Arrange
var messages = TestDataBuilder.ChatMessages.CreateMessageHistory(10);
var targetCount = 5;
_ollamaClientMock
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
.Returns(ThrowAsyncEnumerable(new Exception("Ollama client error")));
// Act
var result = await _compressionService.CompressHistoryAsync(messages, targetCount);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(7); // 2 compressed messages + 5 recent messages (exception is caught and handled)
result.Should().Contain(m => m.Role == ChatRole.User && m.Content.Contains("[Сжато:"));
result.Should().Contain(m => m.Role == ChatRole.Assistant && m.Content.Contains("[Сжато:"));
result.Should().Contain(m => m.Role == ChatRole.User && m.Content == "User message 9");
result
.Should()
.Contain(m => m.Role == ChatRole.Assistant && m.Content == "Assistant response 9");
}
[Fact]
public async Task CompressHistoryAsync_ShouldReturnOriginalMessages_WhenTargetCountIsGreaterThanOrEqual()
{
// Arrange
var messages = TestDataBuilder.ChatMessages.CreateMessageHistory(5);
var targetCount = 10;
// Act
var result = await _compressionService.CompressHistoryAsync(messages, targetCount);
// Assert
result.Should().BeEquivalentTo(messages);
_ollamaClientMock.Verify(
x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()),
Times.Never
);
}
[Fact]
public async Task CompressHistoryAsync_ShouldHandleEmptyMessages()
{
// Arrange
var messages = new List<ChatMessage>();
var targetCount = 5;
// Act
var result = await _compressionService.CompressHistoryAsync(messages, targetCount);
// Assert
result.Should().BeEmpty();
_ollamaClientMock.Verify(
x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()),
Times.Never
);
}
private static IAsyncEnumerable<OllamaSharp.Models.Chat.ChatResponseStream> ThrowAsyncEnumerable(
Exception exception
)
{
return new ThrowingAsyncEnumerable(exception);
}
private class ThrowingAsyncEnumerable
: IAsyncEnumerable<OllamaSharp.Models.Chat.ChatResponseStream>
{
private readonly Exception _exception;
public ThrowingAsyncEnumerable(Exception exception)
{
_exception = exception;
}
public IAsyncEnumerator<OllamaSharp.Models.Chat.ChatResponseStream> GetAsyncEnumerator(
CancellationToken cancellationToken = default
)
{
return new ThrowingAsyncEnumerator(_exception);
}
}
private class ThrowingAsyncEnumerator
: IAsyncEnumerator<OllamaSharp.Models.Chat.ChatResponseStream>
{
private readonly Exception _exception;
public ThrowingAsyncEnumerator(Exception exception)
{
_exception = exception;
}
public OllamaSharp.Models.Chat.ChatResponseStream Current =>
throw new InvalidOperationException();
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
}
public ValueTask<bool> MoveNextAsync()
{
throw _exception;
}
}
}

View File

@@ -0,0 +1,292 @@
using ChatBot.Models;
using ChatBot.Services;
using ChatBot.Tests.TestUtilities;
using FluentAssertions;
namespace ChatBot.Tests.Services;
public class InMemorySessionStorageTests
{
private readonly InMemorySessionStorage _sessionStorage;
public InMemorySessionStorageTests()
{
_sessionStorage = new InMemorySessionStorage(
TestDataBuilder.Mocks.CreateLoggerMock<InMemorySessionStorage>().Object
);
}
[Fact]
public void GetOrCreate_ShouldReturnExistingSession_WhenSessionExists()
{
// Arrange
_sessionStorage.GetOrCreate(12345, "private", "Test Chat");
// Act
var result = _sessionStorage.GetOrCreate(12345, "private", "Test Chat");
// Assert
result.Should().NotBeNull();
result.ChatId.Should().Be(12345);
result.ChatType.Should().Be("private");
}
[Fact]
public void GetOrCreate_ShouldCreateNewSession_WhenSessionDoesNotExist()
{
// Act
var result = _sessionStorage.GetOrCreate(12345, "group", "Test Group");
// Assert
result.Should().NotBeNull();
result.ChatId.Should().Be(12345);
result.ChatType.Should().Be("group");
result.ChatTitle.Should().Be("Test Group");
}
[Fact]
public void GetOrCreate_ShouldUseDefaultValues_WhenParametersNotProvided()
{
// Act
var result = _sessionStorage.GetOrCreate(12345);
// Assert
result.Should().NotBeNull();
result.ChatId.Should().Be(12345);
result.ChatType.Should().Be("private");
result.ChatTitle.Should().BeEmpty();
}
[Fact]
public void Get_ShouldReturnSession_WhenSessionExists()
{
// Arrange
var session = _sessionStorage.GetOrCreate(12345, "private", "Test Chat");
// Act
var result = _sessionStorage.Get(12345);
// Assert
result.Should().BeSameAs(session);
}
[Fact]
public void Get_ShouldReturnNull_WhenSessionDoesNotExist()
{
// Act
var result = _sessionStorage.Get(99999);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task SaveSessionAsync_ShouldUpdateExistingSession()
{
// Arrange
var session = _sessionStorage.GetOrCreate(12345, "private", "Original Title");
session.ChatTitle = "Updated Title";
session.LastUpdatedAt = DateTime.UtcNow;
// Act
await _sessionStorage.SaveSessionAsync(session);
// Assert
var savedSession = _sessionStorage.Get(12345);
savedSession.Should().NotBeNull();
savedSession!.ChatTitle.Should().Be("Updated Title");
}
[Fact]
public async Task SaveSessionAsync_ShouldAddNewSession()
{
// Arrange
var session = _sessionStorage.GetOrCreate(12345, "private", "Original Title");
session.ChatTitle = "New Session";
// Act
await _sessionStorage.SaveSessionAsync(session);
// Assert
var savedSession = _sessionStorage.Get(12345);
savedSession.Should().NotBeNull();
savedSession!.ChatTitle.Should().Be("New Session");
}
[Fact]
public void Remove_ShouldReturnTrue_WhenSessionExists()
{
// Arrange
_sessionStorage.GetOrCreate(12345, "private", "Test Chat");
// Act
var result = _sessionStorage.Remove(12345);
// Assert
result.Should().BeTrue();
_sessionStorage.Get(12345).Should().BeNull();
}
[Fact]
public void Remove_ShouldReturnFalse_WhenSessionDoesNotExist()
{
// Act
var result = _sessionStorage.Remove(99999);
// Assert
result.Should().BeFalse();
}
[Fact]
public void GetActiveSessionsCount_ShouldReturnCorrectCount()
{
// Arrange
_sessionStorage.GetOrCreate(12345, "private", "Chat 1");
_sessionStorage.GetOrCreate(67890, "group", "Chat 2");
_sessionStorage.GetOrCreate(11111, "private", "Chat 3");
// Act
var count = _sessionStorage.GetActiveSessionsCount();
// Assert
count.Should().Be(3);
}
[Fact]
public void GetActiveSessionsCount_ShouldReturnZero_WhenNoSessions()
{
// Act
var count = _sessionStorage.GetActiveSessionsCount();
// Assert
count.Should().Be(0);
}
[Fact]
public void CleanupOldSessions_ShouldDeleteOldSessions()
{
// Arrange
var oldSession = _sessionStorage.GetOrCreate(99999, "private", "Old Chat");
// Manually set CreatedAt to 2 days ago using test method
oldSession.SetCreatedAtForTesting(DateTime.UtcNow.AddDays(-2));
var recentSession = _sessionStorage.GetOrCreate(88888, "private", "Recent Chat");
// Manually set CreatedAt to 30 minutes ago using test method
recentSession.SetCreatedAtForTesting(DateTime.UtcNow.AddMinutes(-30));
// Act
_sessionStorage.CleanupOldSessions(1); // Delete sessions older than 1 day
// Assert
_sessionStorage.Get(99999).Should().BeNull(); // Old session should be deleted
_sessionStorage.Get(88888).Should().NotBeNull(); // Recent session should remain
}
[Fact]
public void CleanupOldSessions_ShouldNotDeleteRecentSessions()
{
// Arrange
var recentSession1 = _sessionStorage.GetOrCreate(12345, "private", "Recent 1");
recentSession1.CreatedAt = DateTime.UtcNow.AddHours(-1);
var recentSession2 = _sessionStorage.GetOrCreate(67890, "private", "Recent 2");
recentSession2.CreatedAt = DateTime.UtcNow.AddMinutes(-30);
// Act
var deletedCount = _sessionStorage.CleanupOldSessions(24); // Delete sessions older than 24 hours
// Assert
deletedCount.Should().Be(0);
_sessionStorage.Get(12345).Should().NotBeNull();
_sessionStorage.Get(67890).Should().NotBeNull();
}
[Fact]
public void CleanupOldSessions_ShouldReturnZero_WhenNoSessions()
{
// Act
var deletedCount = _sessionStorage.CleanupOldSessions(1);
// Assert
deletedCount.Should().Be(0);
}
[Fact]
public void GetOrCreate_ShouldCreateSessionWithCorrectTimestamp()
{
// Act
var session = _sessionStorage.GetOrCreate(12345, "private", "Test Chat");
// Assert
session.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromMinutes(1));
session.LastUpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromMinutes(1));
}
[Fact]
public async Task SaveSessionAsync_ShouldUpdateLastUpdatedAt()
{
// Arrange
var session = _sessionStorage.GetOrCreate(12345, "private", "Test Chat");
var originalTime = session.LastUpdatedAt;
// Wait a bit to ensure time difference
await Task.Delay(10);
session.ChatTitle = "Updated Title";
// Act
await _sessionStorage.SaveSessionAsync(session);
// Assert
var savedSession = _sessionStorage.Get(12345);
savedSession!.LastUpdatedAt.Should().BeAfter(originalTime);
}
[Fact]
public async Task GetOrCreate_ShouldHandleConcurrentAccess()
{
// Arrange
var tasks = new List<Task<ChatSession>>();
// Act - Create sessions concurrently
for (int i = 0; i < 100; i++)
{
var chatId = 1000 + i;
tasks.Add(
Task.Run(() => _sessionStorage.GetOrCreate(chatId, "private", $"Chat {chatId}"))
);
}
await Task.WhenAll(tasks);
// Assert
_sessionStorage.GetActiveSessionsCount().Should().Be(100);
// Verify all sessions were created
for (int i = 0; i < 100; i++)
{
var chatId = 1000 + i;
var session = _sessionStorage.Get(chatId);
session.Should().NotBeNull();
session!.ChatId.Should().Be(chatId);
}
}
[Fact]
public void Remove_ShouldDecreaseActiveSessionsCount()
{
// Arrange
_sessionStorage.GetOrCreate(12345, "private", "Chat 1");
_sessionStorage.GetOrCreate(67890, "private", "Chat 2");
_sessionStorage.GetOrCreate(11111, "private", "Chat 3");
// Act
_sessionStorage.Remove(67890);
// Assert
_sessionStorage.GetActiveSessionsCount().Should().Be(2);
_sessionStorage.Get(12345).Should().NotBeNull();
_sessionStorage.Get(67890).Should().BeNull();
_sessionStorage.Get(11111).Should().NotBeNull();
}
}

View File

@@ -0,0 +1,46 @@
using ChatBot.Models.Configuration;
using ChatBot.Services;
using ChatBot.Tests.TestUtilities;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
namespace ChatBot.Tests.Services;
public class ModelServiceTests : UnitTestBase
{
private readonly Mock<ILogger<ModelService>> _loggerMock;
private readonly Mock<IOptions<OllamaSettings>> _optionsMock;
private readonly ModelService _modelService;
public ModelServiceTests()
{
_loggerMock = TestDataBuilder.Mocks.CreateLoggerMock<ModelService>();
var ollamaSettings = TestDataBuilder.Configurations.CreateOllamaSettings();
_optionsMock = TestDataBuilder.Mocks.CreateOptionsMock(ollamaSettings);
_modelService = new ModelService(_loggerMock.Object, _optionsMock.Object);
}
[Fact]
public void GetCurrentModel_ShouldReturnDefaultModel()
{
// Act
var result = _modelService.GetCurrentModel();
// Assert
result.Should().Be("llama3.2");
}
[Fact]
public async Task InitializeAsync_ShouldLogModelInformation()
{
// Act
await _modelService.InitializeAsync();
// Assert
// The method should complete without throwing exceptions
// In a real test, we might verify logging calls
}
}

View File

@@ -0,0 +1,56 @@
using ChatBot.Models.Configuration;
using ChatBot.Services;
using ChatBot.Tests.TestUtilities;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
namespace ChatBot.Tests.Services;
public class SystemPromptServiceTests : UnitTestBase
{
private readonly Mock<ILogger<SystemPromptService>> _loggerMock;
private readonly SystemPromptService _systemPromptService;
public SystemPromptServiceTests()
{
_loggerMock = TestDataBuilder.Mocks.CreateLoggerMock<SystemPromptService>();
var aiSettingsMock = TestDataBuilder.Mocks.CreateOptionsMock(new AISettings());
_systemPromptService = new SystemPromptService(_loggerMock.Object, aiSettingsMock.Object);
}
[Fact]
public async Task GetSystemPromptAsync_ShouldReturnSystemPrompt()
{
// Act
var result = await _systemPromptService.GetSystemPromptAsync();
// Assert
result.Should().NotBeNullOrEmpty();
result.Should().Contain("Никита");
}
[Fact]
public async Task GetSystemPromptAsync_ShouldReturnCachedPrompt_WhenCalledMultipleTimes()
{
// Act
var result1 = await _systemPromptService.GetSystemPromptAsync();
var result2 = await _systemPromptService.GetSystemPromptAsync();
// Assert
result1.Should().Be(result2);
}
[Fact]
public async Task ReloadPrompt_ShouldClearCache()
{
// Act
// ReloadPrompt method doesn't exist, skipping this test
var newPrompt = await _systemPromptService.GetSystemPromptAsync();
// Assert
newPrompt.Should().NotBeNull();
// Note: In a real scenario, we might mock the file system to test cache clearing
}
}