120 lines
6.0 KiB
TypeScript
120 lines
6.0 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import { createApp } from "../../apps/api/src/app.js";
|
|
import { ProjectService } from "../../apps/api/src/projects.js";
|
|
import { RegistrationService } from "../../apps/api/src/registration.js";
|
|
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
|
|
|
|
const now = Date.parse("2026-07-28T08:00:00.000Z");
|
|
const headers = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
|
|
const roots: string[] = [];
|
|
const registrations: RegistrationService[] = [];
|
|
const projects: ProjectService[] = [];
|
|
|
|
function harness() {
|
|
const root = mkdtempSync(join(tmpdir(), "dada-wp2-01-api-"));
|
|
roots.push(root);
|
|
const databasePath = join(root, "dada.sqlite3");
|
|
const registration = new RegistrationService({
|
|
challengePepper: Buffer.alloc(32, 0x21), clock: () => now,
|
|
currentPrivacyNoticeVersion: "p0a-registration-notice-v1", databasePath,
|
|
invitePepper: Buffer.alloc(32, 0x22), resend: new MockResendAdapter(), sessionPepper: Buffer.alloc(32, 0x23),
|
|
});
|
|
registrations.push(registration);
|
|
const projectService = new ProjectService({ clock: () => now, databasePath });
|
|
projects.push(projectService);
|
|
const ownerId = randomUUID();
|
|
registration.database.prepare(`
|
|
INSERT INTO users (
|
|
user_id, normalized_email, role, status, counts_toward_stage_limit, registration_id, created_at
|
|
) VALUES (?, 'projects@example.invalid', 'user', 'active', 1, ?, ?)
|
|
`).run(ownerId, randomUUID(), now);
|
|
registration.database.prepare(`
|
|
INSERT INTO user_profiles (user_id, creator_name, social_id) VALUES (?, 'Project User', '@project_user')
|
|
`).run(ownerId);
|
|
registration.database.prepare(`
|
|
INSERT INTO credit_accounts (user_id, available_balance, reserved_balance, updated_at) VALUES (?, 10, 0, ?)
|
|
`).run(ownerId, now);
|
|
const session = registration.issueAuthenticatedSession(ownerId, "user");
|
|
return { ownerId, projectService, registration, session };
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const project of projects.splice(0)) project.close();
|
|
for (const registration of registrations.splice(0)) registration.close();
|
|
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
|
|
});
|
|
|
|
describe("TASK-WP2-01 project API", () => {
|
|
it("returns owner-scoped summaries/detail and applies rename plus failed-empty batch trash", async () => {
|
|
const fixture = harness();
|
|
const failed = fixture.projectService.createProjectForGeneration({
|
|
ownerId: fixture.ownerId, prompt: "API 失败草稿", ratio: "3:4", status: "failed",
|
|
});
|
|
const succeeded = fixture.projectService.createProjectForGeneration({
|
|
ownerId: fixture.ownerId, prompt: "API 成功项目", ratio: "1:1", status: "running",
|
|
});
|
|
fixture.projectService.recordSuccessfulImage({ generationId: succeeded.generation.generationId, imageId: randomUUID() });
|
|
const app = await createApp({
|
|
browserGate: false, networkBoundary: { allowTestPort: true }, projects: fixture.projectService, registration: fixture.registration,
|
|
});
|
|
const cookie = `dada_session=${fixture.session.sessionToken}`;
|
|
const session = await app.inject({ headers: { ...headers, cookie }, method: "GET", url: "/api/v1/auth/session" });
|
|
const csrf = session.json().csrf_token;
|
|
|
|
const list = await app.inject({ headers: { ...headers, cookie }, method: "GET", url: "/api/v1/projects?status=active" });
|
|
expect(list.statusCode).toBe(200);
|
|
expect(list.json()).toMatchObject({ active_count: 2, active_limit: 20 });
|
|
expect(list.json().projects).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ project_id: failed.project.projectId, status: "failed_empty", successful_image_count: 0 }),
|
|
expect.objectContaining({ project_id: succeeded.project.projectId, status: "active", successful_image_count: 1 }),
|
|
]));
|
|
expect(list.json().projects[0]).not.toHaveProperty("draft_prompt");
|
|
expect(list.json().projects[0]).not.toHaveProperty("generations");
|
|
|
|
const detail = await app.inject({
|
|
headers: { ...headers, cookie }, method: "GET", url: `/api/v1/projects/${succeeded.project.projectId}`,
|
|
});
|
|
expect(detail.statusCode).toBe(200);
|
|
expect(detail.json()).toMatchObject({ project_id: succeeded.project.projectId, ratio: "1:1", successful_image_count: 1 });
|
|
|
|
const rename = await app.inject({
|
|
headers: { ...headers, cookie, "x-csrf-token": csrf }, method: "PATCH", payload: { name: "API 新名称" },
|
|
url: `/api/v1/projects/${succeeded.project.projectId}`,
|
|
});
|
|
expect(rename.statusCode).toBe(200);
|
|
expect(rename.json()).toMatchObject({ name: "API 新名称", status: "renamed" });
|
|
|
|
const batch = await app.inject({
|
|
headers: { ...headers, cookie, "x-csrf-token": csrf }, method: "POST",
|
|
payload: { project_ids: [failed.project.projectId, succeeded.project.projectId] },
|
|
url: "/api/v1/projects/failed-empty/trash",
|
|
});
|
|
expect(batch.statusCode).toBe(200);
|
|
expect(batch.json()).toEqual({ ignored_project_ids: [succeeded.project.projectId], trashed_project_ids: [failed.project.projectId] });
|
|
expect(fixture.projectService.getProject(fixture.ownerId, succeeded.project.projectId).status).toBe("active");
|
|
await app.close();
|
|
});
|
|
|
|
it("does not reveal another owner's project", async () => {
|
|
const fixture = harness();
|
|
const hidden = fixture.projectService.createProjectForGeneration({
|
|
ownerId: randomUUID(), prompt: "另一个用户", ratio: "4:3", status: "failed",
|
|
});
|
|
const app = await createApp({
|
|
browserGate: false, networkBoundary: { allowTestPort: true }, projects: fixture.projectService, registration: fixture.registration,
|
|
});
|
|
const response = await app.inject({
|
|
headers: { ...headers, cookie: `dada_session=${fixture.session.sessionToken}` }, method: "GET",
|
|
url: `/api/v1/projects/${hidden.project.projectId}`,
|
|
});
|
|
expect(response.statusCode).toBe(404);
|
|
await app.close();
|
|
});
|
|
});
|