151 lines
8.7 KiB
TypeScript
151 lines
8.7 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import { createApp } from "../../apps/api/src/app.js";
|
|
import { RegistrationService } from "../../apps/api/src/registration.js";
|
|
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
|
|
import { createAssetReleaseManifest } from "../../packages/asset-release-manifest/src/index.js";
|
|
|
|
const now = Date.parse("2026-08-03T10:00:00.000Z");
|
|
const releaseVersion = "asset-20260803.1";
|
|
const publicId = "7f0c9530-a7d9-4bf1-8c65-0e9298dd04ac";
|
|
const previewId = "ab18fd72-60e1-44e3-a9a0-3dfccb12e17c";
|
|
const ungrantedPreviewId = "d8fe890d-6df4-46a9-a578-96c1f8361ac0";
|
|
const privateId = "e3792605-5252-4d3b-a101-827408ab3515";
|
|
const headers = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
|
|
const roots: string[] = [];
|
|
const registrations: RegistrationService[] = [];
|
|
|
|
function addUser(registration: RegistrationService, role: "super_admin" | "user") {
|
|
const userId = randomUUID();
|
|
registration.database.prepare(`INSERT INTO users (
|
|
user_id, normalized_email, role, status, counts_toward_stage_limit, registration_id, created_at
|
|
) VALUES (?, ?, ?, 'active', ?, ?, ?)`).run(
|
|
userId,
|
|
`${role}-${userId}@example.invalid`,
|
|
role,
|
|
role === "user" ? 1 : 0,
|
|
randomUUID(),
|
|
now,
|
|
);
|
|
if (role === "user") {
|
|
registration.database.prepare("INSERT INTO user_profiles (user_id, creator_name, social_id) VALUES (?, 'Asset User', '@asset_user')").run(userId);
|
|
registration.database.prepare("INSERT INTO credit_accounts (user_id, available_balance, reserved_balance, updated_at) VALUES (?, 10, 0, ?)").run(userId, now);
|
|
} else {
|
|
registration.database.prepare("INSERT INTO admin_access (user_id, allowed) VALUES (?, 1)").run(userId);
|
|
}
|
|
return { session: registration.issueAuthenticatedSession(userId, role === "user" ? "user" : "admin"), userId };
|
|
}
|
|
|
|
function evidence(name: string, value: unknown) {
|
|
const directory = process.env.DADA_EVIDENCE_DIR_WP5_RES;
|
|
if (!directory) return;
|
|
mkdirSync(directory, { recursive: true });
|
|
writeFileSync(resolve(directory, name), `${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
function harness() {
|
|
const root = mkdtempSync(join(tmpdir(), "dada-wp5-04-api-"));
|
|
roots.push(root);
|
|
const registration = new RegistrationService({
|
|
challengePepper: Buffer.alloc(32, 0x31), clock: () => now,
|
|
currentPrivacyNoticeVersion: "p0a-registration-notice-v1", databasePath: join(root, "dada.sqlite3"),
|
|
invitePepper: Buffer.alloc(32, 0x32), resend: new MockResendAdapter(), sessionPepper: Buffer.alloc(32, 0x33),
|
|
});
|
|
registrations.push(registration);
|
|
const owner = addUser(registration, "user");
|
|
const intruder = addUser(registration, "user");
|
|
const admin = addUser(registration, "super_admin");
|
|
const assetReleases = createAssetReleaseManifest({
|
|
items: [
|
|
{ access_class: "public_release_asset", cache_kind: "thumbnail", content: Buffer.from("public"), mime_type: "image/png", relative_path: "public/FLOWER001.png", resource_id: publicId, root_ref: "canonical-assets" },
|
|
{ access_class: "internal_preview_asset", content: Buffer.from("preview"), mime_type: "image/webp", relative_path: "preview/FLOWER009.webp", resource_id: previewId, root_ref: "canonical-assets" },
|
|
{ access_class: "internal_preview_asset", content: Buffer.from("ungranted-preview"), mime_type: "image/webp", relative_path: "preview/FLOWER010.webp", resource_id: ungrantedPreviewId, root_ref: "canonical-assets" },
|
|
{ access_class: "private_user_asset", content: Buffer.from("private"), mime_type: "image/png", owner_id: owner.userId, relative_path: "private/generated.png", resource_id: privateId, root_ref: "managed-assets" },
|
|
],
|
|
release_version: releaseVersion,
|
|
});
|
|
let previewGranted = true;
|
|
let previewChecks = 0;
|
|
const appPromise = createApp({
|
|
assetReleases,
|
|
browserGate: false,
|
|
networkBoundary: { allowTestPort: true },
|
|
previewAssetAuthorizer: ({ resourceId, userId }) => {
|
|
previewChecks += 1;
|
|
return previewGranted && userId === owner.userId && resourceId === previewId;
|
|
},
|
|
privateAssetAdminAuthorizer: ({ adminUserId, ownerId }) => adminUserId === admin.userId && ownerId === owner.userId,
|
|
registration,
|
|
});
|
|
return { admin, appPromise, intruder, owner, previewChecks: () => previewChecks, revokePreview: () => { previewGranted = false; } };
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const registration of registrations.splice(0)) registration.close();
|
|
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
|
|
});
|
|
|
|
describe("TDD-WP5-RES-001 three access classes", () => {
|
|
it("separates route projections, per-request authorization, and cache headers", async () => {
|
|
const test = harness();
|
|
const app = await test.appPromise;
|
|
const ownerCookie = `dada_session=${test.owner.session.sessionToken}`;
|
|
const intruderCookie = `dada_session=${test.intruder.session.sessionToken}`;
|
|
const adminCookie = `dada_admin_session=${test.admin.session.sessionToken}`;
|
|
|
|
const publicManifest = await app.inject({ headers, method: "GET", url: `/api/v1/assets/public/${releaseVersion}/manifest` });
|
|
const publicAsset = await app.inject({ headers, method: "GET", url: `/api/v1/assets/public/${releaseVersion}/${publicId}` });
|
|
const previewManifest = await app.inject({ headers: { ...headers, cookie: ownerCookie }, method: "GET", url: `/api/v1/assets/preview/${releaseVersion}/manifest` });
|
|
const previewAsset = await app.inject({ headers: { ...headers, cookie: ownerCookie }, method: "GET", url: `/api/v1/assets/preview/${releaseVersion}/${previewId}` });
|
|
const privateManifest = await app.inject({ headers: { ...headers, cookie: ownerCookie }, method: "GET", url: `/api/v1/private-assets/${releaseVersion}/manifest` });
|
|
const privateAsset = await app.inject({ headers: { ...headers, cookie: ownerCookie }, method: "GET", url: `/api/v1/private-assets/${releaseVersion}/${privateId}` });
|
|
const deniedPrivate = await app.inject({ headers: { ...headers, cookie: intruderCookie }, method: "GET", url: `/api/v1/private-assets/${releaseVersion}/${privateId}` });
|
|
const controlledAdmin = await app.inject({ headers: { ...headers, cookie: adminCookie }, method: "GET", url: `/api/v1/private-assets/${releaseVersion}/${privateId}` });
|
|
|
|
expect(publicManifest.statusCode).toBe(200);
|
|
expect(publicManifest.headers["cache-control"]).toBe("public, max-age=31536000, immutable");
|
|
expect(publicManifest.json().items).toEqual([expect.objectContaining({ access_class: "public_release_asset", resource_id: publicId })]);
|
|
expect(JSON.stringify(publicManifest.json())).not.toMatch(/preview|private|relative_path|root_ref|[A-Z]:\\\\/i);
|
|
expect(publicAsset.statusCode).toBe(200);
|
|
expect(publicAsset.rawPayload).toEqual(Buffer.from("public"));
|
|
expect(publicAsset.headers["cache-control"]).toBe("public, max-age=31536000, immutable");
|
|
|
|
expect(previewManifest.statusCode).toBe(200);
|
|
expect(previewManifest.headers["cache-control"]).toBe("private, no-store");
|
|
expect(previewManifest.json().items).toEqual([expect.objectContaining({ resource_id: previewId })]);
|
|
expect(JSON.stringify(previewManifest.json())).not.toContain(ungrantedPreviewId);
|
|
expect(previewAsset.statusCode).toBe(200);
|
|
expect(previewAsset.headers["cache-control"]).toBe("private, no-store");
|
|
expect(privateManifest.json().items).toEqual([expect.objectContaining({ access_class: "private_user_asset", resource_id: privateId })]);
|
|
expect(privateAsset.rawPayload).toEqual(Buffer.from("private"));
|
|
expect(privateAsset.headers["cache-control"]).toBe("private, no-store");
|
|
expect(deniedPrivate.statusCode).toBe(404);
|
|
expect(controlledAdmin.statusCode).toBe(200);
|
|
|
|
const publicGuess = await app.inject({ headers, method: "GET", url: `/api/v1/assets/public/${releaseVersion}/${privateId}` });
|
|
expect(publicGuess.statusCode).toBe(404);
|
|
test.revokePreview();
|
|
const revoked = await app.inject({ headers: { ...headers, cookie: ownerCookie }, method: "GET", url: `/api/v1/assets/preview/${releaseVersion}/${previewId}` });
|
|
expect(revoked.statusCode).toBe(404);
|
|
expect(test.previewChecks()).toBe(4);
|
|
|
|
evidence("response.json", {
|
|
controlled_admin_status: controlledAdmin.statusCode,
|
|
private_intruder_status: deniedPrivate.statusCode,
|
|
public_guess_status: publicGuess.statusCode,
|
|
revoked_preview_status: revoked.statusCode,
|
|
});
|
|
evidence("headers.json", {
|
|
private: privateAsset.headers["cache-control"],
|
|
preview: previewAsset.headers["cache-control"],
|
|
public: publicAsset.headers["cache-control"],
|
|
});
|
|
await app.close();
|
|
});
|
|
});
|