test: add unit and integration tests
Domain Tests: - StudentTests: entity creation and validation - EmailTests: value object validation - EnrollmentDomainServiceTests: business rules Application Tests: - EnrollStudentCommandTests: enrollment scenarios - Max 3 subjects validation - Same professor constraint Uses xUnit, Moq, and FluentAssertions
This commit is contained in:
parent
84d88c91d1
commit
874b67d07f
|
|
@ -0,0 +1,30 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="*" />
|
||||
<PackageReference Include="xunit" Version="*" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="*">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="*" />
|
||||
<PackageReference Include="NSubstitute" Version="*" />
|
||||
<PackageReference Include="coverlet.collector" Version="*">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\backend\Application\Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
namespace Application.Tests.Enrollments;
|
||||
|
||||
using Application.Enrollments.Commands;
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using Domain.Ports.Repositories;
|
||||
using Domain.Services;
|
||||
using Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
public class EnrollStudentCommandTests
|
||||
{
|
||||
private readonly IStudentRepository _studentRepo = Substitute.For<IStudentRepository>();
|
||||
private readonly ISubjectRepository _subjectRepo = Substitute.For<ISubjectRepository>();
|
||||
private readonly IEnrollmentRepository _enrollmentRepo = Substitute.For<IEnrollmentRepository>();
|
||||
private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
|
||||
private readonly EnrollmentDomainService _enrollmentService = new();
|
||||
private readonly EnrollStudentHandler _sut;
|
||||
|
||||
public EnrollStudentCommandTests()
|
||||
{
|
||||
_sut = new EnrollStudentHandler(
|
||||
_studentRepo,
|
||||
_subjectRepo,
|
||||
_enrollmentRepo,
|
||||
_enrollmentService,
|
||||
_unitOfWork);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenValid_ShouldEnrollStudent()
|
||||
{
|
||||
var student = CreateStudent();
|
||||
var subject = CreateSubjectWithProfessor(1, "Math", 1, "Prof. Smith");
|
||||
|
||||
_studentRepo.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
_subjectRepo.GetByIdWithProfessorAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(subject);
|
||||
|
||||
var command = new EnrollStudentCommand(1, 1);
|
||||
var result = await _sut.Handle(command, CancellationToken.None);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.SubjectName.Should().Be("Math");
|
||||
result.ProfessorName.Should().Be("Prof. Smith");
|
||||
|
||||
_enrollmentRepo.Received(1).Add(Arg.Any<Enrollment>());
|
||||
await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentNotFound_ShouldThrow()
|
||||
{
|
||||
_studentRepo.GetByIdWithEnrollmentsAsync(999, Arg.Any<CancellationToken>())
|
||||
.Returns((Student?)null);
|
||||
|
||||
var command = new EnrollStudentCommand(999, 1);
|
||||
var act = () => _sut.Handle(command, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<StudentNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenSubjectNotFound_ShouldThrow()
|
||||
{
|
||||
var student = CreateStudent();
|
||||
_studentRepo.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
_subjectRepo.GetByIdWithProfessorAsync(999, Arg.Any<CancellationToken>())
|
||||
.Returns((Subject?)null);
|
||||
|
||||
var command = new EnrollStudentCommand(1, 999);
|
||||
var act = () => _sut.Handle(command, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<SubjectNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenMaxEnrollmentsReached_ShouldThrow()
|
||||
{
|
||||
var student = CreateStudentWithEnrollments(3);
|
||||
var subject = CreateSubjectWithProfessor(10, "Physics", 99, "New Prof");
|
||||
|
||||
_studentRepo.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
_subjectRepo.GetByIdWithProfessorAsync(10, Arg.Any<CancellationToken>())
|
||||
.Returns(subject);
|
||||
|
||||
var command = new EnrollStudentCommand(1, 10);
|
||||
var act = () => _sut.Handle(command, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<MaxEnrollmentsExceededException>();
|
||||
_enrollmentRepo.DidNotReceive().Add(Arg.Any<Enrollment>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenSameProfessor_ShouldThrow()
|
||||
{
|
||||
var student = CreateStudent();
|
||||
var existingSubject = CreateSubjectWithProfessor(1, "Math", 1, "Prof. Smith");
|
||||
AddEnrollmentToStudent(student, existingSubject);
|
||||
|
||||
var newSubject = CreateSubjectWithProfessor(2, "Algebra", 1, "Prof. Smith");
|
||||
|
||||
_studentRepo.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
_subjectRepo.GetByIdWithProfessorAsync(2, Arg.Any<CancellationToken>())
|
||||
.Returns(newSubject);
|
||||
|
||||
var command = new EnrollStudentCommand(1, 2);
|
||||
var act = () => _sut.Handle(command, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<SameProfessorConstraintException>();
|
||||
}
|
||||
|
||||
private static Student CreateStudent()
|
||||
{
|
||||
var student = new Student("John Doe", Email.Create("john@test.com"));
|
||||
typeof(Student).GetProperty("Id")!.SetValue(student, 1);
|
||||
return student;
|
||||
}
|
||||
|
||||
private static Student CreateStudentWithEnrollments(int count)
|
||||
{
|
||||
var student = CreateStudent();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var subject = CreateSubjectWithProfessor(i + 1, $"Subject{i}", i + 1, $"Prof{i}");
|
||||
AddEnrollmentToStudent(student, subject);
|
||||
}
|
||||
return student;
|
||||
}
|
||||
|
||||
private static Subject CreateSubjectWithProfessor(int id, string name, int profId, string profName)
|
||||
{
|
||||
var professor = new Professor(profName);
|
||||
typeof(Professor).GetProperty("Id")!.SetValue(professor, profId);
|
||||
|
||||
var subject = new Subject(name, profId);
|
||||
typeof(Subject).GetProperty("Id")!.SetValue(subject, id);
|
||||
typeof(Subject).GetProperty("Professor")!.SetValue(subject, professor);
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
private static void AddEnrollmentToStudent(Student student, Subject subject)
|
||||
{
|
||||
var enrollment = new Enrollment(student.Id, subject.Id);
|
||||
typeof(Enrollment).GetProperty("Subject")!.SetValue(enrollment, subject);
|
||||
student.AddEnrollment(enrollment);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
namespace Application.Tests.Enrollments;
|
||||
|
||||
using Application.Enrollments.Queries;
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using Domain.Ports.Repositories;
|
||||
using Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
public class GetClassmatesQueryTests
|
||||
{
|
||||
private readonly IStudentRepository _studentRepository;
|
||||
private readonly IEnrollmentRepository _enrollmentRepository;
|
||||
private readonly GetClassmatesHandler _handler;
|
||||
|
||||
public GetClassmatesQueryTests()
|
||||
{
|
||||
_studentRepository = Substitute.For<IStudentRepository>();
|
||||
_enrollmentRepository = Substitute.For<IEnrollmentRepository>();
|
||||
_handler = new GetClassmatesHandler(_studentRepository, _enrollmentRepository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentNotFound_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns((Student?)null);
|
||||
|
||||
var query = new GetClassmatesQuery(1);
|
||||
|
||||
// Act
|
||||
var act = () => _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<StudentNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentHasNoEnrollments_ShouldReturnEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Student("John Doe", Email.Create("john@test.com"));
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
|
||||
var query = new GetClassmatesQuery(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeEmpty();
|
||||
await _enrollmentRepository.DidNotReceive()
|
||||
.GetClassmatesBatchAsync(Arg.Any<int>(), Arg.Any<IEnumerable<int>>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentHasEnrollments_ShouldUseBatchQuery()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Student("John Doe", Email.Create("john@test.com"));
|
||||
SetEntityId(student, 1);
|
||||
|
||||
// Create enrollments with IDs
|
||||
var enrollment1 = new Enrollment(1, 1);
|
||||
var enrollment2 = new Enrollment(1, 2);
|
||||
SetEntityId(enrollment1, 1);
|
||||
SetEntityId(enrollment2, 2);
|
||||
|
||||
// Use reflection to set Subject property for name access
|
||||
var subject1 = new Subject("Math", 1);
|
||||
var subject2 = new Subject("Physics", 2);
|
||||
SetEntityId(subject1, 1);
|
||||
SetEntityId(subject2, 2);
|
||||
SetNavigationProperty(enrollment1, "Subject", subject1);
|
||||
SetNavigationProperty(enrollment2, "Subject", subject2);
|
||||
|
||||
student.AddEnrollment(enrollment1);
|
||||
student.AddEnrollment(enrollment2);
|
||||
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
|
||||
var classmate1 = new Student("Jane Doe", Email.Create("jane@test.com"));
|
||||
var classmate2 = new Student("Bob Smith", Email.Create("bob@test.com"));
|
||||
SetEntityId(classmate1, 2);
|
||||
SetEntityId(classmate2, 3);
|
||||
|
||||
var batchResult = new Dictionary<int, IReadOnlyList<Student>>
|
||||
{
|
||||
[1] = [classmate1],
|
||||
[2] = [classmate2]
|
||||
};
|
||||
|
||||
_enrollmentRepository.GetClassmatesBatchAsync(1, Arg.Any<IEnumerable<int>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(batchResult);
|
||||
|
||||
var query = new GetClassmatesQuery(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
result.Should().Contain(r => r.SubjectId == 1 && r.Classmates.Any(c => c.Name == "Jane Doe"));
|
||||
result.Should().Contain(r => r.SubjectId == 2 && r.Classmates.Any(c => c.Name == "Bob Smith"));
|
||||
|
||||
await _enrollmentRepository.Received(1)
|
||||
.GetClassmatesBatchAsync(1, Arg.Any<IEnumerable<int>>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenNoClassmatesFound_ShouldReturnEmptyClassmatesLists()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Student("John Doe", Email.Create("john@test.com"));
|
||||
SetEntityId(student, 1);
|
||||
|
||||
var enrollment = new Enrollment(1, 1);
|
||||
SetEntityId(enrollment, 1);
|
||||
var subject = new Subject("Math", 1);
|
||||
SetEntityId(subject, 1);
|
||||
SetNavigationProperty(enrollment, "Subject", subject);
|
||||
student.AddEnrollment(enrollment);
|
||||
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
|
||||
var emptyBatchResult = new Dictionary<int, IReadOnlyList<Student>>();
|
||||
_enrollmentRepository.GetClassmatesBatchAsync(1, Arg.Any<IEnumerable<int>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(emptyBatchResult);
|
||||
|
||||
var query = new GetClassmatesQuery(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(1);
|
||||
result[0].Classmates.Should().BeEmpty();
|
||||
}
|
||||
|
||||
private static void SetEntityId<T>(T entity, int id) where T : class
|
||||
{
|
||||
var prop = typeof(T).GetProperty("Id");
|
||||
prop?.SetValue(entity, id);
|
||||
}
|
||||
|
||||
private static void SetNavigationProperty<T>(T entity, string propertyName, object value) where T : class
|
||||
{
|
||||
var prop = typeof(T).GetProperty(propertyName);
|
||||
prop?.SetValue(entity, value);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
namespace Application.Tests.Enrollments;
|
||||
|
||||
using Application.Enrollments.Commands;
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using Domain.Ports.Repositories;
|
||||
using Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
public class UnenrollStudentCommandTests
|
||||
{
|
||||
private readonly IEnrollmentRepository _enrollmentRepository;
|
||||
private readonly IStudentRepository _studentRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly UnenrollStudentHandler _handler;
|
||||
|
||||
public UnenrollStudentCommandTests()
|
||||
{
|
||||
_enrollmentRepository = Substitute.For<IEnrollmentRepository>();
|
||||
_studentRepository = Substitute.For<IStudentRepository>();
|
||||
_unitOfWork = Substitute.For<IUnitOfWork>();
|
||||
_handler = new UnenrollStudentHandler(_enrollmentRepository, _studentRepository, _unitOfWork);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenEnrollmentExists_ShouldUnenrollStudent()
|
||||
{
|
||||
// Arrange
|
||||
var enrollment = new Enrollment(1, 1);
|
||||
SetEntityId(enrollment, 1);
|
||||
|
||||
var student = new Student("John Doe", Email.Create("john@example.com"));
|
||||
SetEntityId(student, 1);
|
||||
student.AddEnrollment(enrollment);
|
||||
|
||||
_enrollmentRepository.GetByIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(enrollment);
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
|
||||
var command = new UnenrollStudentCommand(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
_enrollmentRepository.Received(1).Delete(enrollment);
|
||||
await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenEnrollmentNotFound_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
_enrollmentRepository.GetByIdAsync(999, Arg.Any<CancellationToken>())
|
||||
.Returns((Enrollment?)null);
|
||||
|
||||
var command = new UnenrollStudentCommand(999);
|
||||
|
||||
// Act
|
||||
var act = () => _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<EnrollmentNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentNotFound_ShouldStillDeleteEnrollment()
|
||||
{
|
||||
// Arrange
|
||||
var enrollment = new Enrollment(999, 1);
|
||||
SetEntityId(enrollment, 1);
|
||||
|
||||
_enrollmentRepository.GetByIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(enrollment);
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(999, Arg.Any<CancellationToken>())
|
||||
.Returns((Student?)null);
|
||||
|
||||
var command = new UnenrollStudentCommand(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
_enrollmentRepository.Received(1).Delete(enrollment);
|
||||
}
|
||||
|
||||
private static void SetEntityId<T>(T entity, int id) where T : class
|
||||
{
|
||||
typeof(T).GetProperty("Id")?.SetValue(entity, id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
namespace Application.Tests.Professors;
|
||||
|
||||
using Application.Professors.DTOs;
|
||||
using Application.Professors.Queries;
|
||||
using Domain.Ports.Repositories;
|
||||
using FluentAssertions;
|
||||
using NSubstitute;
|
||||
using System.Linq.Expressions;
|
||||
using Xunit;
|
||||
|
||||
public class GetProfessorsQueryTests
|
||||
{
|
||||
private readonly IProfessorRepository _professorRepository;
|
||||
private readonly GetProfessorsHandler _handler;
|
||||
|
||||
public GetProfessorsQueryTests()
|
||||
{
|
||||
_professorRepository = Substitute.For<IProfessorRepository>();
|
||||
_handler = new GetProfessorsHandler(_professorRepository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldReturnAllProfessors()
|
||||
{
|
||||
// Arrange
|
||||
var professors = new List<ProfessorDto>
|
||||
{
|
||||
new(1, "Prof A", [new ProfessorSubjectDto(1, "Math", 3), new ProfessorSubjectDto(2, "Physics", 3)]),
|
||||
new(2, "Prof B", [new ProfessorSubjectDto(3, "Chemistry", 3)])
|
||||
};
|
||||
|
||||
_professorRepository.GetAllProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Professor, ProfessorDto>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(professors);
|
||||
|
||||
var query = new GetProfessorsQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
result[0].Name.Should().Be("Prof A");
|
||||
result[0].Subjects.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenNoProfessors_ShouldReturnEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
_professorRepository.GetAllProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Professor, ProfessorDto>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ProfessorDto>());
|
||||
|
||||
var query = new GetProfessorsQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldReturnProfessorsWithSubjects()
|
||||
{
|
||||
// Arrange
|
||||
var subjects = new List<ProfessorSubjectDto>
|
||||
{
|
||||
new(1, "Math", 3),
|
||||
new(2, "Physics", 3)
|
||||
};
|
||||
var professors = new List<ProfessorDto>
|
||||
{
|
||||
new(1, "Prof A", subjects)
|
||||
};
|
||||
|
||||
_professorRepository.GetAllProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Professor, ProfessorDto>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(professors);
|
||||
|
||||
var query = new GetProfessorsQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result[0].Subjects.Should().Contain(s => s.Name == "Math");
|
||||
result[0].Subjects.Should().Contain(s => s.Name == "Physics");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
namespace Application.Tests.Students;
|
||||
|
||||
using Application.Students.Commands;
|
||||
using Application.Students.DTOs;
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using Domain.Ports.Repositories;
|
||||
using Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
public class CreateStudentCommandTests
|
||||
{
|
||||
private readonly IStudentRepository _studentRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly CreateStudentHandler _handler;
|
||||
|
||||
public CreateStudentCommandTests()
|
||||
{
|
||||
_studentRepository = Substitute.For<IStudentRepository>();
|
||||
_unitOfWork = Substitute.For<IUnitOfWork>();
|
||||
_handler = new CreateStudentHandler(_studentRepository, _unitOfWork);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidData_ShouldCreateStudent()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateStudentCommand("John Doe", "john@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Name.Should().Be("John Doe");
|
||||
result.Email.Should().Be("john@example.com");
|
||||
result.TotalCredits.Should().Be(0);
|
||||
result.Enrollments.Should().BeEmpty();
|
||||
|
||||
_studentRepository.Received(1).Add(Arg.Any<Student>());
|
||||
await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithInvalidEmail_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateStudentCommand("John Doe", "invalid-email");
|
||||
|
||||
// Act
|
||||
var act = () => _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithEmptyName_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateStudentCommand("", "john@example.com");
|
||||
|
||||
// Act
|
||||
var act = () => _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentException>();
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateStudentCommandTests
|
||||
{
|
||||
private readonly IStudentRepository _studentRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly UpdateStudentHandler _handler;
|
||||
|
||||
public UpdateStudentCommandTests()
|
||||
{
|
||||
_studentRepository = Substitute.For<IStudentRepository>();
|
||||
_unitOfWork = Substitute.For<IUnitOfWork>();
|
||||
_handler = new UpdateStudentHandler(_studentRepository, _unitOfWork);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentExists_ShouldUpdateStudent()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Student("Old Name", Email.Create("old@example.com"));
|
||||
SetEntityId(student, 1);
|
||||
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
|
||||
var command = new UpdateStudentCommand(1, "New Name", "new@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Name.Should().Be("New Name");
|
||||
result.Email.Should().Be("new@example.com");
|
||||
|
||||
_studentRepository.Received(1).Update(student);
|
||||
await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentNotFound_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(999, Arg.Any<CancellationToken>())
|
||||
.Returns((Student?)null);
|
||||
|
||||
var command = new UpdateStudentCommand(999, "Name", "email@test.com");
|
||||
|
||||
// Act
|
||||
var act = () => _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<StudentNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithInvalidEmail_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Student("Name", Email.Create("old@example.com"));
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
|
||||
var command = new UpdateStudentCommand(1, "Name", "invalid");
|
||||
|
||||
// Act
|
||||
var act = () => _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentException>();
|
||||
}
|
||||
|
||||
private static void SetEntityId<T>(T entity, int id) where T : class
|
||||
{
|
||||
typeof(T).GetProperty("Id")?.SetValue(entity, id);
|
||||
}
|
||||
}
|
||||
|
||||
public class DeleteStudentCommandTests
|
||||
{
|
||||
private readonly IStudentRepository _studentRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly DeleteStudentHandler _handler;
|
||||
|
||||
public DeleteStudentCommandTests()
|
||||
{
|
||||
_studentRepository = Substitute.For<IStudentRepository>();
|
||||
_unitOfWork = Substitute.For<IUnitOfWork>();
|
||||
_handler = new DeleteStudentHandler(_studentRepository, _unitOfWork);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentExists_ShouldDeleteStudent()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Student("John Doe", Email.Create("john@example.com"));
|
||||
_studentRepository.GetByIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
|
||||
var command = new DeleteStudentCommand(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
_studentRepository.Received(1).Delete(student);
|
||||
await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentNotFound_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
_studentRepository.GetByIdAsync(999, Arg.Any<CancellationToken>())
|
||||
.Returns((Student?)null);
|
||||
|
||||
var command = new DeleteStudentCommand(999);
|
||||
|
||||
// Act
|
||||
var act = () => _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<StudentNotFoundException>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
namespace Application.Tests.Students;
|
||||
|
||||
using Application.Students.DTOs;
|
||||
using Application.Students.Queries;
|
||||
using Domain.Exceptions;
|
||||
using Domain.Ports.Repositories;
|
||||
using FluentAssertions;
|
||||
using NSubstitute;
|
||||
using System.Linq.Expressions;
|
||||
using Xunit;
|
||||
|
||||
public class GetStudentByIdQueryTests
|
||||
{
|
||||
private readonly IStudentRepository _studentRepository;
|
||||
private readonly GetStudentByIdHandler _handler;
|
||||
|
||||
public GetStudentByIdQueryTests()
|
||||
{
|
||||
_studentRepository = Substitute.For<IStudentRepository>();
|
||||
_handler = new GetStudentByIdHandler(_studentRepository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentExists_ShouldReturnStudentDto()
|
||||
{
|
||||
// Arrange
|
||||
var expectedDto = new StudentDto(1, "John Doe", "john@example.com", 0, []);
|
||||
|
||||
_studentRepository.GetByIdProjectedAsync(
|
||||
1,
|
||||
Arg.Any<Expression<Func<Domain.Entities.Student, StudentDto>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(expectedDto);
|
||||
|
||||
var query = new GetStudentByIdQuery(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Id.Should().Be(1);
|
||||
result.Name.Should().Be("John Doe");
|
||||
result.Email.Should().Be("john@example.com");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentNotFound_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
_studentRepository.GetByIdProjectedAsync<StudentDto>(
|
||||
999,
|
||||
Arg.Any<Expression<Func<Domain.Entities.Student, StudentDto>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns((StudentDto?)null);
|
||||
|
||||
var query = new GetStudentByIdQuery(999);
|
||||
|
||||
// Act
|
||||
var act = () => _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<StudentNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentHasEnrollments_ShouldReturnWithEnrollments()
|
||||
{
|
||||
// Arrange
|
||||
var enrollments = new List<EnrollmentDto>
|
||||
{
|
||||
new(1, 1, "Math", 3, "Prof A", DateTime.UtcNow),
|
||||
new(2, 2, "Physics", 3, "Prof B", DateTime.UtcNow)
|
||||
};
|
||||
var expectedDto = new StudentDto(1, "John Doe", "john@example.com", 6, enrollments);
|
||||
|
||||
_studentRepository.GetByIdProjectedAsync(
|
||||
1,
|
||||
Arg.Any<Expression<Func<Domain.Entities.Student, StudentDto>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(expectedDto);
|
||||
|
||||
var query = new GetStudentByIdQuery(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Enrollments.Should().HaveCount(2);
|
||||
result.TotalCredits.Should().Be(6);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetStudentsQueryTests
|
||||
{
|
||||
private readonly IStudentRepository _studentRepository;
|
||||
private readonly GetStudentsHandler _handler;
|
||||
|
||||
public GetStudentsQueryTests()
|
||||
{
|
||||
_studentRepository = Substitute.For<IStudentRepository>();
|
||||
_handler = new GetStudentsHandler(_studentRepository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldReturnAllStudents()
|
||||
{
|
||||
// Arrange
|
||||
var students = new List<StudentDto>
|
||||
{
|
||||
new(1, "John Doe", "john@example.com", 0, []),
|
||||
new(2, "Jane Doe", "jane@example.com", 3, [])
|
||||
};
|
||||
|
||||
_studentRepository.GetAllProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Student, StudentDto>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(students);
|
||||
|
||||
var query = new GetStudentsQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
result[0].Name.Should().Be("John Doe");
|
||||
result[1].Name.Should().Be("Jane Doe");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenNoStudents_ShouldReturnEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
_studentRepository.GetAllProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Student, StudentDto>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(new List<StudentDto>());
|
||||
|
||||
var query = new GetStudentsQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class GetStudentsPagedQueryTests
|
||||
{
|
||||
private readonly IStudentRepository _studentRepository;
|
||||
private readonly GetStudentsPagedHandler _handler;
|
||||
|
||||
public GetStudentsPagedQueryTests()
|
||||
{
|
||||
_studentRepository = Substitute.For<IStudentRepository>();
|
||||
_handler = new GetStudentsPagedHandler(_studentRepository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldReturnPagedResult()
|
||||
{
|
||||
// Arrange
|
||||
var items = new List<StudentPagedDto>
|
||||
{
|
||||
new(1, "John Doe", "john@example.com", 0),
|
||||
new(2, "Jane Doe", "jane@example.com", 3)
|
||||
};
|
||||
|
||||
_studentRepository.GetPagedProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Student, StudentPagedDto>>>(),
|
||||
null,
|
||||
10,
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns((items, (int?)3, 5));
|
||||
|
||||
var query = new GetStudentsPagedQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Items.Should().HaveCount(2);
|
||||
result.NextCursor.Should().Be(3);
|
||||
result.TotalCount.Should().Be(5);
|
||||
result.HasNextPage.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithCursor_ShouldPassCursorToRepository()
|
||||
{
|
||||
// Arrange
|
||||
_studentRepository.GetPagedProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Student, StudentPagedDto>>>(),
|
||||
5,
|
||||
10,
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns((new List<StudentPagedDto>(), (int?)null, 5));
|
||||
|
||||
var query = new GetStudentsPagedQuery(AfterCursor: 5);
|
||||
|
||||
// Act
|
||||
await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await _studentRepository.Received(1).GetPagedProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Student, StudentPagedDto>>>(),
|
||||
5,
|
||||
10,
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithLargePageSize_ShouldLimitTo50()
|
||||
{
|
||||
// Arrange
|
||||
_studentRepository.GetPagedProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Student, StudentPagedDto>>>(),
|
||||
Arg.Any<int?>(),
|
||||
50,
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns((new List<StudentPagedDto>(), (int?)null, 0));
|
||||
|
||||
var query = new GetStudentsPagedQuery(PageSize: 100);
|
||||
|
||||
// Act
|
||||
await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await _studentRepository.Received(1).GetPagedProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Student, StudentPagedDto>>>(),
|
||||
null,
|
||||
50,
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenNoNextPage_ShouldReturnHasNextPageFalse()
|
||||
{
|
||||
// Arrange
|
||||
_studentRepository.GetPagedProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Student, StudentPagedDto>>>(),
|
||||
Arg.Any<int?>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns((new List<StudentPagedDto>(), (int?)null, 2));
|
||||
|
||||
var query = new GetStudentsPagedQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.HasNextPage.Should().BeFalse();
|
||||
result.NextCursor.Should().BeNull();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
namespace Application.Tests.Subjects;
|
||||
|
||||
using Application.Subjects.DTOs;
|
||||
using Application.Subjects.Queries;
|
||||
using Domain.Ports.Repositories;
|
||||
using FluentAssertions;
|
||||
using NSubstitute;
|
||||
using System.Linq.Expressions;
|
||||
using Xunit;
|
||||
|
||||
public class GetSubjectsQueryTests
|
||||
{
|
||||
private readonly ISubjectRepository _subjectRepository;
|
||||
private readonly GetSubjectsHandler _handler;
|
||||
|
||||
public GetSubjectsQueryTests()
|
||||
{
|
||||
_subjectRepository = Substitute.For<ISubjectRepository>();
|
||||
_handler = new GetSubjectsHandler(_subjectRepository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldReturnAllSubjects()
|
||||
{
|
||||
// Arrange
|
||||
var subjects = new List<SubjectDto>
|
||||
{
|
||||
new(1, "Math", 3, 1, "Prof A"),
|
||||
new(2, "Physics", 3, 1, "Prof A"),
|
||||
new(3, "Chemistry", 3, 2, "Prof B")
|
||||
};
|
||||
|
||||
_subjectRepository.GetAllProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Subject, SubjectDto>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(subjects);
|
||||
|
||||
var query = new GetSubjectsQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(3);
|
||||
result[0].Name.Should().Be("Math");
|
||||
result[0].ProfessorName.Should().Be("Prof A");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenNoSubjects_ShouldReturnEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
_subjectRepository.GetAllProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Subject, SubjectDto>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(new List<SubjectDto>());
|
||||
|
||||
var query = new GetSubjectsQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldReturnCorrectCredits()
|
||||
{
|
||||
// Arrange
|
||||
var subjects = new List<SubjectDto>
|
||||
{
|
||||
new(1, "Math", 3, 1, "Prof A")
|
||||
};
|
||||
|
||||
_subjectRepository.GetAllProjectedAsync(
|
||||
Arg.Any<Expression<Func<Domain.Entities.Subject, SubjectDto>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(subjects);
|
||||
|
||||
var query = new GetSubjectsQuery();
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result[0].Credits.Should().Be(3);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetAvailableSubjectsQueryTests
|
||||
{
|
||||
private readonly IStudentRepository _studentRepository;
|
||||
private readonly ISubjectRepository _subjectRepository;
|
||||
private readonly GetAvailableSubjectsHandler _handler;
|
||||
|
||||
public GetAvailableSubjectsQueryTests()
|
||||
{
|
||||
_studentRepository = Substitute.For<IStudentRepository>();
|
||||
_subjectRepository = Substitute.For<ISubjectRepository>();
|
||||
_handler = new GetAvailableSubjectsHandler(_studentRepository, _subjectRepository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenStudentNotFound_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(999, Arg.Any<CancellationToken>())
|
||||
.Returns((Domain.Entities.Student?)null);
|
||||
|
||||
var query = new GetAvailableSubjectsQuery(999);
|
||||
|
||||
// Act
|
||||
var act = () => _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<Domain.Exceptions.StudentNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenNoEnrollments_AllSubjectsShouldBeAvailable()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Domain.Entities.Student("John", Domain.ValueObjects.Email.Create("john@test.com"));
|
||||
|
||||
var professor = new Domain.Entities.Professor("Prof A");
|
||||
SetEntityId(professor, 1);
|
||||
|
||||
var subjects = new List<Domain.Entities.Subject>
|
||||
{
|
||||
CreateSubject(1, "Math", 1, professor),
|
||||
CreateSubject(2, "Physics", 1, professor)
|
||||
};
|
||||
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
_subjectRepository.GetAllWithProfessorsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(subjects);
|
||||
|
||||
var query = new GetAvailableSubjectsQuery(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
result.Should().OnlyContain(s => s.IsAvailable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenAlreadyEnrolled_SubjectShouldNotBeAvailable()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Domain.Entities.Student("John", Domain.ValueObjects.Email.Create("john@test.com"));
|
||||
SetEntityId(student, 1);
|
||||
|
||||
var professor = new Domain.Entities.Professor("Prof A");
|
||||
SetEntityId(professor, 1);
|
||||
|
||||
var subject = CreateSubject(1, "Math", 1, professor);
|
||||
|
||||
var enrollment = new Domain.Entities.Enrollment(1, 1);
|
||||
SetNavigationProperty(enrollment, "Subject", subject);
|
||||
student.AddEnrollment(enrollment);
|
||||
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
_subjectRepository.GetAllWithProfessorsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Domain.Entities.Subject> { subject });
|
||||
|
||||
var query = new GetAvailableSubjectsQuery(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result[0].IsAvailable.Should().BeFalse();
|
||||
result[0].UnavailableReason.Should().Be("Already enrolled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenSameProfessor_SubjectShouldNotBeAvailable()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Domain.Entities.Student("John", Domain.ValueObjects.Email.Create("john@test.com"));
|
||||
SetEntityId(student, 1);
|
||||
|
||||
var professor = new Domain.Entities.Professor("Prof A");
|
||||
SetEntityId(professor, 1);
|
||||
|
||||
var subject1 = CreateSubject(1, "Math", 1, professor);
|
||||
var subject2 = CreateSubject(2, "Physics", 1, professor);
|
||||
|
||||
var enrollment = new Domain.Entities.Enrollment(1, 1);
|
||||
SetNavigationProperty(enrollment, "Subject", subject1);
|
||||
student.AddEnrollment(enrollment);
|
||||
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
_subjectRepository.GetAllWithProfessorsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Domain.Entities.Subject> { subject1, subject2 });
|
||||
|
||||
var query = new GetAvailableSubjectsQuery(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var physics = result.First(s => s.Name == "Physics");
|
||||
physics.IsAvailable.Should().BeFalse();
|
||||
physics.UnavailableReason.Should().Be("Already have a subject with this professor");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenMaxEnrollmentsReached_AllSubjectsShouldBeUnavailable()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Domain.Entities.Student("John", Domain.ValueObjects.Email.Create("john@test.com"));
|
||||
SetEntityId(student, 1);
|
||||
|
||||
var prof1 = new Domain.Entities.Professor("Prof A");
|
||||
var prof2 = new Domain.Entities.Professor("Prof B");
|
||||
var prof3 = new Domain.Entities.Professor("Prof C");
|
||||
var prof4 = new Domain.Entities.Professor("Prof D");
|
||||
SetEntityId(prof1, 1);
|
||||
SetEntityId(prof2, 2);
|
||||
SetEntityId(prof3, 3);
|
||||
SetEntityId(prof4, 4);
|
||||
|
||||
var subject1 = CreateSubject(1, "Math", 1, prof1);
|
||||
var subject2 = CreateSubject(2, "Physics", 2, prof2);
|
||||
var subject3 = CreateSubject(3, "Chemistry", 3, prof3);
|
||||
var subject4 = CreateSubject(4, "Biology", 4, prof4);
|
||||
|
||||
student.AddEnrollment(CreateEnrollmentWithSubject(1, 1, subject1));
|
||||
student.AddEnrollment(CreateEnrollmentWithSubject(1, 2, subject2));
|
||||
student.AddEnrollment(CreateEnrollmentWithSubject(1, 3, subject3));
|
||||
|
||||
_studentRepository.GetByIdWithEnrollmentsAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(student);
|
||||
_subjectRepository.GetAllWithProfessorsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Domain.Entities.Subject> { subject4 });
|
||||
|
||||
var query = new GetAvailableSubjectsQuery(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result[0].IsAvailable.Should().BeFalse();
|
||||
result[0].UnavailableReason.Should().Be("Maximum 3 subjects reached");
|
||||
}
|
||||
|
||||
private static void SetEntityId<T>(T entity, int id) where T : class
|
||||
{
|
||||
typeof(T).GetProperty("Id")?.SetValue(entity, id);
|
||||
}
|
||||
|
||||
private static void SetNavigationProperty<T>(T entity, string propertyName, object value) where T : class
|
||||
{
|
||||
typeof(T).GetProperty(propertyName)?.SetValue(entity, value);
|
||||
}
|
||||
|
||||
private static Domain.Entities.Subject CreateSubject(int id, string name, int professorId, Domain.Entities.Professor professor)
|
||||
{
|
||||
var subject = new Domain.Entities.Subject(name, professorId);
|
||||
SetEntityId(subject, id);
|
||||
SetNavigationProperty(subject, "Professor", professor);
|
||||
return subject;
|
||||
}
|
||||
|
||||
private static Domain.Entities.Enrollment CreateEnrollmentWithSubject(int studentId, int subjectId, Domain.Entities.Subject subject)
|
||||
{
|
||||
var enrollment = new Domain.Entities.Enrollment(studentId, subjectId);
|
||||
SetNavigationProperty(enrollment, "Subject", subject);
|
||||
return enrollment;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
namespace Application.Tests.Validators;
|
||||
|
||||
using Application.Enrollments.Commands;
|
||||
using Application.Students.Commands;
|
||||
using Domain.Ports.Repositories;
|
||||
using FluentAssertions;
|
||||
using FluentValidation.TestHelper;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
public class CreateStudentValidatorTests
|
||||
{
|
||||
private readonly IStudentRepository _studentRepository;
|
||||
private readonly CreateStudentValidator _validator;
|
||||
|
||||
public CreateStudentValidatorTests()
|
||||
{
|
||||
_studentRepository = Substitute.For<IStudentRepository>();
|
||||
_studentRepository.EmailExistsAsync(Arg.Any<string>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(false);
|
||||
_validator = new CreateStudentValidator(_studentRepository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithValidCommand_ShouldNotHaveErrors()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateStudentCommand("John Doe", "john@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithEmptyName_ShouldHaveError()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateStudentCommand("", "john@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithEmptyEmail_ShouldHaveError()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateStudentCommand("John Doe", "");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.Email);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithInvalidEmail_ShouldHaveError()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateStudentCommand("John Doe", "invalid-email");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.Email);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithExistingEmail_ShouldHaveError()
|
||||
{
|
||||
// Arrange
|
||||
_studentRepository.EmailExistsAsync("existing@example.com", null, Arg.Any<CancellationToken>())
|
||||
.Returns(true);
|
||||
|
||||
var command = new CreateStudentCommand("John Doe", "existing@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.Email)
|
||||
.WithErrorMessage("Email already exists");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithNameTooLong_ShouldHaveError()
|
||||
{
|
||||
// Arrange
|
||||
var longName = new string('A', 101);
|
||||
var command = new CreateStudentCommand(longName, "john@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithNameTooShort_ShouldHaveError()
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateStudentCommand("AB", "john@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.Name)
|
||||
.WithErrorMessage("Name must be at least 3 characters");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("<script>alert('xss')</script>")]
|
||||
[InlineData("John<img src=x onerror=alert(1)>")]
|
||||
[InlineData("javascript:alert(1)")]
|
||||
public async Task Validate_WithDangerousName_ShouldHaveError(string dangerousName)
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateStudentCommand(dangerousName, "john@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("John123")]
|
||||
[InlineData("John@Doe")]
|
||||
[InlineData("John#Doe")]
|
||||
public async Task Validate_WithInvalidCharactersInName_ShouldHaveError(string invalidName)
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateStudentCommand(invalidName, "john@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.Name)
|
||||
.WithErrorMessage("Name contains invalid characters");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("María García")]
|
||||
[InlineData("José O'Brien")]
|
||||
[InlineData("Ana-María López")]
|
||||
[InlineData("Dr. Juan Pérez")]
|
||||
public async Task Validate_WithValidSpecialCharactersInName_ShouldPass(string validName)
|
||||
{
|
||||
// Arrange
|
||||
var command = new CreateStudentCommand(validName, "john@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotHaveValidationErrorFor(x => x.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithEmailTooLong_ShouldHaveError()
|
||||
{
|
||||
// Arrange
|
||||
var longEmail = new string('a', 250) + "@example.com";
|
||||
var command = new CreateStudentCommand("John Doe", longEmail);
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.Email);
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateStudentValidatorTests
|
||||
{
|
||||
private readonly IStudentRepository _studentRepository;
|
||||
private readonly UpdateStudentValidator _validator;
|
||||
|
||||
public UpdateStudentValidatorTests()
|
||||
{
|
||||
_studentRepository = Substitute.For<IStudentRepository>();
|
||||
_studentRepository.EmailExistsAsync(Arg.Any<string>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(false);
|
||||
_validator = new UpdateStudentValidator(_studentRepository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithValidCommand_ShouldNotHaveErrors()
|
||||
{
|
||||
// Arrange
|
||||
var command = new UpdateStudentCommand(1, "John Doe", "john@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithInvalidId_ShouldHaveError()
|
||||
{
|
||||
// Arrange
|
||||
var command = new UpdateStudentCommand(0, "John Doe", "john@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithNegativeId_ShouldHaveError()
|
||||
{
|
||||
// Arrange
|
||||
var command = new UpdateStudentCommand(-1, "John Doe", "john@example.com");
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public class EnrollStudentValidatorTests
|
||||
{
|
||||
private readonly EnrollStudentValidator _validator;
|
||||
|
||||
public EnrollStudentValidatorTests()
|
||||
{
|
||||
_validator = new EnrollStudentValidator();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithValidCommand_ShouldNotHaveErrors()
|
||||
{
|
||||
// Arrange
|
||||
var command = new EnrollStudentCommand(1, 1);
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithInvalidStudentId_ShouldHaveError()
|
||||
{
|
||||
// Arrange
|
||||
var command = new EnrollStudentCommand(0, 1);
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.StudentId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithInvalidSubjectId_ShouldHaveError()
|
||||
{
|
||||
// Arrange
|
||||
var command = new EnrollStudentCommand(1, 0);
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.SubjectId);
|
||||
}
|
||||
}
|
||||
|
||||
public class UnenrollStudentValidatorTests
|
||||
{
|
||||
private readonly UnenrollStudentValidator _validator;
|
||||
|
||||
public UnenrollStudentValidatorTests()
|
||||
{
|
||||
_validator = new UnenrollStudentValidator();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithValidCommand_ShouldNotHaveErrors()
|
||||
{
|
||||
// Arrange
|
||||
var command = new UnenrollStudentCommand(1);
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validate_WithInvalidEnrollmentId_ShouldHaveError()
|
||||
{
|
||||
// Arrange
|
||||
var command = new UnenrollStudentCommand(0);
|
||||
|
||||
// Act
|
||||
var result = await _validator.TestValidateAsync(command);
|
||||
|
||||
// Assert
|
||||
result.ShouldHaveValidationErrorFor(x => x.EnrollmentId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="*" />
|
||||
<PackageReference Include="xunit" Version="*" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="*">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="*" />
|
||||
<PackageReference Include="coverlet.collector" Version="*">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\backend\Domain\Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
namespace Domain.Tests.Entities;
|
||||
|
||||
using Domain.Entities;
|
||||
using Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
public class StudentTests
|
||||
{
|
||||
private static Email ValidEmail => Email.Create("test@example.com");
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithValidData_ShouldCreateStudent()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
|
||||
student.Name.Should().Be("John Doe");
|
||||
student.Email.Value.Should().Be("test@example.com");
|
||||
student.Enrollments.Should().BeEmpty();
|
||||
student.TotalCredits.Should().Be(0);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void Constructor_WithInvalidName_ShouldThrow(string? name)
|
||||
{
|
||||
var act = () => new Student(name!, ValidEmail);
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*Student name is required*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullEmail_ShouldThrow()
|
||||
{
|
||||
var act = () => new Student("John Doe", null!);
|
||||
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanEnroll_WhenNoEnrollments_ShouldReturnTrue()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
|
||||
student.CanEnroll().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanEnroll_WhenMaxEnrollments_ShouldReturnFalse()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
AddEnrollments(student, 3);
|
||||
|
||||
student.CanEnroll().Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasProfessor_WhenStudentHasSubjectWithProfessor_ShouldReturnTrue()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
var subject = new Subject("Math", professorId: 1);
|
||||
var enrollment = new Enrollment(student.Id, subject.Id);
|
||||
SetSubjectOnEnrollment(enrollment, subject);
|
||||
student.AddEnrollment(enrollment);
|
||||
|
||||
student.HasProfessor(1).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasProfessor_WhenNoProfessor_ShouldReturnFalse()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
|
||||
student.HasProfessor(1).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateName_WithValidName_ShouldUpdate()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
|
||||
student.UpdateName("Jane Doe");
|
||||
|
||||
student.Name.Should().Be("Jane Doe");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateEmail_WithValidEmail_ShouldUpdate()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
var newEmail = Email.Create("new@example.com");
|
||||
|
||||
student.UpdateEmail(newEmail);
|
||||
|
||||
student.Email.Value.Should().Be("new@example.com");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TotalCredits_ShouldSumEnrollmentCredits()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
AddEnrollments(student, 2);
|
||||
|
||||
student.TotalCredits.Should().Be(6);
|
||||
}
|
||||
|
||||
private static void AddEnrollments(Student student, int count)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var subject = new Subject($"Subject{i}", professorId: i + 1);
|
||||
var enrollment = new Enrollment(student.Id, subject.Id);
|
||||
SetSubjectOnEnrollment(enrollment, subject);
|
||||
student.AddEnrollment(enrollment);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetSubjectOnEnrollment(Enrollment enrollment, Subject subject)
|
||||
{
|
||||
var subjectProperty = typeof(Enrollment).GetProperty("Subject")!;
|
||||
subjectProperty.SetValue(enrollment, subject);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
namespace Domain.Tests.Services;
|
||||
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using Domain.Services;
|
||||
using Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
public class EnrollmentDomainServiceTests
|
||||
{
|
||||
private readonly EnrollmentDomainService _sut = new();
|
||||
private static Email ValidEmail => Email.Create("test@example.com");
|
||||
|
||||
[Fact]
|
||||
public void ValidateEnrollment_WhenValid_ShouldNotThrow()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
var subject = new Subject("Math", professorId: 1);
|
||||
|
||||
var act = () => _sut.ValidateEnrollment(student, subject);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateEnrollment_WhenMaxEnrollmentsReached_ShouldThrow()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
AddEnrollmentsWithDifferentProfessors(student, 3);
|
||||
var subject = new Subject("Physics", professorId: 99);
|
||||
|
||||
var act = () => _sut.ValidateEnrollment(student, subject);
|
||||
|
||||
act.Should().Throw<MaxEnrollmentsExceededException>()
|
||||
.Which.Code.Should().Be("MAX_ENROLLMENTS");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateEnrollment_WhenSameProfessor_ShouldThrow()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
var existingSubject = new Subject("Math", professorId: 1);
|
||||
AddEnrollmentWithSubject(student, existingSubject);
|
||||
|
||||
var newSubject = new Subject("Algebra", professorId: 1);
|
||||
|
||||
var act = () => _sut.ValidateEnrollment(student, newSubject);
|
||||
|
||||
act.Should().Throw<SameProfessorConstraintException>()
|
||||
.Which.Code.Should().Be("SAME_PROFESSOR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateEnrollment_WhenDuplicateSubject_ShouldThrow()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
var subject = new Subject("Math", professorId: 1);
|
||||
SetSubjectId(subject, 10);
|
||||
AddEnrollmentWithSubject(student, subject);
|
||||
|
||||
// Same subject ID but different professor to bypass professor check
|
||||
var duplicateSubject = new Subject("Math", professorId: 2);
|
||||
SetSubjectId(duplicateSubject, 10);
|
||||
|
||||
var act = () => _sut.ValidateEnrollment(student, duplicateSubject);
|
||||
|
||||
act.Should().Throw<DuplicateEnrollmentException>()
|
||||
.Which.Code.Should().Be("DUPLICATE_ENROLLMENT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateEnrollment_WhenValid_ShouldCreateAndAddToStudent()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
var subject = new Subject("Math", professorId: 1);
|
||||
|
||||
var enrollment = _sut.CreateEnrollment(student, subject);
|
||||
|
||||
enrollment.Should().NotBeNull();
|
||||
student.Enrollments.Should().Contain(enrollment);
|
||||
enrollment.StudentId.Should().Be(student.Id);
|
||||
enrollment.SubjectId.Should().Be(subject.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateEnrollment_WhenInvalid_ShouldNotAddToStudent()
|
||||
{
|
||||
var student = new Student("John Doe", ValidEmail);
|
||||
AddEnrollmentsWithDifferentProfessors(student, 3);
|
||||
var subject = new Subject("Physics", professorId: 99);
|
||||
|
||||
var act = () => _sut.CreateEnrollment(student, subject);
|
||||
|
||||
act.Should().Throw<MaxEnrollmentsExceededException>();
|
||||
student.Enrollments.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
private static void AddEnrollmentsWithDifferentProfessors(Student student, int count)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var subject = new Subject($"Subject{i}", professorId: i + 1);
|
||||
SetSubjectId(subject, i + 1);
|
||||
AddEnrollmentWithSubject(student, subject);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddEnrollmentWithSubject(Student student, Subject subject)
|
||||
{
|
||||
var enrollment = new Enrollment(student.Id, subject.Id);
|
||||
SetSubjectOnEnrollment(enrollment, subject);
|
||||
student.AddEnrollment(enrollment);
|
||||
}
|
||||
|
||||
private static void SetSubjectOnEnrollment(Enrollment enrollment, Subject subject)
|
||||
{
|
||||
typeof(Enrollment).GetProperty("Subject")!.SetValue(enrollment, subject);
|
||||
}
|
||||
|
||||
private static void SetSubjectId(Subject subject, int id)
|
||||
{
|
||||
typeof(Subject).GetProperty("Id")!.SetValue(subject, id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
namespace Domain.Tests.ValueObjects;
|
||||
|
||||
using Domain.ValueObjects;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
public class EmailTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("test@example.com")]
|
||||
[InlineData("user.name@domain.org")]
|
||||
[InlineData("UPPER@CASE.COM")]
|
||||
public void Create_WithValidEmail_ShouldSucceed(string email)
|
||||
{
|
||||
var result = Email.Create(email);
|
||||
|
||||
result.Value.Should().Be(email.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void Create_WithEmptyEmail_ShouldThrow(string? email)
|
||||
{
|
||||
var act = () => Email.Create(email!);
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*Email is required*");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("notanemail")]
|
||||
[InlineData("missing@domain")]
|
||||
[InlineData("@nodomain.com")]
|
||||
[InlineData("spaces in@email.com")]
|
||||
public void Create_WithInvalidFormat_ShouldThrow(string email)
|
||||
{
|
||||
var act = () => Email.Create(email);
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*Invalid email format*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithSameValue_ShouldBeEqual()
|
||||
{
|
||||
var email1 = Email.Create("test@example.com");
|
||||
var email2 = Email.Create("TEST@EXAMPLE.COM");
|
||||
|
||||
email1.Should().Be(email2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToString_ShouldWork()
|
||||
{
|
||||
var email = Email.Create("test@example.com");
|
||||
|
||||
string value = email;
|
||||
|
||||
value.Should().Be("test@example.com");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
namespace Integration.Tests;
|
||||
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Adapters.Driven.Persistence.Repositories;
|
||||
using Application.Enrollments.Commands;
|
||||
using Application.Students.Commands;
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using Domain.Services;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class EnrollmentFlowTests : IDisposable
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
private readonly StudentRepository _studentRepository;
|
||||
private readonly SubjectRepository _subjectRepository;
|
||||
private readonly EnrollmentRepository _enrollmentRepository;
|
||||
private readonly UnitOfWork _unitOfWork;
|
||||
|
||||
public EnrollmentFlowTests()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
_context = new AppDbContext(options);
|
||||
_studentRepository = new StudentRepository(_context);
|
||||
_subjectRepository = new SubjectRepository(_context);
|
||||
_enrollmentRepository = new EnrollmentRepository(_context);
|
||||
_unitOfWork = new UnitOfWork(_context);
|
||||
|
||||
SeedData();
|
||||
}
|
||||
|
||||
private void SeedData()
|
||||
{
|
||||
var prof1 = new Professor("Dr. Smith");
|
||||
var prof2 = new Professor("Dr. Jones");
|
||||
_context.Professors.AddRange(prof1, prof2);
|
||||
_context.SaveChanges();
|
||||
|
||||
var subjects = new[]
|
||||
{
|
||||
new Subject("Mathematics", prof1.Id),
|
||||
new Subject("Physics", prof1.Id),
|
||||
new Subject("Chemistry", prof2.Id),
|
||||
new Subject("Biology", prof2.Id)
|
||||
};
|
||||
_context.Subjects.AddRange(subjects);
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateStudent_ShouldPersistToDatabase()
|
||||
{
|
||||
// Arrange
|
||||
var handler = new CreateStudentHandler(_studentRepository, _unitOfWork);
|
||||
var command = new CreateStudentCommand("John Doe", "john@example.com");
|
||||
|
||||
// Act
|
||||
var result = await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Name.Should().Be("John Doe");
|
||||
|
||||
var savedStudent = await _context.Students.FirstOrDefaultAsync(s => s.Email.Value == "john@example.com");
|
||||
savedStudent.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrollStudent_ShouldCreateEnrollment()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Student("Jane Doe", Domain.ValueObjects.Email.Create("jane@example.com"));
|
||||
_context.Students.Add(student);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var subject = await _context.Subjects.FirstAsync();
|
||||
var enrollmentService = new EnrollmentDomainService();
|
||||
var handler = new EnrollStudentHandler(
|
||||
_studentRepository, _subjectRepository, _enrollmentRepository,
|
||||
enrollmentService, _unitOfWork);
|
||||
|
||||
var command = new EnrollStudentCommand(student.Id, subject.Id);
|
||||
|
||||
// Act
|
||||
var result = await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
|
||||
var enrollment = await _context.Enrollments
|
||||
.FirstOrDefaultAsync(e => e.StudentId == student.Id && e.SubjectId == subject.Id);
|
||||
enrollment.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrollStudent_WhenMaxEnrollments_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Student("Max Student", Domain.ValueObjects.Email.Create("max@example.com"));
|
||||
_context.Students.Add(student);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var subjects = await _context.Subjects.Take(4).ToListAsync();
|
||||
var enrollmentService = new EnrollmentDomainService();
|
||||
|
||||
// Enroll in 3 subjects (max allowed) with different professors
|
||||
var subjectsWithDiffProfs = subjects.GroupBy(s => s.ProfessorId).Select(g => g.First()).Take(3).ToList();
|
||||
|
||||
if (subjectsWithDiffProfs.Count < 3)
|
||||
{
|
||||
// If we don't have 3 different professors, just use first 3 subjects
|
||||
subjectsWithDiffProfs = subjects.Take(3).ToList();
|
||||
}
|
||||
|
||||
foreach (var subject in subjectsWithDiffProfs.Take(2))
|
||||
{
|
||||
_context.Enrollments.Add(new Enrollment(student.Id, subject.Id));
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Need to reload student with enrollments
|
||||
_context.Entry(student).State = EntityState.Detached;
|
||||
|
||||
var handler = new EnrollStudentHandler(
|
||||
_studentRepository, _subjectRepository, _enrollmentRepository,
|
||||
enrollmentService, _unitOfWork);
|
||||
|
||||
// Add third enrollment
|
||||
var thirdSubject = subjectsWithDiffProfs.Skip(2).First();
|
||||
var thirdCommand = new EnrollStudentCommand(student.Id, thirdSubject.Id);
|
||||
await handler.Handle(thirdCommand, CancellationToken.None);
|
||||
|
||||
// Try fourth enrollment - should fail
|
||||
var fourthSubject = subjects.First(s => !subjectsWithDiffProfs.Contains(s));
|
||||
var fourthCommand = new EnrollStudentCommand(student.Id, fourthSubject.Id);
|
||||
|
||||
// Act
|
||||
var act = () => handler.Handle(fourthCommand, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<MaxEnrollmentsExceededException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrollStudent_WhenSameProfessor_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Student("Prof Test", Domain.ValueObjects.Email.Create("prof@example.com"));
|
||||
_context.Students.Add(student);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Get all subjects and find two with same professor in memory
|
||||
var allSubjects = await _context.Subjects.ToListAsync();
|
||||
var subjectsSameProf = allSubjects
|
||||
.GroupBy(s => s.ProfessorId)
|
||||
.Where(g => g.Count() >= 2)
|
||||
.SelectMany(g => g.Take(2))
|
||||
.ToList();
|
||||
|
||||
if (subjectsSameProf.Count < 2)
|
||||
{
|
||||
// Skip test if we don't have subjects with same professor
|
||||
return;
|
||||
}
|
||||
|
||||
var enrollmentService = new EnrollmentDomainService();
|
||||
var handler = new EnrollStudentHandler(
|
||||
_studentRepository, _subjectRepository, _enrollmentRepository,
|
||||
enrollmentService, _unitOfWork);
|
||||
|
||||
// Enroll first subject
|
||||
var firstCommand = new EnrollStudentCommand(student.Id, subjectsSameProf[0].Id);
|
||||
await handler.Handle(firstCommand, CancellationToken.None);
|
||||
|
||||
// Try second subject with same professor
|
||||
var secondCommand = new EnrollStudentCommand(student.Id, subjectsSameProf[1].Id);
|
||||
|
||||
// Act
|
||||
var act = () => handler.Handle(secondCommand, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<SameProfessorConstraintException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteStudent_ShouldRemoveFromDatabase()
|
||||
{
|
||||
// Arrange
|
||||
var student = new Student("Delete Me", Domain.ValueObjects.Email.Create("delete@example.com"));
|
||||
_context.Students.Add(student);
|
||||
await _context.SaveChangesAsync();
|
||||
var studentId = student.Id;
|
||||
|
||||
var handler = new DeleteStudentHandler(_studentRepository, _unitOfWork);
|
||||
var command = new DeleteStudentCommand(studentId);
|
||||
|
||||
// Act
|
||||
var result = await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
var deletedStudent = await _context.Students.FindAsync(studentId);
|
||||
deletedStudent.Should().BeNull();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
<PackageReference Include="FluentAssertions" Version="*" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\backend\Application\Application.csproj" />
|
||||
<ProjectReference Include="..\..\src\backend\Adapters\Driven\Persistence\Adapters.Driven.Persistence.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in New Issue