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: per-item metadata validator tests. Each RuleFor in // UavTileMetadataValidator gets at least one passing + one failing case. // Required-field detection lives at the deserializer layer ([JsonRequired] // on UavTileMetadata) and is exercised at the integration layer. public class UavTileMetadataValidatorTests { private readonly UavTileMetadataValidator _validator; private readonly DateTime _now; public UavTileMetadataValidatorTests() { GlobalValidatorConfig.ApplyOnce(); var config = Options.Create(new UavQualityConfig { MaxAgeDays = 7, CapturedAtFutureSkewSeconds = 30, }); _now = new DateTime(2026, 5, 22, 12, 0, 0, DateTimeKind.Utc); _validator = new UavTileMetadataValidator(config, new FixedTimeProvider(_now)); } // Mirrors the existing pattern in UavTileUploadHandlerTests / UavTileQualityGateTests // (those tests inline the same shape). Kept private here for SRP; if a third // consumer appears, promote to SatelliteProvider.TestSupport. private sealed class FixedTimeProvider : TimeProvider { private readonly DateTime _utcNow; public FixedTimeProvider(DateTime utcNow) => _utcNow = utcNow; public override DateTimeOffset GetUtcNow() => new(_utcNow, TimeSpan.Zero); } private static UavTileMetadata ValidMetadata(DateTime capturedAt) => new() { Latitude = 50.10, Longitude = 36.10, TileZoom = 18, TileSizeMeters = 200.0, CapturedAt = capturedAt, FlightId = null, }; [Fact] public void Validate_AllValid_Passes() { // Arrange var metadata = ValidMetadata(_now.AddMinutes(-5)); // Act var result = _validator.TestValidate(metadata); // Assert result.ShouldNotHaveAnyValidationErrors(); } [Theory] [InlineData(-91.0)] [InlineData(90.001)] [InlineData(180.0)] public void Validate_LatOutOfRange_FailsRangeRule(double lat) { // Arrange var metadata = ValidMetadata(_now) with { Latitude = lat }; // Act var result = _validator.TestValidate(metadata); // Assert result.ShouldHaveValidationErrorFor("latitude"); } [Theory] [InlineData(-181.0)] [InlineData(180.001)] [InlineData(360.0)] public void Validate_LonOutOfRange_FailsRangeRule(double lon) { // Arrange var metadata = ValidMetadata(_now) with { Longitude = lon }; // Act var result = _validator.TestValidate(metadata); // Assert result.ShouldHaveValidationErrorFor("longitude"); } [Theory] [InlineData(-1)] [InlineData(23)] [InlineData(100)] public void Validate_TileZoomOutOfRange_FailsRangeRule(int zoom) { // Arrange var metadata = ValidMetadata(_now) with { TileZoom = zoom }; // Act var result = _validator.TestValidate(metadata); // Assert result.ShouldHaveValidationErrorFor("tileZoom"); } [Theory] [InlineData(0.0)] [InlineData(-1.0)] public void Validate_TileSizeMetersNonPositive_FailsGreaterThanRule(double size) { // Arrange var metadata = ValidMetadata(_now) with { TileSizeMeters = size }; // Act var result = _validator.TestValidate(metadata); // Assert result.ShouldHaveValidationErrorFor("tileSizeMeters"); } [Fact] public void Validate_CapturedAtFuture_FailsFreshnessRule() { // Arrange — 60s in the future (skew limit is 30s). var metadata = ValidMetadata(_now.AddSeconds(60)); // Act var result = _validator.TestValidate(metadata); // Assert result.ShouldHaveValidationErrorFor("capturedAt") .WithErrorMessage("`capturedAt` must be within 30s of the current time (no future-dated tiles)."); } [Fact] public void Validate_CapturedAtNearFutureWithinSkew_Passes() { // Arrange — 10s in the future (within the 30s skew window). var metadata = ValidMetadata(_now.AddSeconds(10)); // Act var result = _validator.TestValidate(metadata); // Assert result.ShouldNotHaveValidationErrorFor("capturedAt"); } [Fact] public void Validate_CapturedAtTooOld_FailsFreshnessRule() { // Arrange — 8 days ago (cap is 7 days). var metadata = ValidMetadata(_now.AddDays(-8)); // Act var result = _validator.TestValidate(metadata); // Assert result.ShouldHaveValidationErrorFor("capturedAt") .WithErrorMessage("`capturedAt` must be within the last 7 days."); } [Fact] public void Validate_FlightIdNull_Passes() { // Arrange — AZ-503 anonymous-flight semantics: null FlightId is valid. var metadata = ValidMetadata(_now) with { FlightId = null }; // Act var result = _validator.TestValidate(metadata); // Assert result.ShouldNotHaveValidationErrorFor("flightId"); } [Fact] public void Validate_FlightIdSet_Passes() { // Arrange var metadata = ValidMetadata(_now) with { FlightId = Guid.NewGuid() }; // Act var result = _validator.TestValidate(metadata); // Assert result.ShouldNotHaveValidationErrorFor("flightId"); } }