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(); 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); }