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,159 @@
using System.Net;
using System.Net.Http.Json;
using ItWorx.KnowledgeHub.Contracts.Admin;
using ItWorx.KnowledgeHub.Contracts.Auth;
using ItWorx.KnowledgeHub.Contracts.Decisions;
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
namespace ItWorx.KnowledgeHub.IntegrationTests.Projects;
[Collection(PostgresTestGroup.Name)]
public sealed class DecisionTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
{
private async Task<HttpClient> AuthenticatedClientAsync()
{
var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
return client;
}
private static async Task<DecisionDetail> CreateDecisionAsync(HttpClient client, string title, Guid? supersedes = null)
{
var response = await SetupHelper.PostWithCsrfAsync(
client, "/api/v1/decisions",
new CreateDecisionRequest(null, null, title, "## Context\n\nEen besluit.", "Samenvatting van het besluit.", supersedes));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var decision = await response.Content.ReadFromJsonAsync<DecisionDetail>();
Assert.NotNull(decision);
return decision;
}
[Fact]
public async Task Creating_a_decision_creates_a_backing_ArchitectureDecision_page_with_a_profile()
{
using var client = await AuthenticatedClientAsync();
var decision = await CreateDecisionAsync(client, "Gebruik PostgreSQL als opslag");
Assert.Equal("Proposed", decision.Status);
Assert.Equal(1, decision.DecisionNumber);
Assert.Equal("Gebruik PostgreSQL als opslag", decision.Title);
var pageResponse = await client.GetAsync($"/api/v1/pages/{decision.PageId}");
Assert.Equal(HttpStatusCode.OK, pageResponse.StatusCode);
var page = await pageResponse.Content.ReadFromJsonAsync<ItWorx.KnowledgeHub.Contracts.Knowledge.PageDetail>();
Assert.Equal("ArchitectureDecision", page!.DocumentType);
}
[Fact]
public async Task Accepting_a_proposed_decision_transitions_its_status()
{
using var client = await AuthenticatedClientAsync();
var decision = await CreateDecisionAsync(client, "Gebruik trigger-based search_vector");
var response = await SetupHelper.PostWithCsrfAndIfMatchAsync(
client, $"/api/v1/decisions/{decision.Id}/accept", new { }, decision.ETag);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var accepted = await response.Content.ReadFromJsonAsync<DecisionDetail>();
Assert.Equal("Accepted", accepted!.Status);
}
[Fact]
public async Task Rejecting_an_already_accepted_decision_is_an_invalid_transition()
{
using var client = await AuthenticatedClientAsync();
var decision = await CreateDecisionAsync(client, "Al geaccepteerd besluit");
var acceptResponse = await SetupHelper.PostWithCsrfAndIfMatchAsync(
client, $"/api/v1/decisions/{decision.Id}/accept", new { }, decision.ETag);
var accepted = await acceptResponse.Content.ReadFromJsonAsync<DecisionDetail>();
var rejectResponse = await SetupHelper.PostWithCsrfAndIfMatchAsync(
client, $"/api/v1/decisions/{decision.Id}/reject", new { }, accepted!.ETag);
Assert.Equal(HttpStatusCode.Conflict, rejectResponse.StatusCode);
}
[Fact]
public async Task Accepting_a_decision_that_supersedes_an_accepted_one_flips_the_original_to_superseded_with_bidirectional_links()
{
using var client = await AuthenticatedClientAsync();
var original = await CreateDecisionAsync(client, "Oorspronkelijk besluit");
var acceptOriginalResponse = await SetupHelper.PostWithCsrfAndIfMatchAsync(
client, $"/api/v1/decisions/{original.Id}/accept", new { }, original.ETag);
var acceptedOriginal = await acceptOriginalResponse.Content.ReadFromJsonAsync<DecisionDetail>();
var replacement = await CreateDecisionAsync(client, "Vervangend besluit", acceptedOriginal!.Id);
var acceptReplacementResponse = await SetupHelper.PostWithCsrfAndIfMatchAsync(
client, $"/api/v1/decisions/{replacement.Id}/accept", new { }, replacement.ETag);
Assert.Equal(HttpStatusCode.OK, acceptReplacementResponse.StatusCode);
var acceptedReplacement = await acceptReplacementResponse.Content.ReadFromJsonAsync<DecisionDetail>();
Assert.Equal("Accepted", acceptedReplacement!.Status);
Assert.Equal(acceptedOriginal.Id, acceptedReplacement.Supersedes!.Id);
var originalAfter = await client.GetFromJsonAsync<DecisionDetail>($"/api/v1/decisions/{original.Id}");
Assert.Equal("Superseded", originalAfter!.Status);
Assert.Equal(replacement.Id, originalAfter.SupersededBy!.Id);
}
[Fact]
public async Task Withdrawing_a_proposed_decision_transitions_its_status()
{
using var client = await AuthenticatedClientAsync();
var decision = await CreateDecisionAsync(client, "In te trekken besluit");
var response = await SetupHelper.PostWithCsrfAndIfMatchAsync(
client, $"/api/v1/decisions/{decision.Id}/withdraw", new { }, decision.ETag);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var withdrawn = await response.Content.ReadFromJsonAsync<DecisionDetail>();
Assert.Equal("Withdrawn", withdrawn!.Status);
}
[Fact]
public async Task Decisions_appear_in_the_workspace_decision_list()
{
using var client = await AuthenticatedClientAsync();
var decision = await CreateDecisionAsync(client, "Vindbaar besluit");
var list = await client.GetFromJsonAsync<DecisionListResponse>("/api/v1/decisions");
Assert.NotNull(list);
Assert.Contains(list.Items, item => item.Id == decision.Id && item.Title == "Vindbaar besluit");
}
// Reuses the exact reader-boundary pattern already established in
// ProjectHubTests.A_reader_cannot_create_or_mutate_projects_but_can_read_them —
// V2-M5 found this same boundary was left untested for decisions/runbooks even
// though the endpoints themselves (DecisionsEndpoints.cs) already correctly
// require Editor for create/accept/reject/withdraw.
[Fact]
public async Task A_reader_can_read_decisions_but_cannot_create_or_transition_them()
{
using var adminClient = await AuthenticatedClientAsync();
var decision = await CreateDecisionAsync(adminClient, "Alleen-lezenbesluit");
const string readerEmail = "besluitlezer@itworx.test";
const string readerPassword = "reader-strong-password";
var createUserResponse = await SetupHelper.PostWithCsrfAsync(
adminClient, "/api/v1/admin/users", new CreateUserRequest("Testlezer", readerEmail, readerPassword, "Reader"));
Assert.Equal(HttpStatusCode.OK, createUserResponse.StatusCode);
using var readerClient = Factory.CreateClient();
await SetupHelper.LoginAsync(readerClient, readerEmail, readerPassword);
var readResponse = await readerClient.GetAsync($"/api/v1/decisions/{decision.Id}");
Assert.Equal(HttpStatusCode.OK, readResponse.StatusCode);
var createResponse = await SetupHelper.PostWithCsrfAsync(
readerClient, "/api/v1/decisions",
new CreateDecisionRequest(null, null, "Ongeautoriseerd besluit", null, "S", null));
Assert.Equal(HttpStatusCode.Forbidden, createResponse.StatusCode);
var acceptResponse = await SetupHelper.PostWithCsrfAndIfMatchAsync(
readerClient, $"/api/v1/decisions/{decision.Id}/accept", new { }, decision.ETag);
Assert.Equal(HttpStatusCode.Forbidden, acceptResponse.StatusCode);
}
}