Files
kuber/tests/server/kubernetes-audit-store.test.ts
T
2026-09-27 10:40:50 +00:00

76 lines
4.0 KiB
TypeScript

import { expect, test } from "bun:test";
import type { KubernetesObject } from "@kubernetes/client-node";
import { KubernetesAuditPersistence } from "../../server/kubernetes-state";
import type { AuditEvent } from "../../server/audit-store";
type Obj = KubernetesObject & { data?: Record<string, string> };
class Objects {
values = new Map<string, Obj>();
revision = 0;
deletes = 0;
failReplace = false;
failCreate = false;
key(v: KubernetesObject) { return `${v.kind}:${v.metadata?.name}`; }
async read(v: KubernetesObject) {
const found = this.values.get(this.key(v));
if (!found) throw { code: 404 };
return structuredClone(found);
}
async create(v: Obj) {
if (this.failCreate) throw new Error("write failed");
const key = this.key(v);
if (this.values.has(key)) throw { code: 409 };
const saved = structuredClone(v);
saved.metadata = { ...saved.metadata, resourceVersion: String(++this.revision) };
this.values.set(key, saved);
return saved;
}
async replace(v: Obj) {
if (this.failReplace) throw new Error("write failed");
const key = this.key(v), previous = this.values.get(key);
if (!previous || previous.metadata?.resourceVersion !== v.metadata?.resourceVersion) throw { code: 409 };
const saved = structuredClone(v);
saved.metadata = { ...saved.metadata, resourceVersion: String(++this.revision) };
this.values.set(key, saved);
return saved;
}
async delete(v: KubernetesObject) { this.deletes++; this.values.delete(this.key(v)); }
async list(_api: string, _kind: string, _ns: string, _p?: string, _e?: boolean, _x?: boolean, _f?: string, selector?: string) {
const [label, expected] = selector?.split("=") ?? [];
return { items: [...this.values.values()].filter((v) => !label || v.metadata?.labels?.[label] === expected) };
}
api() { return this as never; }
}
function event(id: number, workspaceId?: string): AuditEvent {
return { apiVersion: "kuber.astrxl.dev/v2", kind: "AuditEvent", metadata: { name: `audit-${id}`, uid: `uid-${id}`, resourceVersion: "1", creationTimestamp: new Date(id * 1000).toISOString() }, spec: { actor: { username: "user" }, action: `action-${id}`, outcome: "success", ...(workspaceId && { workspaceId }) } };
}
test("Kubernetes audit persistence stores one bounded ring and filters workspaces", async () => {
const objects = new Objects(), persistence = new KubernetesAuditPersistence(objects.api());
for (let i = 0; i < 105; i++) await persistence.append(event(i, i % 2 ? "one" : "two"));
expect([...objects.values.values()].filter((v) => v.metadata?.labels?.["kuber.astrxl.dev/type"] === "audit-log")).toHaveLength(1);
expect(await persistence.list()).toHaveLength(100);
expect((await persistence.list("one")).every((v) => v.spec.workspaceId === "one")).toBe(true);
expect((await persistence.list())[0]?.spec.action).toBe("action-5");
});
test("conflicting replicas preserve both appended events", async () => {
const objects = new Objects(), a = new KubernetesAuditPersistence(objects.api()), b = new KubernetesAuditPersistence(objects.api());
await Promise.all(Array.from({ length: 40 }, (_, i) => (i % 2 ? a : b).append(event(i))));
expect((await a.list()).map((v) => v.metadata.uid)).toHaveLength(40);
});
test("legacy records are migrated before deletion and newest 100 survive", async () => {
const objects = new Objects();
for (let i = 0; i < 110; i++) await objects.create({ apiVersion: "v1", kind: "ConfigMap", metadata: { name: `audit-${i}`, namespace: "kuber-system", labels: { "kuber.astrxl.dev/type": "audit" } }, data: { payload: JSON.stringify(event(i)) } });
const persistence = new KubernetesAuditPersistence(objects.api());
objects.failCreate = true;
await expect(persistence.list()).rejects.toThrow("write failed");
expect(objects.deletes).toBe(0);
objects.failCreate = false;
const migrated = await persistence.list();
expect(migrated).toHaveLength(100);
expect(migrated[0]?.spec.action).toBe("action-10");
expect(objects.values.has("ConfigMap:audit-log")).toBe(true);
});