using FluentValidation.TestHelper; using Microsoft.Extensions.Options; using SatelliteProvider.Api.Validators; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.DTO; namespace SatelliteProvider.Tests.Validators; // AZ-810: root metadata-envelope validator tests. Covers `items` non-null + // non-empty + cap rules. The per-item rules are covered by UavTileMetadataValidatorTests. public class UavTileBatchMetadataPayloadValidatorTests { private readonly UavTileBatchMetadataPayloadValidator _validator; private readonly DateTime _now; public UavTileBatchMetadataPayloadValidatorTests() { GlobalValidatorConfig.ApplyOnce(); var config = Options.Create(new UavQualityConfig { MaxBatchSize = 100, MaxAgeDays = 7, CapturedAtFutureSkewSeconds = 30, }); _now = new DateTime(2026, 5, 22, 12, 0, 0, DateTimeKind.Utc); _validator = new UavTileBatchMetadataPayloadValidator(config, new FixedTimeProvider(_now)); } private UavTileMetadata ValidItem() => new() { Latitude = 50.10, Longitude = 36.10, TileZoom = 18, TileSizeMeters = 200.0, CapturedAt = _now.AddMinutes(-5), FlightId = null, }; [Fact] public void Validate_OneValidItem_Passes() { // Arrange var payload = new UavTileBatchMetadataPayload { Items = new() { ValidItem() } }; // Act var result = _validator.TestValidate(payload); // Assert result.ShouldNotHaveAnyValidationErrors(); } [Fact] public void Validate_ItemsEmpty_FailsNotEmptyRule() { // Arrange var payload = new UavTileBatchMetadataPayload { Items = new() }; // Act var result = _validator.TestValidate(payload); // Assert result.ShouldHaveValidationErrorFor("items") .WithErrorMessage("`items` must contain at least one entry."); } [Fact] public void Validate_ItemsTooMany_FailsCountRule() { // Arrange — 101 items (cap = 100) var items = Enumerable.Range(0, 101).Select(_ => ValidItem()).ToList(); var payload = new UavTileBatchMetadataPayload { Items = items }; // Act var result = _validator.TestValidate(payload); // Assert result.ShouldHaveValidationErrorFor("items") .WithErrorMessage("`items` must contain at most 100 entries."); } [Fact] public void Validate_PerItemFailure_PropagatesWithIndexedPath() { // Arrange — first item valid, second item lat out-of-range var payload = new UavTileBatchMetadataPayload { Items = new() { ValidItem(), ValidItem() with { Latitude = 91.0 } }, }; // Act var result = _validator.TestValidate(payload); // Assert — error key follows the wire format produced by RuleForEach. result.ShouldHaveValidationErrorFor("items[1].latitude"); } private sealed class FixedTimeProvider : TimeProvider { private readonly DateTime _utcNow; public FixedTimeProvider(DateTime utcNow) => _utcNow = utcNow; public override DateTimeOffset GetUtcNow() => new(_utcNow, TimeSpan.Zero); } }