test(backend/infrastructure): add unit tests for MistralConnector
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>
This commit is contained in:
Lasse Rune Hansen 2026-06-09 20:00:14 +02:00
parent 3ff57ab0a6
commit 57e51d0d0b
2 changed files with 315 additions and 1 deletions

View file

@ -35,7 +35,7 @@ public class MistralConnector : IMistralConnector
_httpClient.BaseAddress = new Uri(_config.BaseUrl);
_httpClient.Timeout = TimeSpan.FromSeconds(_config.TimeoutSeconds);
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_config.ApiKey}");
_httpClient.DefaultRequestHeaders.Add("Content-Type", "application/json");
_httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
_jsonOptions = new JsonSerializerOptions
{

View file

@ -0,0 +1,314 @@
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);
}
}