feat(housekeeping): unify complete rebuild in gated preview
This commit is contained in:
1 parent
4ad007d241
commit
43470ebf82
402 files changed
+78313
-1663
No files matched your search
@@ -0,0 +1,225 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { PERMS } from "@/lib/permission-slugs";
|
||||
import { dispatchHousekeepingCommand } from "../../../foundation/commands/dispatcher";
|
||||
import {
|
||||
type HousekeepingCommand,
|
||||
registerHousekeepingCommand,
|
||||
} from "../../../foundation/commands/registry";
|
||||
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
|
||||
|
||||
const doubles = vi.hoisted(() => ({
|
||||
createEmulatorRank: vi.fn(),
|
||||
dbExecute: vi.fn(),
|
||||
dbInsert: vi.fn(),
|
||||
dbSelect: vi.fn(),
|
||||
dbTransaction: vi.fn(),
|
||||
dbUpdate: vi.fn(),
|
||||
deleteEmulatorRank: vi.fn(),
|
||||
logStaffActivity: vi.fn(),
|
||||
rconSend: vi.fn(),
|
||||
rconSetRank: vi.fn(),
|
||||
updateEmulatorRank: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/db", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/db")>();
|
||||
return {
|
||||
...actual,
|
||||
db: {
|
||||
...actual.db,
|
||||
execute: doubles.dbExecute,
|
||||
insert: doubles.dbInsert,
|
||||
select: doubles.dbSelect,
|
||||
transaction: doubles.dbTransaction,
|
||||
update: doubles.dbUpdate,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/lib/services/permission-ranks", () => ({
|
||||
createEmulatorRank: doubles.createEmulatorRank,
|
||||
deleteEmulatorRank: doubles.deleteEmulatorRank,
|
||||
updateEmulatorRank: doubles.updateEmulatorRank,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/services/rcon", () => ({
|
||||
rcon: { send: doubles.rconSend, setRank: doubles.rconSetRank },
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/services/staff-activity", () => ({
|
||||
logStaffActivity: doubles.logStaffActivity,
|
||||
}));
|
||||
|
||||
import { createSystemCommands } from "../commands/system-commands";
|
||||
import {
|
||||
type SystemMutationOperation,
|
||||
systemMutationService,
|
||||
} from "./mutations";
|
||||
|
||||
function capabilityContext(
|
||||
granted: readonly string[],
|
||||
): HousekeepingCapabilityContext {
|
||||
const permissions = new Set(granted);
|
||||
return {
|
||||
actor: { id: 42, username: "operator", rank: 500 },
|
||||
isSuperAdmin: false,
|
||||
has: (slug) => permissions.has(slug),
|
||||
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
|
||||
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
|
||||
};
|
||||
}
|
||||
|
||||
function serviceContext(permission: string) {
|
||||
return {
|
||||
capability: capabilityContext([permission]),
|
||||
correlationId: "rank-mutation-correlation",
|
||||
};
|
||||
}
|
||||
|
||||
function insertChain() {
|
||||
return {
|
||||
values: () => ({
|
||||
onDuplicateKeyUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function limitedSelection(rows: readonly unknown[]) {
|
||||
return {
|
||||
from: () => ({
|
||||
where: () => ({ limit: () => Promise.resolve(rows) }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function plainSelection(rows: readonly unknown[]) {
|
||||
return {
|
||||
from: () => ({ where: () => Promise.resolve(rows) }),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
for (const mock of Object.values(doubles)) mock.mockReset();
|
||||
doubles.createEmulatorRank.mockResolvedValue(7);
|
||||
doubles.deleteEmulatorRank.mockResolvedValue(undefined);
|
||||
doubles.updateEmulatorRank.mockResolvedValue(undefined);
|
||||
doubles.logStaffActivity.mockResolvedValue(undefined);
|
||||
doubles.dbInsert.mockImplementation(insertChain);
|
||||
});
|
||||
|
||||
describe("rank synchronization failures", () => {
|
||||
it("returns failure and audits failure when create committed but updatepermissions returned false", async () => {
|
||||
doubles.rconSend.mockResolvedValue(false);
|
||||
const command = createSystemCommands(systemMutationService).find(
|
||||
(entry) => entry.id === "system.access.rank.create",
|
||||
) as
|
||||
| HousekeepingCommand<{ name: string; level: number }, unknown>
|
||||
| undefined;
|
||||
if (!command) throw new Error("rank create command missing");
|
||||
registerHousekeepingCommand(command);
|
||||
const outcomes: string[] = [];
|
||||
|
||||
const result = await dispatchHousekeepingCommand(
|
||||
{
|
||||
commandId: command.id,
|
||||
input: { name: "Administrator", level: 7 },
|
||||
reason: "Create the administrator rank",
|
||||
},
|
||||
{
|
||||
context: capabilityContext([PERMS.PERMISSIONS_MANAGE]),
|
||||
ipAddress: "198.51.100.8",
|
||||
audit: {
|
||||
write: async (entry) => {
|
||||
if (entry.outcome) outcomes.push(entry.outcome);
|
||||
},
|
||||
},
|
||||
rateLimit: async () => true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "DEPENDENCY_UNAVAILABLE",
|
||||
messageKey: "errors.housekeeping.system.rankSynchronizationIncomplete",
|
||||
fieldErrors: {
|
||||
operation: ["access.rank.create"],
|
||||
completed: ["database", "audit"],
|
||||
pending: ["emulator-permission-cache"],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(outcomes).toEqual(["intent", "failure"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"access.rank.delete",
|
||||
{ id: 7 },
|
||||
() => {
|
||||
doubles.dbSelect
|
||||
.mockImplementationOnce(() => plainSelection([{ total: 0 }]))
|
||||
.mockImplementationOnce(() => limitedSelection([]));
|
||||
},
|
||||
],
|
||||
["access.rank.update", { id: 7, fields: { badge: "ADM" } }, () => {}],
|
||||
] as const)(
|
||||
"returns failure when %s committed but updatepermissions returned false",
|
||||
async (operation, input, prepare) => {
|
||||
prepare();
|
||||
doubles.rconSend.mockResolvedValue(false);
|
||||
|
||||
const result = await systemMutationService.execute(
|
||||
serviceContext(PERMS.PERMISSIONS_MANAGE),
|
||||
operation as SystemMutationOperation,
|
||||
input,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "DEPENDENCY_UNAVAILABLE",
|
||||
messageKey:
|
||||
"errors.housekeeping.system.rankSynchronizationIncomplete",
|
||||
fieldErrors: {
|
||||
operation: [operation],
|
||||
completed: ["database", "audit"],
|
||||
pending: ["emulator-permission-cache"],
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("reports external completion when set-rank RCON succeeded but DB persistence failed", async () => {
|
||||
doubles.dbSelect.mockImplementationOnce(() =>
|
||||
limitedSelection([{ rank: 3 }]),
|
||||
);
|
||||
doubles.dbExecute.mockResolvedValue([[{ id: 4 }]]);
|
||||
doubles.rconSetRank.mockResolvedValue(true);
|
||||
doubles.dbUpdate.mockReturnValue({
|
||||
set: () => ({
|
||||
where: () => Promise.reject(new Error("users update unavailable")),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await systemMutationService.execute(
|
||||
serviceContext(PERMS.RCON_EXECUTE),
|
||||
"rcon.set-rank",
|
||||
{ userId: 8, rank: 4 },
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "DEPENDENCY_UNAVAILABLE",
|
||||
messageKey: "errors.housekeeping.system.rankPersistenceIncomplete",
|
||||
fieldErrors: {
|
||||
operation: ["rcon.set-rank"],
|
||||
completed: ["emulator"],
|
||||
pending: ["database"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user