DeutschLernen/Tests/Unit/Application/Services/StoryServiceTests.cs
Mistral Vibe 01f6a1fd30 test(backend): add Phase 3 unit tests for Story Integration
- StoryServiceTests.cs: 536 lines, comprehensive tests for StoryService
- StoryGenerationServiceTests.cs: 469 lines, tests for AI story generation
- StoryUnlockServiceTests.cs: 292 lines, tests for story unlock logic
- StoryRepositoryTests.cs: 120 lines, tests for EF Core repository
- StoryProgressRepositoryTests.cs: 74 lines, tests for progress repository
- AppDbContext.cs: Made DbSet properties virtual for Moq compatibility
- Fixed return types in StoryRepository and StoryProgressRepository
- Updated docs/features/story-integration.md for Phase 3 completion
- All 324 unit tests pass

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-13 14:23:54 +02:00

536 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using GermanApp.Application.DTOs;
using GermanApp.Application.Services;
using GermanApp.Domain.Entities;
using GermanApp.Domain.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace GermanApp.Tests.Unit.Application.Services;
[TestClass]
public class StoryServiceTests
{
private Mock<IStoryRepository> _mockStoryRepository;
private Mock<IStoryProgressRepository> _mockProgressRepository;
private Mock<ILogger<StoryService>> _mockLogger;
private StoryService _service;
[TestInitialize]
public void Setup()
{
_mockStoryRepository = new Mock<IStoryRepository>();
_mockProgressRepository = new Mock<IStoryProgressRepository>();
_mockLogger = new Mock<ILogger<StoryService>>();
_service = new StoryService(
_mockStoryRepository.Object,
_mockProgressRepository.Object,
_mockLogger.Object);
}
[TestMethod]
public async Task GetByIdAsync_WithExistingId_ReturnsSegment()
{
// Arrange
var segment = StorySegment.Create(1, 1, "Test content", 1, "Test Title", "Test Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(segment, 1);
_mockStoryRepository.Setup(s => s.GetByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(segment);
// Act
var result = await _service.GetByIdAsync(1);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Id);
Assert.AreEqual("Test content", result.Content);
Assert.AreEqual("Test Title", result.Title);
}
[TestMethod]
public async Task GetByIdAsync_WithNonExistingId_ReturnsNull()
{
// Arrange
_mockStoryRepository.Setup(s => s.GetByIdAsync(999, It.IsAny<CancellationToken>()))
.ReturnsAsync((StorySegment?)null);
// Act
var result = await _service.GetByIdAsync(999);
// Assert
Assert.IsNull(result);
}
[TestMethod]
public async Task GetByLevelAsync_WithExistingLevel_ReturnsSegments()
{
// Arrange
var segment1 = StorySegment.Create(1, 1, "Content 1", 1, "Title 1", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(segment1, 1);
var segment2 = StorySegment.Create(1, 2, "Content 2", 2, "Title 2", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(segment2, 2);
_mockStoryRepository.Setup(s => s.GetByLevelAsync(1, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<StorySegment> { segment1, segment2 });
// Act
var result = await _service.GetByLevelAsync(1);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(2, result.Count);
Assert.AreEqual("Content 1", result[0].Content);
Assert.AreEqual("Content 2", result[1].Content);
}
[TestMethod]
public async Task GetByLevelAsync_WithNoSegments_ReturnsEmptyList()
{
// Arrange
_mockStoryRepository.Setup(s => s.GetByLevelAsync(1, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<StorySegment>());
// Act
var result = await _service.GetByLevelAsync(1);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Count);
}
[TestMethod]
public async Task GetByLessonAsync_WithExistingLesson_ReturnsSegments()
{
// Arrange
var segment = StorySegment.Create(1, 1, "Test content", 1, "Test Title", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(segment, 1);
_mockStoryRepository.Setup(s => s.GetByLessonAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<StorySegment> { segment });
// Act
var result = await _service.GetByLessonAsync(1);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Count);
Assert.AreEqual("Test content", result[0].Content);
}
[TestMethod]
public async Task CreateAsync_WithValidData_CreatesSegment()
{
// Arrange
var dto = new CreateStorySegmentDto(1, 1, "New content", 1, "New Title", "New Theme");
var createdSegment = StorySegment.Create(1, 1, "New content", 1, "New Title", "New Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(createdSegment, 10);
_mockStoryRepository.Setup(s => s.GetByOrderRangeAsync(1, 1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<StorySegment>()); // No existing segment
_mockStoryRepository.Setup(s => s.AddAsync(It.IsAny<StorySegment>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(createdSegment);
// Act
var result = await _service.CreateAsync(dto);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(10, result.Id);
Assert.AreEqual("New content", result.Content);
_mockStoryRepository.Verify(s => s.AddAsync(It.IsAny<StorySegment>(), It.IsAny<CancellationToken>()), Times.Once);
}
[TestMethod]
public async Task CreateAsync_WithDuplicateOrder_ThrowsException()
{
// Arrange
var dto = new CreateStorySegmentDto(1, 1, "New content", 1, "New Title", "New Theme");
var existingSegment = StorySegment.Create(1, 1, "Existing", 1, "Existing", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(existingSegment, 1);
_mockStoryRepository.Setup(s => s.GetByOrderRangeAsync(1, 1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<StorySegment> { existingSegment });
// Act & Assert
try
{
await _service.CreateAsync(dto);
Assert.Fail("Expected InvalidOperationException was not thrown");
}
catch (InvalidOperationException ex)
{
Assert.IsTrue(ex.Message.Contains("already exists"));
}
}
[TestMethod]
public async Task UpdateAsync_WithExistingId_UpdatesSegment()
{
// Arrange
var existingSegment = StorySegment.Create(1, 1, "Old content", 1, "Old Title", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(existingSegment, 1);
var dto = new UpdateStorySegmentDto(Content: "New content", Title: "New Title");
_mockStoryRepository.Setup(s => s.GetByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(existingSegment);
_mockStoryRepository.Setup(s => s.UpdateAsync(It.IsAny<StorySegment>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
// Act
var result = await _service.UpdateAsync(1, dto);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("New content", result.Content);
Assert.AreEqual("New Title", result.Title);
_mockStoryRepository.Verify(s => s.UpdateAsync(It.IsAny<StorySegment>(), It.IsAny<CancellationToken>()), Times.Once);
}
[TestMethod]
public async Task UpdateAsync_WithNonExistingId_ReturnsNull()
{
// Arrange
var dto = new UpdateStorySegmentDto(Content: "New content");
_mockStoryRepository.Setup(s => s.GetByIdAsync(999, It.IsAny<CancellationToken>()))
.ReturnsAsync((StorySegment?)null);
// Act
var result = await _service.UpdateAsync(999, dto);
// Assert
Assert.IsNull(result);
}
[TestMethod]
public async Task DeleteAsync_WithExistingId_DeletesSegment()
{
// Arrange
_mockStoryRepository.Setup(s => s.ExistsAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_mockStoryRepository.Setup(s => s.DeleteAsync(1, It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
// Act
var result = await _service.DeleteAsync(1);
// Assert
Assert.IsTrue(result);
_mockStoryRepository.Verify(s => s.DeleteAsync(1, It.IsAny<CancellationToken>()), Times.Once);
}
[TestMethod]
public async Task DeleteAsync_WithNonExistingId_ReturnsFalse()
{
// Arrange
_mockStoryRepository.Setup(s => s.ExistsAsync(999, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
// Act
var result = await _service.DeleteAsync(999);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public async Task GetNextSegmentToUnlockAsync_WithValidLesson_ReturnsSegment()
{
// Arrange
var segment = StorySegment.Create(1, 2, "Next content", 2, "Next Title", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(segment, 2);
_mockStoryRepository.Setup(s => s.GetNextSegmentToUnlockAsync(1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(segment);
// Act
var result = await _service.GetNextSegmentToUnlockAsync(1, 1);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(2, result.Id);
Assert.AreEqual("Next content", result.Content);
}
[TestMethod]
public async Task GetNextSegmentToUnlockAsync_WithNoSegment_ReturnsNull()
{
// Arrange
_mockStoryRepository.Setup(s => s.GetNextSegmentToUnlockAsync(1, 10, It.IsAny<CancellationToken>()))
.ReturnsAsync((StorySegment?)null);
// Act
var result = await _service.GetNextSegmentToUnlockAsync(1, 10);
// Assert
Assert.IsNull(result);
}
[TestMethod]
public async Task UpdateAudioUrlAsync_WithExistingSegment_UpdatesUrl()
{
// Arrange
var segment = StorySegment.Create(1, 1, "Content", 1, "Title", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(segment, 1);
_mockStoryRepository.Setup(s => s.GetByIdAsync(1, It.IsAny<CancellationToken>()))
.ReturnsAsync(segment);
_mockStoryRepository.Setup(s => s.UpdateAsync(It.IsAny<StorySegment>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
// Act
var result = await _service.UpdateAudioUrlAsync(1, "/audio/test.wav");
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("/audio/test.wav", result.AudioUrl);
}
[TestMethod]
public async Task UpdateAudioUrlAsync_WithNonExistingSegment_ReturnsNull()
{
// Arrange
_mockStoryRepository.Setup(s => s.GetByIdAsync(999, It.IsAny<CancellationToken>()))
.ReturnsAsync((StorySegment?)null);
// Act
var result = await _service.UpdateAudioUrlAsync(999, "/audio/test.wav");
// Assert
Assert.IsNull(result);
}
[TestMethod]
public async Task MarkSegmentAsCompletedAsync_WithExistingProgress_MarksAsCompleted()
{
// Arrange
var progress = StoryProgress.Create(1, 1, 1);
typeof(StoryProgress).GetProperty("Id")?.SetValue(progress, 1);
_mockProgressRepository.Setup(p => p.GetByUserAndSegmentAsync(1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(progress);
_mockProgressRepository.Setup(p => p.UpdateAsync(It.IsAny<StoryProgress>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
// Act
var result = await _service.MarkSegmentAsCompletedAsync(1, 1);
// Assert
Assert.IsTrue(result);
Assert.IsTrue(progress.IsCompleted);
Assert.IsNotNull(progress.CompletedAt);
}
[TestMethod]
public async Task MarkSegmentAsCompletedAsync_WithNonExistingProgress_ReturnsFalse()
{
// Arrange
_mockProgressRepository.Setup(p => p.GetByUserAndSegmentAsync(1, 999, It.IsAny<CancellationToken>()))
.ReturnsAsync((StoryProgress?)null);
// Act
var result = await _service.MarkSegmentAsCompletedAsync(1, 999);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public async Task MarkSegmentAsCompletedAsync_WithAlreadyCompleted_ReturnsFalse()
{
// Arrange
var progress = StoryProgress.Create(1, 1, 1);
typeof(StoryProgress).GetProperty("Id")?.SetValue(progress, 1);
progress.MarkAsCompleted();
_mockProgressRepository.Setup(p => p.GetByUserAndSegmentAsync(1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(progress);
// Act
var result = await _service.MarkSegmentAsCompletedAsync(1, 1);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public async Task UnlockNextSegmentAsync_WithValidLesson_UnlocksSegment()
{
// Arrange
var segment = StorySegment.Create(1, 2, "Next content", 2, "Next Title", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(segment, 2);
_mockStoryRepository.Setup(s => s.GetNextSegmentToUnlockAsync(1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(segment);
_mockProgressRepository.Setup(p => p.IsSegmentUnlockedAsync(1, 2, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
_mockProgressRepository.Setup(p => p.AddAsync(It.IsAny<StoryProgress>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((StoryProgress p, CancellationToken ct) => p);
// Act
var result = await _service.UnlockNextSegmentAsync(1, 1, 1);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(2, result.Id);
_mockProgressRepository.Verify(p => p.AddAsync(It.IsAny<StoryProgress>(), It.IsAny<CancellationToken>()), Times.Once);
}
[TestMethod]
public async Task UnlockNextSegmentAsync_WithAlreadyUnlocked_ReturnsSegmentWithoutAdding()
{
// Arrange
var segment = StorySegment.Create(1, 2, "Next content", 2, "Next Title", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(segment, 2);
_mockStoryRepository.Setup(s => s.GetNextSegmentToUnlockAsync(1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(segment);
_mockProgressRepository.Setup(p => p.IsSegmentUnlockedAsync(1, 2, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
// Act
var result = await _service.UnlockNextSegmentAsync(1, 1, 1);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(2, result.Id);
_mockProgressRepository.Verify(p => p.AddAsync(It.IsAny<StoryProgress>(), It.IsAny<CancellationToken>()), Times.Never);
}
[TestMethod]
public async Task UnlockNextSegmentAsync_WithNoSegment_ReturnsNull()
{
// Arrange
_mockStoryRepository.Setup(s => s.GetNextSegmentToUnlockAsync(1, 10, It.IsAny<CancellationToken>()))
.ReturnsAsync((StorySegment?)null);
// Act
var result = await _service.UnlockNextSegmentAsync(1, 1, 10);
// Assert
Assert.IsNull(result);
}
[TestMethod]
public async Task GetUserProgressAsync_WithSegments_ReturnsProgress()
{
// Arrange
var segment1 = StorySegment.Create(1, 1, "Content 1", 1, "Title 1", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(segment1, 1);
var segment2 = StorySegment.Create(1, 2, "Content 2", 2, "Title 2", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(segment2, 2);
var progress1 = StoryProgress.Create(1, 1, 1);
typeof(StoryProgress).GetProperty("Id")?.SetValue(progress1, 1);
progress1.MarkAsCompleted();
_mockStoryRepository.Setup(s => s.GetByLevelAsync(1, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<StorySegment> { segment1, segment2 });
_mockProgressRepository.Setup(p => p.GetByUserAndLevelAsync(1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<StoryProgress> { progress1 });
_mockProgressRepository.Setup(p => p.IsSegmentUnlockedAsync(1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_mockProgressRepository.Setup(p => p.IsSegmentUnlockedAsync(1, 2, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
_mockProgressRepository.Setup(p => p.IsSegmentCompletedAsync(1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_mockProgressRepository.Setup(p => p.IsSegmentCompletedAsync(1, 2, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
_mockProgressRepository.Setup(p => p.GetHighestUnlockedOrderAsync(1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(1);
// Act
var result = await _service.GetUserProgressAsync(1, 1);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(1, result.LevelId);
Assert.AreEqual(2, result.TotalSegments);
Assert.AreEqual(1, result.UnlockedSegments);
Assert.AreEqual(2, result.Segments.Count);
}
[TestMethod]
public async Task GetSegmentsNeedingAudioAsync_WithSegmentsNeedingAudio_ReturnsSegments()
{
// Arrange
var segment1 = StorySegment.Create(1, 1, "Content 1", 1, "Title 1", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(segment1, 1);
var segment2 = StorySegment.Create(1, 2, "Content 2", 2, "Title 2", "Theme");
typeof(StorySegment).GetProperty("Id")?.SetValue(segment2, 2);
segment2.UpdateAudioUrl("/audio/existing.wav");
_mockStoryRepository.Setup(s => s.GetSegmentsNeedingAudioAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<StorySegment> { segment1 });
// Act
var result = await _service.GetSegmentsNeedingAudioAsync();
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Count);
Assert.IsNull(result[0].AudioUrl);
}
[TestMethod]
public async Task IsSegmentUnlockedAsync_WithUnlockedSegment_ReturnsTrue()
{
// Arrange
_mockProgressRepository.Setup(p => p.IsSegmentUnlockedAsync(1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
// Act
var result = await _service.IsSegmentUnlockedAsync(1, 1);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
public async Task IsSegmentUnlockedAsync_WithLockedSegment_ReturnsFalse()
{
// Arrange
_mockProgressRepository.Setup(p => p.IsSegmentUnlockedAsync(1, 999, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
// Act
var result = await _service.IsSegmentUnlockedAsync(1, 999);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public async Task IsSegmentCompletedAsync_WithCompletedSegment_ReturnsTrue()
{
// Arrange
_mockProgressRepository.Setup(p => p.IsSegmentCompletedAsync(1, 1, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
// Act
var result = await _service.IsSegmentCompletedAsync(1, 1);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
public async Task IsSegmentCompletedAsync_WithIncompleteSegment_ReturnsFalse()
{
// Arrange
_mockProgressRepository.Setup(p => p.IsSegmentCompletedAsync(1, 999, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
// Act
var result = await _service.IsSegmentCompletedAsync(1, 999);
// Assert
Assert.IsFalse(result);
}
}