using System.IO; using ChatBot.Models.Configuration; using ChatBot.Models.Configuration.Validators; using FluentAssertions; using Microsoft.Extensions.Options; namespace ChatBot.Tests.Configuration.Validators; public class AISettingsValidatorTests : IDisposable { private readonly string _testPromptPath; private readonly AISettingsValidator _validator; private bool _disposed; public AISettingsValidatorTests() { _testPromptPath = Path.Combine(Path.GetTempPath(), "test-prompt.txt"); _validator = new AISettingsValidator(); } [Fact] public void Validate_WithValidSettings_ShouldReturnSuccess() { // Arrange var settings = CreateValidAISettings(); // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); result.Failed.Should().BeFalse(); result.FailureMessage.Should().BeNullOrEmpty(); } [Theory] [InlineData(-0.1)] [InlineData(2.1)] [InlineData(-1.0)] [InlineData(3.0)] public void Validate_WithInvalidTemperature_ShouldReturnFailure(double temperature) { // Arrange var settings = CreateValidAISettings(); settings.Temperature = temperature; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.Failed.Should().BeTrue(); result.FailureMessage.Should().Contain("Temperature must be between 0.0 and 2.0"); } [Theory] [InlineData(0.0)] [InlineData(1.0)] [InlineData(2.0)] [InlineData(0.5)] [InlineData(1.5)] public void Validate_WithValidTemperature_ShouldReturnSuccess(double temperature) { // Arrange var settings = CreateValidAISettings(); settings.Temperature = temperature; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); } [Fact] public void Validate_WithEmptySystemPromptPath_ShouldReturnFailure() { // Arrange var settings = CreateValidAISettings(); settings.SystemPromptPath = string.Empty; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("System prompt path cannot be empty"); } [Fact] public void Validate_WithNullSystemPromptPath_ShouldReturnFailure() { // Arrange var settings = CreateValidAISettings(); settings.SystemPromptPath = null!; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("System prompt path cannot be empty"); } [Fact] public void Validate_WithNonExistentSystemPromptFile_ShouldReturnFailure() { // Arrange var settings = CreateValidAISettings(); settings.SystemPromptPath = "non-existent-file.txt"; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("System prompt file not found at path"); } [Fact] public void Validate_WithEmptySystemPromptFile_ShouldReturnFailure() { // Arrange var testPromptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "test-prompt.txt"); File.WriteAllText(testPromptPath, ""); var settings = CreateValidAISettings(); settings.SystemPromptPath = "test-prompt.txt"; try { // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("System prompt file is empty"); } finally { if (File.Exists(testPromptPath)) { File.Delete(testPromptPath); } } } [Fact] public void Validate_WithTooLongSystemPromptFile_ShouldReturnFailure() { // Arrange var testPromptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "test-prompt.txt"); var longContent = new string('A', 4001); File.WriteAllText(testPromptPath, longContent); var settings = CreateValidAISettings(); settings.SystemPromptPath = "test-prompt.txt"; try { // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result .FailureMessage.Should() .Contain("System prompt content cannot exceed 4000 characters"); } finally { if (File.Exists(testPromptPath)) { File.Delete(testPromptPath); } } } [Fact] public void Validate_WithValidSystemPromptFile_ShouldReturnSuccess() { // Arrange var testPromptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "test-prompt.txt"); var content = "Valid system prompt content"; File.WriteAllText(testPromptPath, content); var settings = CreateValidAISettings(); settings.SystemPromptPath = "test-prompt.txt"; try { // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); } finally { if (File.Exists(testPromptPath)) { File.Delete(testPromptPath); } } } [Theory] [InlineData(0)] [InlineData(-1)] [InlineData(11)] [InlineData(100)] public void Validate_WithInvalidMaxRetryAttempts_ShouldReturnFailure(int maxRetryAttempts) { // Arrange var settings = CreateValidAISettings(); settings.MaxRetryAttempts = maxRetryAttempts; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("Max retry attempts"); } [Theory] [InlineData(1)] [InlineData(5)] [InlineData(10)] public void Validate_WithValidMaxRetryAttempts_ShouldReturnSuccess(int maxRetryAttempts) { // Arrange var settings = CreateValidAISettings(); settings.MaxRetryAttempts = maxRetryAttempts; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); } [Theory] [InlineData(99)] [InlineData(30001)] [InlineData(0)] [InlineData(-1)] public void Validate_WithInvalidRetryDelay_ShouldReturnFailure(int retryDelayMs) { // Arrange var settings = CreateValidAISettings(); settings.RetryDelayMs = retryDelayMs; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("Retry delay"); } [Theory] [InlineData(100)] [InlineData(1000)] [InlineData(30000)] public void Validate_WithValidRetryDelay_ShouldReturnSuccess(int retryDelayMs) { // Arrange var settings = CreateValidAISettings(); settings.RetryDelayMs = retryDelayMs; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); } [Theory] [InlineData(9)] [InlineData(301)] [InlineData(0)] [InlineData(-1)] public void Validate_WithInvalidRequestTimeout_ShouldReturnFailure(int requestTimeoutSeconds) { // Arrange var settings = CreateValidAISettings(); settings.RequestTimeoutSeconds = requestTimeoutSeconds; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("Request timeout"); } [Theory] [InlineData(10)] [InlineData(60)] [InlineData(300)] public void Validate_WithValidRequestTimeout_ShouldReturnSuccess(int requestTimeoutSeconds) { // Arrange var settings = CreateValidAISettings(); settings.RequestTimeoutSeconds = requestTimeoutSeconds; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); } [Theory] [InlineData(4)] [InlineData(101)] [InlineData(0)] [InlineData(-1)] public void Validate_WithInvalidCompressionThreshold_ShouldReturnFailure( int compressionThreshold ) { // Arrange var settings = CreateValidAISettings(); settings.CompressionThreshold = compressionThreshold; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("Compression threshold"); } [Theory] [InlineData(20)] [InlineData(50)] [InlineData(100)] public void Validate_WithValidCompressionThreshold_ShouldReturnSuccess(int compressionThreshold) { // Arrange var settings = CreateValidAISettings(); settings.CompressionThreshold = compressionThreshold; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); } [Theory] [InlineData(2)] [InlineData(0)] [InlineData(-1)] public void Validate_WithInvalidCompressionTarget_ShouldReturnFailure(int compressionTarget) { // Arrange var settings = CreateValidAISettings(); settings.CompressionTarget = compressionTarget; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("Compression target"); } [Fact] public void Validate_WithCompressionTargetGreaterThanOrEqualToThreshold_ShouldReturnFailure() { // Arrange var settings = CreateValidAISettings(); settings.CompressionThreshold = 20; settings.CompressionTarget = 20; // Equal to threshold // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result .FailureMessage.Should() .Contain("Compression target must be less than compression threshold"); } [Theory] [InlineData(3)] [InlineData(10)] [InlineData(19)] public void Validate_WithValidCompressionTarget_ShouldReturnSuccess(int compressionTarget) { // Arrange var settings = CreateValidAISettings(); settings.CompressionTarget = compressionTarget; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); } [Theory] [InlineData(9)] [InlineData(501)] [InlineData(0)] [InlineData(-1)] public void Validate_WithInvalidMinMessageLengthForSummarization_ShouldReturnFailure( int minLength ) { // Arrange var settings = CreateValidAISettings(); settings.MinMessageLengthForSummarization = minLength; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("Minimum message length for summarization"); } [Theory] [InlineData(10)] [InlineData(50)] [InlineData(100)] public void Validate_WithValidMinMessageLengthForSummarization_ShouldReturnSuccess( int minLength ) { // Arrange var settings = CreateValidAISettings(); settings.MinMessageLengthForSummarization = minLength; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); } [Theory] [InlineData(19)] [InlineData(1001)] [InlineData(0)] [InlineData(-1)] public void Validate_WithInvalidMaxSummarizedMessageLength_ShouldReturnFailure(int maxLength) { // Arrange var settings = CreateValidAISettings(); settings.MaxSummarizedMessageLength = maxLength; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("Maximum summarized message length"); } [Theory] [InlineData(100)] [InlineData(200)] [InlineData(1000)] public void Validate_WithValidMaxSummarizedMessageLength_ShouldReturnSuccess(int maxLength) { // Arrange var settings = CreateValidAISettings(); settings.MaxSummarizedMessageLength = maxLength; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); } [Fact] public void Validate_WithMaxSummarizedMessageLengthLessThanOrEqualToMinLength_ShouldReturnFailure() { // Arrange var settings = CreateValidAISettings(); settings.MinMessageLengthForSummarization = 50; settings.MaxSummarizedMessageLength = 50; // Equal to min length // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result .FailureMessage.Should() .Contain( "Maximum summarized message length must be greater than minimum message length for summarization" ); } [Theory] [InlineData(999)] [InlineData(300001)] [InlineData(0)] [InlineData(-1)] public void Validate_WithInvalidMaxRetryDelay_ShouldReturnFailure(int maxRetryDelayMs) { // Arrange var settings = CreateValidAISettings(); settings.MaxRetryDelayMs = maxRetryDelayMs; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("Maximum retry delay"); } [Theory] [InlineData(1000)] [InlineData(30000)] [InlineData(300000)] public void Validate_WithValidMaxRetryDelay_ShouldReturnSuccess(int maxRetryDelayMs) { // Arrange var settings = CreateValidAISettings(); settings.MaxRetryDelayMs = maxRetryDelayMs; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); } [Theory] [InlineData(4)] [InlineData(301)] [InlineData(0)] [InlineData(-1)] public void Validate_WithInvalidCompressionTimeout_ShouldReturnFailure( int compressionTimeoutSeconds ) { // Arrange var settings = CreateValidAISettings(); settings.CompressionTimeoutSeconds = compressionTimeoutSeconds; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("Compression timeout"); } [Theory] [InlineData(5)] [InlineData(30)] [InlineData(300)] public void Validate_WithValidCompressionTimeout_ShouldReturnSuccess( int compressionTimeoutSeconds ) { // Arrange var settings = CreateValidAISettings(); settings.CompressionTimeoutSeconds = compressionTimeoutSeconds; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); } [Theory] [InlineData(1)] [InlineData(61)] [InlineData(0)] [InlineData(-1)] public void Validate_WithInvalidStatusCheckTimeout_ShouldReturnFailure( int statusCheckTimeoutSeconds ) { // Arrange var settings = CreateValidAISettings(); settings.StatusCheckTimeoutSeconds = statusCheckTimeoutSeconds; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("Status check timeout"); } [Theory] [InlineData(2)] [InlineData(10)] [InlineData(60)] public void Validate_WithValidStatusCheckTimeout_ShouldReturnSuccess( int statusCheckTimeoutSeconds ) { // Arrange var settings = CreateValidAISettings(); settings.StatusCheckTimeoutSeconds = statusCheckTimeoutSeconds; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeTrue(); } [Fact] public void Validate_WithMultipleValidationErrors_ShouldReturnAllErrors() { // Arrange var settings = new AISettings { Temperature = -1.0, // Invalid SystemPromptPath = string.Empty, // Invalid MaxRetryAttempts = 0, // Invalid RetryDelayMs = 50, // Invalid RequestTimeoutSeconds = 5, // Invalid CompressionThreshold = 2, // Invalid CompressionTarget = 1, // Invalid MinMessageLengthForSummarization = 5, // Invalid MaxSummarizedMessageLength = 10, // Invalid MaxRetryDelayMs = 500, // Invalid CompressionTimeoutSeconds = 2, // Invalid StatusCheckTimeoutSeconds = 1, // Invalid }; // Act var result = _validator.Validate(null, settings); // Assert result.Succeeded.Should().BeFalse(); result.FailureMessage.Should().Contain("Temperature must be between 0.0 and 2.0"); result.FailureMessage.Should().Contain("System prompt path cannot be empty"); result.FailureMessage.Should().Contain("Max retry attempts must be at least 1"); result.FailureMessage.Should().Contain("Retry delay must be at least 100ms"); result.FailureMessage.Should().Contain("Request timeout must be at least 10 seconds"); result.FailureMessage.Should().Contain("Compression threshold must be at least 5 messages"); result.FailureMessage.Should().Contain("Compression target must be at least 3 messages"); result .FailureMessage.Should() .Contain("Minimum message length for summarization must be at least 10 characters"); result .FailureMessage.Should() .Contain("Maximum summarized message length must be at least 20 characters"); result.FailureMessage.Should().Contain("Maximum retry delay must be at least 1000ms"); result.FailureMessage.Should().Contain("Compression timeout must be at least 5 seconds"); result.FailureMessage.Should().Contain("Status check timeout must be at least 2 seconds"); } private static AISettings CreateValidAISettings() { return new AISettings { Temperature = 0.7, SystemPromptPath = "Prompts/system-prompt.txt", MaxRetryAttempts = 3, RetryDelayMs = 1000, RequestTimeoutSeconds = 60, EnableHistoryCompression = true, CompressionThreshold = 20, CompressionTarget = 10, MinMessageLengthForSummarization = 50, MaxSummarizedMessageLength = 200, EnableExponentialBackoff = true, MaxRetryDelayMs = 30000, CompressionTimeoutSeconds = 30, StatusCheckTimeoutSeconds = 10, }; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_disposed && disposing) { if (File.Exists(_testPromptPath)) { File.Delete(_testPromptPath); } _disposed = true; } } }