fix: 修正 markdown-to-jsx 导入方式 + 新增 formatDateLabel 日期工具函数
- TextPart: default import → named import - MaterialCard: 使用 formatDateLabel 显示今天/昨天/日期 - 清理旧测试文件,新增 ResourceTable 测试
This commit is contained in:
@@ -1,60 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { ResolvedConfig } from "../../src/server/config/types";
|
||||
|
||||
import { bootstrap, type BootstrapDependencies } from "../../src/server/bootstrap";
|
||||
import { createMemoryLogger } from "../../src/server/logger";
|
||||
|
||||
function makeTempConfig(overrides: Partial<ResolvedConfig> = {}): ResolvedConfig {
|
||||
const base = join(tmpdir(), `bootstrap-db-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
mkdirSync(base, { recursive: true });
|
||||
return {
|
||||
configDir: base,
|
||||
dataDir: join(base, "data"),
|
||||
host: "127.0.0.1",
|
||||
logging: {
|
||||
consoleLevel: "info",
|
||||
fileLevel: "info",
|
||||
filePath: join(base, "data", "logs", "test.log"),
|
||||
rotationFrequency: "daily",
|
||||
rotationMaxFiles: 14,
|
||||
rotationSizeBytes: 52428800,
|
||||
rotationSizeRaw: "50MB",
|
||||
},
|
||||
port: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("bootstrap 数据库集成", () => {
|
||||
test("启动时将数据库传递给 startServer", async () => {
|
||||
let started = false;
|
||||
let receivedDb: unknown = undefined;
|
||||
|
||||
const cfg = makeTempConfig();
|
||||
const mockLoadConfig = (async () => cfg) as unknown as BootstrapDependencies["loadConfig"];
|
||||
const mockOnSignal = (_signal: string, _handler: () => void) => {};
|
||||
const mockStartServer = (options: { db: unknown }) => {
|
||||
receivedDb = options.db;
|
||||
started = true;
|
||||
return { close: () => {} };
|
||||
};
|
||||
|
||||
const deps: BootstrapDependencies = {
|
||||
createLogger: async () => createMemoryLogger(),
|
||||
loadConfig: mockLoadConfig,
|
||||
onSignal: mockOnSignal,
|
||||
startServer: mockStartServer,
|
||||
};
|
||||
|
||||
await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps);
|
||||
|
||||
expect(started).toBe(true);
|
||||
expect(receivedDb).not.toBeUndefined();
|
||||
expect(typeof (receivedDb as { close?: unknown }).close).toBe("function");
|
||||
});
|
||||
});
|
||||
@@ -265,4 +265,27 @@ describe("bootstrap", () => {
|
||||
expect(flushed).toBe(true);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("启动时将数据库传递给 startServer", async () => {
|
||||
let started = false;
|
||||
let receivedDb: unknown = undefined;
|
||||
|
||||
const cfg = makeTempConfig();
|
||||
const deps: BootstrapDependencies = {
|
||||
createLogger: async () => createMemoryLogger(),
|
||||
loadConfig: async () => cfg,
|
||||
onSignal: (_signal, _handler) => {},
|
||||
startServer: (options: { db: unknown }) => {
|
||||
receivedDb = options.db;
|
||||
started = true;
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps);
|
||||
|
||||
expect(started).toBe(true);
|
||||
expect(receivedDb).not.toBeUndefined();
|
||||
expect(typeof (receivedDb as { close?: unknown }).close).toBe("function");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import type { Project, RuntimeMode } from "../../../src/shared/api";
|
||||
|
||||
import { createNoopLogger } from "../../../src/server/logger";
|
||||
import { createMigratedMemoryTestDatabase } from "../../helpers";
|
||||
|
||||
const MODE: RuntimeMode = "test";
|
||||
const LOG = createNoopLogger();
|
||||
|
||||
async function archiveProjectViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleArchiveProject: h } = await import("../../../src/server/routes/projects/archive");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
// Inline imports for actual route handler tests (each handler is in separate file)
|
||||
async function createProjectViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleCreateProject: h } = await import("../../../src/server/routes/projects/create");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
function createTestProject(db: Database, name = "测试项目"): Project {
|
||||
const result = createProject(db, { name }, LOG);
|
||||
if ("error" in result) throw new Error(result.error);
|
||||
return result.project;
|
||||
}
|
||||
|
||||
async function deleteProjectViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleDeleteProject: h } = await import("../../../src/server/routes/projects/delete");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function getProjectViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleGetProject: h } = await import("../../../src/server/routes/projects/get");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function listProjectsViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleListProjects: h } = await import("../../../src/server/routes/projects/list");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function restoreProjectViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleRestoreProject: h } = await import("../../../src/server/routes/projects/restore");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function updateProjectViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleUpdateProject: h } = await import("../../../src/server/routes/projects/update");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
// Need db/projects for setup
|
||||
import { archiveProject, createProject, getProject } from "../../../src/server/db/projects";
|
||||
|
||||
async function withRouteDb(callback: (db: Database) => Promise<void>): Promise<void> {
|
||||
const handle = createMigratedMemoryTestDatabase("route-test");
|
||||
try {
|
||||
await callback(handle.db);
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
describe("项目 API 路由", () => {
|
||||
test("POST /api/projects 创建项目", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const req = new Request("http://localhost/api/projects", {
|
||||
body: JSON.stringify({ description: "路由测试", name: "路由项目" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
const res = await createProjectViaHandler(req, db);
|
||||
expect(res.status).toBe(201);
|
||||
const body = (await res.json()) as { project: Project };
|
||||
expect(body.project.name).toBe("路由项目");
|
||||
});
|
||||
});
|
||||
|
||||
test("GET /api/projects 列表查询", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
createTestProject(db, "A项目");
|
||||
createTestProject(db, "B项目");
|
||||
|
||||
const req = new Request("http://localhost/api/projects?page=1&pageSize=20");
|
||||
const res = await listProjectsViaHandler(req, db);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { items: Project[]; total: number };
|
||||
expect(body.total).toBe(2);
|
||||
expect(body.items.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
test("GET /api/projects/:id 获取详情", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db, "详情路由");
|
||||
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}`);
|
||||
const res = await getProjectViaHandler(req, db);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { project: Project };
|
||||
expect(body.project.name).toBe("详情路由");
|
||||
});
|
||||
});
|
||||
|
||||
test("PATCH /api/projects/:id 更新项目", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db, "更新路由");
|
||||
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}`, {
|
||||
body: JSON.stringify({ name: "已更新" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
});
|
||||
const res = await updateProjectViaHandler(req, db);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { project: Project };
|
||||
expect(body.project.name).toBe("已更新");
|
||||
});
|
||||
});
|
||||
|
||||
test("POST /api/projects/:id/archive 归档项目", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db, "归档路由");
|
||||
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}/archive`, { method: "POST" });
|
||||
const res = await archiveProjectViaHandler(req, db);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { project: Project };
|
||||
expect(body.project.status).toBe("archived");
|
||||
});
|
||||
});
|
||||
|
||||
test("POST /api/projects/:id/restore 恢复项目", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db, "恢复路由");
|
||||
archiveProject(db, project.id, LOG);
|
||||
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}/restore`, { method: "POST" });
|
||||
const res = await restoreProjectViaHandler(req, db);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { project: Project };
|
||||
expect(body.project.status).toBe("active");
|
||||
});
|
||||
});
|
||||
|
||||
test("DELETE /api/projects/:id 永久删除已归档项目", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db, "删除路由");
|
||||
archiveProject(db, project.id, LOG);
|
||||
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}`, { method: "DELETE" });
|
||||
const res = await deleteProjectViaHandler(req, db);
|
||||
expect(res.status).toBe(204);
|
||||
|
||||
const after = getProject(db, project.id);
|
||||
expect("error" in after).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("创建同名项目返回 409", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const req1 = new Request("http://localhost/api/projects", {
|
||||
body: JSON.stringify({ name: "重复名" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
await createProjectViaHandler(req1, db);
|
||||
|
||||
const req2 = new Request("http://localhost/api/projects", {
|
||||
body: JSON.stringify({ name: "重复名" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
const res = await createProjectViaHandler(req2, db);
|
||||
expect(res.status).toBe(409);
|
||||
});
|
||||
});
|
||||
|
||||
test("删除 active 项目返回 409", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db, "活项目");
|
||||
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}`, { method: "DELETE" });
|
||||
const res = await deleteProjectViaHandler(req, db);
|
||||
expect(res.status).toBe(409);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user