Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/services/admin/admin-action-audit.server.test.ts
T

114 lines
3.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
ADMIN_REASON_MIN_LENGTH,
AdminActionInputError,
assertAdminActionReason,
assertConfirmPhrase,
buildAdminActionAuditInput,
confirmPhraseMatches,
validateAdminActionReason,
} from "./admin-action-audit.server";
describe("validateAdminActionReason", () => {
it("rejects an empty or whitespace-only reason", () => {
expect(validateAdminActionReason(undefined)).toEqual({ ok: false, code: "REASON_REQUIRED" });
expect(validateAdminActionReason(" ")).toEqual({ ok: false, code: "REASON_REQUIRED" });
});
it("rejects a reason below the minimum length", () => {
const short = "a".repeat(ADMIN_REASON_MIN_LENGTH - 1);
expect(validateAdminActionReason(short)).toEqual({ ok: false, code: "REASON_TOO_SHORT" });
});
it("accepts and trims a meaningful reason", () => {
expect(validateAdminActionReason(" Tenant auf Wunsch deaktiviert ")).toEqual({
ok: true,
value: "Tenant auf Wunsch deaktiviert",
});
});
});
describe("assertAdminActionReason", () => {
it("throws a catalog error carrying an i18n key", () => {
try {
assertAdminActionReason("");
throw new Error("expected throw");
} catch (error) {
expect(error).toBeInstanceOf(AdminActionInputError);
expect((error as AdminActionInputError).code).toBe("REASON_REQUIRED");
expect((error as AdminActionInputError).i18nKey).toBe("admin.audit.errors.reasonRequired");
}
});
it("returns the trimmed reason when valid", () => {
expect(assertAdminActionReason(" valid reason ")).toBe("valid reason");
});
});
describe("confirmPhraseMatches", () => {
it("matches only the exact trimmed phrase", () => {
expect(confirmPhraseMatches("acme", "acme")).toBe(true);
expect(confirmPhraseMatches("acme", " acme ")).toBe(true);
expect(confirmPhraseMatches("acme", "Acme")).toBe(false);
expect(confirmPhraseMatches("acme", "acm")).toBe(false);
});
it("never matches an empty expected phrase", () => {
expect(confirmPhraseMatches("", "")).toBe(false);
expect(confirmPhraseMatches(" ", "")).toBe(false);
});
it("assertConfirmPhrase throws on mismatch", () => {
expect(() => assertConfirmPhrase("acme", "nope")).toThrowError(AdminActionInputError);
expect(() => assertConfirmPhrase("acme", "acme")).not.toThrow();
});
});
describe("buildAdminActionAuditInput", () => {
it("always logs cross-tenant as an admin actor with the reason in metadata", () => {
const input = buildAdminActionAuditInput({
action: "tenant.statusChanged",
actorUserId: "admin-1",
actorEmail: "ops@hivegrid.de",
targetKind: "tenant",
targetId: "tenant-9",
reason: "Zahlungsverzug",
before: { active: true },
after: { active: false },
});
expect(input.tenantId).toBeNull();
expect(input.actorType).toBe("admin");
expect(input.actorId).toBe("admin-1");
expect(input.action).toBe("admin.tenant.statusChanged");
expect(input.targetType).toBe("tenant");
expect(input.targetId).toBe("tenant-9");
expect(input.sourceModule).toBe("admin");
expect(input.outcome).toBe("success");
expect(input.metadata).toMatchObject({
reason: "Zahlungsverzug",
before: { active: true },
after: { active: false },
});
});
it("extracts client ip + user-agent from a request when present", () => {
const request = new Request("https://admin.hivegrid.de/tenants/9", {
headers: {
"cf-connecting-ip": "203.0.113.7",
"x-forwarded-for": "198.51.100.250, 173.245.48.10",
"user-agent": "ops-browser/1.0",
},
});
const input = buildAdminActionAuditInput({
action: "impersonation.started",
actorUserId: "admin-1",
targetKind: "user",
targetId: "user-4",
reason: "Support-Ticket #42",
request,
});
expect(input.actorIp).toBe("203.0.113.7");
expect(input.actorUserAgent).toBe("ops-browser/1.0");
});
});