fix(housekeeping): harden people account workflows
This commit is contained in:
1 parent
e1b31ff773
commit
25b76437ff
41 files changed
+2657
-838
No files matched your search
@@ -0,0 +1,143 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { PERMS } from "@/lib/permission-slugs";
|
||||
import type { AuditEntry } from "@/lib/services/audit";
|
||||
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
|
||||
|
||||
const { alertUser, audit, resolveServerContext, selectLimit } = vi.hoisted(
|
||||
() => ({
|
||||
alertUser: vi.fn(),
|
||||
audit: vi.fn(),
|
||||
resolveServerContext: vi.fn(),
|
||||
selectLimit: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
|
||||
getHousekeepingCapabilityContext: resolveServerContext,
|
||||
}));
|
||||
vi.mock("@/lib/auth", () => ({ invalidateLoginCache: vi.fn() }));
|
||||
vi.mock("@/lib/auth/password", () => ({ hashPassword: vi.fn() }));
|
||||
vi.mock("@/lib/db", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/db")>();
|
||||
return {
|
||||
...actual,
|
||||
db: {
|
||||
...actual.db,
|
||||
select: vi.fn(() => ({
|
||||
from: vi.fn(() => ({
|
||||
where: vi.fn(() => ({ limit: selectLimit })),
|
||||
})),
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("@/lib/services/audit", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/lib/services/audit")>()),
|
||||
logAudit: audit,
|
||||
}));
|
||||
vi.mock("@/lib/services/rcon", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/lib/services/rcon")>()),
|
||||
rcon: { alertUser },
|
||||
}));
|
||||
vi.mock("@/lib/services/webhook", () => ({ notify: vi.fn() }));
|
||||
|
||||
import { peopleMutationService } from "./mutations";
|
||||
|
||||
function context(): HousekeepingCapabilityContext {
|
||||
return {
|
||||
actor: { id: 42, username: "operator", rank: 6 },
|
||||
isSuperAdmin: false,
|
||||
has: (slug) => slug === PERMS.USERS_EDIT,
|
||||
hasAny: (...slugs) => slugs.includes(PERMS.USERS_EDIT),
|
||||
hasAll: (...slugs) => slugs.every((slug) => slug === PERMS.USERS_EDIT),
|
||||
};
|
||||
}
|
||||
|
||||
const invocation = {
|
||||
correlationId: "audit-contract",
|
||||
expectedActorId: 42,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resolveServerContext.mockResolvedValue(context());
|
||||
selectLimit.mockResolvedValue([
|
||||
{
|
||||
id: 7,
|
||||
username: "Alice",
|
||||
rank: 2,
|
||||
motto: "Ready",
|
||||
credits: 100,
|
||||
pixels: 50,
|
||||
online: "1",
|
||||
},
|
||||
]);
|
||||
alertUser.mockResolvedValue(true);
|
||||
audit.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
describe("People external audit contract", () => {
|
||||
it("blocks the external operation when intent persistence fails", async () => {
|
||||
audit.mockRejectedValueOnce(new Error("intent unavailable"));
|
||||
|
||||
const result = await peopleMutationService.execute(
|
||||
invocation,
|
||||
"user.alert",
|
||||
{ userId: 7, message: "Hello" },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({ ok: false });
|
||||
expect(alertUser).not.toHaveBeenCalled();
|
||||
expect(audit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ outcome: "intent" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("persists a correlated failure outcome when the external operation fails", async () => {
|
||||
alertUser.mockResolvedValue(false);
|
||||
|
||||
const result = await peopleMutationService.execute(
|
||||
invocation,
|
||||
"user.alert",
|
||||
{ userId: 7, message: "Hello" },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "DEPENDENCY_UNAVAILABLE" },
|
||||
});
|
||||
expect(
|
||||
audit.mock.calls.map((call) => {
|
||||
const entry = call[0] as AuditEntry;
|
||||
return {
|
||||
outcome: entry.outcome,
|
||||
correlationId: entry.correlationId,
|
||||
};
|
||||
}),
|
||||
).toEqual([
|
||||
{ outcome: "intent", correlationId: "audit-contract" },
|
||||
{ outcome: "failure", correlationId: "audit-contract" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("throws typed completed-operation evidence when final outcome persistence fails", async () => {
|
||||
audit.mockImplementation(async (entry: AuditEntry) => {
|
||||
if (entry.outcome === "success") throw new Error("outcome unavailable");
|
||||
});
|
||||
|
||||
await expect(
|
||||
peopleMutationService.execute(invocation, "user.alert", {
|
||||
userId: 7,
|
||||
message: "Hello",
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
name: "AuditOutcomePersistenceError",
|
||||
operationCompleted: true,
|
||||
operationResult: {
|
||||
before: expect.objectContaining({ id: 7 }),
|
||||
after: expect.objectContaining({ alertDelivered: true }),
|
||||
},
|
||||
});
|
||||
expect(alertUser).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user