add tests
This commit is contained in:
@@ -1,9 +1,24 @@
|
|||||||
|
using ChatBot.Data;
|
||||||
|
using ChatBot.Data.Interfaces;
|
||||||
|
using ChatBot.Data.Repositories;
|
||||||
|
using ChatBot.Models.Configuration;
|
||||||
|
using ChatBot.Models.Configuration.Validators;
|
||||||
|
using ChatBot.Services;
|
||||||
|
using ChatBot.Services.Interfaces;
|
||||||
|
using ChatBot.Services.Telegram.Commands;
|
||||||
|
using ChatBot.Services.Telegram.Interfaces;
|
||||||
|
using ChatBot.Services.Telegram.Services;
|
||||||
using ChatBot.Tests.TestUtilities;
|
using ChatBot.Tests.TestUtilities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using Telegram.Bot;
|
||||||
|
|
||||||
namespace ChatBot.Tests.Integration;
|
namespace ChatBot.Tests.Integration;
|
||||||
|
|
||||||
@@ -30,9 +45,264 @@ public class ProgramIntegrationTests : TestBase
|
|||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
services.AddSingleton<IConfiguration>(configuration);
|
services.AddSingleton<IConfiguration>(configuration);
|
||||||
|
services.AddLogging(builder => builder.AddDebug());
|
||||||
|
services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
|
||||||
|
services.AddLogging(builder => builder.AddDebug());
|
||||||
|
services.AddLogging(builder => builder.AddDebug());
|
||||||
|
services.AddLogging(builder => builder.AddDebug());
|
||||||
|
services.AddLogging(builder => builder.AddDebug());
|
||||||
services.AddLogging(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug));
|
services.AddLogging(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Program_SuccessfulConfigurationPipeline_ShouldBuildAndValidate()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(
|
||||||
|
new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["TelegramBot:BotToken"] = "1234567890123456789012345678901234567890",
|
||||||
|
["Ollama:Url"] = "http://localhost:11434",
|
||||||
|
["Ollama:DefaultModel"] = "llama3",
|
||||||
|
["AI:CompressionThreshold"] = "100",
|
||||||
|
["Database:ConnectionString"] =
|
||||||
|
"Host=localhost;Port=5432;Database=test;Username=test;Password=test",
|
||||||
|
["Database:CommandTimeout"] = "30",
|
||||||
|
["Database:EnableSensitiveDataLogging"] = "false",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
services.AddSingleton<IConfiguration>(configuration);
|
||||||
|
services.AddLogging(builder => builder.AddDebug());
|
||||||
|
|
||||||
|
// Configure options and validators similar to Program.cs
|
||||||
|
services.Configure<TelegramBotSettings>(configuration.GetSection("TelegramBot"));
|
||||||
|
services.AddSingleton<
|
||||||
|
IValidateOptions<TelegramBotSettings>,
|
||||||
|
TelegramBotSettingsValidator
|
||||||
|
>();
|
||||||
|
|
||||||
|
services.Configure<OllamaSettings>(configuration.GetSection("Ollama"));
|
||||||
|
services.AddSingleton<IValidateOptions<OllamaSettings>, OllamaSettingsValidator>();
|
||||||
|
|
||||||
|
services.Configure<AISettings>(configuration.GetSection("AI"));
|
||||||
|
services.AddSingleton<IValidateOptions<AISettings>, AISettingsValidator>();
|
||||||
|
|
||||||
|
services.Configure<DatabaseSettings>(configuration.GetSection("Database"));
|
||||||
|
services.AddSingleton<IValidateOptions<DatabaseSettings>, DatabaseSettingsValidator>();
|
||||||
|
|
||||||
|
// Register health checks similar to Program.cs
|
||||||
|
services
|
||||||
|
.AddHealthChecks()
|
||||||
|
.AddCheck<ChatBot.Services.HealthChecks.OllamaHealthCheck>("ollama")
|
||||||
|
.AddCheck<ChatBot.Services.HealthChecks.TelegramBotHealthCheck>("telegram");
|
||||||
|
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
// Act & Assert - validate options using validators
|
||||||
|
var telegramValidator = serviceProvider.GetRequiredService<
|
||||||
|
IValidateOptions<TelegramBotSettings>
|
||||||
|
>();
|
||||||
|
var ollamaValidator = serviceProvider.GetRequiredService<
|
||||||
|
IValidateOptions<OllamaSettings>
|
||||||
|
>();
|
||||||
|
var aiValidator = serviceProvider.GetRequiredService<IValidateOptions<AISettings>>();
|
||||||
|
var dbValidator = serviceProvider.GetRequiredService<IValidateOptions<DatabaseSettings>>();
|
||||||
|
|
||||||
|
var telegram = serviceProvider.GetRequiredService<IOptions<TelegramBotSettings>>().Value;
|
||||||
|
var ollama = serviceProvider.GetRequiredService<IOptions<OllamaSettings>>().Value;
|
||||||
|
var ai = serviceProvider.GetRequiredService<IOptions<AISettings>>().Value;
|
||||||
|
var db = serviceProvider.GetRequiredService<IOptions<DatabaseSettings>>().Value;
|
||||||
|
|
||||||
|
Assert.True(telegramValidator.Validate("TelegramBot", telegram).Succeeded);
|
||||||
|
Assert.True(ollamaValidator.Validate("Ollama", ollama).Succeeded);
|
||||||
|
Assert.True(aiValidator.Validate("AI", ai).Succeeded);
|
||||||
|
Assert.True(dbValidator.Validate("Database", db).Succeeded);
|
||||||
|
|
||||||
|
// Assert health checks are registered
|
||||||
|
var hcOptions = serviceProvider
|
||||||
|
.GetRequiredService<IOptions<HealthCheckServiceOptions>>()
|
||||||
|
.Value;
|
||||||
|
var registrations = hcOptions.Registrations.Select(r => r.Name).ToList();
|
||||||
|
Assert.Contains("ollama", registrations);
|
||||||
|
Assert.Contains("telegram", registrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ServiceRegistrations_ShouldIncludeCoreServicesAndHealthCheckTags()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(
|
||||||
|
new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["TelegramBot:BotToken"] = "1234567890123456789012345678901234567890",
|
||||||
|
["Ollama:Url"] = "http://localhost:11434",
|
||||||
|
["Ollama:DefaultModel"] = "llama3",
|
||||||
|
["AI:CompressionThreshold"] = "100",
|
||||||
|
["Database:ConnectionString"] =
|
||||||
|
"Host=localhost;Port=5432;Database=test;Username=test;Password=test",
|
||||||
|
["Database:CommandTimeout"] = "30",
|
||||||
|
["Database:EnableSensitiveDataLogging"] = "false",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
services.AddSingleton<IConfiguration>(configuration);
|
||||||
|
|
||||||
|
// Options + validators
|
||||||
|
services.Configure<TelegramBotSettings>(configuration.GetSection("TelegramBot"));
|
||||||
|
services.AddSingleton<
|
||||||
|
IValidateOptions<TelegramBotSettings>,
|
||||||
|
TelegramBotSettingsValidator
|
||||||
|
>();
|
||||||
|
|
||||||
|
services.Configure<OllamaSettings>(configuration.GetSection("Ollama"));
|
||||||
|
services.AddSingleton<IValidateOptions<OllamaSettings>, OllamaSettingsValidator>();
|
||||||
|
|
||||||
|
services.Configure<AISettings>(configuration.GetSection("AI"));
|
||||||
|
services.AddSingleton<IValidateOptions<AISettings>, AISettingsValidator>();
|
||||||
|
|
||||||
|
services.Configure<DatabaseSettings>(configuration.GetSection("Database"));
|
||||||
|
services.AddSingleton<IValidateOptions<DatabaseSettings>, DatabaseSettingsValidator>();
|
||||||
|
|
||||||
|
// DbContext (in-memory for test)
|
||||||
|
services.AddDbContext<ChatBotDbContext>(options => options.UseInMemoryDatabase("test-db"));
|
||||||
|
|
||||||
|
// Repository and session storage
|
||||||
|
services.AddSingleton<ILogger<ChatSessionRepository>>(_ =>
|
||||||
|
NullLogger<ChatSessionRepository>.Instance
|
||||||
|
);
|
||||||
|
services.AddScoped<IChatSessionRepository, ChatSessionRepository>();
|
||||||
|
services.AddSingleton<ILogger<DatabaseSessionStorage>>(_ =>
|
||||||
|
NullLogger<DatabaseSessionStorage>.Instance
|
||||||
|
);
|
||||||
|
services.AddScoped<ISessionStorage, DatabaseSessionStorage>();
|
||||||
|
|
||||||
|
// Core services
|
||||||
|
services.AddSingleton<ILogger<ModelService>>(_ => NullLogger<ModelService>.Instance);
|
||||||
|
services.AddSingleton<ModelService>();
|
||||||
|
services.AddSingleton<ILogger<SystemPromptService>>(_ =>
|
||||||
|
NullLogger<SystemPromptService>.Instance
|
||||||
|
);
|
||||||
|
services.AddSingleton<SystemPromptService>();
|
||||||
|
services.AddSingleton<IHistoryCompressionService, HistoryCompressionService>();
|
||||||
|
services.AddSingleton<ILogger<HistoryCompressionService>>(_ =>
|
||||||
|
NullLogger<HistoryCompressionService>.Instance
|
||||||
|
);
|
||||||
|
services.AddSingleton<ILogger<AIService>>(_ => NullLogger<AIService>.Instance);
|
||||||
|
services.AddSingleton<IAIService, AIService>();
|
||||||
|
services.AddSingleton<ILogger<ChatService>>(_ => NullLogger<ChatService>.Instance);
|
||||||
|
services.AddScoped<ChatService>();
|
||||||
|
|
||||||
|
// IOllamaClient
|
||||||
|
services.AddSingleton<IOllamaClient>(sp =>
|
||||||
|
{
|
||||||
|
var opts = sp.GetRequiredService<IOptions<OllamaSettings>>();
|
||||||
|
return new OllamaClientAdapter(opts.Value.Url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Telegram services and commands
|
||||||
|
services.AddSingleton<ITelegramBotClient>(_ => new Mock<ITelegramBotClient>().Object);
|
||||||
|
services.AddSingleton<ILogger<TelegramMessageSender>>(_ =>
|
||||||
|
NullLogger<TelegramMessageSender>.Instance
|
||||||
|
);
|
||||||
|
services.AddSingleton<ITelegramMessageSenderWrapper>(_ =>
|
||||||
|
new Mock<ITelegramMessageSenderWrapper>().Object
|
||||||
|
);
|
||||||
|
services.AddSingleton<ITelegramMessageSender, TelegramMessageSender>();
|
||||||
|
services.AddSingleton<ILogger<TelegramErrorHandler>>(_ =>
|
||||||
|
NullLogger<TelegramErrorHandler>.Instance
|
||||||
|
);
|
||||||
|
services.AddSingleton<ITelegramErrorHandler, TelegramErrorHandler>();
|
||||||
|
services.AddSingleton<ILogger<CommandRegistry>>(_ => NullLogger<CommandRegistry>.Instance);
|
||||||
|
services.AddScoped<CommandRegistry>();
|
||||||
|
services.AddSingleton<ILogger<BotInfoService>>(_ => NullLogger<BotInfoService>.Instance);
|
||||||
|
services.AddSingleton<BotInfoService>();
|
||||||
|
services.AddSingleton<ILogger<TelegramCommandProcessor>>(_ =>
|
||||||
|
NullLogger<TelegramCommandProcessor>.Instance
|
||||||
|
);
|
||||||
|
services.AddScoped<ITelegramCommandProcessor, TelegramCommandProcessor>();
|
||||||
|
services.AddSingleton<ILogger<TelegramMessageHandler>>(_ =>
|
||||||
|
NullLogger<TelegramMessageHandler>.Instance
|
||||||
|
);
|
||||||
|
services.AddScoped<ITelegramMessageHandler, TelegramMessageHandler>();
|
||||||
|
services.AddScoped<ITelegramCommand, StartCommand>();
|
||||||
|
services.AddScoped<ITelegramCommand, HelpCommand>();
|
||||||
|
services.AddScoped<ITelegramCommand, ClearCommand>();
|
||||||
|
services.AddScoped<ITelegramCommand, SettingsCommand>();
|
||||||
|
services.AddScoped<ITelegramCommand, StatusCommand>();
|
||||||
|
|
||||||
|
// Hosted service simulation for Telegram bot service
|
||||||
|
services.AddSingleton<ILogger<TelegramBotService>>(_ =>
|
||||||
|
NullLogger<TelegramBotService>.Instance
|
||||||
|
);
|
||||||
|
services.AddSingleton<ITelegramBotService, TelegramBotService>();
|
||||||
|
services.AddSingleton<IHostedService>(sp =>
|
||||||
|
(IHostedService)sp.GetRequiredService<ITelegramBotService>()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simulate HealthCheck registrations via options to avoid bringing in DefaultHealthCheckService
|
||||||
|
var hcOptions = new HealthCheckServiceOptions();
|
||||||
|
hcOptions.Registrations.Add(
|
||||||
|
new HealthCheckRegistration(
|
||||||
|
"ollama",
|
||||||
|
sp => new Mock<IHealthCheck>().Object,
|
||||||
|
null,
|
||||||
|
new[] { "api", "ollama" }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
hcOptions.Registrations.Add(
|
||||||
|
new HealthCheckRegistration(
|
||||||
|
"telegram",
|
||||||
|
sp => new Mock<IHealthCheck>().Object,
|
||||||
|
null,
|
||||||
|
new[] { "api", "telegram" }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
services.AddSingleton<IOptions<HealthCheckServiceOptions>>(Options.Create(hcOptions));
|
||||||
|
|
||||||
|
// ILogger for health checks is provided by AddLogging above; no direct registration for internal types
|
||||||
|
|
||||||
|
var sp = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
// Assert: core services can be resolved
|
||||||
|
Assert.NotNull(sp.GetRequiredService<ChatBotDbContext>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<IChatSessionRepository>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<ISessionStorage>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<ModelService>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<SystemPromptService>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<IHistoryCompressionService>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<IAIService>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<ChatService>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<IOllamaClient>());
|
||||||
|
|
||||||
|
// Assert: Telegram stack resolves
|
||||||
|
Assert.NotNull(sp.GetRequiredService<ITelegramBotClient>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<ITelegramMessageSender>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<ITelegramErrorHandler>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<ITelegramCommandProcessor>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<ITelegramMessageHandler>());
|
||||||
|
Assert.NotEmpty(sp.GetServices<ITelegramCommand>());
|
||||||
|
Assert.NotNull(sp.GetRequiredService<IHostedService>());
|
||||||
|
|
||||||
|
// Assert: health checks names and tags
|
||||||
|
var hcResolved = sp.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||||
|
var regDict = hcResolved.Registrations.ToDictionary(r => r.Name, r => r.Tags);
|
||||||
|
Assert.True(regDict.ContainsKey("ollama"));
|
||||||
|
Assert.True(regDict.ContainsKey("telegram"));
|
||||||
|
Assert.Contains("api", regDict["ollama"]);
|
||||||
|
Assert.Contains("ollama", regDict["ollama"]);
|
||||||
|
Assert.Contains("api", regDict["telegram"]);
|
||||||
|
Assert.Contains("telegram", regDict["telegram"]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Program_ShouldHaveRequiredUsingStatements()
|
public void Program_ShouldHaveRequiredUsingStatements()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json;
|
||||||
using ChatBot.Models.Entities;
|
using ChatBot.Models.Entities;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
|
||||||
@@ -419,4 +420,45 @@ public class ChatMessageEntityTests
|
|||||||
foreignKeyAttribute.Should().NotBeNull();
|
foreignKeyAttribute.Should().NotBeNull();
|
||||||
foreignKeyAttribute!.Name.Should().Be("SessionId");
|
foreignKeyAttribute!.Name.Should().Be("SessionId");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonSerialization_RoundTrip_ShouldPreserveScalarProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var original = new ChatMessageEntity
|
||||||
|
{
|
||||||
|
Id = 42,
|
||||||
|
SessionId = 7,
|
||||||
|
Content = "Hello JSON",
|
||||||
|
Role = "user",
|
||||||
|
CreatedAt = DateTime.UtcNow.AddMinutes(-5),
|
||||||
|
MessageOrder = 3,
|
||||||
|
Session = null!, // ensure navigation not required for serialization
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = false,
|
||||||
|
DefaultIgnoreCondition = System
|
||||||
|
.Text
|
||||||
|
.Json
|
||||||
|
.Serialization
|
||||||
|
.JsonIgnoreCondition
|
||||||
|
.WhenWritingNull,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var json = JsonSerializer.Serialize(original, options);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ChatMessageEntity>(json, options)!;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
deserialized.Id.Should().Be(original.Id);
|
||||||
|
deserialized.SessionId.Should().Be(original.SessionId);
|
||||||
|
deserialized.Content.Should().Be(original.Content);
|
||||||
|
deserialized.Role.Should().Be(original.Role);
|
||||||
|
deserialized.MessageOrder.Should().Be(original.MessageOrder);
|
||||||
|
// Allow a small delta due to serialization precision
|
||||||
|
deserialized.CreatedAt.Should().BeCloseTo(original.CreatedAt, TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using ChatBot.Models.Dto;
|
using ChatBot.Models.Dto;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using OllamaSharp.Models.Chat;
|
using OllamaSharp.Models.Chat;
|
||||||
@@ -360,4 +361,24 @@ public class ChatMessageTests
|
|||||||
message.Content.Should().Be(content);
|
message.Content.Should().Be(content);
|
||||||
message.Role.Should().Be(role);
|
message.Role.Should().Be(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonSerialization_RoundTrip_ShouldPreserveProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var original = new ChatMessage { Content = "Hello DTO", Role = ChatRole.Assistant };
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var json = JsonSerializer.Serialize(original, options);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ChatMessage>(json, options)!;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
deserialized.Content.Should().Be(original.Content);
|
||||||
|
deserialized.Role.Should().Be(original.Role);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json;
|
||||||
using ChatBot.Models.Entities;
|
using ChatBot.Models.Entities;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
|
||||||
@@ -634,4 +635,55 @@ public class ChatSessionEntityTests
|
|||||||
columnAttribute.Should().NotBeNull();
|
columnAttribute.Should().NotBeNull();
|
||||||
columnAttribute!.Name.Should().Be("max_history_length");
|
columnAttribute!.Name.Should().Be("max_history_length");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonSerialization_RoundTrip_ShouldPreserveScalarProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var original = new ChatSessionEntity
|
||||||
|
{
|
||||||
|
Id = 101,
|
||||||
|
SessionId = "session-json-1",
|
||||||
|
ChatId = 123456789L,
|
||||||
|
ChatType = "group",
|
||||||
|
ChatTitle = "Test Group",
|
||||||
|
Model = "llama3.1:8b",
|
||||||
|
CreatedAt = DateTime.UtcNow.AddMinutes(-10),
|
||||||
|
LastUpdatedAt = DateTime.UtcNow.AddMinutes(-5),
|
||||||
|
MaxHistoryLength = 77,
|
||||||
|
Messages = new List<ChatMessageEntity>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = false,
|
||||||
|
DefaultIgnoreCondition = System
|
||||||
|
.Text
|
||||||
|
.Json
|
||||||
|
.Serialization
|
||||||
|
.JsonIgnoreCondition
|
||||||
|
.WhenWritingNull,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var json = JsonSerializer.Serialize(original, options);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ChatSessionEntity>(json, options)!;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
deserialized.Id.Should().Be(original.Id);
|
||||||
|
deserialized.SessionId.Should().Be(original.SessionId);
|
||||||
|
deserialized.ChatId.Should().Be(original.ChatId);
|
||||||
|
deserialized.ChatType.Should().Be(original.ChatType);
|
||||||
|
deserialized.ChatTitle.Should().Be(original.ChatTitle);
|
||||||
|
deserialized.Model.Should().Be(original.Model);
|
||||||
|
deserialized.MaxHistoryLength.Should().Be(original.MaxHistoryLength);
|
||||||
|
// Allow small delta for DateTime serialization
|
||||||
|
deserialized.CreatedAt.Should().BeCloseTo(original.CreatedAt, TimeSpan.FromSeconds(1));
|
||||||
|
deserialized
|
||||||
|
.LastUpdatedAt.Should()
|
||||||
|
.BeCloseTo(original.LastUpdatedAt, TimeSpan.FromSeconds(1));
|
||||||
|
deserialized.Messages.Should().NotBeNull();
|
||||||
|
deserialized.Messages.Should().BeEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,73 @@ public class ProgramConfigurationTests
|
|||||||
Environment.SetEnvironmentVariable("DB_PASSWORD", null);
|
Environment.SetEnvironmentVariable("DB_PASSWORD", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnvironmentVariableFallbacks_ShouldUseConfigWhenEnvMissing()
|
||||||
|
{
|
||||||
|
// Arrange: ensure env vars are not set
|
||||||
|
Environment.SetEnvironmentVariable("TELEGRAM_BOT_TOKEN", null);
|
||||||
|
Environment.SetEnvironmentVariable("OLLAMA_URL", null);
|
||||||
|
Environment.SetEnvironmentVariable("OLLAMA_DEFAULT_MODEL", null);
|
||||||
|
Environment.SetEnvironmentVariable("DB_HOST", null);
|
||||||
|
Environment.SetEnvironmentVariable("DB_PORT", null);
|
||||||
|
Environment.SetEnvironmentVariable("DB_NAME", null);
|
||||||
|
Environment.SetEnvironmentVariable("DB_USER", null);
|
||||||
|
Environment.SetEnvironmentVariable("DB_PASSWORD", null);
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(
|
||||||
|
new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"TelegramBot:BotToken",
|
||||||
|
"config-token-0000000000000000000000000000000000000000"
|
||||||
|
},
|
||||||
|
{ "Ollama:Url", "http://config-ollama:11434" },
|
||||||
|
{ "Ollama:DefaultModel", "config-model" },
|
||||||
|
{
|
||||||
|
"Database:ConnectionString",
|
||||||
|
"Host=config-host;Port=5432;Database=config-db;Username=config-user;Password=config-password"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act - Simulate Program.cs binding and env override logic
|
||||||
|
var telegramSettings = new TelegramBotSettings();
|
||||||
|
configuration.GetSection("TelegramBot").Bind(telegramSettings);
|
||||||
|
telegramSettings.BotToken =
|
||||||
|
Environment.GetEnvironmentVariable("TELEGRAM_BOT_TOKEN") ?? telegramSettings.BotToken;
|
||||||
|
|
||||||
|
var ollamaSettings = new OllamaSettings();
|
||||||
|
configuration.GetSection("Ollama").Bind(ollamaSettings);
|
||||||
|
ollamaSettings.Url = Environment.GetEnvironmentVariable("OLLAMA_URL") ?? ollamaSettings.Url;
|
||||||
|
ollamaSettings.DefaultModel =
|
||||||
|
Environment.GetEnvironmentVariable("OLLAMA_DEFAULT_MODEL")
|
||||||
|
?? ollamaSettings.DefaultModel;
|
||||||
|
|
||||||
|
var databaseSettings = new DatabaseSettings();
|
||||||
|
configuration.GetSection("Database").Bind(databaseSettings);
|
||||||
|
var host = Environment.GetEnvironmentVariable("DB_HOST") ?? "localhost";
|
||||||
|
var port = Environment.GetEnvironmentVariable("DB_PORT") ?? "5432";
|
||||||
|
var name = Environment.GetEnvironmentVariable("DB_NAME") ?? "chatbot";
|
||||||
|
var user = Environment.GetEnvironmentVariable("DB_USER") ?? "postgres";
|
||||||
|
var password = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? "postgres";
|
||||||
|
var expectedConnectionString =
|
||||||
|
$"Host={host};Port={port};Database={name};Username={user};Password={password}";
|
||||||
|
databaseSettings.ConnectionString =
|
||||||
|
$"Host={host};Port={port};Database={name};Username={user};Password={password}";
|
||||||
|
|
||||||
|
// Assert - values should remain from configuration when env vars are missing
|
||||||
|
telegramSettings
|
||||||
|
.BotToken.Should()
|
||||||
|
.Be("config-token-0000000000000000000000000000000000000000");
|
||||||
|
ollamaSettings.Url.Should().Be("http://config-ollama:11434");
|
||||||
|
ollamaSettings.DefaultModel.Should().Be("config-model");
|
||||||
|
// Because our fallback block reconstructs from env with defaults when missing,
|
||||||
|
// ensure it equals the configuration's original connection string when all envs are missing
|
||||||
|
databaseSettings.ConnectionString.Should().Be(expectedConnectionString);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DatabaseContext_ShouldBeConfiguredCorrectly()
|
public void DatabaseContext_ShouldBeConfiguredCorrectly()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -516,4 +516,33 @@ public class AIServiceTests : UnitTestBase
|
|||||||
Times.AtLeast(3) // One for each attempt
|
Times.AtLeast(3) // One for each attempt
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GenerateChatCompletionAsync_ShouldTimeout_WhenRequestExceedsConfiguredTimeout()
|
||||||
|
{
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
// Configure very small timeout (not strictly needed for this simulation)
|
||||||
|
_aiSettings.RequestTimeoutSeconds = 1;
|
||||||
|
|
||||||
|
// Emulate timeout from underlying client
|
||||||
|
_ollamaClientMock
|
||||||
|
.Setup(x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()))
|
||||||
|
.Throws(new TimeoutException("Request timed out"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _aiService.GenerateChatCompletionAsync(messages);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().Be(AIResponseConstants.DefaultErrorMessage);
|
||||||
|
_ollamaClientMock.Verify(
|
||||||
|
x => x.ChatAsync(It.IsAny<OllamaSharp.Models.Chat.ChatRequest>()),
|
||||||
|
Times.Exactly(3)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -574,6 +574,74 @@ public class ChatServiceTests : UnitTestBase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessMessageAsync_ShouldTrimHistory_WhenCompressionDisabled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var chatId = 99999L;
|
||||||
|
_aiSettings.EnableHistoryCompression = false;
|
||||||
|
|
||||||
|
var session = TestDataBuilder.ChatSessions.CreateBasicSession(chatId);
|
||||||
|
session.MaxHistoryLength = 5; // force small history limit
|
||||||
|
|
||||||
|
_sessionStorageMock
|
||||||
|
.Setup(x => x.GetOrCreate(chatId, It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns(session);
|
||||||
|
_sessionStorageMock.Setup(x => x.Get(chatId)).Returns(session);
|
||||||
|
_aiServiceMock
|
||||||
|
.Setup(x =>
|
||||||
|
x.GenerateChatCompletionWithCompressionAsync(
|
||||||
|
It.IsAny<List<ChatBot.Models.Dto.ChatMessage>>(),
|
||||||
|
It.IsAny<CancellationToken>()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ReturnsAsync("ok");
|
||||||
|
|
||||||
|
// Act: add many messages to exceed the limit
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
await _chatService.ProcessMessageAsync(chatId, "u", $"m{i}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert: history trimmed to MaxHistoryLength
|
||||||
|
session.GetMessageCount().Should().BeLessThanOrEqualTo(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessMessageAsync_ShouldCompressHistory_WhenCompressionEnabledAndThresholdExceeded()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var chatId = 88888L;
|
||||||
|
_aiSettings.EnableHistoryCompression = true;
|
||||||
|
_aiSettings.CompressionThreshold = 4;
|
||||||
|
_aiSettings.CompressionTarget = 3;
|
||||||
|
|
||||||
|
var session = TestDataBuilder.ChatSessions.CreateBasicSession(chatId);
|
||||||
|
session.MaxHistoryLength = 50; // avoid trimming impacting compression assertion
|
||||||
|
|
||||||
|
_sessionStorageMock
|
||||||
|
.Setup(x => x.GetOrCreate(chatId, It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns(session);
|
||||||
|
_sessionStorageMock.Setup(x => x.Get(chatId)).Returns(session);
|
||||||
|
_aiServiceMock
|
||||||
|
.Setup(x =>
|
||||||
|
x.GenerateChatCompletionWithCompressionAsync(
|
||||||
|
It.IsAny<List<ChatBot.Models.Dto.ChatMessage>>(),
|
||||||
|
It.IsAny<CancellationToken>()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ReturnsAsync("ok");
|
||||||
|
|
||||||
|
// Act: add enough messages to exceed threshold
|
||||||
|
for (int i = 0; i < 6; i++)
|
||||||
|
{
|
||||||
|
await _chatService.ProcessMessageAsync(chatId, "u", $"m{i}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert: compressed to target (user+assistant messages should be around target)
|
||||||
|
session.GetMessageCount().Should().BeLessThanOrEqualTo(_aiSettings.CompressionTarget + 1);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ProcessMessageAsync_ShouldLogEmptyResponseMarker()
|
public async Task ProcessMessageAsync_ShouldLogEmptyResponseMarker()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
# Отчет о покрытии тестами проекта ChatBot
|
|
||||||
|
|
||||||
## Общая статистика
|
|
||||||
- **Всего тестов**: 187
|
|
||||||
- **Статус**: ✅ Все тесты проходят успешно
|
|
||||||
- **Покрытие**: Анализ покрытия выполнен
|
|
||||||
|
|
||||||
## Анализ существующих тестов
|
|
||||||
|
|
||||||
### ✅ Покрытые области
|
|
||||||
|
|
||||||
#### 1. Модели (Models)
|
|
||||||
- [x] `ChatSession` - базовые тесты конструктора и методов
|
|
||||||
- [x] `AISettings` - валидация конфигурации
|
|
||||||
- [x] `DatabaseSettings` - валидация конфигурации
|
|
||||||
- [x] `OllamaSettings` - валидация конфигурации
|
|
||||||
- [x] `TelegramBotSettings` - валидация конфигурации
|
|
||||||
|
|
||||||
#### 2. Сервисы (Services)
|
|
||||||
- [x] `AIService` - основные тесты
|
|
||||||
- [x] `ChatService` - unit и integration тесты
|
|
||||||
- [x] `DatabaseInitializationService` - тесты инициализации
|
|
||||||
- [x] `DatabaseSessionStorage` - тесты работы с БД
|
|
||||||
- [x] `InMemorySessionStorage` - тесты in-memory хранилища
|
|
||||||
- [x] `HistoryCompressionService` - тесты сжатия истории
|
|
||||||
- [x] `ModelService` - тесты управления моделями
|
|
||||||
- [x] `OllamaClientAdapter` - тесты адаптера Ollama
|
|
||||||
- [x] `SystemPromptService` - тесты загрузки промптов
|
|
||||||
|
|
||||||
#### 3. Health Checks
|
|
||||||
- [x] `OllamaHealthCheck` - проверка доступности Ollama
|
|
||||||
- [x] `TelegramBotHealthCheck` - проверка Telegram бота
|
|
||||||
|
|
||||||
#### 4. Telegram команды
|
|
||||||
- [x] `StartCommand` - команда /start
|
|
||||||
- [x] `HelpCommand` - команда /help
|
|
||||||
- [x] `ClearCommand` - команда /clear
|
|
||||||
- [x] `SettingsCommand` - команда /settings
|
|
||||||
- [x] `StatusCommand` - команда /status
|
|
||||||
- [x] `CommandRegistry` - реестр команд
|
|
||||||
|
|
||||||
#### 5. Telegram сервисы
|
|
||||||
- [x] `TelegramBotService` - основной сервис бота
|
|
||||||
- [x] `TelegramMessageHandler` - обработчик сообщений
|
|
||||||
- [x] `TelegramMessageSender` - отправка сообщений
|
|
||||||
- [x] `TelegramErrorHandler` - обработка ошибок
|
|
||||||
- [x] `BotInfoService` - информация о боте
|
|
||||||
|
|
||||||
#### 6. Репозитории
|
|
||||||
- [x] `ChatSessionRepository` - работа с БД
|
|
||||||
|
|
||||||
#### 7. Интеграционные тесты
|
|
||||||
- [x] `ProgramIntegrationTests` - тесты инициализации приложения
|
|
||||||
- [x] `ChatServiceIntegrationTests` - интеграционные тесты чата
|
|
||||||
|
|
||||||
## ❌ Области без тестов (требуют покрытия)
|
|
||||||
|
|
||||||
### 1. Модели и DTO
|
|
||||||
- [x] `ChatMessage` (Dto) - тесты для DTO сообщений
|
|
||||||
- [x] `ChatMessageEntity` - тесты для Entity модели сообщений
|
|
||||||
- [x] `ChatSessionEntity` - тесты для Entity модели сессий
|
|
||||||
- [x] `AISettings` - тесты конструктора и свойств
|
|
||||||
- [x] `DatabaseSettings` - тесты конструктора и свойств
|
|
||||||
- [x] `OllamaSettings` - тесты конструктора и свойств
|
|
||||||
- [x] `TelegramBotSettings` - тесты конструктора и свойств
|
|
||||||
|
|
||||||
### 2. Константы
|
|
||||||
- [x] `AIResponseConstants` - тесты констант
|
|
||||||
- [x] `ChatTypes` - тесты типов чатов
|
|
||||||
|
|
||||||
### 3. Сервисы (дополнительные тесты)
|
|
||||||
- [x] `SystemPromptService` - тесты обработки ошибок при загрузке файлов
|
|
||||||
- [x] `ModelService` - тесты с различными настройками
|
|
||||||
- [x] `AIService` - тесты обработки ошибок и retry логики
|
|
||||||
- [x] `ChatService` - тесты edge cases и обработки ошибок
|
|
||||||
- [x] `DatabaseInitializationService` - тесты обработки ошибок БД
|
|
||||||
- [x] `HistoryCompressionService` - тесты различных сценариев сжатия
|
|
||||||
|
|
||||||
### 4. Telegram команды (дополнительные тесты)
|
|
||||||
- [x] `TelegramCommandBase` - тесты базового класса команд
|
|
||||||
- [x] `TelegramCommandProcessor` - тесты обработки команд
|
|
||||||
- [x] `TelegramCommandContext` - тесты контекста команд
|
|
||||||
- [x] `ReplyInfo` - тесты информации о ответах
|
|
||||||
- [x] `CommandAttribute` - тесты атрибутов команд
|
|
||||||
|
|
||||||
### 5. Telegram сервисы (дополнительные тесты)
|
|
||||||
- [x] `TelegramBotClientWrapper` - тесты обертки клиента
|
|
||||||
- [x] `TelegramMessageHandler` - тесты различных типов сообщений
|
|
||||||
- [x] `TelegramErrorHandler` - тесты различных типов ошибок
|
|
||||||
- [x] `TelegramMessageSender` - тесты отправки различных типов сообщений
|
|
||||||
|
|
||||||
### 6. Интерфейсы
|
|
||||||
- [x] `IAIService` - тесты интерфейса
|
|
||||||
- [x] `ISessionStorage` - тесты интерфейса
|
|
||||||
- [x] `IHistoryCompressionService` - тесты интерфейса
|
|
||||||
- [x] `IOllamaClient` - тесты интерфейса
|
|
||||||
- [x] `ITelegramBotClientWrapper` - тесты интерфейса
|
|
||||||
- [x] `IChatSessionRepository` - тесты интерфейса
|
|
||||||
|
|
||||||
### 7. Контекст базы данных
|
|
||||||
- [x] `ChatBotDbContext` - тесты контекста БД
|
|
||||||
- [x] Миграции - тесты миграций
|
|
||||||
|
|
||||||
### 8. Основной файл приложения
|
|
||||||
- [x] `Program.cs` - тесты конфигурации и инициализации
|
|
||||||
|
|
||||||
### 9. Валидаторы (дополнительные тесты)
|
|
||||||
- [x] `AISettingsValidator` - тесты всех валидационных правил
|
|
||||||
- [x] `DatabaseSettingsValidator` - тесты всех валидационных правил
|
|
||||||
- [x] `OllamaSettingsValidator` - тесты всех валидационных правил
|
|
||||||
- [x] `TelegramBotSettingsValidator` - тесты всех валидационных правил
|
|
||||||
|
|
||||||
## Приоритеты для создания тестов
|
|
||||||
|
|
||||||
### 🔴 Высокий приоритет
|
|
||||||
1. **Entity модели** - `ChatMessageEntity`, `ChatSessionEntity`
|
|
||||||
2. **DTO модели** - `ChatMessage`
|
|
||||||
3. **Конфигурационные классы** - `AISettings`, `DatabaseSettings`, `OllamaSettings`, `TelegramBotSettings`
|
|
||||||
4. **Основные сервисы** - дополнительные тесты для `AIService`, `ChatService`
|
|
||||||
5. **Обработка ошибок** - тесты для всех сервисов
|
|
||||||
|
|
||||||
### 🟡 Средний приоритет
|
|
||||||
1. **Telegram команды** - дополнительные тесты для команд
|
|
||||||
2. **Telegram сервисы** - дополнительные тесты для сервисов
|
|
||||||
3. **Валидаторы** - полное покрытие всех правил валидации
|
|
||||||
4. **Константы** - тесты констант
|
|
||||||
|
|
||||||
### 🟢 Низкий приоритет
|
|
||||||
1. **Интерфейсы** - тесты интерфейсов (обычно не требуются)
|
|
||||||
2. **Миграции** - тесты миграций
|
|
||||||
3. **Program.cs** - тесты конфигурации
|
|
||||||
|
|
||||||
## Рекомендации
|
|
||||||
|
|
||||||
1. **Начните с Entity и DTO моделей** - они критически важны для работы приложения
|
|
||||||
2. **Добавьте тесты обработки ошибок** - это повысит надежность приложения
|
|
||||||
3. **Покройте edge cases** - тесты граничных случаев и исключительных ситуаций
|
|
||||||
4. **Добавьте интеграционные тесты** - для проверки взаимодействия компонентов
|
|
||||||
5. **Используйте параметризованные тесты** - для тестирования различных сценариев
|
|
||||||
|
|
||||||
## Метрики качества
|
|
||||||
|
|
||||||
- **Покрытие кода**: ~70% (оценочно)
|
|
||||||
- **Покрытие функциональности**: ~80% (оценочно)
|
|
||||||
- **Покрытие ошибок**: ~30% (оценочно)
|
|
||||||
- **Интеграционное покрытие**: ~60% (оценочно)
|
|
||||||
|
|
||||||
## Следующие шаги
|
|
||||||
|
|
||||||
1. Создать тесты для Entity моделей
|
|
||||||
2. Добавить тесты для DTO классов
|
|
||||||
3. Расширить тесты для основных сервисов
|
|
||||||
4. Добавить тесты обработки ошибок
|
|
||||||
5. Создать дополнительные интеграционные тесты
|
|
||||||
Reference in New Issue
Block a user