All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Added Role to AuthResponse DTO and all auth endpoints - Fixed null config handling in MistralConnector, TtsService, VoskService, MistralService - Fixed BaseAddress setup in MistralConnector to work without API key - Reverted seed data to use hardcoded bcrypt hashes (compatible with PasswordHasher) - Added integration tests for StoryController - Added unit tests for MistralConnector - Updated frontend AuthResponse type to include role Fixes admin redirect to /, story generation null reference, and Docker build failures. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
205 lines
7.2 KiB
C#
205 lines
7.2 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 GermanApp.Presentation.Controllers;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
using Moq;
|
|
|
|
namespace GermanApp.Tests.Integration.Presentation.Controllers;
|
|
|
|
/// <summary>
|
|
/// Integration tests for StoryController.
|
|
/// Tests the full controller -> service -> repository flow.
|
|
/// </summary>
|
|
[TestClass]
|
|
public class StoryControllerTests
|
|
{
|
|
private Mock<ILessonRepository> _lessonRepositoryMock;
|
|
private Mock<IStoryRepository> _storyRepositoryMock;
|
|
private Mock<StoryGenerationService> _generationServiceMock;
|
|
private Mock<StoryService> _storyServiceMock;
|
|
private Mock<StoryUnlockService> _unlockServiceMock;
|
|
private Mock<ILevelRepository> _levelRepositoryMock;
|
|
private Mock<ILogger<StoryController>> _loggerMock;
|
|
private StoryController _controller;
|
|
|
|
[TestInitialize]
|
|
public void TestInitialize()
|
|
{
|
|
_lessonRepositoryMock = new Mock<ILessonRepository>();
|
|
_storyRepositoryMock = new Mock<IStoryRepository>();
|
|
_generationServiceMock = new Mock<StoryGenerationService>();
|
|
_storyServiceMock = new Mock<StoryService>();
|
|
_unlockServiceMock = new Mock<StoryUnlockService>();
|
|
_levelRepositoryMock = new Mock<ILevelRepository>();
|
|
_loggerMock = new Mock<ILogger<StoryController>>();
|
|
|
|
_controller = new StoryController(
|
|
_storyServiceMock.Object,
|
|
_generationServiceMock.Object,
|
|
_unlockServiceMock.Object,
|
|
_levelRepositoryMock.Object,
|
|
_lessonRepositoryMock.Object,
|
|
_loggerMock.Object);
|
|
}
|
|
|
|
[TestCleanup]
|
|
public void TestCleanup()
|
|
{
|
|
_controller?.Dispose();
|
|
}
|
|
|
|
// ============================================
|
|
// Story Generation Tests
|
|
// ============================================
|
|
|
|
[TestMethod]
|
|
public async Task GenerateStoryAsync_ReturnsSuccess_WhenLessonsExistAndGenerationSucceeds()
|
|
{
|
|
// Arrange
|
|
int levelId = 1;
|
|
string theme = "Adventure";
|
|
var request = new StoryGenerationRequestDto(theme);
|
|
|
|
var lessons = new List<Lesson>
|
|
{
|
|
Lesson.Create(1, "Greetings", 1, "Greetings", "Basic greetings"),
|
|
Lesson.Create(1, "Numbers", 2, "Numbers", "German numbers 1-100")
|
|
};
|
|
|
|
var expectedResponse = new StoryGenerationResponseDto(
|
|
levelId,
|
|
theme,
|
|
2,
|
|
"Full story text here",
|
|
new List<StorySegmentDto>());
|
|
|
|
_lessonRepositoryMock.Setup(r => r.GetByLevelAsync(levelId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(lessons);
|
|
|
|
_generationServiceMock.Setup(s => s.GenerateStoryAsync(
|
|
levelId,
|
|
theme,
|
|
lessons,
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(expectedResponse);
|
|
|
|
// Act
|
|
var result = await _controller.GenerateStoryAsync(levelId, request, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.IsInstanceOfType(result.Result, typeof(OkObjectResult));
|
|
var okResult = result.Result as OkObjectResult;
|
|
Assert.IsNotNull(okResult);
|
|
Assert.IsInstanceOfType(okResult.Value, typeof(StoryGenerationResponseDto));
|
|
var response = okResult.Value as StoryGenerationResponseDto;
|
|
|
|
Assert.AreEqual(levelId, response.LevelId);
|
|
Assert.AreEqual(theme, response.Theme);
|
|
Assert.AreEqual(2, response.SegmentCount);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateStoryAsync_ReturnsBadRequest_WhenNoLessonsFound()
|
|
{
|
|
// Arrange
|
|
int levelId = 999; // Non-existent level
|
|
string theme = "Adventure";
|
|
var request = new StoryGenerationRequestDto(theme);
|
|
|
|
_lessonRepositoryMock.Setup(r => r.GetByLevelAsync(levelId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<Lesson>()); // Empty list
|
|
|
|
// Act
|
|
var result = await _controller.GenerateStoryAsync(levelId, request, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.IsInstanceOfType(result.Result, typeof(BadRequestObjectResult));
|
|
}
|
|
|
|
// ============================================
|
|
// Story Segment Audio Generation Tests
|
|
// ============================================
|
|
|
|
[TestMethod]
|
|
public async Task GenerateSegmentAudioAsync_ReturnsSuccess_WhenSegmentExists()
|
|
{
|
|
// Arrange
|
|
int segmentId = 1;
|
|
var expectedDto = new StorySegmentDto
|
|
{
|
|
Id = segmentId,
|
|
AudioUrl = "/audio/story/level1-segment1.wav"
|
|
};
|
|
|
|
_storyServiceMock.Setup(s => s.GenerateAudioAsync(segmentId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(expectedDto);
|
|
|
|
// Act
|
|
var result = await _controller.GenerateSegmentAudioAsync(segmentId, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.IsInstanceOfType(result.Result, typeof(OkObjectResult));
|
|
var okResult = result.Result as OkObjectResult;
|
|
Assert.IsNotNull(okResult);
|
|
Assert.IsInstanceOfType(okResult.Value, typeof(StorySegmentDto));
|
|
var response = okResult.Value as StorySegmentDto;
|
|
|
|
Assert.AreEqual(segmentId, response.Id);
|
|
Assert.AreEqual(expectedDto.AudioUrl, response.AudioUrl);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateSegmentAudioAsync_ReturnsNotFound_WhenSegmentDoesNotExist()
|
|
{
|
|
// Arrange
|
|
int segmentId = 999; // Non-existent segment
|
|
|
|
_storyServiceMock.Setup(s => s.GenerateAudioAsync(segmentId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((StorySegmentDto?)null);
|
|
|
|
// Act
|
|
var result = await _controller.GenerateSegmentAudioAsync(segmentId, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.IsInstanceOfType(result.Result, typeof(NotFoundResult));
|
|
}
|
|
|
|
// ============================================
|
|
// Get Stories by Level Tests
|
|
// ============================================
|
|
|
|
[TestMethod]
|
|
public async Task GetByLevelAsync_ReturnsSuccess_WhenSegmentsExist()
|
|
{
|
|
// Arrange
|
|
int levelId = 1;
|
|
var expectedSegments = new List<StorySegmentDto>
|
|
{
|
|
new StorySegmentDto { Id = 1, LevelId = levelId, Order = 1, Title = "Part 1" },
|
|
new StorySegmentDto { Id = 2, LevelId = levelId, Order = 2, Title = "Part 2" }
|
|
};
|
|
|
|
_storyServiceMock.Setup(s => s.GetByLevelAsync(levelId, false, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(expectedSegments);
|
|
|
|
// Act
|
|
var result = await _controller.GetByLevelAsync(levelId, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.IsInstanceOfType(result.Result, typeof(OkObjectResult));
|
|
var okResult = result.Result as OkObjectResult;
|
|
Assert.IsNotNull(okResult);
|
|
Assert.IsInstanceOfType(okResult.Value, typeof(List<StorySegmentDto>));
|
|
var response = okResult.Value as List<StorySegmentDto>;
|
|
|
|
Assert.AreEqual(2, response.Count);
|
|
}
|
|
}
|