Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Add Tests/Unit/Infrastructure/Services/MistralConnectorTests.cs with 20 tests - Constructor validation tests - Configuration validation tests - Model factory method tests - Rate limiter tests - Circuit breaker tests - Fix HttpClient Content-Type header issue (use Accept header instead) - All 137 unit tests passing (117 previous + 20 new) Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
314 lines
9.6 KiB
C#
314 lines
9.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using GermanApp.Application.Models;
|
|
using GermanApp.Infrastructure.Configuration;
|
|
using GermanApp.Infrastructure.Services;
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
|
|
namespace GermanApp.Tests.Unit.Infrastructure.Services;
|
|
|
|
[TestClass]
|
|
public class MistralConnectorTests
|
|
{
|
|
private HttpClient _httpClient;
|
|
private MistralConfig _config;
|
|
private MistralConnector _connector;
|
|
|
|
[TestInitialize]
|
|
public void Setup()
|
|
{
|
|
_config = new MistralConfig
|
|
{
|
|
ApiKey = "test-api-key",
|
|
BaseUrl = "https://api.mistral.ai/v1/",
|
|
DefaultModel = "mistral-medium",
|
|
TimeoutSeconds = 30,
|
|
MaxRetries = 3,
|
|
RateLimitPerMinute = 10,
|
|
EnableCaching = true,
|
|
CacheTTLMinutes = 60,
|
|
CircuitBreakerFailureThreshold = 5,
|
|
CircuitBreakerResetMinutes = 1
|
|
};
|
|
|
|
_httpClient = new HttpClient();
|
|
_connector = new MistralConnector(_httpClient, _config,
|
|
Microsoft.Extensions.Logging.Abstractions.NullLogger<MistralConnector>.Instance,
|
|
new Microsoft.Extensions.Caching.Memory.MemoryCache(
|
|
Microsoft.Extensions.Options.Options.Create(new Microsoft.Extensions.Caching.Memory.MemoryCacheOptions())));
|
|
}
|
|
|
|
[TestCleanup]
|
|
public void Cleanup()
|
|
{
|
|
_httpClient?.Dispose();
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Constructor_WithValidParameters_CreatesConnector()
|
|
{
|
|
Assert.IsNotNull(_connector);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Constructor_WithEmptyApiKey_ThrowsArgumentException()
|
|
{
|
|
var invalidConfig = new MistralConfig { ApiKey = "", BaseUrl = "https://api.mistral.ai/v1/" };
|
|
|
|
try
|
|
{
|
|
new MistralConnector(_httpClient, invalidConfig,
|
|
Microsoft.Extensions.Logging.Abstractions.NullLogger<MistralConnector>.Instance,
|
|
new Microsoft.Extensions.Caching.Memory.MemoryCache(
|
|
Microsoft.Extensions.Options.Options.Create(new Microsoft.Extensions.Caching.Memory.MemoryCacheOptions())));
|
|
Assert.Fail("Expected ArgumentException was not thrown");
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Constructor_WithInvalidBaseUrl_ThrowsArgumentException()
|
|
{
|
|
var invalidConfig = new MistralConfig { ApiKey = "key", BaseUrl = "not-a-url" };
|
|
|
|
try
|
|
{
|
|
new MistralConnector(_httpClient, invalidConfig,
|
|
Microsoft.Extensions.Logging.Abstractions.NullLogger<MistralConnector>.Instance,
|
|
new Microsoft.Extensions.Caching.Memory.MemoryCache(
|
|
Microsoft.Extensions.Options.Options.Create(new Microsoft.Extensions.Caching.Memory.MemoryCacheOptions())));
|
|
Assert.Fail("Expected ArgumentException was not thrown");
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Constructor_ConfiguresHttpClient()
|
|
{
|
|
Assert.AreEqual(new Uri("https://api.mistral.ai/v1/"), _httpClient.BaseAddress);
|
|
Assert.AreEqual(TimeSpan.FromSeconds(30), _httpClient.Timeout);
|
|
Assert.AreEqual("Bearer test-api-key", _httpClient.DefaultRequestHeaders.Authorization?.ToString());
|
|
Assert.IsTrue(_httpClient.DefaultRequestHeaders.Accept.Any(h => h.MediaType == "application/json"));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralConfig_Validate_WithValidConfig_Passes()
|
|
{
|
|
var config = new MistralConfig
|
|
{
|
|
ApiKey = "valid-key",
|
|
BaseUrl = "https://api.mistral.ai/v1/"
|
|
};
|
|
|
|
// Should not throw
|
|
config.Validate();
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralConfig_Validate_WithEmptyApiKey_Throws()
|
|
{
|
|
var config = new MistralConfig { ApiKey = "", BaseUrl = "https://api.mistral.ai/v1/" };
|
|
|
|
try
|
|
{
|
|
config.Validate();
|
|
Assert.Fail("Expected ArgumentException was not thrown");
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralConfig_Validate_WithInvalidTimeout_Throws()
|
|
{
|
|
var config = new MistralConfig { ApiKey = "key", BaseUrl = "https://api.mistral.ai/v1/", TimeoutSeconds = 0 };
|
|
|
|
try
|
|
{
|
|
config.Validate();
|
|
Assert.Fail("Expected ArgumentException was not thrown");
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralConfig_Validate_WithNegativeMaxRetries_Throws()
|
|
{
|
|
var config = new MistralConfig { ApiKey = "key", BaseUrl = "https://api.mistral.ai/v1/", MaxRetries = -1 };
|
|
|
|
try
|
|
{
|
|
config.Validate();
|
|
Assert.Fail("Expected ArgumentException was not thrown");
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralRequest_CreateCompletion_CreatesRequest()
|
|
{
|
|
var request = MistralRequest.CreateCompletion("Test prompt", "mistral-medium", 512);
|
|
|
|
Assert.AreEqual("Test prompt", request.Prompt);
|
|
Assert.AreEqual("mistral-medium", request.Model);
|
|
Assert.AreEqual(512, request.MaxTokens);
|
|
Assert.AreEqual(0.7, request.Temperature);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralChatRequest_CreateChat_CreatesRequest()
|
|
{
|
|
var messages = new List<MistralMessage>
|
|
{
|
|
MistralMessage.User("Hello")
|
|
};
|
|
var request = MistralChatRequest.CreateChat(messages, "mistral-medium", 512);
|
|
|
|
Assert.AreEqual(1, request.Messages.Count);
|
|
Assert.AreEqual("user", request.Messages[0].Role);
|
|
Assert.AreEqual("Hello", request.Messages[0].Content);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralMessage_FactoryMethods_CreateMessages()
|
|
{
|
|
var userMsg = MistralMessage.User("User message");
|
|
var assistantMsg = MistralMessage.Assistant("Assistant message");
|
|
var systemMsg = MistralMessage.System("System message");
|
|
|
|
Assert.AreEqual("user", userMsg.Role);
|
|
Assert.AreEqual("User message", userMsg.Content);
|
|
|
|
Assert.AreEqual("assistant", assistantMsg.Role);
|
|
Assert.AreEqual("Assistant message", assistantMsg.Content);
|
|
|
|
Assert.AreEqual("system", systemMsg.Role);
|
|
Assert.AreEqual("System message", systemMsg.Content);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralResponse_HasValidChoices_WithEmptyChoices_ReturnsFalse()
|
|
{
|
|
var response = new MistralResponse
|
|
{
|
|
Model = "mistral-medium",
|
|
Choices = new List<MistralChoice>(),
|
|
Usage = new MistralUsage()
|
|
};
|
|
|
|
Assert.IsFalse(response.HasValidChoices);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralResponse_HasValidChoices_WithText_ReturnsTrue()
|
|
{
|
|
var choices = new List<MistralChoice>
|
|
{
|
|
new MistralChoice { Text = "Test completion", Index = 0 }
|
|
};
|
|
var response = new MistralResponse
|
|
{
|
|
Model = "mistral-medium",
|
|
Choices = choices,
|
|
Usage = new MistralUsage()
|
|
};
|
|
|
|
Assert.IsTrue(response.HasValidChoices);
|
|
Assert.AreEqual("Test completion", response.FirstChoiceText);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralUsage_EstimatedCost_CalculatesCorrectly()
|
|
{
|
|
var usage = new MistralUsage
|
|
{
|
|
PromptTokens = 100,
|
|
CompletionTokens = 50,
|
|
TotalTokens = 150
|
|
};
|
|
|
|
// Approx $0.0000025 per token for mistral-medium
|
|
decimal expectedCost = 150 * 0.0000025m;
|
|
Assert.AreEqual(expectedCost, usage.EstimatedCost);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralRateLimiter_TryAcquire_WithinLimit_ReturnsTrue()
|
|
{
|
|
var limiter = new MistralRateLimiter(10);
|
|
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
Assert.IsTrue(limiter.TryAcquire("test"));
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralRateLimiter_TryAcquire_ExceedsLimit_ReturnsFalse()
|
|
{
|
|
var limiter = new MistralRateLimiter(2);
|
|
|
|
Assert.IsTrue(limiter.TryAcquire("test"));
|
|
Assert.IsTrue(limiter.TryAcquire("test"));
|
|
Assert.IsFalse(limiter.TryAcquire("test"));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralCircuitBreaker_IsClosed_Initially_ReturnsTrue()
|
|
{
|
|
var breaker = new MistralCircuitBreaker(5, TimeSpan.FromMinutes(1));
|
|
Assert.IsTrue(breaker.IsClosed);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralCircuitBreaker_AfterFailures_Opens()
|
|
{
|
|
var breaker = new MistralCircuitBreaker(2, TimeSpan.FromMinutes(1));
|
|
|
|
breaker.RecordFailure();
|
|
breaker.RecordFailure();
|
|
|
|
Assert.IsFalse(breaker.IsClosed);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralCircuitBreaker_AfterSuccess_Resets()
|
|
{
|
|
var breaker = new MistralCircuitBreaker(2, TimeSpan.FromMinutes(1));
|
|
|
|
breaker.RecordFailure();
|
|
breaker.RecordSuccess();
|
|
|
|
Assert.IsTrue(breaker.IsClosed);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void MistralCircuitBreaker_AfterResetTimeout_ResetsAutomatically()
|
|
{
|
|
var breaker = new MistralCircuitBreaker(1, TimeSpan.FromMilliseconds(10));
|
|
|
|
breaker.RecordFailure();
|
|
Assert.IsFalse(breaker.IsClosed);
|
|
|
|
// Wait for reset
|
|
System.Threading.Tasks.Task.Delay(20).Wait();
|
|
|
|
Assert.IsTrue(breaker.IsClosed);
|
|
}
|
|
}
|