[AZ-812] Region API: rename Latitude/Longitude → Lat/Lon (OSM convention)

Mirror of AZ-794 (inventory z/x/y rename). RequestRegionRequest.cs renames C#
props Latitude→Lat / Longitude→Lon and adds [JsonPropertyName("lat"/"lon")] so
the wire format is unambiguous under the AZ-795 strict-parsing stack
(UnmappedMemberHandling.Disallow → legacy {"latitude":..,"longitude":..} now
returns HTTP 400 instead of silently coercing).

Updates all in-repo consumers: API handler (Program.cs), integration tests
(Models.cs, RegionTests.cs, IdempotentPostTests.cs, SecurityTests.cs), the
performance harness (run-performance-tests.sh PT-03/04/05/07), and module
docs (common_dtos.md, api_program.md; system-flows.md F2 already used
lat/lon). New RegionFieldRenameTests.cs covers AC-4 both directions (new
format → 200, legacy format → 400). Smoke green; no regressions.

region-request.md contract doc not bumped here — AZ-808 publishes v1.0.0
directly with the post-rename names per AZ-812 coordination clause.

Batch 01 of cycle 8. PASS_WITH_WARNINGS (one Low DRY finding for follow-up
test-helper consolidation; details in
_docs/03_implementation/reviews/batch_01_cycle8_review.md).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-22 15:54:53 +03:00
parent 0810a89ef1
commit fcd494f67e
14 changed files with 268 additions and 18 deletions
@@ -30,8 +30,8 @@ public static class IdempotentPostTests
var body = JsonSerializer.Serialize(new
{
id = regionId,
latitude = 47.4617,
longitude = 37.6470,
lat = 47.4617,
lon = 37.6470,
sizeMeters = 200,
zoomLevel = 18,
stitchTiles = false,
+7 -2
View File
@@ -17,8 +17,13 @@ public record DownloadTileResponse
public record RequestRegionRequest
{
public Guid Id { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("lat")]
public double Lat { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("lon")]
public double Lon { get; set; }
public double SizeMeters { get; set; }
public int ZoomLevel { get; set; }
public bool StitchTiles { get; set; } = false;
@@ -140,6 +140,7 @@ class Program
await IdempotentPostTests.RunAll(httpClient);
await TileInventoryTests.RunAll(httpClient);
await TileInventoryValidationTests.RunAll(httpClient);
await RegionFieldRenameTests.RunAll(httpClient);
await LeafletPathIndexOnlyTests.RunAll(connectionString);
await MigrationTests.RunAll();
}
@@ -164,6 +165,7 @@ class Program
await IdempotentPostTests.RunAll(httpClient);
await TileInventoryTests.RunAll(httpClient);
await TileInventoryValidationTests.RunAll(httpClient);
await RegionFieldRenameTests.RunAll(httpClient);
await LeafletPathIndexOnlyTests.RunAll(connectionString);
await MigrationTests.RunAll();
}
@@ -0,0 +1,111 @@
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
namespace SatelliteProvider.IntegrationTests;
// AZ-812: wire-format rename for POST /api/satellite/request.
// `RequestRegionRequest` now uses `lat`/`lon` (OSM convention) on the wire,
// replacing the previous verbose `latitude`/`longitude`. The strict-parsing
// infrastructure landed by AZ-795 (UnmappedMemberHandling.Disallow +
// GlobalExceptionHandler) means the old wire format must now be rejected
// explicitly, not silently coerced. AC-4 from the AZ-812 task spec.
public static class RegionFieldRenameTests
{
private const string RegionPath = "/api/satellite/request";
public static async Task RunAll(HttpClient httpClient)
{
RouteTestHelpers.PrintTestHeader("Test: Region endpoint OSM field-name rename (AZ-812)");
await NewLatLonFormat_Returns200(httpClient);
await OldLatitudeLongitudeFormat_Returns400(httpClient);
Console.WriteLine("✓ Region field-rename tests: PASSED");
}
private static async Task NewLatLonFormat_Returns200(HttpClient httpClient)
{
Console.WriteLine();
Console.WriteLine("AZ-812 AC-4 (positive): new {lat,lon} wire format → HTTP 200");
// Arrange
var regionId = Guid.NewGuid();
var body = $"{{\"id\":\"{regionId}\",\"lat\":47.461747,\"lon\":37.647063,\"sizeMeters\":200,\"zoomLevel\":18,\"stitchTiles\":false}}";
// Act
var response = await PostJsonAsync(httpClient, body);
var status = (int)response.StatusCode;
var responseBody = await response.Content.ReadAsStringAsync();
// Assert
if (status != 200)
{
throw new Exception($"AZ-812 AC-4 positive: expected HTTP 200 for {{lat,lon}} body, got {status}. Body: {responseBody}");
}
Console.WriteLine(" ✓ {lat,lon} body accepted with HTTP 200");
}
private static async Task OldLatitudeLongitudeFormat_Returns400(HttpClient httpClient)
{
Console.WriteLine();
Console.WriteLine("AZ-812 AC-4 (negative): legacy {latitude,longitude} wire format → HTTP 400 (UnmappedMemberHandling.Disallow)");
// Arrange — exact pre-AZ-812 wire format; must now fail explicitly instead
// of silently mapping to the renamed Lat/Lon properties.
var regionId = Guid.NewGuid();
var body = $"{{\"id\":\"{regionId}\",\"latitude\":47.461747,\"longitude\":37.647063,\"sizeMeters\":200,\"zoomLevel\":18,\"stitchTiles\":false}}";
// Act
var response = await PostJsonAsync(httpClient, body);
var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-812 legacy field names");
// Assert
ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-812 legacy field names");
AssertErrorsContainsMention(problem, expectedMention: "latitude", label: "AZ-812 legacy field names");
Console.WriteLine(" ✓ Legacy {latitude,longitude} body rejected with HTTP 400; errors map names the unknown field");
}
private static Task<HttpResponseMessage> PostJsonAsync(HttpClient httpClient, string body)
{
var content = new StringContent(body, Encoding.UTF8, "application/json");
return httpClient.PostAsync(RegionPath, content);
}
private static void AssertErrorsContainsMention(JsonElement problem, string expectedMention, string label)
{
if (!problem.TryGetProperty("errors", out var errorsEl) || errorsEl.ValueKind != JsonValueKind.Object)
{
throw new Exception($"{label}: expected 'errors' object in ProblemDetails body.");
}
var found = false;
foreach (var prop in errorsEl.EnumerateObject())
{
if (prop.Name.Contains(expectedMention, StringComparison.OrdinalIgnoreCase))
{
found = true;
break;
}
foreach (var msg in prop.Value.EnumerateArray())
{
if (msg.GetString()?.Contains(expectedMention, StringComparison.OrdinalIgnoreCase) == true)
{
found = true;
break;
}
}
if (found) break;
}
if (!found)
{
var paths = string.Join(", ", errorsEl.EnumerateObject().Select(p => p.Name));
throw new Exception($"{label}: expected '{expectedMention}' to appear in errors keys or messages. Available paths: {paths}.");
}
}
}
@@ -84,8 +84,8 @@ public static class RegionTests
var requestRegion = new RequestRegionRequest
{
Id = regionId,
Latitude = latitude,
Longitude = longitude,
Lat = latitude,
Lon = longitude,
SizeMeters = sizeMeters,
ZoomLevel = zoomLevel,
StitchTiles = stitchTiles
@@ -66,7 +66,7 @@ public static class SecurityTests
Console.WriteLine("SEC-03: Oversized region request (sizeMeters beyond allowed cap)");
var regionId = Guid.NewGuid();
var body = $"{{\"id\":\"{regionId}\",\"latitude\":47.461747,\"longitude\":37.647063,\"sizeMeters\":1000000,\"zoomLevel\":18,\"stitchTiles\":false}}";
var body = $"{{\"id\":\"{regionId}\",\"lat\":47.461747,\"lon\":37.647063,\"sizeMeters\":1000000,\"zoomLevel\":18,\"stitchTiles\":false}}";
var content = new StringContent(body, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync("/api/satellite/request", content);
var status = (int)response.StatusCode;