114 lines
3.9 KiB
TypeScript
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");
|
|
});
|
|
});
|