using FluentAssertions; using FluentValidation.TestHelper; using SatelliteProvider.Api.Validators; using SatelliteProvider.Common.DTO; namespace SatelliteProvider.Tests.Validators; public class InventoryRequestValidatorTests { private readonly InventoryRequestValidator _validator = new(); [Fact] public void Validate_TilesPopulated_LocationHashesNull_Passes() { // Arrange var request = new TileInventoryRequest { Tiles = new[] { new TileCoord { Z = 18, X = 1, Y = 1 } } }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldNotHaveAnyValidationErrors(); } [Fact] public void Validate_LocationHashesPopulated_TilesNull_Passes() { // Arrange var request = new TileInventoryRequest { LocationHashes = new[] { Guid.NewGuid() } }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldNotHaveAnyValidationErrors(); } [Fact] public void Validate_BothPopulated_FailsXorRule() { // Arrange var request = new TileInventoryRequest { Tiles = new[] { new TileCoord { Z = 18, X = 1, Y = 1 } }, LocationHashes = new[] { Guid.NewGuid() } }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("$") .WithErrorMessage("Populate exactly one of `tiles` or `locationHashes` (sending both, or neither, is not allowed)."); } [Fact] public void Validate_NeitherPopulated_FailsXorRule() { // Arrange var request = new TileInventoryRequest(); // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("$"); } [Fact] public void Validate_BothEmpty_FailsXorRule() { // Arrange var request = new TileInventoryRequest { Tiles = Array.Empty(), LocationHashes = Array.Empty() }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("$"); } [Fact] public void Validate_TilesAtCap_Passes() { // Arrange var coords = Enumerable.Range(0, TileInventoryLimits.MaxEntriesPerRequest) .Select(_ => new TileCoord { Z = 18, X = 1, Y = 1 }) .ToArray(); var request = new TileInventoryRequest { Tiles = coords }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldNotHaveValidationErrorFor("tiles"); } [Fact] public void Validate_TilesOverCap_FailsCapRule() { // Arrange var coords = Enumerable.Range(0, TileInventoryLimits.MaxEntriesPerRequest + 1) .Select(_ => new TileCoord { Z = 18, X = 1, Y = 1 }) .ToArray(); var request = new TileInventoryRequest { Tiles = coords }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("tiles") .WithErrorMessage($"`tiles` must contain at most {TileInventoryLimits.MaxEntriesPerRequest} entries."); } [Fact] public void Validate_LocationHashesOverCap_FailsCapRule() { // Arrange var hashes = Enumerable.Range(0, TileInventoryLimits.MaxEntriesPerRequest + 1) .Select(_ => Guid.NewGuid()) .ToArray(); var request = new TileInventoryRequest { LocationHashes = hashes }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("locationHashes") .WithErrorMessage($"`locationHashes` must contain at most {TileInventoryLimits.MaxEntriesPerRequest} entries."); } [Theory] [InlineData(-1)] [InlineData(23)] [InlineData(100)] public void Validate_TileZoomOutOfRange_FailsRangeRule(int z) { // Arrange var request = new TileInventoryRequest { Tiles = new[] { new TileCoord { Z = z, X = 0, Y = 0 } } }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("tiles[0].z"); } [Theory] [InlineData(0)] [InlineData(18)] [InlineData(22)] public void Validate_TileZoomInRange_PassesRangeRule(int z) { // Arrange var maxAxis = (1 << z) - 1; var request = new TileInventoryRequest { Tiles = new[] { new TileCoord { Z = z, X = maxAxis, Y = maxAxis } } }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldNotHaveValidationErrorFor("tiles[0].z"); } [Fact] public void Validate_TileXNegative_FailsRangeRule() { // Arrange var request = new TileInventoryRequest { Tiles = new[] { new TileCoord { Z = 18, X = -1, Y = 0 } } }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("tiles[0].x"); } [Fact] public void Validate_TileXAtUpperBound_FailsRangeRule() { // Arrange — at z=2, valid x is 0..3, so x=4 is invalid var request = new TileInventoryRequest { Tiles = new[] { new TileCoord { Z = 2, X = 4, Y = 0 } } }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("tiles[0].x") .WithErrorMessage("`x` must be < 2^z = 4 for z=2."); } [Fact] public void Validate_TileYNegative_FailsRangeRule() { // Arrange var request = new TileInventoryRequest { Tiles = new[] { new TileCoord { Z = 18, X = 0, Y = -1 } } }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("tiles[0].y"); } [Fact] public void Validate_TileYAtUpperBound_FailsRangeRule() { // Arrange — at z=0, valid y is 0..0, so y=1 is invalid var request = new TileInventoryRequest { Tiles = new[] { new TileCoord { Z = 0, X = 0, Y = 1 } } }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldHaveValidationErrorFor("tiles[0].y"); } [Fact] public void Validate_AxesAtMaxForZoom_Passes() { // Arrange — at z=18, valid x/y is 0..(2^18 - 1) = 0..262143 var request = new TileInventoryRequest { Tiles = new[] { new TileCoord { Z = 18, X = 262_143, Y = 262_143 } } }; // Act var result = _validator.TestValidate(request); // Assert result.ShouldNotHaveAnyValidationErrors(); } }