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,184 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using ItWorx.KnowledgeHub.Contracts.Admin;
using ItWorx.KnowledgeHub.Contracts.Ingestion;
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
namespace ItWorx.KnowledgeHub.IntegrationTests.Ingestion;
[Collection(PostgresTestGroup.Name)]
public sealed class IngestionProposalPrivacyTests(PostgresContainerFixture postgres)
: OwnerSetupTestBase(postgres, new Dictionary<string, string?> { ["IngestionProposal:Enabled"] = "true" })
{
private const string EditorPassword = "strong-editor-password";
private const string ReaderPassword = "strong-reader-password";
[Fact]
public async Task Proposal_details_changes_and_source_bytes_follow_submitter_and_review_roles()
{
using var administrator = Factory.CreateClient();
await SetupHelper.LoginAsync(administrator, OwnerEmail, OwnerPassword);
using var submitter = await CreateUserAsync(administrator, "indiener@itworx.test", "Editor", EditorPassword);
using var otherEditor = await CreateUserAsync(administrator, "reviewer@itworx.test", "Editor", EditorPassword);
using var reader = await CreateUserAsync(administrator, "lezer@itworx.test", "Reader", ReaderPassword);
var created = await CreateProposalAsync(submitter);
Assert.Equal(HttpStatusCode.Created, created.StatusCode);
var proposal = await created.Content.ReadFromJsonAsync<IngestionProposalDetail>();
Assert.NotNull(proposal);
Assert.Equal(HttpStatusCode.OK, (await GetProposalAsync(submitter, proposal.Id)).StatusCode);
Assert.Equal(HttpStatusCode.OK, (await GetProposalAsync(administrator, proposal.Id)).StatusCode);
Assert.Equal(HttpStatusCode.NotFound, (await GetProposalAsync(otherEditor, proposal.Id)).StatusCode);
var submitterReceivedQueue = await submitter.GetFromJsonAsync<IngestionProposalListResponse>("/api/v1/ingestion-proposals?status=Received");
Assert.Equal(proposal.Id, Assert.Single(submitterReceivedQueue!.Items).Id);
var administratorReceivedQueue = await administrator.GetFromJsonAsync<IngestionProposalListResponse>("/api/v1/ingestion-proposals?status=Received");
Assert.Equal(proposal.Id, Assert.Single(administratorReceivedQueue!.Items).Id);
var foreignReceivedQueue = await otherEditor.GetFromJsonAsync<IngestionProposalListResponse>("/api/v1/ingestion-proposals?status=Received");
Assert.Empty(foreignReceivedQueue!.Items);
Assert.Equal(0, foreignReceivedQueue.Total);
using (var foreignPatch = await PatchProposalAsync(otherEditor, proposal.Id, created.Headers.ETag!.Tag, "not allowed"))
Assert.Equal(HttpStatusCode.NotFound, foreignPatch.StatusCode);
using var ownerPatch = await PatchProposalAsync(submitter, proposal.Id, created.Headers.ETag.Tag, "owner edit");
Assert.Equal(HttpStatusCode.OK, ownerPatch.StatusCode);
using var adminPatch = await PatchProposalAsync(administrator, proposal.Id, ownerPatch.Headers.ETag!.Tag, "admin edit");
Assert.Equal(HttpStatusCode.OK, adminPatch.StatusCode);
var staged = await UploadSourceAsync(submitter, proposal.Id, adminPatch.Headers.ETag!.Tag);
Assert.Equal(HttpStatusCode.OK, staged.StatusCode);
var stagedDetail = await staged.Content.ReadFromJsonAsync<IngestionProposalDetail>();
var source = Assert.Single(stagedDetail!.Sources);
var sourceUrl = $"/api/v1/attachments/{source.AttachmentReferenceId}/content";
// A received proposal and its original bytes remain private to the submitter
// and administrators, even from another workspace Editor.
Assert.Equal(HttpStatusCode.OK, (await submitter.GetAsync(sourceUrl)).StatusCode);
Assert.Equal(HttpStatusCode.OK, (await administrator.GetAsync(sourceUrl)).StatusCode);
Assert.Equal(HttpStatusCode.NotFound, (await otherEditor.GetAsync(sourceUrl)).StatusCode);
Assert.Equal(HttpStatusCode.NotFound, (await reader.GetAsync(sourceUrl)).StatusCode);
using var submitted = await SubmitAsync(submitter, proposal.Id, staged.Headers.ETag!.Tag);
Assert.Equal(HttpStatusCode.OK, submitted.StatusCode);
var ready = await submitted.Content.ReadFromJsonAsync<IngestionProposalDetail>();
Assert.Equal("ReadyForReview", ready?.Status);
// During review, workspace Editors retain the established reviewer access.
Assert.Equal(HttpStatusCode.OK, (await GetProposalAsync(otherEditor, proposal.Id)).StatusCode);
var reviewerQueue = await otherEditor.GetFromJsonAsync<IngestionProposalListResponse>("/api/v1/ingestion-proposals?status=ReadyForReview");
Assert.Contains(reviewerQueue!.Items, item => item.Id == proposal.Id);
Assert.Equal(HttpStatusCode.OK, (await otherEditor.GetAsync(sourceUrl)).StatusCode);
Assert.Equal(HttpStatusCode.NotFound, (await reader.GetAsync(sourceUrl)).StatusCode);
using var reviewerPatch = await PatchProposalAsync(otherEditor, proposal.Id, submitted.Headers.ETag!.Tag, "reviewer edit");
Assert.Equal(HttpStatusCode.OK, reviewerPatch.StatusCode);
using var changesRequested = await RequestChangesAsync(otherEditor, proposal.Id, reviewerPatch.Headers.ETag!.Tag);
Assert.Equal(HttpStatusCode.OK, changesRequested.StatusCode);
var changed = await changesRequested.Content.ReadFromJsonAsync<IngestionProposalDetail>();
Assert.Equal("ChangesRequested", changed?.Status);
// Once returned for changes, proposal metadata and source bytes are private
// to the submitter and administrators again.
Assert.Equal(HttpStatusCode.OK, (await GetProposalAsync(submitter, proposal.Id)).StatusCode);
Assert.Equal(HttpStatusCode.OK, (await GetProposalAsync(administrator, proposal.Id)).StatusCode);
Assert.Equal(HttpStatusCode.NotFound, (await GetProposalAsync(otherEditor, proposal.Id)).StatusCode);
var submitterChangesQueue = await submitter.GetFromJsonAsync<IngestionProposalListResponse>("/api/v1/ingestion-proposals?status=ChangesRequested");
Assert.Contains(submitterChangesQueue!.Items, item => item.Id == proposal.Id);
var administratorChangesQueue = await administrator.GetFromJsonAsync<IngestionProposalListResponse>("/api/v1/ingestion-proposals?status=ChangesRequested");
Assert.Contains(administratorChangesQueue!.Items, item => item.Id == proposal.Id);
var foreignChangesQueue = await otherEditor.GetFromJsonAsync<IngestionProposalListResponse>("/api/v1/ingestion-proposals?status=ChangesRequested");
Assert.DoesNotContain(foreignChangesQueue!.Items, item => item.Id == proposal.Id);
Assert.Equal(0, foreignChangesQueue.Total);
Assert.Equal(HttpStatusCode.NotFound, (await otherEditor.GetAsync(sourceUrl)).StatusCode);
using (var foreignChangesPatch = await PatchProposalAsync(otherEditor, proposal.Id, changesRequested.Headers.ETag!.Tag, "not allowed"))
Assert.Equal(HttpStatusCode.NotFound, foreignChangesPatch.StatusCode);
using var submitterChangesPatch = await PatchProposalAsync(submitter, proposal.Id, changesRequested.Headers.ETag.Tag, "submitter update");
Assert.Equal(HttpStatusCode.OK, submitterChangesPatch.StatusCode);
using var adminChangesPatch = await PatchProposalAsync(administrator, proposal.Id, submitterChangesPatch.Headers.ETag!.Tag, "admin update");
Assert.Equal(HttpStatusCode.OK, adminChangesPatch.StatusCode);
Assert.Equal(HttpStatusCode.OK, (await administrator.GetAsync(sourceUrl)).StatusCode);
}
private async Task<HttpClient> CreateUserAsync(HttpClient administrator, string email, string role, string password)
{
var response = await SetupHelper.PostWithCsrfAsync(administrator, "/api/v1/admin/users",
new CreateUserRequest(email, email, password, role));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, email, password);
return client;
}
private static async Task<HttpResponseMessage> CreateProposalAsync(HttpClient client)
{
var csrf = await SetupHelper.GetCsrfTokenAsync(client);
using var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/ingestion-proposals")
{
Content = JsonContent.Create(new CreateIngestionProposalRequest(
"Private source manifest", Guid.NewGuid().ToString(),
new IngestionPublicationRequest("Private source", "Source summary", "Reference", "Draft", "CreatePage")))
};
request.Headers.Add(csrf.HeaderName, csrf.Value);
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
return await client.SendAsync(request);
}
private static Task<HttpResponseMessage> GetProposalAsync(HttpClient client, Guid proposalId) =>
client.GetAsync($"/api/v1/ingestion-proposals/{proposalId}");
private static async Task<HttpResponseMessage> PatchProposalAsync(HttpClient client, Guid proposalId, string etag, string manifest)
{
var csrf = await SetupHelper.GetCsrfTokenAsync(client);
using var request = new HttpRequestMessage(HttpMethod.Patch, $"/api/v1/ingestion-proposals/{proposalId}")
{
Content = JsonContent.Create(new PatchIngestionProposalRequest(manifest, null))
};
request.Headers.Add(csrf.HeaderName, csrf.Value);
request.Headers.TryAddWithoutValidation("If-Match", etag);
return await client.SendAsync(request);
}
private static async Task<HttpResponseMessage> UploadSourceAsync(HttpClient client, Guid proposalId, string etag)
{
var csrf = await SetupHelper.GetCsrfTokenAsync(client);
using var form = new MultipartFormDataContent();
var source = new ByteArrayContent("unpublished document bytes"u8.ToArray());
source.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
form.Add(source, "file", "private-source.txt");
using var request = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/ingestion-proposals/{proposalId}/sources")
{
Content = form
};
request.Headers.Add(csrf.HeaderName, csrf.Value);
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Headers.TryAddWithoutValidation("If-Match", etag);
return await client.SendAsync(request);
}
private static async Task<HttpResponseMessage> SubmitAsync(HttpClient client, Guid proposalId, string etag)
{
var csrf = await SetupHelper.GetCsrfTokenAsync(client);
using var request = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/ingestion-proposals/{proposalId}/submit")
{
Content = JsonContent.Create(new { })
};
request.Headers.Add(csrf.HeaderName, csrf.Value);
request.Headers.TryAddWithoutValidation("If-Match", etag);
return await client.SendAsync(request);
}
private static async Task<HttpResponseMessage> RequestChangesAsync(HttpClient client, Guid proposalId, string etag)
{
var csrf = await SetupHelper.GetCsrfTokenAsync(client);
using var request = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/ingestion-proposals/{proposalId}/request-changes")
{
Content = JsonContent.Create(new IngestionProposalDecisionRequest("Please revise."))
};
request.Headers.Add(csrf.HeaderName, csrf.Value);
request.Headers.TryAddWithoutValidation("If-Match", etag);
return await client.SendAsync(request);
}
}