- 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>
292 lines
11 KiB
C#
292 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
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 StoryUnlockServiceTests
|
|
{
|
|
private Mock<StoryService> _mockStoryService;
|
|
private Mock<IUserProgressRepository> _mockUserProgressRepository;
|
|
private Mock<ILessonRepository> _mockLessonRepository;
|
|
private Mock<ILogger<StoryUnlockService>> _mockLogger;
|
|
private StoryUnlockService _service;
|
|
|
|
[TestInitialize]
|
|
public void Setup()
|
|
{
|
|
_mockStoryService = new Mock<StoryService>(
|
|
new Mock<IStoryRepository>().Object,
|
|
new Mock<IStoryProgressRepository>().Object,
|
|
new Mock<ILogger<StoryService>>().Object);
|
|
_mockUserProgressRepository = new Mock<IUserProgressRepository>();
|
|
_mockLessonRepository = new Mock<ILessonRepository>();
|
|
_mockLogger = new Mock<ILogger<StoryUnlockService>>();
|
|
|
|
_service = new StoryUnlockService(
|
|
_mockStoryService.Object,
|
|
_mockUserProgressRepository.Object,
|
|
_mockLessonRepository.Object,
|
|
_mockLogger.Object);
|
|
}
|
|
|
|
private Lesson CreateTestLesson(int id, int levelId, int order, string title = "Test", string topic = "Test")
|
|
{
|
|
var lesson = Lesson.Create(levelId, title, order, topic);
|
|
typeof(Lesson).GetProperty("Id")?.SetValue(lesson, id);
|
|
return lesson;
|
|
}
|
|
|
|
private StorySegment CreateTestSegment(int id, int levelId, int? lessonId, int order, string content = "Content", string title = "Title", string theme = "Theme")
|
|
{
|
|
var segment = StorySegment.Create(levelId, lessonId, content, order, title, theme);
|
|
typeof(StorySegment).GetProperty("Id")?.SetValue(segment, id);
|
|
return segment;
|
|
}
|
|
|
|
#region HandleLessonCompletionAsync Tests
|
|
|
|
[TestMethod]
|
|
public async Task HandleLessonCompletionAsync_WithValidLesson_UnlocksSegment()
|
|
{
|
|
// Arrange
|
|
var userId = 1;
|
|
var lessonId = 1;
|
|
var lesson = CreateTestLesson(lessonId, 1, 1);
|
|
var segment = CreateTestSegment(100, 1, 2, 2, "Next segment", "Next Title", "Theme");
|
|
|
|
_mockLessonRepository.Setup(l => l.GetByIdAsync(lessonId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(lesson);
|
|
|
|
_mockUserProgressRepository.Setup(u => u.HasUserCompletedLessonAsync(userId, lessonId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(true);
|
|
|
|
_mockStoryService.Setup(s => s.GetNextSegmentToUnlockAsync(lesson.LevelId, lesson.Order, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(StorySegmentDto.FromEntity(segment));
|
|
|
|
_mockStoryService.Setup(s => s.IsSegmentUnlockedAsync(userId, segment.Id, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(false);
|
|
|
|
_mockStoryService.Setup(s => s.UnlockNextSegmentAsync(userId, lesson.LevelId, lesson.Order, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(StorySegmentDto.FromEntity(segment));
|
|
|
|
// Act
|
|
var result = await _service.HandleLessonCompletionAsync(userId, lessonId);
|
|
|
|
// Assert
|
|
Assert.IsTrue(result);
|
|
_mockLessonRepository.Verify(l => l.GetByIdAsync(lessonId, It.IsAny<CancellationToken>()), Times.Once);
|
|
_mockUserProgressRepository.Verify(u => u.HasUserCompletedLessonAsync(userId, lessonId, It.IsAny<CancellationToken>()), Times.Once);
|
|
_mockStoryService.Verify(s => s.UnlockNextSegmentAsync(userId, lesson.LevelId, lesson.Order, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task HandleLessonCompletionAsync_WithNonExistingLesson_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var userId = 1;
|
|
var lessonId = 999;
|
|
|
|
_mockLessonRepository.Setup(l => l.GetByIdAsync(lessonId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((Lesson?)null);
|
|
|
|
// Act
|
|
var result = await _service.HandleLessonCompletionAsync(userId, lessonId);
|
|
|
|
// Assert
|
|
Assert.IsFalse(result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task HandleLessonCompletionAsync_WithLessonNotCompleted_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var userId = 1;
|
|
var lessonId = 1;
|
|
var lesson = CreateTestLesson(lessonId, 1, 1);
|
|
|
|
_mockLessonRepository.Setup(l => l.GetByIdAsync(lessonId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(lesson);
|
|
|
|
_mockUserProgressRepository.Setup(u => u.HasUserCompletedLessonAsync(userId, lessonId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(false);
|
|
|
|
// Act
|
|
var result = await _service.HandleLessonCompletionAsync(userId, lessonId);
|
|
|
|
// Assert
|
|
Assert.IsFalse(result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task HandleLessonCompletionAsync_WithNoSegmentToUnlock_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var userId = 1;
|
|
var lessonId = 1;
|
|
var lesson = CreateTestLesson(lessonId, 1, 10); // High order, no next segment
|
|
|
|
_mockLessonRepository.Setup(l => l.GetByIdAsync(lessonId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(lesson);
|
|
|
|
_mockUserProgressRepository.Setup(u => u.HasUserCompletedLessonAsync(userId, lessonId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(true);
|
|
|
|
_mockStoryService.Setup(s => s.GetNextSegmentToUnlockAsync(lesson.LevelId, lesson.Order, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((StorySegmentDto?)null);
|
|
|
|
// Act
|
|
var result = await _service.HandleLessonCompletionAsync(userId, lessonId);
|
|
|
|
// Assert
|
|
Assert.IsFalse(result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task HandleLessonCompletionAsync_WithSegmentAlreadyUnlocked_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var userId = 1;
|
|
var lessonId = 1;
|
|
var lesson = CreateTestLesson(lessonId, 1, 1);
|
|
var segment = CreateTestSegment(100, 1, 2, 2, "Next segment", "Next Title", "Theme");
|
|
|
|
_mockLessonRepository.Setup(l => l.GetByIdAsync(lessonId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(lesson);
|
|
|
|
_mockUserProgressRepository.Setup(u => u.HasUserCompletedLessonAsync(userId, lessonId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(true);
|
|
|
|
_mockStoryService.Setup(s => s.GetNextSegmentToUnlockAsync(lesson.LevelId, lesson.Order, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(StorySegmentDto.FromEntity(segment));
|
|
|
|
_mockStoryService.Setup(s => s.IsSegmentUnlockedAsync(userId, segment.Id, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(true); // Already unlocked
|
|
|
|
// Act
|
|
var result = await _service.HandleLessonCompletionAsync(userId, lessonId);
|
|
|
|
// Assert
|
|
Assert.IsFalse(result);
|
|
_mockStoryService.Verify(s => s.UnlockNextSegmentAsync(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task HandleLessonCompletionAsync_WithUnlockFailure_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var userId = 1;
|
|
var lessonId = 1;
|
|
var lesson = CreateTestLesson(lessonId, 1, 1);
|
|
var segment = CreateTestSegment(100, 1, 2, 2, "Next segment", "Next Title", "Theme");
|
|
|
|
_mockLessonRepository.Setup(l => l.GetByIdAsync(lessonId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(lesson);
|
|
|
|
_mockUserProgressRepository.Setup(u => u.HasUserCompletedLessonAsync(userId, lessonId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(true);
|
|
|
|
_mockStoryService.Setup(s => s.GetNextSegmentToUnlockAsync(lesson.LevelId, lesson.Order, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(StorySegmentDto.FromEntity(segment));
|
|
|
|
_mockStoryService.Setup(s => s.IsSegmentUnlockedAsync(userId, segment.Id, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(false);
|
|
|
|
_mockStoryService.Setup(s => s.UnlockNextSegmentAsync(userId, lesson.LevelId, lesson.Order, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((StorySegmentDto?)null); // Unlock failed
|
|
|
|
// Act
|
|
var result = await _service.HandleLessonCompletionAsync(userId, lessonId);
|
|
|
|
// Assert
|
|
Assert.IsFalse(result);
|
|
}
|
|
#endregion
|
|
|
|
#region Delegation Tests
|
|
|
|
[TestMethod]
|
|
public async Task IsSegmentUnlockedAsync_DelegatesToStoryService()
|
|
{
|
|
// Arrange
|
|
var userId = 1;
|
|
var segmentId = 100;
|
|
|
|
_mockStoryService.Setup(s => s.IsSegmentUnlockedAsync(userId, segmentId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(true);
|
|
|
|
// Act
|
|
var result = await _service.IsSegmentUnlockedAsync(userId, segmentId);
|
|
|
|
// Assert
|
|
Assert.IsTrue(result);
|
|
_mockStoryService.Verify(s => s.IsSegmentUnlockedAsync(userId, segmentId, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task IsSegmentCompletedAsync_DelegatesToStoryService()
|
|
{
|
|
// Arrange
|
|
var userId = 1;
|
|
var segmentId = 100;
|
|
|
|
_mockStoryService.Setup(s => s.IsSegmentCompletedAsync(userId, segmentId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(true);
|
|
|
|
// Act
|
|
var result = await _service.IsSegmentCompletedAsync(userId, segmentId);
|
|
|
|
// Assert
|
|
Assert.IsTrue(result);
|
|
_mockStoryService.Verify(s => s.IsSegmentCompletedAsync(userId, segmentId, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task MarkSegmentAsCompletedAsync_DelegatesToStoryService()
|
|
{
|
|
// Arrange
|
|
var userId = 1;
|
|
var segmentId = 100;
|
|
|
|
_mockStoryService.Setup(s => s.MarkSegmentAsCompletedAsync(userId, segmentId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(true);
|
|
|
|
// Act
|
|
var result = await _service.MarkSegmentAsCompletedAsync(userId, segmentId);
|
|
|
|
// Assert
|
|
Assert.IsTrue(result);
|
|
_mockStoryService.Verify(s => s.MarkSegmentAsCompletedAsync(userId, segmentId, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GetUserProgressAsync_DelegatesToStoryService()
|
|
{
|
|
// Arrange
|
|
var userId = 1;
|
|
var levelId = 1;
|
|
var expectedProgress = new StoryProgressDto(1, "A1", 5, 3, 2, new List<StorySegmentProgressDto>());
|
|
|
|
_mockStoryService.Setup(s => s.GetUserProgressAsync(userId, levelId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(expectedProgress);
|
|
|
|
// Act
|
|
var result = await _service.GetUserProgressAsync(userId, levelId);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(levelId, result.LevelId);
|
|
Assert.AreEqual("A1", result.LevelName);
|
|
_mockStoryService.Verify(s => s.GetUserProgressAsync(userId, levelId, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
#endregion
|
|
}
|