Fix all 45 audited bugs from bug-fixes.md
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>
This commit is contained in:
145
tests/SportsDivision.Application.Tests/ScoringPlacementTests.cs
Normal file
145
tests/SportsDivision.Application.Tests/ScoringPlacementTests.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using Moq;
|
||||
using SportsDivision.Application.Services;
|
||||
using SportsDivision.Domain.Entities;
|
||||
using SportsDivision.Domain.Enums;
|
||||
using SportsDivision.Domain.Interfaces;
|
||||
|
||||
namespace SportsDivision.Application.Tests;
|
||||
|
||||
public class ScoringPlacementTests
|
||||
{
|
||||
private readonly Mock<IUnitOfWork> _mockUow;
|
||||
private readonly ScoringService _service;
|
||||
|
||||
public ScoringPlacementTests()
|
||||
{
|
||||
_mockUow = new Mock<IUnitOfWork>();
|
||||
_service = new ScoringService(_mockUow.Object, null!);
|
||||
}
|
||||
|
||||
private void Setup(EventCategory category, List<Score> scores)
|
||||
{
|
||||
_mockUow.Setup(u => u.TournamentEventLevels.GetWithRegistrationsAsync(It.IsAny<int>()))
|
||||
.ReturnsAsync(new TournamentEventLevel
|
||||
{
|
||||
TournamentEventLevelId = 1,
|
||||
Event = new Event { Category = category }
|
||||
});
|
||||
_mockUow.Setup(u => u.Scores.GetByTournamentEventLevelAsync(It.IsAny<int>()))
|
||||
.ReturnsAsync(scores);
|
||||
_mockUow.Setup(u => u.PlacementPointConfigs.GetAllAsync())
|
||||
.ReturnsAsync(new List<PlacementPointConfig>
|
||||
{
|
||||
new() { Placement = 1, Points = 10 },
|
||||
new() { Placement = 2, Points = 8 },
|
||||
new() { Placement = 3, Points = 6 },
|
||||
new() { Placement = 4, Points = 5 },
|
||||
});
|
||||
_mockUow.Setup(u => u.SaveChangesAsync()).ReturnsAsync(1);
|
||||
}
|
||||
|
||||
private static Score MakeScore(int regId, decimal performance) =>
|
||||
new() { EventRegistrationId = regId, RawPerformance = performance };
|
||||
|
||||
[Fact]
|
||||
public async Task CalculatePlacements_FieldEvent_RanksByPerformanceDescending()
|
||||
{
|
||||
// Points-based ranking used to assign arbitrary places when no scoring
|
||||
// constant existed (all CalculatedPoints = 0); ranking is now on the mark.
|
||||
var s1 = MakeScore(1, 10.00m);
|
||||
var s2 = MakeScore(2, 12.50m);
|
||||
var s3 = MakeScore(3, 11.00m);
|
||||
Setup(EventCategory.Field, new List<Score> { s1, s2, s3 });
|
||||
|
||||
await _service.CalculatePlacementsAsync(1);
|
||||
|
||||
Assert.Equal(1, s2.Placement);
|
||||
Assert.Equal(2, s3.Placement);
|
||||
Assert.Equal(3, s1.Placement);
|
||||
Assert.Equal(10, s2.PlacementPoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CalculatePlacements_TrackEvent_RanksAscending()
|
||||
{
|
||||
var s1 = MakeScore(1, 11.20m);
|
||||
var s2 = MakeScore(2, 10.90m);
|
||||
Setup(EventCategory.Track, new List<Score> { s1, s2 });
|
||||
|
||||
await _service.CalculatePlacementsAsync(1);
|
||||
|
||||
Assert.Equal(1, s2.Placement);
|
||||
Assert.Equal(2, s1.Placement);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CalculatePlacements_Ties_SharePlaceAndSplitPooledPoints()
|
||||
{
|
||||
// Two identical throws share 1st (competition ranking 1, 1, 3) and split
|
||||
// the pooled points for places 1 and 2: (10 + 8) / 2 = 9.
|
||||
var s1 = MakeScore(1, 12.50m);
|
||||
var s2 = MakeScore(2, 12.50m);
|
||||
var s3 = MakeScore(3, 11.00m);
|
||||
Setup(EventCategory.Field, new List<Score> { s1, s2, s3 });
|
||||
|
||||
await _service.CalculatePlacementsAsync(1);
|
||||
|
||||
Assert.Equal(1, s1.Placement);
|
||||
Assert.Equal(1, s2.Placement);
|
||||
Assert.Equal(9, s1.PlacementPoints);
|
||||
Assert.Equal(9, s2.PlacementPoints);
|
||||
Assert.Equal(3, s3.Placement);
|
||||
Assert.Equal(6, s3.PlacementPoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CalculatePlacements_ZeroMark_GetsNoPlacementAndOldPlacementCleared()
|
||||
{
|
||||
var foul = MakeScore(1, 0m);
|
||||
foul.Placement = 1; // stale from an earlier calculation
|
||||
foul.PlacementPoints = 10;
|
||||
var valid = MakeScore(2, 9.50m);
|
||||
Setup(EventCategory.Field, new List<Score> { foul, valid });
|
||||
|
||||
await _service.CalculatePlacementsAsync(1);
|
||||
|
||||
Assert.Null(foul.Placement);
|
||||
Assert.Equal(0, foul.PlacementPoints);
|
||||
Assert.Equal(1, valid.Placement);
|
||||
}
|
||||
}
|
||||
|
||||
public class HeatAdvancementResetTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CalculateAdvancement_ClearsStaleFlagsBeforeRecalculating()
|
||||
{
|
||||
// An athlete who advanced in a previous calculation but no longer qualifies
|
||||
// after a time correction must lose the flag on recalculation.
|
||||
var stale = new HeatLane { HeatLaneId = 1, Time = 12.5m, IsAdvanced = true, AdvanceReason = AdvanceReason.TopN };
|
||||
var faster = new HeatLane { HeatLaneId = 2, Time = 11.0m };
|
||||
var round = new Round
|
||||
{
|
||||
RoundId = 1,
|
||||
AdvanceTopN = 1,
|
||||
AdvanceFastestLosers = 0,
|
||||
Heats = new List<Heat>
|
||||
{
|
||||
new() { HeatId = 1, HeatLanes = new List<HeatLane> { stale, faster } }
|
||||
}
|
||||
};
|
||||
|
||||
var mockUow = new Mock<IUnitOfWork>();
|
||||
mockUow.Setup(u => u.Rounds.GetWithHeatsAsync(1)).ReturnsAsync(round);
|
||||
mockUow.Setup(u => u.HeatLanes.Update(It.IsAny<HeatLane>()));
|
||||
mockUow.Setup(u => u.SaveChangesAsync()).ReturnsAsync(1);
|
||||
var service = new HeatManagementService(mockUow.Object, null!);
|
||||
|
||||
await service.CalculateAdvancementAsync(1);
|
||||
|
||||
Assert.False(stale.IsAdvanced);
|
||||
Assert.Null(stale.AdvanceReason);
|
||||
Assert.True(faster.IsAdvanced);
|
||||
Assert.Equal(AdvanceReason.TopN, faster.AdvanceReason);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user