Files
homelab/edge-cache/controller.test.mjs
T

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