Add testing for validation

This commit is contained in:
2025-12-14 11:08:04 -05:00
parent 73ad730b38
commit c77d2117cd
8 changed files with 1713 additions and 5 deletions
+11 -4
View File
@@ -148,7 +148,12 @@ public class ValidationConfiguration
{
try
{
return JsonSerializer.Deserialize<ValidationConfiguration>(json) ?? Default;
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }
};
return JsonSerializer.Deserialize<ValidationConfiguration>(json, options) ?? Default;
}
catch
{
@@ -183,10 +188,12 @@ public class ValidationConfiguration
/// <param name="path">Path where the JSON file should be saved</param>
public async Task SaveToFileAsync(string path)
{
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions
var options = new JsonSerializerOptions
{
WriteIndented = true
});
WriteIndented = true,
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }
};
var json = JsonSerializer.Serialize(this, options);
await File.WriteAllTextAsync(path, json);
}
}
+1
View File
@@ -147,6 +147,7 @@ public class StudentBuilder
Rank = rank
};
student.EventRankings.Add(ranking);
student.RankedEvents.Add(eventDef); // Also add to RankedEvents for validation rules
}
return student;
+3 -1
View File
@@ -21,10 +21,12 @@ public class TeamBuilder
return this;
}
public TeamBuilder WithStudent(Student student)
public TeamBuilder WithStudent(Student student, bool isCaptain = false)
{
if (!_students.Contains(student))
_students.Add(student);
if (isCaptain)
_captain = student;
return this;
}
@@ -0,0 +1,440 @@
using Core.Entities;
using Core.Validation;
using Core.Validation.Rules.StudentAssignmentRules;
using Tests.Builders;
namespace Tests.Validation.Rules;
[TestFixture]
public class StudentAssignmentRulesTests
{
private Student _testStudent;
[SetUp]
public void SetUp()
{
BuilderExtensions.ResetAllBuilders();
_testStudent = StudentBuilder.Default().WithName("Test", "Student").Build();
}
#region TooFewEventsRule Tests
[Test]
public void TooFewEventsRule_AboveMinimum_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { MinRecommendedEvents = 2 };
var rule = new TooFewEventsRule();
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = new List<EventDefinition>
{
EventDefinitionBuilder.Individual("Flight").Build(),
EventDefinitionBuilder.Individual("Coding").Build(),
EventDefinitionBuilder.Individual("Speech").Build()
}
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void TooFewEventsRule_BelowMinimum_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
MinRecommendedEvents = 3,
MinCriticalEvents = 1,
EventCountSeverity = ValidationSeverity.Warning
};
var rule = new TooFewEventsRule();
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = new List<EventDefinition>
{
EventDefinitionBuilder.Individual("Flight").Build()
}
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Code, Is.EqualTo("TOO_FEW_EVENTS"));
Assert.That(warning.Message, Does.Contain("1 events"));
Assert.That(warning.Message, Does.Contain("min recommended: 3"));
Assert.That(warning.Severity, Is.EqualTo(ValidationSeverity.Warning));
}
[Test]
public void TooFewEventsRule_BelowCritical_EscalatesToError()
{
// Arrange
var config = new ValidationConfiguration
{
MinRecommendedEvents = 3,
MinCriticalEvents = 2,
EventCountSeverity = ValidationSeverity.Warning
};
var rule = new TooFewEventsRule();
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = new List<EventDefinition>
{
EventDefinitionBuilder.Individual("Flight").Build()
}
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Severity, Is.EqualTo(ValidationSeverity.Error),
"Should escalate to Error when below critical threshold");
}
#endregion
#region TooManyEventsRule Tests
[Test]
public void TooManyEventsRule_BelowMaximum_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { MaxRecommendedEvents = 4 };
var rule = new TooManyEventsRule();
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = new List<EventDefinition>
{
EventDefinitionBuilder.Individual("Event1").Build(),
EventDefinitionBuilder.Individual("Event2").Build(),
EventDefinitionBuilder.Individual("Event3").Build()
}
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void TooManyEventsRule_AboveMaximum_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
MaxRecommendedEvents = 3,
MaxCriticalEvents = 6,
EventCountSeverity = ValidationSeverity.Warning
};
var rule = new TooManyEventsRule();
var events = new List<EventDefinition>();
for (int i = 0; i < 5; i++)
{
events.Add(EventDefinitionBuilder.Individual($"Event{i}").Build());
}
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = events
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Code, Is.EqualTo("TOO_MANY_EVENTS"));
Assert.That(warning.Message, Does.Contain("5 events"));
Assert.That(warning.Message, Does.Contain("max recommended: 3"));
Assert.That(warning.Severity, Is.EqualTo(ValidationSeverity.Warning));
}
[Test]
public void TooManyEventsRule_AboveCritical_EscalatesToError()
{
// Arrange
var config = new ValidationConfiguration
{
MaxRecommendedEvents = 4,
MaxCriticalEvents = 6,
EventCountSeverity = ValidationSeverity.Warning
};
var rule = new TooManyEventsRule();
var events = new List<EventDefinition>();
for (int i = 0; i < 7; i++)
{
events.Add(EventDefinitionBuilder.Individual($"Event{i}").Build());
}
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = events
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Severity, Is.EqualTo(ValidationSeverity.Error),
"Should escalate to Error when above critical threshold");
}
[Test]
public void TooManyEventsRule_ContainsMetadata()
{
// Arrange
var config = new ValidationConfiguration
{
MaxRecommendedEvents = 2,
MaxCriticalEvents = 5
};
var rule = new TooManyEventsRule();
var events = new List<EventDefinition>();
for (int i = 0; i < 4; i++)
{
events.Add(EventDefinitionBuilder.Individual($"Event{i}").Build());
}
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = events
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Metadata, Contains.Key("MaxRecommended"));
Assert.That(warning.Metadata, Contains.Key("MaxCritical"));
Assert.That(warning.Metadata["MaxRecommended"], Is.EqualTo(2));
Assert.That(warning.Metadata["MaxCritical"], Is.EqualTo(5));
}
#endregion
#region NoRegionalEventAssignmentRule Tests
[Test]
public void NoRegionalEventAssignmentRule_HasRegionalEvent_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { RequireRegionalEvent = true };
var rule = new NoRegionalEventAssignmentRule();
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = new List<EventDefinition>
{
EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build()
}
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void NoRegionalEventAssignmentRule_LacksRegionalEvent_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
RequireRegionalEvent = true,
NoRegionalEventSeverity = ValidationSeverity.Warning
};
var rule = new NoRegionalEventAssignmentRule();
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = new List<EventDefinition>
{
EventDefinitionBuilder.Individual("Coding").Build()
}
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Code, Is.EqualTo("NO_REGIONAL_EVENT_ASSIGNED"));
Assert.That(warning.Context, Is.EqualTo(ValidationContext.StudentAssignment));
}
#endregion
#region NoOnSiteActivityAssignmentRule Tests
[Test]
public void NoOnSiteActivityAssignmentRule_HasOnSiteActivity_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { RequireOnSiteActivity = true };
var rule = new NoOnSiteActivityAssignmentRule();
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = new List<EventDefinition>
{
EventDefinitionBuilder.Individual("Speech").AsOnSite().Build()
}
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void NoOnSiteActivityAssignmentRule_LacksOnSiteActivity_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
RequireOnSiteActivity = true,
NoOnSiteActivitySeverity = ValidationSeverity.Error
};
var rule = new NoOnSiteActivityAssignmentRule();
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = new List<EventDefinition>
{
EventDefinitionBuilder.Individual("Flight").Build()
}
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Code, Is.EqualTo("NO_ONSITE_ACTIVITY_ASSIGNED"));
Assert.That(warning.Severity, Is.EqualTo(ValidationSeverity.Error));
}
#endregion
#region TooManyRegionalEventsAssignmentRule Tests
[Test]
public void TooManyRegionalEventsAssignmentRule_WithinLimit_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { MaxRegionalEvents = 3 };
var rule = new TooManyRegionalEventsAssignmentRule();
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = new List<EventDefinition>
{
EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build(),
EventDefinitionBuilder.Individual("Coding").AsRegionalEvent().Build()
}
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void TooManyRegionalEventsAssignmentRule_ExceedsLimit_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
MaxRegionalEvents = 2,
TooManyRegionalEventsSeverity = ValidationSeverity.Warning
};
var rule = new TooManyRegionalEventsAssignmentRule();
var stats = new StudentEventStatistics
{
Student = _testStudent,
Events = new List<EventDefinition>
{
EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build(),
EventDefinitionBuilder.Individual("Coding").AsRegionalEvent().Build(),
EventDefinitionBuilder.Individual("Essays").AsRegionalEvent().Build()
}
};
// Act
var warning = rule.Validate(stats, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Code, Is.EqualTo("TOO_MANY_REGIONAL_EVENTS"));
Assert.That(warning.Message, Does.Contain("3 regional events"));
}
#endregion
#region AppliesTo Tests
[Test]
public void AssignmentRules_ApplyToCorrectContexts()
{
// Arrange
var rules = new IValidationRule<StudentEventStatistics>[]
{
new TooFewEventsRule(),
new TooManyEventsRule(),
new NoRegionalEventAssignmentRule(),
new NoOnSiteActivityAssignmentRule(),
new TooManyRegionalEventsAssignmentRule()
};
// Act & Assert - all assignment rules should apply to these contexts
foreach (var rule in rules)
{
Assert.That(rule.AppliesTo(ValidationContext.StudentAssignment), Is.True,
$"{rule.GetType().Name} should apply to StudentAssignment");
Assert.That(rule.AppliesTo(ValidationContext.StudentRegistration), Is.True,
$"{rule.GetType().Name} should apply to StudentRegistration");
Assert.That(rule.AppliesTo(ValidationContext.StudentRanking), Is.False,
$"{rule.GetType().Name} should not apply to StudentRanking");
Assert.That(rule.AppliesTo(ValidationContext.Team), Is.False,
$"{rule.GetType().Name} should not apply to Team");
}
}
#endregion
}
@@ -0,0 +1,294 @@
using Core.Entities;
using Core.Validation;
using Core.Validation.Rules.StudentRankingRules;
using Tests.Builders;
namespace Tests.Validation.Rules;
[TestFixture]
public class StudentRankingRulesTests
{
[SetUp]
public void SetUp()
{
BuilderExtensions.ResetAllBuilders();
}
#region NoRegionalEventRule Tests
[Test]
public void NoRegionalEventRule_StudentHasRegionalEvent_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration
{
RequireRegionalEvent = true,
RequireOnSiteActivity = false,
RequireIndividualEvent = false
};
var rule = new NoRegionalEventRule();
var flight = EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build();
var student = StudentBuilder.Default()
.WithRanking(flight, 1)
.Build();
// Act
var warning = rule.Validate(student, config);
// Assert
Assert.That(warning, Is.Null, "Student with regional event should not trigger warning");
}
[Test]
public void NoRegionalEventRule_StudentLacksRegionalEvent_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
RequireRegionalEvent = true,
NoRegionalEventSeverity = ValidationSeverity.Warning
};
var rule = new NoRegionalEventRule();
var coding = EventDefinitionBuilder.Individual("Coding").Build(); // Not regional
var student = StudentBuilder.Default()
.WithRanking(coding, 1)
.Build();
// Act
var warning = rule.Validate(student, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Code, Is.EqualTo("NO_REGIONAL_EVENT"));
Assert.That(warning.Message, Is.EqualTo("No Regional Event"));
Assert.That(warning.Severity, Is.EqualTo(ValidationSeverity.Warning));
Assert.That(warning.IconIdentifier, Is.EqualTo("RegionalEvent"));
}
[Test]
public void NoRegionalEventRule_RequirementDisabled_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { RequireRegionalEvent = false };
var rule = new NoRegionalEventRule();
var coding = EventDefinitionBuilder.Individual("Coding").Build();
var student = StudentBuilder.Default()
.WithRanking(coding, 1)
.Build();
// Act
var warning = rule.Validate(student, config);
// Assert
Assert.That(warning, Is.Null, "Warning should not be returned when requirement is disabled");
}
[Test]
public void NoRegionalEventRule_AppliesTo_CorrectContexts()
{
// Arrange
var rule = new NoRegionalEventRule();
// Act & Assert
Assert.That(rule.AppliesTo(ValidationContext.StudentRanking), Is.True);
Assert.That(rule.AppliesTo(ValidationContext.StudentRegistration), Is.True);
Assert.That(rule.AppliesTo(ValidationContext.StudentAssignment), Is.False);
Assert.That(rule.AppliesTo(ValidationContext.Team), Is.False);
}
#endregion
#region NoOnSiteActivityRule Tests
[Test]
public void NoOnSiteActivityRule_StudentHasOnSiteActivity_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration
{
RequireOnSiteActivity = true,
RequireRegionalEvent = false,
RequireIndividualEvent = false
};
var rule = new NoOnSiteActivityRule();
var speech = EventDefinitionBuilder.Individual("Speech").AsOnSite().Build();
var student = StudentBuilder.Default()
.WithRanking(speech, 1)
.Build();
// Act
var warning = rule.Validate(student, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void NoOnSiteActivityRule_StudentLacksOnSiteActivity_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
RequireOnSiteActivity = true,
NoOnSiteActivitySeverity = ValidationSeverity.Error
};
var rule = new NoOnSiteActivityRule();
var flight = EventDefinitionBuilder.Individual("Flight").Build(); // Not on-site
var student = StudentBuilder.Default()
.WithRanking(flight, 1)
.Build();
// Act
var warning = rule.Validate(student, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Code, Is.EqualTo("NO_ONSITE_ACTIVITY"));
Assert.That(warning.Severity, Is.EqualTo(ValidationSeverity.Error));
}
#endregion
#region NoIndividualEventRule Tests
[Test]
public void NoIndividualEventRule_StudentHasIndividualEvent_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration
{
RequireIndividualEvent = true,
RequireRegionalEvent = false,
RequireOnSiteActivity = false
};
var rule = new NoIndividualEventRule();
var flight = EventDefinitionBuilder.Individual("Flight").Build();
var student = StudentBuilder.Default()
.WithRanking(flight, 1)
.Build();
// Act
var warning = rule.Validate(student, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void NoIndividualEventRule_StudentOnlyHasTeamEvents_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
RequireIndividualEvent = true,
NoIndividualEventSeverity = ValidationSeverity.Warning
};
var rule = new NoIndividualEventRule();
var robotics = EventDefinitionBuilder.Team("Robotics", 2, 5).Build();
var student = StudentBuilder.Default()
.WithRanking(robotics, 1)
.Build();
// Act
var warning = rule.Validate(student, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Code, Is.EqualTo("NO_INDIVIDUAL_EVENT"));
Assert.That(warning.Message, Is.EqualTo("No Individual Event"));
}
#endregion
#region TooManyRegionalEventsRule Tests
[Test]
public void TooManyRegionalEventsRule_WithinLimit_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { MaxRegionalEvents = 3 };
var rule = new TooManyRegionalEventsRule();
var flight = EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build();
var coding = EventDefinitionBuilder.Individual("Coding").AsRegionalEvent().Build();
var student = StudentBuilder.Default()
.WithRanking(flight, 1)
.WithRanking(coding, 2)
.Build();
// Act
var warning = rule.Validate(student, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void TooManyRegionalEventsRule_ExceedsLimit_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
MaxRegionalEvents = 2,
TooManyRegionalEventsSeverity = ValidationSeverity.Warning
};
var rule = new TooManyRegionalEventsRule();
var flight = EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build();
var coding = EventDefinitionBuilder.Individual("Coding").AsRegionalEvent().Build();
var essays = EventDefinitionBuilder.Individual("Essays").AsRegionalEvent().Build();
var student = StudentBuilder.Default()
.WithRanking(flight, 1)
.WithRanking(coding, 2)
.WithRanking(essays, 3)
.Build();
// Act
var warning = rule.Validate(student, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Code, Is.EqualTo("TOO_MANY_REGIONAL_EVENTS"));
Assert.That(warning.Message, Does.Contain("3 regional events"));
Assert.That(warning.Message, Does.Contain("max recommended: 2"));
Assert.That(warning.IconIdentifier, Is.EqualTo("RegionalEvent"));
}
[Test]
public void TooManyRegionalEventsRule_ContainsMetadata()
{
// Arrange
var config = new ValidationConfiguration { MaxRegionalEvents = 1 };
var rule = new TooManyRegionalEventsRule();
var flight = EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build();
var coding = EventDefinitionBuilder.Individual("Coding").AsRegionalEvent().Build();
var student = StudentBuilder.Default()
.WithName("Alice", "Anderson")
.WithRanking(flight, 1)
.WithRanking(coding, 2)
.Build();
// Act
var warning = rule.Validate(student, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Metadata, Contains.Key("RegionalEventCount"));
Assert.That(warning.Metadata, Contains.Key("MaxRegionalEvents"));
Assert.That(warning.Metadata["RegionalEventCount"], Is.EqualTo(2));
Assert.That(warning.Metadata["MaxRegionalEvents"], Is.EqualTo(1));
}
#endregion
}
+310
View File
@@ -0,0 +1,310 @@
using Core.Entities;
using Core.Validation;
using Core.Validation.Rules.TeamRules;
using Tests.Builders;
namespace Tests.Validation.Rules;
[TestFixture]
public class TeamRulesTests
{
[SetUp]
public void SetUp()
{
BuilderExtensions.ResetAllBuilders();
}
#region MissingCaptainRule Tests
[Test]
public void MissingCaptainRule_TeamHasCaptain_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { RequireTeamCaptain = true };
var rule = new MissingCaptainRule();
var robotics = EventDefinitionBuilder.Team("Robotics", 2, 4).Build();
var captain = StudentBuilder.Default().WithName("Alice", "A").Build();
var member = StudentBuilder.Default().WithName("Bob", "B").Build();
var team = TeamBuilder.Default()
.ForEvent(robotics)
.WithStudent(captain, isCaptain: true)
.WithStudent(member)
.Build();
// Act
var warning = rule.Validate(team, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void MissingCaptainRule_TeamLacksCaptain_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
RequireTeamCaptain = true,
MissingCaptainSeverity = ValidationSeverity.Warning
};
var rule = new MissingCaptainRule();
var robotics = EventDefinitionBuilder.Team("Robotics", 2, 4).Build();
var team = TeamBuilder.Default()
.ForEvent(robotics)
.WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build())
.WithStudent(StudentBuilder.Default().WithName("Bob", "B").Build())
.Build();
// Act
var warning = rule.Validate(team, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Code, Is.EqualTo("MISSING_CAPTAIN"));
Assert.That(warning.Message, Does.Contain("no captain assigned"));
Assert.That(warning.Severity, Is.EqualTo(ValidationSeverity.Warning));
Assert.That(warning.IconIdentifier, Is.EqualTo("Captain"));
}
[Test]
public void MissingCaptainRule_RequirementDisabled_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { RequireTeamCaptain = false };
var rule = new MissingCaptainRule();
var robotics = EventDefinitionBuilder.Team("Robotics", 2, 4).Build();
var team = TeamBuilder.Default()
.ForEvent(robotics)
.WithStudent(StudentBuilder.Default().Build())
.Build();
// Act
var warning = rule.Validate(team, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void MissingCaptainRule_IndividualEvent_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { RequireTeamCaptain = true };
var rule = new MissingCaptainRule();
var flight = EventDefinitionBuilder.Individual("Flight").Build();
var team = TeamBuilder.Default()
.ForEvent(flight)
.WithStudent(StudentBuilder.Default().Build())
.Build();
// Act
var warning = rule.Validate(team, config);
// Assert
Assert.That(warning, Is.Null, "Individual events should not require a captain");
}
#endregion
#region TeamSizeTooSmallRule Tests
[Test]
public void TeamSizeTooSmallRule_MeetsMinimum_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { TeamSizeSeverity = ValidationSeverity.Warning };
var rule = new TeamSizeTooSmallRule();
var robotics = EventDefinitionBuilder.Team("Robotics", 2, 4).Build();
var team = TeamBuilder.Default()
.ForEvent(robotics)
.WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build())
.WithStudent(StudentBuilder.Default().WithName("Bob", "B").Build())
.Build();
// Act
var warning = rule.Validate(team, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void TeamSizeTooSmallRule_BelowMinimum_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration { TeamSizeSeverity = ValidationSeverity.Error };
var rule = new TeamSizeTooSmallRule();
var robotics = EventDefinitionBuilder.Team("Robotics", 3, 5).Build();
var team = TeamBuilder.Default()
.ForEvent(robotics)
.WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build())
.Build();
// Act
var warning = rule.Validate(team, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Code, Is.EqualTo("TEAM_SIZE_TOO_SMALL"));
Assert.That(warning.Message, Does.Contain("1 members"));
Assert.That(warning.Message, Does.Contain("min: 3"));
Assert.That(warning.Severity, Is.EqualTo(ValidationSeverity.Error));
}
[Test]
public void TeamSizeTooSmallRule_IndividualEvent_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { TeamSizeSeverity = ValidationSeverity.Warning };
var rule = new TeamSizeTooSmallRule();
var flight = EventDefinitionBuilder.Individual("Flight").Build();
var team = TeamBuilder.Default()
.ForEvent(flight)
.WithStudent(StudentBuilder.Default().Build())
.Build();
// Act
var warning = rule.Validate(team, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void TeamSizeTooSmallRule_ContainsMetadata()
{
// Arrange
var config = new ValidationConfiguration { TeamSizeSeverity = ValidationSeverity.Warning };
var rule = new TeamSizeTooSmallRule();
var robotics = EventDefinitionBuilder.Team("Robotics", 4, 6).Build();
var team = TeamBuilder.Default()
.ForEvent(robotics)
.WithStudent(StudentBuilder.Default().Build())
.WithStudent(StudentBuilder.Default().Build())
.Build();
// Act
var warning = rule.Validate(team, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Metadata, Contains.Key("EventName"));
Assert.That(warning.Metadata, Contains.Key("ActualSize"));
Assert.That(warning.Metadata, Contains.Key("MinSize"));
Assert.That(warning.Metadata["ActualSize"], Is.EqualTo(2));
Assert.That(warning.Metadata["MinSize"], Is.EqualTo(4));
}
#endregion
#region TeamSizeTooLargeRule Tests
[Test]
public void TeamSizeTooLargeRule_MeetsMaximum_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { TeamSizeSeverity = ValidationSeverity.Warning };
var rule = new TeamSizeTooLargeRule();
var robotics = EventDefinitionBuilder.Team("Robotics", 2, 4).Build();
var team = TeamBuilder.Default()
.ForEvent(robotics)
.WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build())
.WithStudent(StudentBuilder.Default().WithName("Bob", "B").Build())
.WithStudent(StudentBuilder.Default().WithName("Carol", "C").Build())
.Build();
// Act
var warning = rule.Validate(team, config);
// Assert
Assert.That(warning, Is.Null);
}
[Test]
public void TeamSizeTooLargeRule_ExceedsMaximum_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration { TeamSizeSeverity = ValidationSeverity.Warning };
var rule = new TeamSizeTooLargeRule();
var robotics = EventDefinitionBuilder.Team("Robotics", 2, 3).Build();
var team = TeamBuilder.Default()
.ForEvent(robotics)
.WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build())
.WithStudent(StudentBuilder.Default().WithName("Bob", "B").Build())
.WithStudent(StudentBuilder.Default().WithName("Carol", "C").Build())
.WithStudent(StudentBuilder.Default().WithName("David", "D").Build())
.WithStudent(StudentBuilder.Default().WithName("Eve", "E").Build())
.Build();
// Act
var warning = rule.Validate(team, config);
// Assert
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Code, Is.EqualTo("TEAM_SIZE_TOO_LARGE"));
Assert.That(warning.Message, Does.Contain("5 members"));
Assert.That(warning.Message, Does.Contain("max: 3"));
}
[Test]
public void TeamSizeTooLargeRule_IndividualEvent_ReturnsNull()
{
// Arrange
var config = new ValidationConfiguration { TeamSizeSeverity = ValidationSeverity.Warning };
var rule = new TeamSizeTooLargeRule();
var flight = EventDefinitionBuilder.Individual("Flight").Build();
var team = TeamBuilder.Default()
.ForEvent(flight)
.WithStudent(StudentBuilder.Default().Build())
.Build();
// Act
var warning = rule.Validate(team, config);
// Assert
Assert.That(warning, Is.Null);
}
#endregion
#region AppliesTo Tests
[Test]
public void TeamRules_ApplyToCorrectContexts()
{
// Arrange
var rules = new IValidationRule<Team>[]
{
new MissingCaptainRule(),
new TeamSizeTooSmallRule(),
new TeamSizeTooLargeRule()
};
// Act & Assert - all team rules should apply to Team context only
foreach (var rule in rules)
{
Assert.That(rule.AppliesTo(ValidationContext.Team), Is.True,
$"{rule.GetType().Name} should apply to Team");
Assert.That(rule.AppliesTo(ValidationContext.StudentRanking), Is.False,
$"{rule.GetType().Name} should not apply to StudentRanking");
Assert.That(rule.AppliesTo(ValidationContext.StudentAssignment), Is.False,
$"{rule.GetType().Name} should not apply to StudentAssignment");
Assert.That(rule.AppliesTo(ValidationContext.StudentRegistration), Is.False,
$"{rule.GetType().Name} should not apply to StudentRegistration");
}
}
#endregion
}
@@ -0,0 +1,229 @@
using Core.Entities;
using Core.Validation;
using System.Text.Json;
namespace Tests.Validation;
[TestFixture]
public class ValidationConfigurationTests
{
[Test]
public void Default_ReturnsExpectedValues()
{
// Arrange & Act
var config = ValidationConfiguration.Default;
// Assert
Assert.That(config.MinRecommendedEvents, Is.EqualTo(2));
Assert.That(config.MaxRecommendedEvents, Is.EqualTo(4));
Assert.That(config.MinCriticalEvents, Is.EqualTo(1));
Assert.That(config.MaxCriticalEvents, Is.EqualTo(6));
Assert.That(config.MaxRegionalEvents, Is.EqualTo(3));
Assert.That(config.RequireRegionalEvent, Is.True);
Assert.That(config.RequireOnSiteActivity, Is.True);
Assert.That(config.RequireIndividualEvent, Is.False);
Assert.That(config.RequireTeamCaptain, Is.True);
Assert.That(config.NoRegionalEventSeverity, Is.EqualTo(ValidationSeverity.Warning));
Assert.That(config.EventCountSeverity, Is.EqualTo(ValidationSeverity.Warning));
Assert.That(config.MissingCaptainSeverity, Is.EqualTo(ValidationSeverity.Warning));
Assert.That(config.TooManyRegionalEventsSeverity, Is.EqualTo(ValidationSeverity.Warning));
}
[Test]
public void FromAssignmentParameters_CreatesCorrectConfiguration()
{
// Arrange
var parameters = new AssignmentParameters(
effortLowerBound: 5,
effortUpperBound: 8,
eventsLowerBound: 2,
eventsUpperBound: 4,
requireRegional: true,
requireOnSite: false
);
// Act
var config = ValidationConfiguration.FromAssignmentParameters(parameters);
// Assert
Assert.That(config.MinRecommendedEvents, Is.EqualTo(2));
Assert.That(config.MaxRecommendedEvents, Is.EqualTo(4));
Assert.That(config.RequireRegionalEvent, Is.True);
Assert.That(config.RequireOnSiteActivity, Is.False);
Assert.That(config.MinCriticalEvents, Is.EqualTo(1));
Assert.That(config.MaxCriticalEvents, Is.EqualTo(6));
Assert.That(config.RequireTeamCaptain, Is.True);
}
[Test]
public void FromJson_ValidJson_ReturnsConfiguration()
{
// Arrange
var json = @"{
""MinRecommendedEvents"": 3,
""MaxRecommendedEvents"": 5,
""RequireRegionalEvent"": false,
""NoRegionalEventSeverity"": ""Error""
}";
// Act
var config = ValidationConfiguration.FromJson(json);
// Assert
Assert.That(config.MinRecommendedEvents, Is.EqualTo(3));
Assert.That(config.MaxRecommendedEvents, Is.EqualTo(5));
Assert.That(config.RequireRegionalEvent, Is.False);
Assert.That(config.NoRegionalEventSeverity, Is.EqualTo(ValidationSeverity.Error));
}
[Test]
public void FromJson_InvalidJson_ReturnsDefault()
{
// Arrange
var invalidJson = "{ invalid json }";
// Act
var config = ValidationConfiguration.FromJson(invalidJson);
// Assert - should return Default config on error
Assert.That(config, Is.Not.Null);
Assert.That(config.MinRecommendedEvents, Is.EqualTo(ValidationConfiguration.Default.MinRecommendedEvents));
}
[Test]
public async Task LoadFromFileAsync_NonExistentFile_ReturnsDefault()
{
// Arrange
var nonExistentPath = Path.Combine(Path.GetTempPath(), $"non_existent_{Guid.NewGuid()}.json");
// Act
var config = await ValidationConfiguration.LoadFromFileAsync(nonExistentPath);
// Assert
Assert.That(config, Is.Not.Null);
Assert.That(config.MinRecommendedEvents, Is.EqualTo(ValidationConfiguration.Default.MinRecommendedEvents));
}
[Test]
public async Task SaveToFileAsync_And_LoadFromFileAsync_RoundTrip()
{
// Arrange
var tempPath = Path.Combine(Path.GetTempPath(), $"test_config_{Guid.NewGuid()}.json");
var originalConfig = new ValidationConfiguration
{
MinRecommendedEvents = 3,
MaxRecommendedEvents = 5,
RequireRegionalEvent = false,
NoRegionalEventSeverity = ValidationSeverity.Error,
MaxRegionalEvents = 2
};
try
{
// Act
await originalConfig.SaveToFileAsync(tempPath);
var loadedConfig = await ValidationConfiguration.LoadFromFileAsync(tempPath);
// Assert
Assert.That(loadedConfig.MinRecommendedEvents, Is.EqualTo(3));
Assert.That(loadedConfig.MaxRecommendedEvents, Is.EqualTo(5));
Assert.That(loadedConfig.RequireRegionalEvent, Is.False);
Assert.That(loadedConfig.NoRegionalEventSeverity, Is.EqualTo(ValidationSeverity.Error));
Assert.That(loadedConfig.MaxRegionalEvents, Is.EqualTo(2));
}
finally
{
// Cleanup
if (File.Exists(tempPath))
File.Delete(tempPath);
}
}
[Test]
public async Task SaveToFileAsync_CreatesReadableJson()
{
// Arrange
var tempPath = Path.Combine(Path.GetTempPath(), $"test_config_{Guid.NewGuid()}.json");
var config = new ValidationConfiguration
{
MinRecommendedEvents = 2,
MaxRecommendedEvents = 4
};
try
{
// Act
await config.SaveToFileAsync(tempPath);
var jsonContent = await File.ReadAllTextAsync(tempPath);
// Assert
Assert.That(jsonContent, Does.Contain("MinRecommendedEvents"));
Assert.That(jsonContent, Does.Contain("MaxRecommendedEvents"));
// Verify it's valid JSON
var doc = JsonDocument.Parse(jsonContent);
Assert.That(doc, Is.Not.Null);
}
finally
{
// Cleanup
if (File.Exists(tempPath))
File.Delete(tempPath);
}
}
[Test]
public void ValidationConfiguration_CanBeModified()
{
// Arrange
var config = ValidationConfiguration.Default;
// Act
config.MinRecommendedEvents = 5;
config.RequireRegionalEvent = false;
config.NoRegionalEventSeverity = ValidationSeverity.Error;
// Assert
Assert.That(config.MinRecommendedEvents, Is.EqualTo(5));
Assert.That(config.RequireRegionalEvent, Is.False);
Assert.That(config.NoRegionalEventSeverity, Is.EqualTo(ValidationSeverity.Error));
}
[Test]
public void ValidationConfiguration_AllPropertiesSerializable()
{
// Arrange
var config = new ValidationConfiguration
{
MinRecommendedEvents = 1,
MaxRecommendedEvents = 2,
MinCriticalEvents = 3,
MaxCriticalEvents = 4,
MaxRegionalEvents = 5,
RequireRegionalEvent = true,
RequireOnSiteActivity = false,
RequireIndividualEvent = true,
RequireTeamCaptain = false,
NoRegionalEventSeverity = ValidationSeverity.Error,
NoOnSiteActivitySeverity = ValidationSeverity.Warning,
NoIndividualEventSeverity = ValidationSeverity.Error,
TeamSizeSeverity = ValidationSeverity.Warning,
EventCountSeverity = ValidationSeverity.Error,
MissingCaptainSeverity = ValidationSeverity.Warning,
TooManyRegionalEventsSeverity = ValidationSeverity.Error
};
// Act
var json = JsonSerializer.Serialize(config);
var deserialized = JsonSerializer.Deserialize<ValidationConfiguration>(json);
// Assert
Assert.That(deserialized, Is.Not.Null);
Assert.That(deserialized!.MinRecommendedEvents, Is.EqualTo(config.MinRecommendedEvents));
Assert.That(deserialized.MaxRecommendedEvents, Is.EqualTo(config.MaxRecommendedEvents));
Assert.That(deserialized.RequireRegionalEvent, Is.EqualTo(config.RequireRegionalEvent));
Assert.That(deserialized.NoRegionalEventSeverity, Is.EqualTo(config.NoRegionalEventSeverity));
Assert.That(deserialized.MaxRegionalEvents, Is.EqualTo(config.MaxRegionalEvents));
Assert.That(deserialized.TooManyRegionalEventsSeverity, Is.EqualTo(config.TooManyRegionalEventsSeverity));
}
}
+425
View File
@@ -0,0 +1,425 @@
using Core.Entities;
using Core.Validation;
using Tests.Builders;
namespace Tests.Validation;
[TestFixture]
public class ValidationServiceTests
{
private ValidationConfiguration _defaultConfig;
[SetUp]
public void SetUp()
{
BuilderExtensions.ResetAllBuilders();
_defaultConfig = ValidationConfiguration.Default;
}
[Test]
public void ValidationService_DiscoverRules_FindsAllRules()
{
// Arrange
var service = new ValidationService(_defaultConfig);
// Act - rules are discovered in constructor via reflection
// Assert - verify rules were discovered by validating entities
var student = StudentBuilder.Default()
.WithRanking(EventDefinitionBuilder.Individual("Flight").Build(), 1)
.Build();
var warnings = service.ValidateStudentRankings(student, ValidationContext.StudentRanking);
// Should find warnings since student has no regional event (if required by config)
Assert.That(warnings, Is.Not.Null);
}
[Test]
public void ValidateStudent_WithValidRankings_ReturnsNoWarnings()
{
// Arrange
var config = new ValidationConfiguration
{
RequireRegionalEvent = true,
RequireOnSiteActivity = true,
RequireIndividualEvent = false,
MinRecommendedEvents = 2,
MaxRecommendedEvents = 4
};
var service = new ValidationService(config);
var flight = EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build();
var speech = EventDefinitionBuilder.Individual("Speech").AsRegionalEvent().AsOnSite().Build();
var coding = EventDefinitionBuilder.Individual("Coding").Build();
var student = StudentBuilder.Default()
.WithRanking(flight, 1)
.WithRanking(speech, 2)
.WithRanking(coding, 3)
.Build();
// Act
var warnings = service.ValidateStudentRankings(student, ValidationContext.StudentRanking);
// Assert
Assert.That(warnings, Is.Empty, "Student with valid rankings should have no warnings");
}
[Test]
public void ValidateStudent_MissingRegionalEvent_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
RequireRegionalEvent = true,
RequireOnSiteActivity = false,
RequireIndividualEvent = false,
NoRegionalEventSeverity = ValidationSeverity.Warning
};
var service = new ValidationService(config);
var coding = EventDefinitionBuilder.Individual("Coding").Build(); // Not regional
var student = StudentBuilder.Default()
.WithRanking(coding, 1)
.Build();
// Act
var warnings = service.ValidateStudentRankings(student, ValidationContext.StudentRanking);
// Assert
Assert.That(warnings, Has.Count.EqualTo(1));
Assert.That(warnings[0].Code, Is.EqualTo("NO_REGIONAL_EVENT"));
Assert.That(warnings[0].Severity, Is.EqualTo(ValidationSeverity.Warning));
Assert.That(warnings[0].Context, Is.EqualTo(ValidationContext.StudentRanking));
}
[Test]
public void ValidateStudent_MissingOnSiteActivity_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
RequireOnSiteActivity = true,
RequireRegionalEvent = false,
NoOnSiteActivitySeverity = ValidationSeverity.Warning
};
var service = new ValidationService(config);
var flight = EventDefinitionBuilder.Individual("Flight").Build(); // Not on-site
var student = StudentBuilder.Default()
.WithRanking(flight, 1)
.Build();
// Act
var warnings = service.ValidateStudentRankings(student, ValidationContext.StudentRanking);
// Assert
Assert.That(warnings, Has.Count.EqualTo(1));
Assert.That(warnings[0].Code, Is.EqualTo("NO_ONSITE_ACTIVITY"));
Assert.That(warnings[0].Severity, Is.EqualTo(ValidationSeverity.Warning));
}
[Test]
public void ValidateStudent_TooManyRegionalEvents_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
MaxRegionalEvents = 2,
TooManyRegionalEventsSeverity = ValidationSeverity.Warning,
RequireRegionalEvent = false,
RequireOnSiteActivity = false
};
var service = new ValidationService(config);
var flight = EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build();
var coding = EventDefinitionBuilder.Individual("Coding").AsRegionalEvent().Build();
var essays = EventDefinitionBuilder.Individual("Essays").AsRegionalEvent().Build();
var student = StudentBuilder.Default()
.WithRanking(flight, 1)
.WithRanking(coding, 2)
.WithRanking(essays, 3)
.Build();
// Act
var warnings = service.ValidateStudentRankings(student, ValidationContext.StudentRanking);
// Assert
Assert.That(warnings, Has.Count.EqualTo(1));
Assert.That(warnings[0].Code, Is.EqualTo("TOO_MANY_REGIONAL_EVENTS"));
Assert.That(warnings[0].Message, Does.Contain("3 regional events"));
Assert.That(warnings[0].Message, Does.Contain("max recommended: 2"));
}
[Test]
public void ValidateStudentAssignment_ValidAssignment_ReturnsNoWarnings()
{
// Arrange
var config = new ValidationConfiguration
{
RequireRegionalEvent = true,
RequireOnSiteActivity = true,
MinRecommendedEvents = 2,
MaxRecommendedEvents = 4
};
var service = new ValidationService(config);
var flight = EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build();
var speech = EventDefinitionBuilder.Individual("Speech").AsOnSite().Build();
var coding = EventDefinitionBuilder.Individual("Coding").Build();
var student = StudentBuilder.Default().Build();
var stats = new StudentEventStatistics
{
Student = student,
Events = new List<EventDefinition> { flight, speech, coding }
};
// Act
var warnings = service.ValidateStudentStatistics(stats, ValidationContext.StudentAssignment);
// Assert
Assert.That(warnings, Is.Empty);
}
[Test]
public void ValidateStudentAssignment_TooFewEvents_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
MinRecommendedEvents = 3,
EventCountSeverity = ValidationSeverity.Warning,
RequireRegionalEvent = false,
RequireOnSiteActivity = false
};
var service = new ValidationService(config);
var flight = EventDefinitionBuilder.Individual("Flight").Build();
var student = StudentBuilder.Default().Build();
var stats = new StudentEventStatistics
{
Student = student,
Events = new List<EventDefinition> { flight }
};
// Act
var warnings = service.ValidateStudentStatistics(stats, ValidationContext.StudentAssignment);
// Assert
Assert.That(warnings, Has.Count.GreaterThan(0));
var warning = warnings.FirstOrDefault(w => w.Code == "TOO_FEW_EVENTS");
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Severity, Is.EqualTo(ValidationSeverity.Warning));
}
[Test]
public void ValidateStudentAssignment_TooManyEvents_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
MaxRecommendedEvents = 3,
EventCountSeverity = ValidationSeverity.Warning,
RequireRegionalEvent = false,
RequireOnSiteActivity = false
};
var service = new ValidationService(config);
var student = StudentBuilder.Default().Build();
var events = new List<EventDefinition>();
for (int i = 0; i < 5; i++)
{
events.Add(EventDefinitionBuilder.Individual($"Event{i}").Build());
}
var stats = new StudentEventStatistics
{
Student = student,
Events = events
};
// Act
var warnings = service.ValidateStudentStatistics(stats, ValidationContext.StudentAssignment);
// Assert
var warning = warnings.FirstOrDefault(w => w.Code == "TOO_MANY_EVENTS");
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Message, Does.Contain("5 events"));
}
[Test]
public void ValidateTeam_ValidTeam_ReturnsNoWarnings()
{
// Arrange
var config = new ValidationConfiguration
{
RequireTeamCaptain = true
};
var service = new ValidationService(config);
var robotics = EventDefinitionBuilder.Team("Robotics", 2, 4).Build();
var team = TeamBuilder.Default()
.ForEvent(robotics)
.WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build(), isCaptain: true)
.WithStudent(StudentBuilder.Default().WithName("Bob", "B").Build())
.Build();
// Act
var warnings = service.ValidateTeam(team);
// Assert
Assert.That(warnings, Is.Empty);
}
[Test]
public void ValidateTeam_MissingCaptain_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
RequireTeamCaptain = true,
MissingCaptainSeverity = ValidationSeverity.Warning
};
var service = new ValidationService(config);
var robotics = EventDefinitionBuilder.Team("Robotics", 2, 4).Build();
var team = TeamBuilder.Default()
.ForEvent(robotics)
.WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build())
.WithStudent(StudentBuilder.Default().WithName("Bob", "B").Build())
.Build();
// Act
var warnings = service.ValidateTeam(team);
// Assert
Assert.That(warnings, Has.Count.EqualTo(1));
Assert.That(warnings[0].Code, Is.EqualTo("MISSING_CAPTAIN"));
Assert.That(warnings[0].Severity, Is.EqualTo(ValidationSeverity.Warning));
}
[Test]
public void ValidateTeam_TeamTooSmall_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
TeamSizeSeverity = ValidationSeverity.Warning,
RequireTeamCaptain = false
};
var service = new ValidationService(config);
var robotics = EventDefinitionBuilder.Team("Robotics", 3, 5).Build();
var team = TeamBuilder.Default()
.ForEvent(robotics)
.WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build())
.Build();
// Act
var warnings = service.ValidateTeam(team);
// Assert
var warning = warnings.FirstOrDefault(w => w.Code == "TEAM_SIZE_TOO_SMALL");
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Message, Does.Contain("1 members"));
Assert.That(warning.Message, Does.Contain("min: 3"));
}
[Test]
public void ValidateTeam_TeamTooLarge_ReturnsWarning()
{
// Arrange
var config = new ValidationConfiguration
{
TeamSizeSeverity = ValidationSeverity.Warning,
RequireTeamCaptain = false
};
var service = new ValidationService(config);
var robotics = EventDefinitionBuilder.Team("Robotics", 2, 3).Build();
var team = TeamBuilder.Default()
.ForEvent(robotics)
.WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build())
.WithStudent(StudentBuilder.Default().WithName("Bob", "B").Build())
.WithStudent(StudentBuilder.Default().WithName("Carol", "C").Build())
.WithStudent(StudentBuilder.Default().WithName("David", "D").Build())
.Build();
// Act
var warnings = service.ValidateTeam(team);
// Assert
var warning = warnings.FirstOrDefault(w => w.Code == "TEAM_SIZE_TOO_LARGE");
Assert.That(warning, Is.Not.Null);
Assert.That(warning!.Message, Does.Contain("4 members"));
Assert.That(warning.Message, Does.Contain("max: 3"));
}
[Test]
public void ValidationService_ConfigurationChanges_AffectResults()
{
// Arrange
var student = StudentBuilder.Default()
.WithRanking(EventDefinitionBuilder.Individual("Flight").Build(), 1)
.Build();
// Strict config
var strictConfig = new ValidationConfiguration
{
RequireRegionalEvent = true,
RequireOnSiteActivity = false,
RequireIndividualEvent = false,
NoRegionalEventSeverity = ValidationSeverity.Error
};
var strictService = new ValidationService(strictConfig);
// Lenient config
var lenientConfig = new ValidationConfiguration
{
RequireRegionalEvent = false,
RequireOnSiteActivity = false,
RequireIndividualEvent = false
};
var lenientService = new ValidationService(lenientConfig);
// Act
var strictWarnings = strictService.ValidateStudentRankings(student, ValidationContext.StudentRanking);
var lenientWarnings = lenientService.ValidateStudentRankings(student, ValidationContext.StudentRanking);
// Assert
Assert.That(strictWarnings, Has.Count.EqualTo(1));
Assert.That(strictWarnings[0].Severity, Is.EqualTo(ValidationSeverity.Error));
Assert.That(lenientWarnings, Is.Empty, "Lenient config should not require regional event");
}
[Test]
public void ValidationWarning_ContainsMetadata()
{
// Arrange
var config = new ValidationConfiguration
{
RequireRegionalEvent = true,
RequireOnSiteActivity = false
};
var service = new ValidationService(config);
var student = StudentBuilder.Default()
.WithName("Alice", "Anderson")
.WithRanking(EventDefinitionBuilder.Individual("Coding").Build(), 1)
.Build();
// Act
var warnings = service.ValidateStudentRankings(student, ValidationContext.StudentRanking);
// Assert
Assert.That(warnings, Has.Count.EqualTo(1));
var warning = warnings[0];
Assert.That(warning.Metadata, Contains.Key("StudentId"));
Assert.That(warning.Metadata, Contains.Key("StudentName"));
Assert.That(warning.Metadata["StudentName"], Is.EqualTo("Alice Anderson"));
}
}