using System.Net; using System.Net.Http.Json; using ItWorx.KnowledgeHub.Contracts.Admin; using ItWorx.KnowledgeHub.Contracts.Auth; using ItWorx.KnowledgeHub.Contracts.Runbooks; using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure; namespace ItWorx.KnowledgeHub.IntegrationTests.Projects; [Collection(PostgresTestGroup.Name)] public sealed class RunbookTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres) { private async Task AuthenticatedClientAsync() { var client = Factory.CreateClient(); await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword); return client; } private static async Task CreateRunbookAsync(HttpClient client, string title) { var response = await SetupHelper.PostWithCsrfAsync( client, "/api/v1/runbooks", new CreateRunbookRequest(null, title, "## Doel\n\nHerstel de dienst.", null, "High", true, 45)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var runbook = await response.Content.ReadFromJsonAsync(); Assert.NotNull(runbook); return runbook; } [Fact] public async Task Creating_a_runbook_creates_a_backing_Runbook_page_with_a_profile_and_is_stale_until_validated() { using var client = await AuthenticatedClientAsync(); var runbook = await CreateRunbookAsync(client, "Databaseherstel"); Assert.Equal("Draft", runbook.Status); Assert.Equal("High", runbook.RiskLevel); Assert.True(runbook.IsStale); Assert.Null(runbook.LastValidatedAt); var pageResponse = await client.GetAsync($"/api/v1/pages/{runbook.PageId}"); var page = await pageResponse.Content.ReadFromJsonAsync(); Assert.Equal("Runbook", page!.DocumentType); } [Fact] public async Task Validating_a_runbook_against_its_current_revision_clears_staleness() { using var client = await AuthenticatedClientAsync(); var runbook = await CreateRunbookAsync(client, "Te valideren procedure"); var response = await SetupHelper.PostWithCsrfAndIfMatchAsync( client, $"/api/v1/runbooks/{runbook.Id}/validate", new { }, runbook.ETag); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var validated = await response.Content.ReadFromJsonAsync(); Assert.False(validated!.IsStale); Assert.NotNull(validated.LastValidatedAt); } [Fact] public async Task Editing_the_underlying_page_after_validation_makes_the_runbook_stale_again() { using var client = await AuthenticatedClientAsync(); var runbook = await CreateRunbookAsync(client, "Procedure die verandert"); var validateResponse = await SetupHelper.PostWithCsrfAndIfMatchAsync( client, $"/api/v1/runbooks/{runbook.Id}/validate", new { }, runbook.ETag); var validated = await validateResponse.Content.ReadFromJsonAsync(); Assert.False(validated!.IsStale); var pageResponse = await client.GetFromJsonAsync($"/api/v1/pages/{runbook.PageId}"); await SetupHelper.PutWithCsrfAsync( client, $"/api/v1/pages/{runbook.PageId}", new ItWorx.KnowledgeHub.Contracts.Knowledge.UpdatePageRequest( pageResponse!.Title, "## Doel\n\nBijgewerkte procedure.", "Runbook", "Active", "Procedure bijgewerkt."), pageResponse.ETag); var afterEdit = await client.GetFromJsonAsync($"/api/v1/runbooks/{runbook.Id}"); Assert.True(afterEdit!.IsStale); } [Fact] public async Task Updating_the_runbook_profile_changes_risk_level_and_status() { using var client = await AuthenticatedClientAsync(); var runbook = await CreateRunbookAsync(client, "Aan te passen profiel"); var response = await SetupHelper.PutWithCsrfAsync( client, $"/api/v1/runbooks/{runbook.Id}/profile", new UpdateRunbookProfileRequest(runbook.Owner, "Critical", "Active", true, 90, null), runbook.ETag); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var updated = await response.Content.ReadFromJsonAsync(); Assert.Equal("Critical", updated!.RiskLevel); Assert.Equal("Active", updated.Status); } [Fact] public async Task Runbooks_appear_in_the_workspace_runbook_list() { using var client = await AuthenticatedClientAsync(); var runbook = await CreateRunbookAsync(client, "Vindbare procedure"); var list = await client.GetFromJsonAsync("/api/v1/runbooks"); Assert.NotNull(list); Assert.Contains(list.Items, item => item.Id == runbook.Id && item.Title == "Vindbare procedure"); } // Reuses the exact reader-boundary pattern already established in // ProjectHubTests.A_reader_cannot_create_or_mutate_projects_but_can_read_them — // see DecisionTests' identical addition for the same V2-M5 rationale. [Fact] public async Task A_reader_can_read_runbooks_but_cannot_create_or_validate_them() { using var adminClient = await AuthenticatedClientAsync(); var runbook = await CreateRunbookAsync(adminClient, "Alleen-lezenprocedure"); const string readerEmail = "runbooklezer@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/runbooks/{runbook.Id}"); Assert.Equal(HttpStatusCode.OK, readResponse.StatusCode); var createResponse = await SetupHelper.PostWithCsrfAsync( readerClient, "/api/v1/runbooks", new CreateRunbookRequest(null, "Ongeautoriseerde procedure", "## Doel\n\nX.", null, "Low", false, null)); Assert.Equal(HttpStatusCode.Forbidden, createResponse.StatusCode); var validateResponse = await SetupHelper.PostWithCsrfAndIfMatchAsync( readerClient, $"/api/v1/runbooks/{runbook.Id}/validate", new { }, runbook.ETag); Assert.Equal(HttpStatusCode.Forbidden, validateResponse.StatusCode); } }