mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 02:28:27 +00:00
test(config): avoid duplicate include resolution in throw assertions
This commit is contained in:
@@ -45,6 +45,23 @@ function resolve(obj: unknown, files: Record<string, unknown> = {}, basePath = D
|
|||||||
return resolveConfigIncludes(obj, basePath, createMockResolver(files));
|
return resolveConfigIncludes(obj, basePath, createMockResolver(files));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectResolveIncludeError(
|
||||||
|
run: () => unknown,
|
||||||
|
expectedPattern?: RegExp,
|
||||||
|
): ConfigIncludeError {
|
||||||
|
let thrown: unknown;
|
||||||
|
try {
|
||||||
|
run();
|
||||||
|
} catch (error) {
|
||||||
|
thrown = error;
|
||||||
|
}
|
||||||
|
expect(thrown).toBeInstanceOf(ConfigIncludeError);
|
||||||
|
if (expectedPattern) {
|
||||||
|
expect((thrown as Error).message).toMatch(expectedPattern);
|
||||||
|
}
|
||||||
|
return thrown as ConfigIncludeError;
|
||||||
|
}
|
||||||
|
|
||||||
describe("resolveConfigIncludes", () => {
|
describe("resolveConfigIncludes", () => {
|
||||||
it("passes through primitives unchanged", () => {
|
it("passes through primitives unchanged", () => {
|
||||||
expect(resolve("hello")).toBe("hello");
|
expect(resolve("hello")).toBe("hello");
|
||||||
@@ -74,8 +91,7 @@ describe("resolveConfigIncludes", () => {
|
|||||||
const absolute = etcOpenClawPath("agents.json");
|
const absolute = etcOpenClawPath("agents.json");
|
||||||
const files = { [absolute]: { list: [{ id: "main" }] } };
|
const files = { [absolute]: { list: [{ id: "main" }] } };
|
||||||
const obj = { agents: { $include: absolute } };
|
const obj = { agents: { $include: absolute } };
|
||||||
expect(() => resolve(obj, files)).toThrow(ConfigIncludeError);
|
expectResolveIncludeError(() => resolve(obj, files), /escapes config directory/);
|
||||||
expect(() => resolve(obj, files)).toThrow(/escapes config directory/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves array $include with deep merge", () => {
|
it("resolves array $include with deep merge", () => {
|
||||||
@@ -146,8 +162,7 @@ describe("resolveConfigIncludes", () => {
|
|||||||
|
|
||||||
it("throws ConfigIncludeError for missing file", () => {
|
it("throws ConfigIncludeError for missing file", () => {
|
||||||
const obj = { $include: "./missing.json" };
|
const obj = { $include: "./missing.json" };
|
||||||
expect(() => resolve(obj)).toThrow(ConfigIncludeError);
|
expectResolveIncludeError(() => resolve(obj), /Failed to read include file/);
|
||||||
expect(() => resolve(obj)).toThrow(/Failed to read include file/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws ConfigIncludeError for invalid JSON", () => {
|
it("throws ConfigIncludeError for invalid JSON", () => {
|
||||||
@@ -156,10 +171,8 @@ describe("resolveConfigIncludes", () => {
|
|||||||
parseJson: JSON.parse,
|
parseJson: JSON.parse,
|
||||||
};
|
};
|
||||||
const obj = { $include: "./bad.json" };
|
const obj = { $include: "./bad.json" };
|
||||||
expect(() => resolveConfigIncludes(obj, DEFAULT_BASE_PATH, resolver)).toThrow(
|
expectResolveIncludeError(
|
||||||
ConfigIncludeError,
|
() => resolveConfigIncludes(obj, DEFAULT_BASE_PATH, resolver),
|
||||||
);
|
|
||||||
expect(() => resolveConfigIncludes(obj, DEFAULT_BASE_PATH, resolver)).toThrow(
|
|
||||||
/Failed to parse include file/,
|
/Failed to parse include file/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -215,8 +228,7 @@ describe("resolveConfigIncludes", () => {
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
for (const testCase of cases) {
|
for (const testCase of cases) {
|
||||||
expect(() => resolve(testCase.obj, files)).toThrow(ConfigIncludeError);
|
expectResolveIncludeError(() => resolve(testCase.obj, files), testCase.expectedPattern);
|
||||||
expect(() => resolve(testCase.obj, files)).toThrow(testCase.expectedPattern);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -230,8 +242,7 @@ describe("resolveConfigIncludes", () => {
|
|||||||
files[configPath("level15.json")] = { done: true };
|
files[configPath("level15.json")] = { done: true };
|
||||||
|
|
||||||
const obj = { $include: "./level0.json" };
|
const obj = { $include: "./level0.json" };
|
||||||
expect(() => resolve(obj, files)).toThrow(ConfigIncludeError);
|
expectResolveIncludeError(() => resolve(obj, files), /Maximum include depth/);
|
||||||
expect(() => resolve(obj, files)).toThrow(/Maximum include depth/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows depth 10 but rejects depth 11", () => {
|
it("allows depth 10 but rejects depth 11", () => {
|
||||||
@@ -251,8 +262,10 @@ describe("resolveConfigIncludes", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
failFiles[configPath("fail10.json")] = { done: true };
|
failFiles[configPath("fail10.json")] = { done: true };
|
||||||
expect(() => resolve({ $include: "./fail0.json" }, failFiles)).toThrow(ConfigIncludeError);
|
expectResolveIncludeError(
|
||||||
expect(() => resolve({ $include: "./fail0.json" }, failFiles)).toThrow(/Maximum include depth/);
|
() => resolve({ $include: "./fail0.json" }, failFiles),
|
||||||
|
/Maximum include depth/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles relative paths correctly", () => {
|
it("handles relative paths correctly", () => {
|
||||||
@@ -279,10 +292,8 @@ describe("resolveConfigIncludes", () => {
|
|||||||
it("rejects parent directory traversal escaping config directory (CWE-22)", () => {
|
it("rejects parent directory traversal escaping config directory (CWE-22)", () => {
|
||||||
const files = { [sharedPath("common.json")]: { shared: true } };
|
const files = { [sharedPath("common.json")]: { shared: true } };
|
||||||
const obj = { $include: "../../shared/common.json" };
|
const obj = { $include: "../../shared/common.json" };
|
||||||
expect(() => resolve(obj, files, configPath("sub", "openclaw.json"))).toThrow(
|
expectResolveIncludeError(
|
||||||
ConfigIncludeError,
|
() => resolve(obj, files, configPath("sub", "openclaw.json")),
|
||||||
);
|
|
||||||
expect(() => resolve(obj, files, configPath("sub", "openclaw.json"))).toThrow(
|
|
||||||
/escapes config directory/,
|
/escapes config directory/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -388,9 +399,9 @@ describe("security: path traversal protection (CWE-22)", () => {
|
|||||||
] as const;
|
] as const;
|
||||||
for (const testCase of cases) {
|
for (const testCase of cases) {
|
||||||
const obj = { $include: testCase.includePath };
|
const obj = { $include: testCase.includePath };
|
||||||
expect(() => resolve(obj, {}), testCase.includePath).toThrow(ConfigIncludeError);
|
expectResolveIncludeError(() => resolve(obj, {}));
|
||||||
if (testCase.expectEscapesMessage) {
|
if (testCase.expectEscapesMessage) {
|
||||||
expect(() => resolve(obj, {}), testCase.includePath).toThrow(/escapes config directory/);
|
expectResolveIncludeError(() => resolve(obj, {}), /escapes config directory/);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -407,9 +418,9 @@ describe("security: path traversal protection (CWE-22)", () => {
|
|||||||
] as const;
|
] as const;
|
||||||
for (const testCase of cases) {
|
for (const testCase of cases) {
|
||||||
const obj = { $include: testCase.includePath };
|
const obj = { $include: testCase.includePath };
|
||||||
expect(() => resolve(obj, {}), testCase.includePath).toThrow(ConfigIncludeError);
|
expectResolveIncludeError(() => resolve(obj, {}));
|
||||||
if (testCase.expectEscapesMessage) {
|
if (testCase.expectEscapesMessage) {
|
||||||
expect(() => resolve(obj, {}), testCase.includePath).toThrow(/escapes config directory/);
|
expectResolveIncludeError(() => resolve(obj, {}), /escapes config directory/);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -558,7 +569,7 @@ describe("security: path traversal protection (CWE-22)", () => {
|
|||||||
for (const testCase of cases) {
|
for (const testCase of cases) {
|
||||||
const obj = { $include: testCase.includePath };
|
const obj = { $include: testCase.includePath };
|
||||||
if (testCase.expectedError) {
|
if (testCase.expectedError) {
|
||||||
expect(() => resolve(obj, {}), testCase.includePath).toThrow(testCase.expectedError);
|
expectResolveIncludeError(() => resolve(obj, {}));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Path with null byte should be rejected or handled safely.
|
// Path with null byte should be rejected or handled safely.
|
||||||
|
|||||||
Reference in New Issue
Block a user