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> }): 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 = {}; 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 = {}; 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] })); expect(issues).toHaveLength(0); }); });