新增 YAML 配置解析(Bun 内置 YAML)、SQLite 数据存储(bun:sqlite)、按 interval 分组并发拨测引擎、REST API(/api/summary、/api/targets、/api/targets/:id/history、/api/targets/:id/trend)、React 前端 Dashboard(统计卡片、目标表格、可展开详情面板、recharts 趋势图)。CLI 简化为仅接受配置文件路径。移除 /api/demo 路由和相关 demo 代码。保留 /health、静态资源服务和 SPA fallback。
196 lines
5.5 KiB
TypeScript
196 lines
5.5 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
import { ProbeStore } from "../../../src/server/checker/store";
|
|
import type { ResolvedTarget } from "../../../src/server/checker/types";
|
|
import { mkdir, rm } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
|
|
describe("ProbeStore", () => {
|
|
let tempDir: string;
|
|
let store: ProbeStore;
|
|
|
|
const target1: ResolvedTarget = {
|
|
name: "test-a",
|
|
url: "http://a.com",
|
|
method: "GET",
|
|
headers: {},
|
|
intervalMs: 30000,
|
|
timeoutMs: 10000,
|
|
};
|
|
|
|
const target2: ResolvedTarget = {
|
|
name: "test-b",
|
|
url: "http://b.com",
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: '{"ping": true}',
|
|
intervalMs: 60000,
|
|
timeoutMs: 5000,
|
|
expect: { status: [200], maxLatencyMs: 3000 },
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
tempDir = join(tmpdir(), `gc-store-test-${Date.now()}`);
|
|
await mkdir(tempDir, { recursive: true });
|
|
store = new ProbeStore(join(tempDir, "test.db"));
|
|
});
|
|
|
|
afterAll(async () => {
|
|
store.close();
|
|
await rm(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test("初始化后无 targets", () => {
|
|
expect(store.getTargets()).toHaveLength(0);
|
|
});
|
|
|
|
test("同步新增 targets", () => {
|
|
store.syncTargets([target1, target2]);
|
|
const targets = store.getTargets();
|
|
expect(targets).toHaveLength(2);
|
|
expect(targets[0]!.name).toBe("test-a");
|
|
expect(targets[1]!.name).toBe("test-b");
|
|
});
|
|
|
|
test("同步后 target 字段正确", () => {
|
|
const targets = store.getTargets();
|
|
const t2 = targets.find((t) => t.name === "test-b")!;
|
|
expect(t2.url).toBe("http://b.com");
|
|
expect(t2.method).toBe("POST");
|
|
expect(JSON.parse(t2.headers)).toEqual({ "Content-Type": "application/json" });
|
|
expect(t2.body).toBe('{"ping": true}');
|
|
expect(t2.interval_ms).toBe(60000);
|
|
expect(t2.expect).toBe(JSON.stringify({ status: [200], maxLatencyMs: 3000 }));
|
|
});
|
|
|
|
test("同步更新已有 target", () => {
|
|
store.syncTargets([{ ...target1, url: "http://a-v2.com" }, target2]);
|
|
const targets = store.getTargets();
|
|
const t1 = targets.find((t) => t.name === "test-a")!;
|
|
expect(t1.url).toBe("http://a-v2.com");
|
|
expect(targets).toHaveLength(2);
|
|
});
|
|
|
|
test("同步删除 target", () => {
|
|
store.syncTargets([target1]);
|
|
const targets = store.getTargets();
|
|
expect(targets).toHaveLength(1);
|
|
expect(targets[0]!.name).toBe("test-a");
|
|
});
|
|
|
|
test("重新同步回来", () => {
|
|
store.syncTargets([target1, target2]);
|
|
expect(store.getTargets()).toHaveLength(2);
|
|
});
|
|
|
|
test("getTargetById", () => {
|
|
const targets = store.getTargets();
|
|
const found = store.getTargetById(targets[0]!.id);
|
|
expect(found).toBeDefined();
|
|
expect(found!.name).toBe("test-a");
|
|
});
|
|
|
|
test("getTargetById 不存在", () => {
|
|
expect(store.getTargetById(99999)).toBeNull();
|
|
});
|
|
|
|
test("写入和查询 check result", () => {
|
|
const targets = store.getTargets();
|
|
const t1Id = targets[0]!.id;
|
|
|
|
store.insertCheckResult({
|
|
targetId: t1Id,
|
|
timestamp: "2025-01-01T00:00:00.000Z",
|
|
success: true,
|
|
statusCode: 200,
|
|
latencyMs: 150,
|
|
error: null,
|
|
matched: true,
|
|
});
|
|
|
|
store.insertCheckResult({
|
|
targetId: t1Id,
|
|
timestamp: "2025-01-01T00:00:30.000Z",
|
|
success: true,
|
|
statusCode: 200,
|
|
latencyMs: 300,
|
|
error: null,
|
|
matched: true,
|
|
});
|
|
|
|
store.insertCheckResult({
|
|
targetId: t1Id,
|
|
timestamp: "2025-01-01T00:01:00.000Z",
|
|
success: false,
|
|
statusCode: null,
|
|
latencyMs: null,
|
|
error: "timeout",
|
|
matched: false,
|
|
});
|
|
|
|
const history = store.getHistory(t1Id, 10);
|
|
expect(history).toHaveLength(3);
|
|
expect(history[0]!.timestamp).toBe("2025-01-01T00:01:00.000Z");
|
|
|
|
const latest = store.getLatestCheck(t1Id)!;
|
|
expect(latest.success).toBe(0);
|
|
expect(latest.error).toBe("timeout");
|
|
});
|
|
|
|
test("getHistory 默认 limit=20", () => {
|
|
const targets = store.getTargets();
|
|
const t1Id = targets[0]!.id;
|
|
|
|
for (let i = 0; i < 25; i++) {
|
|
store.insertCheckResult({
|
|
targetId: t1Id,
|
|
timestamp: `2025-01-01T00:${String(i).padStart(2, "0")}:00.000Z`,
|
|
success: true,
|
|
statusCode: 200,
|
|
latencyMs: 100 + i,
|
|
error: null,
|
|
matched: true,
|
|
});
|
|
}
|
|
|
|
const history = store.getHistory(t1Id);
|
|
expect(history).toHaveLength(20);
|
|
});
|
|
|
|
test("getTargetStats 计算可用率和延迟", () => {
|
|
const targets = store.getTargets();
|
|
const t1Id = targets[0]!.id;
|
|
|
|
const stats = store.getTargetStats(t1Id);
|
|
expect(stats.totalChecks).toBeGreaterThan(0);
|
|
expect(stats.availability).toBeGreaterThanOrEqual(0);
|
|
expect(stats.availability).toBeLessThanOrEqual(100);
|
|
expect(stats.avgLatencyMs).not.toBeNull();
|
|
});
|
|
|
|
test("无记录目标的 stats", () => {
|
|
const targets = store.getTargets();
|
|
const t2Id = targets.find((t) => t.name === "test-b")!.id;
|
|
|
|
const stats = store.getTargetStats(t2Id);
|
|
expect(stats.totalChecks).toBe(0);
|
|
expect(stats.availability).toBe(0);
|
|
expect(stats.avgLatencyMs).toBeNull();
|
|
});
|
|
|
|
test("getSummary 返回总览统计", () => {
|
|
const summary = store.getSummary();
|
|
expect(summary.total).toBe(2);
|
|
expect(summary.up + summary.down).toBe(2);
|
|
expect(summary.lastCheckTime).not.toBeNull();
|
|
});
|
|
|
|
test("getTrend 返回趋势数据", () => {
|
|
const targets = store.getTargets();
|
|
const t1Id = targets[0]!.id;
|
|
|
|
const trend = store.getTrend(t1Id, 24);
|
|
expect(Array.isArray(trend)).toBe(true);
|
|
});
|
|
});
|