Correlation id becomes real ASP.NET Core middleware instead of a per-endpoint
read: every request gets one (client-supplied or generated), it's echoed as
an X-Correlation-Id response header, and pushed into the logging scope so
every log line for that request carries it — not just the Submit helper's,
verified against LogBrief which never threads it explicitly.
Idempotency-Key moves from per-HTTP-attempt (defeating its own purpose) to
per-logical-submit: runSubmit mints one key and threads it through a small
bridge (withIdempotencyKey/currentIdempotencyKey) since the NSwag-generated
client has no per-call header hook. Backend gains an IdempotencyStore that
short-circuits a replayed key to the first call's result instead of minting
a second reference — scoped to the Submit-helper endpoints per the WP's own
decision.
GET requests now retry transient failures (rxjs retry({count:2, delay:500}));
writes never auto-retry. Proven with a fake-HttpClient spec
(api-client.provider.spec.ts) rather than a manual network-tab check — the
WP's suggested `?scenario=error` check turned out not to exercise a real
network call at all (the interceptor throws before calling next()), so the
automated test is the actual proof.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
70 lines
2.7 KiB
C#
70 lines
2.7 KiB
C#
using System.Net.Http.Json;
|
|
using BigRegister.Api.Contracts;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
|
|
namespace BigRegister.Tests;
|
|
|
|
public class IdempotencyTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
|
|
{
|
|
private readonly HttpClient _client = factory.CreateClient();
|
|
|
|
private static HttpRequestMessage ChangeRequestWithKey(string key)
|
|
{
|
|
var req = new HttpRequestMessage(HttpMethod.Post, "/api/v1/change-requests")
|
|
{
|
|
Content = JsonContent.Create(new { straat = "Lange Voorhout 9", postcode = "2514 EA", woonplaats = "Den Haag" }),
|
|
};
|
|
req.Headers.Add("Idempotency-Key", key);
|
|
return req;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Replaying_the_same_idempotency_key_returns_the_same_reference_not_a_new_one()
|
|
{
|
|
var key = Guid.NewGuid().ToString();
|
|
|
|
var first = await _client.SendAsync(ChangeRequestWithKey(key));
|
|
first.EnsureSuccessStatusCode();
|
|
var firstBody = await first.Content.ReadFromJsonAsync<ReferentieResponse>();
|
|
|
|
var replay = await _client.SendAsync(ChangeRequestWithKey(key));
|
|
replay.EnsureSuccessStatusCode();
|
|
var replayBody = await replay.Content.ReadFromJsonAsync<ReferentieResponse>();
|
|
|
|
Assert.Equal(firstBody!.Referentie, replayBody!.Referentie);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Different_idempotency_keys_are_independent_submissions()
|
|
{
|
|
var first = await _client.SendAsync(ChangeRequestWithKey(Guid.NewGuid().ToString()));
|
|
var second = await _client.SendAsync(ChangeRequestWithKey(Guid.NewGuid().ToString()));
|
|
var firstBody = await first.Content.ReadFromJsonAsync<ReferentieResponse>();
|
|
var secondBody = await second.Content.ReadFromJsonAsync<ReferentieResponse>();
|
|
|
|
Assert.NotEqual(firstBody!.Referentie, secondBody!.Referentie);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_rejected_submission_replays_the_same_rejection_not_a_retry()
|
|
{
|
|
var key = Guid.NewGuid().ToString();
|
|
var badRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/change-requests")
|
|
{
|
|
Content = JsonContent.Create(new { straat = "Straat 1", postcode = "nope", woonplaats = "Den Haag" }),
|
|
};
|
|
badRequest.Headers.Add("Idempotency-Key", key);
|
|
|
|
var first = await _client.SendAsync(badRequest);
|
|
Assert.Equal(System.Net.HttpStatusCode.UnprocessableEntity, first.StatusCode);
|
|
|
|
var replayRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/change-requests")
|
|
{
|
|
Content = JsonContent.Create(new { straat = "Straat 1", postcode = "nope", woonplaats = "Den Haag" }),
|
|
};
|
|
replayRequest.Headers.Add("Idempotency-Key", key);
|
|
var replay = await _client.SendAsync(replayRequest);
|
|
Assert.Equal(System.Net.HttpStatusCode.UnprocessableEntity, replay.StatusCode);
|
|
}
|
|
}
|