fix(housekeeping): complete people workflow fidelity

This commit is contained in:
Simo committed 2026-08-29 14:39:38 +02:00
1 parent 25b76437ff
commit 91c9efcbb4
29 files changed
+1365 -290

No files matched your search

+1 -2
View File
@@ -14,8 +14,7 @@ export async function dismissApplication(formData: FormData): Promise<void> {
const staff = await requirePermission(PERMS.USERS_EDIT);
const rawId = formPositiveBigInt(formData, "id");
if (!rawId) return;
const applicationId = Number(rawId);
if (!Number.isSafeInteger(applicationId) || applicationId <= 0) return;
const applicationId = rawId.toString();
await peopleMutationService.execute(
createPeopleMutationInvocation(staff, createCorrelationId()),
+10 -2
View File
@@ -51,8 +51,8 @@ it("preserves all four IP actions and /admin revalidation", async () => {
"ip.action",
{ action: "add-blacklist", ipAddress: "198.51.100.1", asn: "" },
],
["ip.action", { action: "delete-whitelist", id: 42 }],
["ip.action", { action: "delete-blacklist", id: 99 }],
["ip.action", { action: "delete-whitelist", id: "42" }],
["ip.action", { action: "delete-blacklist", id: "99" }],
]);
expect(revalidatePath).toHaveBeenCalledTimes(4);
});
@@ -62,3 +62,11 @@ it("keeps empty IP input as a no-op after authorization", async () => {
expect(requirePermission).toHaveBeenCalledWith("admin.settings.edit");
expect(execute).not.toHaveBeenCalled();
});
it("preserves an IP rule ID above Number.MAX_SAFE_INTEGER", async () => {
await deleteBlacklist(form({ id: "9007199254740993" }));
expect(execute).toHaveBeenCalledWith(expect.anything(), "ip.action", {
action: "delete-blacklist",
id: "9007199254740993",
});
});
+4 -3
View File
@@ -7,6 +7,7 @@ import {
} from "@/features/housekeeping/domains/people/services/mutations";
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
import { requirePermission } from "@/lib/admin/guard";
import { formPositiveBigInt } from "@/lib/form-data";
import { PERMS } from "@/lib/permissions";
function parse(formData: FormData, key: string): string {
@@ -26,16 +27,16 @@ async function run(
): Promise<void> {
const staff = await requirePermission(PERMS.SETTINGS_EDIT);
const adding = action.startsWith("add-");
const rawId = adding ? null : formPositiveBigInt(formData, "id");
const input = adding
? {
action,
ipAddress: parse(formData, "ipAddress"),
asn: parse(formData, "asn"),
}
: { action, id: Number(formData.get("id")) };
: { action, id: rawId?.toString() ?? "" };
if (adding && !("ipAddress" in input && input.ipAddress)) return;
const id = "id" in input ? input.id : undefined;
if (!adding && !(Number.isSafeInteger(id) && Number(id) > 0)) return;
if (!adding && !rawId) return;
const result = await peopleMutationService.execute(
createPeopleMutationInvocation(staff, createCorrelationId()),
"ip.action",
+9 -1
View File
@@ -50,7 +50,7 @@ it("preserves create and delete team payloads plus /admin revalidation", async (
hiddenRank: false,
},
],
["team.change", { action: "delete", teamId: 42 }],
["team.change", { action: "delete", teamId: "42" }],
]);
expect(revalidatePath).toHaveBeenCalledTimes(2);
});
@@ -59,3 +59,11 @@ it("preserves empty rank name as a no-op", async () => {
await createTeam(form({ rankName: "" }));
expect(execute).not.toHaveBeenCalled();
});
it("preserves a team ID above Number.MAX_SAFE_INTEGER", async () => {
await deleteTeam(form({ id: "9007199254740993" }));
expect(execute).toHaveBeenCalledWith(expect.anything(), "team.change", {
action: "delete",
teamId: "9007199254740993",
});
});
+4 -2
View File
@@ -7,6 +7,7 @@ import {
} from "@/features/housekeeping/domains/people/services/mutations";
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
import { requirePermission } from "@/lib/admin/guard";
import { formPositiveBigInt } from "@/lib/form-data";
import { PERMS } from "@/lib/permissions";
function text(formData: FormData, key: string): string {
@@ -37,8 +38,9 @@ export async function createTeam(formData: FormData): Promise<void> {
export async function deleteTeam(formData: FormData): Promise<void> {
const staff = await requirePermission(PERMS.USERS_EDIT);
const teamId = Number(formData.get("id"));
if (!Number.isSafeInteger(teamId) || teamId <= 0) return;
const rawTeamId = formPositiveBigInt(formData, "id");
if (!rawTeamId) return;
const teamId = rawTeamId.toString();
const result = await peopleMutationService.execute(
createPeopleMutationInvocation(staff, createCorrelationId()),
"team.change",
+4 -3
View File
@@ -7,6 +7,7 @@ import {
} from "@/features/housekeeping/domains/people/services/mutations";
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
import { requirePermission } from "@/lib/admin/guard";
import { positiveBigInt } from "@/lib/api";
import { PERMS } from "@/lib/permissions";
import {
type ActionResult,
@@ -36,9 +37,9 @@ export async function addWord(input: {
export async function deleteWord(input: { id: string }): Promise<ActionResult> {
const staff = await requirePermission(PERMS.WORDFILTER_EDIT);
const id = Number(String(input.id ?? "").normalize("NFC"));
if (!Number.isSafeInteger(id) || id <= 0)
return actionError("Missing word id");
const parsedId = positiveBigInt(String(input.id ?? "").normalize("NFC"));
if (!parsedId) return actionError("Missing word id");
const id = parsedId.toString();
const result = await peopleMutationService.execute(
createPeopleMutationInvocation(staff, createCorrelationId()),
"word-filter.update",
+37 -5
View File
@@ -84,6 +84,13 @@ registerHousekeepingCommand({
ipAddress: commandContext.ipAddress,
},
commandContext.correlationId,
input.value === "partial"
? {
status: "partial",
external: "failed",
audit: "persisted",
}
: undefined,
);
},
});
@@ -136,6 +143,28 @@ describe("executeHousekeepingCommand", () => {
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",
@@ -185,7 +214,7 @@ describe("executeHousekeepingCommand", () => {
expect(commandExecutions).toEqual([]);
});
it("maps completed-operation audit failures to a typed partial result", async () => {
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");
@@ -200,13 +229,16 @@ describe("executeHousekeepingCommand", () => {
expect(commandExecutions).toEqual(["changed"]);
expect(result).toMatchObject({
ok: false,
error: {
code: "INTERNAL",
messageKey: "errors.housekeeping.partial",
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();
});
});
-25
View File
@@ -1,10 +1,8 @@
"use server";
import "@/features/housekeeping/foundation/commands/bootstrap";
import { AuditOutcomePersistenceError } from "@/features/housekeeping/foundation/commands/audit-envelope";
import { dispatchHousekeepingCommand } from "@/features/housekeeping/foundation/commands/dispatcher";
import {
fail,
type HousekeepingResult,
mapUnknownError,
} from "@/features/housekeeping/foundation/contracts";
@@ -29,29 +27,6 @@ export async function executeHousekeepingCommand(
(await rateLimit(key, attempts, windowMs)).ok,
});
} catch (error) {
if (
error instanceof AuditOutcomePersistenceError &&
isHousekeepingResult(error.operationResult)
) {
return fail(
"INTERNAL",
"errors.housekeeping.partial",
error.operationResult.correlationId,
);
}
return mapUnknownError(error);
}
}
function isHousekeepingResult(
value: unknown,
): value is HousekeepingResult<unknown> {
return (
typeof value === "object" &&
value !== null &&
"ok" in value &&
typeof (value as { ok?: unknown }).ok === "boolean" &&
"correlationId" in value &&
typeof (value as { correlationId?: unknown }).correlationId === "string"
);
}
+13 -2
View File
@@ -114,11 +114,22 @@ describe("legacy application and word-filter wrappers", () => {
expect(execute).toHaveBeenCalledWith(
expect.objectContaining({ expectedActorId: 1 }),
"application.decide",
{ applicationId: 9, decision: "dismiss" },
{ applicationId: "9", decision: "dismiss" },
);
expect(revalidatePath).toHaveBeenCalledWith("/admin/applications");
});
it("preserves application and wordfilter IDs above Number.MAX_SAFE_INTEGER", async () => {
await dismissApplication(form({ id: "9007199254740993" }));
await expect(deleteWord({ id: "9007199254740993" })).resolves.toEqual({
ok: true,
data: {},
});
expect(execute.mock.calls.slice(-2).map((call) => call[2])).toEqual([
{ applicationId: "9007199254740993", decision: "dismiss" },
{ action: "delete", id: "9007199254740993" },
]);
});
it("preserves word-filter ActionResult shapes and /admin revalidation", async () => {
await expect(addWord({ word: "spam" })).resolves.toEqual({
ok: true,
@@ -130,7 +141,7 @@ describe("legacy application and word-filter wrappers", () => {
});
expect(execute.mock.calls.slice(-2).map((call) => call[2])).toEqual([
{ action: "add", word: "spam" },
{ action: "delete", id: 12 },
{ action: "delete", id: "12" },
]);
expect(revalidatePath).toHaveBeenCalledWith("/admin/wordfilter");
});