110 lines
6.1 KiB
TypeScript
110 lines
6.1 KiB
TypeScript
import { createHash, 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 { ManagedStorage } from "../../apps/api/src/managed-storage.js";
|
|
import { RegistrationService } from "../../apps/api/src/registration.js";
|
|
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
|
|
import { StickerReleaseService } from "../../apps/api/src/sticker-releases.js";
|
|
|
|
const now = Date.parse("2026-08-03T12:00:00.000Z");
|
|
const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAEklEQVQImWO4E6XxHxkzEBQAANIxHF3ECQOzAAAAAElFTkSuQmCC", "base64");
|
|
const roots: string[] = [];
|
|
const closeables: Array<{ close(): void }> = [];
|
|
const baseHeaders = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
|
|
|
|
async function multipart() {
|
|
const form = new FormData();
|
|
form.append("stable_id", "STK1408");
|
|
form.append("part", "25");
|
|
form.append("order", "184");
|
|
form.append("enabled", "true");
|
|
form.append("original_byte_size", String(png.byteLength));
|
|
form.append("original_sha256", createHash("sha256").update(png).digest("hex"));
|
|
form.append("sticker_file", new Blob([png], { type: "image/png" }), "STK1408.png");
|
|
const serialized = new Response(form);
|
|
return { contentType: serialized.headers.get("content-type")!, payload: Buffer.from(await serialized.arrayBuffer()) };
|
|
}
|
|
|
|
function evidence(value: unknown) {
|
|
const directory = process.env.DADA_EVIDENCE_DIR_WP5_UPL;
|
|
if (!directory) return;
|
|
mkdirSync(directory, { recursive: true });
|
|
writeFileSync(resolve(directory, "response.json"), `${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const value of closeables.splice(0).reverse()) value.close();
|
|
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
|
|
});
|
|
|
|
describe("TASK-WP5-05 admin sticker API", () => {
|
|
it("requires admin mutation controls, publishes upload, and exposes current and versioned public resources", async () => {
|
|
const dataRoot = mkdtempSync(join(tmpdir(), "dada-wp5-05-api-"));
|
|
roots.push(dataRoot);
|
|
mkdirSync(join(dataRoot, "db"), { recursive: true });
|
|
const databasePath = join(dataRoot, "db", "dada.sqlite3");
|
|
const registration = new RegistrationService({
|
|
challengePepper: Buffer.alloc(32, 0x41), clock: () => now,
|
|
currentPrivacyNoticeVersion: "p0a-registration-notice-v1", databasePath,
|
|
invitePepper: Buffer.alloc(32, 0x42), resend: new MockResendAdapter(), sessionPepper: Buffer.alloc(32, 0x43),
|
|
});
|
|
const storage = new ManagedStorage({ dataRoot, databasePath });
|
|
const stickers = new StickerReleaseService({ clock: () => now, databasePath, storage });
|
|
closeables.push(stickers, storage, registration);
|
|
const adminId = randomUUID();
|
|
registration.database.prepare(`INSERT INTO users (
|
|
user_id, normalized_email, role, status, counts_toward_stage_limit, registration_id, created_at
|
|
) VALUES (?, 'sticker-admin@example.invalid', 'super_admin', 'active', 0, ?, ?)`).run(adminId, randomUUID(), now);
|
|
registration.database.prepare("INSERT INTO admin_access (user_id, allowed) VALUES (?, 1)").run(adminId);
|
|
const session = registration.issueAuthenticatedSession(adminId, "admin");
|
|
const csrf = registration.issueAdminCsrfToken(session.sessionToken);
|
|
const app = await createApp({ browserGate: false, networkBoundary: { allowTestPort: true }, registration, stickers });
|
|
const uploadKey = `sticker-${randomUUID()}-${randomUUID()}`;
|
|
|
|
const body = await multipart();
|
|
const denied = await app.inject({ headers: { ...baseHeaders, "content-type": body.contentType }, method: "POST", payload: body.payload, url: "/api/v1/admin/assets/static-stickers" });
|
|
expect(denied.statusCode).toBe(401);
|
|
|
|
const acceptedBody = await multipart();
|
|
const accepted = await app.inject({
|
|
headers: {
|
|
...baseHeaders, cookie: `dada_admin_session=${session.sessionToken}`, "content-type": acceptedBody.contentType,
|
|
"idempotency-key": uploadKey, "x-csrf-token": csrf,
|
|
},
|
|
method: "POST", payload: acceptedBody.payload, url: "/api/v1/admin/assets/static-stickers",
|
|
});
|
|
expect(accepted.statusCode).toBe(201);
|
|
expect(accepted.json()).toMatchObject({ item: { stable_id: "STK1408" }, release_version: "asset-20260803.1" });
|
|
const replayBody = await multipart();
|
|
const replay = await app.inject({
|
|
headers: {
|
|
...baseHeaders, cookie: `dada_admin_session=${session.sessionToken}`, "content-type": replayBody.contentType,
|
|
"idempotency-key": uploadKey, "x-csrf-token": csrf,
|
|
},
|
|
method: "POST", payload: replayBody.payload, url: "/api/v1/admin/assets/static-stickers",
|
|
});
|
|
expect(replay.statusCode).toBe(200);
|
|
expect(replay.json()).toMatchObject({ created: false, release_version: "asset-20260803.1" });
|
|
expect(storage.inspectCounts()).toMatchObject({ managed_files: 2 });
|
|
|
|
const adminList = await app.inject({ headers: { ...baseHeaders, cookie: `dada_admin_session=${session.sessionToken}` }, method: "GET", url: "/api/v1/admin/assets/static-stickers" });
|
|
const publicList = await app.inject({ headers: baseHeaders, method: "GET", url: "/api/v1/static-stickers/current" });
|
|
const original = await app.inject({ headers: baseHeaders, method: "GET", url: "/api/v1/assets/public/asset-20260803.1/STK1408" });
|
|
const thumbnail = await app.inject({ headers: baseHeaders, method: "GET", url: "/api/v1/assets/public/asset-20260803.1/STK1408?variant=thumbnail" });
|
|
expect(adminList.statusCode).toBe(200);
|
|
expect(publicList.json()).toMatchObject({ count: 1, items: [{ stable_id: "STK1408" }] });
|
|
expect(original.rawPayload).toEqual(png);
|
|
expect(thumbnail.statusCode).toBe(200);
|
|
expect(thumbnail.headers["content-type"]).toMatch(/^image\/png/);
|
|
expect(thumbnail.rawPayload).not.toEqual(png);
|
|
|
|
evidence({ admin_list_status: adminList.statusCode, public_count: publicList.json().count, upload_status: accepted.statusCode });
|
|
await app.close();
|
|
});
|
|
});
|