1
0
Files
DiAL/tests/server/checker/runner/udp/execute.test.ts
lanyuanxiaoyao 52262a31f6 feat: 新增 UDP checker,支持自定义 payload 请求-响应探测与断言
基于 Bun connected UDP socket 实现通用 UDP 拨测能力:
- 支持 text/hex/base64 payload 编码与独立 responseEncoding 响应视图
- 支持 responded、response、responseSize、sourceHost、sourcePort、maxDurationMs 专属 expect
- 单 datagram 发送,仅断言首个 UDP 响应 datagram
- 通过 maxResponseBytes 和 flags.truncated 进行响应大小限制与截断保护
- payload 可选,省略时发送空 datagram
- 自包含模块结构(types/schema/validate/expect/encoding/execute)
- 新增 741 tests(含 unit、execute 集成、expect 和编码 roundtrip),全部通过
2026-05-18 17:23:17 +08:00

365 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ResolvedUdpTarget["udp"]> = {}, 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.statusDetail).toContain("responded");
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).not.toBeNull();
} 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.statusDetail).toContain("no response");
} 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.statusDetail).toContain("5 bytes");
} 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 maxDurationMs", 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 }, { maxDurationMs: 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<string, unknown>;
expect(parsed["host"]).toBe("10.0.0.1");
expect(parsed["port"]).toBe(9000);
});
});