import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import type { ResolvedTcpTarget } from "../../../../../src/server/checker/runner/tcp/types"; import type { CheckerContext } from "../../../../../src/server/checker/runner/types"; import { TcpChecker } from "../../../../../src/server/checker/runner/tcp/execute"; const checker = new TcpChecker(); let server: Bun.TCPSocketListener; let serverPort: number; let bannerServer: Bun.TCPSocketListener; let bannerServerPort: number; let largeBannerServer: Bun.TCPSocketListener; let largeBannerServerPort: number; function makeCtx(timeoutMs = 5000): CheckerContext { const controller = new AbortController(); setTimeout(() => controller.abort(), timeoutMs); return { signal: controller.signal }; } function makeTarget(tcp: Partial, overrides?: Partial): ResolvedTcpTarget { return { description: null, group: "default", id: "test-tcp", intervalMs: 60000, name: "test-tcp", tcp: { bannerReadTimeout: 2000, host: "127.0.0.1", maxBannerBytes: 4096, port: serverPort, readBanner: false, ...tcp, }, timeoutMs: 5000, type: "tcp", ...overrides, }; } beforeAll(() => { server = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() { // Bun.listen 必填 handler,echo server 不处理数据 }, end(socket) { try { socket.close(); } catch { // best-effort 关闭 } }, error() { // Bun.listen 必填 handler,测试 server 忽略错误 }, open() { // Bun.listen 必填 handler,open 时无需操作 }, }, }); serverPort = server.port; bannerServer = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() { // Bun.listen 必填 handler }, end(socket) { try { socket.close(); } catch { // best-effort 关闭 } }, error() { // Bun.listen 必填 handler }, open(socket) { socket.write("220 smtp.example.com ESMTP\r\n"); }, }, }); bannerServerPort = bannerServer.port; largeBannerServer = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() { // Bun.listen 必填 handler }, end(socket) { try { socket.close(); } catch { // best-effort 关闭 } }, error() { // Bun.listen 必填 handler }, open(socket) { socket.write("X".repeat(8192)); }, }, }); largeBannerServerPort = largeBannerServer.port; }); afterAll(() => { server.stop(); bannerServer.stop(); largeBannerServer.stop(); }); describe("TcpChecker execute", () => { test("TCP 连接成功", async () => { const result = await checker.execute(makeTarget({}), makeCtx()); expect(result.matched).toBe(true); expect(result.failure).toBeNull(); expect(result.durationMs).toBeGreaterThanOrEqual(0); expect(result.statusDetail).toMatch(/^connected in \d+ms$/); }); test("TCP 连接失败", async () => { const result = await checker.execute(makeTarget({ host: "127.0.0.1", port: 1 }), makeCtx()); expect(result.matched).toBe(false); expect(result.failure!.kind).toBe("error"); expect(result.failure!.phase).toBe("connect"); expect(result.failure!.message).toBeTruthy(); }); test("期望端口不可达且连接失败", async () => { const result = await checker.execute( makeTarget({ host: "127.0.0.1", port: 1 }, { expect: { connected: false } }), makeCtx(), ); expect(result.matched).toBe(true); expect(result.failure).toBeNull(); expect(result.statusDetail).toBeTruthy(); }); test("期望端口不可达但连接成功", async () => { const result = await checker.execute(makeTarget({}, { expect: { connected: false } }), makeCtx()); expect(result.matched).toBe(false); expect(result.failure!.kind).toBe("mismatch"); expect(result.failure!.phase).toBe("connected"); }); test("maxDurationMs 超时返回失败", async () => { const result = await checker.execute(makeTarget({}, { expect: { maxDurationMs: -1 } }), makeCtx()); expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("duration"); }); test("读取服务端 banner 成功", async () => { const result = await checker.execute( makeTarget({ host: "127.0.0.1", port: bannerServerPort, readBanner: true, }), makeCtx(), ); expect(result.matched).toBe(true); expect(result.statusDetail).toContain("banner:"); expect(result.statusDetail).toContain("220 smtp.example.com ESMTP"); }); test("banner operator 校验通过", async () => { const result = await checker.execute( makeTarget( { host: "127.0.0.1", port: bannerServerPort, readBanner: true }, { expect: { banner: { contains: "ESMTP" } } }, ), makeCtx(), ); expect(result.matched).toBe(true); }); test("banner operator 校验失败", async () => { const result = await checker.execute( makeTarget( { host: "127.0.0.1", port: bannerServerPort, readBanner: true }, { expect: { banner: { contains: "POSTFIX" } } }, ), makeCtx(), ); expect(result.matched).toBe(false); expect(result.failure!.kind).toBe("mismatch"); expect(result.failure!.phase).toBe("banner"); expect(result.failure!.path).toBe("banner"); }); test("默认不读取 banner", async () => { const result = await checker.execute( makeTarget({ host: "127.0.0.1", port: bannerServerPort, readBanner: false }), makeCtx(), ); expect(result.matched).toBe(true); expect(result.statusDetail).not.toContain("banner:"); }); test("banner 超时空字符串继续执行", async () => { const result = await checker.execute( makeTarget({ bannerReadTimeout: 100, host: "127.0.0.1", port: serverPort, readBanner: true, }), makeCtx(), ); expect(result.matched).toBe(true); }); test("banner 读取超过最大字节数", async () => { const result = await checker.execute( makeTarget({ bannerReadTimeout: 2000, host: "127.0.0.1", maxBannerBytes: 1024, port: largeBannerServerPort, readBanner: true, }), makeCtx(), ); expect(result.matched).toBe(false); expect(result.failure!.kind).toBe("error"); expect(result.failure!.phase).toBe("banner"); expect(result.failure!.message).toContain("字节限制"); }); test("TCP 执行超时(预 abort)", async () => { const controller = new AbortController(); controller.abort(); const result = await checker.execute(makeTarget({ host: "127.0.0.1", port: serverPort }), { signal: controller.signal, }); expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("connect"); }); test("banner 读取过程中 abort", async () => { const controller = new AbortController(); setTimeout(() => controller.abort(), 10); const result = await checker.execute( makeTarget({ bannerReadTimeout: 5000, host: "127.0.0.1", port: serverPort, readBanner: true, }), { signal: controller.signal }, ); expect(result.matched).toBe(false); expect(result.failure!.kind).toBe("error"); expect(["connect", "banner"]).toContain(result.failure!.phase); }); test("serialize 返回 host:port 和 config JSON", () => { const target = makeTarget({ host: "10.0.0.1", port: 8080 }); const s = checker.serialize(target); expect(s.target).toBe("10.0.0.1:8080"); const config = JSON.parse(s.config) as Record; expect(config["host"]).toBe("10.0.0.1"); expect(config["port"]).toBe(8080); expect(config["readBanner"]).toBe(false); }); }); describe("TcpChecker resolve", () => { test("最简 tcp 配置解析默认值", () => { const target = checker.resolve( { id: "t", tcp: { host: "127.0.0.1", port: 6379 }, type: "tcp" }, { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 }, ); expect(target.tcp.host).toBe("127.0.0.1"); expect(target.tcp.port).toBe(6379); expect(target.tcp.readBanner).toBe(false); expect(target.tcp.bannerReadTimeout).toBe(2000); expect(target.tcp.maxBannerBytes).toBe(4096); expect(target.group).toBe("default"); expect(target.name).toBeNull(); expect(target.intervalMs).toBe(30000); expect(target.timeoutMs).toBe(10000); }); test("bannerReadTimeout 和 maxBannerBytes 支持 per-target 覆盖", () => { const target = checker.resolve( { id: "t", tcp: { bannerReadTimeout: 5000, host: "127.0.0.1", maxBannerBytes: "1KB", port: 80, readBanner: true }, type: "tcp", }, { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 }, ); expect(target.tcp.bannerReadTimeout).toBe(5000); expect(target.tcp.maxBannerBytes).toBe(1024); expect(target.tcp.readBanner).toBe(true); }); test("defaults.tcp 合并到 target", () => { const target = checker.resolve( { id: "t", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }, { configDir: "/tmp", defaultIntervalMs: 30000, defaults: { tcp: { bannerReadTimeout: 1000, maxBannerBytes: "8KB" } }, defaultTimeoutMs: 10000, }, ); expect(target.tcp.bannerReadTimeout).toBe(1000); expect(target.tcp.maxBannerBytes).toBe(8192); }); test("per-target 覆盖 defaults.tcp", () => { const target = checker.resolve( { id: "t", tcp: { bannerReadTimeout: 5000, host: "127.0.0.1", port: 80 }, type: "tcp" }, { configDir: "/tmp", defaultIntervalMs: 30000, defaults: { tcp: { bannerReadTimeout: 1000 } }, defaultTimeoutMs: 10000, }, ); expect(target.tcp.bannerReadTimeout).toBe(5000); }); test("maxBannerBytes 整数默认值解析", () => { const target = checker.resolve( { id: "t", tcp: { host: "127.0.0.1", maxBannerBytes: 2048, port: 80 }, type: "tcp" }, { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 }, ); expect(target.tcp.maxBannerBytes).toBe(2048); }); test("expect 配置解析", () => { const target = checker.resolve( { expect: { banner: { contains: "ESMTP" }, connected: false, maxDurationMs: 5000 }, id: "t", tcp: { host: "127.0.0.1", port: 80, readBanner: true }, type: "tcp", }, { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 }, ); expect(target.expect).toEqual({ banner: { contains: "ESMTP" }, connected: false, maxDurationMs: 5000 }); }); test("name 和 group 解析", () => { const target = checker.resolve( { group: "infra", id: "t", name: "redis", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }, { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 }, ); expect(target.name).toBe("redis"); expect(target.group).toBe("infra"); }); });