import type Database from "bun:sqlite"; import { describe, expect, test } from "bun:test"; import { mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createDatabase } from "../../../src/server/db/connection"; import { runMigrations } from "../../../src/server/db/migrate"; import { archiveProject, createProject, deleteProject, getProject, listProjects, restoreProject, updateProject, } from "../../../src/server/db/projects"; import { createMemoryLogger } from "../../../src/server/logger"; const MIGRATION_SQL = ` CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived')), archived_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS schema_migrations ( id TEXT PRIMARY KEY, checksum TEXT NOT NULL, applied_at TEXT NOT NULL ); `; function makeTempDir(): string { const dir = join(tmpdir(), `projects-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); mkdirSync(dir, { recursive: true }); return dir; } function setupDb(dir: string): Database { const logger = createMemoryLogger(); const db = createDatabase(dir, logger); runMigrations(db, [{ checksum: "init", id: "001_init", sql: MIGRATION_SQL }], dir, logger); return db; } describe("项目数据访问层", () => { test("创建项目", () => { const dir = makeTempDir(); try { const db = setupDb(dir); const result = createProject(db, { description: "测试描述", name: "测试项目" }); expect("error" in result).toBe(false); expect((result as { project: unknown }).project).toBeDefined(); const row = db.query("SELECT name, description, status FROM projects WHERE name = '测试项目'").get() as { description: string; name: string; status: string; }; expect(row.name).toBe("测试项目"); expect(row.description).toBe("测试描述"); expect(row.status).toBe("active"); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("项目名称全局唯一(含归档项目)", () => { const dir = makeTempDir(); try { const db = setupDb(dir); createProject(db, { name: "唯一名称" }); const result2 = createProject(db, { name: "唯一名称" }); expect("error" in result2).toBe(true); expect((result2 as unknown as { error: string }).error).toContain("已存在"); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("trim 后名称为空时创建失败", () => { const dir = makeTempDir(); try { const db = setupDb(dir); const result = createProject(db, { name: " " }); expect("error" in result).toBe(true); expect((result as unknown as { error: string }).error).toContain("不能为空"); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("列表查询(分页和关键字)", () => { const dir = makeTempDir(); try { const db = setupDb(dir); createProject(db, { description: "descA", name: "项目A" }); createProject(db, { description: "descB", name: "项目B" }); createProject(db, { name: "其他" }); const result1 = listProjects(db, { page: 1, pageSize: 20 }); expect(result1.total).toBe(3); expect(result1.items.length).toBe(3); const result2 = listProjects(db, { keyword: "项目", page: 1, pageSize: 20 }); expect(result2.total).toBe(2); const result3 = listProjects(db, { page: 1, pageSize: 1 }); expect(result3.total).toBe(3); expect(result3.items.length).toBe(1); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("获取项目详情", () => { const dir = makeTempDir(); try { const db = setupDb(dir); const created = createProject(db, { description: "详情", name: "详情项目" }); const id = (created as { project: { id: string } }).project.id; const result = getProject(db, id); expect("error" in result).toBe(false); const projectResult = result as { project: { description: string } }; expect(projectResult.project.description).toBe("详情"); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("获取不存在的项目返回 404 错误", () => { const dir = makeTempDir(); try { const db = setupDb(dir); const result = getProject(db, "nonexistent"); expect("error" in result).toBe(true); expect((result as unknown as { status: number }).status).toBe(404); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("更新项目名称和描述", () => { const dir = makeTempDir(); try { const db = setupDb(dir); const created = createProject(db, { name: "原名" }); const id = (created as { project: { id: string } }).project.id; const result = updateProject(db, id, { description: "新描述", name: "新名" }); expect("error" in result).toBe(false); const updated = result as { project: { description: string; name: string } }; expect(updated.project.name).toBe("新名"); expect(updated.project.description).toBe("新描述"); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("更新已归档项目失败", () => { const dir = makeTempDir(); try { const db = setupDb(dir); const created = createProject(db, { name: "待归档" }); const id = (created as { project: { id: string } }).project.id; archiveProject(db, id); const result = updateProject(db, id, { name: "新名称" }); expect("error" in result).toBe(true); expect((result as unknown as { status: number }).status).toBe(409); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("归档项目", () => { const dir = makeTempDir(); try { const db = setupDb(dir); const created = createProject(db, { name: "待归档" }); const id = (created as { project: { id: string } }).project.id; const result = archiveProject(db, id); expect("error" in result).toBe(false); const archived = (result as { project: { archivedAt: null | string; status: string } }).project; expect(archived.status).toBe("archived"); expect(archived.archivedAt).not.toBeNull(); const row = db.query("SELECT status, archived_at FROM projects WHERE id = ?").get(id) as { archived_at: null | string; status: string; }; expect(row.status).toBe("archived"); expect(row.archived_at).not.toBeNull(); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("对已归档项目重复归档失败", () => { const dir = makeTempDir(); try { const db = setupDb(dir); const created = createProject(db, { name: "测试" }); const id = (created as { project: { id: string } }).project.id; archiveProject(db, id); const result = archiveProject(db, id); expect("error" in result).toBe(true); expect((result as unknown as { status: number }).status).toBe(409); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("恢复已归档项目", () => { const dir = makeTempDir(); try { const db = setupDb(dir); const created = createProject(db, { name: "恢复测试" }); const id = (created as { project: { id: string } }).project.id; archiveProject(db, id); const result = restoreProject(db, id); expect("error" in result).toBe(false); const restored = (result as { project: { archivedAt: null | string; status: string } }).project; expect(restored.status).toBe("active"); expect(restored.archivedAt).toBeNull(); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("恢复 active 项目失败", () => { const dir = makeTempDir(); try { const db = setupDb(dir); const created = createProject(db, { name: "活跃项目" }); const id = (created as { project: { id: string } }).project.id; const result = restoreProject(db, id); expect("error" in result).toBe(true); expect((result as unknown as { status: number }).status).toBe(409); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("永久删除已归档项目", () => { const dir = makeTempDir(); try { const db = setupDb(dir); const created = createProject(db, { name: "删除测试" }); const id = (created as { project: { id: string } }).project.id; archiveProject(db, id); const result = deleteProject(db, id); expect("error" in result).toBe(false); const after = getProject(db, id); expect("error" in after).toBe(true); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("删除 active 项目失败", () => { const dir = makeTempDir(); try { const db = setupDb(dir); const created = createProject(db, { name: "活跃项目" }); const id = (created as { project: { id: string } }).project.id; const result = deleteProject(db, id); expect("error" in result).toBe(true); expect((result as unknown as { status: number }).status).toBe(409); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); });