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

@@ -95,6 +95,50 @@ describe("People community and staff commands", () => {
},
);
it.each([
[
"people.application.decide",
{ applicationId: "9007199254740993", decision: "dismiss" },
],
["people.team.change", { action: "delete", teamId: "9007199254740993" }],
[
"people.ip.action",
{ action: "delete-blacklist", id: "9007199254740993" },
],
["people.word-filter.update", { action: "delete", id: "9007199254740993" }],
] as const)(
"accepts a canonical BIGINT identifier for %s",
(commandId, input) => {
const command = commands.find((entry) => entry.id === commandId);
if (!command) throw new Error(`command missing: ${commandId}`);
expect(command.input.safeParse(input).success).toBe(true);
},
);
it.each([
[
"people.application.decide",
{ applicationId: "18446744073709551616", decision: "dismiss" },
],
[
"people.team.change",
{ action: "delete", teamId: "18446744073709551616" },
],
[
"people.ip.action",
{ action: "delete-blacklist", id: "18446744073709551616" },
],
[
"people.word-filter.update",
{ action: "delete", id: "18446744073709551616" },
],
] as const)(
"rejects an overflowing BIGINT identifier for %s",
(commandId, input) => {
const command = commands.find((entry) => entry.id === commandId);
if (!command) throw new Error(`command missing: ${commandId}`);
expect(command.input.safeParse(input).success).toBe(false);
},
);
it("delegates the canonical operation without a redirect", async () => {
const execute = vi.fn(async (context, operation, input) => ({
ok: true as const,
@@ -53,6 +53,30 @@ function communityCommand<I>(
}
const positiveId = z.number().int().positive();
const MAX_UNSIGNED_BIGINT = "18446744073709551615";
function boundedDecimalPattern(maximum: string): RegExp {
const alternatives = [`[1-9]\\d{0,${maximum.length - 2}}`];
for (let index = 0; index < maximum.length; index += 1) {
const maximumDigit = Number(maximum[index]);
const minimumDigit = index === 0 ? 1 : 0;
const upperDigit = maximumDigit - 1;
if (upperDigit < minimumDigit) continue;
const digit =
upperDigit === minimumDigit
? String(minimumDigit)
: `[${minimumDigit}-${upperDigit}]`;
alternatives.push(
`${maximum.slice(0, index)}${digit}\\d{${maximum.length - index - 1}}`,
);
}
alternatives.push(maximum);
return new RegExp(`^(?:${alternatives.join("|")})$`, "u");
}
const unsignedBigIntId = z
.string()
.regex(boundedDecimalPattern(MAX_UNSIGNED_BIGINT));
const requiredText = (max: number) => z.string().min(1).max(max).regex(/\S/u);
export function createCommunityCommands(
@@ -70,7 +94,7 @@ export function createCommunityCommands(
operation: "application.decide",
capability: PERMS.USERS_EDIT,
input: z.object({
applicationId: positiveId,
applicationId: unsignedBigIntId,
decision: z.literal("dismiss"),
}),
}),
@@ -87,7 +111,7 @@ export function createCommunityCommands(
staffColor: z.string().min(1).max(255).optional(),
hiddenRank: z.boolean().optional(),
}),
z.object({ action: z.literal("delete"), teamId: positiveId }),
z.object({ action: z.literal("delete"), teamId: unsignedBigIntId }),
]),
}),
communityCommand(service, {
@@ -102,7 +126,7 @@ export function createCommunityCommands(
}),
z.object({
action: z.enum(["delete-whitelist", "delete-blacklist"]),
id: positiveId,
id: unsignedBigIntId,
}),
]),
}),
@@ -123,7 +147,7 @@ export function createCommunityCommands(
capability: PERMS.WORDFILTER_EDIT,
input: z.discriminatedUnion("action", [
z.object({ action: z.literal("add"), word: requiredText(255) }),
z.object({ action: z.literal("delete"), id: positiveId }),
z.object({ action: z.literal("delete"), id: unsignedBigIntId }),
]),
}),
] as const;
@@ -111,7 +111,7 @@ export interface PeopleGuildDetail extends PeopleGuildSummary {
}
export interface PeopleStaffApplication {
readonly id: number;
readonly id: string;
readonly userId: number;
readonly username: string | null;
readonly rankId: number;
@@ -120,9 +120,9 @@ export interface PeopleStaffApplication {
}
export interface PeopleTeam {
readonly id: number;
readonly id: string;
readonly name: string;
readonly rank: number;
readonly rank: string;
readonly hidden: boolean;
readonly badge: string | null;
readonly jobDescription: string | null;
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
import { executeHousekeepingCommand } from "@/actions/housekeeping-command";
import { PERMS } from "@/lib/permission-slugs";
vi.mock("@/actions/housekeeping-command", () => ({
@@ -26,6 +27,10 @@ import {
import { PeopleCommunityPage } from "./community";
import { PeopleMultiAccountsPage } from "./multi-accounts";
import { parsePeopleListInput } from "./page-state";
import {
PeopleCommandResult,
submitPeopleCommandForm,
} from "./people-command-form";
import { PeopleStaffPage } from "./staff";
import { PeopleUserDetailPage } from "./user-detail";
import { PeopleUserEditPage } from "./user-edit";
@@ -215,7 +220,7 @@ const cases: readonly PageCase[] = [
page: {
items: [
{
id: 5,
id: "5",
userId: 7,
username: "Alice",
rankId: 4,
@@ -342,17 +347,58 @@ describe("People URL list input", () => {
});
});
describe("People actionable form contract", () => {
it("submits bounded command input and reason through the existing server action", () => {
const source = readFileSync(
"src/features/housekeeping/domains/people/pages/people-command-form.tsx",
"utf8",
it("submits bounded command input and reason through the actual form action adapter", async () => {
vi.mocked(executeHousekeepingCommand).mockResolvedValue(
ok({ before: null, after: { amount: 100 } }, "form-action"),
);
expect(source).toContain("executeHousekeepingCommand({");
expect(source).toContain("commandId");
expect(source).toContain("input: commandInput");
expect(source).toContain("{ reason }");
expect(source).toContain("action={submit}");
expect(source).not.toContain("<span data-housekeeping-command");
const formData = new FormData();
formData.set("amount", "999");
formData.set("reason", " Planned grant ");
const result = await submitPeopleCommandForm(
{
commandId: "people.user.send-currency",
input: { userId: 7 },
fields: [
{
name: "amount",
label: "Amount",
type: "number",
min: 1,
max: 100,
},
],
requiresReason: true,
includeReasonInInput: true,
},
null,
formData,
);
expect(executeHousekeepingCommand).toHaveBeenCalledWith({
commandId: "people.user.send-currency",
input: { userId: 7, amount: 100, reason: "Planned grant" },
reason: "Planned grant",
});
expect(result).toMatchObject({ ok: true });
});
it("renders a generated reset credential once in the current accessible result", () => {
const password = "one-time-secret";
const html = renderToStaticMarkup(
<PeopleCommandResult
result={ok(
{
before: { passwordReset: false },
after: { passwordReset: true },
output: { newPassword: password },
},
"reset-result",
)}
/>,
);
expect(html).toContain('aria-label="Generated temporary password"');
expect(html.match(new RegExp(password, "gu"))).toHaveLength(1);
});
it("provides a real Next loading boundary", () => {
@@ -427,7 +427,7 @@ describe("People community and staff queries", () => {
loadApplications: async () => ({
rows: [
{
id: 11,
id: "9007199254740993",
userId: 5,
username: "Applicant",
rankId: 3,
@@ -456,7 +456,10 @@ describe("People community and staff queries", () => {
expect(applications).toMatchObject({
ok: true,
data: { kind: "applications", page: { items: [{ id: 11 }] } },
data: {
kind: "applications",
page: { items: [{ id: "9007199254740993" }] },
},
});
expect(teams).toMatchObject({ ok: true, data: { kind: "teams" } });
expect(moderationTeam).toMatchObject({
@@ -169,7 +169,7 @@ const peopleStaffAdapters: PeopleStaffAdapters = {
content: string;
createdAt: Date | string | null;
}>(rowsResult).map((row) => ({
id: Number(row.id),
id: String(row.id),
userId: Number(row.userId),
username: row.username,
rankId: Number(row.rankId),
@@ -210,9 +210,9 @@ const peopleStaffAdapters: PeopleStaffAdapters = {
badge: string | null;
jobDescription: string | null;
}>(rowsResult).map((row) => ({
id: Number(row.id),
id: String(row.id),
name: row.name,
rank: Number(row.id),
rank: String(row.id),
hidden: Boolean(row.hidden),
badge: row.badge,
jobDescription: row.jobDescription,
File diff suppressed because it is too large. Load diff
@@ -99,14 +99,14 @@ describe("People production mutation boundary", () => {
expect(result).toMatchObject({
ok: true,
data: {
before: { id: 7, username: "Alice", online: true },
after: { id: 7, alertDelivered: true },
before: null,
after: { userId: 7, alertDelivered: true },
},
});
expect(audit).toHaveBeenCalledWith(
expect.objectContaining({
action: "people.user.alert",
before: expect.objectContaining({ id: 7 }),
before: undefined,
after: expect.objectContaining({ alertDelivered: true }),
correlationId: "production-alert",
outcome: "success",
@@ -10,6 +10,7 @@ const {
selectLimit,
transaction,
txSelectLimit,
txSelectOrderBy,
} = vi.hoisted(() => ({
audit: vi.fn(),
disconnectUser: vi.fn(),
@@ -18,6 +19,7 @@ const {
selectLimit: vi.fn(),
transaction: vi.fn(),
txSelectLimit: vi.fn(),
txSelectOrderBy: vi.fn(),
}));
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
@@ -30,7 +32,7 @@ vi.mock("@/lib/db", async (importOriginal) => {
const tx = {
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({ limit: txSelectLimit })),
where: vi.fn(() => ({ orderBy: txSelectOrderBy })),
})),
})),
insert: vi.fn(() => ({ values: insertValues })),
@@ -87,7 +89,24 @@ beforeEach(() => {
online: "1",
},
]);
txSelectLimit.mockResolvedValue([]);
txSelectLimit
.mockResolvedValueOnce([
{
id: 88,
banExpire: 0,
banReason: "Permanent active ban",
type: "account",
},
])
.mockResolvedValue([
{
id: 89,
banExpire: Math.floor(Date.now() / 1000) + 24 * 3600,
banReason: "Repeated harassment",
type: "account",
},
]);
txSelectOrderBy.mockReturnValue({ limit: txSelectLimit });
insertValues.mockResolvedValue([{}]);
disconnectUser.mockResolvedValue(true);
audit.mockResolvedValue(undefined);
@@ -112,7 +131,12 @@ describe("People production reason audit", () => {
expect.objectContaining({
action: "people.user.ban",
outcome: "intent",
before: expect.objectContaining({ id: 7, banned: false }),
before: expect.objectContaining({
id: 7,
banned: true,
banExpire: 0,
reason: "Permanent active ban",
}),
after: expect.objectContaining({
id: 7,
banned: true,
@@ -121,6 +145,7 @@ describe("People production reason audit", () => {
}),
expect.anything(),
);
expect(txSelectOrderBy).toHaveBeenCalledTimes(2);
expect(audit.mock.calls[0]?.[0]?.before).not.toHaveProperty("mail");
expect(audit.mock.calls[0]?.[0]?.after).not.toHaveProperty("mail");
});
@@ -109,7 +109,7 @@ describe("People transactional database audit", () => {
expect(audit).toHaveBeenCalledWith(
expect.objectContaining({
outcome: "success",
before: expect.objectContaining({ id: 9 }),
before: expect.objectContaining({ id: "9" }),
after: undefined,
}),
expect.objectContaining({ insert: expect.any(Function) }),
File diff suppressed because it is too large. Load diff