76 lines
4.0 KiB
TypeScript
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);
|
|
});
|