@@ -0,0 +1,407 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The route files of the Next tree's replacement (M6b, M6c): the table in apps/web/ROUTES.md has a file for
|
||||
// every row, what reaches the browser imports no server code, and the loaders under an organization answer
|
||||
// every refusal with its status.
|
||||
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
||||
import { AppError } from "@conusai-kit/core"
|
||||
import { isNotFound, isRedirect } from "@tanstack/react-router"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const web = new URL("../apps/web/", import.meta.url)
|
||||
const routes = new URL("src/routes/", web)
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
pageContext: vi.fn(),
|
||||
holds: vi.fn(),
|
||||
requirePermission: vi.fn(),
|
||||
permissionsIn: vi.fn(),
|
||||
call: vi.fn(),
|
||||
requireSection: vi.fn(),
|
||||
membersLoad: vi.fn(),
|
||||
invitationsLoad: vi.fn(),
|
||||
setResponseHeader: vi.fn<(name: string, value: string) => void>(),
|
||||
nav: vi.fn((): unknown[] => []),
|
||||
request: vi.fn(() => new Request("http://app.test/")),
|
||||
singleOrganization: vi.fn(() => false),
|
||||
selected: new Set(["documents", "knowledge"]),
|
||||
}))
|
||||
|
||||
vi.mock("@tanstack/react-start/server", () => ({
|
||||
getRequest: () => mocks.request(),
|
||||
setResponseStatus: vi.fn(),
|
||||
setResponseHeader: mocks.setResponseHeader,
|
||||
}))
|
||||
vi.mock("../apps/web/src/server/page", () => ({
|
||||
holds: mocks.holds,
|
||||
pageContext: mocks.pageContext,
|
||||
permissionsIn: mocks.permissionsIn,
|
||||
requirePermission: mocks.requirePermission,
|
||||
}))
|
||||
vi.mock("../apps/web/src/server/app", () => ({
|
||||
webRuntime: () => ({
|
||||
env: { deployment: {} },
|
||||
observe: { log: { warn: vi.fn() } },
|
||||
registry: { nav: mocks.nav() },
|
||||
singleOrganization: mocks.singleOrganization(),
|
||||
}),
|
||||
}))
|
||||
vi.mock("../apps/web/src/server/project-entries", () => ({
|
||||
PROJECT_ENTRIES: [{ href: "archive", label: "archive.title", module: "archive" }],
|
||||
PROJECT_MODULE_KEYS: ["archive"],
|
||||
}))
|
||||
vi.mock("@app/config", () => ({ PROFILE: "postgres" }))
|
||||
vi.mock("@app/modules", () => ({
|
||||
moduleSelected: (id: string) => mocks.selected.has(id),
|
||||
moduleAdopted: (id: string) => mocks.selected.has(id),
|
||||
appModules: [
|
||||
{
|
||||
id: "tenancy",
|
||||
version: "1.0.0",
|
||||
procedures: [{ name: "a function that must not reach the browser" }],
|
||||
ui: {
|
||||
nav: [
|
||||
{ scope: "org", label: "tenancy.nav.members", href: "members" },
|
||||
{ scope: "org", label: "tenancy.nav.settings", href: "settings", permission: "tenancy.org.manage" },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ id: "worker-only", version: "1.0.0" },
|
||||
],
|
||||
}))
|
||||
vi.mock("@app/modules/branding", () => ({
|
||||
brandGet: "brand",
|
||||
logoUrl: (path: string, size: string) => `${path}?s=${size}`,
|
||||
tenantLogo: (brand: { logoLight: string | null }) => (brand.logoLight ? { path: brand.logoLight } : null),
|
||||
tenantStyle: () => ({ style: { "--tenant": "1" } }),
|
||||
}))
|
||||
vi.mock("@app/modules/branding/ui", () => ({ brandFormLabels: () => ({}) }))
|
||||
// kit:documents:begin
|
||||
vi.mock("@app/modules/documents", () => ({
|
||||
documentGet: "documentGet",
|
||||
documentList: "documentList",
|
||||
READABLE_CONTENT_TYPES: ["application/pdf"],
|
||||
}))
|
||||
vi.mock("@app/modules/documents/ui", () => ({
|
||||
exportLabels: () => ({}),
|
||||
listLabels: () => ({}),
|
||||
reviewLabels: () => ({}),
|
||||
uploadLabels: () => ({}),
|
||||
}))
|
||||
// kit:documents:end
|
||||
vi.mock("@app/modules/knowledge", () => ({ settingsGet: "settingsGet" })) // kit:knowledge
|
||||
vi.mock("@conusai-kit/brand", () => ({ tokens: { color: { dark: { accent: "#fff" } } } }))
|
||||
vi.mock("@conusai-kit/host/http", () => ({ isOwnedBy: () => true }))
|
||||
vi.mock("@conusai-kit/tenancy", () => ({
|
||||
brandingSchema: { safeParse: (value: unknown) => ({ success: true, data: value ?? {} }) },
|
||||
meGet: "me",
|
||||
ORG_ROLES: ["viewer", "owner"],
|
||||
orgGet: "org",
|
||||
projectGet: "project",
|
||||
projectList: "projects",
|
||||
roleList: "roles",
|
||||
workspaceList: "workspaces",
|
||||
}))
|
||||
vi.mock("@conusai-kit/tenancy/ui", () => ({
|
||||
accessAuditPage: { load: vi.fn() },
|
||||
builtInRoleName: (_i18n: unknown, role: string) => `kit:${role}`,
|
||||
collectAccessEvents: vi.fn(),
|
||||
invitationsPage: { load: mocks.invitationsLoad },
|
||||
membersPage: { load: mocks.membersLoad },
|
||||
requireSection: mocks.requireSection,
|
||||
roleName: (_i18n: unknown, role: { name: string }) => `kit:${role.name}`,
|
||||
rolePage: { load: vi.fn() },
|
||||
rolesPage: { load: vi.fn() },
|
||||
}))
|
||||
vi.mock("@conusai-sdk/node-sdk", () => ({ ConusAIClient: class {} }))
|
||||
|
||||
const {
|
||||
documentsData, // kit:documents
|
||||
invitationsData,
|
||||
membersData,
|
||||
orgHomeData,
|
||||
orgLayoutData,
|
||||
requireAccessSection,
|
||||
requireWorkspace,
|
||||
} = await import("../apps/web/src/server/org-pages")
|
||||
|
||||
const i18n = { message: (key: string) => key, date: () => "2 Nov 2026", locale: "en" }
|
||||
const page = (signedIn = true) => ({ i18n, caller: { call: mocks.call }, signedIn, path: "/acme" })
|
||||
const refusal = (status: number): AppError =>
|
||||
new AppError({ type: "/problems/refused", code: "refused", title: "Refused", status })
|
||||
const thrown = (run: Promise<unknown>): Promise<unknown> =>
|
||||
run.then(
|
||||
() => {
|
||||
throw new Error("expected a refusal")
|
||||
},
|
||||
(error: unknown) => error,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.selected = new Set(["documents", "knowledge"])
|
||||
mocks.pageContext.mockResolvedValue(page())
|
||||
mocks.holds.mockResolvedValue(true)
|
||||
mocks.requireSection.mockResolvedValue(undefined)
|
||||
mocks.permissionsIn.mockResolvedValue({ permissions: ["tenancy.audit.read"] })
|
||||
})
|
||||
|
||||
describe("the route files", () => {
|
||||
const table = readFileSync(new URL("ROUTES.md", web), "utf8")
|
||||
const files = readdirSync(routes)
|
||||
|
||||
it("have nothing left of the Next tree", () => {
|
||||
expect(existsSync(new URL("app", web))).toBe(false)
|
||||
expect(table).not.toContain("to port")
|
||||
})
|
||||
|
||||
it("exist for every row of the table", () => {
|
||||
const named = [...table.matchAll(/\| `([^`|]+\.tsx?)`[^|]*\| done/g)].map((row) => row[1] as string)
|
||||
expect(named.length).toBeGreaterThan(30)
|
||||
// `kit template:prune` removes these routes, and ROUTES.md stays the whole record of the port.
|
||||
const pruned = new Set([
|
||||
"auth.sign-up.tsx",
|
||||
"$org.$workspace.$project.documents.index.tsx",
|
||||
"$org.$workspace.$project.documents.$fileId.tsx",
|
||||
])
|
||||
expect(named.filter((file) => !files.includes(file) && !pruned.has(file))).toEqual([])
|
||||
})
|
||||
|
||||
it.each(files.filter((file) => file !== "__root.tsx"))("%s keeps server code on the server", (file) => {
|
||||
const source = readFileSync(new URL(file.replaceAll("$", "%24"), routes), "utf8")
|
||||
// Only a type may be imported statically; the handlers import the file on the server.
|
||||
expect(source).not.toMatch(/^import \{[^}]*\} from "\.\.\/server\//m)
|
||||
expect(source).not.toMatch(/^import (?!type ).*"@app\/(config|modules)"$/m)
|
||||
expect(source).not.toMatch(/\bexport const (dynamic|runtime)\b/)
|
||||
expect(source).not.toMatch(/from "next/)
|
||||
})
|
||||
|
||||
it("reach the browser with no root of the app's own packages", () => {
|
||||
// Every file a route reaches by a static value import: what the browser's bundle starts from.
|
||||
const seen = new Set<string>()
|
||||
const walk = (url: URL): void => {
|
||||
if (seen.has(url.href)) return
|
||||
seen.add(url.href)
|
||||
const source = readFileSync(url, "utf8")
|
||||
for (const [, name] of source.matchAll(/^(?:import|export) (?!type )[^"]*"([^"]+)"/gm)) {
|
||||
const label = `${url.pathname.split("/apps/web/")[1]} imports ${name}`
|
||||
expect(label).not.toMatch(/imports @app\/(config|modules)$/)
|
||||
expect(label).not.toMatch(/imports (\.\.?\/)+server\//)
|
||||
if (!name?.startsWith(".") || name.includes("?")) continue
|
||||
const target = [".ts", ".tsx"].map((ext) => new URL(`${name}${ext}`, url)).find((file) => existsSync(file))
|
||||
if (target) walk(target)
|
||||
}
|
||||
}
|
||||
for (const file of files.filter((name) => name !== "__root.tsx")) walk(new URL(file.replaceAll("$", "%24"), routes))
|
||||
expect(seen.size).toBeGreaterThan(files.length)
|
||||
})
|
||||
|
||||
it("leave the guard free of the page loaders", () => {
|
||||
const guard = readFileSync(new URL("src/server/guard.ts", web), "utf8")
|
||||
expect(guard).not.toMatch(/from "\.\/page"/)
|
||||
})
|
||||
})
|
||||
|
||||
describe("the organization route's loader", () => {
|
||||
const organization = { name: "Acme", branding: { displayName: "Acme Co", logoLight: "/acme/brand/logo/light" } }
|
||||
const answers = (procedure: unknown): unknown =>
|
||||
procedure === "org"
|
||||
? organization
|
||||
: {
|
||||
userId: "u1",
|
||||
displayName: "Ada",
|
||||
email: "ada@example.test",
|
||||
organizations: [{ slug: "acme", name: "Acme" }],
|
||||
workspaces: [],
|
||||
}
|
||||
|
||||
it("sends a visitor to sign in", async () => {
|
||||
mocks.pageContext.mockResolvedValue(page(false))
|
||||
expect(isRedirect(await thrown(orgLayoutData({ org: "acme" })))).toBe(true)
|
||||
expect(mocks.call).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("answers a stranger and a missing organization with one 404", async () => {
|
||||
mocks.call.mockRejectedValue(refusal(404))
|
||||
expect(isNotFound(await thrown(orgLayoutData({ org: "acme" })))).toBe(true)
|
||||
expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", "404")
|
||||
})
|
||||
|
||||
it("hands the browser the navigation and nothing else of a manifest, with its labels translated", async () => {
|
||||
mocks.call.mockImplementation(async (procedure: unknown) => answers(procedure))
|
||||
const data = await orgLayoutData({ org: "acme" })
|
||||
// The organization's own tab first, as on stage, whatever order the manifest gives.
|
||||
expect(data.settings.sections).toEqual([
|
||||
{ group: "organization", label: "shell.general", href: "/acme/settings", permission: "tenancy.org.manage" },
|
||||
{ group: "organization", label: "tenancy.nav.members", href: "/acme/members" },
|
||||
{ group: "organization", label: "app.audit", href: "/acme/audit", permission: "tenancy.audit.read" },
|
||||
{ group: "personal", label: "account.link", href: "/acme/settings/account" },
|
||||
])
|
||||
expect(JSON.stringify(data)).not.toContain("a function that must not reach the browser")
|
||||
})
|
||||
|
||||
it("shows an organization's own page in the workspace the person was last in, else the default one", async () => {
|
||||
const workspaces = [
|
||||
{ slug: "general", name: "General", isDefault: true },
|
||||
{ slug: "research", name: "Research" },
|
||||
]
|
||||
mocks.call.mockImplementation(async (procedure: unknown) =>
|
||||
procedure === "workspaces" ? { workspaces } : answers(procedure),
|
||||
)
|
||||
const lastIn = async (cookie?: string) => {
|
||||
mocks.request.mockReturnValue(new Request("http://app.test/", cookie ? { headers: { cookie } } : {}))
|
||||
return (await orgLayoutData({ org: "acme" })).workspace?.workspace.id
|
||||
}
|
||||
expect(await lastIn("app_workspace=acme%2Fresearch")).toBe("research")
|
||||
expect(await lastIn()).toBe("general")
|
||||
// Another organization's workspace, and one the person no longer sees, are not this organization's.
|
||||
expect(await lastIn("app_workspace=northwind%2Fresearch")).toBe("general")
|
||||
expect(await lastIn("app_workspace=acme%2Fgone")).toBe("general")
|
||||
mocks.request.mockReturnValue(new Request("http://app.test/"))
|
||||
})
|
||||
|
||||
it("puts the audit in Settings, and opens Settings on the first tab the person may open", async () => {
|
||||
mocks.call.mockImplementation(async (procedure: unknown) => answers(procedure))
|
||||
mocks.permissionsIn.mockResolvedValue({ permissions: ["tenancy.audit.read"] })
|
||||
expect(await orgLayoutData({ org: "acme" })).toMatchObject({ settingsHref: "/acme/members" })
|
||||
expect(await orgLayoutData({ org: "acme" })).not.toHaveProperty("nav")
|
||||
})
|
||||
|
||||
it("offers a new workspace to whoever may make one, on a one-organization deployment too, and a new organization to whoever may found one", async () => {
|
||||
mocks.call.mockImplementation(async (procedure: unknown) => answers(procedure))
|
||||
mocks.permissionsIn.mockResolvedValue({ permissions: ["tenancy.workspace.manage"] })
|
||||
mocks.pageContext.mockResolvedValue({ ...page(), mayCreateOrganization: true })
|
||||
mocks.singleOrganization.mockReturnValue(true)
|
||||
expect(await orgLayoutData({ org: "acme" })).toMatchObject({
|
||||
workspaceCreateHref: "/acme/workspaces",
|
||||
organizationCreateHref: "/onboarding",
|
||||
})
|
||||
mocks.permissionsIn.mockResolvedValue({ permissions: [] })
|
||||
mocks.pageContext.mockResolvedValue({ ...page(), mayCreateOrganization: false })
|
||||
const member = await orgLayoutData({ org: "acme" })
|
||||
expect(member).not.toHaveProperty("workspaceCreateHref")
|
||||
expect(member).not.toHaveProperty("organizationCreateHref")
|
||||
mocks.pageContext.mockResolvedValue(page())
|
||||
mocks.permissionsIn.mockResolvedValue({ permissions: ["tenancy.audit.read"] })
|
||||
mocks.singleOrganization.mockReturnValue(false)
|
||||
})
|
||||
|
||||
it("carries the organization's brand and the scope's choices", async () => {
|
||||
mocks.call.mockImplementation(async (procedure: unknown) => answers(procedure))
|
||||
const data = await orgLayoutData({ org: "acme" })
|
||||
expect(data).toMatchObject({
|
||||
name: "Acme Co",
|
||||
style: { "--tenant": "1" },
|
||||
logoLight: { src: "/acme/brand/logo/light?s=1x" },
|
||||
organizations: [{ id: "acme", label: "Acme" }],
|
||||
client: { org: "acme", user: "u1", locale: "en" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("a page under an organization", () => {
|
||||
it("is 404 for a workspace the person cannot see", async () => {
|
||||
mocks.call.mockResolvedValue({ workspaces: [{ slug: "general", name: "General" }] })
|
||||
const { workspace, nav } = await requireWorkspace({ org: "acme", workspace: "general" })
|
||||
expect(workspace).toEqual({ id: "general", label: "General" })
|
||||
expect(nav.map((item) => item.href)).toEqual(["/acme/general"])
|
||||
expect(isNotFound(await thrown(requireWorkspace({ org: "acme", workspace: "restricted" })))).toBe(true)
|
||||
})
|
||||
|
||||
it("links the modules' pages of the workspace this person may open, and no other scope's", async () => {
|
||||
mocks.call.mockResolvedValue({ workspaces: [{ slug: "general", name: "General" }] })
|
||||
mocks.nav.mockReturnValue([
|
||||
{
|
||||
scope: "workspace",
|
||||
label: "reports.title",
|
||||
href: "reports",
|
||||
permission: "tenancy.audit.read",
|
||||
module: "reports",
|
||||
},
|
||||
{
|
||||
scope: "workspace",
|
||||
label: "budgets.title",
|
||||
href: "budgets",
|
||||
permission: "budgets.budget.read",
|
||||
module: "budgets",
|
||||
},
|
||||
{ scope: "project", label: "notes.title", href: "notes", module: "notes" },
|
||||
])
|
||||
const { nav } = await requireWorkspace({ org: "acme", workspace: "general" })
|
||||
expect(nav.map((item) => item.href)).toEqual(["/acme/general", "/acme/general/modules/reports"])
|
||||
mocks.nav.mockReturnValue([])
|
||||
})
|
||||
|
||||
it("makes a page the module placed in Settings a tab there, and leaves it out of the sidebar", async () => {
|
||||
mocks.call.mockResolvedValue({ workspaces: [{ slug: "general", name: "General" }] })
|
||||
mocks.nav.mockReturnValue([
|
||||
{ scope: "workspace", label: "reports.title", href: "reports", module: "reports" },
|
||||
{
|
||||
scope: "workspace",
|
||||
label: "reports.ai",
|
||||
href: "reports/settings/ai",
|
||||
placement: "settings",
|
||||
module: "reports",
|
||||
},
|
||||
])
|
||||
const { nav, settings } = await requireWorkspace({ org: "acme", workspace: "general" })
|
||||
expect(nav.map((item) => item.href)).toEqual(["/acme/general", "/acme/general/modules/reports"])
|
||||
expect(settings).toEqual([
|
||||
{ group: "workspace", label: "reports.ai", href: "/acme/general/modules/reports/settings/ai" },
|
||||
])
|
||||
mocks.nav.mockReturnValue([])
|
||||
})
|
||||
|
||||
it("is 404 in every loader at once, so whichever finishes first gives the same status", async () => {
|
||||
mocks.call.mockRejectedValue(refusal(404))
|
||||
expect(isNotFound(await thrown(orgHomeData({ org: "other" })))).toBe(true)
|
||||
expect(isNotFound(await thrown(documentsData({ org: "other", workspace: "w", project: "p" })))).toBe(true) // kit:documents
|
||||
})
|
||||
|
||||
// kit:documents:begin
|
||||
it("refuses a default-off Documents page before any data call", async () => {
|
||||
mocks.selected.clear()
|
||||
expect(isNotFound(await thrown(documentsData({ org: "acme", workspace: "w", project: "p" })))).toBe(true)
|
||||
expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", "404")
|
||||
expect(mocks.call).not.toHaveBeenCalled()
|
||||
})
|
||||
// kit:documents:end
|
||||
|
||||
// kit:documents:begin
|
||||
it("carries the documents as the live list's seed, with the locale the browser writes their dates in", async () => {
|
||||
const documents = [{ uploadedAt: "2026-11-02T00:00:00.000Z" }]
|
||||
mocks.call.mockResolvedValue({ documents })
|
||||
const data = await documentsData({ org: "acme", workspace: "general", project: "demo" })
|
||||
expect(data.documents).toEqual(documents)
|
||||
expect(data.locale).toBe(i18n.locale)
|
||||
expect(data.accept).toEqual(["application/pdf"])
|
||||
})
|
||||
// kit:documents:end
|
||||
})
|
||||
|
||||
describe("kit-tenancy's access pages", () => {
|
||||
it("are 403 for a member without the section's permission", async () => {
|
||||
mocks.requireSection.mockRejectedValue(refusal(403))
|
||||
const error = (await thrown(requireAccessSection({ org: "acme", section: "members" }))) as { data?: unknown }
|
||||
expect(error.data).toEqual({ forbidden: true })
|
||||
expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", "403")
|
||||
})
|
||||
|
||||
it("load at the level the path says", async () => {
|
||||
mocks.membersLoad.mockResolvedValue({})
|
||||
await membersData({ org: "acme" })
|
||||
await membersData({ org: "acme", workspace: "w" })
|
||||
await membersData({ org: "acme", workspace: "w", project: "p" })
|
||||
expect(mocks.membersLoad.mock.calls.map((call) => call[1])).toEqual([
|
||||
{ level: "org" },
|
||||
{ level: "workspace", workspace: "w" },
|
||||
{ level: "project", workspace: "w", project: "p" },
|
||||
])
|
||||
})
|
||||
|
||||
it("hand the invitations page its link path", async () => {
|
||||
mocks.invitationsLoad.mockResolvedValue({})
|
||||
await invitationsData({ org: "acme" })
|
||||
expect(mocks.invitationsLoad.mock.calls[0]?.[1]).toEqual({ linkPath: "/invitations/acme/" })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user