diff --git a/GermanApp/Application/Services/LessonService.cs b/GermanApp/Application/Services/LessonService.cs
index 1755620..5289061 100644
--- a/GermanApp/Application/Services/LessonService.cs
+++ b/GermanApp/Application/Services/LessonService.cs
@@ -22,7 +22,7 @@ public class LessonService
///
/// Gets all lessons.
///
- public async Task> GetAllLessonsAsync(CancellationToken cancellationToken = default)
+ public virtual async Task> GetAllLessonsAsync(CancellationToken cancellationToken = default)
{
var lessons = await _lessonRepository.GetAllAsync(cancellationToken);
return lessons.Select(l => l.ToDto()).ToList();
@@ -31,7 +31,7 @@ public class LessonService
///
/// Gets all lessons ordered by level and lesson order.
///
- public async Task> GetAllOrderedLessonsAsync(CancellationToken cancellationToken = default)
+ public virtual async Task> GetAllOrderedLessonsAsync(CancellationToken cancellationToken = default)
{
var lessons = await _lessonRepository.GetAllOrderedAsync(cancellationToken);
return lessons.Select(l => l.ToDto()).ToList();
@@ -40,7 +40,7 @@ public class LessonService
///
/// Gets a lesson by its ID.
///
- public async Task GetLessonByIdAsync(int id, CancellationToken cancellationToken = default)
+ public virtual async Task GetLessonByIdAsync(int id, CancellationToken cancellationToken = default)
{
var lesson = await _lessonRepository.GetByIdAsync(id, cancellationToken);
return lesson?.ToDto();
@@ -49,7 +49,7 @@ public class LessonService
///
/// Gets lessons by level ID.
///
- public async Task> GetLessonsByLevelAsync(int levelId, CancellationToken cancellationToken = default)
+ public virtual async Task> GetLessonsByLevelAsync(int levelId, CancellationToken cancellationToken = default)
{
var lessons = await _lessonRepository.GetByLevelAsync(levelId, cancellationToken);
return lessons.Select(l => l.ToDto()).ToList();
@@ -58,7 +58,7 @@ public class LessonService
///
/// Gets beginner lessons (A1 and A2 - levels 1 and 2).
///
- public async Task> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default)
+ public virtual async Task> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default)
{
var level1Lessons = await _lessonRepository.GetByLevelAsync(1, cancellationToken);
var level2Lessons = await _lessonRepository.GetByLevelAsync(2, cancellationToken);
@@ -72,7 +72,7 @@ public class LessonService
///
/// Gets advanced lessons (B2 and C1 - levels 4 and 5).
///
- public async Task> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default)
+ public virtual async Task> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default)
{
var level4Lessons = await _lessonRepository.GetByLevelAsync(4, cancellationToken);
var level5Lessons = await _lessonRepository.GetByLevelAsync(5, cancellationToken);
@@ -86,7 +86,7 @@ public class LessonService
///
/// Creates a new lesson.
///
- public async Task CreateLessonAsync(CreateLessonDto dto, CancellationToken cancellationToken = default)
+ public virtual async Task CreateLessonAsync(CreateLessonDto dto, CancellationToken cancellationToken = default)
{
// Validate that the level exists
var level = await _levelRepository.GetByIdAsync(dto.LevelId, cancellationToken);
@@ -101,7 +101,7 @@ public class LessonService
///
/// Updates an existing lesson.
///
- public async Task UpdateLessonAsync(int id, UpdateLessonDto dto, CancellationToken cancellationToken = default)
+ public virtual async Task UpdateLessonAsync(int id, UpdateLessonDto dto, CancellationToken cancellationToken = default)
{
var lesson = await _lessonRepository.GetByIdAsync(id, cancellationToken);
if (lesson == null)
@@ -120,7 +120,7 @@ public class LessonService
///
/// Deletes a lesson by its ID.
///
- public async Task DeleteLessonAsync(int id, CancellationToken cancellationToken = default)
+ public virtual async Task DeleteLessonAsync(int id, CancellationToken cancellationToken = default)
{
var lesson = await _lessonRepository.GetByIdAsync(id, cancellationToken);
if (lesson == null)
@@ -133,7 +133,7 @@ public class LessonService
///
/// Gets the first lesson in a level.
///
- public async Task GetFirstLessonInLevelAsync(int levelId, CancellationToken cancellationToken = default)
+ public virtual async Task GetFirstLessonInLevelAsync(int levelId, CancellationToken cancellationToken = default)
{
var lesson = await _lessonRepository.GetFirstLessonInLevelAsync(levelId, cancellationToken);
return lesson?.ToDto();
@@ -142,7 +142,7 @@ public class LessonService
///
/// Gets the next lesson after the specified one.
///
- public async Task GetNextLessonAsync(int currentLessonId, CancellationToken cancellationToken = default)
+ public virtual async Task GetNextLessonAsync(int currentLessonId, CancellationToken cancellationToken = default)
{
var lesson = await _lessonRepository.GetNextLessonAsync(currentLessonId, cancellationToken);
return lesson?.ToDto();
@@ -151,7 +151,7 @@ public class LessonService
///
/// Gets lessons that the user can access (based on completion of previous lessons).
///
- public async Task> GetAccessibleLessonsAsync(int userId, CancellationToken cancellationToken = default)
+ public virtual async Task> GetAccessibleLessonsAsync(int userId, CancellationToken cancellationToken = default)
{
var lessons = await _lessonRepository.GetAccessibleLessonsAsync(userId, cancellationToken);
return lessons.Select(l => l.ToDto()).ToList();
diff --git a/GermanApp/Application/Services/LevelService.cs b/GermanApp/Application/Services/LevelService.cs
index b206aa3..87a3034 100644
--- a/GermanApp/Application/Services/LevelService.cs
+++ b/GermanApp/Application/Services/LevelService.cs
@@ -20,7 +20,7 @@ public class LevelService
///
/// Gets all CEFR levels ordered by their sort order.
///
- public async Task> GetAllLevelsAsync(CancellationToken cancellationToken = default)
+ public virtual async Task> GetAllLevelsAsync(CancellationToken cancellationToken = default)
{
var levels = await _levelRepository.GetAllOrderedAsync(cancellationToken);
return levels.Select(l => l.ToDto()).ToList();
@@ -29,7 +29,7 @@ public class LevelService
///
/// Gets a level by its ID.
///
- public async Task GetLevelByIdAsync(int id, CancellationToken cancellationToken = default)
+ public virtual async Task GetLevelByIdAsync(int id, CancellationToken cancellationToken = default)
{
var level = await _levelRepository.GetByIdAsync(id, cancellationToken);
return level?.ToDto();
@@ -38,7 +38,7 @@ public class LevelService
///
/// Gets a level by its code (e.g., "A1", "B2").
///
- public async Task GetLevelByCodeAsync(string code, CancellationToken cancellationToken = default)
+ public virtual async Task GetLevelByCodeAsync(string code, CancellationToken cancellationToken = default)
{
var level = await _levelRepository.GetByCodeAsync(code, cancellationToken);
return level?.ToDto();
@@ -47,7 +47,7 @@ public class LevelService
///
/// Creates a new CEFR level.
///
- public async Task CreateLevelAsync(CreateLevelDto dto, CancellationToken cancellationToken = default)
+ public virtual async Task CreateLevelAsync(CreateLevelDto dto, CancellationToken cancellationToken = default)
{
var level = dto.ToEntity();
var createdLevel = await _levelRepository.AddAsync(level, cancellationToken);
@@ -57,7 +57,7 @@ public class LevelService
///
/// Updates an existing CEFR level.
///
- public async Task UpdateLevelAsync(int id, UpdateLevelDto dto, CancellationToken cancellationToken = default)
+ public virtual async Task UpdateLevelAsync(int id, UpdateLevelDto dto, CancellationToken cancellationToken = default)
{
var level = await _levelRepository.GetByIdAsync(id, cancellationToken);
if (level == null)
@@ -71,7 +71,7 @@ public class LevelService
///
/// Deletes a CEFR level by its ID.
///
- public async Task DeleteLevelAsync(int id, CancellationToken cancellationToken = default)
+ public virtual async Task DeleteLevelAsync(int id, CancellationToken cancellationToken = default)
{
var level = await _levelRepository.GetByIdAsync(id, cancellationToken);
if (level == null)
@@ -84,7 +84,7 @@ public class LevelService
///
/// Gets the first level (lowest order number) - typically A1.
///
- public async Task GetFirstLevelAsync(CancellationToken cancellationToken = default)
+ public virtual async Task GetFirstLevelAsync(CancellationToken cancellationToken = default)
{
var level = await _levelRepository.GetFirstLevelAsync(cancellationToken);
return level?.ToDto();
@@ -93,7 +93,7 @@ public class LevelService
///
/// Gets the next level after the specified one.
///
- public async Task GetNextLevelAsync(int currentLevelId, CancellationToken cancellationToken = default)
+ public virtual async Task GetNextLevelAsync(int currentLevelId, CancellationToken cancellationToken = default)
{
var level = await _levelRepository.GetNextLevelAsync(currentLevelId, cancellationToken);
return level?.ToDto();
diff --git a/GermanApp/Application/Services/ProgressService.cs b/GermanApp/Application/Services/ProgressService.cs
index b2bb4a0..6247d93 100644
--- a/GermanApp/Application/Services/ProgressService.cs
+++ b/GermanApp/Application/Services/ProgressService.cs
@@ -27,7 +27,7 @@ public class ProgressService
///
/// Gets user progress for a specific lesson.
///
- public async Task GetUserProgressAsync(int userId, int lessonId, CancellationToken cancellationToken = default)
+ public virtual async Task GetUserProgressAsync(int userId, int lessonId, CancellationToken cancellationToken = default)
{
var progress = await _userProgressRepository.GetByUserAndLessonAsync(userId, lessonId, cancellationToken);
return progress?.ToDto();
@@ -36,7 +36,7 @@ public class ProgressService
///
/// Gets all progress records for a user.
///
- public async Task> GetUserProgressAsync(int userId, CancellationToken cancellationToken = default)
+ public virtual async Task> GetUserProgressAsync(int userId, CancellationToken cancellationToken = default)
{
var progressRecords = await _userProgressRepository.GetByUserAsync(userId, cancellationToken);
return progressRecords.Select(p => p.ToDto()).ToList();
@@ -45,7 +45,7 @@ public class ProgressService
///
/// Gets progress for all lessons in a specific level for a user.
///
- public async Task> GetUserProgressByLevelAsync(int userId, int levelId, CancellationToken cancellationToken = default)
+ public virtual async Task> GetUserProgressByLevelAsync(int userId, int levelId, CancellationToken cancellationToken = default)
{
var progressRecords = await _userProgressRepository.GetByUserAndLevelAsync(userId, levelId, cancellationToken);
return progressRecords.Select(p => p.ToDto()).ToList();
@@ -54,7 +54,7 @@ public class ProgressService
///
/// Checks if a user has completed a lesson.
///
- public async Task HasUserCompletedLessonAsync(int userId, int lessonId, CancellationToken cancellationToken = default)
+ public virtual async Task HasUserCompletedLessonAsync(int userId, int lessonId, CancellationToken cancellationToken = default)
{
return await _userProgressRepository.HasUserCompletedLessonAsync(userId, lessonId, cancellationToken);
}
@@ -62,7 +62,7 @@ public class ProgressService
///
/// Marks a lesson as completed for a user with a quiz score.
///
- public async Task MarkLessonAsCompletedAsync(int userId, int lessonId, int quizScore, CancellationToken cancellationToken = default)
+ public virtual async Task MarkLessonAsCompletedAsync(int userId, int lessonId, int quizScore, CancellationToken cancellationToken = default)
{
var progress = await _userProgressRepository.GetByUserAndLessonAsync(userId, lessonId, cancellationToken);
@@ -86,7 +86,7 @@ public class ProgressService
///
/// Updates user progress for a lesson without marking as completed.
///
- public async Task UpdateUserProgressAsync(int userId, UpdateUserProgressDto dto, CancellationToken cancellationToken = default)
+ public virtual async Task UpdateUserProgressAsync(int userId, UpdateUserProgressDto dto, CancellationToken cancellationToken = default)
{
var progress = await _userProgressRepository.GetByUserAndLessonAsync(userId, dto.LessonId, cancellationToken);
@@ -109,7 +109,7 @@ public class ProgressService
///
/// Resets user progress for a lesson.
///
- public async Task ResetLessonProgressAsync(int userId, int lessonId, CancellationToken cancellationToken = default)
+ public virtual async Task ResetLessonProgressAsync(int userId, int lessonId, CancellationToken cancellationToken = default)
{
var progress = await _userProgressRepository.GetByUserAndLessonAsync(userId, lessonId, cancellationToken);
if (progress == null)
@@ -123,7 +123,7 @@ public class ProgressService
///
/// Gets the user's average score for a level.
///
- public async Task GetAverageScoreForLevelAsync(int userId, int levelId, CancellationToken cancellationToken = default)
+ public virtual async Task GetAverageScoreForLevelAsync(int userId, int levelId, CancellationToken cancellationToken = default)
{
return await _userProgressRepository.GetAverageScoreForLevelAsync(userId, levelId, cancellationToken);
}
@@ -131,7 +131,7 @@ public class ProgressService
///
/// Gets the percentage of lessons completed in a level.
///
- public async Task GetLevelCompletionPercentageAsync(int userId, int levelId, CancellationToken cancellationToken = default)
+ public virtual async Task GetLevelCompletionPercentageAsync(int userId, int levelId, CancellationToken cancellationToken = default)
{
return await _userProgressRepository.GetLevelCompletionPercentageAsync(userId, levelId, cancellationToken);
}
@@ -139,7 +139,7 @@ public class ProgressService
///
/// Gets level completion summaries for a user.
///
- public async Task> GetLevelCompletionsAsync(int userId, CancellationToken cancellationToken = default)
+ public virtual async Task> GetLevelCompletionsAsync(int userId, CancellationToken cancellationToken = default)
{
var levels = await _levelRepository.GetAllOrderedAsync(cancellationToken);
var completions = new List();
@@ -176,7 +176,7 @@ public class ProgressService
///
/// Gets lessons that the user can access (completed previous lessons or first in level).
///
- public async Task> GetAccessibleLessonsAsync(int userId, CancellationToken cancellationToken = default)
+ public virtual async Task> GetAccessibleLessonsAsync(int userId, CancellationToken cancellationToken = default)
{
var lessons = await _lessonRepository.GetAccessibleLessonsAsync(userId, cancellationToken);
return lessons.Select(l => l.ToDto()).ToList();
@@ -185,7 +185,7 @@ public class ProgressService
///
/// Checks if the next lesson in a level is unlocked for the user.
///
- public async Task IsNextLessonUnlockedAsync(int userId, int currentLessonId, CancellationToken cancellationToken = default)
+ public virtual async Task IsNextLessonUnlockedAsync(int userId, int currentLessonId, CancellationToken cancellationToken = default)
{
var currentLesson = await _lessonRepository.GetByIdAsync(currentLessonId, cancellationToken);
if (currentLesson == null)
diff --git a/GermanApp/GermanApp.csproj b/GermanApp/GermanApp.csproj
index 4334781..5da0698 100644
--- a/GermanApp/GermanApp.csproj
+++ b/GermanApp/GermanApp.csproj
@@ -13,6 +13,7 @@
+
diff --git a/GermanApp/Presentation/Controllers/LessonsController.cs b/GermanApp/Presentation/Controllers/LessonsController.cs
index 3648942..f271209 100644
--- a/GermanApp/Presentation/Controllers/LessonsController.cs
+++ b/GermanApp/Presentation/Controllers/LessonsController.cs
@@ -29,7 +29,7 @@ public class LessonsController : ControllerBase
/// List of all lessons
[HttpGet]
[AllowAnonymous]
- public async Task>> GetAllLessonsAsync(CancellationToken cancellationToken = default)
+ public async Task GetAllLessonsAsync(CancellationToken cancellationToken = default)
{
var lessons = await _lessonService.GetAllLessonsAsync(cancellationToken);
return Ok(lessons);
@@ -41,7 +41,7 @@ public class LessonsController : ControllerBase
/// List of all lessons in order
[HttpGet("ordered")]
[AllowAnonymous]
- public async Task>> GetAllOrderedLessonsAsync(CancellationToken cancellationToken = default)
+ public async Task GetAllOrderedLessonsAsync(CancellationToken cancellationToken = default)
{
var lessons = await _lessonService.GetAllOrderedLessonsAsync(cancellationToken);
return Ok(lessons);
@@ -54,7 +54,7 @@ public class LessonsController : ControllerBase
/// The lesson with the specified ID
[HttpGet("{id}")]
[AllowAnonymous]
- public async Task> GetLessonByIdAsync(int id, CancellationToken cancellationToken = default)
+ public async Task GetLessonByIdAsync(int id, CancellationToken cancellationToken = default)
{
var lesson = await _lessonService.GetLessonByIdAsync(id, cancellationToken);
if (lesson == null)
@@ -69,7 +69,7 @@ public class LessonsController : ControllerBase
/// List of lessons for the specified level
[HttpGet("by-level/{levelId}")]
[AllowAnonymous]
- public async Task>> GetLessonsByLevelAsync(int levelId, CancellationToken cancellationToken = default)
+ public async Task GetLessonsByLevelAsync(int levelId, CancellationToken cancellationToken = default)
{
var lessons = await _lessonService.GetLessonsByLevelAsync(levelId, cancellationToken);
return Ok(lessons);
@@ -81,7 +81,7 @@ public class LessonsController : ControllerBase
/// List of beginner lessons
[HttpGet("beginner")]
[AllowAnonymous]
- public async Task>> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default)
+ public async Task GetBeginnerLessonsAsync(CancellationToken cancellationToken = default)
{
var lessons = await _lessonService.GetBeginnerLessonsAsync(cancellationToken);
return Ok(lessons);
@@ -93,7 +93,7 @@ public class LessonsController : ControllerBase
/// List of advanced lessons
[HttpGet("advanced")]
[AllowAnonymous]
- public async Task>> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default)
+ public async Task GetAdvancedLessonsAsync(CancellationToken cancellationToken = default)
{
var lessons = await _lessonService.GetAdvancedLessonsAsync(cancellationToken);
return Ok(lessons);
@@ -106,7 +106,7 @@ public class LessonsController : ControllerBase
/// The created lesson
[HttpPost]
[Authorize(Roles = "Admin")]
- public async Task> CreateLessonAsync(CreateLessonDto dto, CancellationToken cancellationToken = default)
+ public async Task CreateLessonAsync(CreateLessonDto dto, CancellationToken cancellationToken = default)
{
var lesson = await _lessonService.CreateLessonAsync(dto, cancellationToken);
return CreatedAtAction(nameof(GetLessonByIdAsync), new { id = lesson.Id }, lesson);
@@ -120,7 +120,7 @@ public class LessonsController : ControllerBase
/// The updated lesson
[HttpPut("{id}")]
[Authorize(Roles = "Admin")]
- public async Task> UpdateLessonAsync(int id, UpdateLessonDto dto, CancellationToken cancellationToken = default)
+ public async Task UpdateLessonAsync(int id, UpdateLessonDto dto, CancellationToken cancellationToken = default)
{
var lesson = await _lessonService.UpdateLessonAsync(id, dto, cancellationToken);
if (lesson == null)
@@ -135,7 +135,7 @@ public class LessonsController : ControllerBase
/// No content on success
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")]
- public async Task DeleteLessonAsync(int id, CancellationToken cancellationToken = default)
+ public async Task DeleteLessonAsync(int id, CancellationToken cancellationToken = default)
{
var result = await _lessonService.DeleteLessonAsync(id, cancellationToken);
if (!result)
@@ -150,7 +150,7 @@ public class LessonsController : ControllerBase
/// The first lesson in the level
[HttpGet("first/{levelId}")]
[AllowAnonymous]
- public async Task> GetFirstLessonInLevelAsync(int levelId, CancellationToken cancellationToken = default)
+ public async Task GetFirstLessonInLevelAsync(int levelId, CancellationToken cancellationToken = default)
{
var lesson = await _lessonService.GetFirstLessonInLevelAsync(levelId, cancellationToken);
if (lesson == null)
@@ -165,7 +165,7 @@ public class LessonsController : ControllerBase
/// The next lesson
[HttpGet("next/{currentLessonId}")]
[AllowAnonymous]
- public async Task> GetNextLessonAsync(int currentLessonId, CancellationToken cancellationToken = default)
+ public async Task GetNextLessonAsync(int currentLessonId, CancellationToken cancellationToken = default)
{
var lesson = await _lessonService.GetNextLessonAsync(currentLessonId, cancellationToken);
if (lesson == null)
@@ -180,7 +180,7 @@ public class LessonsController : ControllerBase
/// List of accessible lessons for the user
[HttpGet("accessible/{userId}")]
[Authorize]
- public async Task>> GetAccessibleLessonsAsync(int userId, CancellationToken cancellationToken = default)
+ public async Task GetAccessibleLessonsAsync(int userId, CancellationToken cancellationToken = default)
{
var lessons = await _lessonService.GetAccessibleLessonsAsync(userId, cancellationToken);
return Ok(lessons);
@@ -194,7 +194,7 @@ public class LessonsController : ControllerBase
/// True if the next lesson is unlocked
[HttpGet("unlocked/{userId}/{currentLessonId}")]
[Authorize]
- public async Task> IsNextLessonUnlockedAsync(int userId, int currentLessonId, CancellationToken cancellationToken = default)
+ public async Task IsNextLessonUnlockedAsync(int userId, int currentLessonId, CancellationToken cancellationToken = default)
{
var isUnlocked = await _progressService.IsNextLessonUnlockedAsync(userId, currentLessonId, cancellationToken);
return Ok(isUnlocked);
diff --git a/GermanApp/Presentation/Controllers/LevelsController.cs b/GermanApp/Presentation/Controllers/LevelsController.cs
index 7bc034b..e59afcd 100644
--- a/GermanApp/Presentation/Controllers/LevelsController.cs
+++ b/GermanApp/Presentation/Controllers/LevelsController.cs
@@ -27,7 +27,7 @@ public class LevelsController : ControllerBase
/// List of all levels
[HttpGet]
[AllowAnonymous]
- public async Task>> GetAllLevelsAsync(CancellationToken cancellationToken = default)
+ public async Task GetAllLevelsAsync(CancellationToken cancellationToken = default)
{
var levels = await _levelService.GetAllLevelsAsync(cancellationToken);
return Ok(levels);
@@ -40,7 +40,7 @@ public class LevelsController : ControllerBase
/// The level with the specified ID
[HttpGet("{id}")]
[AllowAnonymous]
- public async Task> GetLevelByIdAsync(int id, CancellationToken cancellationToken = default)
+ public async Task GetLevelByIdAsync(int id, CancellationToken cancellationToken = default)
{
var level = await _levelService.GetLevelByIdAsync(id, cancellationToken);
if (level == null)
@@ -55,7 +55,7 @@ public class LevelsController : ControllerBase
/// The level with the specified code
[HttpGet("by-code/{code}")]
[AllowAnonymous]
- public async Task> GetLevelByCodeAsync(string code, CancellationToken cancellationToken = default)
+ public async Task GetLevelByCodeAsync(string code, CancellationToken cancellationToken = default)
{
var level = await _levelService.GetLevelByCodeAsync(code, cancellationToken);
if (level == null)
@@ -70,7 +70,7 @@ public class LevelsController : ControllerBase
/// The created level
[HttpPost]
[Authorize(Roles = "Admin")]
- public async Task> CreateLevelAsync(CreateLevelDto dto, CancellationToken cancellationToken = default)
+ public async Task CreateLevelAsync(CreateLevelDto dto, CancellationToken cancellationToken = default)
{
var level = await _levelService.CreateLevelAsync(dto, cancellationToken);
return CreatedAtAction(nameof(GetLevelByIdAsync), new { id = level.Id }, level);
@@ -84,7 +84,7 @@ public class LevelsController : ControllerBase
/// The updated level
[HttpPut("{id}")]
[Authorize(Roles = "Admin")]
- public async Task> UpdateLevelAsync(int id, UpdateLevelDto dto, CancellationToken cancellationToken = default)
+ public async Task UpdateLevelAsync(int id, UpdateLevelDto dto, CancellationToken cancellationToken = default)
{
var level = await _levelService.UpdateLevelAsync(id, dto, cancellationToken);
if (level == null)
@@ -99,7 +99,7 @@ public class LevelsController : ControllerBase
/// No content on success
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")]
- public async Task DeleteLevelAsync(int id, CancellationToken cancellationToken = default)
+ public async Task DeleteLevelAsync(int id, CancellationToken cancellationToken = default)
{
var result = await _levelService.DeleteLevelAsync(id, cancellationToken);
if (!result)
@@ -113,7 +113,7 @@ public class LevelsController : ControllerBase
/// The first level
[HttpGet("first")]
[AllowAnonymous]
- public async Task> GetFirstLevelAsync(CancellationToken cancellationToken = default)
+ public async Task GetFirstLevelAsync(CancellationToken cancellationToken = default)
{
var level = await _levelService.GetFirstLevelAsync(cancellationToken);
if (level == null)
@@ -128,7 +128,7 @@ public class LevelsController : ControllerBase
/// The next level
[HttpGet("next/{currentLevelId}")]
[AllowAnonymous]
- public async Task> GetNextLevelAsync(int currentLevelId, CancellationToken cancellationToken = default)
+ public async Task GetNextLevelAsync(int currentLevelId, CancellationToken cancellationToken = default)
{
var level = await _levelService.GetNextLevelAsync(currentLevelId, cancellationToken);
if (level == null)
diff --git a/GermanApp/Presentation/Validators/LessonValidators.cs b/GermanApp/Presentation/Validators/LessonValidators.cs
new file mode 100644
index 0000000..cc5dfc8
--- /dev/null
+++ b/GermanApp/Presentation/Validators/LessonValidators.cs
@@ -0,0 +1,64 @@
+using FluentValidation;
+using GermanApp.Application.DTOs;
+
+namespace GermanApp.Presentation.Validators;
+
+///
+/// Validator for CreateLessonDto.
+/// Validates input when creating a new lesson.
+///
+public class CreateLessonValidator : AbstractValidator
+{
+ public CreateLessonValidator()
+ {
+ RuleFor(x => x.Title)
+ .NotEmpty().WithMessage("Lesson title is required.")
+ .MinimumLength(3).WithMessage("Lesson title must be at least 3 characters long.")
+ .MaximumLength(200).WithMessage("Lesson title must not exceed 200 characters.");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(2000).WithMessage("Lesson description must not exceed 2000 characters.");
+
+ RuleFor(x => x.LevelId)
+ .GreaterThan(0).WithMessage("Level ID must be greater than 0.");
+
+ RuleFor(x => x.Order)
+ .GreaterThanOrEqualTo(0).WithMessage("Order must be at least 0.")
+ .LessThanOrEqualTo(1000).WithMessage("Order must not exceed 1000.");
+
+ RuleFor(x => x.Topic)
+ .NotEmpty().WithMessage("Topic is required.")
+ .MinimumLength(2).WithMessage("Topic must be at least 2 characters long.")
+ .MaximumLength(100).WithMessage("Topic must not exceed 100 characters.");
+ }
+}
+
+///
+/// Validator for UpdateLessonDto.
+/// Validates input when updating an existing lesson.
+///
+public class UpdateLessonValidator : AbstractValidator
+{
+ public UpdateLessonValidator()
+ {
+ RuleFor(x => x.Title)
+ .NotEmpty().WithMessage("Lesson title is required.")
+ .MinimumLength(3).WithMessage("Lesson title must be at least 3 characters long.")
+ .MaximumLength(200).WithMessage("Lesson title must not exceed 200 characters.");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(2000).WithMessage("Lesson description must not exceed 2000 characters.");
+
+ RuleFor(x => x.LevelId)
+ .GreaterThan(0).WithMessage("Level ID must be greater than 0.");
+
+ RuleFor(x => x.Order)
+ .GreaterThanOrEqualTo(0).WithMessage("Order must be at least 0.")
+ .LessThanOrEqualTo(1000).WithMessage("Order must not exceed 1000.");
+
+ RuleFor(x => x.Topic)
+ .NotEmpty().WithMessage("Topic is required.")
+ .MinimumLength(2).WithMessage("Topic must be at least 2 characters long.")
+ .MaximumLength(100).WithMessage("Topic must not exceed 100 characters.");
+ }
+}
diff --git a/GermanApp/Presentation/Validators/LevelValidators.cs b/GermanApp/Presentation/Validators/LevelValidators.cs
new file mode 100644
index 0000000..895d786
--- /dev/null
+++ b/GermanApp/Presentation/Validators/LevelValidators.cs
@@ -0,0 +1,78 @@
+using FluentValidation;
+using GermanApp.Application.DTOs;
+
+namespace GermanApp.Presentation.Validators;
+
+///
+/// Validator for CreateLevelDto.
+/// Validates input when creating a new CEFR level.
+///
+public class CreateLevelValidator : AbstractValidator
+{
+ public CreateLevelValidator()
+ {
+ RuleFor(x => x.Name)
+ .NotEmpty().WithMessage("Level name is required.")
+ .MinimumLength(2).WithMessage("Level name must be at least 2 characters long.")
+ .MaximumLength(100).WithMessage("Level name must not exceed 100 characters.");
+
+ RuleFor(x => x.Code)
+ .NotEmpty().WithMessage("Level code is required.")
+ .MinimumLength(1).WithMessage("Level code must be at least 1 character long.")
+ .MaximumLength(10).WithMessage("Level code must not exceed 10 characters.")
+ .Matches("^[A-Za-z][A-Za-z0-9]*$").WithMessage("Level code must start with a letter and contain only letters and numbers.")
+ .Must(BeValidCefrCode).WithMessage("Level code must be a valid CEFR code (A1, A2, B1, B2, C1, C2).");
+
+ RuleFor(x => x.Order)
+ .GreaterThan(0).WithMessage("Order must be greater than 0.")
+ .LessThanOrEqualTo(100).WithMessage("Order must not exceed 100.");
+ }
+
+ private static bool BeValidCefrCode(string code)
+ {
+ // Normalize to uppercase for comparison
+ var upperCode = code.ToUpperInvariant();
+
+ // Valid CEFR levels
+ var validCodes = new[] { "A1", "A2", "B1", "B2", "C1", "C2" };
+
+ return validCodes.Contains(upperCode);
+ }
+}
+
+///
+/// Validator for UpdateLevelDto.
+/// Validates input when updating an existing CEFR level.
+///
+public class UpdateLevelValidator : AbstractValidator
+{
+ public UpdateLevelValidator()
+ {
+ RuleFor(x => x.Name)
+ .NotEmpty().WithMessage("Level name is required.")
+ .MinimumLength(2).WithMessage("Level name must be at least 2 characters long.")
+ .MaximumLength(100).WithMessage("Level name must not exceed 100 characters.");
+
+ RuleFor(x => x.Code)
+ .NotEmpty().WithMessage("Level code is required.")
+ .MinimumLength(1).WithMessage("Level code must be at least 1 character long.")
+ .MaximumLength(10).WithMessage("Level code must not exceed 10 characters.")
+ .Matches("^[A-Za-z][A-Za-z0-9]*$").WithMessage("Level code must start with a letter and contain only letters and numbers.")
+ .Must(BeValidCefrCode).WithMessage("Level code must be a valid CEFR code (A1, A2, B1, B2, C1, C2).");
+
+ RuleFor(x => x.Order)
+ .GreaterThan(0).WithMessage("Order must be greater than 0.")
+ .LessThanOrEqualTo(100).WithMessage("Order must not exceed 100.");
+ }
+
+ private static bool BeValidCefrCode(string code)
+ {
+ // Normalize to uppercase for comparison
+ var upperCode = code.ToUpperInvariant();
+
+ // Valid CEFR levels
+ var validCodes = new[] { "A1", "A2", "B1", "B2", "C1", "C2" };
+
+ return validCodes.Contains(upperCode);
+ }
+}
diff --git a/GermanApp/Program.cs b/GermanApp/Program.cs
index dab9913..8fa3d2c 100644
--- a/GermanApp/Program.cs
+++ b/GermanApp/Program.cs
@@ -1,3 +1,5 @@
+using FluentValidation;
+using FluentValidation.AspNetCore;
using GermanApp.Application.DTOs;
using GermanApp.Application.Services;
using GermanApp.Application.Interfaces;
@@ -9,6 +11,7 @@ using GermanApp.Infrastructure.Data.Repositories;
using GermanApp.Infrastructure.Data.SeedData;
using GermanApp.Infrastructure.Services;
using GermanApp.Presentation.Controllers;
+using GermanApp.Presentation.Validators;
using GermanApp.Presentation.Endpoints;
using GermanApp.Shared.Middleware;
using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -92,6 +95,15 @@ try
});
builder.Services.AddControllers();
+
+ // ============================================
+ // PRESENTATION LAYER - Validation
+ // ============================================
+
+ // Add FluentValidation
+ builder.Services.AddValidatorsFromAssemblyContaining();
+ builder.Services.AddFluentValidationAutoValidation();
+
// Add services for Minimal APIs
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
diff --git a/Tests/Integration/Controllers/LessonsControllerTests.cs b/Tests/Integration/Controllers/LessonsControllerTests.cs
new file mode 100644
index 0000000..5f70c26
--- /dev/null
+++ b/Tests/Integration/Controllers/LessonsControllerTests.cs
@@ -0,0 +1,372 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using GermanApp.Application.DTOs;
+using GermanApp.Application.Services;
+using GermanApp.Presentation.Controllers;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace GermanApp.Tests.Integration.Controllers;
+
+///
+/// Integration tests for LessonsController.
+/// Tests controller behavior with mocked services.
+///
+[TestClass]
+public class LessonsControllerTests
+{
+ private Mock? _mockLessonService;
+ private Mock? _mockProgressService;
+ private LessonsController? _controller;
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ _mockLessonService = new Mock(null!, null!);
+ _mockProgressService = new Mock(null!, null!, null!);
+ _controller = new LessonsController(_mockLessonService.Object, _mockProgressService.Object);
+ }
+
+ [TestCleanup]
+ public void TestCleanup()
+ {
+ _controller = null;
+ _mockLessonService = null;
+ _mockProgressService = null;
+ }
+
+ private static DateTime TestDate => new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+ // ==================== GET ALL LESSONS TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("GetAllLessons")]
+ public async Task GetAllLessons_WithData_ReturnsOkWithLessons()
+ {
+ // Arrange
+ var expectedLessons = new List
+ {
+ new LessonDto(1, "Greetings", "Learn basic greetings", 1, "A1", "Beginner A1", 1, "Vocabulary", TestDate, null),
+ new LessonDto(2, "Introductions", "Learn to introduce yourself", 1, "A1", "Beginner A1", 2, "Vocabulary", TestDate, null)
+ };
+
+ _mockLessonService!.Setup(s => s.GetAllLessonsAsync(It.IsAny()))
+ .ReturnsAsync(expectedLessons);
+
+ // Act
+ var result = await _controller!.GetAllLessonsAsync();
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLessons = okResult.Value as IReadOnlyList;
+ Assert.IsNotNull(returnedLessons);
+ Assert.AreEqual(2, returnedLessons.Count);
+ }
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("GetAllLessons")]
+ public async Task GetAllLessons_WithNoData_ReturnsEmptyList()
+ {
+ // Arrange
+ var expectedLessons = new List();
+
+ _mockLessonService!.Setup(s => s.GetAllLessonsAsync(It.IsAny()))
+ .ReturnsAsync(expectedLessons);
+
+ // Act
+ var result = await _controller!.GetAllLessonsAsync();
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLessons = okResult.Value as IReadOnlyList;
+ Assert.IsNotNull(returnedLessons);
+ Assert.AreEqual(0, returnedLessons.Count);
+ }
+
+ // ==================== GET LESSON BY ID TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("GetLessonById")]
+ public async Task GetLessonById_WithExistingId_ReturnsOkWithLesson()
+ {
+ // Arrange
+ var expectedLesson = new LessonDto(1, "Greetings", "Learn basic greetings", 1, "A1", "Beginner A1", 1, "Vocabulary", TestDate, null);
+
+ _mockLessonService!.Setup(s => s.GetLessonByIdAsync(1, It.IsAny()))
+ .ReturnsAsync(expectedLesson);
+
+ // Act
+ var result = await _controller!.GetLessonByIdAsync(1);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLesson = okResult.Value as LessonDto;
+ Assert.IsNotNull(returnedLesson);
+ Assert.AreEqual(1, returnedLesson.Id);
+ Assert.AreEqual("Greetings", returnedLesson.Title);
+ }
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("GetLessonById")]
+ public async Task GetLessonById_WithNonExistingId_ReturnsNotFound()
+ {
+ // Arrange
+ _mockLessonService!.Setup(s => s.GetLessonByIdAsync(999, It.IsAny()))
+ .ReturnsAsync((LessonDto?)null);
+
+ // Act
+ var result = await _controller!.GetLessonByIdAsync(999);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(NotFoundResult));
+ }
+
+ // ==================== GET LESSONS BY LEVEL TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("GetLessonsByLevel")]
+ public async Task GetLessonsByLevel_WithExistingLevel_ReturnsOkWithLessons()
+ {
+ // Arrange
+ var expectedLessons = new List
+ {
+ new LessonDto(1, "Greetings", "Learn basic greetings", 1, "A1", "Beginner A1", 1, "Vocabulary", TestDate, null)
+ };
+
+ _mockLessonService!.Setup(s => s.GetLessonsByLevelAsync(1, It.IsAny()))
+ .ReturnsAsync(expectedLessons);
+
+ // Act
+ var result = await _controller!.GetLessonsByLevelAsync(1);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLessons = okResult.Value as IReadOnlyList;
+ Assert.IsNotNull(returnedLessons);
+ Assert.AreEqual(1, returnedLessons.Count);
+ }
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("GetLessonsByLevel")]
+ public async Task GetLessonsByLevel_WithNonExistingLevel_ReturnsEmptyList()
+ {
+ // Arrange
+ var expectedLessons = new List();
+
+ _mockLessonService!.Setup(s => s.GetLessonsByLevelAsync(999, It.IsAny()))
+ .ReturnsAsync(expectedLessons);
+
+ // Act
+ var result = await _controller!.GetLessonsByLevelAsync(999);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLessons = okResult.Value as IReadOnlyList;
+ Assert.IsNotNull(returnedLessons);
+ Assert.AreEqual(0, returnedLessons.Count);
+ }
+
+ // ==================== CREATE LESSON TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("CreateLesson")]
+ public async Task CreateLesson_WithValidData_ReturnsCreatedAtAction()
+ {
+ // Arrange
+ var createDto = new CreateLessonDto("Greetings", "Learn basic greetings", 1, 1, "Vocabulary");
+ var expectedLesson = new LessonDto(1, "Greetings", "Learn basic greetings", 1, "A1", "Beginner A1", 1, "Vocabulary", TestDate, null);
+
+ _mockLessonService!.Setup(s => s.CreateLessonAsync(createDto, It.IsAny()))
+ .ReturnsAsync(expectedLesson);
+
+ // Act
+ var result = await _controller!.CreateLessonAsync(createDto);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(CreatedAtActionResult));
+ var createdResult = result as CreatedAtActionResult;
+ Assert.IsNotNull(createdResult);
+ var returnedLesson = createdResult.Value as LessonDto;
+ Assert.IsNotNull(returnedLesson);
+ Assert.AreEqual(1, returnedLesson.Id);
+ }
+
+ // ==================== UPDATE LESSON TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("UpdateLesson")]
+ public async Task UpdateLesson_WithExistingId_ReturnsOkWithUpdatedLesson()
+ {
+ // Arrange
+ var updateDto = new UpdateLessonDto("Updated Greetings", "Updated description", 1, 1, "Updated Topic");
+ var expectedLesson = new LessonDto(1, "Updated Greetings", "Updated description", 1, "A1", "Beginner A1", 1, "Updated Topic", TestDate, TestDate);
+
+ _mockLessonService!.Setup(s => s.UpdateLessonAsync(1, updateDto, It.IsAny()))
+ .ReturnsAsync(expectedLesson);
+
+ // Act
+ var result = await _controller!.UpdateLessonAsync(1, updateDto);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLesson = okResult.Value as LessonDto;
+ Assert.IsNotNull(returnedLesson);
+ Assert.AreEqual("Updated Greetings", returnedLesson.Title);
+ }
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("UpdateLesson")]
+ public async Task UpdateLesson_WithNonExistingId_ReturnsNotFound()
+ {
+ // Arrange
+ var updateDto = new UpdateLessonDto("Updated Greetings", "Updated description", 1, 1, "Updated Topic");
+
+ _mockLessonService!.Setup(s => s.UpdateLessonAsync(999, updateDto, It.IsAny()))
+ .ReturnsAsync((LessonDto?)null);
+
+ // Act
+ var result = await _controller!.UpdateLessonAsync(999, updateDto);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(NotFoundResult));
+ }
+
+ // ==================== DELETE LESSON TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("DeleteLesson")]
+ public async Task DeleteLesson_WithExistingId_ReturnsNoContent()
+ {
+ // Arrange
+ _mockLessonService!.Setup(s => s.DeleteLessonAsync(1, It.IsAny()))
+ .ReturnsAsync(true);
+
+ // Act
+ var result = await _controller!.DeleteLessonAsync(1);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(NoContentResult));
+ }
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("DeleteLesson")]
+ public async Task DeleteLesson_WithNonExistingId_ReturnsNotFound()
+ {
+ // Arrange
+ _mockLessonService!.Setup(s => s.DeleteLessonAsync(999, It.IsAny()))
+ .ReturnsAsync(false);
+
+ // Act
+ var result = await _controller!.DeleteLessonAsync(999);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(NotFoundResult));
+ }
+
+ // ==================== GET FIRST LESSON IN LEVEL TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("GetFirstLessonInLevel")]
+ public async Task GetFirstLessonInLevel_WithExistingLevel_ReturnsOkWithLesson()
+ {
+ // Arrange
+ var expectedLesson = new LessonDto(1, "Greetings", "Learn basic greetings", 1, "A1", "Beginner A1", 1, "Vocabulary", TestDate, null);
+
+ _mockLessonService!.Setup(s => s.GetFirstLessonInLevelAsync(1, It.IsAny()))
+ .ReturnsAsync(expectedLesson);
+
+ // Act
+ var result = await _controller!.GetFirstLessonInLevelAsync(1);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLesson = okResult.Value as LessonDto;
+ Assert.IsNotNull(returnedLesson);
+ Assert.AreEqual(1, returnedLesson.Order);
+ }
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("GetFirstLessonInLevel")]
+ public async Task GetFirstLessonInLevel_WithNonExistingLevel_ReturnsNotFound()
+ {
+ // Arrange
+ _mockLessonService!.Setup(s => s.GetFirstLessonInLevelAsync(999, It.IsAny()))
+ .ReturnsAsync((LessonDto?)null);
+
+ // Act
+ var result = await _controller!.GetFirstLessonInLevelAsync(999);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(NotFoundResult));
+ }
+
+ // ==================== IS NEXT LESSON UNLOCKED TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("IsNextLessonUnlocked")]
+ public async Task IsNextLessonUnlocked_WithCompletedPrevious_ReturnsTrue()
+ {
+ // Arrange
+ _mockProgressService!.Setup(s => s.IsNextLessonUnlockedAsync(1, 1, It.IsAny()))
+ .ReturnsAsync(true);
+
+ // Act
+ var result = await _controller!.IsNextLessonUnlockedAsync(1, 1);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ Assert.AreEqual(true, okResult.Value);
+ }
+
+ [TestMethod]
+ [TestCategory("LessonsController")]
+ [TestCategory("IsNextLessonUnlocked")]
+ public async Task IsNextLessonUnlocked_WithIncompletePrevious_ReturnsFalse()
+ {
+ // Arrange
+ _mockProgressService!.Setup(s => s.IsNextLessonUnlockedAsync(1, 1, It.IsAny()))
+ .ReturnsAsync(false);
+
+ // Act
+ var result = await _controller!.IsNextLessonUnlockedAsync(1, 1);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ Assert.AreEqual(false, okResult.Value);
+ }
+}
diff --git a/Tests/Integration/Controllers/LevelsControllerTests.cs b/Tests/Integration/Controllers/LevelsControllerTests.cs
new file mode 100644
index 0000000..7a57322
--- /dev/null
+++ b/Tests/Integration/Controllers/LevelsControllerTests.cs
@@ -0,0 +1,358 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using GermanApp.Application.DTOs;
+using GermanApp.Application.Services;
+using GermanApp.Presentation.Controllers;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace GermanApp.Tests.Integration.Controllers;
+
+///
+/// Integration tests for LevelsController.
+/// Tests controller behavior with mocked services.
+///
+[TestClass]
+public class LevelsControllerTests
+{
+ private Mock? _mockLevelService;
+ private LevelsController? _controller;
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ _mockLevelService = new Mock(null!);
+ _controller = new LevelsController(_mockLevelService.Object);
+ }
+
+ [TestCleanup]
+ public void TestCleanup()
+ {
+ _controller = null;
+ _mockLevelService = null;
+ }
+
+ // ==================== GET ALL LEVELS TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("GetAllLevels")]
+ public async Task GetAllLevels_WithData_ReturnsOkWithLevels()
+ {
+ // Arrange
+ var expectedLevels = new List
+ {
+ new LevelDto(1, "Beginner A1", "A1", 1),
+ new LevelDto(2, "Elementary A2", "A2", 2)
+ };
+
+ _mockLevelService!.Setup(s => s.GetAllLevelsAsync(It.IsAny()))
+ .ReturnsAsync(expectedLevels);
+
+ // Act
+ var result = await _controller!.GetAllLevelsAsync();
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLevels = okResult.Value as IReadOnlyList;
+ Assert.IsNotNull(returnedLevels);
+ Assert.AreEqual(2, returnedLevels.Count);
+ }
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("GetAllLevels")]
+ public async Task GetAllLevels_WithNoData_ReturnsEmptyList()
+ {
+ // Arrange
+ var expectedLevels = new List();
+
+ _mockLevelService!.Setup(s => s.GetAllLevelsAsync(It.IsAny()))
+ .ReturnsAsync(expectedLevels);
+
+ // Act
+ var result = await _controller!.GetAllLevelsAsync();
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLevels = okResult.Value as IReadOnlyList;
+ Assert.IsNotNull(returnedLevels);
+ Assert.AreEqual(0, returnedLevels.Count);
+ }
+
+ // ==================== GET LEVEL BY ID TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("GetLevelById")]
+ public async Task GetLevelById_WithExistingId_ReturnsOkWithLevel()
+ {
+ // Arrange
+ var expectedLevel = new LevelDto(1, "Beginner A1", "A1", 1);
+
+ _mockLevelService!.Setup(s => s.GetLevelByIdAsync(1, It.IsAny()))
+ .ReturnsAsync(expectedLevel);
+
+ // Act
+ var result = await _controller!.GetLevelByIdAsync(1);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLevel = okResult.Value as LevelDto;
+ Assert.IsNotNull(returnedLevel);
+ Assert.AreEqual(1, returnedLevel.Id);
+ Assert.AreEqual("Beginner A1", returnedLevel.Name);
+ }
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("GetLevelById")]
+ public async Task GetLevelById_WithNonExistingId_ReturnsNotFound()
+ {
+ // Arrange
+ _mockLevelService!.Setup(s => s.GetLevelByIdAsync(999, It.IsAny()))
+ .ReturnsAsync((LevelDto?)null);
+
+ // Act
+ var result = await _controller!.GetLevelByIdAsync(999);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(NotFoundResult));
+ }
+
+ // ==================== GET LEVEL BY CODE TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("GetLevelByCode")]
+ public async Task GetLevelByCode_WithExistingCode_ReturnsOkWithLevel()
+ {
+ // Arrange
+ var expectedLevel = new LevelDto(1, "Beginner A1", "A1", 1);
+
+ _mockLevelService!.Setup(s => s.GetLevelByCodeAsync("A1", It.IsAny()))
+ .ReturnsAsync(expectedLevel);
+
+ // Act
+ var result = await _controller!.GetLevelByCodeAsync("A1");
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLevel = okResult.Value as LevelDto;
+ Assert.IsNotNull(returnedLevel);
+ Assert.AreEqual("A1", returnedLevel.Code);
+ }
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("GetLevelByCode")]
+ public async Task GetLevelByCode_WithNonExistingCode_ReturnsNotFound()
+ {
+ // Arrange
+ _mockLevelService!.Setup(s => s.GetLevelByCodeAsync("INVALID", It.IsAny()))
+ .ReturnsAsync((LevelDto?)null);
+
+ // Act
+ var result = await _controller!.GetLevelByCodeAsync("INVALID");
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(NotFoundResult));
+ }
+
+ // ==================== CREATE LEVEL TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("CreateLevel")]
+ public async Task CreateLevel_WithValidData_ReturnsCreatedAtAction()
+ {
+ // Arrange
+ var createDto = new CreateLevelDto("Beginner A1", "A1", 1);
+ var expectedLevel = new LevelDto(1, "Beginner A1", "A1", 1);
+
+ _mockLevelService!.Setup(s => s.CreateLevelAsync(createDto, It.IsAny()))
+ .ReturnsAsync(expectedLevel);
+
+ // Act
+ var result = await _controller!.CreateLevelAsync(createDto);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(CreatedAtActionResult));
+ var createdResult = result as CreatedAtActionResult;
+ Assert.IsNotNull(createdResult);
+ var returnedLevel = createdResult.Value as LevelDto;
+ Assert.IsNotNull(returnedLevel);
+ Assert.AreEqual(1, returnedLevel.Id);
+ }
+
+ // ==================== UPDATE LEVEL TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("UpdateLevel")]
+ public async Task UpdateLevel_WithExistingId_ReturnsOkWithUpdatedLevel()
+ {
+ // Arrange
+ var updateDto = new UpdateLevelDto("Updated A1", "A1", 1);
+ var expectedLevel = new LevelDto(1, "Updated A1", "A1", 1);
+
+ _mockLevelService!.Setup(s => s.UpdateLevelAsync(1, updateDto, It.IsAny()))
+ .ReturnsAsync(expectedLevel);
+
+ // Act
+ var result = await _controller!.UpdateLevelAsync(1, updateDto);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLevel = okResult.Value as LevelDto;
+ Assert.IsNotNull(returnedLevel);
+ Assert.AreEqual("Updated A1", returnedLevel.Name);
+ }
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("UpdateLevel")]
+ public async Task UpdateLevel_WithNonExistingId_ReturnsNotFound()
+ {
+ // Arrange
+ var updateDto = new UpdateLevelDto("Updated A1", "A1", 1);
+
+ _mockLevelService!.Setup(s => s.UpdateLevelAsync(999, updateDto, It.IsAny()))
+ .ReturnsAsync((LevelDto?)null);
+
+ // Act
+ var result = await _controller!.UpdateLevelAsync(999, updateDto);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(NotFoundResult));
+ }
+
+ // ==================== DELETE LEVEL TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("DeleteLevel")]
+ public async Task DeleteLevel_WithExistingId_ReturnsNoContent()
+ {
+ // Arrange
+ _mockLevelService!.Setup(s => s.DeleteLevelAsync(1, It.IsAny()))
+ .ReturnsAsync(true);
+
+ // Act
+ var result = await _controller!.DeleteLevelAsync(1);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(NoContentResult));
+ }
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("DeleteLevel")]
+ public async Task DeleteLevel_WithNonExistingId_ReturnsNotFound()
+ {
+ // Arrange
+ _mockLevelService!.Setup(s => s.DeleteLevelAsync(999, It.IsAny()))
+ .ReturnsAsync(false);
+
+ // Act
+ var result = await _controller!.DeleteLevelAsync(999);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(NotFoundResult));
+ }
+
+ // ==================== GET FIRST LEVEL TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("GetFirstLevel")]
+ public async Task GetFirstLevel_WithData_ReturnsOkWithLevel()
+ {
+ // Arrange
+ var expectedLevel = new LevelDto(1, "Beginner A1", "A1", 1);
+
+ _mockLevelService!.Setup(s => s.GetFirstLevelAsync(It.IsAny()))
+ .ReturnsAsync(expectedLevel);
+
+ // Act
+ var result = await _controller!.GetFirstLevelAsync();
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLevel = okResult.Value as LevelDto;
+ Assert.IsNotNull(returnedLevel);
+ Assert.AreEqual(1, returnedLevel.Order);
+ }
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("GetFirstLevel")]
+ public async Task GetFirstLevel_WithNoData_ReturnsNotFound()
+ {
+ // Arrange
+ _mockLevelService!.Setup(s => s.GetFirstLevelAsync(It.IsAny()))
+ .ReturnsAsync((LevelDto?)null);
+
+ // Act
+ var result = await _controller!.GetFirstLevelAsync();
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(NotFoundResult));
+ }
+
+ // ==================== GET NEXT LEVEL TESTS ====================
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("GetNextLevel")]
+ public async Task GetNextLevel_WithExistingCurrentLevel_ReturnsOkWithNextLevel()
+ {
+ // Arrange
+ var expectedLevel = new LevelDto(2, "Elementary A2", "A2", 2);
+
+ _mockLevelService!.Setup(s => s.GetNextLevelAsync(1, It.IsAny()))
+ .ReturnsAsync(expectedLevel);
+
+ // Act
+ var result = await _controller!.GetNextLevelAsync(1);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(OkObjectResult));
+ var okResult = result as OkObjectResult;
+ Assert.IsNotNull(okResult);
+ var returnedLevel = okResult.Value as LevelDto;
+ Assert.IsNotNull(returnedLevel);
+ Assert.AreEqual(2, returnedLevel.Order);
+ }
+
+ [TestMethod]
+ [TestCategory("LevelsController")]
+ [TestCategory("GetNextLevel")]
+ public async Task GetNextLevel_WithNoNextLevel_ReturnsNotFound()
+ {
+ // Arrange
+ _mockLevelService!.Setup(s => s.GetNextLevelAsync(5, It.IsAny()))
+ .ReturnsAsync((LevelDto?)null);
+
+ // Act
+ var result = await _controller!.GetNextLevelAsync(5);
+
+ // Assert
+ Assert.IsInstanceOfType(result, typeof(NotFoundResult));
+ }
+}