mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-09 12:16:36 +00:00
test: add comprehensive backward compatibility tests for path routing fix
This commit is contained in:
committed by
Shreyas Papinwar
parent
e58f0c9f07
commit
244f497a9c
@@ -1,10 +1,93 @@
|
|||||||
import { assertEquals } from "../../../test/assert";
|
import { assertEquals } from "../../../test/assert";
|
||||||
import { encodePath, sanitize } from "./pathUtils";
|
import { encodePath, sanitize } from "./pathUtils";
|
||||||
|
|
||||||
function runTests() {
|
// ── Helpers ──────────────────────────────────────────────────────────
|
||||||
console.log("Running path encoding tests...\n");
|
|
||||||
|
|
||||||
// Test 1: null and empty return empty string
|
/**
|
||||||
|
* Exact replica of the OLD key computation from upstream main.
|
||||||
|
* This is what existing Pangolin deployments use today for both
|
||||||
|
* map grouping AND Traefik router/service names.
|
||||||
|
*
|
||||||
|
* Source: origin/main server/lib/traefik/getTraefikConfig.ts lines 130-146
|
||||||
|
*/
|
||||||
|
function oldKeyComputation(
|
||||||
|
resourceId: number,
|
||||||
|
path: string | null,
|
||||||
|
pathMatchType: string | null,
|
||||||
|
rewritePath: string | null,
|
||||||
|
rewritePathType: string | null
|
||||||
|
): string {
|
||||||
|
const targetPath = sanitize(path) || "";
|
||||||
|
const pmt = pathMatchType || "";
|
||||||
|
const rp = rewritePath || "";
|
||||||
|
const rpt = rewritePathType || "";
|
||||||
|
const pathKey = [targetPath, pmt, rp, rpt].filter(Boolean).join("-");
|
||||||
|
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
||||||
|
return sanitize(mapKey) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replica of the NEW dual-key computation from our fix.
|
||||||
|
* Returns both the internal map key (for grouping) and the
|
||||||
|
* Traefik-facing key (for router/service names).
|
||||||
|
*
|
||||||
|
* Source: our getTraefikConfig.ts lines 135-163
|
||||||
|
*/
|
||||||
|
function newKeyComputation(
|
||||||
|
resourceId: number,
|
||||||
|
path: string | null,
|
||||||
|
pathMatchType: string | null,
|
||||||
|
rewritePath: string | null,
|
||||||
|
rewritePathType: string | null
|
||||||
|
): { internalMapKey: string; traefikKey: string } {
|
||||||
|
const pmt = pathMatchType || "";
|
||||||
|
const rp = rewritePath || "";
|
||||||
|
const rpt = rewritePathType || "";
|
||||||
|
|
||||||
|
// Internal map key: uses encodePath (collision-free)
|
||||||
|
const encodedPath = encodePath(path);
|
||||||
|
const internalPathKey = [encodedPath, pmt, rp, rpt]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("-");
|
||||||
|
const internalMapKey = [resourceId, internalPathKey]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("-");
|
||||||
|
|
||||||
|
// Traefik-facing key: uses sanitize (backward-compatible)
|
||||||
|
const sanitizedPath = sanitize(path) || "";
|
||||||
|
const traefikPathKey = [sanitizedPath, pmt, rp, rpt]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("-");
|
||||||
|
const traefikKey = sanitize(
|
||||||
|
[resourceId, traefikPathKey].filter(Boolean).join("-")
|
||||||
|
);
|
||||||
|
|
||||||
|
return { internalMapKey, traefikKey: traefikKey || "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the full Traefik router/service names the way getTraefikConfig does.
|
||||||
|
*/
|
||||||
|
function buildTraefikNames(key: string, resourceName: string) {
|
||||||
|
const name = sanitize(resourceName) || "";
|
||||||
|
return {
|
||||||
|
routerName: `${key}-${name}-router`,
|
||||||
|
serviceName: `${key}-${name}-service`,
|
||||||
|
transportName: `${key}-transport`,
|
||||||
|
headersMiddlewareName: `${key}-headers-middleware`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
console.log("Running path encoding & backward compatibility tests...\n");
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
|
||||||
|
// ── encodePath unit tests ────────────────────────────────────────
|
||||||
|
|
||||||
|
// Test 1: null/undefined/empty
|
||||||
{
|
{
|
||||||
assertEquals(encodePath(null), "", "null should return empty");
|
assertEquals(encodePath(null), "", "null should return empty");
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@@ -13,131 +96,43 @@ function runTests() {
|
|||||||
"undefined should return empty"
|
"undefined should return empty"
|
||||||
);
|
);
|
||||||
assertEquals(encodePath(""), "", "empty string should return empty");
|
assertEquals(encodePath(""), "", "empty string should return empty");
|
||||||
console.log(" PASS: null/undefined/empty return empty string");
|
console.log(" PASS: encodePath handles null/undefined/empty");
|
||||||
|
passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 2: root path "/" encodes to something non-empty
|
// Test 2: root path
|
||||||
{
|
{
|
||||||
const result = encodePath("/");
|
assertEquals(encodePath("/"), "2f", "/ should encode to 2f");
|
||||||
assertEquals(result !== "", true, "root path should not be empty");
|
console.log(" PASS: encodePath encodes root path");
|
||||||
assertEquals(result, "2f", "root path should encode to hex of '/'");
|
passed++;
|
||||||
console.log(" PASS: root path encodes to non-empty string");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 3: different paths produce different encoded values
|
// Test 3: alphanumeric passthrough
|
||||||
{
|
{
|
||||||
const paths = [
|
assertEquals(encodePath("/api"), "2fapi", "/api encodes slash only");
|
||||||
"/",
|
assertEquals(encodePath("/v1"), "2fv1", "/v1 encodes slash only");
|
||||||
"/api",
|
assertEquals(encodePath("abc"), "abc", "plain alpha passes through");
|
||||||
"/a/b",
|
console.log(" PASS: encodePath preserves alphanumeric chars");
|
||||||
"/a-b",
|
passed++;
|
||||||
"/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
|
// Test 4: all special chars produce unique hex
|
||||||
{
|
{
|
||||||
|
const paths = ["/a/b", "/a-b", "/a.b", "/a_b", "/a b"];
|
||||||
|
const results = paths.map((p) => encodePath(p));
|
||||||
|
const unique = new Set(results);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
encodePath("/api"),
|
unique.size,
|
||||||
"2fapi",
|
paths.length,
|
||||||
"/api should encode slash only"
|
"all special-char paths must produce unique encodings"
|
||||||
);
|
);
|
||||||
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(
|
console.log(
|
||||||
" PASS: different paths create different resource map keys"
|
" PASS: encodePath produces unique output for different special chars"
|
||||||
);
|
);
|
||||||
|
passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 7: same path always produces same key (deterministic)
|
// Test 5: output is always alphanumeric (safe for Traefik names)
|
||||||
{
|
|
||||||
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 = [
|
const paths = [
|
||||||
"/",
|
"/",
|
||||||
@@ -149,83 +144,491 @@ function runTests() {
|
|||||||
];
|
];
|
||||||
for (const p of paths) {
|
for (const p of paths) {
|
||||||
const e = encodePath(p);
|
const e = encodePath(p);
|
||||||
const isAlphanumeric = /^[a-zA-Z0-9]+$/.test(e);
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
isAlphanumeric,
|
/^[a-zA-Z0-9]+$/.test(e),
|
||||||
true,
|
true,
|
||||||
`encodePath("${p}") = "${e}" should be alphanumeric`
|
`encodePath("${p}") = "${e}" must be alphanumeric`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log(" PASS: encoded values are alphanumeric");
|
console.log(" PASS: encodePath output is always alphanumeric");
|
||||||
|
passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 9: backward compatibility - Traefik names use sanitize, internal keys use encodePath
|
// Test 6: deterministic
|
||||||
{
|
{
|
||||||
// Simulate the dual-key approach from getTraefikConfig
|
assertEquals(
|
||||||
function makeKeys(
|
encodePath("/api"),
|
||||||
resourceId: number,
|
encodePath("/api"),
|
||||||
path: string | null,
|
"same input same output"
|
||||||
pathMatchType: string | null
|
);
|
||||||
) {
|
assertEquals(
|
||||||
// Internal map key (collision-free grouping)
|
encodePath("/a/b/c"),
|
||||||
const encodedPath = encodePath(path);
|
encodePath("/a/b/c"),
|
||||||
const internalPathKey = [encodedPath, pathMatchType || ""]
|
"same input same output"
|
||||||
.filter(Boolean)
|
);
|
||||||
.join("-");
|
console.log(" PASS: encodePath is deterministic");
|
||||||
const internalMapKey = [resourceId, internalPathKey]
|
passed++;
|
||||||
.filter(Boolean)
|
}
|
||||||
.join("-");
|
|
||||||
|
|
||||||
// Traefik-facing key (backward-compatible naming)
|
// Test 7: many distinct paths never collide
|
||||||
const sanitizedPath = sanitize(path) || "";
|
{
|
||||||
const traefikPathKey = [sanitizedPath, pathMatchType || ""]
|
const paths = [
|
||||||
.filter(Boolean)
|
"/",
|
||||||
.join("-");
|
"/api",
|
||||||
const traefikKey = sanitize(
|
"/api/v1",
|
||||||
[resourceId, traefikPathKey].filter(Boolean).join("-")
|
"/api/v2",
|
||||||
|
"/a/b",
|
||||||
|
"/a-b",
|
||||||
|
"/a.b",
|
||||||
|
"/a_b",
|
||||||
|
"/health",
|
||||||
|
"/health/check",
|
||||||
|
"/admin",
|
||||||
|
"/admin/users",
|
||||||
|
"/api/v1/users",
|
||||||
|
"/api/v1/posts",
|
||||||
|
"/app",
|
||||||
|
"/app/dashboard"
|
||||||
|
];
|
||||||
|
const encoded = new Set(paths.map((p) => encodePath(p)));
|
||||||
|
assertEquals(
|
||||||
|
encoded.size,
|
||||||
|
paths.length,
|
||||||
|
`expected ${paths.length} unique encodings, got ${encoded.size}`
|
||||||
|
);
|
||||||
|
console.log(" PASS: 16 realistic paths all produce unique encodings");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Backward compatibility: Traefik names must match old code ─────
|
||||||
|
|
||||||
|
// Test 8: simple resource, no path — Traefik name unchanged
|
||||||
|
{
|
||||||
|
const oldKey = oldKeyComputation(1, null, null, null, null);
|
||||||
|
const { traefikKey } = newKeyComputation(1, null, null, null, null);
|
||||||
|
assertEquals(
|
||||||
|
traefikKey,
|
||||||
|
oldKey,
|
||||||
|
"no-path resource: Traefik key must match old"
|
||||||
|
);
|
||||||
|
console.log(" PASS: backward compat — no path resource");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 9: resource with /api prefix — Traefik name unchanged
|
||||||
|
{
|
||||||
|
const oldKey = oldKeyComputation(1, "/api", "prefix", null, null);
|
||||||
|
const { traefikKey } = newKeyComputation(
|
||||||
|
1,
|
||||||
|
"/api",
|
||||||
|
"prefix",
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
traefikKey,
|
||||||
|
oldKey,
|
||||||
|
"/api prefix: Traefik key must match old"
|
||||||
|
);
|
||||||
|
console.log(" PASS: backward compat — /api prefix");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 10: resource with exact path — Traefik name unchanged
|
||||||
|
{
|
||||||
|
const oldKey = oldKeyComputation(5, "/health", "exact", null, null);
|
||||||
|
const { traefikKey } = newKeyComputation(
|
||||||
|
5,
|
||||||
|
"/health",
|
||||||
|
"exact",
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
traefikKey,
|
||||||
|
oldKey,
|
||||||
|
"/health exact: Traefik key must match old"
|
||||||
|
);
|
||||||
|
console.log(" PASS: backward compat — /health exact");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 11: resource with regex path — Traefik name unchanged
|
||||||
|
{
|
||||||
|
const oldKey = oldKeyComputation(
|
||||||
|
3,
|
||||||
|
"^/api/v[0-9]+",
|
||||||
|
"regex",
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const { traefikKey } = newKeyComputation(
|
||||||
|
3,
|
||||||
|
"^/api/v[0-9]+",
|
||||||
|
"regex",
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
traefikKey,
|
||||||
|
oldKey,
|
||||||
|
"regex path: Traefik key must match old"
|
||||||
|
);
|
||||||
|
console.log(" PASS: backward compat — regex path");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 12: resource with path rewrite — Traefik name unchanged
|
||||||
|
{
|
||||||
|
const oldKey = oldKeyComputation(
|
||||||
|
10,
|
||||||
|
"/api",
|
||||||
|
"prefix",
|
||||||
|
"/backend",
|
||||||
|
"prefix"
|
||||||
|
);
|
||||||
|
const { traefikKey } = newKeyComputation(
|
||||||
|
10,
|
||||||
|
"/api",
|
||||||
|
"prefix",
|
||||||
|
"/backend",
|
||||||
|
"prefix"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
traefikKey,
|
||||||
|
oldKey,
|
||||||
|
"path rewrite: Traefik key must match old"
|
||||||
|
);
|
||||||
|
console.log(" PASS: backward compat — path rewrite (prefix→prefix)");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 13: resource with stripPrefix rewrite — Traefik name unchanged
|
||||||
|
{
|
||||||
|
const oldKey = oldKeyComputation(
|
||||||
|
7,
|
||||||
|
"/app",
|
||||||
|
"prefix",
|
||||||
|
null,
|
||||||
|
"stripPrefix"
|
||||||
|
);
|
||||||
|
const { traefikKey } = newKeyComputation(
|
||||||
|
7,
|
||||||
|
"/app",
|
||||||
|
"prefix",
|
||||||
|
null,
|
||||||
|
"stripPrefix"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
traefikKey,
|
||||||
|
oldKey,
|
||||||
|
"stripPrefix: Traefik key must match old"
|
||||||
|
);
|
||||||
|
console.log(" PASS: backward compat — stripPrefix rewrite");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 14: root path "/" — Traefik name unchanged
|
||||||
|
{
|
||||||
|
const oldKey = oldKeyComputation(1, "/", "prefix", null, null);
|
||||||
|
const { traefikKey } = newKeyComputation(1, "/", "prefix", null, null);
|
||||||
|
assertEquals(
|
||||||
|
traefikKey,
|
||||||
|
oldKey,
|
||||||
|
"root path: Traefik key must match old"
|
||||||
|
);
|
||||||
|
console.log(" PASS: backward compat — root path /");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 15: full Traefik router/service names unchanged for existing users
|
||||||
|
{
|
||||||
|
const scenarios = [
|
||||||
|
{
|
||||||
|
rid: 1,
|
||||||
|
name: "my-webapp",
|
||||||
|
path: "/api",
|
||||||
|
pmt: "prefix" as const,
|
||||||
|
rp: null,
|
||||||
|
rpt: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rid: 2,
|
||||||
|
name: "backend",
|
||||||
|
path: "/",
|
||||||
|
pmt: "prefix" as const,
|
||||||
|
rp: null,
|
||||||
|
rpt: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rid: 3,
|
||||||
|
name: "docs",
|
||||||
|
path: "/docs",
|
||||||
|
pmt: "prefix" as const,
|
||||||
|
rp: "/",
|
||||||
|
rpt: "stripPrefix" as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rid: 42,
|
||||||
|
name: "api-service",
|
||||||
|
path: null,
|
||||||
|
pmt: null,
|
||||||
|
rp: null,
|
||||||
|
rpt: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rid: 100,
|
||||||
|
name: "grafana",
|
||||||
|
path: "/grafana",
|
||||||
|
pmt: "prefix" as const,
|
||||||
|
rp: null,
|
||||||
|
rpt: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
for (const s of scenarios) {
|
||||||
|
const oldKey = oldKeyComputation(s.rid, s.path, s.pmt, s.rp, s.rpt);
|
||||||
|
const { traefikKey } = newKeyComputation(
|
||||||
|
s.rid,
|
||||||
|
s.path,
|
||||||
|
s.pmt,
|
||||||
|
s.rp,
|
||||||
|
s.rpt
|
||||||
|
);
|
||||||
|
const oldNames = buildTraefikNames(oldKey, s.name);
|
||||||
|
const newNames = buildTraefikNames(traefikKey, s.name);
|
||||||
|
assertEquals(
|
||||||
|
newNames.routerName,
|
||||||
|
oldNames.routerName,
|
||||||
|
`router name mismatch for resource ${s.rid} ${s.name} path=${s.path}`
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
newNames.serviceName,
|
||||||
|
oldNames.serviceName,
|
||||||
|
`service name mismatch for resource ${s.rid} ${s.name} path=${s.path}`
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
newNames.transportName,
|
||||||
|
oldNames.transportName,
|
||||||
|
`transport name mismatch for resource ${s.rid} ${s.name} path=${s.path}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return { internalMapKey, traefikKey };
|
|
||||||
}
|
}
|
||||||
|
console.log(
|
||||||
|
" PASS: backward compat — full router/service/transport names match old code for 5 scenarios"
|
||||||
|
);
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
// /a/b and /a-b should have DIFFERENT internal keys (no collision)
|
// Test 16: large resourceId — Traefik name unchanged
|
||||||
const keysAB = makeKeys(1, "/a/b", "prefix");
|
{
|
||||||
const keysDash = makeKeys(1, "/a-b", "prefix");
|
const oldKey = oldKeyComputation(
|
||||||
|
99999,
|
||||||
|
"/dashboard",
|
||||||
|
"prefix",
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const { traefikKey } = newKeyComputation(
|
||||||
|
99999,
|
||||||
|
"/dashboard",
|
||||||
|
"prefix",
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
traefikKey,
|
||||||
|
oldKey,
|
||||||
|
"large resourceId: Traefik key must match old"
|
||||||
|
);
|
||||||
|
console.log(" PASS: backward compat — large resourceId");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Collision fix: the actual bug we're fixing ───────────────────
|
||||||
|
|
||||||
|
// Test 17: /a/b and /a-b now have different internal keys (THE BUG FIX)
|
||||||
|
{
|
||||||
|
const keysAB = newKeyComputation(1, "/a/b", "prefix", null, null);
|
||||||
|
const keysDash = newKeyComputation(1, "/a-b", "prefix", null, null);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
keysAB.internalMapKey !== keysDash.internalMapKey,
|
keysAB.internalMapKey !== keysDash.internalMapKey,
|
||||||
true,
|
true,
|
||||||
"/a/b and /a-b must have different internal map keys"
|
"/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(
|
console.log(
|
||||||
" PASS: backward compatibility - internal keys differ, Traefik names preserved"
|
" PASS: collision fix — /a/b vs /a-b have different internal keys"
|
||||||
);
|
);
|
||||||
|
passed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("\nAll path encoding tests passed!");
|
// Test 18: demonstrate the old bug — old code maps /a/b and /a-b to same key
|
||||||
|
{
|
||||||
|
const oldKeyAB = oldKeyComputation(1, "/a/b", "prefix", null, null);
|
||||||
|
const oldKeyDash = oldKeyComputation(1, "/a-b", "prefix", null, null);
|
||||||
|
assertEquals(
|
||||||
|
oldKeyAB,
|
||||||
|
oldKeyDash,
|
||||||
|
"old code MUST have this collision (confirms the bug exists)"
|
||||||
|
);
|
||||||
|
console.log(" PASS: confirmed old code bug — /a/b and /a-b collided");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 19: /api/v1 and /api-v1 — old code collision, new code fixes it
|
||||||
|
{
|
||||||
|
const oldKey1 = oldKeyComputation(1, "/api/v1", "prefix", null, null);
|
||||||
|
const oldKey2 = oldKeyComputation(1, "/api-v1", "prefix", null, null);
|
||||||
|
assertEquals(
|
||||||
|
oldKey1,
|
||||||
|
oldKey2,
|
||||||
|
"old code collision for /api/v1 vs /api-v1"
|
||||||
|
);
|
||||||
|
|
||||||
|
const new1 = newKeyComputation(1, "/api/v1", "prefix", null, null);
|
||||||
|
const new2 = newKeyComputation(1, "/api-v1", "prefix", null, null);
|
||||||
|
assertEquals(
|
||||||
|
new1.internalMapKey !== new2.internalMapKey,
|
||||||
|
true,
|
||||||
|
"new code must separate /api/v1 and /api-v1"
|
||||||
|
);
|
||||||
|
console.log(" PASS: collision fix — /api/v1 vs /api-v1");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 20: /app.v2 and /app/v2 and /app-v2 — three-way collision fixed
|
||||||
|
{
|
||||||
|
const a = newKeyComputation(1, "/app.v2", "prefix", null, null);
|
||||||
|
const b = newKeyComputation(1, "/app/v2", "prefix", null, null);
|
||||||
|
const c = newKeyComputation(1, "/app-v2", "prefix", null, null);
|
||||||
|
const keys = new Set([
|
||||||
|
a.internalMapKey,
|
||||||
|
b.internalMapKey,
|
||||||
|
c.internalMapKey
|
||||||
|
]);
|
||||||
|
assertEquals(
|
||||||
|
keys.size,
|
||||||
|
3,
|
||||||
|
"three paths must produce three unique internal keys"
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
" PASS: collision fix — three-way /app.v2, /app/v2, /app-v2"
|
||||||
|
);
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Edge cases ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Test 21: same path in different resources — always separate
|
||||||
|
{
|
||||||
|
const res1 = newKeyComputation(1, "/api", "prefix", null, null);
|
||||||
|
const res2 = newKeyComputation(2, "/api", "prefix", null, null);
|
||||||
|
assertEquals(
|
||||||
|
res1.internalMapKey !== res2.internalMapKey,
|
||||||
|
true,
|
||||||
|
"different resources with same path must have different keys"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
res1.traefikKey !== res2.traefikKey,
|
||||||
|
true,
|
||||||
|
"different resources with same path must have different Traefik keys"
|
||||||
|
);
|
||||||
|
console.log(" PASS: edge case — same path, different resources");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 22: same resource, different pathMatchType — separate keys
|
||||||
|
{
|
||||||
|
const exact = newKeyComputation(1, "/api", "exact", null, null);
|
||||||
|
const prefix = newKeyComputation(1, "/api", "prefix", null, null);
|
||||||
|
assertEquals(
|
||||||
|
exact.internalMapKey !== prefix.internalMapKey,
|
||||||
|
true,
|
||||||
|
"exact vs prefix must have different internal keys"
|
||||||
|
);
|
||||||
|
console.log(" PASS: edge case — same path, different match types");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 23: same resource and path, different rewrite config — separate keys
|
||||||
|
{
|
||||||
|
const noRewrite = newKeyComputation(1, "/api", "prefix", null, null);
|
||||||
|
const withRewrite = newKeyComputation(
|
||||||
|
1,
|
||||||
|
"/api",
|
||||||
|
"prefix",
|
||||||
|
"/backend",
|
||||||
|
"prefix"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
noRewrite.internalMapKey !== withRewrite.internalMapKey,
|
||||||
|
true,
|
||||||
|
"with vs without rewrite must have different internal keys"
|
||||||
|
);
|
||||||
|
console.log(" PASS: edge case — same path, different rewrite config");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 24: paths with special URL characters
|
||||||
|
{
|
||||||
|
const paths = ["/api?foo", "/api#bar", "/api%20baz", "/api+qux"];
|
||||||
|
const internal = new Set(
|
||||||
|
paths.map(
|
||||||
|
(p) =>
|
||||||
|
newKeyComputation(1, p, "prefix", null, null).internalMapKey
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
internal.size,
|
||||||
|
paths.length,
|
||||||
|
"special URL chars must produce unique keys"
|
||||||
|
);
|
||||||
|
console.log(" PASS: edge case — special URL characters in paths");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 25: very long path (sanitize truncates at 50 chars — verify consistency)
|
||||||
|
{
|
||||||
|
const longPath = "/" + "a".repeat(100);
|
||||||
|
const oldKey = oldKeyComputation(1, longPath, "prefix", null, null);
|
||||||
|
const { traefikKey } = newKeyComputation(
|
||||||
|
1,
|
||||||
|
longPath,
|
||||||
|
"prefix",
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
traefikKey,
|
||||||
|
oldKey,
|
||||||
|
"long path: Traefik key must match old (both truncate)"
|
||||||
|
);
|
||||||
|
console.log(" PASS: edge case — very long path (50-char truncation)");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 26: sticky session cookie safety — service name doesn't change
|
||||||
|
{
|
||||||
|
// Sticky sessions use cookie name "p_sticky" tied to the service name.
|
||||||
|
// If service name changes, existing cookies become invalid.
|
||||||
|
const oldKey = oldKeyComputation(1, "/api", "prefix", null, null);
|
||||||
|
const { traefikKey } = newKeyComputation(
|
||||||
|
1,
|
||||||
|
"/api",
|
||||||
|
"prefix",
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const oldServiceName = `${oldKey}-my-app-service`;
|
||||||
|
const newServiceName = `${traefikKey}-my-app-service`;
|
||||||
|
assertEquals(
|
||||||
|
newServiceName,
|
||||||
|
oldServiceName,
|
||||||
|
"service name must not change (would break sticky session cookies)"
|
||||||
|
);
|
||||||
|
console.log(" PASS: sticky session safety — service name preserved");
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nAll ${passed} tests passed!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user