474 lines
16 KiB
TypeScript
474 lines
16 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
||
|
||
import type { CheckerValidationInput } from "../../../../../src/server/checker/runner/types";
|
||
|
||
import { validateDnsConfig } from "../../../../../src/server/checker/runner/dns/validate";
|
||
|
||
function makeInput(overrides: { targets?: Array<Record<string, unknown>> }): CheckerValidationInput {
|
||
return { targets: (overrides.targets ?? []) as CheckerValidationInput["targets"] };
|
||
}
|
||
|
||
describe("validateDnsConfig", () => {
|
||
it("接受合法的 system 目标(resolver/name/family)", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [{ dns: { family: "ipv4", name: "example.com", resolver: "system" }, id: "t1", type: "dns" }],
|
||
}),
|
||
);
|
||
expect(issues).toHaveLength(0);
|
||
});
|
||
|
||
it("接受合法的 server 目标(resolver/name/server/port/recordType)", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: {
|
||
name: "example.com",
|
||
port: 53,
|
||
protocol: "udp",
|
||
recordType: "A",
|
||
resolver: "server",
|
||
server: "8.8.8.8",
|
||
},
|
||
id: "t2",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues).toHaveLength(0);
|
||
});
|
||
|
||
it("拒绝缺少 dns 配置分组", () => {
|
||
const issues = validateDnsConfig(makeInput({ targets: [{ id: "t3", type: "dns" }] }));
|
||
expect(issues).toHaveLength(1);
|
||
expect(issues[0]!.code).toBe("required");
|
||
expect(issues[0]!.path).toContain("dns");
|
||
expect(issues[0]!.message).toContain("dns");
|
||
});
|
||
|
||
it("拒绝缺少 dns.resolver", () => {
|
||
const issues = validateDnsConfig(makeInput({ targets: [{ dns: { name: "example.com" }, id: "t4", type: "dns" }] }));
|
||
expect(issues).toHaveLength(1);
|
||
expect(issues[0]!.code).toBe("required");
|
||
expect(issues[0]!.path).toContain("resolver");
|
||
});
|
||
|
||
it("拒绝无效的 dns.resolver 值", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({ targets: [{ dns: { name: "example.com", resolver: "unknown" }, id: "t5", type: "dns" }] }),
|
||
);
|
||
expect(issues).toHaveLength(1);
|
||
expect(issues[0]!.code).toBe("invalid-value");
|
||
expect(issues[0]!.path).toContain("resolver");
|
||
expect(issues[0]!.message).toContain("system");
|
||
expect(issues[0]!.message).toContain("server");
|
||
});
|
||
|
||
it("拒绝缺少 dns.name", () => {
|
||
const issues = validateDnsConfig(makeInput({ targets: [{ dns: { resolver: "system" }, id: "t6", type: "dns" }] }));
|
||
expect(issues.length).toBeGreaterThanOrEqual(1);
|
||
expect(issues.some((i) => i.code === "required" && i.path.includes("name"))).toBe(true);
|
||
});
|
||
|
||
it("System 模式:拒绝空白 dns.name", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({ targets: [{ dns: { name: " ", resolver: "system" }, id: "t7", type: "dns" }] }),
|
||
);
|
||
expect(issues.some((i) => i.code === "required" && i.path.includes("name"))).toBe(true);
|
||
});
|
||
|
||
it("System 模式:接受 family any/ipv4/ipv6,拒绝无效 family", () => {
|
||
for (const family of ["any", "ipv4", "ipv6"]) {
|
||
const issues = validateDnsConfig(
|
||
makeInput({ targets: [{ dns: { family, name: "x.com", resolver: "system" }, id: "tf", type: "dns" }] }),
|
||
);
|
||
expect(issues).toHaveLength(0);
|
||
}
|
||
const issues = validateDnsConfig(
|
||
makeInput({ targets: [{ dns: { family: "ipx", name: "x.com", resolver: "system" }, id: "tf", type: "dns" }] }),
|
||
);
|
||
expect(issues.some((i) => i.code === "invalid-value" && i.path.includes("family"))).toBe(true);
|
||
});
|
||
|
||
it("System 模式:拒绝 dns 中的未知字段", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [{ dns: { bogus: true, name: "x.com", resolver: "system" }, id: "t8", type: "dns" }],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("bogus"))).toBe(true);
|
||
});
|
||
|
||
it("System 模式:拒绝 server 专用字段(server/port/protocol/recordType)", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { name: "x.com", port: 53, protocol: "udp", recordType: "A", resolver: "system", server: "8.8.8.8" },
|
||
id: "t9",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("server"))).toBe(true);
|
||
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("port"))).toBe(true);
|
||
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("protocol"))).toBe(true);
|
||
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("recordType"))).toBe(true);
|
||
});
|
||
|
||
it("Server 模式:拒绝缺少 dns.server", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({ targets: [{ dns: { name: "x.com", resolver: "server" }, id: "t10", type: "dns" }] }),
|
||
);
|
||
expect(issues.some((i) => i.code === "required" && i.path.includes("server"))).toBe(true);
|
||
});
|
||
|
||
it("Server 模式:拒绝无效 port(0、-1、65536、1.5)", () => {
|
||
for (const port of [0, -1, 65536, 1.5]) {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [{ dns: { name: "x.com", port, resolver: "server", server: "8.8.8.8" }, id: "tp", type: "dns" }],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "invalid-value" && i.path.includes("port"))).toBe(true);
|
||
}
|
||
});
|
||
|
||
it("Server 模式:接受 protocol udp/tcp,拒绝无效值", () => {
|
||
for (const protocol of ["udp", "tcp"]) {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{ dns: { name: "x.com", protocol, resolver: "server", server: "8.8.8.8" }, id: "tpr", type: "dns" },
|
||
],
|
||
}),
|
||
);
|
||
expect(issues).toHaveLength(0);
|
||
}
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{ dns: { name: "x.com", protocol: "http", resolver: "server", server: "8.8.8.8" }, id: "tpr", type: "dns" },
|
||
],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "invalid-value" && i.path.includes("protocol"))).toBe(true);
|
||
});
|
||
|
||
it("Server 模式:接受有效 recordType,拒绝无效 recordType", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{ dns: { name: "x.com", recordType: "A", resolver: "server", server: "8.8.8.8" }, id: "trt", type: "dns" },
|
||
],
|
||
}),
|
||
);
|
||
expect(issues).toHaveLength(0);
|
||
|
||
const badIssues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{ dns: { name: "x.com", recordType: "FAKE", resolver: "server", server: "8.8.8.8" }, id: "trt", type: "dns" },
|
||
],
|
||
}),
|
||
);
|
||
expect(badIssues.some((i) => i.code === "invalid-value" && i.path.includes("recordType"))).toBe(true);
|
||
});
|
||
|
||
it("Server 模式:接受有效 maxResponseBytes(数字和字符串)", () => {
|
||
const issues1 = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { maxResponseBytes: 512, name: "x.com", resolver: "server", server: "8.8.8.8" },
|
||
id: "tmr",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues1).toHaveLength(0);
|
||
|
||
const issues2 = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { maxResponseBytes: "1KB", name: "x.com", resolver: "server", server: "8.8.8.8" },
|
||
id: "tmr",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues2).toHaveLength(0);
|
||
});
|
||
|
||
it("Server 模式:拒绝无效 maxResponseBytes 字符串", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { maxResponseBytes: "1kb", name: "x.com", resolver: "server", server: "8.8.8.8" },
|
||
id: "tmr-invalid",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "invalid-value" && i.path.includes("maxResponseBytes"))).toBe(true);
|
||
});
|
||
|
||
it("Server 模式:拒绝 recursionDesired/tcpFallback 非布尔值", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: {
|
||
name: "x.com",
|
||
recursionDesired: "false",
|
||
resolver: "server",
|
||
server: "8.8.8.8",
|
||
tcpFallback: "true",
|
||
},
|
||
id: "tb-dns",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("recursionDesired"))).toBe(true);
|
||
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("tcpFallback"))).toBe(true);
|
||
});
|
||
|
||
it("Server 模式:拒绝 dns 中的未知字段", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{ dns: { name: "x.com", resolver: "server", server: "8.8.8.8", unknown: true }, id: "t16", type: "dns" },
|
||
],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("unknown"))).toBe(true);
|
||
});
|
||
|
||
it("System 模式 expect:拒绝 rcode/ttlMin/ttlMax/answerCount/authoritative/recursionAvailable/truncated/authenticatedData/result/responded → dns-unsupported-expect", () => {
|
||
const serverOnlyFields = [
|
||
"rcode",
|
||
"ttlMin",
|
||
"ttlMax",
|
||
"answerCount",
|
||
"authoritative",
|
||
"recursionAvailable",
|
||
"truncated",
|
||
"authenticatedData",
|
||
"result",
|
||
"responded",
|
||
];
|
||
for (const field of serverOnlyFields) {
|
||
const expectObj: Record<string, unknown> = {};
|
||
expectObj[field] = field === "rcode" ? ["NOERROR"] : field === "result" ? ["ok"] : true;
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { name: "x.com", resolver: "system" },
|
||
expect: expectObj,
|
||
id: "tse",
|
||
name: "sys-target",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
const matched = issues.find((i) => i.code === "dns-unsupported-expect" && i.path.includes(field));
|
||
expect(matched).toBeDefined();
|
||
expect(matched!.message).toContain("system");
|
||
}
|
||
});
|
||
|
||
it("System 模式 expect:接受 values/valueCount/durationMs", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { name: "x.com", resolver: "system" },
|
||
expect: { durationMs: { lte: 100 }, valueCount: { gte: 1 }, values: { exact: ["1.2.3.4"] } },
|
||
id: "t18",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues).toHaveLength(0);
|
||
});
|
||
|
||
it("Server 模式 expect:接受所有 server expect 字段", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
|
||
expect: {
|
||
answerCount: { gte: 1 },
|
||
authenticatedData: false,
|
||
authoritative: false,
|
||
durationMs: { lte: 100 },
|
||
rcode: ["NOERROR"],
|
||
recursionAvailable: true,
|
||
responded: true,
|
||
result: [{ contains: "ok" }],
|
||
truncated: false,
|
||
ttlMax: { lte: 3600 },
|
||
ttlMin: { gte: 0 },
|
||
valueCount: { gte: 1 },
|
||
values: { exact: ["1.2.3.4"] },
|
||
},
|
||
id: "t19",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues).toHaveLength(0);
|
||
});
|
||
|
||
it("Server 模式 expect:responded=false 时拒绝协议级响应断言", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
|
||
expect: { rcode: ["NOERROR"], responded: false },
|
||
id: "trf",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "invalid-value" && i.path.includes("responded"))).toBe(true);
|
||
});
|
||
|
||
it("Server 模式 expect:拒绝未知 expect 字段", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
|
||
expect: { bogusField: 123 },
|
||
id: "t20",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("bogusField"))).toBe(true);
|
||
});
|
||
|
||
it("验证 rcode 值必须为已知 RCODE", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
|
||
expect: { rcode: ["NOTAREALCODE"] },
|
||
id: "trc",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "invalid-value" && i.path.includes("rcode"))).toBe(true);
|
||
});
|
||
|
||
it("验证布尔字段(responded/authoritative 等)", () => {
|
||
const fields = ["responded", "authoritative", "recursionAvailable", "truncated", "authenticatedData"];
|
||
for (const field of fields) {
|
||
const expectObj: Record<string, unknown> = {};
|
||
expectObj[field] = "notbool";
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
|
||
expect: expectObj,
|
||
id: "tb",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes(field))).toBe(true);
|
||
}
|
||
});
|
||
|
||
it("验证 ValueExpectation 字段(durationMs/valueCount/answerCount/ttlMin/ttlMax)", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
|
||
expect: { answerCount: [4], durationMs: [1, 2], ttlMax: [6], ttlMin: [5], valueCount: [3] },
|
||
id: "tve",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("durationMs"))).toBe(true);
|
||
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("valueCount"))).toBe(true);
|
||
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("answerCount"))).toBe(true);
|
||
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("ttlMin"))).toBe(true);
|
||
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("ttlMax"))).toBe(true);
|
||
});
|
||
|
||
it("验证 ContentExpectations 字段(result)", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
|
||
expect: { result: "not-array" },
|
||
id: "tce",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("result"))).toBe(true);
|
||
});
|
||
|
||
it("验证 DnsValuesExpectation(exact/include/exclude 数组)", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({
|
||
targets: [
|
||
{
|
||
dns: { name: "x.com", resolver: "server", server: "8.8.8.8" },
|
||
expect: { values: { exact: "not-array", exclude: true, include: 123, unknownKey: "x" } },
|
||
id: "tdv",
|
||
type: "dns",
|
||
},
|
||
],
|
||
}),
|
||
);
|
||
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("exact"))).toBe(true);
|
||
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("include"))).toBe(true);
|
||
expect(issues.some((i) => i.code === "invalid-type" && i.path.includes("exclude"))).toBe(true);
|
||
expect(issues.some((i) => i.code === "unknown-field" && i.path.includes("unknownKey"))).toBe(true);
|
||
});
|
||
|
||
it("跳过非 dns 类型目标", () => {
|
||
const issues = validateDnsConfig(
|
||
makeInput({ targets: [{ http: { url: "http://example.com" }, id: "tother", type: "http" }] }),
|
||
);
|
||
expect(issues).toHaveLength(0);
|
||
});
|
||
|
||
it("跳过非对象目标", () => {
|
||
const issues = validateDnsConfig(makeInput({ targets: ["not-an-object" as unknown as Record<string, unknown>] }));
|
||
expect(issues).toHaveLength(0);
|
||
});
|
||
});
|