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."); } } private static IEnumerable EnumeratePaths(JsonElement errorsEl) { foreach (var prop in errorsEl.EnumerateObject()) { yield return prop.Name; } } }