using System.Net.Http.Json; using BigRegister.Api.Contracts; using Microsoft.AspNetCore.Mvc.Testing; namespace BigRegister.Tests; public class IdempotencyTests(TestWebApplicationFactory factory) : IClassFixture { 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(); var replay = await _client.SendAsync(ChangeRequestWithKey(key)); replay.EnsureSuccessStatusCode(); var replayBody = await replay.Content.ReadFromJsonAsync(); 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(); var secondBody = await second.Content.ReadFromJsonAsync(); Assert.NotEqual(firstBody!.Referentie, secondBody!.Referentie); } // RB-18/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(); var callerBRequest = ChangeRequestWithKey(sharedKey); callerBRequest.Headers.Add("X-Subject", "999888777"); var callerB = await _client.SendAsync(callerBRequest); callerB.EnsureSuccessStatusCode(); var callerBBody = await callerB.Content.ReadFromJsonAsync(); 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); } }