Files
kuber/tests/server/maintenance.test.ts
2026-10-05 19:35:58 +00:00

258 lines
9.2 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
MaintenanceService,
maintenanceMiddleware,
maintenanceRoute,
normalizeMaintenanceHost,
type MaintenancePersistence,
} from "../../server/maintenance";
import { createApp } from "../../server/app";
import { hashToken, MemoryAuthStore } from "../../server/auth";
class MemoryPersistence implements MaintenancePersistence {
state: { hosts: string[] } | undefined;
resources: Record<string, unknown>[] = [];
deleted = 0;
routePresent = false;
failApply = false;
failWrite = false;
async readState() {
return this.state;
}
async writeState(value: { hosts: string[] }) {
if (this.failWrite) throw new Error("state write failed");
this.state = value;
}
async routeExists() {
return this.routePresent;
}
async apply(resource: Record<string, unknown>) {
if (this.failApply) throw new Error("route apply failed");
this.resources.push(resource);
if (resource.kind === "IngressRoute") this.routePresent = true;
}
async deleteRoute() {
this.deleted += 1;
this.routePresent = false;
}
}
const lease = { acquire: async () => ({ release: async () => {} }) };
describe("maintenance override", () => {
test("normalizes DNS hostnames and leading wildcards only", () => {
expect(normalizeMaintenanceHost(" Sub.Domain.COM. ")).toBe(
"sub.domain.com",
);
expect(normalizeMaintenanceHost(" *.EXAMPLE.COM. ")).toBe("*.example.com");
expect(normalizeMaintenanceHost(" *.Sub.Example.COM. ")).toBe("*.sub.example.com");
for (const host of [
"http://example.com",
"example.com:443",
"127.0.0.1",
"[::1]",
"*",
"*.",
"example.*.com",
"a.*.example.com",
"**.example.com",
"*.*.example.com",
"*.example.com:443",
"*.127.0.0.1",
"example",
"a..com",
])
expect(() => normalizeMaintenanceHost(host)).toThrow();
});
test("enables, deduplicates, and disables the last host", async () => {
const persistence = new MemoryPersistence();
const service = new MaintenanceService(persistence, lease);
expect((await service.status("a.example.com")).enabled).toBe(false);
expect((await service.toggle("A.example.com")).hosts).toEqual([
"a.example.com",
]);
persistence.state = {
hosts: ["a.example.com", "b.example.com", "A.example.com"],
};
expect((await service.toggle("a.example.com")).hosts).toEqual([
"b.example.com",
]);
expect((await service.toggle("b.example.com")).hosts).toEqual([]);
expect(persistence.deleted).toBe(2);
expect(persistence.state).toEqual({ hosts: [] });
});
test("persists normalized wildcards and renders them as Host rules", async () => {
const persistence = new MemoryPersistence();
const service = new MaintenanceService(persistence, lease);
expect(await service.set(" *.EXAMPLE.COM. ", true)).toMatchObject({
host: "*.example.com",
enabled: true,
hosts: ["*.example.com"],
});
expect(persistence.state).toEqual({ hosts: ["*.example.com"] });
expect(persistence.resources.at(-1)).toMatchObject({
spec: { routes: [{ match: "Host(`*.example.com`)" }] },
});
expect(await service.status("*.EXAMPLE.COM.")).toMatchObject({
host: "*.example.com",
enabled: true,
});
expect(await service.set("*.example.com", false)).toMatchObject({
enabled: false,
hosts: [],
});
});
test("renders the single shared error route and rewrite middleware", () => {
expect(maintenanceMiddleware()).toMatchObject({
metadata: { name: "maintenance-override", namespace: "routing" },
spec: {
replacePathRegex: { regex: "^/.*", replacement: "/__error/1001" },
},
});
expect(maintenanceRoute(["a.example.com", "b.example.com"])).toMatchObject({
metadata: { name: "maintenance-override", namespace: "routing" },
spec: {
routes: [
{
match: "Host(`a.example.com`) || Host(`b.example.com`)",
priority: 1_000_000,
services: [
{
name: "error-page",
namespace: "routing",
port: 3000,
scheme: "http",
},
],
},
],
},
});
expect(maintenanceRoute(["*.example.com", "a.example.com"])).toMatchObject({
spec: { routes: [{ match: "Host(`*.example.com`) || Host(`a.example.com`)" }] },
});
});
test("maps an unavailable global lease to a retryable API conflict", async () => {
const persistence = new MemoryPersistence();
const store = new MemoryAuthStore();
await store.putUser({ username: "operator", passwordHash: "hash", roles: ["operator"] });
await store.putSession({ tokenHash: hashToken("operator-token"), username: "operator", roles: ["operator"], expiresAt: "2099-01-01T00:00:00.000Z" });
const app = createApp({
store,
maintenance: new MaintenanceService(persistence, { acquire: async () => undefined }),
});
const result = await app(new Request("https://kuber.astrxl.dev/api/v2/maintenance/a.example.com", {
method: "POST",
headers: { authorization: "Bearer operator-token", "content-type": "application/json" },
body: JSON.stringify({ enabled: true }),
}));
expect(result.status).toBe(409);
expect(result.headers.get("retry-after")).toBe("1");
expect(await result.json()).toMatchObject({ code: "MAINTENANCE_BUSY" });
expect(persistence.state).toBeUndefined();
});
test("recovers the persisted desired state after route or state failures", async () => {
const persistence = new MemoryPersistence();
const service = new MaintenanceService(persistence, lease);
persistence.failApply = true;
await expect(service.set("a.example.com", true)).rejects.toThrow("route apply failed");
expect(persistence.state).toBeUndefined();
persistence.failApply = false;
expect(await service.status("a.example.com")).toMatchObject({ enabled: false });
persistence.failWrite = true;
await expect(service.set("a.example.com", true)).rejects.toThrow("state write failed");
expect(persistence.routePresent).toBe(true);
persistence.failWrite = false;
expect(await service.status("a.example.com")).toMatchObject({ enabled: false });
expect(persistence.routePresent).toBe(false);
expect(await service.set("a.example.com", true)).toMatchObject({ enabled: true });
persistence.routePresent = false;
expect(await service.status("a.example.com")).toMatchObject({ enabled: true });
expect(persistence.routePresent).toBe(true);
});
test("requires kubernetes write without workspace or trust context", async () => {
const store = new MemoryAuthStore();
await store.putUser({
username: "viewer",
passwordHash: "hash",
roles: ["viewer"],
});
await store.putUser({
username: "operator",
passwordHash: "hash",
roles: ["operator"],
});
for (const token of ["viewer-token", "operator-token"])
await store.putSession({
tokenHash: hashToken(token),
username: token.startsWith("viewer") ? "viewer" : "operator",
roles: token.startsWith("viewer") ? ["viewer"] : ["operator"],
expiresAt: "2099-01-01T00:00:00.000Z",
});
await store.createApiKey({
id: "maintenance-scoped-key",
tokenHash: hashToken("scoped-key"),
username: "operator",
capabilities: ["kubernetes:write"],
workspace: "shop",
expiresAt: "2099-01-01T00:00:00.000Z",
});
await store.createApiKey({
id: "maintenance-global-key",
tokenHash: hashToken("global-key"),
username: "operator",
capabilities: ["kubernetes:write"],
expiresAt: "2099-01-01T00:00:00.000Z",
});
const persistence = new MemoryPersistence();
const app = createApp({
store,
maintenance: new MaintenanceService(persistence, lease),
});
const viewer = await app(
new Request("https://kuber.astrxl.dev/api/v2/maintenance/a.example.com", {
headers: { authorization: "Bearer viewer-token" },
}),
);
expect(viewer.status).toBe(403);
const scoped = await app(
new Request("https://kuber.astrxl.dev/api/v2/maintenance/a.example.com", {
headers: { authorization: "Bearer scoped-key" },
}),
);
expect(scoped.status).toBe(403);
expect(await scoped.json()).toMatchObject({ code: "MAINTENANCE_GLOBAL_SCOPE_REQUIRED" });
const global = await app(
new Request("https://kuber.astrxl.dev/api/v2/maintenance/a.example.com", {
headers: { authorization: "Bearer global-key" },
}),
);
expect(global.status).toBe(200);
const status = await app(
new Request("https://kuber.astrxl.dev/api/v2/maintenance/a.example.com", {
headers: { authorization: "Bearer operator-token" },
}),
);
expect(await status.json()).toMatchObject({ enabled: false });
const toggle = await app(
new Request("https://kuber.astrxl.dev/api/v2/maintenance/a.example.com", {
method: "POST",
headers: {
authorization: "Bearer operator-token",
"content-type": "application/json",
},
body: JSON.stringify({ enabled: true }),
}),
);
expect(await toggle.json()).toMatchObject({ enabled: true });
});
});