169 lines
8.7 KiB
C#
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);
|
|
}
|
|
}
|