Files
itworx-knowledge-hub-Public/tests/backend/ItWorx.KnowledgeHub.IntegrationTests/Migration/SourceBatchPurgeTests.cs
T

169 lines
8.7 KiB
C#

using System.Net;
using System.Net.Http.Json;
using System.Security.Cryptography;
using ItWorx.KnowledgeHub.Contracts.Attachments;
using ItWorx.KnowledgeHub.Contracts.Knowledge;
using ItWorx.KnowledgeHub.Contracts.Migration;
using ItWorx.KnowledgeHub.Infrastructure.Persistence;
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace ItWorx.KnowledgeHub.IntegrationTests.Migration;
[Collection(PostgresTestGroup.Name)]
public sealed class SourceBatchPurgeTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
{
private static readonly byte[] PngBytes = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x01, 0x02, 0x03];
private const string BatchId = "canvas-20260801T134020Z";
[Fact]
public async Task The_administrator_only_endpoint_rejects_anonymous_requests()
{
using var client = Factory.CreateClient();
var response = await client.PostAsJsonAsync(
"/api/v1/admin/source-batches/purge/dry-run",
new SourceBatchPurgeRequest(BatchId, "canvas", [new SourceBatchPurgePageEvidence(Guid.NewGuid(), new string('0', 64))]));
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task Dry_run_blocks_when_a_canvas_candidate_is_not_in_the_reviewed_batch_list()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var selected = await CreatePageAsync(client, "Geselecteerde bron", ["source:canvas", "course:test"]);
await CreatePageAsync(client, "Niet geselecteerde bron", ["source:canvas", "course:test"]);
var response = await SetupHelper.PostWithCsrfAsync(
client,
"/api/v1/admin/source-batches/purge/dry-run",
new SourceBatchPurgeRequest(BatchId, "canvas", [await EvidenceAsync(selected)]));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var dryRun = await response.Content.ReadFromJsonAsync<SourceBatchPurgeDryRunResponse>();
Assert.NotNull(dryRun);
Assert.False(dryRun.Safe);
Assert.Contains(dryRun.Blockers, blocker => blocker.Contains("buiten de gecontroleerde batchlijst", StringComparison.Ordinal));
}
[Fact]
public async Task Dry_run_blocks_when_a_selected_page_no_longer_matches_its_reviewed_content_hash()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var pageId = await CreatePageAsync(client, "Canvasbron", ["source:canvas", "course:test"]);
var response = await SetupHelper.PostWithCsrfAsync(
client,
"/api/v1/admin/source-batches/purge/dry-run",
new SourceBatchPurgeRequest(BatchId, "canvas", [new SourceBatchPurgePageEvidence(pageId, new string('0', 64))]));
var dryRun = await response.Content.ReadFromJsonAsync<SourceBatchPurgeDryRunResponse>();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(dryRun);
Assert.False(dryRun.Safe);
Assert.Contains(dryRun.Blockers, blocker => blocker.Contains("gevalideerde bronhash", StringComparison.Ordinal));
}
[Fact]
public async Task A_confirmed_safe_purge_removes_the_selected_page_references_and_verified_orphan_blob()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var pageId = await CreatePageAsync(client, "Canvasbron", ["source:canvas", "course:test"]);
var upload = await SetupHelper.PostMultipartWithCsrfAsync(client, $"/api/v1/pages/{pageId}/attachments", PngBytes, "canvas.png", "image/png");
Assert.Equal(HttpStatusCode.OK, upload.StatusCode);
var dryRun = await DryRunAsync(client, [pageId]);
Assert.True(dryRun.Safe);
Assert.Equal(1, dryRun.Pages);
Assert.Equal(1, dryRun.AttachmentReferences);
Assert.Equal(1, dryRun.OrphanedBlobs);
var execute = await SetupHelper.PostWithCsrfAsync(
client,
"/api/v1/admin/source-batches/purge/execute",
new ExecuteSourceBatchPurgeRequest(BatchId, "canvas", [await EvidenceAsync(pageId)], dryRun.ConfirmationHash));
Assert.Equal(HttpStatusCode.OK, execute.StatusCode);
var result = await execute.Content.ReadFromJsonAsync<SourceBatchPurgeResponse>();
Assert.NotNull(result);
Assert.Equal("purged", result.Status);
Assert.Equal(1, result.RemovedPages);
Assert.Equal(1, result.RemovedAttachmentReferences);
Assert.Equal(1, result.RemovedBlobs);
Assert.Equal(HttpStatusCode.NotFound, (await client.GetAsync($"/api/v1/pages/{pageId}")).StatusCode);
using var scope = Factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<KnowledgeHubDbContext>();
Assert.Equal(0, await db.AttachmentReferences.CountAsync());
Assert.Equal(0, await db.AttachmentBlobs.CountAsync());
var sha256 = Convert.ToHexString(SHA256.HashData(PngBytes)).ToLowerInvariant();
Assert.False(File.Exists(Path.Combine(Factory.AttachmentsDirectory, sha256[..2], sha256[2..4], sha256)));
}
[Fact]
public async Task A_shared_blob_is_retained_when_a_non_batch_page_still_references_it()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var canvasPageId = await CreatePageAsync(client, "Canvasbron", ["source:canvas", "course:test"]);
var retainedPageId = await CreatePageAsync(client, "Infrastructuur", ["infrastructure"]);
var canvasUpload = await SetupHelper.PostMultipartWithCsrfAsync(client, $"/api/v1/pages/{canvasPageId}/attachments", PngBytes, "canvas.png", "image/png");
var retainedUpload = await SetupHelper.PostMultipartWithCsrfAsync(client, $"/api/v1/pages/{retainedPageId}/attachments", PngBytes, "infra.png", "image/png");
Assert.Equal(HttpStatusCode.OK, canvasUpload.StatusCode);
Assert.Equal(HttpStatusCode.OK, retainedUpload.StatusCode);
var retainedAttachment = await retainedUpload.Content.ReadFromJsonAsync<AttachmentSummary>();
Assert.NotNull(retainedAttachment);
var dryRun = await DryRunAsync(client, [canvasPageId]);
Assert.True(dryRun.Safe);
Assert.Equal(0, dryRun.OrphanedBlobs);
Assert.Equal(1, dryRun.SharedBlobs);
var execute = await SetupHelper.PostWithCsrfAsync(
client,
"/api/v1/admin/source-batches/purge/execute",
new ExecuteSourceBatchPurgeRequest(BatchId, "canvas", [await EvidenceAsync(canvasPageId)], dryRun.ConfirmationHash));
var result = await execute.Content.ReadFromJsonAsync<SourceBatchPurgeResponse>();
Assert.NotNull(result);
Assert.Equal(HttpStatusCode.OK, execute.StatusCode);
Assert.Equal(0, result.RemovedBlobs);
Assert.Equal(HttpStatusCode.OK, (await client.GetAsync($"/api/v1/attachments/{retainedAttachment.Id}/content")).StatusCode);
}
private static async Task<Guid> CreatePageAsync(HttpClient client, string title, IReadOnlyList<string> tags)
{
var response = await SetupHelper.PostWithCsrfAsync(
client,
"/api/v1/pages",
new CreatePageRequest(null, title, "WikiPage", "<!-- managed-source:start -->\nBron\n<!-- managed-source:end -->", null, tags));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var page = await response.Content.ReadFromJsonAsync<PageDetail>();
Assert.NotNull(page);
return page.Id;
}
private async Task<SourceBatchPurgeDryRunResponse> DryRunAsync(HttpClient client, IReadOnlyList<Guid> pageIds)
{
var response = await SetupHelper.PostWithCsrfAsync(
client,
"/api/v1/admin/source-batches/purge/dry-run",
new SourceBatchPurgeRequest(BatchId, "canvas", await Task.WhenAll(pageIds.Select(pageId => EvidenceAsync(pageId)))));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
return await response.Content.ReadFromJsonAsync<SourceBatchPurgeDryRunResponse>()
?? throw new InvalidOperationException("Geen dry-run ontvangen.");
}
private async Task<SourceBatchPurgePageEvidence> EvidenceAsync(Guid pageId)
{
using var scope = Factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<KnowledgeHubDbContext>();
var contentHash = await db.Pages
.Where(page => page.Id == pageId)
.Select(page => page.ContentHash)
.SingleAsync();
return new SourceBatchPurgePageEvidence(pageId, contentHash);
}
}