- 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>
307 lines
10 KiB
C#
307 lines
10 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 StoryGenerationServiceTests
|
|
{
|
|
private Mock<IMistralService> _mockMistralService;
|
|
private Mock<ILogger<StoryGenerationService>> _mockLogger;
|
|
private StoryGenerationService _service;
|
|
|
|
[TestInitialize]
|
|
public void Setup()
|
|
{
|
|
_mockMistralService = new Mock<IMistralService>();
|
|
_mockLogger = new Mock<ILogger<StoryGenerationService>>();
|
|
_service = new StoryGenerationService(
|
|
_mockMistralService.Object,
|
|
_mockLogger.Object);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateStoryAsync_WithValidParameters_ReturnsStory()
|
|
{
|
|
// Arrange
|
|
var expectedStory = "Once upon a time in Germany...";
|
|
var level = "A1";
|
|
var topic = "Travel";
|
|
var vocabularyWords = new List<string> { "Bahn", "Reise", "Stadt" };
|
|
var length = 200;
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync(
|
|
level, topic, vocabularyWords, length, cancellationToken))
|
|
.ReturnsAsync(expectedStory);
|
|
|
|
// Act
|
|
var result = await _service.GenerateStoryAsync(
|
|
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 GenerateStoryAsync_WithEmptyStory_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var level = "A1";
|
|
var topic = "Travel";
|
|
var vocabularyWords = new List<string> { "Bahn" };
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync(
|
|
level, topic, vocabularyWords, 200, cancellationToken))
|
|
.ReturnsAsync(string.Empty);
|
|
|
|
// Act & Assert
|
|
try
|
|
{
|
|
await _service.GenerateStoryAsync(level, topic, vocabularyWords, 200, cancellationToken);
|
|
Assert.Fail("Expected InvalidOperationException was not thrown");
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateStoryAsync_WithNullStory_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var level = "A1";
|
|
var topic = "Travel";
|
|
var vocabularyWords = new List<string> { "Bahn" };
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync(
|
|
level, topic, vocabularyWords, 200, cancellationToken))
|
|
.ReturnsAsync((string?)null);
|
|
|
|
// Act & Assert
|
|
try
|
|
{
|
|
await _service.GenerateStoryAsync(level, topic, vocabularyWords, 200, cancellationToken);
|
|
Assert.Fail("Expected InvalidOperationException was not thrown");
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateStoryAsync_WhenMistralThrows_ThrowsAiServiceException()
|
|
{
|
|
// Arrange
|
|
var level = "A1";
|
|
var topic = "Travel";
|
|
var vocabularyWords = new List<string> { "Bahn" };
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync(
|
|
level, topic, vocabularyWords, 200, cancellationToken))
|
|
.ThrowsAsync(new Exception("API Error"));
|
|
|
|
// Act & Assert
|
|
try
|
|
{
|
|
await _service.GenerateStoryAsync(level, topic, vocabularyWords, 200, cancellationToken);
|
|
Assert.Fail("Expected AiServiceException was not thrown");
|
|
}
|
|
catch (AiServiceException)
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateLessonStoryAsync_WithValidParameters_CallsGenerateStory()
|
|
{
|
|
// Arrange
|
|
var expectedStory = "Lesson story...";
|
|
var lessonTitle = "Greetings";
|
|
var level = "A1";
|
|
var vocabularyWords = new List<string> { "Hallo", "Tschüss" };
|
|
var length = 250;
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync(
|
|
level, lessonTitle, vocabularyWords, length, cancellationToken))
|
|
.ReturnsAsync(expectedStory);
|
|
|
|
// Act
|
|
var result = await _service.GenerateLessonStoryAsync(
|
|
lessonTitle, level, vocabularyWords, length, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.AreEqual(expectedStory, result);
|
|
_mockMistralService.Verify(s => s.GenerateStoryAsync(
|
|
level, lessonTitle, vocabularyWords, length, cancellationToken), Times.Once);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateLessonStoryAsync_WithDefaultLength_Uses250()
|
|
{
|
|
// Arrange
|
|
var expectedStory = "Lesson story...";
|
|
var lessonTitle = "Greetings";
|
|
var level = "A1";
|
|
var vocabularyWords = new List<string> { "Hallo" };
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync(
|
|
level, lessonTitle, vocabularyWords, 250, cancellationToken))
|
|
.ReturnsAsync(expectedStory);
|
|
|
|
// Act
|
|
var result = await _service.GenerateLessonStoryAsync(
|
|
lessonTitle, level, vocabularyWords, cancellationToken: cancellationToken);
|
|
|
|
// Assert
|
|
Assert.AreEqual(expectedStory, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateStoriesByLevelAsync_WithMultipleLevels_ReturnsAllStories()
|
|
{
|
|
// Arrange
|
|
var topic = "Food";
|
|
var vocabularyByLevel = new Dictionary<string, IReadOnlyList<string>>
|
|
{
|
|
["A1"] = new List<string> { "Apfel", "Banane" },
|
|
["A2"] = new List<string> { "Restaurant", "Bestrellung" }
|
|
};
|
|
var lengthByLevel = new Dictionary<string, int>
|
|
{
|
|
["A1"] = 100,
|
|
["A2"] = 150
|
|
};
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync("A1", topic, vocabularyByLevel["A1"], 100, cancellationToken))
|
|
.ReturnsAsync("A1 story");
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync("A2", topic, vocabularyByLevel["A2"], 150, cancellationToken))
|
|
.ReturnsAsync("A2 story");
|
|
|
|
// Act
|
|
var result = await _service.GenerateStoriesByLevelAsync(
|
|
topic, vocabularyByLevel, lengthByLevel, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.AreEqual(2, result.Count);
|
|
Assert.AreEqual("A1 story", result["A1"]);
|
|
Assert.AreEqual("A2 story", result["A2"]);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateStoriesByLevelAsync_WithMissingLength_UsesDefault()
|
|
{
|
|
// Arrange
|
|
var topic = "Food";
|
|
var vocabularyByLevel = new Dictionary<string, IReadOnlyList<string>>
|
|
{
|
|
["A1"] = new List<string> { "Apfel" }
|
|
};
|
|
// No lengthByLevel provided
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync("A1", topic, vocabularyByLevel["A1"], 200, cancellationToken))
|
|
.ReturnsAsync("A1 story");
|
|
|
|
// Act
|
|
var result = await _service.GenerateStoriesByLevelAsync(
|
|
topic, vocabularyByLevel, null, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.AreEqual(1, result.Count);
|
|
Assert.AreEqual("A1 story", result["A1"]);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateStoriesByLevelAsync_WhenOneLevelFails_ReturnsEmptyForThatLevel()
|
|
{
|
|
// Arrange
|
|
var topic = "Food";
|
|
var vocabularyByLevel = new Dictionary<string, IReadOnlyList<string>>
|
|
{
|
|
["A1"] = new List<string> { "Apfel" },
|
|
["A2"] = new List<string> { "Restaurant" }
|
|
};
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync("A1", topic, vocabularyByLevel["A1"], 200, cancellationToken))
|
|
.ReturnsAsync("A1 story");
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync("A2", topic, vocabularyByLevel["A2"], 200, cancellationToken))
|
|
.ThrowsAsync(new Exception("Error"));
|
|
|
|
// Act
|
|
var result = await _service.GenerateStoriesByLevelAsync(
|
|
topic, vocabularyByLevel, null, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.AreEqual(2, result.Count);
|
|
Assert.AreEqual("A1 story", result["A1"]);
|
|
Assert.AreEqual(string.Empty, result["A2"]);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestServiceAsync_WithWorkingService_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var cancellationToken = CancellationToken.None;
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync(
|
|
"A1", "Test", It.IsAny<IReadOnlyList<string>>(), 50, cancellationToken))
|
|
.ReturnsAsync("Test story");
|
|
|
|
// Act
|
|
var result = await _service.TestServiceAsync(cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsTrue(result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestServiceAsync_WhenServiceFails_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var cancellationToken = CancellationToken.None;
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync(
|
|
"A1", "Test", It.IsAny<IReadOnlyList<string>>(), 50, cancellationToken))
|
|
.ThrowsAsync(new Exception("Error"));
|
|
|
|
// Act
|
|
var result = await _service.TestServiceAsync(cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsFalse(result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestServiceAsync_WithEmptyResult_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var cancellationToken = CancellationToken.None;
|
|
_mockMistralService.Setup(s => s.GenerateStoryAsync(
|
|
"A1", "Test", It.IsAny<IReadOnlyList<string>>(), 50, cancellationToken))
|
|
.ReturnsAsync(string.Empty);
|
|
|
|
// Act
|
|
var result = await _service.TestServiceAsync(cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsFalse(result);
|
|
}
|
|
}
|