245 lines
6.8 KiB
TypeScript
245 lines
6.8 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { z } from "zod";
|
|
import { registerHousekeepingCommand } from "@/features/housekeeping/foundation/commands/registry";
|
|
|
|
vi.mock(
|
|
"@/features/housekeeping/foundation/commands/registry",
|
|
async (importOriginal) => ({
|
|
...(await importOriginal<
|
|
typeof import("@/features/housekeeping/foundation/commands/registry")
|
|
>()),
|
|
sealHousekeepingCommandRegistry: sealRegistryMock,
|
|
}),
|
|
);
|
|
|
|
vi.mock("@/features/housekeeping/commands", () => ({
|
|
housekeepingCommandRegistryReady: true,
|
|
}));
|
|
|
|
import {
|
|
anyCapability,
|
|
ok,
|
|
} from "@/features/housekeeping/foundation/contracts";
|
|
import type { AuditEntry } from "@/lib/services/audit";
|
|
|
|
const {
|
|
auditEntries,
|
|
auditWriteMock,
|
|
commandExecutions,
|
|
context,
|
|
getContextMock,
|
|
getIpMock,
|
|
rateLimitCalls,
|
|
sealRegistryMock,
|
|
} = vi.hoisted(() => ({
|
|
auditEntries: [] as AuditEntry[],
|
|
auditWriteMock: vi.fn(),
|
|
commandExecutions: [] as string[],
|
|
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"),
|
|
},
|
|
getContextMock: vi.fn(),
|
|
getIpMock: vi.fn(),
|
|
rateLimitCalls: [] as Array<[string, number, number]>,
|
|
sealRegistryMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
|
|
getHousekeepingCapabilityContext: getContextMock,
|
|
}));
|
|
|
|
vi.mock("@/lib/services/audit", () => ({
|
|
housekeepingAuditWriter: { write: auditWriteMock },
|
|
}));
|
|
|
|
vi.mock("@/lib/rate-limit", () => ({
|
|
clientIp: getIpMock,
|
|
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) => {
|
|
commandExecutions.push(input.value);
|
|
return ok(
|
|
{
|
|
value: input.value,
|
|
actorId: commandContext.capability.actor.id,
|
|
ipAddress: commandContext.ipAddress,
|
|
},
|
|
commandContext.correlationId,
|
|
input.value === "partial"
|
|
? {
|
|
status: "partial",
|
|
external: "failed",
|
|
audit: "persisted",
|
|
}
|
|
: undefined,
|
|
);
|
|
},
|
|
});
|
|
|
|
beforeEach(() => {
|
|
auditEntries.length = 0;
|
|
commandExecutions.length = 0;
|
|
rateLimitCalls.length = 0;
|
|
getContextMock.mockReset().mockResolvedValue(context);
|
|
getIpMock.mockReset().mockResolvedValue("203.0.113.7");
|
|
sealRegistryMock.mockReset();
|
|
auditWriteMock.mockReset().mockImplementation(async (entry: AuditEntry) => {
|
|
auditEntries.push({ ...entry });
|
|
});
|
|
});
|
|
|
|
describe("executeHousekeepingCommand", () => {
|
|
it("accepts a plain request and derives all policy metadata server-side", async () => {
|
|
const result = await executeHousekeepingCommand({
|
|
commandId: "system.server-action.serializable",
|
|
input: { value: "saved" },
|
|
});
|
|
|
|
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);
|
|
expect(sealRegistryMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("preserves one returned partial completion and its correlation through the real action dispatcher", async () => {
|
|
const result = await executeHousekeepingCommand({
|
|
commandId: "system.server-action.serializable",
|
|
input: { value: "partial" },
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
data: { value: "partial" },
|
|
completion: {
|
|
status: "partial",
|
|
external: "failed",
|
|
audit: "persisted",
|
|
},
|
|
});
|
|
expect(auditEntries).toHaveLength(1);
|
|
expect(auditEntries[0]).toMatchObject({
|
|
outcome: "partial",
|
|
correlationId: result.correlationId,
|
|
});
|
|
expect(() => JSON.stringify(result)).not.toThrow();
|
|
});
|
|
it("strictly rejects spoofed server-owned metadata before execution", async () => {
|
|
const result = await executeHousekeepingCommand({
|
|
commandId: "system.server-action.serializable",
|
|
input: { value: "forged" },
|
|
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" },
|
|
} as never);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { code: "VALIDATION" },
|
|
});
|
|
expect(commandExecutions).toEqual([]);
|
|
expect(rateLimitCalls).toEqual([]);
|
|
expect(auditEntries).toMatchObject([
|
|
{
|
|
userId: 71,
|
|
action: "housekeeping.command.dispatch",
|
|
target: "request-envelope",
|
|
ipAddress: "203.0.113.7",
|
|
outcome: "denied",
|
|
},
|
|
]);
|
|
expect(JSON.stringify(auditEntries)).not.toContain("forged");
|
|
});
|
|
|
|
it("sanitizes server context acquisition failures into typed results", async () => {
|
|
getContextMock.mockRejectedValue(
|
|
new Error("session database secret exposed"),
|
|
);
|
|
|
|
const result = await executeHousekeepingCommand({
|
|
commandId: "system.server-action.serializable",
|
|
input: { value: "blocked" },
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { code: "INTERNAL", messageKey: "errors.housekeeping.internal" },
|
|
});
|
|
expect(JSON.stringify(result)).not.toContain("secret exposed");
|
|
expect(commandExecutions).toEqual([]);
|
|
});
|
|
|
|
it("returns one truthful partial completion when outcome audit persistence fails", async () => {
|
|
auditWriteMock.mockImplementation(async (entry: AuditEntry) => {
|
|
if (entry.outcome === "success") {
|
|
throw new Error("success audit unavailable");
|
|
}
|
|
auditEntries.push({ ...entry });
|
|
});
|
|
|
|
const result = await executeHousekeepingCommand({
|
|
commandId: "system.server-action.serializable",
|
|
input: { value: "changed" },
|
|
});
|
|
|
|
expect(commandExecutions).toEqual(["changed"]);
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
data: { value: "changed" },
|
|
completion: {
|
|
status: "partial",
|
|
external: "not-required",
|
|
audit: "persisted",
|
|
},
|
|
});
|
|
expect(auditEntries.map((entry) => entry.outcome)).toEqual(["partial"]);
|
|
expect(auditEntries[0]?.correlationId).toBe(result.correlationId);
|
|
expect(() => JSON.stringify(result)).not.toThrow();
|
|
});
|
|
});
|