fix(housekeeping): harden people account workflows
This commit is contained in:
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"');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user