import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { createFetchHandler, type StaticAssets } from "../../src/server/app"; import { ProbeStore } from "../../src/server/checker/store"; import type { SummaryResponse, TargetStatus, HealthResponse } from "../../src/shared/api"; import { mkdir, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; const staticAssets: StaticAssets = { indexHtml: new Blob(['Gateway Checker
'], { type: "text/html", }), files: { "/assets/app.js": new Blob(["console.log('app');"], { type: "text/javascript" }), }, }; describe("API 路由", () => { let tempDir: string; let store: ProbeStore; let fetchHandler: ReturnType; beforeAll(async () => { tempDir = join(tmpdir(), `gc-api-test-${Date.now()}`); await mkdir(tempDir, { recursive: true }); store = new ProbeStore(join(tempDir, "test.db")); store.syncTargets([ { name: "test-a", url: "http://a.com", method: "GET", headers: {}, intervalMs: 30000, timeoutMs: 10000, }, { name: "test-b", url: "http://b.com", method: "POST", headers: {}, intervalMs: 60000, timeoutMs: 5000, }, ]); const targets = store.getTargets(); store.insertCheckResult({ targetId: targets[0]!.id, timestamp: "2025-01-01T00:00:00.000Z", success: true, statusCode: 200, latencyMs: 150, error: null, matched: true, }); store.insertCheckResult({ targetId: targets[0]!.id, timestamp: "2025-01-01T00:00:30.000Z", success: false, statusCode: null, latencyMs: null, error: "timeout", matched: false, }); fetchHandler = createFetchHandler({ mode: "test", staticAssets, store }); }); afterAll(async () => { store.close(); await rm(tempDir, { recursive: true, force: true }); }); test("/health 返回健康检查", async () => { const response = await fetchHandler(new Request("http://localhost/health")); const body = (await response.json()) as HealthResponse; expect(response.status).toBe(200); expect(body.ok).toBe(true); expect(body.service).toBe("gateway-checker"); }); test("/api/summary 返回总览统计", async () => { const response = await fetchHandler(new Request("http://localhost/api/summary")); const body = (await response.json()) as SummaryResponse; expect(response.status).toBe(200); expect(body.total).toBe(2); expect(body.up).toBeGreaterThanOrEqual(0); expect(body.down).toBeGreaterThanOrEqual(0); expect(body.up + body.down).toBe(2); }); test("/api/targets 返回目标列表", async () => { const response = await fetchHandler(new Request("http://localhost/api/targets")); const body = (await response.json()) as TargetStatus[]; expect(response.status).toBe(200); expect(body).toHaveLength(2); expect(body[0]!.name).toBe("test-a"); expect(body[0]!.latestCheck).not.toBeNull(); expect(body[0]!.latestCheck!.success).toBe(false); expect(body[0]!.sparkline).toBeDefined(); expect(Array.isArray(body[0]!.sparkline)).toBe(true); expect(body[1]!.latestCheck).toBeNull(); }); test("/api/targets/:id/history 返回历史记录", async () => { const targets = store.getTargets(); const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/history`)); const body = await response.json(); expect(response.status).toBe(200); expect(body).toHaveLength(2); }); test("/api/targets/:id/history 支持 limit 参数", async () => { const targets = store.getTargets(); const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/history?limit=1`)); const body = await response.json(); expect(response.status).toBe(200); expect(body).toHaveLength(1); }); test("/api/targets/:id/trend 返回趋势数据", async () => { const targets = store.getTargets(); const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/trend`)); const body = await response.json(); expect(response.status).toBe(200); expect(Array.isArray(body)).toBe(true); }); test("查询不存在的目标返回 404", async () => { const response = await fetchHandler(new Request("http://localhost/api/targets/99999/history")); const body = await response.json(); expect(response.status).toBe(404); expect(body.error).toBe("Target not found"); }); test("无效 limit 参数返回 400", async () => { const targets = store.getTargets(); const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/history?limit=abc`)); const body = await response.json(); expect(response.status).toBe(400); expect(body.error).toBe("Invalid limit parameter"); }); test("无效目标 ID 返回 400", async () => { const response = await fetchHandler(new Request("http://localhost/api/targets/abc/history")); const body = await response.json(); expect(response.status).toBe(400); expect(body.error).toBe("Invalid target ID"); }); test("未知 /api/* 返回 404", async () => { const response = await fetchHandler(new Request("http://localhost/api/missing")); expect(response.status).toBe(404); }); test("HEAD 请求返回 headers 无 body", async () => { const response = await 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", async () => { const response = await fetchHandler(new Request("http://localhost/api/summary", { method: "POST" })); expect(response.status).toBe(405); expect(response.headers.get("allow")).toBe("GET, HEAD"); }); test("生产响应包含安全 headers", async () => { const prodHandler = createFetchHandler({ mode: "production", staticAssets, store }); const response = await 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 正常工作", async () => { const root = await fetchHandler(new Request("http://localhost/")); expect(root.status).toBe(200); const fallback = await fetchHandler(new Request("http://localhost/dashboard")); expect(fallback.status).toBe(200); const asset = await fetchHandler(new Request("http://localhost/assets/app.js")); expect(asset.status).toBe(200); }); });