Files
itworx-knowledge-hub-Public/tests/backend/ItWorx.KnowledgeHub.IntegrationTests/Automation/IdempotencyTests.cs
T

84 lines
4.3 KiB
C#

using System.Net;
using System.Net.Http.Json;
using ItWorx.KnowledgeHub.Contracts.Projects;
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
namespace ItWorx.KnowledgeHub.IntegrationTests.Automation;
/// <summary>A retried create/append request carrying the same
/// <c>Idempotency-Key</c> must never execute the mutation twice
/// (docs/09-api-contract.md).</summary>
[Collection(PostgresTestGroup.Name)]
public sealed class IdempotencyTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
{
[Fact]
public async Task Retrying_a_project_update_with_the_same_Idempotency_Key_does_not_create_a_second_update()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var projectResponse = await SetupHelper.PostWithCsrfAsync(
client, "/api/v1/projects",
new CreateProjectRequest("Idempotentieproject", "S.", "Medium", "Start.", Guid.NewGuid()));
var project = await projectResponse.Content.ReadFromJsonAsync<ProjectDetail>();
Assert.NotNull(project);
var idempotencyKey = Guid.NewGuid().ToString();
var request = new CreateProjectUpdateRequest(
"Progress", "Idempotente update", "Samenvatting.", "Reported", null, null, null, null, null, null, null, null, null, null);
var first = await PostWithIdempotencyKeyAsync(client, $"/api/v1/projects/{project.Id}/updates", request, idempotencyKey);
Assert.Equal(HttpStatusCode.OK, first.StatusCode);
var firstBody = await first.Content.ReadFromJsonAsync<ProjectUpdateSummary>();
var second = await PostWithIdempotencyKeyAsync(client, $"/api/v1/projects/{project.Id}/updates", request, idempotencyKey);
Assert.Equal(HttpStatusCode.OK, second.StatusCode);
var secondBody = await second.Content.ReadFromJsonAsync<ProjectUpdateSummary>();
// The replayed response is the exact cached original — same update ID — not a
// freshly created second row.
Assert.Equal(firstBody!.Id, secondBody!.Id);
var timeline = await client.GetFromJsonAsync<ProjectUpdatesEnvelope>($"/api/v1/projects/{project.Id}/updates");
Assert.NotNull(timeline);
Assert.Single(timeline.Items, update => update.Title == "Idempotente update");
}
[Fact]
public async Task Reusing_an_Idempotency_Key_with_different_request_content_is_a_conflict()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var projectResponse = await SetupHelper.PostWithCsrfAsync(
client, "/api/v1/projects",
new CreateProjectRequest("Conflictproject idempotentie", "S.", "Medium", "Start.", Guid.NewGuid()));
var project = await projectResponse.Content.ReadFromJsonAsync<ProjectDetail>();
Assert.NotNull(project);
var idempotencyKey = Guid.NewGuid().ToString();
var first = new CreateProjectUpdateRequest(
"Progress", "Eerste inhoud", "S.", "Reported", null, null, null, null, null, null, null, null, null, null);
var different = new CreateProjectUpdateRequest(
"Progress", "Andere inhoud", "S.", "Reported", null, null, null, null, null, null, null, null, null, null);
var firstResponse = await PostWithIdempotencyKeyAsync(client, $"/api/v1/projects/{project.Id}/updates", first, idempotencyKey);
Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode);
var conflictResponse = await PostWithIdempotencyKeyAsync(client, $"/api/v1/projects/{project.Id}/updates", different, idempotencyKey);
Assert.Equal(HttpStatusCode.Conflict, conflictResponse.StatusCode);
}
private static async Task<HttpResponseMessage> PostWithIdempotencyKeyAsync<TBody>(
HttpClient client, string path, TBody body, string idempotencyKey)
{
var csrf = await SetupHelper.GetCsrfTokenAsync(client);
using var request = new HttpRequestMessage(HttpMethod.Post, path) { Content = JsonContent.Create(body) };
request.Headers.Add(csrf.HeaderName, csrf.Value);
request.Headers.Add("Idempotency-Key", idempotencyKey);
return await client.SendAsync(request);
}
private sealed record ProjectUpdatesEnvelope(IReadOnlyList<ProjectUpdateSummary> Items, string? NextCursor);
}