#!/usr/bin/env node
/**
* Test 09: Secure Document CRUD
*
* Tests the secure-documents plugin REST API:
* - Upload document (multipart)
* - List documents (ABAC-filtered)
* - Get document metadata
* - Update document metadata/policy
* - Get indexing status
* - Download (presigned URL)
* - Delete document
* - Auth enforcement (401 without token)
*/
import {
requireStrapi,
section,
pass,
fail,
skip,
assert,
assertEqual,
assertStatus,
summary,
getSuperAdminToken,
adminLogin,
STRAPI_URL,
fetchJSON,
} from './helpers.js';
const DOCS_URL = `${STRAPI_URL}/api/secure-documents/documents`;
async function main() {
console.log('\x1b[1m=== Test 09: Secure Document CRUD ===\x1b[0m');
await requireStrapi();
// ── Setup ──────────────────────────────────────────────────────────
section('Setup');
const adminToken = await getSuperAdminToken();
if (!adminToken) {
fail('Super admin login', 'Cannot login');
summary();
process.exit(1);
}
pass('Super admin login');
const authHeaders = {
Authorization: `Bearer ${adminToken}`,
};
// ── Auth enforcement ───────────────────────────────────────────────
section('Auth Enforcement');
{
const { status } = await fetchJSON(DOCS_URL);
assertStatus(status, 401, 'List documents without auth returns 401');
}
{
const { status } = await fetchJSON(`${DOCS_URL}/nonexistent`);
assertStatus(status, 401, 'Get document without auth returns 401');
}
// ── Upload document ────────────────────────────────────────────────
section('Upload Document');
let uploadedDocId = null;
{
// Build a minimal text file for upload
const boundary = '----TestBoundary' + Date.now();
const fileContent = 'This is a test document for integration testing.\nIt has multiple lines.\nThird line here.';
const policy = JSON.stringify({
classification: 'internal',
min_clearance: 0,
allowed_departments: [],
});
const metadata = JSON.stringify({
title: 'Test Document 09',
description: 'Integration test document',
});
const body = [
`--${boundary}`,
'Content-Disposition: form-data; name="file"; filename="test-doc-09.txt"',
'Content-Type: text/plain',
'',
fileContent,
`--${boundary}`,
'Content-Disposition: form-data; name="policy"',
'',
policy,
`--${boundary}`,
'Content-Disposition: form-data; name="metadata"',
'',
metadata,
`--${boundary}--`,
].join('\r\n');
const resp = await fetch(DOCS_URL, {
method: 'POST',
headers: {
...authHeaders,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
},
body,
});
const data = await resp.json().catch(() => null);
if (resp.status === 201 && data?.data?.documentId) {
uploadedDocId = data.data.documentId;
pass('Upload text file');
assertEqual(data.data.title, 'Test Document 09', 'Upload returns correct title');
assertEqual(data.data.mimeType, 'text/plain', 'Upload returns correct mime type');
assert(data.data.fileSize > 0, 'Upload returns file size', `Got fileSize: ${data.data.fileSize}`);
assert(data.data.checksum, 'Upload returns checksum', 'No checksum');
assertEqual(data.data.status, 'uploaded', 'Upload status is "uploaded"');
assertEqual(data.data.classification, 'internal', 'Upload classification is "internal"');
} else {
fail('Upload text file', `HTTP ${resp.status}: ${JSON.stringify(data)}`);
}
}
if (!uploadedDocId) {
fail('Upload prerequisite', 'No document to test with');
summary();
process.exit(1);
}
// ── Invalid upload (no file) ───────────────────────────────────────
{
const { status, data } = await fetchJSON(DOCS_URL, {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({}),
});
assert(status === 400, 'Upload without file returns 400', `Got HTTP ${status}`);
}
// ── List documents ─────────────────────────────────────────────────
section('List Documents');
{
const { status, data } = await fetchJSON(DOCS_URL, { headers: authHeaders });
if (assertStatus(status, 200, 'List documents returns 200')) {
assert(Array.isArray(data?.data), 'List returns data array', `Got: ${typeof data?.data}`);
const found = data.data.find((d) => d.documentId === uploadedDocId);
assert(!!found, 'Uploaded document appears in list', `documentId ${uploadedDocId} not found in list`);
if (found) {
assertEqual(found.title, 'Test Document 09', 'Listed document has correct title');
assert(!found.storageKey, 'storageKey is not exposed in list', 'storageKey leaked!');
}
}
}
// ── Get single document ────────────────────────────────────────────
section('Get Document');
{
const { status, data } = await fetchJSON(`${DOCS_URL}/${uploadedDocId}`, { headers: authHeaders });
if (assertStatus(status, 200, 'Get document returns 200')) {
assertEqual(data.data.documentId, uploadedDocId, 'Get returns correct documentId');
assertEqual(data.data.title, 'Test Document 09', 'Get returns correct title');
assertEqual(data.data.classification, 'internal', 'Get returns classification');
assert(!data.data.storageKey, 'storageKey is not exposed', 'storageKey leaked!');
}
}
// ── Get non-existent document ──────────────────────────────────────
{
const { status } = await fetchJSON(`${DOCS_URL}/aaaa-bbbb-cccc-not-real`, { headers: authHeaders });
assertStatus(status, 404, 'Get non-existent document returns 404');
}
// ── Update document ────────────────────────────────────────────────
section('Update Document');
{
const { status, data } = await fetchJSON(`${DOCS_URL}/${uploadedDocId}`, {
method: 'PUT',
headers: authHeaders,
body: JSON.stringify({
title: 'Updated Test Document 09',
description: 'Updated description',
classification: 'confidential',
}),
});
if (assertStatus(status, 200, 'Update document returns 200')) {
assertEqual(data.data.title, 'Updated Test Document 09', 'Update changes title');
assertEqual(data.data.classification, 'confidential', 'Update changes classification');
}
}
// Verify update persisted
{
const { data } = await fetchJSON(`${DOCS_URL}/${uploadedDocId}`, { headers: authHeaders });
assertEqual(data?.data?.title, 'Updated Test Document 09', 'Title update persisted');
assertEqual(data?.data?.description, 'Updated description', 'Description update persisted');
}
// ── Get indexing status ────────────────────────────────────────────
section('Indexing Status');
{
const { status, data } = await fetchJSON(`${DOCS_URL}/${uploadedDocId}/status`, { headers: authHeaders });
if (assertStatus(status, 200, 'Get status returns 200')) {
assertEqual(data.data.documentId, uploadedDocId, 'Status returns correct documentId');
assert(typeof data.data.status === 'string', 'Status field is a string', `Got: ${typeof data.data.status}`);
}
}
// ── Download (signed URL) ──────────────────────────────────────────
section('Download');
{
const { status, data } = await fetchJSON(`${DOCS_URL}/${uploadedDocId}/download`, { headers: authHeaders });
if (status === 200 && data?.data?.url) {
pass('Download returns signed URL');
assert(data.data.url.includes('X-Amz') || data.data.url.startsWith('http'), 'URL looks like a signed URL', `Got: ${data.data.url.substring(0, 80)}`);
assert(data.data.expiresIn > 0, 'Download includes expiresIn', `Got: ${data.data.expiresIn}`);
} else if (status === 500) {
// S3/MinIO might not be available in all test environments
skip('Download returns signed URL', 'S3/MinIO not available');
} else {
fail('Download returns signed URL', `HTTP ${status}: ${JSON.stringify(data)}`);
}
}
// ── Delete document ────────────────────────────────────────────────
section('Delete Document');
{
const resp = await fetch(`${DOCS_URL}/${uploadedDocId}`, {
method: 'DELETE',
headers: authHeaders,
});
assertStatus(resp.status, 204, 'Delete document returns 204');
}
// Verify deletion
{
const { status } = await fetchJSON(`${DOCS_URL}/${uploadedDocId}`, { headers: authHeaders });
assertStatus(status, 404, 'Deleted document returns 404');
}
// ── Summary ────────────────────────────────────────────────────────
section('Summary');
const results = summary();
process.exit(results.failed > 0 ? 1 : 0);
}
main().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});