feat(housekeeping): deliver people account workflows
This commit is contained in:
1 parent
7920d4f46c
commit
e1b31ff773
45 files changed
+4290
-1243
No files matched your search
@@ -0,0 +1,316 @@
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PERMS } from "@/lib/permission-slugs";
|
||||
import {
|
||||
fail,
|
||||
type HousekeepingCapabilityContext,
|
||||
type HousekeepingResult,
|
||||
ok,
|
||||
} from "../../../foundation/contracts";
|
||||
import { HOUSEKEEPING_MANIFESTS } from "../../../manifests";
|
||||
import { HOUSEKEEPING_ROUTE_HANDLERS } from "../../../route-handlers";
|
||||
import { peopleManifest } from "../manifest";
|
||||
import type { PeopleCommunityQueryData } from "../queries/community";
|
||||
import type { PeopleStaffQueryData } from "../queries/staff";
|
||||
import type { PeopleUsersQueryData } from "../queries/users";
|
||||
import {
|
||||
PEOPLE_PRIMARY_ROUTE_HANDLERS,
|
||||
PEOPLE_PRIMARY_ROUTE_IDS,
|
||||
} from "../route-handlers";
|
||||
import { PeopleCommunityPage } from "./community";
|
||||
import { PeopleMultiAccountsPage } from "./multi-accounts";
|
||||
import { PeopleStaffPage } from "./staff";
|
||||
import { PeopleUserDetailPage } from "./user-detail";
|
||||
import { PeopleUserEditPage } from "./user-edit";
|
||||
import { PeopleUsersPage } from "./users";
|
||||
|
||||
const correlationId = "people-pages-test";
|
||||
|
||||
function capabilityContext(
|
||||
granted: readonly string[],
|
||||
): HousekeepingCapabilityContext {
|
||||
const permissions = new Set(granted);
|
||||
return {
|
||||
actor: { id: 42, username: "operator", rank: 6 },
|
||||
isSuperAdmin: false,
|
||||
has: (slug) => permissions.has(slug),
|
||||
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
|
||||
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
|
||||
};
|
||||
}
|
||||
|
||||
const readContext = capabilityContext([PERMS.USERS_VIEW]);
|
||||
const modReadContext = capabilityContext([PERMS.MOD_USERS_VIEW]);
|
||||
|
||||
const user = {
|
||||
id: 7,
|
||||
username: "Alice",
|
||||
rank: 2,
|
||||
online: true,
|
||||
mail: "[email protected]",
|
||||
ipCurrent: "198.51.100.7",
|
||||
bannedUntil: null,
|
||||
href: "/ase/people/users/7" as const,
|
||||
};
|
||||
|
||||
const detail = {
|
||||
...user,
|
||||
motto: "Ready",
|
||||
look: "hd-180-1",
|
||||
accountCreated: "2026-08-01T00:00:00.000Z",
|
||||
lastLogin: "2026-08-28T00:00:00.000Z",
|
||||
sanctions: [],
|
||||
watched: false,
|
||||
permission: { rankName: "Member", ranks: [{ id: 2, name: "Member" }] },
|
||||
};
|
||||
|
||||
type PageCase = {
|
||||
readonly name: string;
|
||||
readonly render: (
|
||||
result?: HousekeepingResult<unknown>,
|
||||
partialDependencies?: readonly string[],
|
||||
) => string;
|
||||
readonly empty: unknown;
|
||||
readonly ready: unknown;
|
||||
};
|
||||
|
||||
const cases: readonly PageCase[] = [
|
||||
{
|
||||
name: "users",
|
||||
render: (result, partialDependencies) =>
|
||||
renderToStaticMarkup(
|
||||
<PeopleUsersPage
|
||||
context={readContext}
|
||||
result={result as HousekeepingResult<PeopleUsersQueryData>}
|
||||
partialDependencies={partialDependencies}
|
||||
/>,
|
||||
),
|
||||
empty: {
|
||||
kind: "users",
|
||||
page: { items: [], total: 0, pageSize: 20, offset: 0 },
|
||||
},
|
||||
ready: {
|
||||
kind: "users",
|
||||
page: { items: [user], total: 1, pageSize: 20, offset: 0 },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user-detail",
|
||||
render: (result, partialDependencies) =>
|
||||
renderToStaticMarkup(
|
||||
<PeopleUserDetailPage
|
||||
context={readContext}
|
||||
result={result as HousekeepingResult<PeopleUsersQueryData>}
|
||||
partialDependencies={partialDependencies}
|
||||
/>,
|
||||
),
|
||||
empty: { kind: "user", user: { ...detail, sanctions: [] } },
|
||||
ready: {
|
||||
kind: "user",
|
||||
user: {
|
||||
...detail,
|
||||
sanctions: [
|
||||
{
|
||||
id: 3,
|
||||
kind: "account",
|
||||
reason: "spam",
|
||||
createdAt: null,
|
||||
expiresAt: 0,
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user-edit",
|
||||
render: (result, partialDependencies) =>
|
||||
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),
|
||||
ready: { kind: "user", user: { ...detail, watched: true } },
|
||||
},
|
||||
{
|
||||
name: "multi-accounts",
|
||||
render: (result, partialDependencies) =>
|
||||
renderToStaticMarkup(
|
||||
<PeopleMultiAccountsPage
|
||||
context={readContext}
|
||||
result={result as HousekeepingResult<PeopleUsersQueryData>}
|
||||
partialDependencies={partialDependencies}
|
||||
/>,
|
||||
),
|
||||
empty: {
|
||||
kind: "multi-accounts",
|
||||
page: { items: [], total: 0, pageSize: 20, offset: 0 },
|
||||
},
|
||||
ready: {
|
||||
kind: "multi-accounts",
|
||||
page: {
|
||||
items: [{ key: "198.51.100.7", accountCount: 2, accounts: [user] }],
|
||||
total: 1,
|
||||
pageSize: 20,
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "community",
|
||||
render: (result, partialDependencies) =>
|
||||
renderToStaticMarkup(
|
||||
<PeopleCommunityPage
|
||||
context={readContext}
|
||||
result={result as HousekeepingResult<PeopleCommunityQueryData>}
|
||||
partialDependencies={partialDependencies}
|
||||
/>,
|
||||
),
|
||||
empty: {
|
||||
kind: "guilds",
|
||||
page: { items: [], total: 0, pageSize: 20, offset: 0 },
|
||||
},
|
||||
ready: {
|
||||
kind: "guilds",
|
||||
page: {
|
||||
items: [
|
||||
{
|
||||
id: 9,
|
||||
name: "Builders",
|
||||
description: "Rooms",
|
||||
userId: 7,
|
||||
ownerUsername: "Alice",
|
||||
roomId: 4,
|
||||
memberCount: 2,
|
||||
createdAt: null,
|
||||
href: "/ase/people/community/guilds/9",
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
pageSize: 20,
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "staff",
|
||||
render: (result, partialDependencies) =>
|
||||
renderToStaticMarkup(
|
||||
<PeopleStaffPage
|
||||
context={readContext}
|
||||
result={result as HousekeepingResult<PeopleStaffQueryData>}
|
||||
partialDependencies={partialDependencies}
|
||||
/>,
|
||||
),
|
||||
empty: {
|
||||
kind: "applications",
|
||||
page: { items: [], total: 0, pageSize: 20, offset: 0 },
|
||||
},
|
||||
ready: {
|
||||
kind: "applications",
|
||||
page: {
|
||||
items: [
|
||||
{
|
||||
id: 5,
|
||||
userId: 7,
|
||||
username: "Alice",
|
||||
rankId: 4,
|
||||
content: "Experienced",
|
||||
createdAt: null,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
pageSize: 20,
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe.each(cases)("People $name page", ({ render, empty, ready }) => {
|
||||
it("renders loading, forbidden, dependency error, empty, partial, and ready states", () => {
|
||||
expect(render()).toContain('data-housekeeping-state="loading"');
|
||||
expect(
|
||||
render(fail("FORBIDDEN", "errors.housekeeping.forbidden", correlationId)),
|
||||
).toContain('data-housekeeping-state="forbidden"');
|
||||
expect(
|
||||
render(
|
||||
fail(
|
||||
"DEPENDENCY_UNAVAILABLE",
|
||||
"errors.housekeeping.dependencyUnavailable",
|
||||
correlationId,
|
||||
),
|
||||
),
|
||||
).toContain('data-housekeeping-state="error"');
|
||||
const emptyResult =
|
||||
typeof empty === "object" && empty !== null && "ok" in empty
|
||||
? (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"',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("People primary route registration", () => {
|
||||
it("registers exactly the nine real primary routes and handlers", () => {
|
||||
const expected = [
|
||||
"people.users.list",
|
||||
"people.users.edit",
|
||||
"people.users.multi-accounts",
|
||||
"people.users.detail",
|
||||
"people.community.online",
|
||||
"people.community.guilds",
|
||||
"people.community.guild-detail",
|
||||
"people.staff.applications",
|
||||
"people.staff.teams",
|
||||
];
|
||||
expect(PEOPLE_PRIMARY_ROUTE_IDS).toEqual(expected);
|
||||
expect(peopleManifest.routes.map((route) => route.id)).toEqual(expected);
|
||||
expect(
|
||||
PEOPLE_PRIMARY_ROUTE_HANDLERS.map((handler) => handler.routeId),
|
||||
).toEqual(expected);
|
||||
expect(
|
||||
HOUSEKEEPING_ROUTE_HANDLERS.map((handler) => handler.routeId),
|
||||
).toEqual(expect.arrayContaining(expected));
|
||||
expect(
|
||||
HOUSEKEEPING_MANIFESTS.flatMap((manifest) => manifest.routes).map(
|
||||
(route) => route.id,
|
||||
),
|
||||
).toEqual(HOUSEKEEPING_ROUTE_HANDLERS.map((handler) => handler.routeId));
|
||||
});
|
||||
|
||||
it("uses canonical People links and exposes PII and actions only with exact capabilities", () => {
|
||||
const redacted = {
|
||||
...detail,
|
||||
mail: null,
|
||||
ipCurrent: null,
|
||||
};
|
||||
const modHtml = renderToStaticMarkup(
|
||||
<PeopleUserDetailPage
|
||||
context={modReadContext}
|
||||
result={ok({ kind: "user", user: redacted }, correlationId)}
|
||||
/>,
|
||||
);
|
||||
expect(modHtml).not.toContain("[email protected]");
|
||||
expect(modHtml).not.toContain("198.51.100.7");
|
||||
expect(modHtml).not.toContain("people.user.update");
|
||||
|
||||
const editorHtml = renderToStaticMarkup(
|
||||
<PeopleUserDetailPage
|
||||
context={capabilityContext([PERMS.USERS_VIEW, PERMS.USERS_EDIT])}
|
||||
result={ok({ kind: "user", user: detail }, correlationId)}
|
||||
/>,
|
||||
);
|
||||
expect(editorHtml).toContain('href="/ase/people/users/7/edit"');
|
||||
expect(editorHtml).toContain("people.user.update");
|
||||
expect(editorHtml).not.toContain("/admin");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user