Files
Alfred/tests/server/routes/materials.test.ts

494 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type Database from "bun:sqlite";
import { describe, expect, test } from "bun:test";
import type { Material, RuntimeMode } from "../../../src/shared/api";
import { archiveProject, createProject } from "../../../src/server/db/projects";
import { createNoopLogger } from "../../../src/server/logger";
import { createMigratedMemoryTestDatabase } from "../../helpers";
const MODE: RuntimeMode = "test";
const LOG = createNoopLogger();
async function createMaterialViaHandler(req: Request, db: Database): Promise<Response> {
const { handleCreateMaterial: h } = await import("../../../src/server/routes/materials/create");
return h(req, db, MODE, LOG);
}
function createTestProject(db: Database, name = "测试项目") {
const result = createProject(db, { name }, LOG);
if ("error" in result) throw new Error(result.error);
return result.project;
}
async function deleteMaterialViaHandler(req: Request, db: Database): Promise<Response> {
const { handleDeleteMaterial: h } = await import("../../../src/server/routes/materials/delete");
return h(req, db, MODE, LOG);
}
async function approveMaterialViaHandler(req: Request, db: Database): Promise<Response> {
const { handleApproveMaterial: h } = await import("../../../src/server/routes/materials/approve");
return h(req, db, MODE, LOG);
}
async function discardMaterialViaHandler(req: Request, db: Database): Promise<Response> {
const { handleDiscardMaterial: h } = await import("../../../src/server/routes/materials/discard");
return h(req, db, MODE, LOG);
}
async function retryMaterialViaHandler(req: Request, db: Database): Promise<Response> {
const { handleRetryMaterial: h } = await import("../../../src/server/routes/materials/retry");
return h(req, db, MODE, LOG);
}
async function getMaterialViaHandler(req: Request, db: Database): Promise<Response> {
const { handleGetMaterial: h } = await import("../../../src/server/routes/materials/get");
return h(req, db, MODE, LOG);
}
async function listMaterialsViaHandler(req: Request, db: Database): Promise<Response> {
const { handleListMaterials: h } = await import("../../../src/server/routes/materials/list");
return h(req, db, MODE, LOG);
}
async function withRouteDb(callback: (db: Database) => Promise<void>): Promise<void> {
const handle = createMigratedMemoryTestDatabase("material-route-test");
try {
await callback(handle.db);
handle.close();
} finally {
handle.cleanup();
}
}
describe("素材 API 路由", () => {
describe("POST /api/projects/:id/materials", () => {
test("正常创建素材", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const req = new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试素材" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const res = await createMaterialViaHandler(req, db);
expect(res.status).toBe(201);
const body = (await res.json()) as { material: Material };
expect(body.material.description).toBe("测试素材");
expect(body.material.associatedDate).toBe("2024-01-15");
expect(body.material.projectId).toBe(project.id);
expect(body.material.status).toBe("pending");
});
});
test("缺少 description 返回 400", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const req = new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const res = await createMaterialViaHandler(req, db);
expect(res.status).toBe(400);
});
});
test("缺少 associatedDate 返回 400", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const req = new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ description: "测试素材" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const res = await createMaterialViaHandler(req, db);
expect(res.status).toBe(400);
});
});
test("associatedDate 格式错误返回 400", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const req = new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024/01/15", description: "测试素材" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const res = await createMaterialViaHandler(req, db);
expect(res.status).toBe(400);
});
});
test("项目不存在返回 404", async () => {
await withRouteDb(async (db) => {
const req = new Request("http://localhost/api/projects/nonexistent/materials", {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试素材" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const res = await createMaterialViaHandler(req, db);
expect(res.status).toBe(404);
});
});
test("已归档项目返回 409", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
archiveProject(db, project.id, LOG);
const req = new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试素材" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const res = await createMaterialViaHandler(req, db);
expect(res.status).toBe(409);
});
});
});
describe("GET /api/projects/:id/materials", () => {
test("正常列表查询", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const req1 = new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "素材1" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
await createMaterialViaHandler(req1, db);
const req2 = new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-16", description: "素材2" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
await createMaterialViaHandler(req2, db);
const req = new Request(`http://localhost/api/projects/${project.id}/materials?page=1&pageSize=20`);
const res = await listMaterialsViaHandler(req, db);
expect(res.status).toBe(200);
const body = (await res.json()) as { items: Material[]; total: number };
expect(body.total).toBe(2);
expect(body.items.length).toBe(2);
});
});
test("空项目返回空列表", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const req = new Request(`http://localhost/api/projects/${project.id}/materials?page=1&pageSize=20`);
const res = await listMaterialsViaHandler(req, db);
expect(res.status).toBe(200);
const body = (await res.json()) as { items: Material[]; total: number };
expect(body.total).toBe(0);
expect(body.items.length).toBe(0);
});
});
});
describe("GET /api/projects/:id/materials/:mid", () => {
test("正常获取详情", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const createReq = new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "详情测试" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const createRes = await createMaterialViaHandler(createReq, db);
const createBody = (await createRes.json()) as { material: Material };
const req = new Request(`http://localhost/api/projects/${project.id}/materials/${createBody.material.id}`);
const res = await getMaterialViaHandler(req, db);
expect(res.status).toBe(200);
const body = (await res.json()) as { material: Material };
expect(body.material.description).toBe("详情测试");
});
});
test("归属错误返回 403", async () => {
await withRouteDb(async (db) => {
const projectA = createTestProject(db, "项目A");
const projectB = createProject(db, { name: "项目B" }, LOG);
if ("error" in projectB) throw new Error(projectB.error);
const createReq = new Request(`http://localhost/api/projects/${projectA.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const createRes = await createMaterialViaHandler(createReq, db);
const createBody = (await createRes.json()) as { material: Material };
const req = new Request(
`http://localhost/api/projects/${projectB.project.id}/materials/${createBody.material.id}`,
);
const res = await getMaterialViaHandler(req, db);
expect(res.status).toBe(403);
});
});
});
describe("DELETE /api/projects/:id/materials/:mid", () => {
test("正常删除", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const createReq = new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "待删除" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const createRes = await createMaterialViaHandler(createReq, db);
const createBody = (await createRes.json()) as { material: Material };
const req = new Request(`http://localhost/api/projects/${project.id}/materials/${createBody.material.id}`, {
method: "DELETE",
});
const res = await deleteMaterialViaHandler(req, db);
expect(res.status).toBe(204);
});
});
test("删除后再查返回 404", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const createReq = new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "待删除" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const createRes = await createMaterialViaHandler(createReq, db);
const createBody = (await createRes.json()) as { material: Material };
const delReq = new Request(`http://localhost/api/projects/${project.id}/materials/${createBody.material.id}`, {
method: "DELETE",
});
await deleteMaterialViaHandler(delReq, db);
const getReq = new Request(`http://localhost/api/projects/${project.id}/materials/${createBody.material.id}`);
const res = await getMaterialViaHandler(getReq, db);
expect(res.status).toBe(404);
});
});
test("归属错误返回 403", async () => {
await withRouteDb(async (db) => {
const projectA = createTestProject(db, "项目A");
const projectB = createProject(db, { name: "项目B" }, LOG);
if ("error" in projectB) throw new Error(projectB.error);
const createReq = new Request(`http://localhost/api/projects/${projectA.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试" }),
headers: { "Content-Type": "application/json" },
method: "POST",
});
const createRes = await createMaterialViaHandler(createReq, db);
const createBody = (await createRes.json()) as { material: Material };
const req = new Request(
`http://localhost/api/projects/${projectB.project.id}/materials/${createBody.material.id}`,
{
method: "DELETE",
},
);
const res = await deleteMaterialViaHandler(req, db);
expect(res.status).toBe(403);
});
});
});
describe("POST /api/projects/:id/materials/:mid/approve", () => {
test("review 状态通过返回 201", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const createRes = await createMaterialViaHandler(
new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试" }),
headers: { "Content-Type": "application/json" },
method: "POST",
}),
db,
);
const created = (await createRes.json()) as { material: Material };
setMaterialStatusRaw(db, created.material.id, "review");
const req = new Request(
`http://localhost/api/projects/${project.id}/materials/${created.material.id}/approve`,
{ method: "POST" },
);
const res = await approveMaterialViaHandler(req, db);
expect(res.status).toBe(201);
const body = (await res.json()) as { material: Material };
expect(body.material.status).toBe("approved");
});
});
test("非 review 状态返回 409", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const createRes = await createMaterialViaHandler(
new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试" }),
headers: { "Content-Type": "application/json" },
method: "POST",
}),
db,
);
const created = (await createRes.json()) as { material: Material };
const req = new Request(
`http://localhost/api/projects/${project.id}/materials/${created.material.id}/approve`,
{ method: "POST" },
);
const res = await approveMaterialViaHandler(req, db);
expect(res.status).toBe(409);
});
});
test("素材不存在返回 404", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const req = new Request(`http://localhost/api/projects/${project.id}/materials/nonexistent/approve`, {
method: "POST",
});
const res = await approveMaterialViaHandler(req, db);
expect(res.status).toBe(404);
});
});
});
describe("POST /api/projects/:id/materials/:mid/discard", () => {
test("review 状态放弃返回 201", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const createRes = await createMaterialViaHandler(
new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试" }),
headers: { "Content-Type": "application/json" },
method: "POST",
}),
db,
);
const created = (await createRes.json()) as { material: Material };
setMaterialStatusRaw(db, created.material.id, "review");
const req = new Request(
`http://localhost/api/projects/${project.id}/materials/${created.material.id}/discard`,
{ method: "POST" },
);
const res = await discardMaterialViaHandler(req, db);
expect(res.status).toBe(201);
const body = (await res.json()) as { material: Material };
expect(body.material.status).toBe("discarded");
});
});
test("非 review 状态返回 409", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const createRes = await createMaterialViaHandler(
new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试" }),
headers: { "Content-Type": "application/json" },
method: "POST",
}),
db,
);
const created = (await createRes.json()) as { material: Material };
const req = new Request(
`http://localhost/api/projects/${project.id}/materials/${created.material.id}/discard`,
{ method: "POST" },
);
const res = await discardMaterialViaHandler(req, db);
expect(res.status).toBe(409);
});
});
});
describe("POST /api/projects/:id/materials/:mid/retry", () => {
test("failed 状态重试返回 201processedContent 已清空", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const createRes = await createMaterialViaHandler(
new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试" }),
headers: { "Content-Type": "application/json" },
method: "POST",
}),
db,
);
const created = (await createRes.json()) as { material: Material };
setMaterialStatusRaw(db, created.material.id, "failed", "之前的内容");
const req = new Request(`http://localhost/api/projects/${project.id}/materials/${created.material.id}/retry`, {
method: "POST",
});
const res = await retryMaterialViaHandler(req, db);
expect(res.status).toBe(201);
const body = (await res.json()) as { material: Material };
expect(body.material.status).toBe("pending");
expect(body.material.processedContent).toBeNull();
});
});
test("非 failed 状态返回 409", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const createRes = await createMaterialViaHandler(
new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试" }),
headers: { "Content-Type": "application/json" },
method: "POST",
}),
db,
);
const created = (await createRes.json()) as { material: Material };
const req = new Request(`http://localhost/api/projects/${project.id}/materials/${created.material.id}/retry`, {
method: "POST",
});
const res = await retryMaterialViaHandler(req, db);
expect(res.status).toBe(409);
});
});
});
describe("DELETE 状态校验扩展", () => {
test("processing 状态返回 409", async () => {
await withRouteDb(async (db) => {
const project = createTestProject(db);
const createRes = await createMaterialViaHandler(
new Request(`http://localhost/api/projects/${project.id}/materials`, {
body: JSON.stringify({ associatedDate: "2024-01-15", description: "测试" }),
headers: { "Content-Type": "application/json" },
method: "POST",
}),
db,
);
const created = (await createRes.json()) as { material: Material };
setMaterialStatusRaw(db, created.material.id, "processing");
const req = new Request(`http://localhost/api/projects/${project.id}/materials/${created.material.id}`, {
method: "DELETE",
});
const res = await deleteMaterialViaHandler(req, db);
expect(res.status).toBe(409);
});
});
});
});
function setMaterialStatusRaw(
db: Database,
materialId: string,
status: "approved" | "discarded" | "failed" | "pending" | "processing" | "review",
processedContent: string | null = null,
): void {
db.prepare("UPDATE materials SET status = ?, processed_content = ?, updated_at = ? WHERE id = ?").run(
status,
processedContent,
new Date().toISOString(),
materialId,
);
}