Files
atomic-design-poc/backend/tests/BigRegister.Tests/IdempotencyTests.cs
T
ehoandClaude Sonnet 5 8560746d15 refactor: strip WP-/RB- ticket refs from backend (RD-19)
The backend half of the sweep RD-18 did for the front end. git blame
holds the provenance and stays correct when the code moves; the
comment names a closed ticket and tells the reader nothing the
sentence around it does not.

public/letter.css and LetterHtml.golden.html change together, because
the renderer inlines the CSS and the golden file snapshots the
result.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 21:48:08 +02:00

95 lines
3.7 KiB
C#

using System.Net.Http.Json;
using BigRegister.Api.Contracts;
using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
public class IdempotencyTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
{
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 { telefoon = "0612345678" }),
};
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);
}
// BIO-018: IdempotencyStore used to key on the raw client-supplied header alone, so
// caller B replaying caller A's Idempotency-Key got caller A's cached reference back —
// a cross-caller leak of a value caller B never submitted. The store now keys on
// "{SubjectId}:{idemKey}", so the same header value from two different callers is two
// independent submissions.
[Fact]
public async Task A_caller_replaying_another_callers_idempotency_key_does_not_get_their_cached_result()
{
var sharedKey = Guid.NewGuid().ToString();
var callerARequest = ChangeRequestWithKey(sharedKey);
callerARequest.Headers.Add("X-Subject", "111222333");
var callerA = await _client.SendAsync(callerARequest);
callerA.EnsureSuccessStatusCode();
var callerABody = await callerA.Content.ReadFromJsonAsync<ReferentieResponse>();
var callerBRequest = ChangeRequestWithKey(sharedKey);
callerBRequest.Headers.Add("X-Subject", "999888777");
var callerB = await _client.SendAsync(callerBRequest);
callerB.EnsureSuccessStatusCode();
var callerBBody = await callerB.Content.ReadFromJsonAsync<ReferentieResponse>();
Assert.NotEqual(callerABody!.Referentie, callerBBody!.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 { telefoon = "nope" }),
};
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 { telefoon = "nope" }),
};
replayRequest.Headers.Add("Idempotency-Key", key);
var replay = await _client.SendAsync(replayRequest);
Assert.Equal(System.Net.HttpStatusCode.UnprocessableEntity, replay.StatusCode);
}
}