317 lines
8.4 KiB
TypeScript
317 lines
8.4 KiB
TypeScript
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");
|
|
});
|
|
});
|