84 lines
4.3 KiB
C#
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);
|
|
}
|