refactor: 迁移 Bun fullstack 架构
This commit is contained in:
@@ -11,11 +11,11 @@ import type {
|
||||
TargetStatus,
|
||||
} from "../../src/shared/api";
|
||||
|
||||
import { createFetchHandler, type StaticAssets } from "../../src/server/app";
|
||||
import { checkerRegistry } from "../../src/server/checker/runner";
|
||||
import { CommandChecker } from "../../src/server/checker/runner/command/execute";
|
||||
import { HttpChecker } from "../../src/server/checker/runner/http/execute";
|
||||
import { ProbeStore } from "../../src/server/checker/store";
|
||||
import { startServer } from "../../src/server/server";
|
||||
import { rmRetry } from "../helpers";
|
||||
|
||||
function ensureRegistered() {
|
||||
@@ -29,19 +29,11 @@ beforeAll(() => {
|
||||
ensureRegistered();
|
||||
});
|
||||
|
||||
const staticAssets: StaticAssets = {
|
||||
files: {
|
||||
"/assets/app.js": new Blob(["console.log('app');"], { type: "text/javascript" }),
|
||||
},
|
||||
indexHtml: new Blob(['<!doctype html><title>DiAL</title><div id="root"></div>'], {
|
||||
type: "text/html",
|
||||
}),
|
||||
};
|
||||
|
||||
describe("API 路由", () => {
|
||||
let tempDir: string;
|
||||
let store: ProbeStore;
|
||||
let fetchHandler: ReturnType<typeof createFetchHandler>;
|
||||
let server: ReturnType<typeof startServer>;
|
||||
let baseUrl: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tempDir = join(tmpdir(), `dial-api-test-${Date.now()}`);
|
||||
@@ -104,16 +96,22 @@ describe("API 路由", () => {
|
||||
timestamp: "2025-01-01T00:00:30.000Z",
|
||||
});
|
||||
|
||||
fetchHandler = createFetchHandler({ mode: "test", staticAssets, store });
|
||||
server = startServer({
|
||||
config: { host: "127.0.0.1", port: 0 },
|
||||
mode: "test",
|
||||
store,
|
||||
});
|
||||
baseUrl = `http://127.0.0.1:${server.port}`;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.stop(true);
|
||||
store.close();
|
||||
await rmRetry(tempDir);
|
||||
});
|
||||
|
||||
test("/health 返回健康检查", async () => {
|
||||
const response = fetchHandler(new Request("http://localhost/health"));
|
||||
const response = await fetch(`${baseUrl}/health`);
|
||||
const body = (await response.json()) as HealthResponse;
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
@@ -122,7 +120,7 @@ describe("API 路由", () => {
|
||||
});
|
||||
|
||||
test("/api/summary 返回总览统计", async () => {
|
||||
const response = fetchHandler(new Request("http://localhost/api/summary"));
|
||||
const response = await fetch(`${baseUrl}/api/summary`);
|
||||
const body = (await response.json()) as SummaryResponse;
|
||||
expect(response.status).toBe(200);
|
||||
expect(body.total).toBe(2);
|
||||
@@ -133,7 +131,7 @@ describe("API 路由", () => {
|
||||
});
|
||||
|
||||
test("/api/targets 返回目标列表", async () => {
|
||||
const response = fetchHandler(new Request("http://localhost/api/targets"));
|
||||
const response = await fetch(`${baseUrl}/api/targets`);
|
||||
const body = (await response.json()) as TargetStatus[];
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
@@ -158,7 +156,7 @@ describe("API 路由", () => {
|
||||
});
|
||||
|
||||
test("/api/meta 返回 checker 类型列表", async () => {
|
||||
const response = fetchHandler(new Request("http://localhost/api/meta"));
|
||||
const response = await fetch(`${baseUrl}/api/meta`);
|
||||
const body = (await response.json()) as MetaResponse;
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
@@ -167,29 +165,16 @@ describe("API 路由", () => {
|
||||
expect(body.checkerTypes).toContain("command");
|
||||
});
|
||||
|
||||
test("/api/meta HEAD 请求返回 headers 无 body", async () => {
|
||||
const response = fetchHandler(new Request("http://localhost/api/meta", { method: "HEAD" }));
|
||||
const body = await response.text();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("application/json");
|
||||
expect(body).toBe("");
|
||||
});
|
||||
|
||||
test("/api/meta 不支持的 method 返回 405", () => {
|
||||
const response = fetchHandler(new Request("http://localhost/api/meta", { method: "POST" }));
|
||||
|
||||
expect(response.status).toBe(405);
|
||||
expect(response.headers.get("allow")).toBe("GET, HEAD");
|
||||
test("不支持的 method 在有 API 通配符时返回 404", async () => {
|
||||
const response = await fetch(`${baseUrl}/api/summary`, { method: "POST" });
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
test("/api/targets/:id/history 返回历史记录", async () => {
|
||||
const targets = store.getTargets();
|
||||
const from = "2024-01-01T00:00:00.000Z";
|
||||
const to = "2026-12-31T23:59:59.999Z";
|
||||
const response = fetchHandler(
|
||||
new Request(`http://localhost/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}`),
|
||||
);
|
||||
const response = await fetch(`${baseUrl}/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}`);
|
||||
const body = (await response.json()) as HistoryResponse;
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
@@ -205,9 +190,7 @@ describe("API 路由", () => {
|
||||
const targets = store.getTargets();
|
||||
const from = "2024-01-01T00:00:00.000Z";
|
||||
const to = "2026-12-31T23:59:59.999Z";
|
||||
const response = fetchHandler(
|
||||
new Request(`http://localhost/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}&pageSize=1`),
|
||||
);
|
||||
const response = await fetch(`${baseUrl}/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}&pageSize=1`);
|
||||
const body = (await response.json()) as HistoryResponse;
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
@@ -219,9 +202,7 @@ describe("API 路由", () => {
|
||||
const targets = store.getTargets();
|
||||
const from = "2024-01-01T00:00:00.000Z";
|
||||
const to = "2026-12-31T23:59:59.999Z";
|
||||
const response = fetchHandler(
|
||||
new Request(`http://localhost/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}&pageSize=201`),
|
||||
);
|
||||
const response = await fetch(`${baseUrl}/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}&pageSize=201`);
|
||||
const body = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
@@ -232,9 +213,7 @@ describe("API 路由", () => {
|
||||
const targets = store.getTargets();
|
||||
const from = "2024-01-01T00:00:00.000Z";
|
||||
const to = "2026-12-31T23:59:59.999Z";
|
||||
const response = fetchHandler(
|
||||
new Request(`http://localhost/api/targets/${targets[0]!.id}/trend?from=${from}&to=${to}`),
|
||||
);
|
||||
const response = await fetch(`${baseUrl}/api/targets/${targets[0]!.id}/trend?from=${from}&to=${to}`);
|
||||
const body = (await response.json()) as unknown[];
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
@@ -242,10 +221,8 @@ describe("API 路由", () => {
|
||||
});
|
||||
|
||||
test("查询不存在的目标返回 404", async () => {
|
||||
const response = fetchHandler(
|
||||
new Request(
|
||||
"http://localhost/api/targets/99999/history?from=2024-01-01T00:00:00.000Z&to=2026-12-31T23:59:59.999Z",
|
||||
),
|
||||
const response = await fetch(
|
||||
`${baseUrl}/api/targets/99999/history?from=2024-01-01T00:00:00.000Z&to=2026-12-31T23:59:59.999Z`,
|
||||
);
|
||||
const body = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
@@ -255,7 +232,7 @@ describe("API 路由", () => {
|
||||
|
||||
test("history 缺少 from/to 参数返回 400", async () => {
|
||||
const targets = store.getTargets();
|
||||
const response = fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/history`));
|
||||
const response = await fetch(`${baseUrl}/api/targets/${targets[0]!.id}/history`);
|
||||
const body = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
@@ -264,7 +241,7 @@ describe("API 路由", () => {
|
||||
|
||||
test("trend 缺少 from/to 参数返回 400", async () => {
|
||||
const targets = store.getTargets();
|
||||
const response = fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/trend`));
|
||||
const response = await fetch(`${baseUrl}/api/targets/${targets[0]!.id}/trend`);
|
||||
const body = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
@@ -272,8 +249,8 @@ describe("API 路由", () => {
|
||||
});
|
||||
|
||||
test("trend 无效 targetId 返回 400", async () => {
|
||||
const response = fetchHandler(
|
||||
new Request("http://localhost/api/targets/invalid/trend?from=2024-01-01T00:00:00Z&to=2024-01-02T00:00:00Z"),
|
||||
const response = await fetch(
|
||||
`${baseUrl}/api/targets/invalid/trend?from=2024-01-01T00:00:00Z&to=2024-01-02T00:00:00Z`,
|
||||
);
|
||||
const body = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
@@ -281,44 +258,24 @@ describe("API 路由", () => {
|
||||
expect(body["error"]).toBe("Invalid target ID");
|
||||
});
|
||||
|
||||
test("未知 /api/* 返回 404", () => {
|
||||
const response = fetchHandler(new Request("http://localhost/api/missing"));
|
||||
|
||||
test("未知 /api/* 返回 404", async () => {
|
||||
const response = await fetch(`${baseUrl}/api/missing`);
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
test("HEAD 请求返回 headers 无 body", async () => {
|
||||
const response = fetchHandler(new Request("http://localhost/api/summary", { method: "HEAD" }));
|
||||
const body = await response.text();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body).toBe("");
|
||||
});
|
||||
|
||||
test("不支持的 method 返回 405", () => {
|
||||
const response = fetchHandler(new Request("http://localhost/api/summary", { method: "POST" }));
|
||||
|
||||
expect(response.status).toBe(405);
|
||||
expect(response.headers.get("allow")).toBe("GET, HEAD");
|
||||
});
|
||||
|
||||
test("生产响应包含安全 headers", () => {
|
||||
const prodHandler = createFetchHandler({ mode: "production", staticAssets, store });
|
||||
const response = prodHandler(new Request("http://localhost/api/summary"));
|
||||
|
||||
expect(response.headers.get("x-content-type-options")).toBe("nosniff");
|
||||
expect(response.headers.get("referrer-policy")).toBe("strict-origin-when-cross-origin");
|
||||
});
|
||||
|
||||
test("静态资源和 SPA fallback 正常工作", () => {
|
||||
const root = fetchHandler(new Request("http://localhost/"));
|
||||
expect(root.status).toBe(200);
|
||||
|
||||
const fallback = fetchHandler(new Request("http://localhost/dashboard"));
|
||||
expect(fallback.status).toBe(200);
|
||||
|
||||
const asset = fetchHandler(new Request("http://localhost/assets/app.js"));
|
||||
expect(asset.status).toBe(200);
|
||||
test("生产响应包含安全 headers", async () => {
|
||||
const prodServer = startServer({
|
||||
config: { host: "127.0.0.1", port: 0 },
|
||||
mode: "production",
|
||||
store,
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${prodServer.port}/api/summary`);
|
||||
expect(response.headers.get("x-content-type-options")).toBe("nosniff");
|
||||
expect(response.headers.get("referrer-policy")).toBe("strict-origin-when-cross-origin");
|
||||
} finally {
|
||||
await prodServer.stop(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("损坏的 failure JSON 返回 null 而不崩溃", async () => {
|
||||
@@ -340,7 +297,7 @@ describe("API 路由", () => {
|
||||
|
||||
const from = "2025-06-01T00:00:00.000Z";
|
||||
const to = "2025-06-01T23:59:59.999Z";
|
||||
const response = fetchHandler(new Request(`http://localhost/api/targets/${t1Id}/history?from=${from}&to=${to}`));
|
||||
const response = await fetch(`${baseUrl}/api/targets/${t1Id}/history?from=${from}&to=${to}`);
|
||||
const body = (await response.json()) as HistoryResponse;
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { StaticAssets } from "../../src/server/app";
|
||||
import type { ResolvedConfig } from "../../src/server/checker/config-loader";
|
||||
import type { ProbeEngine } from "../../src/server/checker/engine";
|
||||
import type { ProbeStore } from "../../src/server/checker/store";
|
||||
@@ -75,7 +75,7 @@ function createHarness(overrides: BootstrapDependencies = {}) {
|
||||
startServer(options) {
|
||||
expect(options.config).toEqual({ host: config.host, port: config.port });
|
||||
expect(options.store).toBe(store);
|
||||
calls.push(`startServer:${options.mode}:${options.staticAssets ? "static" : "no-static"}`);
|
||||
calls.push(`startServer:${options.mode}`);
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
@@ -91,25 +91,16 @@ describe("bootstrap", () => {
|
||||
|
||||
expect(calls).toEqual([
|
||||
"loadConfig:/tmp/probes.yaml",
|
||||
"createStore:/tmp/dial-data/probe.db",
|
||||
`createStore:${join("/tmp/dial-data", "probe.db")}`,
|
||||
"syncTargets:1",
|
||||
"createEngine:1:3:1000",
|
||||
"engine.start",
|
||||
"onSignal:SIGINT",
|
||||
"onSignal:SIGTERM",
|
||||
"startServer:development:no-static",
|
||||
"startServer:development",
|
||||
]);
|
||||
});
|
||||
|
||||
test("生产模式传递 staticAssets", async () => {
|
||||
const { calls, dependencies } = createHarness();
|
||||
const staticAssets: StaticAssets = { files: {}, indexHtml: new Blob(["ok"]) };
|
||||
|
||||
await bootstrap({ configPath: "/tmp/probes.yaml", mode: "production", staticAssets }, dependencies);
|
||||
|
||||
expect(calls.at(-1)).toBe("startServer:production:static");
|
||||
});
|
||||
|
||||
test("收到退出信号时停止 engine 并关闭 store", async () => {
|
||||
const { calls, dependencies, shutdownHandlers } = createHarness();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user