mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 15:31:14 +00:00
[AZ-794] [AZ-795] [AZ-796] Strict input validation + z/x/y rename
AZ-794: rename inventory wire fields tileZoom/tileX/tileY -> z/x/y to match the slippy-map URL convention. Contract bumped to v2.0.0. AZ-795: shared validation infrastructure -- FluentValidation + ValidationEndpointFilter + GlobalValidatorConfig (camelCase paths). GlobalExceptionHandler now converts JsonException (UnmappedMember + JsonRequired) into RFC 7807 ValidationProblemDetails. JSON layer hardened with UnmappedMemberHandling.Disallow + camelCase naming policy. New error-shape.md contract. AZ-796: InventoryRequestValidator covers 9 rules (XOR tiles vs locationHashes, cap 1000, z 0..22, x/y in slippy bounds, hash length/charset). 16 unit tests + 16 integration tests + a manual curl probe script. Adjacent fixes uncovered by the new strict layer: - IdempotentPostTests RoutePoint payload corrected to lat/lon (the DTO has used JsonPropertyName for ages; previously silently ignored under PascalCase fallback). - TileInventoryTests slippy x/y reduced to fit z=18 bounds. - docker-compose.yml host port for Postgres moved 5432 -> 5433 to avoid sibling-project conflict; appsettings.Development + README + AGENTS + architecture + containerization docs aligned. New coderule (suite + repo): API consumer-facing OpenAPI descriptions must not contain task IDs, contract filenames, or version-bump history -- internal change tracking belongs in commits/contract docs/changelogs. Existing offending descriptions in Program.cs cleaned up. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -60,6 +61,30 @@ public sealed class GlobalExceptionHandler : IExceptionHandler
|
||||
{
|
||||
httpContext.Response.StatusCode = badRequest.StatusCode;
|
||||
|
||||
// AZ-795: deserialization failures (unknown field via UnmappedMemberHandling.Disallow,
|
||||
// type mismatch, malformed JSON) surface here as BadHttpRequestException with a
|
||||
// System.Text.Json `JsonException` somewhere in the inner-exception chain. Convert
|
||||
// them to RFC 7807 ValidationProblemDetails so wire-format errors share the same
|
||||
// shape as FluentValidation business-rule errors — see
|
||||
// `_docs/02_document/contracts/api/error-shape.md`.
|
||||
var deserializationErrors = TryExtractDeserializationErrors(badRequest);
|
||||
if (deserializationErrors is not null && badRequest.StatusCode == StatusCodes.Status400BadRequest)
|
||||
{
|
||||
var validation = new ValidationProblemDetails(deserializationErrors)
|
||||
{
|
||||
Status = badRequest.StatusCode,
|
||||
Title = "One or more validation errors occurred.",
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
||||
};
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(
|
||||
validation,
|
||||
options: null,
|
||||
contentType: "application/problem+json",
|
||||
cancellationToken: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var problem = new ProblemDetails
|
||||
{
|
||||
Status = badRequest.StatusCode,
|
||||
@@ -73,4 +98,36 @@ public sealed class GlobalExceptionHandler : IExceptionHandler
|
||||
contentType: "application/problem+json",
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private static IDictionary<string, string[]>? TryExtractDeserializationErrors(BadHttpRequestException ex)
|
||||
{
|
||||
var current = ex.InnerException;
|
||||
while (current is not null)
|
||||
{
|
||||
if (current is JsonException jsonEx)
|
||||
{
|
||||
var path = NormalizeJsonPath(jsonEx.Path);
|
||||
var message = string.IsNullOrEmpty(jsonEx.Message)
|
||||
? "Invalid JSON."
|
||||
: jsonEx.Message;
|
||||
|
||||
return new Dictionary<string, string[]>
|
||||
{
|
||||
[path] = new[] { message }
|
||||
};
|
||||
}
|
||||
|
||||
current = current.InnerException;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string NormalizeJsonPath(string? path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return "$";
|
||||
return path.StartsWith("$.", StringComparison.Ordinal)
|
||||
? path.Substring(2)
|
||||
: path;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user