using FluentValidation.TestHelper; using SatelliteProvider.Api.Validators; using SatelliteProvider.Common.DTO; namespace SatelliteProvider.Tests.Validators; // AZ-809: unit tests for CreateRouteRequestValidator. Each RuleFor / // RuleForEach in the root validator has at least one passing case + one // failing case. Required-field detection lives at the deserializer layer // ([JsonRequired] + UnmappedMemberHandling.Disallow), covered separately // at the integration layer in CreateRouteValidationTests. public class CreateRouteRequestValidatorTests { private readonly CreateRouteRequestValidator _validator; public CreateRouteRequestValidatorTests() { GlobalValidatorConfig.ApplyOnce(); _validator = new CreateRouteRequestValidator(); } private static CreateRouteRequest ValidRequest() { return new CreateRouteRequest { Id = Guid.NewGuid(), Name = "derkachi-flight-1", Description = "AZ-777 Phase 2 seed route", RegionSizeMeters = 1000.0, ZoomLevel = 18, Points = new List { new() { Latitude = 50.10, Longitude = 36.10 }, new() { Latitude = 50.11, Longitude = 36.11 }, }, RequestMaps = true, CreateTilesZip = false, }; } [Fact] public void Validate_AllValid_Passes() { // Arrange var request = ValidRequest(); // Act var result = _validator.TestValidate(request); // Assert result.ShouldNotHaveAnyValidationErrors(); } [Fact] public void Validate_IdEmpty_FailsNotEmptyRule() { // Arrange — reproduces the 2026-05-22 probe finding (silent zero-Guid). var request = ValidRequest(); request.Id = Guid.Empty; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("id") .WithErrorMessage("`id` must be a non-zero GUID (the caller's idempotency key)."); } [Theory] [InlineData("")] [InlineData(" ")] public void Validate_NameMissing_FailsNotEmptyRule(string name) { // Arrange var request = ValidRequest(); request.Name = name; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("name"); } [Fact] public void Validate_NameTooLong_FailsLengthRule() { // Arrange — name length 201 (cap is 200). var request = ValidRequest(); request.Name = new string('a', 201); // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("name"); } [Fact] public void Validate_DescriptionTooLong_FailsLengthRule() { // Arrange — description length 1001 (cap is 1000). var request = ValidRequest(); request.Description = new string('d', 1001); // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("description"); } [Theory] [InlineData(99.999)] [InlineData(0.0)] [InlineData(10000.001)] [InlineData(100000.0)] public void Validate_RegionSizeMetersOutOfRange_FailsRangeRule(double size) { // Arrange var request = ValidRequest(); request.RegionSizeMeters = size; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("regionSizeMeters"); } [Theory] [InlineData(-1)] [InlineData(23)] [InlineData(100)] public void Validate_ZoomLevelOutOfRange_FailsRangeRule(int zoom) { // Arrange var request = ValidRequest(); request.ZoomLevel = zoom; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("zoomLevel"); } [Fact] public void Validate_PointsTooFew_FailsCountRule() { // Arrange — only 1 point; min is 2 (Flow F4 precondition). var request = ValidRequest(); request.Points = new List { new() { Latitude = 50.10, Longitude = 36.10 }, }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("points"); } [Fact] public void Validate_PointsTooMany_FailsCountRule() { // Arrange — 501 points; max is 500. var request = ValidRequest(); request.Points = Enumerable .Range(0, 501) .Select(_ => new RoutePoint { Latitude = 50.10, Longitude = 36.10 }) .ToList(); // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("points"); } [Fact] public void Validate_PointLatOutOfRange_FailsChildRule() { // Arrange — second point's lat is out of range var request = ValidRequest(); request.Points[1].Latitude = 91.0; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("points[1].lat"); } [Fact] public void Validate_PointLonOutOfRange_FailsChildRule() { // Arrange — second point's lon is out of range var request = ValidRequest(); request.Points[1].Longitude = 181.0; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("points[1].lon"); } [Fact] public void Validate_GeofencePolygonNwSwapped_FailsChildInvariant() { // Arrange — NW.Lat <= SE.Lat (NW not north-of SE) var request = ValidRequest(); request.Geofences = new Geofences { Polygons = new List { new() { NorthWest = new GeoPoint(50.05, 36.05), SouthEast = new GeoPoint(50.05, 36.15), } } }; // Act var result = _validator.TestValidate(request); // Assert — the GeofencePolygonValidator child-validator's `.WithName("northWest")` // is prefixed with the RuleForEach path which we OverridePropertyName to // "geofences.polygons", producing the full wire path // `geofences.polygons[0].northWest`. result.ShouldHaveValidationErrorFor("geofences.polygons[0].northWest"); } [Fact] public void Validate_GeofencesPresentButEmpty_FailsNotEmptyRule() { // Arrange — geofences object exists, polygons list is empty var request = ValidRequest(); request.Geofences = new Geofences { Polygons = new List() }; // Act var result = _validator.TestValidate(request); // Assert — OverridePropertyName makes the empty-list rule fire at the // wire-format path `geofences.polygons` instead of the leaf-only `polygons`. result.ShouldHaveValidationErrorFor("geofences.polygons"); } [Fact] public void Validate_CreateTilesZipWithoutRequestMaps_FailsCrossFieldRule() { // Arrange — cannot zip what wasn't downloaded var request = ValidRequest(); request.RequestMaps = false; request.CreateTilesZip = true; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("createTilesZip") .WithErrorMessage("`createTilesZip` requires `requestMaps` to be true (can't zip what wasn't downloaded)."); } [Fact] public void Validate_CreateTilesZipWithRequestMaps_Passes() { // Arrange — both true is valid var request = ValidRequest(); request.RequestMaps = true; request.CreateTilesZip = true; // Act var result = _validator.TestValidate(request); // Assert result.ShouldNotHaveValidationErrorFor("createTilesZip"); } }