Initial curated itworx-knowledge-hub source from 3b1751d0d9dee551aa6994eee0093704dc334817

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:37:37 +02:00
commit ad8ad252a9
825 files changed
+124216

No files matched your search

@@ -0,0 +1,85 @@
using System.Net;
using System.Net.Http.Json;
using ItWorx.KnowledgeHub.Contracts.Knowledge;
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
namespace ItWorx.KnowledgeHub.IntegrationTests.Knowledge;
/// <summary>Concurrent edits must produce an explicit conflict, never a silently lost
/// update (docs/27-acceptance-matrix.md, M2-A05).</summary>
[Collection(PostgresTestGroup.Name)]
public sealed class PageConcurrencyTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
{
[Fact]
public async Task Publishing_with_a_stale_ETag_is_rejected_and_never_overwrites_the_newer_revision()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var createResponse = await SetupHelper.PostWithCsrfAsync(
client, "/api/v1/pages", new CreatePageRequest(null, "Conflicttest", "WikiPage", "Origineel", null));
var page = await createResponse.Content.ReadFromJsonAsync<PageDetail>();
Assert.NotNull(page);
var staleETag = page.ETag;
// Session A publishes successfully first, advancing the ETag.
var firstEdit = await SetupHelper.PutWithCsrfAsync(
client,
$"/api/v1/pages/{page.Id}",
new UpdatePageRequest("Titel van A", "Inhoud van A", "WikiPage", "Active", null),
staleETag);
Assert.Equal(HttpStatusCode.OK, firstEdit.StatusCode);
// Session B still holds the original (now stale) ETag from before A's publish.
var secondEdit = await SetupHelper.PutWithCsrfAsync(
client,
$"/api/v1/pages/{page.Id}",
new UpdatePageRequest("Titel van B", "Inhoud van B", "WikiPage", "Active", null),
staleETag);
Assert.Equal(HttpStatusCode.PreconditionFailed, secondEdit.StatusCode);
// The current server state is still A's edit, not silently overwritten by B.
var current = await client.GetFromJsonAsync<PageDetail>($"/api/v1/pages/{page.Id}");
Assert.NotNull(current);
Assert.Equal("Titel van A", current.Title);
}
[Fact]
public async Task Simultaneous_publishes_return_one_success_and_only_explicit_conflicts()
{
using var creator = Factory.CreateClient();
await SetupHelper.LoginAsync(creator, OwnerEmail, OwnerPassword);
var createResponse = await SetupHelper.PostWithCsrfAsync(
creator, "/api/v1/pages", new CreatePageRequest(null, "Gelijktijdige conflicttest", "WikiPage", "Origineel", null));
var page = await createResponse.Content.ReadFromJsonAsync<PageDetail>();
Assert.NotNull(page);
var clients = Enumerable.Range(0, 8).Select(_ => Factory.CreateClient()).ToArray();
try
{
await Task.WhenAll(clients.Select(client => SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword)));
var responses = await Task.WhenAll(clients.Select((client, index) =>
SetupHelper.PutWithCsrfAsync(
client,
$"/api/v1/pages/{page.Id}",
new UpdatePageRequest($"Kandidaat {index}", $"Inhoud {index}", "WikiPage", "Active", null),
page.ETag)));
Assert.Single(responses, response => response.StatusCode == HttpStatusCode.OK);
Assert.Equal(7, responses.Count(response => response.StatusCode == HttpStatusCode.PreconditionFailed));
}
finally
{
foreach (var client in clients)
{
client.Dispose();
}
}
var revisions = await creator.GetFromJsonAsync<IReadOnlyList<PageRevisionSummary>>($"/api/v1/pages/{page.Id}/revisions");
Assert.NotNull(revisions);
Assert.Equal(2, revisions.Count);
}
}