fix(housekeeping): harden system workflow boundaries
This commit is contained in:
1 parent
3788ecd9f1
commit
c325c53774
18 files changed
+809
-79
No files matched your search
@@ -138,7 +138,7 @@ export function SystemAccessPage({ result }: SystemAccessPageProps) {
|
||||
data.ranks.length === 0 &&
|
||||
data.acl.roles === 0 &&
|
||||
data.acl.permissions === 0;
|
||||
if (empty) {
|
||||
if (empty && data.partialDependencies.length === 0) {
|
||||
body = state(
|
||||
"empty",
|
||||
"No access data",
|
||||
|
||||
@@ -111,7 +111,7 @@ export function SystemConfigurationPage({
|
||||
data.kind === "settings"
|
||||
? data.settings.length === 0
|
||||
: data.settings.length === 0 && data.texts.length === 0;
|
||||
if (empty) {
|
||||
if (empty && data.partialDependencies.length === 0) {
|
||||
body = pageState(
|
||||
"empty",
|
||||
"No configuration entries",
|
||||
|
||||
@@ -162,7 +162,10 @@ export function SystemObservabilityPage({
|
||||
"Observability data unavailable",
|
||||
`Request ${result.correlationId} could not be completed.`,
|
||||
);
|
||||
} else if (isEmpty(result.data)) {
|
||||
} else if (
|
||||
isEmpty(result.data) &&
|
||||
result.data.partialDependencies.length === 0
|
||||
) {
|
||||
body = pageState(
|
||||
"empty",
|
||||
"No observations",
|
||||
|
||||
@@ -155,3 +155,52 @@ describe.each(pageCases)(
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
"access",
|
||||
(result: HousekeepingResult<unknown>) =>
|
||||
renderToStaticMarkup(<SystemAccessPage result={result as never} />),
|
||||
{
|
||||
kind: "permissions",
|
||||
ranks: [],
|
||||
acl: { roles: 0, permissions: 0 },
|
||||
partialDependencies: ["acl"],
|
||||
},
|
||||
],
|
||||
[
|
||||
"configuration",
|
||||
(result: HousekeepingResult<unknown>) =>
|
||||
renderToStaticMarkup(
|
||||
<SystemConfigurationPage result={result as never} />,
|
||||
),
|
||||
{
|
||||
kind: "emulator",
|
||||
settings: [],
|
||||
texts: [],
|
||||
textsTotal: 0,
|
||||
partialDependencies: ["emulator-texts"],
|
||||
},
|
||||
],
|
||||
[
|
||||
"observability",
|
||||
(result: HousekeepingResult<unknown>) =>
|
||||
renderToStaticMarkup(
|
||||
<SystemObservabilityPage result={result as never} />,
|
||||
),
|
||||
{
|
||||
kind: "devops",
|
||||
health: null,
|
||||
errors: [],
|
||||
partialDependencies: ["health"],
|
||||
},
|
||||
],
|
||||
] as const)(
|
||||
"renders %s as partial when a dependency failed and surviving data is empty",
|
||||
(_name, render, data) => {
|
||||
const html = render(ok(data, correlationId));
|
||||
|
||||
expect(html).toContain('data-housekeeping-state="partial"');
|
||||
expect(html).not.toContain('data-housekeeping-state="empty"');
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,64 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { select } = vi.hoisted(() => ({ select: vi.fn() }));
|
||||
|
||||
vi.mock("drizzle-orm", () => ({
|
||||
count: vi.fn(() => "count"),
|
||||
desc: vi.fn(() => "descending"),
|
||||
eq: vi.fn(() => "equals"),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: { select },
|
||||
User: {
|
||||
id: "id",
|
||||
username: "username",
|
||||
look: "look",
|
||||
lastOnline: "lastOnline",
|
||||
online: "online",
|
||||
},
|
||||
}));
|
||||
|
||||
import { fetchOpsOnlineUsers } from "@/lib/admin/ops-online-users";
|
||||
import { systemOperationsAdapters } from "./operations";
|
||||
|
||||
function rejectOnlineUserQueries() {
|
||||
select
|
||||
.mockImplementationOnce(() => ({
|
||||
from: () => ({
|
||||
where: () => Promise.reject(new Error("online count unavailable")),
|
||||
}),
|
||||
}))
|
||||
.mockImplementationOnce(() => ({
|
||||
from: () => ({
|
||||
where: () => ({
|
||||
orderBy: () => ({
|
||||
limit: () => Promise.reject(new Error("online roster unavailable")),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
describe("System online-users production adapter", () => {
|
||||
beforeEach(() => {
|
||||
select.mockReset();
|
||||
});
|
||||
|
||||
it("preserves the tolerant empty fallback for legacy callers", async () => {
|
||||
rejectOnlineUserQueries();
|
||||
|
||||
await expect(fetchOpsOnlineUsers(40)).resolves.toEqual({
|
||||
count: 0,
|
||||
users: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces database failure to the System query boundary", async () => {
|
||||
rejectOnlineUserQueries();
|
||||
|
||||
await expect(systemOperationsAdapters.loadOnlineUsers()).rejects.toThrow(
|
||||
"online count unavailable",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -207,10 +207,10 @@ export const systemOperationsAdapters: SystemOperationsAdapters = {
|
||||
return fetchOpsHealth();
|
||||
},
|
||||
async loadOnlineUsers() {
|
||||
const { fetchOpsOnlineUsers } = await import(
|
||||
const { fetchOpsOnlineUsersStrict } = await import(
|
||||
"@/lib/admin/ops-online-users"
|
||||
);
|
||||
return fetchOpsOnlineUsers(40);
|
||||
return fetchOpsOnlineUsersStrict(40);
|
||||
},
|
||||
async loadMaintenance() {
|
||||
const [{ inArray }, { db, WebsiteSetting }] = await Promise.all([
|
||||
|
||||
@@ -7,103 +7,103 @@ const expectedRoutes = [
|
||||
[
|
||||
"system.access.permissions",
|
||||
"/ase/system/access/permissions",
|
||||
"pages.admin.hubs.tabs.permissions",
|
||||
"pages.housekeeping.routes.system.access.permissions",
|
||||
PERMS.PERMISSIONS_MANAGE,
|
||||
],
|
||||
[
|
||||
"system.access.permission-detail",
|
||||
"/ase/system/access/permissions/:id",
|
||||
"pages.admin.hubs.tabs.permissions",
|
||||
"pages.housekeeping.routes.system.access.permission-detail",
|
||||
PERMS.PERMISSIONS_MANAGE,
|
||||
],
|
||||
[
|
||||
"system.configuration.settings",
|
||||
"/ase/system/configuration/settings",
|
||||
"pages.admin.hubs.tabs.cms",
|
||||
"pages.housekeeping.routes.system.configuration.settings",
|
||||
PERMS.SETTINGS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.configuration.emulator",
|
||||
"/ase/system/configuration/emulator",
|
||||
"pages.admin.hubs.tabs.emulator",
|
||||
"pages.housekeeping.routes.system.configuration.emulator",
|
||||
PERMS.SETTINGS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.observability.analytics",
|
||||
"/ase/system/observability/analytics",
|
||||
"pages.admin.hubs.tabs.analytics",
|
||||
"pages.housekeeping.routes.system.observability.analytics",
|
||||
PERMS.ANALYTICS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.observability.analytics-activity",
|
||||
"/ase/system/observability/analytics/activity",
|
||||
"pages.admin.hubs.tabs.activity",
|
||||
"pages.housekeeping.routes.system.observability.analytics-activity",
|
||||
PERMS.ANALYTICS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.observability.analytics-economy",
|
||||
"/ase/system/observability/analytics/economy",
|
||||
"pages.admin.hubs.tabs.economy",
|
||||
"pages.housekeeping.routes.system.observability.analytics-economy",
|
||||
PERMS.ANALYTICS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.observability.devops",
|
||||
"/ase/system/observability/devops",
|
||||
"pages.admin.hubs.tabs.devops",
|
||||
"pages.housekeeping.routes.system.observability.devops",
|
||||
PERMS.DEVOPS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.observability.devops-errors",
|
||||
"/ase/system/observability/devops/errors",
|
||||
"pages.admin.hubs.tabs.errors",
|
||||
"pages.housekeeping.routes.system.observability.devops-errors",
|
||||
PERMS.DEVOPS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.observability.logs-staff",
|
||||
"/ase/system/observability/logs/staff",
|
||||
"pages.admin.hubs.tabs.logs",
|
||||
"pages.housekeeping.routes.system.observability.logs-staff",
|
||||
PERMS.LOGS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.observability.logs-audit",
|
||||
"/ase/system/observability/logs/audit",
|
||||
"pages.admin.hubs.tabs.audit",
|
||||
"pages.housekeeping.routes.system.observability.logs-audit",
|
||||
PERMS.LOGS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.observability.logs-chat",
|
||||
"/ase/system/observability/logs/chat",
|
||||
"pages.admin.hubs.tabs.chat",
|
||||
"pages.housekeeping.routes.system.observability.logs-chat",
|
||||
PERMS.LOGS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.observability.logs-commands",
|
||||
"/ase/system/observability/logs/commands",
|
||||
"pages.admin.hubs.tabs.commands",
|
||||
"pages.housekeeping.routes.system.observability.logs-commands",
|
||||
PERMS.LOGS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.observability.logs-trades",
|
||||
"/ase/system/observability/logs/trades",
|
||||
"pages.admin.hubs.tabs.trades",
|
||||
"pages.housekeeping.routes.system.observability.logs-trades",
|
||||
PERMS.LOGS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.operations.alerts",
|
||||
"/ase/system/operations/alerts",
|
||||
"pages.admin.hubs.tabs.alerts",
|
||||
"pages.housekeeping.routes.system.operations.alerts",
|
||||
PERMS.NOTIFICATIONS_VIEW,
|
||||
],
|
||||
[
|
||||
"system.operations.command-center",
|
||||
"/ase/system/operations/command-center",
|
||||
"pages.admin.hubs.tabs.commando",
|
||||
"pages.housekeeping.routes.system.operations.command-center",
|
||||
PERMS.RCON_EXECUTE,
|
||||
],
|
||||
[
|
||||
"system.operations.maintenance",
|
||||
"/ase/system/operations/maintenance",
|
||||
"pages.admin.hubs.tabs.maintenance",
|
||||
"pages.housekeeping.routes.system.operations.maintenance",
|
||||
PERMS.SETTINGS_VIEW,
|
||||
],
|
||||
] as const;
|
||||
|
||||
@@ -29,103 +29,105 @@ export type SystemRouteId = (typeof SYSTEM_ROUTE_IDS)[number];
|
||||
export const SYSTEM_ROUTES = [
|
||||
{
|
||||
id: "system.access.permissions",
|
||||
labelKey: "pages.admin.hubs.tabs.permissions",
|
||||
labelKey: "pages.housekeeping.routes.system.access.permissions",
|
||||
href: "/ase/system/access/permissions",
|
||||
capability: anyCapability(PERMS.PERMISSIONS_MANAGE),
|
||||
},
|
||||
{
|
||||
id: "system.access.permission-detail",
|
||||
labelKey: "pages.admin.hubs.tabs.permissions",
|
||||
labelKey: "pages.housekeeping.routes.system.access.permission-detail",
|
||||
href: "/ase/system/access/permissions/:id",
|
||||
capability: anyCapability(PERMS.PERMISSIONS_MANAGE),
|
||||
},
|
||||
{
|
||||
id: "system.configuration.settings",
|
||||
labelKey: "pages.admin.hubs.tabs.cms",
|
||||
labelKey: "pages.housekeeping.routes.system.configuration.settings",
|
||||
href: "/ase/system/configuration/settings",
|
||||
capability: anyCapability(PERMS.SETTINGS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.configuration.emulator",
|
||||
labelKey: "pages.admin.hubs.tabs.emulator",
|
||||
labelKey: "pages.housekeeping.routes.system.configuration.emulator",
|
||||
href: "/ase/system/configuration/emulator",
|
||||
capability: anyCapability(PERMS.SETTINGS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.observability.analytics",
|
||||
labelKey: "pages.admin.hubs.tabs.analytics",
|
||||
labelKey: "pages.housekeeping.routes.system.observability.analytics",
|
||||
href: "/ase/system/observability/analytics",
|
||||
capability: anyCapability(PERMS.ANALYTICS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.observability.analytics-activity",
|
||||
labelKey: "pages.admin.hubs.tabs.activity",
|
||||
labelKey:
|
||||
"pages.housekeeping.routes.system.observability.analytics-activity",
|
||||
href: "/ase/system/observability/analytics/activity",
|
||||
capability: anyCapability(PERMS.ANALYTICS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.observability.analytics-economy",
|
||||
labelKey: "pages.admin.hubs.tabs.economy",
|
||||
labelKey:
|
||||
"pages.housekeeping.routes.system.observability.analytics-economy",
|
||||
href: "/ase/system/observability/analytics/economy",
|
||||
capability: anyCapability(PERMS.ANALYTICS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.observability.devops",
|
||||
labelKey: "pages.admin.hubs.tabs.devops",
|
||||
labelKey: "pages.housekeeping.routes.system.observability.devops",
|
||||
href: "/ase/system/observability/devops",
|
||||
capability: anyCapability(PERMS.DEVOPS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.observability.devops-errors",
|
||||
labelKey: "pages.admin.hubs.tabs.errors",
|
||||
labelKey: "pages.housekeeping.routes.system.observability.devops-errors",
|
||||
href: "/ase/system/observability/devops/errors",
|
||||
capability: anyCapability(PERMS.DEVOPS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.observability.logs-staff",
|
||||
labelKey: "pages.admin.hubs.tabs.logs",
|
||||
labelKey: "pages.housekeeping.routes.system.observability.logs-staff",
|
||||
href: "/ase/system/observability/logs/staff",
|
||||
capability: anyCapability(PERMS.LOGS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.observability.logs-audit",
|
||||
labelKey: "pages.admin.hubs.tabs.audit",
|
||||
labelKey: "pages.housekeeping.routes.system.observability.logs-audit",
|
||||
href: "/ase/system/observability/logs/audit",
|
||||
capability: anyCapability(PERMS.LOGS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.observability.logs-chat",
|
||||
labelKey: "pages.admin.hubs.tabs.chat",
|
||||
labelKey: "pages.housekeeping.routes.system.observability.logs-chat",
|
||||
href: "/ase/system/observability/logs/chat",
|
||||
capability: anyCapability(PERMS.LOGS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.observability.logs-commands",
|
||||
labelKey: "pages.admin.hubs.tabs.commands",
|
||||
labelKey: "pages.housekeeping.routes.system.observability.logs-commands",
|
||||
href: "/ase/system/observability/logs/commands",
|
||||
capability: anyCapability(PERMS.LOGS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.observability.logs-trades",
|
||||
labelKey: "pages.admin.hubs.tabs.trades",
|
||||
labelKey: "pages.housekeeping.routes.system.observability.logs-trades",
|
||||
href: "/ase/system/observability/logs/trades",
|
||||
capability: anyCapability(PERMS.LOGS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.operations.alerts",
|
||||
labelKey: "pages.admin.hubs.tabs.alerts",
|
||||
labelKey: "pages.housekeeping.routes.system.operations.alerts",
|
||||
href: "/ase/system/operations/alerts",
|
||||
capability: anyCapability(PERMS.NOTIFICATIONS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "system.operations.command-center",
|
||||
labelKey: "pages.admin.hubs.tabs.commando",
|
||||
labelKey: "pages.housekeeping.routes.system.operations.command-center",
|
||||
href: "/ase/system/operations/command-center",
|
||||
capability: anyCapability(PERMS.RCON_EXECUTE),
|
||||
},
|
||||
{
|
||||
id: "system.operations.maintenance",
|
||||
labelKey: "pages.admin.hubs.tabs.maintenance",
|
||||
labelKey: "pages.housekeeping.routes.system.operations.maintenance",
|
||||
href: "/ase/system/operations/maintenance",
|
||||
capability: anyCapability(PERMS.SETTINGS_VIEW),
|
||||
},
|
||||
|
||||
@@ -208,6 +208,25 @@ async function requireRcon(result: boolean): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function requireRankPermissionSynchronization(
|
||||
operation: "access.rank.create" | "access.rank.delete" | "access.rank.update",
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (await rcon.send("updatepermissions")) return;
|
||||
} catch {
|
||||
// Report the already committed local changes through the typed envelope.
|
||||
}
|
||||
throw new SystemMutationFailure(
|
||||
"DEPENDENCY_UNAVAILABLE",
|
||||
"errors.housekeeping.system.rankSynchronizationIncomplete",
|
||||
{
|
||||
operation: [operation],
|
||||
completed: ["database", "audit"],
|
||||
pending: ["emulator-permission-cache"],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function upsertWebsiteSetting(
|
||||
key: string,
|
||||
value: string,
|
||||
@@ -244,7 +263,7 @@ async function executeAccessMutation(
|
||||
targetType: "rank",
|
||||
targetId: id,
|
||||
});
|
||||
await rcon.send("updatepermissions");
|
||||
await requireRankPermissionSynchronization("access.rank.create");
|
||||
return { id };
|
||||
}
|
||||
|
||||
@@ -289,7 +308,7 @@ async function executeAccessMutation(
|
||||
targetType: "rank",
|
||||
targetId: id,
|
||||
});
|
||||
await rcon.send("updatepermissions");
|
||||
await requireRankPermissionSynchronization("access.rank.delete");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -316,7 +335,7 @@ async function executeAccessMutation(
|
||||
targetType: "rank",
|
||||
targetId: id,
|
||||
});
|
||||
await rcon.send("updatepermissions");
|
||||
await requireRankPermissionSynchronization("access.rank.update");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -658,7 +677,19 @@ async function executeRconMutation(
|
||||
}
|
||||
}
|
||||
await requireRcon(await rcon.setRank(userId, rank));
|
||||
await db.update(User).set({ rank }).where(eq(User.id, userId));
|
||||
try {
|
||||
await db.update(User).set({ rank }).where(eq(User.id, userId));
|
||||
} catch {
|
||||
throw new SystemMutationFailure(
|
||||
"DEPENDENCY_UNAVAILABLE",
|
||||
"errors.housekeeping.system.rankPersistenceIncomplete",
|
||||
{
|
||||
operation: ["rcon.set-rank"],
|
||||
completed: ["emulator"],
|
||||
pending: ["database"],
|
||||
},
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "rcon.execute-command":
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,26 @@ const requiredKeys = [
|
||||
"pages.housekeeping.states.error.description",
|
||||
];
|
||||
|
||||
const systemRouteMessageKeys = [
|
||||
"pages.housekeeping.routes.system.access.permissions",
|
||||
"pages.housekeeping.routes.system.access.permission-detail",
|
||||
"pages.housekeeping.routes.system.configuration.settings",
|
||||
"pages.housekeeping.routes.system.configuration.emulator",
|
||||
"pages.housekeeping.routes.system.observability.analytics",
|
||||
"pages.housekeeping.routes.system.observability.analytics-activity",
|
||||
"pages.housekeeping.routes.system.observability.analytics-economy",
|
||||
"pages.housekeeping.routes.system.observability.devops",
|
||||
"pages.housekeeping.routes.system.observability.devops-errors",
|
||||
"pages.housekeeping.routes.system.observability.logs-staff",
|
||||
"pages.housekeeping.routes.system.observability.logs-audit",
|
||||
"pages.housekeeping.routes.system.observability.logs-chat",
|
||||
"pages.housekeeping.routes.system.observability.logs-commands",
|
||||
"pages.housekeeping.routes.system.observability.logs-trades",
|
||||
"pages.housekeeping.routes.system.operations.alerts",
|
||||
"pages.housekeeping.routes.system.operations.command-center",
|
||||
"pages.housekeeping.routes.system.operations.maintenance",
|
||||
] as const;
|
||||
|
||||
const expectedDomainMessages = [
|
||||
{
|
||||
id: "operations",
|
||||
@@ -74,6 +94,7 @@ describe("housekeeping localization contract", () => {
|
||||
"preview",
|
||||
"navigation",
|
||||
"domains",
|
||||
"routes",
|
||||
"states",
|
||||
]);
|
||||
|
||||
@@ -81,6 +102,12 @@ describe("housekeeping localization contract", () => {
|
||||
expect(resolveMessage(messages, key), key).toEqual(expect.any(String));
|
||||
}
|
||||
|
||||
for (const key of systemRouteMessageKeys) {
|
||||
const message = resolveMessage(messages, key);
|
||||
expect(message, key).toEqual(expect.any(String));
|
||||
expect((message as string).trim(), key).not.toBe("");
|
||||
}
|
||||
|
||||
for (const [index, expected] of expectedDomainMessages.entries()) {
|
||||
const manifest = HOUSEKEEPING_MANIFESTS[index];
|
||||
|
||||
|
||||
@@ -19,6 +19,42 @@ const routeMocks = vi.hoisted(() => {
|
||||
"domains.people.description": "Localized People description",
|
||||
"domains.economy.title": "Localized Economy",
|
||||
"domains.economy.description": "Localized Economy description",
|
||||
"domains.operations.title": "Localized Operations",
|
||||
"domains.operations.description": "Localized Operations description",
|
||||
"domains.content.title": "Localized Content",
|
||||
"domains.content.description": "Localized Content description",
|
||||
"domains.hotel.title": "Localized Hotel",
|
||||
"domains.hotel.description": "Localized Hotel description",
|
||||
"domains.system.title": "HK::system-title",
|
||||
"domains.system.description": "Localized System description",
|
||||
"routes.system.access.permissions": "HK::system-access-permissions",
|
||||
"routes.system.access.permission-detail":
|
||||
"HK::system-access-permission-detail",
|
||||
"routes.system.configuration.settings": "HK::system-configuration-settings",
|
||||
"routes.system.configuration.emulator": "HK::system-configuration-emulator",
|
||||
"routes.system.observability.analytics":
|
||||
"HK::system-observability-analytics",
|
||||
"routes.system.observability.analytics-activity":
|
||||
"HK::system-observability-analytics-activity",
|
||||
"routes.system.observability.analytics-economy":
|
||||
"HK::system-observability-analytics-economy",
|
||||
"routes.system.observability.devops": "HK::system-observability-devops",
|
||||
"routes.system.observability.devops-errors":
|
||||
"HK::system-observability-devops-errors",
|
||||
"routes.system.observability.logs-staff":
|
||||
"HK::system-observability-logs-staff",
|
||||
"routes.system.observability.logs-audit":
|
||||
"HK::system-observability-logs-audit",
|
||||
"routes.system.observability.logs-chat":
|
||||
"HK::system-observability-logs-chat",
|
||||
"routes.system.observability.logs-commands":
|
||||
"HK::system-observability-logs-commands",
|
||||
"routes.system.observability.logs-trades":
|
||||
"HK::system-observability-logs-trades",
|
||||
"routes.system.operations.alerts": "HK::system-operations-alerts",
|
||||
"routes.system.operations.command-center":
|
||||
"HK::system-operations-command-center",
|
||||
"routes.system.operations.maintenance": "HK::system-operations-maintenance",
|
||||
"states.empty.title": "Localized empty title",
|
||||
"states.empty.description": "Localized empty description",
|
||||
};
|
||||
@@ -599,6 +635,44 @@ describe("/ase-next/[domain] layout", () => {
|
||||
);
|
||||
expect(routeMocks.getTranslations).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("builds and translates the complete real System navigation", async () => {
|
||||
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue({
|
||||
...capabilityContext([]),
|
||||
isSuperAdmin: true,
|
||||
});
|
||||
|
||||
const html = await renderRoute(
|
||||
AdminNextDomainLayout({
|
||||
children: createElement("p", null, "System body"),
|
||||
params: Promise.resolve({ domain: "system" }),
|
||||
}),
|
||||
);
|
||||
|
||||
for (const label of [
|
||||
"HK::system-access-permissions",
|
||||
"HK::system-access-permission-detail",
|
||||
"HK::system-configuration-settings",
|
||||
"HK::system-configuration-emulator",
|
||||
"HK::system-observability-analytics",
|
||||
"HK::system-observability-analytics-activity",
|
||||
"HK::system-observability-analytics-economy",
|
||||
"HK::system-observability-devops",
|
||||
"HK::system-observability-devops-errors",
|
||||
"HK::system-observability-logs-staff",
|
||||
"HK::system-observability-logs-audit",
|
||||
"HK::system-observability-logs-chat",
|
||||
"HK::system-observability-logs-commands",
|
||||
"HK::system-observability-logs-trades",
|
||||
"HK::system-operations-alerts",
|
||||
"HK::system-operations-command-center",
|
||||
"HK::system-operations-maintenance",
|
||||
]) {
|
||||
expect(html).toContain(label);
|
||||
}
|
||||
expect(html).toContain("HK::system-title");
|
||||
expect(html).toContain("System body");
|
||||
});
|
||||
});
|
||||
|
||||
describe("/ase-next/[domain]/[[...segments]] page", () => {
|
||||
|
||||
Reference in new issue
Block a user