1
0
Files
DiAL/tests/server/checker/runner/tcp/execute.test.ts
lanyuanxiaoyao 0a9a9016be feat: 新增 TCP checker,支持端口可达性探测与 banner 读取
- 新增 src/server/checker/runner/tcp/ 自包含目录(types/schema/validate/execute/expect)
- 注册 TcpChecker 到 checkerRegistry,schema/engine/store/config-loader 自动委托
- 支持 expect.connected 正反向语义(默认期待可达,可配置期待不可达)
- 支持 readBanner opt-in banner 读取,受 bannerReadTimeout + maxBannerBytes 双重限制
- 复用电有 expect/operator/duration/failure 基础设施
- 新增 3 个测试文件 51 条用例(execute/validate/expect),全量 634 测试通过
- 更新 README/DEVELOPMENT/probes.example.yaml,新增 tcp-checker capability spec
2026-05-17 23:53:37 +08:00

368 lines
11 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 { 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<ResolvedTcpTarget["tcp"]>, overrides?: Partial<ResolvedTcpTarget>): 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 必填 handlerecho server 不处理数据
},
end(socket) {
try {
socket.close();
} catch {
// best-effort 关闭
}
},
error() {
// Bun.listen 必填 handler测试 server 忽略错误
},
open() {
// Bun.listen 必填 handleropen 时无需操作
},
},
});
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<string, unknown>;
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");
});
});