Verified every finding against the code, then fixed correctness (redirects, advancement reset, duplicate heats, high-jump attempt entry, placement ranking with tie handling, delete-dependency 500s), security (secrets out of config, config-driven admin seed, login lockout, role authorization with school scoping, heat-time IDOR, forwarded headers, last-admin guards), schema integrity (nullable+filtered ExistingStudentId, unique indexes for rounds/heats/bar heights via SchemaIntegrityFixes migration), performance (N+1 removal in high jump/reports/standings/dashboard, SQL-side student paging), and hygiene (duplicate notifications, auto-dismiss scope, local bootstrap-icons, orphaned files, test-data.sql tournament creation). FluentValidation is now registered; AutoMapper bumped to 14.0.0 (advisory fully patched only in licence-changed 15.1.1 — documented). 11 new tests; 63/63 passing. Credential rotation and deploy-time DB_CONNECTION_STRING are required manual follow-ups, documented in bug-fixes.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
358 lines
11 KiB
C#
358 lines
11 KiB
C#
using Moq;
|
|
using SportsDivision.Application.Services;
|
|
using SportsDivision.Domain.Entities;
|
|
using SportsDivision.Domain.Enums;
|
|
using SportsDivision.Domain.Interfaces;
|
|
|
|
namespace SportsDivision.Application.Tests;
|
|
|
|
public class RegistrationEligibilityTests
|
|
{
|
|
private readonly Mock<IUnitOfWork> _mockUow;
|
|
private readonly RegistrationService _service;
|
|
|
|
public RegistrationEligibilityTests()
|
|
{
|
|
_mockUow = new Mock<IUnitOfWork>();
|
|
_service = new RegistrationService(_mockUow.Object, null!);
|
|
}
|
|
|
|
private void SetupTournamentEventLevel(TournamentEventLevel tel)
|
|
{
|
|
_mockUow.Setup(u => u.TournamentEventLevels.GetWithRegistrationsAsync(It.IsAny<int>()))
|
|
.ReturnsAsync(tel);
|
|
}
|
|
|
|
private void SetupStudent(Student student)
|
|
{
|
|
_mockUow.Setup(u => u.Students.GetByIdAsync(student.StudentId))
|
|
.ReturnsAsync(student);
|
|
}
|
|
|
|
private void SetupSchool(School school)
|
|
{
|
|
_mockUow.Setup(u => u.Schools.GetByIdAsync(school.SchoolId))
|
|
.ReturnsAsync(school);
|
|
}
|
|
|
|
private void SetupNotAlreadyRegistered()
|
|
{
|
|
_mockUow.Setup(u => u.EventRegistrations.IsStudentRegisteredAsync(It.IsAny<int>(), It.IsAny<int>()))
|
|
.ReturnsAsync(false);
|
|
}
|
|
|
|
private void SetupAlreadyRegistered()
|
|
{
|
|
_mockUow.Setup(u => u.EventRegistrations.IsStudentRegisteredAsync(It.IsAny<int>(), It.IsAny<int>()))
|
|
.ReturnsAsync(true);
|
|
}
|
|
|
|
private TournamentEventLevel CreateTel(Sex sex = Sex.Male, SchoolLevel schoolLevel = SchoolLevel.Secondary,
|
|
int? maxAge = null, bool isAgeBased = false, bool ageWaived = false)
|
|
{
|
|
return new TournamentEventLevel
|
|
{
|
|
TournamentEventLevelId = 1,
|
|
EventLevel = new EventLevel
|
|
{
|
|
Sex = sex,
|
|
SchoolLevel = schoolLevel,
|
|
MaxAge = maxAge,
|
|
IsAgeBased = isAgeBased
|
|
},
|
|
AgeRestrictionWaived = ageWaived,
|
|
Tournament = new Tournament
|
|
{
|
|
StartDate = new DateOnly(2024, 6, 1)
|
|
},
|
|
Registrations = new List<EventRegistration>()
|
|
};
|
|
}
|
|
|
|
private Student CreateStudent(Sex sex = Sex.Male, int schoolId = 1, DateOnly? dob = null)
|
|
{
|
|
return new Student
|
|
{
|
|
StudentId = 1,
|
|
FirstName = "Test",
|
|
LastName = "Student",
|
|
Sex = sex,
|
|
SchoolId = schoolId,
|
|
DateOfBirth = dob ?? new DateOnly(2010, 1, 1)
|
|
};
|
|
}
|
|
|
|
private School CreateSchool(SchoolLevel level = SchoolLevel.Secondary)
|
|
{
|
|
return new School
|
|
{
|
|
SchoolId = 1,
|
|
Name = "Test School",
|
|
SchoolLevel = level,
|
|
ZoneId = 1
|
|
};
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_EligibleStudent_ReturnsTrue()
|
|
{
|
|
var tel = CreateTel(Sex.Male, SchoolLevel.Secondary);
|
|
var student = CreateStudent(Sex.Male);
|
|
var school = CreateSchool(SchoolLevel.Secondary);
|
|
|
|
SetupTournamentEventLevel(tel);
|
|
SetupStudent(student);
|
|
SetupSchool(school);
|
|
SetupNotAlreadyRegistered();
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 1);
|
|
|
|
Assert.True(isEligible);
|
|
Assert.Null(reason);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_SexMismatch_ReturnsFalse()
|
|
{
|
|
var tel = CreateTel(Sex.Male, SchoolLevel.Secondary);
|
|
var student = CreateStudent(Sex.Female);
|
|
|
|
SetupTournamentEventLevel(tel);
|
|
SetupStudent(student);
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 1);
|
|
|
|
Assert.False(isEligible);
|
|
Assert.Contains("sex", reason!, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_SecondaryStudentInPrimaryEvent_ReturnsFalse()
|
|
{
|
|
var tel = CreateTel(Sex.Male, SchoolLevel.Primary);
|
|
var student = CreateStudent(Sex.Male);
|
|
var school = CreateSchool(SchoolLevel.Secondary);
|
|
|
|
SetupTournamentEventLevel(tel);
|
|
SetupStudent(student);
|
|
SetupSchool(school);
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 1);
|
|
|
|
Assert.False(isEligible);
|
|
Assert.Contains("level", reason!, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_PrimaryStudentInSecondaryEvent_CompeteUp_ReturnsTrue()
|
|
{
|
|
var tel = CreateTel(Sex.Male, SchoolLevel.Secondary);
|
|
var student = CreateStudent(Sex.Male);
|
|
var school = CreateSchool(SchoolLevel.Primary);
|
|
|
|
SetupTournamentEventLevel(tel);
|
|
SetupStudent(student);
|
|
SetupSchool(school);
|
|
SetupNotAlreadyRegistered();
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 1);
|
|
|
|
Assert.True(isEligible);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_AgeExceedsMax_ReturnsFalse()
|
|
{
|
|
var tel = CreateTel(Sex.Male, SchoolLevel.Secondary, maxAge: 13, isAgeBased: true);
|
|
// Student born 2010-01-01, tournament June 2024 → age 14
|
|
var student = CreateStudent(Sex.Male, dob: new DateOnly(2010, 1, 1));
|
|
var school = CreateSchool(SchoolLevel.Secondary);
|
|
|
|
SetupTournamentEventLevel(tel);
|
|
SetupStudent(student);
|
|
SetupSchool(school);
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 1);
|
|
|
|
Assert.False(isEligible);
|
|
Assert.Contains("age", reason!, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_AgeWithinLimit_ReturnsTrue()
|
|
{
|
|
var tel = CreateTel(Sex.Male, SchoolLevel.Secondary, maxAge: 15, isAgeBased: true);
|
|
// Student born 2010-01-01, tournament June 2024 → age 14
|
|
var student = CreateStudent(Sex.Male, dob: new DateOnly(2010, 1, 1));
|
|
var school = CreateSchool(SchoolLevel.Secondary);
|
|
|
|
SetupTournamentEventLevel(tel);
|
|
SetupStudent(student);
|
|
SetupSchool(school);
|
|
SetupNotAlreadyRegistered();
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 1);
|
|
|
|
Assert.True(isEligible);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_AgeExceedsButWaived_ReturnsTrue()
|
|
{
|
|
var tel = CreateTel(Sex.Male, SchoolLevel.Secondary, maxAge: 13, isAgeBased: true, ageWaived: true);
|
|
// Student age 14, max is 13, but waived
|
|
var student = CreateStudent(Sex.Male, dob: new DateOnly(2010, 1, 1));
|
|
var school = CreateSchool(SchoolLevel.Secondary);
|
|
|
|
SetupTournamentEventLevel(tel);
|
|
SetupStudent(student);
|
|
SetupSchool(school);
|
|
SetupNotAlreadyRegistered();
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 1);
|
|
|
|
Assert.True(isEligible);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_AlreadyRegistered_ReturnsFalse()
|
|
{
|
|
var tel = CreateTel(Sex.Male, SchoolLevel.Secondary);
|
|
var student = CreateStudent(Sex.Male);
|
|
var school = CreateSchool(SchoolLevel.Secondary);
|
|
|
|
SetupTournamentEventLevel(tel);
|
|
SetupStudent(student);
|
|
SetupSchool(school);
|
|
SetupAlreadyRegistered();
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 1);
|
|
|
|
Assert.False(isEligible);
|
|
Assert.Contains("already registered", reason!, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_InactiveStudent_ReturnsFalse()
|
|
{
|
|
var tel = CreateTel(Sex.Male, SchoolLevel.Secondary);
|
|
var student = CreateStudent(Sex.Male);
|
|
student.IsActive = false;
|
|
|
|
SetupTournamentEventLevel(tel);
|
|
SetupStudent(student);
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 1);
|
|
|
|
Assert.False(isEligible);
|
|
Assert.Contains("deactivated", reason!, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_InactiveSchool_ReturnsFalse()
|
|
{
|
|
var tel = CreateTel(Sex.Male, SchoolLevel.Secondary);
|
|
var student = CreateStudent(Sex.Male);
|
|
var school = CreateSchool(SchoolLevel.Secondary);
|
|
school.IsActive = false;
|
|
|
|
SetupTournamentEventLevel(tel);
|
|
SetupStudent(student);
|
|
SetupSchool(school);
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 1);
|
|
|
|
Assert.False(isEligible);
|
|
Assert.Contains("school is deactivated", reason!, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_CompletedTournament_ReturnsFalse()
|
|
{
|
|
var tel = CreateTel(Sex.Male, SchoolLevel.Secondary);
|
|
tel.Tournament.Status = TournamentStatus.Completed;
|
|
var student = CreateStudent(Sex.Male);
|
|
|
|
SetupTournamentEventLevel(tel);
|
|
SetupStudent(student);
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 1);
|
|
|
|
Assert.False(isEligible);
|
|
Assert.Contains("completed", reason!, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_ArchivedTournament_ReturnsFalse()
|
|
{
|
|
var tel = CreateTel(Sex.Male, SchoolLevel.Secondary);
|
|
tel.Tournament.IsArchived = true;
|
|
var student = CreateStudent(Sex.Male);
|
|
|
|
SetupTournamentEventLevel(tel);
|
|
SetupStudent(student);
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 1);
|
|
|
|
Assert.False(isEligible);
|
|
Assert.Contains("archived", reason!, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RegisterStudent_RelayOnlyRegistration_ThrowsInsteadOfCrashing()
|
|
{
|
|
var dto = new Application.DTOs.EventRegistrationCreateDto
|
|
{
|
|
TournamentEventLevelId = 1,
|
|
StudentId = null,
|
|
RelayTeamId = 5
|
|
};
|
|
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
|
() => _service.RegisterStudentAsync(dto, "tester"));
|
|
Assert.Contains("relay", ex.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RegisterStudent_NeitherStudentNorRelay_Throws()
|
|
{
|
|
var dto = new Application.DTOs.EventRegistrationCreateDto
|
|
{
|
|
TournamentEventLevelId = 1,
|
|
StudentId = null,
|
|
RelayTeamId = null
|
|
};
|
|
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
|
() => _service.RegisterStudentAsync(dto, "tester"));
|
|
Assert.Contains("student", ex.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_TelNotFound_ReturnsFalse()
|
|
{
|
|
_mockUow.Setup(u => u.TournamentEventLevels.GetWithRegistrationsAsync(It.IsAny<int>()))
|
|
.ReturnsAsync((TournamentEventLevel?)null);
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(999, 1);
|
|
|
|
Assert.False(isEligible);
|
|
Assert.Contains("not found", reason!, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CheckEligibility_StudentNotFound_ReturnsFalse()
|
|
{
|
|
var tel = CreateTel();
|
|
SetupTournamentEventLevel(tel);
|
|
|
|
_mockUow.Setup(u => u.Students.GetByIdAsync(It.IsAny<int>()))
|
|
.ReturnsAsync((Student?)null);
|
|
|
|
var (isEligible, reason) = await _service.CheckEligibilityAsync(1, 999);
|
|
|
|
Assert.False(isEligible);
|
|
Assert.Contains("not found", reason!, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|