using System.Net; using System.Net.Http.Json; using ItWorx.KnowledgeHub.Contracts.Knowledge; using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure; namespace ItWorx.KnowledgeHub.IntegrationTests.Knowledge; [Collection(PostgresTestGroup.Name)] public sealed class PageCrudTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres) { private async Task AuthenticatedClientAsync() { var client = Factory.CreateClient(); await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword); return client; } [Fact] public async Task A_root_page_can_be_created_read_and_appears_among_the_roots() { using var client = await AuthenticatedClientAsync(); var createResponse = await SetupHelper.PostWithCsrfAsync( client, "/api/v1/pages", new CreatePageRequest(null, "Kennisbank startpunt", "WikiPage", "# Welkom", null)); Assert.Equal(HttpStatusCode.OK, createResponse.StatusCode); var created = await createResponse.Content.ReadFromJsonAsync(); Assert.NotNull(created); Assert.Equal("Kennisbank startpunt", created.Title); Assert.Equal("kennisbank-startpunt", created.Slug); Assert.False(string.IsNullOrWhiteSpace(created.ETag)); var getResponse = await client.GetAsync($"/api/v1/pages/{created.Id}"); Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); var fetched = await getResponse.Content.ReadFromJsonAsync(); Assert.NotNull(fetched); // Markdig's advanced extensions add deterministic heading anchor IDs // (docs/11-markdown-and-linking.md — "Heading IDs are deterministic within a // rendered revision"). Assert.Equal("

Welkom

\n", fetched.RenderedHtml); Assert.Single(fetched.Breadcrumb); var roots = await client.GetFromJsonAsync("/api/v1/pages/roots"); Assert.NotNull(roots); Assert.Contains(roots.Items, item => item.Id == created.Id); } [Fact] public async Task A_child_page_reports_a_breadcrumb_and_makes_its_parent_report_children() { using var client = await AuthenticatedClientAsync(); var parent = await CreatePageAsync(client, null, "Ouderpagina"); var child = await CreatePageAsync(client, parent.Id, "Kindpagina"); var childDetail = await client.GetFromJsonAsync($"/api/v1/pages/{child.Id}"); Assert.NotNull(childDetail); Assert.Equal(2, childDetail.Breadcrumb.Count); Assert.Equal(parent.Id, childDetail.Breadcrumb[0].Id); Assert.Equal(child.Id, childDetail.Breadcrumb[1].Id); var children = await client.GetFromJsonAsync($"/api/v1/pages/{parent.Id}/children"); Assert.NotNull(children); Assert.Single(children.Items); Assert.Equal(child.Id, children.Items[0].Id); } [Fact] public async Task Editing_a_page_with_the_current_ETag_succeeds_and_returns_a_new_ETag() { using var client = await AuthenticatedClientAsync(); var page = await CreatePageAsync(client, null, "Bewerkbare pagina"); var updateResponse = await SetupHelper.PutWithCsrfAsync( client, $"/api/v1/pages/{page.Id}", new UpdatePageRequest("Bewerkte titel", "Nieuwe inhoud", "WikiPage", "Active", "Testbewerking"), page.ETag); Assert.Equal(HttpStatusCode.OK, updateResponse.StatusCode); var updated = await updateResponse.Content.ReadFromJsonAsync(); Assert.NotNull(updated); Assert.Equal("Bewerkte titel", updated.Title); Assert.NotEqual(page.ETag, updated.ETag); Assert.Equal(2, updated.CurrentRevisionNumber); } [Fact] public async Task Editing_a_page_without_an_if_match_header_is_rejected() { using var client = await AuthenticatedClientAsync(); var page = await CreatePageAsync(client, null, "Zonder If-Match"); var csrf = await SetupHelper.GetCsrfTokenAsync(client); using var request = new HttpRequestMessage(HttpMethod.Put, $"/api/v1/pages/{page.Id}") { Content = JsonContent.Create(new UpdatePageRequest("Titel", "Inhoud", "WikiPage", "Active", null)) }; request.Headers.Add(csrf.HeaderName, csrf.Value); using var response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.PreconditionRequired, response.StatusCode); } [Fact] public async Task Trashing_a_page_removes_it_from_the_children_listing_and_restoring_brings_it_back() { using var client = await AuthenticatedClientAsync(); var parent = await CreatePageAsync(client, null, "Prullenbaktest ouder"); var page = await CreatePageAsync(client, parent.Id, "Weg te gooien pagina"); var trashResponse = await SetupHelper.PostWithCsrfAndIfMatchAsync( client, $"/api/v1/pages/{page.Id}/trash", (object?)null, page.ETag); Assert.Equal(HttpStatusCode.OK, trashResponse.StatusCode); var childrenAfterTrash = await client.GetFromJsonAsync($"/api/v1/pages/{parent.Id}/children"); Assert.NotNull(childrenAfterTrash); Assert.Empty(childrenAfterTrash.Items); var trashedDetail = await client.GetFromJsonAsync($"/api/v1/pages/{page.Id}"); Assert.NotNull(trashedDetail); Assert.NotNull(trashedDetail.TrashedAt); var restoreResponse = await SetupHelper.PostWithCsrfAndIfMatchAsync( client, $"/api/v1/pages/{page.Id}/restore", (object?)null, trashedDetail.ETag); Assert.Equal(HttpStatusCode.OK, restoreResponse.StatusCode); var childrenAfterRestore = await client.GetFromJsonAsync($"/api/v1/pages/{parent.Id}/children"); Assert.NotNull(childrenAfterRestore); Assert.Single(childrenAfterRestore.Items); } [Fact] public async Task A_trashed_page_cannot_be_edited_until_restored() { using var client = await AuthenticatedClientAsync(); var page = await CreatePageAsync(client, null, "Te bewerken na prullenbak"); await SetupHelper.PostWithCsrfAndIfMatchAsync(client, $"/api/v1/pages/{page.Id}/trash", (object?)null, page.ETag); var trashedDetail = await client.GetFromJsonAsync($"/api/v1/pages/{page.Id}"); Assert.NotNull(trashedDetail); var editResponse = await SetupHelper.PutWithCsrfAsync( client, $"/api/v1/pages/{page.Id}", new UpdatePageRequest("Nieuwe titel", "Nieuwe inhoud", "WikiPage", "Active", null), trashedDetail.ETag); Assert.Equal(HttpStatusCode.Conflict, editResponse.StatusCode); } [Fact] public async Task Moving_a_page_under_its_own_descendant_is_rejected_as_a_cycle() { using var client = await AuthenticatedClientAsync(); var parent = await CreatePageAsync(client, null, "Cyclustest ouder"); var child = await CreatePageAsync(client, parent.Id, "Cyclustest kind"); var moveResponse = await SetupHelper.PostWithCsrfAndIfMatchAsync( client, $"/api/v1/pages/{parent.Id}/move", new MovePageRequest(child.Id, null), parent.ETag); Assert.Equal(HttpStatusCode.Conflict, moveResponse.StatusCode); } [Fact] public async Task Reordering_among_siblings_changes_their_listed_order() { using var client = await AuthenticatedClientAsync(); var parent = await CreatePageAsync(client, null, "Volgordetest ouder"); var first = await CreatePageAsync(client, parent.Id, "Eerste"); var second = await CreatePageAsync(client, parent.Id, "Tweede"); var beforeChildren = await client.GetFromJsonAsync($"/api/v1/pages/{parent.Id}/children"); Assert.NotNull(beforeChildren); Assert.Equal([first.Id, second.Id], beforeChildren.Items.Select(item => item.Id).ToArray()); // Move "first" to come after "second". var moveResponse = await SetupHelper.PostWithCsrfAndIfMatchAsync( client, $"/api/v1/pages/{first.Id}/move", new MovePageRequest(parent.Id, second.Id), first.ETag); Assert.Equal(HttpStatusCode.OK, moveResponse.StatusCode); var afterChildren = await client.GetFromJsonAsync($"/api/v1/pages/{parent.Id}/children"); Assert.NotNull(afterChildren); Assert.Equal([second.Id, first.Id], afterChildren.Items.Select(item => item.Id).ToArray()); } private static async Task CreatePageAsync(HttpClient client, Guid? parentId, string title) { var response = await SetupHelper.PostWithCsrfAsync( client, "/api/v1/pages", new CreatePageRequest(parentId, title, "WikiPage", $"# {title}", null)); response.EnsureSuccessStatusCode(); var page = await response.Content.ReadFromJsonAsync(); Assert.NotNull(page); return page; } }