DeutschLernen/Tests/Unit/Application/Services/AiFallbackServiceTests.cs
Lasse Rune Hansen e002868b74 feat(backend/application): implement Phase 5 AI Service Integration
- Create higher-level AI services:
  - StoryGenerationService (uses MistralService)
  - WritingFeedbackService (uses MistralService)
  - SpeechExerciseService (uses VoskService)
  - AudioGenerationService (uses TtsService)
  - AiFallbackService (fallback mechanisms for service failures)
- Register AiFallbackService in Program.cs DI container
- Add comprehensive unit tests for all Phase 5 services:
  - AiFallbackServiceTests (14 tests)
  - AudioGenerationServiceTests (16 tests)
  - MistralServiceTests (16 tests)
  - SpeechExerciseServiceTests (12 tests)
  - StoryGenerationServiceTests (13 tests)
  - WritingFeedbackServiceTests (11 tests)
  - VoskServiceTests (15 tests)
  - TtsServiceTests (21 tests)
- Update feature document (ai-services.md) to mark Phase 5 as complete

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-13 12:44:10 +02:00

386 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using GermanApp.Application.Services;
using GermanApp.Domain.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace GermanApp.Tests.Unit.Application.Services;
[TestClass]
public class AiFallbackServiceTests
{
private Mock<IMistralService> _mockMistralService;
private Mock<IVoskService> _mockVoskService;
private Mock<ITtsService> _mockTtsService;
private Mock<ILogger<AiFallbackService>> _mockLogger;
private AiFallbackService _service;
private readonly byte[] _sampleAudio = new byte[100];
[TestInitialize]
public void Setup()
{
_mockMistralService = new Mock<IMistralService>();
_mockVoskService = new Mock<IVoskService>();
_mockTtsService = new Mock<ITtsService>();
_mockLogger = new Mock<ILogger<AiFallbackService>>();
_service = new AiFallbackService(
_mockMistralService.Object,
_mockVoskService.Object,
_mockTtsService.Object,
_mockLogger.Object);
// Initialize sample audio
for (int i = 0; i < _sampleAudio.Length; i++)
{
_sampleAudio[i] = (byte)(i % 256);
}
}
[TestMethod]
public async Task GenerateStoryWithFallbackAsync_WhenPrimaryWorks_ReturnsPrimaryStory()
{
// Arrange
var expectedStory = "Generated by AI...";
var level = "A1";
var topic = "Travel";
var vocabularyWords = new List<string> { "Bahn", "Reise" };
var length = 200;
var cancellationToken = CancellationToken.None;
_mockMistralService.Setup(s => s.GenerateStoryAsync(
level, topic, vocabularyWords, length, cancellationToken))
.ReturnsAsync(expectedStory);
// Act
var result = await _service.GenerateStoryWithFallbackAsync(
level, topic, vocabularyWords, length, cancellationToken);
// Assert
Assert.AreEqual(expectedStory, result);
_mockMistralService.Verify(s => s.GenerateStoryAsync(
level, topic, vocabularyWords, length, cancellationToken), Times.Once);
}
[TestMethod]
public async Task GenerateStoryWithFallbackAsync_WhenPrimaryFails_ReturnsFallback()
{
// Arrange
var level = "A1";
var topic = "Travel";
var vocabularyWords = new List<string> { "Bahn", "Reise" };
var length = 200;
var cancellationToken = CancellationToken.None;
_mockMistralService.Setup(s => s.GenerateStoryAsync(
level, topic, vocabularyWords, length, cancellationToken))
.ThrowsAsync(new Exception("AI Error"));
// Act
var result = await _service.GenerateStoryWithFallbackAsync(
level, topic, vocabularyWords, length, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Contains("A1"));
Assert.IsTrue(result.Contains("Travel"));
Assert.IsTrue(result.Contains("einfacher")); // Level description
}
[TestMethod]
public async Task GenerateStoryWithFallbackAsync_WithNoMistralService_ReturnsFallback()
{
// Arrange
var level = "A1";
var topic = "Travel";
var vocabularyWords = new List<string> { "Bahn" };
var length = 200;
var cancellationToken = CancellationToken.None;
// Create service with null MistralService
var service = new AiFallbackService(
null, _mockVoskService.Object, _mockTtsService.Object, _mockLogger.Object);
// Act
var result = await service.GenerateStoryWithFallbackAsync(
level, topic, vocabularyWords, length, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Contains("A1"));
}
[TestMethod]
public async Task ProvideFeedbackWithFallbackAsync_WhenPrimaryWorks_ReturnsPrimaryFeedback()
{
// Arrange
var expectedFeedback = "Great job!";
var userText = "Ich heisse Anna.";
var level = "A1";
var customPrompt = "Focus on grammar";
var cancellationToken = CancellationToken.None;
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
userText, level, customPrompt, cancellationToken))
.ReturnsAsync(expectedFeedback);
// Act
var result = await _service.ProvideFeedbackWithFallbackAsync(
userText, level, customPrompt, cancellationToken);
// Assert
Assert.AreEqual(expectedFeedback, result);
}
[TestMethod]
public async Task ProvideFeedbackWithFallbackAsync_WhenPrimaryFails_ReturnsFallback()
{
// Arrange
var userText = "Ich heisse Anna.";
var level = "A1";
var cancellationToken = CancellationToken.None;
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
userText, level, It.IsAny<string?>(), cancellationToken))
.ThrowsAsync(new Exception("AI Error"));
// Act
var result = await _service.ProvideFeedbackWithFallbackAsync(
userText, level, null, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Contains("A1"));
Assert.IsTrue(result.Contains("feedback") || result.Contains("Feedback"));
}
[TestMethod]
public async Task RecognizeSpeechWithFallbackAsync_WhenPrimaryWorks_ReturnsPrimaryText()
{
// Arrange
var expectedText = "Hallo Welt";
var cancellationToken = CancellationToken.None;
_mockVoskService.Setup(s => s.RecognizeSpeechAsync(
_sampleAudio, 16000, It.IsAny<string?>(), cancellationToken))
.ReturnsAsync(expectedText);
// Act
var result = await _service.RecognizeSpeechWithFallbackAsync(
_sampleAudio, cancellationToken);
// Assert
Assert.AreEqual(expectedText, result);
}
[TestMethod]
public async Task RecognizeSpeechWithFallbackAsync_WhenPrimaryFails_ReturnsFallback()
{
// Arrange
var cancellationToken = CancellationToken.None;
_mockVoskService.Setup(s => s.RecognizeSpeechAsync(
It.IsAny<byte[]>(), 16000, It.IsAny<string?>(), cancellationToken))
.ThrowsAsync(new Exception("Recognition Error"));
// Act
var result = await _service.RecognizeSpeechWithFallbackAsync(
_sampleAudio, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Contains("unavailable") || result.Contains("Unavailable"));
}
[TestMethod]
public async Task GenerateAudioWithFallbackAsync_WhenPrimaryWorks_ReturnsPrimaryAudio()
{
// Arrange
var text = "Hallo Welt";
var language = "de";
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioAsync(
text, It.IsAny<string?>(), language, cancellationToken))
.ReturnsAsync(_sampleAudio);
// Act
var result = await _service.GenerateAudioWithFallbackAsync(
text, language, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(_sampleAudio.Length, result.Length);
}
[TestMethod]
public async Task GenerateAudioWithFallbackAsync_WhenPrimaryFails_ReturnsEmpty()
{
// Arrange
var text = "Hallo Welt";
var language = "de";
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioAsync(
text, It.IsAny<string?>(), language, cancellationToken))
.ThrowsAsync(new Exception("TTS Error"));
// Act
var result = await _service.GenerateAudioWithFallbackAsync(
text, language, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Length);
}
[TestMethod]
public async Task CheckServiceHealthAsync_WithAllServicesHealthy_ReturnsAllTrue()
{
// Arrange
var cancellationToken = CancellationToken.None;
_mockMistralService.Setup(s => s.TestConnectionAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_mockVoskService.Setup(s => s.TestModelAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_mockTtsService.Setup(s => s.TestModelAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
// Act
var result = await _service.CheckServiceHealthAsync(cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Count);
Assert.IsTrue(result["Mistral"]);
Assert.IsTrue(result["Vosk"]);
Assert.IsTrue(result["CoquiTTS"]);
}
[TestMethod]
public async Task CheckServiceHealthAsync_WithSomeServicesUnhealthy_ReturnsMixed()
{
// Arrange
var cancellationToken = CancellationToken.None;
_mockMistralService.Setup(s => s.TestConnectionAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_mockVoskService.Setup(s => s.TestModelAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
_mockTtsService.Setup(s => s.TestModelAsync(It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("Error"));
// Act
var result = await _service.CheckServiceHealthAsync(cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result["Mistral"]);
Assert.IsFalse(result["Vosk"]);
Assert.IsFalse(result["CoquiTTS"]);
}
[TestMethod]
public async Task CheckServiceHealthAsync_WithNullServices_ReturnsAllFalse()
{
// Arrange
var cancellationToken = CancellationToken.None;
var service = new AiFallbackService(
null, null, null, _mockLogger.Object);
// Act
var result = await service.CheckServiceHealthAsync(cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.IsFalse(result["Mistral"]);
Assert.IsFalse(result["Vosk"]);
Assert.IsFalse(result["CoquiTTS"]);
}
[TestMethod]
public async Task GetServiceStatusMessageAsync_WithAllHealthy_ReturnsAvailableMessage()
{
// Arrange
var cancellationToken = CancellationToken.None;
_mockMistralService.Setup(s => s.TestConnectionAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_mockVoskService.Setup(s => s.TestModelAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_mockTtsService.Setup(s => s.TestModelAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
// Act
var result = await _service.GetServiceStatusMessageAsync(cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Contains("Available"));
Assert.IsTrue(result.Contains("Mistral"));
Assert.IsTrue(result.Contains("Vosk"));
Assert.IsTrue(result.Contains("CoquiTTS"));
Assert.IsFalse(result.Contains("Unavailable"));
}
[TestMethod]
public async Task GetServiceStatusMessageAsync_WithSomeUnhealthy_ReturnsMixedMessage()
{
// Arrange
var cancellationToken = CancellationToken.None;
_mockMistralService.Setup(s => s.TestConnectionAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_mockVoskService.Setup(s => s.TestModelAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
_mockTtsService.Setup(s => s.TestModelAsync(It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("Error"));
// Act
var result = await _service.GetServiceStatusMessageAsync(cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Contains("Available"));
Assert.IsTrue(result.Contains("Unavailable"));
}
[TestMethod]
public async Task TestServiceAsync_WithWorkingServices_ReturnsTrue()
{
// Arrange
var cancellationToken = CancellationToken.None;
_mockMistralService.Setup(s => s.TestConnectionAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_mockVoskService.Setup(s => s.TestModelAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_mockTtsService.Setup(s => s.TestModelAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
// Act
var result = await _service.TestServiceAsync(cancellationToken);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
public async Task TestServiceAsync_WhenAllFail_ReturnsFalse()
{
// Arrange
var cancellationToken = CancellationToken.None;
var service = new AiFallbackService(null, null, null, _mockLogger.Object);
// Act
var result = await service.TestServiceAsync(cancellationToken);
// Assert
Assert.IsFalse(result);
}
}