diff --git a/src/Bifrost.Web/Features/Heimdall/HeimdallIdentityMoveClient.cs b/src/Bifrost.Web/Features/Heimdall/HeimdallIdentityMoveClient.cs index e485b37..f285ae2 100644 --- a/src/Bifrost.Web/Features/Heimdall/HeimdallIdentityMoveClient.cs +++ b/src/Bifrost.Web/Features/Heimdall/HeimdallIdentityMoveClient.cs @@ -33,7 +33,7 @@ public enum HeimdallProofTier public sealed record HeimdallMoveReceipt( string MoveId, string AccountId, IReadOnlyList MovedIdentityIds, HeimdallProofTier ProofFrom, HeimdallProofTier? ProofTo, string? ReversesMoveId); -/// Heimdall's fixed refusal codes for a move, plus the one local class: Heimdall's schema 400. +/// Heimdall's fixed refusal codes for a move, plus invalid_request, Heimdall's own schema 400. public enum HeimdallMoveRefusal { AppAuthRequired, @@ -128,12 +128,6 @@ public sealed partial class HeimdallIdentityMoveClient( return new HeimdallMoveOutcome.Unknown($"HTTP {status}"); } - // Fastify's schema 400 answers before the route runs, so it carries no Heimdall code and applied nothing. - if (status == 400) - { - return new HeimdallMoveOutcome.Refused(HeimdallMoveRefusal.RequestInvalid); - } - return Refused(await HeimdallRefusal.ReadAsync(response, token), status); } catch (Exception) when (!cancellationToken.IsCancellationRequested) @@ -144,7 +138,7 @@ public sealed partial class HeimdallIdentityMoveClient( } /// - /// Refused only for the coded refusals Heimdall's move route emits (4fe367a moveIdentitiesRoute). A 4xx without one of those codes + /// Refused only for the coded refusals Heimdall's move route emits (4fe367a moveIdentitiesRoute, and its error handler's invalid_request 400). A 4xx without one of those codes /// (a gateway's HTML 404, a 408, a code a later Heimdall adds) proves nothing was applied, so it is Unknown. move_id_conflict is /// Unknown too: a committed move answers it when its proofs no longer verify. /// @@ -167,6 +161,8 @@ public sealed partial class HeimdallIdentityMoveClient( "already_moved" => HeimdallMoveRefusal.AlreadyMoved, "provider_conflict" => HeimdallMoveRefusal.ProviderConflict, "identity_not_found" => HeimdallMoveRefusal.IdentityNotFound, + // Heimdall's error handler gives its own 400 this code; an nginx or other edge 400 has none, so it is not Heimdall's refusal. + "invalid_request" when status == 400 => HeimdallMoveRefusal.RequestInvalid, _ => (HeimdallMoveRefusal?)null }; if (code is not { } known) diff --git a/tests/Bifrost.Web.Tests/FakeHeimdallMoverTests.cs b/tests/Bifrost.Web.Tests/FakeHeimdallMoverTests.cs index b49c8be..c856c8f 100644 --- a/tests/Bifrost.Web.Tests/FakeHeimdallMoverTests.cs +++ b/tests/Bifrost.Web.Tests/FakeHeimdallMoverTests.cs @@ -71,7 +71,7 @@ public sealed class FakeHeimdallMoverTests }; foreach (var json in bad) { - Assert.Equal(HttpStatusCode.BadRequest, (await Post(host, json)).Status); + Assert.Equal((HttpStatusCode.BadRequest, "invalid_request"), await Post(host, json)); } Assert.Single(moves.Stored); diff --git a/tests/Bifrost.Web.Tests/HeimdallIdentityMoveClientTests.cs b/tests/Bifrost.Web.Tests/HeimdallIdentityMoveClientTests.cs index 8055c6d..a2cc873 100644 --- a/tests/Bifrost.Web.Tests/HeimdallIdentityMoveClientTests.cs +++ b/tests/Bifrost.Web.Tests/HeimdallIdentityMoveClientTests.cs @@ -292,7 +292,7 @@ public sealed class HeimdallIdentityMoveClientTests } [Fact] - public async Task A_400_is_RequestInvalid_a_definitive_refusal_that_moves_nothing() + public async Task A_400_carrying_Heimdalls_invalid_request_code_is_RequestInvalid_a_definitive_refusal_that_moves_nothing() { using var host = new HeimdallHost(); var moves = host.Heimdall.Moves; @@ -307,6 +307,11 @@ public sealed class HeimdallIdentityMoveClientTests [Theory] [InlineData(HttpStatusCode.NotFound, "404 Not Found")] [InlineData(HttpStatusCode.NotFound, "")] + [InlineData(HttpStatusCode.BadRequest, "400 Bad Requestnginx")] + [InlineData(HttpStatusCode.BadRequest, "")] + [InlineData(HttpStatusCode.BadRequest, """{"statusCode":400,"error":"Bad Request","message":"x"}""")] + [InlineData(HttpStatusCode.BadRequest, """{"message":"no error field"}""")] + [InlineData(HttpStatusCode.Conflict, """{"error":"invalid_request"}""")] [InlineData(HttpStatusCode.RequestTimeout, "")] [InlineData(HttpStatusCode.RequestTimeout, """{"error":"request_timeout"}""")] [InlineData(HttpStatusCode.Gone, "")] diff --git a/tests/Bifrost.Web.Tests/Support/FakeMoves.cs b/tests/Bifrost.Web.Tests/Support/FakeMoves.cs index ef3eb35..1a87171 100644 --- a/tests/Bifrost.Web.Tests/Support/FakeMoves.cs +++ b/tests/Bifrost.Web.Tests/Support/FakeMoves.cs @@ -102,7 +102,7 @@ public sealed class FakeMoves(Func now) if (Parse(body) is not { } move) { - return Json(HttpStatusCode.BadRequest, """{"statusCode":400,"error":"Bad Request","message":"body does not match the schema"}"""); + return Json(HttpStatusCode.BadRequest, """{"error":"invalid_request","detail":"body: must match the schema"}"""); } if (MoveStatus is { } forced) @@ -110,7 +110,7 @@ public sealed class FakeMoves(Func now) return forced == HttpStatusCode.NotFound ? new HttpResponseMessage(forced) : Json(forced, forced == HttpStatusCode.BadRequest - ? """{"statusCode":400,"error":"Bad Request"}""" + ? """{"error":"invalid_request","detail":"body: must match the schema"}""" : """{"error":"server_error","detail":"HEIMDALL FREE TEXT"}"""); }