Files
tyx_AI_xhs/tests/api/wp5-04-asset-access.test.ts
T
suyx f1bebab611
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 59s
feat: isolate asset release access classes (TASK-WP5-04)
2026-08-03 19:02:04 +08:00

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();
});
});