@@ -0,0 +1,193 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// What a route loader reads (F10). `pageContext` and `permissionsIn` are kept per request, because React's
|
||||
// `cache` memoizes nothing outside server components, and `modulePageData` is the one module route's
|
||||
// loader: signed out is a redirect, every refusal is a real status, and a module's data passes through.
|
||||
|
||||
import { AppError } from "@conusai-kit/core"
|
||||
import { isNotFound, isRedirect } from "@tanstack/react-router"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
requestContext: vi.fn(),
|
||||
callByName: vi.fn(),
|
||||
setResponseHeader: vi.fn<(name: string, value: string) => void>(),
|
||||
load: vi.fn(),
|
||||
call: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("@tanstack/react-start/server", () => ({
|
||||
getRequest: () => {
|
||||
throw new Error("the tests pass their request")
|
||||
},
|
||||
setResponseStatus: vi.fn(),
|
||||
setResponseHeader: mocks.setResponseHeader,
|
||||
}))
|
||||
vi.mock("../apps/web/src/server/request", () => ({ requestContext: mocks.requestContext }))
|
||||
vi.mock("../apps/web/src/server/app", () => ({
|
||||
webRuntime: () => ({
|
||||
observe: { config: { errors: false } },
|
||||
selfServiceOrganizations: "members",
|
||||
registry: {
|
||||
modules: [{ id: "notes" }, { id: "unpaged" }],
|
||||
nav: [{ module: "notes", scope: "project", href: "notes", label: "workspace.projects" }],
|
||||
},
|
||||
env: { deployment: {} },
|
||||
}),
|
||||
}))
|
||||
vi.mock("../apps/web/src/server/tenancy-calls", () => ({ mayCreateOrganization: () => false }))
|
||||
vi.mock("../apps/web/src/server/platform-theme", () => ({
|
||||
organizationThemeStyle: async () => undefined,
|
||||
organizationIdentity: async () => ({}),
|
||||
}))
|
||||
vi.mock("@conusai-kit/host/http", () => ({ csrfSessionId: () => "session", csrfToken: () => "token" }))
|
||||
vi.mock("@app/modules/pages", () => ({ modulePages: { notes: async () => ({ load: mocks.load }) } }))
|
||||
|
||||
const { modulePageData, pageContext, permissionsIn, projectLayoutData, shellData } = await import(
|
||||
"../apps/web/src/server/page"
|
||||
)
|
||||
|
||||
const person = { actor: { kind: "user" } }
|
||||
const signedIn = () =>
|
||||
mocks.requestContext.mockResolvedValue({
|
||||
caller: { call: mocks.call, callByName: mocks.callByName },
|
||||
container: {},
|
||||
principal: person,
|
||||
user: { email: "ada@example.test", displayName: "Ada", locale: "en" },
|
||||
})
|
||||
const signedOut = () =>
|
||||
mocks.requestContext.mockResolvedValue({
|
||||
caller: { call: mocks.call, callByName: mocks.callByName },
|
||||
container: {},
|
||||
principal: null,
|
||||
})
|
||||
const request = (): Request => new Request("http://app.test/a/b/c/notes", { headers: { "x-path": "/a/b/c/notes" } })
|
||||
const params = { org: "a", workspace: "b", project: "c", module: "notes" }
|
||||
const refusal = (status: number): AppError =>
|
||||
new AppError({ type: "/problems/refused", code: "refused", title: "Refused", status })
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.callByName.mockResolvedValue({ permissions: ["notes.note.read"], revision: "r1" })
|
||||
mocks.call.mockResolvedValue({})
|
||||
mocks.load.mockResolvedValue({ kind: "list", title: "Notes" })
|
||||
})
|
||||
|
||||
describe("the page context", () => {
|
||||
it("is built once per request, and again for the next one", async () => {
|
||||
signedIn()
|
||||
const first = request()
|
||||
await Promise.all([pageContext(first), pageContext(first)])
|
||||
expect(mocks.requestContext).toHaveBeenCalledOnce()
|
||||
await pageContext(request())
|
||||
expect(mocks.requestContext).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("reads the permissions of a scope once per request", async () => {
|
||||
signedIn()
|
||||
const current = request()
|
||||
await Promise.all([permissionsIn({ org: "a" }, current), permissionsIn({ org: "a" }, current)])
|
||||
expect(mocks.callByName).toHaveBeenCalledOnce()
|
||||
await permissionsIn({ org: "a", workspace: "b" }, current)
|
||||
expect(mocks.callByName).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("the shell's loader data", () => {
|
||||
it("is plain values with its labels translated, and nothing a browser must not hold", async () => {
|
||||
vi.stubEnv("CONUSAI_PUBLIC_URL", "https://app.example.test")
|
||||
signedIn()
|
||||
const shell = await shellData(request())
|
||||
expect(JSON.parse(JSON.stringify(shell))).toEqual(shell)
|
||||
expect(shell.signedIn).toBe(true)
|
||||
expect(shell.labels.signIn.length).toBeGreaterThan(0)
|
||||
expect(shell.brand.style).toBeTypeOf("object")
|
||||
expect(shell.publicUrl).toBe("https://app.example.test")
|
||||
expect(JSON.stringify(shell)).not.toContain("ada@example.test")
|
||||
})
|
||||
})
|
||||
|
||||
describe("the module route's loader", () => {
|
||||
const thrown = (run: Promise<unknown>): Promise<unknown> =>
|
||||
run.then(
|
||||
() => "resolved",
|
||||
(error: unknown) => error,
|
||||
)
|
||||
|
||||
it("sends a signed-out visitor to sign in, before reading anything", async () => {
|
||||
signedOut()
|
||||
expect(isRedirect(await thrown(modulePageData(params, request())))).toBe(true)
|
||||
expect(mocks.call).not.toHaveBeenCalled()
|
||||
expect(mocks.load).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
["no registered module", { ...params, module: "nothing" }],
|
||||
["a registered module with no page", { ...params, module: "unpaged" }],
|
||||
["a name every object has", { ...params, module: "constructor" }],
|
||||
])("answers 404 for %s", async (_name, wanted) => {
|
||||
signedIn()
|
||||
expect(isNotFound(await thrown(modulePageData(wanted, request())))).toBe(true)
|
||||
expect(mocks.load).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
[404, undefined],
|
||||
[403, { forbidden: true }],
|
||||
])("turns the module's own %i into that page", async (status, data) => {
|
||||
signedIn()
|
||||
mocks.load.mockRejectedValue(refusal(status))
|
||||
const error = (await thrown(modulePageData(params, request()))) as { data?: unknown }
|
||||
expect(isNotFound(error)).toBe(true)
|
||||
expect(error.data).toEqual(data)
|
||||
expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", String(status))
|
||||
})
|
||||
|
||||
it("leaves a fault to the error boundary", async () => {
|
||||
signedIn()
|
||||
const fault = new Error("database down")
|
||||
mocks.load.mockRejectedValue(fault)
|
||||
expect(await thrown(modulePageData(params, request()))).toBe(fault)
|
||||
})
|
||||
|
||||
it("runs the module's loader with the slugs and the rest of the path", async () => {
|
||||
signedIn()
|
||||
const page = await modulePageData({ ...params, _splat: "note-1/" }, request())
|
||||
expect(mocks.load.mock.calls[0]?.[0]).toMatchObject({
|
||||
slugs: { org: "a", workspace: "b", project: "c" },
|
||||
segments: ["note-1"],
|
||||
})
|
||||
expect(page).toMatchObject({
|
||||
module: "notes",
|
||||
title: "Projects · Example App",
|
||||
segments: ["note-1"],
|
||||
data: { kind: "list", title: "Notes" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("the project route's loader", () => {
|
||||
const slugs = { org: "a", workspace: "b", project: "c" }
|
||||
const refused = (run: Promise<unknown>): Promise<unknown> => run.catch((error: unknown) => error)
|
||||
|
||||
it("sends a signed-out visitor to sign in, before it reads anything", async () => {
|
||||
signedOut()
|
||||
expect(isRedirect(await refused(projectLayoutData(slugs, request())))).toBe(true)
|
||||
expect(mocks.call).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("answers 404 for a project of another tenant", async () => {
|
||||
signedIn()
|
||||
mocks.call.mockRejectedValue(refusal(404))
|
||||
expect(isNotFound(await refused(projectLayoutData(slugs, request())))).toBe(true)
|
||||
expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", "404")
|
||||
})
|
||||
|
||||
it("returns the project's scope and the person's permissions there, the seed of useCan", async () => {
|
||||
signedIn()
|
||||
expect(await projectLayoutData(slugs, request())).toMatchObject({
|
||||
scope: slugs,
|
||||
seed: expect.objectContaining({ permissions: ["notes.note.read"] }),
|
||||
project: { id: slugs.project },
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user