using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Nexus.Api.Http; using Xunit; namespace Nexus.Api.Tests; public sealed class ProblemDetailsContractTests { [Theory] [InlineData(400, NexusProblemCodes.ValidationFailed)] [InlineData(401, NexusProblemCodes.Unauthenticated)] [InlineData(403, NexusProblemCodes.Forbidden)] [InlineData(409, NexusProblemCodes.Conflict)] [InlineData(429, NexusProblemCodes.RateLimited)] [InlineData(503, NexusProblemCodes.DependencyUnavailable)] [InlineData(504, NexusProblemCodes.Timeout)] [InlineData(500, NexusProblemCodes.InternalError)] public void Defaults_AddStableCodeAndTraceId(int status, string expectedCode) { var context = new DefaultHttpContext { TraceIdentifier = "trace-contract" }; var problem = new ProblemDetails { Status = status }; NexusProblemDetailsDefaults.Apply(problem, context); Assert.Equal(expectedCode, problem.Extensions["code"]); Assert.Equal("trace-contract", problem.Extensions["traceId"]); Assert.False(string.IsNullOrWhiteSpace(problem.Title)); Assert.False(string.IsNullOrWhiteSpace(problem.Type)); } [Fact] public void Defaults_PreserveExplicitMachineCode() { var context = new DefaultHttpContext { TraceIdentifier = "trace-contract" }; var problem = new ProblemDetails { Status = 409 }; problem.Extensions["code"] = "config_hash_conflict"; NexusProblemDetailsDefaults.Apply(problem, context); Assert.Equal("config_hash_conflict", problem.Extensions["code"]); } [Fact] public void LegacyError_IsConvertedWithoutLosingTheStableContract() { var context = new DefaultHttpContext { TraceIdentifier = "trace-legacy" }; var problem = NexusProblemDetailsDefaults.FromLegacyError( new { error = "Task not found." }, StatusCodes.Status404NotFound, context); Assert.NotNull(problem); Assert.Equal("Task not found.", problem.Detail); Assert.Equal(NexusProblemCodes.NotFound, problem.Extensions["code"]); Assert.Equal("trace-legacy", problem.Extensions["traceId"]); } [Fact] public void LegacySuccessPayload_IsNotConverted() { var context = new DefaultHttpContext(); var problem = NexusProblemDetailsDefaults.FromLegacyError( new { error = "informational field" }, StatusCodes.Status200OK, context); Assert.Null(problem); } [Fact] public void SharedResult_UsesProblemJsonAndPreservesTypedExtensions() { var result = NexusHttpResults.Problem( StatusCodes.Status409Conflict, "The agent file changed.", extensions: new Dictionary { ["currentHash"] = "hash-current" }); Assert.Equal( "application/problem+json", Assert.IsAssignableFrom(result).ContentType); var problem = Assert.IsType( Assert.IsAssignableFrom(result).Value); Assert.Equal(NexusProblemCodes.Conflict, problem.Extensions["code"]); Assert.Equal("hash-current", problem.Extensions["currentHash"]); } }