fix(housekeeping): harden people account workflows

This commit is contained in:
Simo committed 2026-08-29 13:13:04 +02:00
1 parent e1b31ff773
commit 25b76437ff
41 files changed
+2657 -838

No files matched your search

@@ -1,6 +1,12 @@
import { readFileSync } from "node:fs";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
vi.mock("@/actions/housekeeping-command", () => ({
executeHousekeepingCommand: vi.fn(),
}));
import {
fail,
type HousekeepingCapabilityContext,
@@ -19,6 +25,7 @@ import {
} from "../route-handlers";
import { PeopleCommunityPage } from "./community";
import { PeopleMultiAccountsPage } from "./multi-accounts";
import { parsePeopleListInput } from "./page-state";
import { PeopleStaffPage } from "./staff";
import { PeopleUserDetailPage } from "./user-detail";
import { PeopleUserEditPage } from "./user-edit";
@@ -77,12 +84,11 @@ type PageCase = {
const cases: readonly PageCase[] = [
{
name: "users",
render: (result, partialDependencies) =>
render: (result) =>
renderToStaticMarkup(
<PeopleUsersPage
context={readContext}
result={result as HousekeepingResult<PeopleUsersQueryData>}
partialDependencies={partialDependencies}
/>,
),
empty: {
@@ -96,12 +102,11 @@ const cases: readonly PageCase[] = [
},
{
name: "user-detail",
render: (result, partialDependencies) =>
render: (result) =>
renderToStaticMarkup(
<PeopleUserDetailPage
context={readContext}
result={result as HousekeepingResult<PeopleUsersQueryData>}
partialDependencies={partialDependencies}
/>,
),
empty: { kind: "user", user: { ...detail, sanctions: [] } },
@@ -124,12 +129,11 @@ const cases: readonly PageCase[] = [
},
{
name: "user-edit",
render: (result, partialDependencies) =>
render: (result) =>
renderToStaticMarkup(
<PeopleUserEditPage
context={capabilityContext([PERMS.USERS_EDIT, PERMS.USERS_VIEW])}
result={result as HousekeepingResult<PeopleUsersQueryData>}
partialDependencies={partialDependencies}
/>,
),
empty: fail("NOT_FOUND", "errors.housekeeping.notFound", correlationId),
@@ -137,12 +141,11 @@ const cases: readonly PageCase[] = [
},
{
name: "multi-accounts",
render: (result, partialDependencies) =>
render: (result) =>
renderToStaticMarkup(
<PeopleMultiAccountsPage
context={readContext}
result={result as HousekeepingResult<PeopleUsersQueryData>}
partialDependencies={partialDependencies}
/>,
),
empty: {
@@ -161,12 +164,11 @@ const cases: readonly PageCase[] = [
},
{
name: "community",
render: (result, partialDependencies) =>
render: (result) =>
renderToStaticMarkup(
<PeopleCommunityPage
context={readContext}
result={result as HousekeepingResult<PeopleCommunityQueryData>}
partialDependencies={partialDependencies}
/>,
),
empty: {
@@ -197,12 +199,11 @@ const cases: readonly PageCase[] = [
},
{
name: "staff",
render: (result, partialDependencies) =>
render: (result) =>
renderToStaticMarkup(
<PeopleStaffPage
context={readContext}
result={result as HousekeepingResult<PeopleStaffQueryData>}
partialDependencies={partialDependencies}
/>,
),
empty: {
@@ -231,7 +232,7 @@ const cases: readonly PageCase[] = [
];
describe.each(cases)("People $name page", ({ render, empty, ready }) => {
it("renders loading, forbidden, dependency error, empty, partial, and ready states", () => {
it("renders loading, forbidden, dependency error, empty, and ready states", () => {
expect(render()).toContain('data-housekeeping-state="loading"');
expect(
render(fail("FORBIDDEN", "errors.housekeeping.forbidden", correlationId)),
@@ -250,9 +251,6 @@ describe.each(cases)("People $name page", ({ render, empty, ready }) => {
? (empty as HousekeepingResult<unknown>)
: ok(empty, correlationId);
expect(render(emptyResult)).toContain('data-housekeeping-state="empty"');
expect(render(ok(ready, correlationId), ["secondary"])).toContain(
'data-housekeeping-state="partial"',
);
expect(render(ok(ready, correlationId))).toContain(
'data-housekeeping-state="ready"',
);
@@ -310,7 +308,59 @@ describe("People primary route registration", () => {
/>,
);
expect(editorHtml).toContain('href="/ase/people/users/7/edit"');
expect(editorHtml).toContain("people.user.update");
expect(editorHtml).toContain("<form");
expect(editorHtml).not.toContain("<span data-housekeeping-command");
expect(editorHtml).not.toContain("/admin");
const editHtml = renderToStaticMarkup(
<PeopleUserEditPage
context={capabilityContext([PERMS.USERS_VIEW, PERMS.USERS_EDIT])}
result={ok({ kind: "user", user: detail }, correlationId)}
/>,
);
expect(editHtml).toContain("people.user.update");
expect(editHtml).toContain("<form");
});
});
describe("People URL list input", () => {
it("bounds and normalizes search, page, pageSize, sort, and direction", () => {
expect(
parsePeopleListInput({
search: [" Alice ", "ignored"],
page: "999999999999",
pageSize: "5000",
sort: "username",
direction: "desc",
}),
).toEqual({
search: "Alice",
pageSize: 100,
offset: 100000,
sort: "username",
order: "desc",
});
});
});
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",
);
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");
});
it("provides a real Next loading boundary", () => {
const source = readFileSync(
"src/app/ase-next/[domain]/[[...segments]]/loading.tsx",
"utf8",
);
expect(source).toContain('data-housekeeping-state="loading"');
expect(source).toContain('state="loading"');
});
});