using System.Text.Json; namespace SatelliteProvider.IntegrationTests; // AZ-795: shared ProblemDetails / ValidationProblemDetails assertion helper // for integration tests. Every endpoint that emits a 4xx error MUST produce // a body matching the contract in // `_docs/02_document/contracts/api/error-shape.md` (v1.0.0). Tests use this // helper instead of re-deriving the shape per call site. public static class ProblemDetailsAssertions { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; public static async Task ReadProblemDetailsAsync(HttpResponseMessage response, string label) { var contentType = response.Content.Headers.ContentType?.MediaType; if (contentType is null || !contentType.Contains("application/problem+json", StringComparison.OrdinalIgnoreCase)) { var body = await response.Content.ReadAsStringAsync(); throw new Exception( $"{label}: expected Content-Type 'application/problem+json', got '{contentType}'. Body: {body}"); } var stream = await response.Content.ReadAsStreamAsync(); using var doc = await JsonDocument.ParseAsync(stream); return doc.RootElement.Clone(); } public static void AssertValidationProblem( JsonElement problem, int expectedStatus, string label, string? expectedErrorPath = null, string? expectedErrorContains = null) { if (!problem.TryGetProperty("status", out var statusEl) || statusEl.GetInt32() != expectedStatus) { throw new Exception( $"{label}: expected status={expectedStatus}, got {(statusEl.ValueKind == JsonValueKind.Number ? statusEl.GetInt32().ToString() : "missing")}"); } if (!problem.TryGetProperty("title", out var titleEl) || string.IsNullOrEmpty(titleEl.GetString())) { throw new Exception($"{label}: expected non-empty 'title', got missing/empty."); } if (!problem.TryGetProperty("errors", out var errorsEl) || errorsEl.ValueKind != JsonValueKind.Object) { throw new Exception($"{label}: expected 'errors' object, got {errorsEl.ValueKind}."); } if (expectedErrorPath is not null) { if (!errorsEl.TryGetProperty(expectedErrorPath, out var fieldEl) || fieldEl.ValueKind != JsonValueKind.Array) { throw new Exception( $"{label}: expected errors['{expectedErrorPath}'] array, got {(errorsEl.TryGetProperty(expectedErrorPath, out var raw) ? raw.ValueKind.ToString() : "missing")}. " + $"Available paths: {string.Join(", ", EnumeratePaths(errorsEl))}."); } if (expectedErrorContains is not null) { var first = fieldEl.EnumerateArray().FirstOrDefault(); var firstStr = first.ValueKind == JsonValueKind.String ? first.GetString() : null; if (firstStr is null || !firstStr.Contains(expectedErrorContains, StringComparison.OrdinalIgnoreCase)) { throw new Exception( $"{label}: expected errors['{expectedErrorPath}'][0] to contain '{expectedErrorContains}', got '{firstStr}'."); } } } } public static void AssertProblemDetails( JsonElement problem, int expectedStatus, string label) { if (!problem.TryGetProperty("status", out var statusEl) || statusEl.GetInt32() != expectedStatus) { throw new Exception( $"{label}: expected status={expectedStatus}, got {(statusEl.ValueKind == JsonValueKind.Number ? statusEl.GetInt32().ToString() : "missing")}"); } if (!problem.TryGetProperty("title", out var titleEl) || string.IsNullOrEmpty(titleEl.GetString())) { throw new Exception($"{label}: expected non-empty 'title', got missing/empty."); } } // AZ-808 cycle 8: promoted from per-test-file private helpers (was // duplicated in TileInventoryValidationTests + RegionFieldRenameTests + // RegionRequestValidationTests) so every validation test points at one // source of truth for "is this field-name or substring mentioned anywhere // in the errors map?". public 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}."); } } private static IEnumerable EnumeratePaths(JsonElement errorsEl) { foreach (var prop in errorsEl.EnumerateObject()) { yield return prop.Name; } } }