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,317 @@
using System.Net;
using System.Net.Http.Json;
using System.Text;
using ItWorx.KnowledgeHub.Contracts.Attachments;
using ItWorx.KnowledgeHub.Contracts.Knowledge;
using ItWorx.KnowledgeHub.Contracts.Migration;
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
namespace ItWorx.KnowledgeHub.IntegrationTests.Migration;
/// <summary>
/// The version-validated LeafWiki v5 filesystem adapter end to end
/// (docs/17-leafwiki-migration.md; docs/adr/ADR-032-leafwiki-v5-import-fidelity.md).
/// Every fixture is synthetic and written fresh per test — it reproduces the *shape* of
/// a real LeafWiki v5 export (schema.json, leafwiki_* frontmatter including
/// YAML-escaped titles, .order.json sibling ordering, source-root-absolute
/// /assets/&lt;page-id&gt;/ image links) without containing any real user content.
/// </summary>
[Collection(PostgresTestGroup.Name)]
public sealed class LeafWikiV5ImportTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
{
private static readonly byte[] PngBytes = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x01, 0x02, 0x03];
private string CreateSourceDirectory()
{
var path = Path.Combine(Factory.ImportsDirectory, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(path);
return path;
}
private string RelativeToImportsRoot(string absolutePath) =>
Path.GetRelativePath(Factory.ImportsDirectory, absolutePath).Replace('\\', '/');
private static void WriteFile(string root, string relativePath, string content)
{
var full = Path.Combine(root, relativePath.Replace('/', Path.DirectorySeparatorChar));
Directory.CreateDirectory(Path.GetDirectoryName(full)!);
File.WriteAllText(full, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
}
private static void WriteBinary(string root, string relativePath, byte[] content)
{
var full = Path.Combine(root, relativePath.Replace('/', Path.DirectorySeparatorChar));
Directory.CreateDirectory(Path.GetDirectoryName(full)!);
File.WriteAllBytes(full, content);
}
/// <summary>
/// Builds a source shaped exactly like the real export: a schema.json version
/// marker, index.md-per-directory, leafwiki_id/leafwiki_title frontmatter (one of
/// them a YAML double-quoted scalar with a \U escape, one of them title-only with
/// no body at all), .order.json files whose order deliberately contradicts
/// alphabetical order, and a source-root-absolute asset reference.
/// </summary>
private string CreateV5Source()
{
var source = CreateSourceDirectory();
WriteFile(source, "schema.json", "{\n \"version\": 5\n}");
WriteFile(source, ".order.json", "{\"ordered_ids\": [\"netw-01\", \"alg-01\"]}");
// "Netwerken" sorts after "Algemeen" alphabetically but is listed first.
WriteFile(source, "netwerken/index.md",
"---\nleafwiki_id: netw-01\nleafwiki_title: \"\\U0001F539 Netwerken\"\n---\n");
WriteFile(source, "netwerken/.order.json", "{\"ordered_ids\": [\"netw-vlan\", \"netw-stp\"]}");
WriteFile(source, "netwerken/stp.md",
"---\nleafwiki_id: netw-stp\nleafwiki_title: Spanning Tree\n---\n# STP – ANDERE KOP\nBlokkeert lussen.");
WriteFile(source, "netwerken/vlan.md",
"---\nleafwiki_id: netw-vlan\n---\n# VLAN's\n![diagram](/assets/netw-vlan/diagram.png)\n");
WriteFile(source, "algemeen/index.md",
"---\nleafwiki_id: alg-01\nleafwiki_title: Algemeen\n---\nInleiding.");
WriteBinary(source, "assets/netw-vlan/diagram.png", PngBytes);
return source;
}
private async Task<ImportPreflightResponse> PreflightAsync(HttpClient client, string source)
{
var response = await SetupHelper.PostWithCsrfAsync(
client,
"/api/v1/admin/imports/preflight",
new PreflightImportRequest(RelativeToImportsRoot(source), $"v5-{Guid.NewGuid():N}"));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var plan = await response.Content.ReadFromJsonAsync<ImportPreflightResponse>();
Assert.NotNull(plan);
return plan;
}
private async Task<ExecuteImportResponse> ExecuteAsync(HttpClient client, string source, Guid? jobId)
{
Assert.NotNull(jobId);
var response = await SetupHelper.PostWithCsrfAsync(
client,
$"/api/v1/admin/imports/{jobId}/execute?sourceRelativePath={Uri.EscapeDataString(RelativeToImportsRoot(source))}",
(object?)null);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var executed = await response.Content.ReadFromJsonAsync<ExecuteImportResponse>();
Assert.NotNull(executed);
return executed;
}
[Fact]
public async Task A_leafwiki_v5_export_is_recognized_and_titled_from_its_own_frontmatter()
{
var source = CreateV5Source();
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var plan = await PreflightAsync(client, source);
Assert.Equal("Success", plan.Status);
Assert.Equal("leafwiki-filesystem-v5", plan.AdapterName);
// The escaped YAML scalar becomes a real title, not the literal "\U0001F539".
Assert.Contains(plan.Items, item => item.ProposedTitle == "\U0001F539 Netwerken");
Assert.DoesNotContain(plan.Items, item => item.ProposedTitle.Contains("\\U0001", StringComparison.Ordinal));
// LeafWiki's stored title wins over a body heading that says something else,
// and a body with no heading at all is still correctly titled (the generic
// fallback adapter would have called this one "Index").
Assert.Contains(plan.Items, item => item.ProposedTitle == "Spanning Tree");
Assert.DoesNotContain(plan.Items, item => item.ProposedTitle == "Index");
// No frontmatter title at all: the body heading is still used.
Assert.Contains(plan.Items, item => item.ProposedTitle == "VLAN's");
}
[Fact]
public async Task Imported_siblings_follow_the_order_files_not_alphabetical_order()
{
var source = CreateV5Source();
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var plan = await PreflightAsync(client, source);
var executed = await ExecuteAsync(client, source, plan.JobId);
Assert.Equal("Success", executed.Status);
Assert.Equal(0, executed.Failed);
var roots = await client.GetFromJsonAsync<PageChildrenResponse>("/api/v1/pages/roots");
Assert.NotNull(roots);
var importedRoots = roots.Items
.Where(item => item.Title is "\U0001F539 Netwerken" or "Algemeen")
.Select(item => item.Title)
.ToList();
Assert.Equal(["\U0001F539 Netwerken", "Algemeen"], importedRoots);
var networks = Assert.Single(roots.Items, item => item.Title == "\U0001F539 Netwerken");
var children = await client.GetFromJsonAsync<PageChildrenResponse>($"/api/v1/pages/{networks.Id}/children");
Assert.NotNull(children);
Assert.Equal(["VLAN's", "Spanning Tree"], children.Items.Select(item => item.Title).ToList());
}
[Fact]
public async Task An_imported_image_link_points_at_the_attachment_the_import_created()
{
var source = CreateV5Source();
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var plan = await PreflightAsync(client, source);
var executed = await ExecuteAsync(client, source, plan.JobId);
Assert.Empty(executed.Diagnostics);
var roots = await client.GetFromJsonAsync<PageChildrenResponse>("/api/v1/pages/roots");
var networks = Assert.Single(roots!.Items, item => item.Title == "\U0001F539 Netwerken");
var children = await client.GetFromJsonAsync<PageChildrenResponse>($"/api/v1/pages/{networks.Id}/children");
var vlan = Assert.Single(children!.Items, item => item.Title == "VLAN's");
var attachments = await client.GetFromJsonAsync<AttachmentListResponse>($"/api/v1/pages/{vlan.Id}/attachments");
var attachment = Assert.Single(attachments!.Items);
var detail = await client.GetFromJsonAsync<PageDetail>($"/api/v1/pages/{vlan.Id}");
Assert.NotNull(detail);
// The stored Markdown must reference the attachment this import created —
// the source path "/assets/netw-vlan/diagram.png" resolves to nothing here.
Assert.Contains($"/api/v1/attachments/{attachment.Id}/content", detail.Markdown, StringComparison.Ordinal);
Assert.DoesNotContain("/assets/netw-vlan/diagram.png", detail.Markdown, StringComparison.Ordinal);
// And the rendered HTML really serves those bytes back.
Assert.Contains($"/api/v1/attachments/{attachment.Id}/content", detail.RenderedHtml, StringComparison.Ordinal);
var content = await client.GetAsync(new Uri($"/api/v1/attachments/{attachment.Id}/content", UriKind.Relative));
Assert.Equal(HttpStatusCode.OK, content.StatusCode);
Assert.Equal(PngBytes, await content.Content.ReadAsByteArrayAsync());
}
[Fact]
public async Task A_page_whose_body_is_only_frontmatter_is_imported_as_an_empty_page_not_skipped()
{
var source = CreateV5Source();
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var plan = await PreflightAsync(client, source);
var executed = await ExecuteAsync(client, source, plan.JobId);
Assert.Equal(4, plan.PlannedCount);
Assert.Equal(4, executed.Imported);
var roots = await client.GetFromJsonAsync<PageChildrenResponse>("/api/v1/pages/roots");
var networks = Assert.Single(roots!.Items, item => item.Title == "\U0001F539 Netwerken");
var detail = await client.GetFromJsonAsync<PageDetail>($"/api/v1/pages/{networks.Id}");
Assert.Equal(string.Empty, detail!.Markdown);
}
[Fact]
public async Task A_body_heading_that_only_repeats_the_title_is_not_imported_twice()
{
var source = CreateV5Source();
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var plan = await PreflightAsync(client, source);
await ExecuteAsync(client, source, plan.JobId);
var roots = await client.GetFromJsonAsync<PageChildrenResponse>("/api/v1/pages/roots");
var networks = Assert.Single(roots!.Items, item => item.Title == "🔹 Netwerken");
var children = await client.GetFromJsonAsync<PageChildrenResponse>($"/api/v1/pages/{networks.Id}/children");
// "# VLAN's" repeats the page title exactly: the reader would otherwise see the
// same title twice, and the document would have two h1 elements.
var vlan = Assert.Single(children!.Items, item => item.Title == "VLAN's");
var vlanDetail = await client.GetFromJsonAsync<PageDetail>($"/api/v1/pages/{vlan.Id}");
Assert.DoesNotContain("# VLAN's", vlanDetail!.Markdown, StringComparison.Ordinal);
// The rendered body no longer carries an h1 of its own; the application renders
// the page title as the document heading above it.
Assert.Empty(System.Text.RegularExpressions.Regex.Matches(vlanDetail.RenderedHtml, "<h1"));
// "# STP – ANDERE KOP" says something different from the title "Spanning Tree":
// that is content, and it is kept.
var stp = Assert.Single(children.Items, item => item.Title == "Spanning Tree");
var stpDetail = await client.GetFromJsonAsync<PageDetail>($"/api/v1/pages/{stp.Id}");
Assert.Contains("# STP – ANDERE KOP", stpDetail!.Markdown, StringComparison.Ordinal);
Assert.Single(System.Text.RegularExpressions.Regex.Matches(stpDetail.RenderedHtml, "<h1"));
}
[Fact]
public async Task An_unsupported_declared_schema_version_falls_back_instead_of_guessing()
{
var source = CreateSourceDirectory();
WriteFile(source, "schema.json", "{\"version\": 99}");
WriteFile(source, "pagina.md", "---\nleafwiki_id: x1\nleafwiki_title: Toekomstige versie\n---\n# Kop\nTekst.");
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var plan = await PreflightAsync(client, source);
// The v5 adapter refuses an unknown declared version; the generic Markdown
// fallback still imports what it safely can, and says so.
Assert.Equal("filesystem-markdown", plan.AdapterName);
Assert.Contains(plan.Items, item => item.ProposedTitle == "Kop");
}
[Fact]
public async Task A_plain_markdown_source_without_leafwiki_frontmatter_still_uses_the_generic_adapter()
{
var source = CreateSourceDirectory();
WriteFile(source, "index.md", "---\ntitle: Startpagina\n---\nWelkom.");
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var plan = await PreflightAsync(client, source);
Assert.Equal("filesystem-markdown", plan.AdapterName);
}
[Fact]
public async Task Rerunning_a_v5_import_is_idempotent()
{
var source = CreateV5Source();
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var firstPlan = await PreflightAsync(client, source);
var first = await ExecuteAsync(client, source, firstPlan.JobId);
Assert.Equal(4, first.Imported);
var secondPlan = await PreflightAsync(client, source);
var second = await ExecuteAsync(client, source, secondPlan.JobId);
Assert.Equal(0, second.Imported);
Assert.Equal(4, second.Skipped);
var roots = await client.GetFromJsonAsync<PageChildrenResponse>("/api/v1/pages/roots");
Assert.Single(roots!.Items, item => item.Title == "\U0001F539 Netwerken");
}
[Fact]
public async Task The_source_is_never_modified_by_a_v5_import()
{
var source = CreateV5Source();
var before = FingerprintDirectory(source);
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var plan = await PreflightAsync(client, source);
var executed = await ExecuteAsync(client, source, plan.JobId);
Assert.True(executed.SourceUnchangedAfterExecution);
Assert.Equal(before, FingerprintDirectory(source));
}
private static string FingerprintDirectory(string root)
{
var builder = new StringBuilder();
foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories).OrderBy(path => path, StringComparer.Ordinal))
{
builder.Append(Path.GetRelativePath(root, file).Replace('\\', '/'))
.Append('=')
.Append(Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(File.ReadAllBytes(file))))
.Append('\n');
}
return builder.ToString();
}
}