import { describe, expect, it } from "bun:test"; import type { ResolvedUdpTarget, UdpExpectConfig } from "../../../../../src/server/checker/runner/udp/types"; import { UdpChecker } from "../../../../../src/server/checker/runner/udp/execute"; async function createEchoServer(): Promise<{ close: () => void; port: number }> { const socket = await Bun.udpSocket({ socket: { data(sock, buf, port, addr) { sock.send(buf, port, addr); }, drain() { // Bun UDP socket handler 必填项 }, error() { // Bun UDP socket handler 必填项 }, }, }); return { close: () => socket.close(), port: socket.port }; } function makeSignal(timeoutMs: number): { cleanup: () => void; signal: AbortSignal } { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); return { cleanup: () => clearTimeout(timer), signal: controller.signal }; } function makeTarget(overrides: Partial = {}, expect?: UdpExpectConfig): ResolvedUdpTarget { return { description: null, expect, group: "default", id: "test-udp", intervalMs: 30000, name: null, timeoutMs: 10000, type: "udp", udp: { encoding: "text", host: "127.0.0.1", maxResponseBytes: 4096, payload: "PING", port: 0, responseEncoding: "text", ...overrides, }, }; } describe("UdpChecker execute", () => { it("should resolve and respond successfully", async () => { const server = await createEchoServer(); try { const checker = new UdpChecker(); const target = makeTarget({ port: server.port }); const { cleanup, signal } = makeSignal(5000); const result = await checker.execute(target, { signal }); cleanup(); expect(result.matched).toBe(true); expect(result.failure).toBeNull(); expect(result.observation).toMatchObject({ responded: true }); expect(result.durationMs).toBeGreaterThanOrEqual(0); } finally { server.close(); } }); it("should work with localhost hostname", async () => { const server = await createEchoServer(); try { const checker = new UdpChecker(); const target = makeTarget({ host: "localhost", port: server.port }); const { cleanup, signal } = makeSignal(5000); const result = await checker.execute(target, { signal }); cleanup(); expect(result.matched).toBe(true); } finally { server.close(); } }); it("should fail when no response and default expect.responded=true", async () => { const server = await Bun.udpSocket({ socket: { data() { // sink - 不回包以模拟无响应 }, drain() { // Bun UDP socket handler 必填项 }, error() { // Bun UDP socket handler 必填项 }, }, }); try { const checker = new UdpChecker(); const target = makeTarget({ port: server.port }); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 200); const result = await checker.execute(target, { signal: controller.signal }); clearTimeout(timer); expect(result.matched).toBe(false); expect(result.failure).toMatchObject({ kind: "error", phase: "response" }); expect(result.observation?.["durationMs"]).toBeGreaterThanOrEqual(0); expect(result.observation?.["error"]).toBeTruthy(); expect(result.observation).toMatchObject({ responded: false }); } finally { server.close(); } }); it("should match when expect.responded=false and no response", async () => { const server = await Bun.udpSocket({ socket: { data() { // sink - 不回包以模拟无响应 }, drain() { // Bun UDP socket handler 必填项 }, error() { // Bun UDP socket handler 必填项 }, }, }); try { const checker = new UdpChecker(); const target = makeTarget({ port: server.port }, { responded: false }); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 200); const result = await checker.execute(target, { signal: controller.signal }); clearTimeout(timer); expect(result.matched).toBe(true); expect(result.observation?.["durationMs"]).toBeGreaterThanOrEqual(0); expect(result.observation).toMatchObject({ error: null, responded: false }); } finally { server.close(); } }); it("should fail when expect.responded=false but response received", async () => { const server = await createEchoServer(); try { const checker = new UdpChecker(); const target = makeTarget({ port: server.port }, { responded: false }); const { cleanup, signal } = makeSignal(5000); const result = await checker.execute(target, { signal }); cleanup(); expect(result.matched).toBe(false); expect(result.failure?.phase).toBe("responded"); } finally { server.close(); } }); it("should validate response content with expect.response", async () => { const server = await createEchoServer(); try { const checker = new UdpChecker(); const target = makeTarget({ port: server.port }, { response: [{ contains: "PING" }] }); const { cleanup, signal } = makeSignal(5000); const result = await checker.execute(target, { signal }); cleanup(); expect(result.matched).toBe(true); } finally { server.close(); } }); it("should fail response content mismatch", async () => { const server = await createEchoServer(); try { const checker = new UdpChecker(); const target = makeTarget({ port: server.port }, { response: [{ contains: "PONG" }] }); const { cleanup, signal } = makeSignal(5000); const result = await checker.execute(target, { signal }); cleanup(); expect(result.matched).toBe(false); expect(result.failure?.phase).toBe("response"); } finally { server.close(); } }); it("should validate responseSize", async () => { const server = await createEchoServer(); try { const checker = new UdpChecker(); const target = makeTarget({ port: server.port }, { responseSize: { gte: 1 } }); const { cleanup, signal } = makeSignal(5000); const result = await checker.execute(target, { signal }); cleanup(); expect(result.matched).toBe(true); } finally { server.close(); } }); it("should only use the first datagram when server sends multiple", async () => { const server = await Bun.udpSocket({ socket: { data(sock, buf, port, addr) { sock.send(Buffer.from("FIRST"), port, addr); setTimeout(() => { try { sock.send(Buffer.from("SECOND"), port, addr); } catch { // checker 已关闭 connected socket,第二次发送可能失败 } }, 30); }, drain() { // Bun UDP socket handler 必填项 }, error() { // Bun UDP socket handler 必填项 }, }, }); try { const checker = new UdpChecker(); const target = makeTarget({ payload: "PING", port: server.port }, { response: [{ contains: "FIRST" }] }); const { cleanup, signal } = makeSignal(5000); const result = await checker.execute(target, { signal }); cleanup(); expect(result.matched).toBe(true); expect(result.observation).toMatchObject({ responded: true, responseSize: 5 }); } finally { server.close(); } }); it("should fail when response exceeds maxResponseBytes", async () => { const server = await createEchoServer(); try { const checker = new UdpChecker(); const longPayload = "X".repeat(100); const target = makeTarget({ maxResponseBytes: 10, payload: longPayload, port: server.port, }); const { cleanup, signal } = makeSignal(5000); const result = await checker.execute(target, { signal }); cleanup(); expect(result.matched).toBe(false); expect(result.failure?.phase).toBe("response"); expect(result.failure?.kind).toBe("error"); } finally { server.close(); } }); it("should fail when duration exceeds durationMs", async () => { const server = await Bun.udpSocket({ socket: { data() { // sink - 不回包,通过 abort 触发 no-response 路径 }, drain() { // Bun UDP socket handler 必填项 }, error() { // Bun UDP socket handler 必填项 }, }, }); try { const checker = new UdpChecker(); const target = makeTarget({ port: server.port }, { durationMs: { lte: 1 }, responded: false }); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 200); const result = await checker.execute(target, { signal: controller.signal }); clearTimeout(timer); expect(result.matched).toBe(false); expect(result.failure?.phase).toBe("duration"); } finally { server.close(); } }); it("should match sourceHost assertion", async () => { const server = await createEchoServer(); try { const checker = new UdpChecker(); const target = makeTarget({ port: server.port }, { sourceHost: { equals: "127.0.0.1" } }); const { cleanup, signal } = makeSignal(5000); const result = await checker.execute(target, { signal }); cleanup(); expect(result.matched).toBe(true); } finally { server.close(); } }); it("should match sourcePort assertion", async () => { const server = await createEchoServer(); try { const checker = new UdpChecker(); const target = makeTarget({ port: server.port }, { sourcePort: { equals: server.port } }); const { cleanup, signal } = makeSignal(5000); const result = await checker.execute(target, { signal }); cleanup(); expect(result.matched).toBe(true); } finally { server.close(); } }); }); describe("UdpChecker resolve", () => { it("should fill defaults for minimal config", () => { const checker = new UdpChecker(); const target = checker.resolve( { id: "test", type: "udp", udp: { host: "127.0.0.1", port: 9000 } }, { configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 }, ); expect(target.udp.payload).toBe(""); expect(target.udp.encoding).toBe("text"); expect(target.udp.responseEncoding).toBe("text"); expect(target.udp.maxResponseBytes).toBe(4096); }); it("should use defaults.udp for missing fields", () => { const checker = new UdpChecker(); const target = checker.resolve( { id: "test", type: "udp", udp: { host: "127.0.0.1", port: 9000 } }, { configDir: "/tmp", defaultIntervalMs: 30000, defaults: { udp: { encoding: "hex", maxResponseBytes: "8KB", responseEncoding: "hex" } }, defaultTimeoutMs: 10000, }, ); expect(target.udp.encoding).toBe("hex"); expect(target.udp.responseEncoding).toBe("hex"); expect(target.udp.maxResponseBytes).toBe(8192); }); it("should override defaults with target-level config", () => { const checker = new UdpChecker(); const target = checker.resolve( { id: "test", type: "udp", udp: { encoding: "base64", host: "127.0.0.1", port: 9000 } }, { configDir: "/tmp", defaultIntervalMs: 30000, defaults: { udp: { encoding: "hex" } }, defaultTimeoutMs: 10000, }, ); expect(target.udp.encoding).toBe("base64"); }); }); describe("UdpChecker serialize", () => { it("should produce udp host:port target summary", () => { const checker = new UdpChecker(); const target = makeTarget({ host: "10.0.0.1", port: 9000 }); const { config, target: display } = checker.serialize(target); expect(display).toBe("udp 10.0.0.1:9000"); const parsed = JSON.parse(config) as Record; expect(parsed["host"]).toBe("10.0.0.1"); expect(parsed["port"]).toBe(9000); }); });