mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-09 20:26:40 +00:00
Use encodePath only for internal map key grouping (collision-free) and sanitize for Traefik-facing router/service names (unchanged for existing users). Extract pure functions into pathUtils.ts so tests can run without DB dependencies.
237 lines
7.4 KiB
TypeScript
237 lines
7.4 KiB
TypeScript
import { assertEquals } from "../../../test/assert";
|
|
import { encodePath, sanitize } from "./pathUtils";
|
|
|
|
function runTests() {
|
|
console.log("Running path encoding tests...\n");
|
|
|
|
// Test 1: null and empty return empty string
|
|
{
|
|
assertEquals(encodePath(null), "", "null should return empty");
|
|
assertEquals(
|
|
encodePath(undefined),
|
|
"",
|
|
"undefined should return empty"
|
|
);
|
|
assertEquals(encodePath(""), "", "empty string should return empty");
|
|
console.log(" PASS: null/undefined/empty return empty string");
|
|
}
|
|
|
|
// Test 2: root path "/" encodes to something non-empty
|
|
{
|
|
const result = encodePath("/");
|
|
assertEquals(result !== "", true, "root path should not be empty");
|
|
assertEquals(result, "2f", "root path should encode to hex of '/'");
|
|
console.log(" PASS: root path encodes to non-empty string");
|
|
}
|
|
|
|
// Test 3: different paths produce different encoded values
|
|
{
|
|
const paths = [
|
|
"/",
|
|
"/api",
|
|
"/a/b",
|
|
"/a-b",
|
|
"/a.b",
|
|
"/a_b",
|
|
"/api/v1",
|
|
"/api/v2"
|
|
];
|
|
const encoded = new Set<string>();
|
|
let collision = false;
|
|
for (const p of paths) {
|
|
const e = encodePath(p);
|
|
if (encoded.has(e)) {
|
|
collision = true;
|
|
break;
|
|
}
|
|
encoded.add(e);
|
|
}
|
|
assertEquals(collision, false, "no two different paths should collide");
|
|
console.log(" PASS: all different paths produce unique encodings");
|
|
}
|
|
|
|
// Test 4: alphanumeric characters pass through unchanged
|
|
{
|
|
assertEquals(
|
|
encodePath("/api"),
|
|
"2fapi",
|
|
"/api should encode slash only"
|
|
);
|
|
assertEquals(encodePath("/v1"), "2fv1", "/v1 should encode slash only");
|
|
console.log(" PASS: alphanumeric characters preserved");
|
|
}
|
|
|
|
// Test 5: special characters are hex-encoded
|
|
{
|
|
const dotEncoded = encodePath("/a.b");
|
|
const dashEncoded = encodePath("/a-b");
|
|
const slashEncoded = encodePath("/a/b");
|
|
const underscoreEncoded = encodePath("/a_b");
|
|
|
|
// all should be different
|
|
const set = new Set([
|
|
dotEncoded,
|
|
dashEncoded,
|
|
slashEncoded,
|
|
underscoreEncoded
|
|
]);
|
|
assertEquals(
|
|
set.size,
|
|
4,
|
|
"dot, dash, slash, underscore paths should all be unique"
|
|
);
|
|
console.log(" PASS: special characters produce unique encodings");
|
|
}
|
|
|
|
// Test 6: full key generation - different paths create different keys
|
|
{
|
|
function makeKey(
|
|
resourceId: number,
|
|
path: string | null,
|
|
pathMatchType: string | null
|
|
) {
|
|
const targetPath = encodePath(path);
|
|
const pmt = pathMatchType || "";
|
|
const pathKey = [targetPath, pmt, "", ""].filter(Boolean).join("-");
|
|
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
|
return sanitize(mapKey);
|
|
}
|
|
|
|
const keySlash = makeKey(1, "/", "prefix");
|
|
const keyApi = makeKey(1, "/api", "prefix");
|
|
const keyNull = makeKey(1, null, null);
|
|
|
|
assertEquals(
|
|
keySlash !== keyApi,
|
|
true,
|
|
"/ and /api should have different keys"
|
|
);
|
|
assertEquals(
|
|
keySlash !== keyNull,
|
|
true,
|
|
"/ and null should have different keys"
|
|
);
|
|
assertEquals(
|
|
keyApi !== keyNull,
|
|
true,
|
|
"/api and null should have different keys"
|
|
);
|
|
|
|
console.log(
|
|
" PASS: different paths create different resource map keys"
|
|
);
|
|
}
|
|
|
|
// Test 7: same path always produces same key (deterministic)
|
|
{
|
|
assertEquals(
|
|
encodePath("/api"),
|
|
encodePath("/api"),
|
|
"same input should produce same output"
|
|
);
|
|
assertEquals(
|
|
encodePath("/a/b/c"),
|
|
encodePath("/a/b/c"),
|
|
"same input should produce same output"
|
|
);
|
|
console.log(" PASS: encoding is deterministic");
|
|
}
|
|
|
|
// Test 8: encoded result is alphanumeric (valid for Traefik names after sanitize)
|
|
{
|
|
const paths = [
|
|
"/",
|
|
"/api",
|
|
"/a/b",
|
|
"/a-b",
|
|
"/a.b",
|
|
"/complex/path/here"
|
|
];
|
|
for (const p of paths) {
|
|
const e = encodePath(p);
|
|
const isAlphanumeric = /^[a-zA-Z0-9]+$/.test(e);
|
|
assertEquals(
|
|
isAlphanumeric,
|
|
true,
|
|
`encodePath("${p}") = "${e}" should be alphanumeric`
|
|
);
|
|
}
|
|
console.log(" PASS: encoded values are alphanumeric");
|
|
}
|
|
|
|
// Test 9: backward compatibility - Traefik names use sanitize, internal keys use encodePath
|
|
{
|
|
// Simulate the dual-key approach from getTraefikConfig
|
|
function makeKeys(
|
|
resourceId: number,
|
|
path: string | null,
|
|
pathMatchType: string | null
|
|
) {
|
|
// Internal map key (collision-free grouping)
|
|
const encodedPath = encodePath(path);
|
|
const internalPathKey = [encodedPath, pathMatchType || ""]
|
|
.filter(Boolean)
|
|
.join("-");
|
|
const internalMapKey = [resourceId, internalPathKey]
|
|
.filter(Boolean)
|
|
.join("-");
|
|
|
|
// Traefik-facing key (backward-compatible naming)
|
|
const sanitizedPath = sanitize(path) || "";
|
|
const traefikPathKey = [sanitizedPath, pathMatchType || ""]
|
|
.filter(Boolean)
|
|
.join("-");
|
|
const traefikKey = sanitize(
|
|
[resourceId, traefikPathKey].filter(Boolean).join("-")
|
|
);
|
|
|
|
return { internalMapKey, traefikKey };
|
|
}
|
|
|
|
// /a/b and /a-b should have DIFFERENT internal keys (no collision)
|
|
const keysAB = makeKeys(1, "/a/b", "prefix");
|
|
const keysDash = makeKeys(1, "/a-b", "prefix");
|
|
assertEquals(
|
|
keysAB.internalMapKey !== keysDash.internalMapKey,
|
|
true,
|
|
"/a/b and /a-b must have different internal map keys"
|
|
);
|
|
|
|
// /a/b and /a-b produce the SAME Traefik key (backward compat — sanitize maps both to "a-b")
|
|
assertEquals(
|
|
keysAB.traefikKey,
|
|
keysDash.traefikKey,
|
|
"/a/b and /a-b should produce same Traefik key (sanitize behavior)"
|
|
);
|
|
|
|
// For a resource with no path, Traefik key should match the old behavior
|
|
const keysNoPath = makeKeys(42, null, null);
|
|
assertEquals(
|
|
keysNoPath.traefikKey,
|
|
"42",
|
|
"no-path resource should have resourceId-only Traefik key"
|
|
);
|
|
|
|
// Traefik key for /api prefix should match old sanitize-based output
|
|
const keysApi = makeKeys(1, "/api", "prefix");
|
|
assertEquals(
|
|
keysApi.traefikKey,
|
|
"1-api-prefix",
|
|
"Traefik key should match old sanitize-based format"
|
|
);
|
|
|
|
console.log(
|
|
" PASS: backward compatibility - internal keys differ, Traefik names preserved"
|
|
);
|
|
}
|
|
|
|
console.log("\nAll path encoding tests passed!");
|
|
}
|
|
|
|
try {
|
|
runTests();
|
|
} catch (error) {
|
|
console.error("Test failed:", error);
|
|
process.exit(1);
|
|
}
|