// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The shell in a browser (plan item AT4.4): sign-in and its error states, the invitation link in every // state it can be in, the organization switcher, the workspace and project pages, the 403 and 404 // pages (and that a stranger cannot tell a missing organization from one they are not in), the // language and theme controls, and the demo data `pnpm dev` writes. Each screen passes axe, in // English, and the ones that carry words of their own also in Lithuanian. import { type Browser, type BrowserContext, expect, type Page, test } from "@playwright/test" import { call, makeOrganization, platformState, signIn, unique, violations } from "./support/app" const base = `http://127.0.0.1:${process.env.E2E_WEB_PORT ?? 3100}` /** A browser of its own, so two people can be signed in at once. */ async function person(browser: Browser, email: string): Promise<{ page: Page; context: BrowserContext }> { const context = await browser.newContext({ baseURL: base }) const page = await context.newPage() await signIn(page, email) return { page, context } } /** The alerts of the page's own content: Next.js keeps a `role="alert"` route announcer of its own in the document. */ const alerts = (page: Page) => page.locator("main [role=alert], p.app-error[role=alert]") const mainText = (page: Page): Promise => page.locator("main").innerText() test("the sign-in page offers the way in, words each failure in the person's language, and passes axe", async ({ page, }) => { await page.goto("/auth/sign-in") await expect(page.getByRole("heading", { level: 1, name: "Sign in" })).toBeVisible() await expect(alerts(page)).toHaveCount(0) expect(await violations(page)).toEqual([]) for (const [code, words] of [ ["denied", /The identity provider did not let you in/], ["noTransaction", /was not started in this browser/], ["sessionExpired", /Your session has ended/], ["provider", /could not complete the sign-in/], ["failed", /Signing in did not work/], ] as const) { await page.goto(`/auth/sign-in?error=${code}`) await expect(alerts(page)).toContainText(words) } expect(await violations(page)).toEqual([]) // Only the five words are ever shown: a code from a URL is not text to print. await page.goto("/auth/sign-in?error=%3Cscript%3Ealert(1)%3C/script%3E") await expect(alerts(page)).toHaveCount(0) // In Lithuanian, with the same structure. await page.goto("/auth/sign-in?error=denied") await page.getByLabel("Language").selectOption("lt") await expect(page.getByRole("heading", { level: 1, name: "Prisijungimas" })).toBeVisible() await expect(alerts(page)).toContainText("Tapatybės teikėjas jūsų neįleido") expect(await violations(page)).toEqual([]) }) test("a failed sign-in ends at the sign-in page with a reason, never at a 500", async ({ page }) => { // The provider says no. const denied = await page.request.get("/auth/callback?error=access_denied&error_description=secret+words", { maxRedirects: 0, }) expect(denied.status()).toBe(302) expect(denied.headers().location).toBe("/auth/sign-in?error=denied") // A callback no sign-in in this browser started. const orphan = await page.request.get("/auth/callback?code=abc&state=xyz", { maxRedirects: 0 }) expect(orphan.status()).toBe(302) // (this stack has no identity provider, so there is nothing a callback could complete) expect(orphan.headers().location).toBe("/auth/sign-in?error=failed") // An address the dev sign-in will not take. const bad = await page.request.get("/auth/dev-login?email=nobody", { maxRedirects: 0 }) expect(bad.status()).toBe(302) expect(bad.headers().location).toBe("/auth/sign-in?error=failed") }) test("an address that needs a person is sent to sign in and brought back, whoever it names", async ({ page }) => { await page.goto("/acme-review/general") await expect(page).toHaveURL(/\/auth\/sign-in\?return_to=%2Facme-review%2Fgeneral$/) await page.getByLabel("E-mail address").fill("ada@example.test") await page.getByRole("button", { name: "Sign in" }).click() await expect(page).toHaveURL(/\/acme-review\/general$/) await expect(page.getByRole("heading", { level: 1, name: "Workspace General" })).toBeVisible() // A return address outside this app is not followed. const away = await page.request.get("/auth/dev-login?email=ada@example.test&return_to=https://evil.example/", { maxRedirects: 0, }) expect(away.headers().location).toBe("/") }) test("the demo data: two organizations, one with a tenant brand, and the switcher between them", async ({ page }) => { await signIn(page, "ada@example.test") await page.goto("/") await expect(page.getByRole("link", { name: "Acme Review" })).toBeVisible() await expect(page.getByRole("link", { name: "Northwind Traders" })).toBeVisible() await page.goto("/acme-review") await expect(page.locator("[data-tenant-brand]")).toHaveCount(0) await expect(page.getByLabel("Switch organization")).toBeVisible() expect(await violations(page)).toEqual([]) await page.getByLabel("Switch organization").selectOption("northwind") await expect(page).toHaveURL(/\/northwind$/) await expect(page.locator('[data-tenant-brand="#1a1a8c"]')).toHaveCount(1) await expect(page.getByRole("link", { name: "Northwind", exact: true })).toBeVisible() expect(await violations(page)).toEqual([]) // Linus is in one organization only: nothing to switch between. const linus = await person(page.context().browser() as Browser, "linus@example.test") await linus.page.goto("/acme-review") await expect(linus.page.getByLabel("Switch organization")).toHaveCount(0) await linus.context.close() }) test("the browser can initialize MCP, list exactly the opted-in tools, and call the scoped note-list tool", async ({ page, }) => { await signIn(page, "ada@example.test") const scope = await makeOrganization(page, "mcp") const csrf = await page.locator('meta[name="csrf-token"]').getAttribute("content") const headers = { "content-type": "application/json", accept: "application/json, text/event-stream", "x-csrf-token": csrf ?? "", "sec-fetch-site": "same-origin", "mcp-protocol-version": "2025-11-25", } const rpcBody = async (response: Awaited>) => { const text = await response.text() if (response.headers()["content-type"]?.includes("text/event-stream")) { const data = text.split("\n").find((line) => line.startsWith("data: ")) if (!data) throw new Error("MCP response had no event data") return JSON.parse(data.slice("data: ".length)) } return JSON.parse(text) } const initialize = await page.request.post("/api/mcp", { headers, data: { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "app-template-browser-e2e", version: "1.0.0" }, }, }, }) expect(initialize.status()).toBe(200) expect((await rpcBody(initialize)).result.protocolVersion).toBe("2025-11-25") const listed = await page.request.post("/api/mcp", { headers, data: { jsonrpc: "2.0", id: 2, method: "tools/list" } }) const tools = (await rpcBody(listed)).result.tools as Array<{ name: string }> expect(listed.status()).toBe(200) // Exactly the procedures whose contract opts in with `.tool()` (docs/reference/app-kit.md), in registry // order: the notes list, the knowledge module's retrieval and the agent module's run status, which opt // in on purpose; no other read or write of any module (test/mcp.test.ts asserts the same list). expect(tools.map((tool) => tool.name)).toEqual(["agent.run.status", "knowledge.search", "notes.note.list"]) const called = await page.request.post("/api/mcp", { headers, data: { jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "notes.note.list", arguments: scope }, }, }) expect(called.status()).toBe(200) expect((await rpcBody(called)).result).toMatchObject({ structuredContent: { notes: [], hasMore: false }, }) const refused = await page.request.post("/api/mcp", { headers, data: { jsonrpc: "2.0", id: 4, method: "tools/call", params: { name: "notes.note.create", arguments: {} } }, }) expect((await rpcBody(refused)).error.code).toBe(-32602) }) test("MCP enforces the real session, CSRF token, and tenant scope at its HTTP boundary", async ({ browser }) => { const owner = await person(browser, `${unique("mcp-owner")}@example.test`) const foreign = await makeOrganization(owner.page, "mcp-private") const secret = unique("tenant-private-note") await call(owner.page, "notes.note.create", { ...foreign, title: "Private note", body: secret }) const noSession = await browser.newContext({ baseURL: base }) const anonymous = await noSession.newPage() const requestBody = (id: number, method: string, params?: unknown) => ({ jsonrpc: "2.0", id, method, ...(params === undefined ? {} : { params }), }) const noAuth = await anonymous.request.post("/api/mcp", { headers: { "content-type": "application/json", accept: "application/json, text/event-stream" }, data: requestBody(1, "initialize"), }) expect(noAuth.status()).toBe(401) await noSession.close() const csrf = await owner.page.locator('meta[name="csrf-token"]').getAttribute("content") const initialized = await owner.page.request.post("/api/mcp", { headers: { "content-type": "application/json", accept: "application/json, text/event-stream", "sec-fetch-site": "same-origin", "x-csrf-token": csrf ?? "", "mcp-protocol-version": "2025-11-25", }, data: requestBody(2, "initialize", { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "app-template-security-e2e", version: "1.0.0" }, }), }) expect(initialized.status()).toBe(200) const withoutCsrf = await owner.page.request.post("/api/mcp", { headers: { "content-type": "application/json", accept: "application/json, text/event-stream", "sec-fetch-site": "same-origin", "mcp-protocol-version": "2025-11-25", }, data: requestBody(3, "tools/list"), }) expect(withoutCsrf.status()).toBe(403) const stranger = await person(browser, `${unique("mcp-stranger")}@example.test`) const strangerCsrf = await stranger.page.locator('meta[name="csrf-token"]').getAttribute("content") const crossTenant = await stranger.page.request.post("/api/mcp", { headers: { "content-type": "application/json", accept: "application/json, text/event-stream", "sec-fetch-site": "same-origin", "x-csrf-token": strangerCsrf ?? "", "mcp-protocol-version": "2025-11-25", }, data: requestBody(4, "tools/call", { name: "notes.note.list", arguments: foreign, }), }) expect(crossTenant.status()).toBe(200) const responseText = await crossTenant.text() const event = responseText.split("\n").find((line) => line.startsWith("data: ")) const rpcResponse = crossTenant.headers()["content-type"]?.includes("text/event-stream") ? event?.slice("data: ".length) : responseText expect(rpcResponse).toBeDefined() const result = JSON.parse(rpcResponse ?? "null") as { result?: { isError?: boolean; content?: Array<{ text?: string }>; structuredContent?: unknown } } expect(result.result?.isError).toBe(true) expect(JSON.stringify(result)).not.toContain(secret) expect(JSON.stringify(result.result?.structuredContent ?? null)).not.toContain(secret) expect(result.result?.content?.[0]?.text).toBe("The tool request was refused.") await owner.context.close() await stranger.context.close() }) test("a stranger cannot tell an organization that is not theirs from one that does not exist", async ({ browser }) => { const { page, context } = await person(browser, `${unique("stranger")}@example.test`) const seen: Array<{ status: number; text: string }> = [] for (const path of ["/acme-review", "/no-such-organization", "/acme-review/general/demo", "/northwind/general"]) { const response = await page.goto(path) seen.push({ status: response?.status() ?? 0, text: await mainText(page) }) } expect(seen.map((entry) => entry.status)).toEqual([404, 404, 404, 404]) expect(new Set(seen.map((entry) => entry.text)).size).toBe(1) expect(seen[0]?.text).toContain("Page not found") expect(await violations(page)).toEqual([]) await context.close() }) test("a member whose role does not allow a page gets a 403 and no link to it", async ({ browser }) => { const { page, context } = await person(browser, "linus@example.test") for (const path of ["/acme-review/settings", "/acme-review/audit"]) { const response = await page.goto(path) expect(response?.status(), path).toBe(403) await expect(page.getByRole("heading", { level: 1, name: "You do not have access" })).toBeVisible() } expect(await violations(page)).toEqual([]) await page.goto("/acme-review") await expect(page.getByRole("link", { name: "Audit" })).toHaveCount(0) // The members page shows a viewer the list and none of the controls. await page.goto("/acme-review/members") await expect(page.getByRole("heading", { level: 1, name: "Members" })).toBeVisible() await expect(page.getByRole("heading", { name: "Invite" })).toHaveCount(0) await context.close() }) test("an organization, a workspace and a project are made from the pages, and each page passes axe", async ({ browser, }) => { const { page, context } = await person(browser, `${unique("founder")}@example.test`) const slug = unique("studio") await page.goto("/onboarding") await page.getByLabel("Name", { exact: true }).fill("Studio") await page.getByLabel("Short name").fill(slug) await page.getByRole("button", { name: "Create" }).click() await expect(page).toHaveURL(new RegExp(`/${slug}$`)) expect(await violations(page)).toEqual([]) await page.goto(`/${slug}/workspaces`) await expect(page.getByRole("heading", { level: 1, name: "Workspaces" })).toBeVisible() await page.getByLabel("Name", { exact: true }).fill("Research") await page.getByLabel("Short name").fill("research") await page.getByRole("button", { name: "Create" }).click() await expect(page.getByRole("link", { name: "Research" })).toBeVisible() expect(await violations(page)).toEqual([]) // A workspace cannot take the name of a page of the shell. await page.getByLabel("Name", { exact: true }).fill("Members") await page.getByLabel("Short name").fill("members") await page.getByRole("button", { name: "Create" }).click() await expect(alerts(page)).toContainText("reserved") await page.goto(`/${slug}/research`) await expect(page.getByRole("heading", { level: 1, name: "Workspace Research" })).toBeVisible() await expect(page.getByText("There are no projects in this workspace yet.")).toBeVisible() await page.getByLabel("Name", { exact: true }).fill("Field notes") await page.getByLabel("Short name").fill("field-notes") await page.getByLabel("Module").selectOption("notes") await page.getByRole("button", { name: "Create" }).click() await page.getByRole("link", { name: "Field notes" }).click() await expect(page).toHaveURL(new RegExp(`/${slug}/research/field-notes$`)) await expect(page.getByRole("heading", { level: 1, name: "Project Field notes" })).toBeVisible() await expect(page.getByRole("link", { name: "Notes" })).toBeVisible() await expect(page.getByRole("link", { name: "Documents" })).toBeVisible() await expect(page.getByRole("navigation", { name: "Path" })).toBeVisible() expect(await violations(page)).toEqual([]) await context.close() }) test("an invitation: made by an administrator, opened signed out, accepted once, and every other state worded", async ({ browser, }) => { const owner = await person(browser, `${unique("owner")}@example.test`) const slug = unique("guild") await call(owner.page, "tenancy.org.create", { slug, name: "Guild" }) await owner.page.goto(`/${slug}/members`) await owner.page.getByLabel("E-mail address").fill("invited@example.test") await owner.page.getByLabel("Role").first().selectOption("member") await owner.page.getByRole("button", { name: "Invite" }).click() await expect(owner.page.getByText("It works once, for that address only")).toBeVisible() const link = await owner.page.getByLabel("Invitation link").inputValue() expect(link).toMatch(new RegExp(`/invitations/${slug}/[A-Za-z0-9_-]{20,}$`)) expect(await violations(owner.page)).toEqual([]) await owner.page.reload() await expect(owner.page.getByRole("cell", { name: "invited@example.test" })).toBeVisible() const path = new URL(link).pathname // Signed out: asked to sign in, brought back to the same link, and opening it consumed nothing. const guestContext = await browser.newContext({ baseURL: base }) const guest = await guestContext.newPage() await guest.goto(path) await expect(guest.getByRole("heading", { level: 1, name: "Your invitation" })).toBeVisible() await expect(guest.getByText("Sign in with the e-mail address the invitation was sent to")).toBeVisible() expect(await violations(guest)).toEqual([]) await guest.getByRole("link", { name: "Sign in" }).last().click() await expect(guest).toHaveURL(/\/auth\/sign-in\?return_to=/) await guest.getByLabel("E-mail address").fill("invited@example.test") await guest.getByRole("button", { name: "Sign in" }).click() await expect(guest).toHaveURL(new RegExp(`${path}$`)) // Signed in as somebody else: the one "not valid", which does not say whether the token exists. const other = await person(browser, `${unique("other")}@example.test`) await other.page.goto(path) await other.page.getByRole("button", { name: "Accept the invitation" }).click() await expect(alerts(other.page)).toContainText("not valid for the account you are signed in with") await other.context.close() // The invitee: accepted, once. await expect(guest.getByText("You are signed in as invited@example.test.")).toBeVisible() expect(await violations(guest)).toEqual([]) await guest.getByRole("button", { name: "Accept the invitation" }).click() await expect(guest.getByRole("status")).toContainText(`You joined ${slug}.`) await guest.getByRole("link", { name: `Open ${slug}` }).click() await expect(guest).toHaveURL(new RegExp(`/${slug}$`)) // The same link again: used. In Lithuanian, too. await guest.goto(path) await guest.getByRole("button", { name: "Accept the invitation" }).click() await expect(alerts(guest)).toContainText("This invitation has already been used.") await guest.getByLabel("Language").selectOption("lt") await guest.getByRole("button", { name: "Priimti kvietimą" }).click() await expect(alerts(guest)).toContainText("Šis kvietimas jau panaudotas.") expect(await violations(guest)).toEqual([]) await guestContext.close() await owner.context.close() }) test("the language is the person's own and follows them; the theme is this screen's", async ({ browser }) => { const email = `${unique("polyglot")}@example.test` const first = await person(browser, email) await first.page.goto("/") await expect(first.page.getByRole("heading", { level: 1, name: "Organizations" })).toBeVisible() await first.page.getByLabel("Language").selectOption("lt") await expect(first.page.getByRole("heading", { level: 1, name: "Organizacijos" })).toBeVisible() await expect(first.page.locator("html")).toHaveAttribute("lang", "lt") expect(await violations(first.page)).toEqual([]) await first.context.close() // Another browser, no cookie: the choice was saved on the user. const second = await person(browser, email) await second.page.goto("/") await expect(second.page.getByRole("heading", { level: 1, name: "Organizacijos" })).toBeVisible() // The theme: dark first, the root carries the class, and the server renders it back. const root = second.page.locator("html") await expect(root).not.toHaveClass(/light/) await second.page.getByRole("button", { name: /^Tema: Tamsi$/ }).click() await expect(root).toHaveClass(/light/) await expect(second.page.locator(".light")).toHaveCount(1) await second.page.reload() await expect(root).toHaveClass(/light/) expect(await violations(second.page)).toEqual([]) const html = await (await second.page.request.get("/")).text() expect(html).toMatch(/]*class="light"/) await second.context.close() }) test("the health and readiness endpoints answer booleans and nothing that names a reason", async ({ request }) => { const health = await request.get("/api/health") expect(health.status()).toBe(200) expect(await health.json()).toMatchObject({ status: "ok" }) const ready = await request.get("/api/ready") expect(ready.status()).toBe(200) expect(await ready.json()).toEqual({ ready: true, checks: { store: true, reader: true, migrations: true, platform: true }, }) }) test("a request carries its trace id into the web shell's logs and spans, and no secret leaves", async ({ page }) => { await signIn(page, "ada@example.test") const trace = "4bf92f3577b34da6a3ce929d0e0e4736" const csrf = (await page.locator('meta[name="csrf-token"]').getAttribute("content")) ?? "" const response = await page.request.post("/api/v1/tenancy.org.get", { headers: { "x-csrf-token": csrf, "content-type": "application/json", "sec-fetch-site": "same-origin", traceparent: `00-${trace}-00f067aa0ba902b7-01`, "x-request-id": "e2e-request-1", }, data: { org: "acme-review" }, }) expect(response.status()).toBe(200) expect(response.headers()["x-request-id"]).toBe("e2e-request-1") await expect(async () => { const { otel } = await platformState() // The bodies of what the platform received: the request's own headers (the token it authenticates with) are not what is under test. const logs = JSON.stringify(otel.logs.map((record) => record.body)) expect(logs).toContain(trace) expect(logs).toContain("tenancy.org.get") const traces = JSON.stringify(otel.traces.map((record) => record.body)) expect(traces).toContain("procedure tenancy.org.get") // The deployment token, the session secret and the person's address never reach the platform's log. for (const secret of ["e2e-token", "ada@example.test"]) expect(logs + traces).not.toContain(secret) }).toPass({ timeout: 30_000 }) })