113 lines
3.1 KiB
TypeScript
113 lines
3.1 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { z } from "zod";
|
|
import { registerHousekeepingCommand } from "@/features/housekeeping/foundation/commands/registry";
|
|
import {
|
|
anyCapability,
|
|
ok,
|
|
} from "@/features/housekeeping/foundation/contracts";
|
|
import type { AuditEntry } from "@/lib/services/audit";
|
|
|
|
const { auditEntries, context, rateLimitCalls } = vi.hoisted(() => ({
|
|
auditEntries: [] as AuditEntry[],
|
|
context: {
|
|
actor: { id: 71, username: "server-operator", rank: 4 },
|
|
isSuperAdmin: false,
|
|
has: (slug: string) => slug === "admin.settings.edit",
|
|
hasAny: (...slugs: string[]) => slugs.includes("admin.settings.edit"),
|
|
hasAll: (...slugs: string[]) =>
|
|
slugs.every((slug) => slug === "admin.settings.edit"),
|
|
},
|
|
rateLimitCalls: [] as Array<[string, number, number]>,
|
|
}));
|
|
|
|
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
|
|
getHousekeepingCapabilityContext: async () => context,
|
|
}));
|
|
|
|
vi.mock("@/lib/services/audit", () => ({
|
|
housekeepingAuditWriter: {
|
|
write: async (entry: AuditEntry) => {
|
|
auditEntries.push({ ...entry });
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/lib/rate-limit", () => ({
|
|
clientIp: async () => "203.0.113.7",
|
|
rateLimit: async (key: string, attempts: number, windowMs: number) => {
|
|
rateLimitCalls.push([key, attempts, windowMs]);
|
|
return { ok: true, retryAfter: 0 };
|
|
},
|
|
}));
|
|
|
|
import { executeHousekeepingCommand } from "./housekeeping-command";
|
|
|
|
registerHousekeepingCommand({
|
|
id: "system.server-action.serializable",
|
|
owner: "system",
|
|
risk: "safe",
|
|
capability: anyCapability("admin.settings.edit"),
|
|
input: z.object({ value: z.string() }),
|
|
requiresReason: false,
|
|
rateLimit: { attempts: 5, windowMs: 120_000 },
|
|
execute: async (commandContext, input) =>
|
|
ok(
|
|
{
|
|
value: input.value,
|
|
actorId: commandContext.capability.actor.id,
|
|
ipAddress: commandContext.ipAddress,
|
|
},
|
|
commandContext.correlationId,
|
|
),
|
|
});
|
|
|
|
beforeEach(() => {
|
|
auditEntries.length = 0;
|
|
rateLimitCalls.length = 0;
|
|
});
|
|
|
|
describe("executeHousekeepingCommand", () => {
|
|
it("accepts a plain request and ignores spoofed server-owned metadata", async () => {
|
|
const request = {
|
|
commandId: "system.server-action.serializable",
|
|
input: { value: "saved" },
|
|
risk: "sensitive",
|
|
owner: "people",
|
|
capability: { mode: "any", slugs: ["forged.permission"] },
|
|
actor: { id: 999 },
|
|
ipAddress: "198.51.100.9",
|
|
rateLimit: { attempts: 999, windowMs: 1 },
|
|
audit: { action: "forged.action", target: "forged-target" },
|
|
};
|
|
|
|
const result = await executeHousekeepingCommand(request);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
data: {
|
|
value: "saved",
|
|
actorId: 71,
|
|
ipAddress: "203.0.113.7",
|
|
},
|
|
});
|
|
expect(rateLimitCalls).toEqual([
|
|
[
|
|
"housekeeping-command:71:203.0.113.7:system.server-action.serializable",
|
|
5,
|
|
120_000,
|
|
],
|
|
]);
|
|
expect(auditEntries).toMatchObject([
|
|
{
|
|
userId: 71,
|
|
action: "system.server-action.serializable",
|
|
target: "system",
|
|
domain: "system",
|
|
ipAddress: "203.0.113.7",
|
|
outcome: "success",
|
|
},
|
|
]);
|
|
expect(auditEntries[0]?.correlationId).toBe(result.correlationId);
|
|
});
|
|
});
|