116 lines
5.7 KiB
JavaScript
116 lines
5.7 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {buildConfiguration, cloudflareRuleFromSpec, definitionsFromLabels, syncCloudflareRule} from "./controller.mjs";
|
|
|
|
const labels = {
|
|
"com.docker.compose.project": "authentik",
|
|
"com.docker.compose.service": "authentik",
|
|
"traefik.enable": "true",
|
|
"traefik.http.routers.authentik.rule": "Host(`auth.xiteng.site`)",
|
|
"traefik.http.routers.authentik.entrypoints": "websecure",
|
|
"traefik.http.routers.authentik.service": "authentik",
|
|
"traefik.http.routers.authentik.middlewares": "force-https-headers",
|
|
"traefik.http.routers.authentik.tls": "true",
|
|
"traefik.http.routers.authentik.tls.certresolver": "cfresolver",
|
|
"traefik.http.routers.authentik-http.rule": "Host(`auth.xiteng.site`)",
|
|
"traefik.http.routers.authentik-http.entrypoints": "web",
|
|
"traefik.http.routers.authentik-http.service": "authentik",
|
|
"xiteng.site.cache.frontend.enabled": "true",
|
|
"xiteng.site.cache.frontend.routers": "authentik,authentik-http",
|
|
"xiteng.site.cache.frontend.paths": "/static/dist/,/assets/",
|
|
"xiteng.site.cache.frontend.edge-ttl": "604800",
|
|
"xiteng.site.cache.frontend.browser-ttl": "3600"
|
|
};
|
|
|
|
function reverseObjectKeys(value) {
|
|
if (Array.isArray(value)) {
|
|
return value.map(reverseObjectKeys);
|
|
}
|
|
if (value && typeof value === "object") {
|
|
return Object.fromEntries(
|
|
Object.entries(value).reverse().map(([key, nested]) => [key, reverseObjectKeys(nested)])
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
test("parses cache definitions without exposing unrelated labels", () => {
|
|
const definitions = definitionsFromLabels(labels);
|
|
assert.equal(definitions.size, 1);
|
|
assert.equal(definitions.get("frontend").paths, "/static/dist/,/assets/");
|
|
assert.equal(definitions.get("frontend").routers, "authentik,authentik-http");
|
|
});
|
|
|
|
test("builds higher-priority GET/HEAD routers and preserves base middleware", () => {
|
|
const built = buildConfiguration([{Id: "one", Labels: labels}], {zoneName: "xiteng.site"});
|
|
assert.deepEqual(built.errors, []);
|
|
assert.equal(built.policyCount, 1);
|
|
assert.equal(built.routerCount, 2);
|
|
const generated = Object.values(built.traefik.http.routers);
|
|
assert.ok(generated.every((router) => router.rule.includes("Method(`GET`)")));
|
|
assert.ok(generated.every((router) => router.rule.includes("PathPrefix(`/static/dist/`)")));
|
|
assert.ok(generated.some((router) => router.middlewares.includes("force-https-headers@docker")));
|
|
assert.ok(generated.every((router) => router.middlewares[0].startsWith("xiteng-static-")));
|
|
assert.ok(generated.some((router) => router.tls?.certResolver === "cfresolver"));
|
|
const middleware = Object.values(built.traefik.http.middlewares)[0];
|
|
assert.equal(middleware.headers.customResponseHeaders["Cache-Control"], "public, max-age=3600, stale-while-revalidate=86400");
|
|
assert.match(middleware.headers.customResponseHeaders["Cloudflare-CDN-Cache-Control"], /s-maxage=604800/);
|
|
});
|
|
|
|
test("creates one Cloudflare expression from exact hosts and safe path prefixes", () => {
|
|
const built = buildConfiguration([{Id: "one", Labels: labels}], {zoneName: "xiteng.site"});
|
|
assert.match(built.cloudflare.expression, /http\.host eq "auth\.xiteng\.site"/);
|
|
assert.match(built.cloudflare.expression, /starts_with\(http\.request\.uri\.path, "\/assets\/"\)/);
|
|
const rule = cloudflareRuleFromSpec(built.cloudflare);
|
|
assert.equal(rule.action_parameters.cache, true);
|
|
assert.equal(rule.action_parameters.edge_ttl.status_code_ttl[1].value, 0);
|
|
assert.equal(rule.enabled, true);
|
|
});
|
|
|
|
test("does not update a semantically identical Cloudflare rule when API reorders fields", async () => {
|
|
const built = buildConfiguration([{Id: "one", Labels: labels}], {zoneName: "xiteng.site"});
|
|
const current = {id: "managed-rule", ...reverseObjectKeys(cloudflareRuleFromSpec(built.cloudflare))};
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () => new Response(JSON.stringify({
|
|
success: true,
|
|
result: {id: "cache-ruleset", rules: [current]}
|
|
}), {status: 200, headers: {"Content-Type": "application/json"}});
|
|
try {
|
|
const result = await syncCloudflareRule(built.cloudflare, {zoneId: "zone", token: "token", dryRun: true});
|
|
assert.deepEqual(result, {changed: false, action: "unchanged", ruleId: "managed-rule"});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("does not override browser Cache-Control unless browser-ttl is explicit", () => {
|
|
const edgeOnly = {...labels};
|
|
delete edgeOnly["xiteng.site.cache.frontend.browser-ttl"];
|
|
const built = buildConfiguration([{Id: "edge-only", Labels: edgeOnly}], {zoneName: "xiteng.site"});
|
|
const headers = Object.values(built.traefik.http.middlewares)[0].headers.customResponseHeaders;
|
|
assert.equal(headers["Cache-Control"], undefined);
|
|
assert.match(headers["Cloudflare-CDN-Cache-Control"], /s-maxage=604800/);
|
|
});
|
|
|
|
test("rejects root and traversal-like cache paths", () => {
|
|
for (const paths of ["/", "/static/../admin/", "/static//nested/"]) {
|
|
const broken = {...labels, "xiteng.site.cache.frontend.paths": paths};
|
|
const built = buildConfiguration([{Id: paths, Labels: broken}], {zoneName: "xiteng.site"});
|
|
assert.equal(built.policyCount, 0);
|
|
assert.equal(built.errors.length, 1);
|
|
assert.equal(Object.keys(built.traefik.http.routers).length, 0);
|
|
assert.equal(Object.keys(built.traefik.http.middlewares).length, 0);
|
|
}
|
|
});
|
|
|
|
test("does not let labels cache a hostname outside the managed zone", () => {
|
|
const broken = {
|
|
...labels,
|
|
"traefik.http.routers.authentik.rule": "Host(`example.com`)",
|
|
"traefik.http.routers.authentik-http.rule": "Host(`example.com`)"
|
|
};
|
|
const built = buildConfiguration([{Id: "external", Labels: broken}], {zoneName: "xiteng.site"});
|
|
assert.equal(built.policyCount, 0);
|
|
assert.match(built.errors[0], /outside xiteng\.site/);
|
|
});
|