1
0
Files
DiAL/tests/server/checker/variables.test.ts
lanyuanxiaoyao 7926514986 feat: 配置变量系统与 target id/name 双字段标识
- 新增顶层 variables 段支持 string/number/boolean 字面量
- target 字符串字段支持 、、{...} 转义语法
- 变量解析优先级: variables -> process.env -> 默认值 -> 报错
- 完整引用保留原始类型,部分引用拼接为字符串
- 变量替换在 YAML 解析后、AJV 校验前执行
- 替换仅作用于 targets,跳过 id/type 字段
- target 新增必填 id 字段作为唯一标识,name 改为可选展示名称
- 数据库存储/API/前端全面迁移到 id 标识
- 统一 checker 运行时类型检查为 es-toolkit predicates
- 同步 delta specs 到主 specs,归档 config-variables 变更
2026-05-17 00:37:54 +08:00

314 lines
10 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, test } from "bun:test";
import { extractVariables, resolveVariables } from "../../../src/server/checker/variables";
describe("config variables", () => {
test("提取合法 variables 类型", () => {
const result = extractVariables({ variables: { enabled: true, host: "example.com", port: 5432 } });
expect(result.issues).toHaveLength(0);
expect(Object.fromEntries(result.variables)).toEqual({ enabled: true, host: "example.com", port: 5432 });
});
test("拒绝非法 variables value 和 key", () => {
const result = extractVariables({
variables: {
"123start": "value",
empty: null,
list: [1, 2, 3],
obj: { a: 1 },
},
});
expect(result.issues.map((item) => [item.code, item.path, item.message])).toEqual([
["invalid-format", "variables.123start", "变量名不符合命名规则"],
["invalid-type", "variables.empty", "变量值不允许为 null"],
["invalid-type", "variables.list", "变量值不允许为 array"],
["invalid-type", "variables.obj", "变量值不允许为 object"],
]);
});
test("解析简单引用、默认值、转义、多变量拼接和无引用字符串", () => {
const result = resolveVariables({
targets: [
{
http: {
body: "Hello $${name}",
headers: {
Authorization: "Bearer ${token}",
Pattern: "${PATTERN|foo|bar}",
},
url: "${protocol}://${host}:${port}/api",
},
id: "api-health",
name: "API",
type: "http",
},
],
variables: { host: "example.com", port: 443, protocol: "https", token: "abc" },
});
expect(result.issues).toHaveLength(0);
const target = (
result.config as { targets: Array<{ http: { body: string; headers: Record<string, string>; url: string } }> }
).targets[0]!;
expect(target.http.url).toBe("https://example.com:443/api");
expect(target.http.headers["Authorization"]).toBe("Bearer abc");
expect(target.http.headers["Pattern"]).toBe("foo|bar");
expect(target.http.body).toBe("Hello ${name}");
});
test("完整引用保留类型,部分引用强制为字符串", () => {
const result = resolveVariables({
targets: [
{
http: {
body: "port: ${port}",
ignoreSSL: "${ssl}",
maxRedirects: "${port}",
url: "${host}",
},
id: "typed-http",
type: "http",
},
],
variables: { host: "https://example.com", port: 5, ssl: true },
});
expect(result.issues).toHaveLength(0);
const http = (result.config as { targets: Array<{ http: Record<string, unknown> }> }).targets[0]!.http;
expect(http["url"]).toBe("https://example.com");
expect(http["maxRedirects"]).toBe(5);
expect(http["ignoreSSL"]).toBe(true);
expect(http["body"]).toBe("port: 5");
});
test("环境变量和默认值在完整引用时做类型推断", () => {
const originalMaxRedirects = process.env["MAX_REDIRECTS"];
const originalIgnoreSsl = process.env["IGNORE_SSL"];
process.env["MAX_REDIRECTS"] = "5";
process.env["IGNORE_SSL"] = "false";
try {
const result = resolveVariables({
targets: [
{
http: {
ignoreSSL: "${IGNORE_SSL}",
maxRedirects: "${MAX_REDIRECTS}",
url: "${HOST|localhost}",
},
id: "env-http",
type: "http",
},
],
});
expect(result.issues).toHaveLength(0);
const http = (result.config as { targets: Array<{ http: Record<string, unknown> }> }).targets[0]!.http;
expect(http["maxRedirects"]).toBe(5);
expect(http["ignoreSSL"]).toBe(false);
expect(http["url"]).toBe("localhost");
} finally {
restoreEnv("MAX_REDIRECTS", originalMaxRedirects);
restoreEnv("IGNORE_SSL", originalIgnoreSsl);
}
});
test("解析优先级为 variables、环境变量、默认值", () => {
const originalPort = process.env["PORT"];
const originalHost = process.env["HOST"];
process.env["PORT"] = "3000";
process.env["HOST"] = "env-host";
try {
const result = resolveVariables({
targets: [
{
http: {
body: "${MISSING|fallback}",
headers: { Host: "${HOST}" },
maxRedirects: "${PORT}",
url: "${HOST_FROM_VARIABLES}",
},
id: "priority-http",
type: "http",
},
],
variables: { HOST_FROM_VARIABLES: "config-host", PORT: 5432 },
});
expect(result.issues).toHaveLength(0);
const http = (result.config as { targets: Array<{ http: Record<string, unknown> }> }).targets[0]!.http;
expect(http["maxRedirects"]).toBe(5432);
expect((http["headers"] as Record<string, string>)["Host"]).toBe("env-host");
expect(http["body"]).toBe("fallback");
expect(http["url"]).toBe("config-host");
} finally {
restoreEnv("PORT", originalPort);
restoreEnv("HOST", originalHost);
}
});
test("替换范围仅 targets且跳过 id 和 type 字段", () => {
const result = resolveVariables({
defaults: { interval: "${interval}" },
server: { host: "${host}" },
targets: [
{
cmd: {
args: ["--host", "${host}"],
env: { TOKEN: "${token}" },
exec: "echo",
},
id: "${id}",
type: "${type}",
},
],
variables: { host: "localhost", id: "resolved", interval: "30s", token: "abc", type: "cmd" },
});
expect(result.issues).toHaveLength(0);
const config = result.config as {
defaults: { interval: string };
server: { host: string };
targets: Array<{ cmd: { args: string[]; env: Record<string, string> }; id: string; type: string }>;
};
expect(config.server.host).toBe("${host}");
expect(config.defaults.interval).toBe("${interval}");
expect(config.targets[0]!.id).toBe("${id}");
expect(config.targets[0]!.type).toBe("${type}");
expect(config.targets[0]!.cmd.args[1]).toBe("localhost");
expect(config.targets[0]!.cmd.env["TOKEN"]).toBe("abc");
});
test("默认值推断为 booleantrue/false", () => {
const result = resolveVariables({
targets: [
{
http: {
ignoreSSL: "${DUMMY_SSL|false}",
url: "${HOST|localhost}",
},
id: "default-bool",
type: "http",
},
],
});
expect(result.issues).toHaveLength(0);
const http = (result.config as { targets: Array<{ http: Record<string, unknown> }> }).targets[0]!.http;
expect(http["ignoreSSL"]).toBe(false);
expect(typeof http["ignoreSSL"]).toBe("boolean");
const result2 = resolveVariables({
targets: [
{
http: {
ignoreSSL: "${DUMMY_SSL|true}",
url: "${HOST|localhost}",
},
id: "default-bool-true",
type: "http",
},
],
});
expect(result2.issues).toHaveLength(0);
const http2 = (result2.config as { targets: Array<{ http: Record<string, unknown> }> }).targets[0]!.http;
expect(http2["ignoreSSL"]).toBe(true);
expect(typeof http2["ignoreSSL"]).toBe("boolean");
});
test("runtime 段不替换", () => {
const result = resolveVariables({
runtime: { maxConcurrentChecks: 10, retention: "${retention}" },
targets: [
{
http: { url: "${host}" },
id: "rt-no-replace",
type: "http",
},
],
variables: { host: "https://example.com", retention: "24h" },
});
expect(result.issues).toHaveLength(0);
const config = result.config as { runtime: { retention: string } };
expect(config.runtime.retention).toBe("${retention}");
});
test("variables 段为非对象时报错", () => {
const strResult = extractVariables({ variables: "invalid" });
expect(strResult.issues).toHaveLength(1);
expect(strResult.issues[0]!.code).toBe("invalid-type");
const numResult = extractVariables({ variables: 123 });
expect(numResult.issues).toHaveLength(1);
expect(numResult.issues[0]!.code).toBe("invalid-type");
const nullResult = extractVariables({ variables: null });
expect(nullResult.issues).toHaveLength(1);
expect(nullResult.issues[0]!.code).toBe("invalid-type");
});
test("无 variables 段时环境变量仍可引用", () => {
const original = process.env["DIAL_TEST_ENV_ONLY"];
process.env["DIAL_TEST_ENV_ONLY"] = "env-value";
try {
const result = resolveVariables({
targets: [
{
http: { url: "${DIAL_TEST_ENV_ONLY}" },
id: "env-only",
type: "http",
},
],
});
expect(result.issues).toHaveLength(0);
const http = (result.config as { targets: Array<{ http: { url: string } }> }).targets[0]!.http;
expect(http.url).toBe("env-value");
} finally {
restoreEnv("DIAL_TEST_ENV_ONLY", original);
}
});
test("缺失变量收集所有 unresolved-variable issue", () => {
const result = resolveVariables({
targets: [
{
http: {
headers: { Authorization: "${missing_token}" },
url: "${missing_base_url}/health/${missing_path}",
},
id: "api-health",
type: "http",
},
],
});
expect(result.issues).toHaveLength(3);
expect(result.issues.map((item) => item.code)).toEqual([
"unresolved-variable",
"unresolved-variable",
"unresolved-variable",
]);
expect(result.issues.map((item) => item.path)).toEqual([
"targets[0].http.headers.Authorization",
"targets[0].http.url",
"targets[0].http.url",
]);
expect(result.issues.every((item) => item.targetId === "api-health")).toBe(true);
expect(result.issues.map((item) => item.message).join("\n")).toContain('变量 "missing_base_url"');
});
});
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[key];
return;
}
process.env[key] = value;
}