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,147 @@
using System.Net;
using System.Net.Http.Json;
using ItWorx.KnowledgeHub.Contracts.Knowledge;
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
namespace ItWorx.KnowledgeHub.IntegrationTests.Knowledge;
/// <summary>PostgreSQL full-text + trigram search: title/body matching, tag/type
/// filters, typo tolerance, trash exclusion and authentication
/// (docs/27-acceptance-matrix.md, M3-A04..A06).</summary>
[Collection(PostgresTestGroup.Name)]
public sealed class SearchTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
{
[Fact]
public async Task Search_finds_a_page_by_title_and_by_body_text()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
await SetupHelper.PostWithCsrfAsync(
client, "/api/v1/pages", new CreatePageRequest(null, "PostgreSQL back-uprunbook", "Runbook", "Stappen om een back-up te herstellen.", null));
await SetupHelper.PostWithCsrfAsync(
client, "/api/v1/pages", new CreatePageRequest(null, "Onderwerp zonder verband", "WikiPage", "Volledig andere inhoud.", null));
var byTitle = await client.GetFromJsonAsync<SearchResponse>("/api/v1/search?q=back-uprunbook");
Assert.NotNull(byTitle);
Assert.Contains(byTitle.Items, item => item.Title == "PostgreSQL back-uprunbook");
var byBody = await client.GetFromJsonAsync<SearchResponse>("/api/v1/search?q=herstellen");
Assert.NotNull(byBody);
Assert.Contains(byBody.Items, item => item.Title == "PostgreSQL back-uprunbook");
Assert.DoesNotContain(byBody.Items, item => item.Title == "Onderwerp zonder verband");
}
[Fact]
public async Task Search_is_typo_tolerant_for_titles_via_trigram_similarity()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
await SetupHelper.PostWithCsrfAsync(
client, "/api/v1/pages", new CreatePageRequest(null, "Troubleshooting netwerkverbinding", "Troubleshooting", "Inhoud.", null));
// Deliberate typo: "Trubleshootng" instead of "Troubleshooting".
var response = await client.GetFromJsonAsync<SearchResponse>("/api/v1/search?q=Trubleshootng");
Assert.NotNull(response);
Assert.Contains(response.Items, item => item.Title == "Troubleshooting netwerkverbinding");
}
[Fact]
public async Task Search_can_be_filtered_by_tag_and_by_document_type()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
await SetupHelper.PostWithCsrfAsync(
client,
"/api/v1/pages",
new CreatePageRequest(null, "Gearchiveerd project", "ProjectDocument", "Inhoud.", null, ["archief", "netwerk"]));
await SetupHelper.PostWithCsrfAsync(
client,
"/api/v1/pages",
new CreatePageRequest(null, "Actief project", "ProjectDocument", "Inhoud.", null, ["actief"]));
var byTag = await client.GetFromJsonAsync<SearchResponse>("/api/v1/search?q=project&tag=archief");
Assert.NotNull(byTag);
Assert.Single(byTag.Items);
Assert.Equal("Gearchiveerd project", byTag.Items[0].Title);
var byType = await client.GetFromJsonAsync<SearchResponse>("/api/v1/search?q=project&type=ProjectDocument");
Assert.NotNull(byType);
Assert.Equal(2, byType.Items.Count);
}
[Fact]
public async Task Search_never_returns_a_trashed_page()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var createResponse = await SetupHelper.PostWithCsrfAsync(
client, "/api/v1/pages", new CreatePageRequest(null, "Binnenkort verwijderde pagina", "WikiPage", "Inhoud.", null));
var page = await createResponse.Content.ReadFromJsonAsync<PageDetail>();
Assert.NotNull(page);
var beforeTrash = await client.GetFromJsonAsync<SearchResponse>("/api/v1/search?q=verwijderde");
Assert.NotNull(beforeTrash);
Assert.Contains(beforeTrash.Items, item => item.Id == page.Id);
await SetupHelper.PostWithCsrfAndIfMatchAsync(client, $"/api/v1/pages/{page.Id}/trash", (object?)null, page.ETag);
var afterTrash = await client.GetFromJsonAsync<SearchResponse>("/api/v1/search?q=verwijderde");
Assert.NotNull(afterTrash);
Assert.DoesNotContain(afterTrash.Items, item => item.Id == page.Id);
}
[Fact]
public async Task Search_requires_authentication()
{
using var client = Factory.CreateClient();
var response = await client.GetAsync("/api/v1/search?q=iets");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task Search_cursor_pagination_is_deterministic_and_covers_every_result_without_duplicates()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
for (var i = 0; i < 12; i++)
{
await SetupHelper.PostWithCsrfAsync(
client,
"/api/v1/pages",
new CreatePageRequest(null, $"Cursortest onderwerp {i:D2}", "WikiPage", "Gedeelde zoekterm cursorproef.", null));
}
var seen = new HashSet<Guid>();
string? cursor = null;
var pages = 0;
do
{
var url = cursor is null
? "/api/v1/search?q=cursorproef&limit=5"
: $"/api/v1/search?q=cursorproef&limit=5&cursor={Uri.EscapeDataString(cursor)}";
var response = await client.GetFromJsonAsync<SearchResponse>(url);
Assert.NotNull(response);
foreach (var item in response.Items)
{
Assert.True(seen.Add(item.Id), $"Item {item.Id} was returned on more than one page.");
}
cursor = response.NextCursor;
pages++;
Assert.True(pages < 10, "Pagination did not terminate within a reasonable number of pages.");
} while (cursor is not null);
Assert.Equal(12, seen.Count);
}
}