160 lines
7.6 KiB
C#
160 lines
7.6 KiB
C#
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);
|
|
}
|
|
}
|