Gehashte Tokens mit Praefix, Rate-Limit je IP und Token, zod-Validierung mit durchgaengig optionalen Feldern, idempotente Reports ueber reportId, Key- und Server-ID-Historie sowie automatischer Laufzeit-Vorschlag. 32 Tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
76 lines
3.1 KiB
TypeScript
76 lines
3.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { RateLimit } from "@/lib/ratelimit";
|
|
import { erzeugeToken, hashGleich, hashToken, tokenAusHeader } from "@/lib/tokens";
|
|
import { datumAusProxmox, statusAusProxmox } from "@/server/agent/schema";
|
|
|
|
describe("Tokens", () => {
|
|
it("erzeugt eindeutige Tokens mit erkennbarem Präfix", () => {
|
|
const a = erzeugeToken("host");
|
|
const b = erzeugeToken("host");
|
|
|
|
expect(a.klartext.startsWith("plx_h_")).toBe(true);
|
|
expect(erzeugeToken("enrollment").klartext.startsWith("plx_e_")).toBe(true);
|
|
expect(a.klartext).not.toBe(b.klartext);
|
|
expect(a.praefix.length).toBe("plx_h_".length + 6);
|
|
});
|
|
|
|
it("hasht deterministisch und vergleicht ohne Zeitunterschiede", () => {
|
|
const token = erzeugeToken("host");
|
|
expect(hashToken(token.klartext)).toBe(token.hash);
|
|
expect(hashToken(` ${token.klartext} `)).toBe(token.hash);
|
|
expect(hashGleich(token.hash, token.hash)).toBe(true);
|
|
expect(hashGleich(token.hash, hashToken("etwas anderes"))).toBe(false);
|
|
});
|
|
|
|
it("liest den Bearer-Token aus dem Header", () => {
|
|
expect(tokenAusHeader("Bearer plx_h_abc")).toBe("plx_h_abc");
|
|
expect(tokenAusHeader("bearer plx_h_abc ")).toBe("plx_h_abc");
|
|
expect(tokenAusHeader("Basic abc")).toBeNull();
|
|
expect(tokenAusHeader(null)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("Datums- und Statusumwandlung", () => {
|
|
it("versteht die Formate von Proxmox", () => {
|
|
expect(datumAusProxmox("2026-09-24")?.toISOString()).toBe("2026-09-24T00:00:00.000Z");
|
|
expect(datumAusProxmox(1789610400)?.toISOString()).toBe("2026-09-17T02:00:00.000Z");
|
|
expect(datumAusProxmox("1789610400")?.toISOString()).toBe("2026-09-17T02:00:00.000Z");
|
|
expect(datumAusProxmox("2026-09-17T03:41:00Z")?.toISOString()).toBe("2026-09-17T03:41:00.000Z");
|
|
});
|
|
|
|
it("liefert null statt eines Fehlers bei unbrauchbaren Werten", () => {
|
|
expect(datumAusProxmox(null)).toBeNull();
|
|
expect(datumAusProxmox("")).toBeNull();
|
|
expect(datumAusProxmox("kein datum")).toBeNull();
|
|
expect(datumAusProxmox(undefined)).toBeNull();
|
|
});
|
|
|
|
it("bildet Proxmox-Status ab und fällt sonst auf UNKNOWN zurück", () => {
|
|
expect(statusAusProxmox("active")).toBe("ACTIVE");
|
|
expect(statusAusProxmox("NotFound")).toBe("NOTFOUND");
|
|
expect(statusAusProxmox("expired")).toBe("EXPIRED");
|
|
expect(statusAusProxmox("etwas neues")).toBe("UNKNOWN");
|
|
expect(statusAusProxmox(null)).toBe("UNKNOWN");
|
|
});
|
|
});
|
|
|
|
describe("Rate-Limit", () => {
|
|
it("lässt die Kapazität zu und bremst danach", () => {
|
|
const limit = new RateLimit(3, 1);
|
|
const jetzt = Date.now();
|
|
|
|
expect(limit.pruefe("a", jetzt).erlaubt).toBe(true);
|
|
expect(limit.pruefe("a", jetzt).erlaubt).toBe(true);
|
|
expect(limit.pruefe("a", jetzt).erlaubt).toBe(true);
|
|
|
|
const abgelehnt = limit.pruefe("a", jetzt);
|
|
expect(abgelehnt.erlaubt).toBe(false);
|
|
expect(abgelehnt.wiederholenNach).toBeGreaterThan(0);
|
|
|
|
// Anderer Schlüssel hat einen eigenen Eimer.
|
|
expect(limit.pruefe("b", jetzt).erlaubt).toBe(true);
|
|
// Nach einer Sekunde ist wieder ein Token verfügbar.
|
|
expect(limit.pruefe("a", jetzt + 1000).erlaubt).toBe(true);
|
|
});
|
|
});
|