// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The loaders of the pages outside an organization (M6b): each answers a signed-out visitor, a refused read // and a redirect with its real status, and returns data that survives the trip to the browser. 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(() => ({ pageContext: vi.fn(), call: vi.fn(), config: vi.fn(), setResponseHeader: vi.fn<(name: string, value: string) => void>(), cookie: "", })) vi.mock("@tanstack/react-start/server", () => ({ getRequest: () => new Request("http://app.test/", { headers: { cookie: mocks.cookie } }), setResponseStatus: vi.fn(), setResponseHeader: mocks.setResponseHeader, })) vi.mock("../apps/web/src/server/page", () => ({ pageContext: mocks.pageContext })) vi.mock("@conusai-kit/auth", async (importOriginal) => ({ ...(await importOriginal>()), loadAuthConfig: mocks.config, })) vi.mock("../apps/web/src/server/better-auth", () => ({ MIN_PASSWORD_LENGTH: 12, betterAuthFormLabels: () => ({ submit: "Go" }), })) vi.mock("@app/modules/account/contract", async (importOriginal) => ({ // The module list behind the app's catalogues reads the account module's entities. ...(await importOriginal>()), ACCOUNT_PATH: "/settings/account", accountDeletionStatus: {}, })) vi.mock("@app/modules/account/ui", () => ({ analyticsLabels: () => ({}), deletionLabels: () => ({}), exportLabels: () => ({}), pageLabels: () => ({ title: "Account" }), })) vi.mock("@conusai-kit/tenancy", async (importOriginal) => ({ ...(await importOriginal>()), meGet: "me", invitationRead: "invitation.read", })) vi.mock("@conusai-kit/tenancy/ui", () => ({ builtInRoleName: (_i18n: unknown, role: string) => `kit:${role}` })) const { signInSearch } = await import("../apps/web/src/ui/sign-in-search") const { accountData, homeData, invitationData, membersEntryData, onboardingData, signInData } = await import( "../apps/web/src/server/account-pages" ) const { signUpData } = await import("../apps/web/src/server/account-pages") // kit:convex-better-auth const i18n = { message: (key: string) => key, locale: "en" } const context = (extra: object) => ({ i18n, caller: { call: mocks.call }, path: "/here", ...extra }) const person = (extra: object = {}) => mocks.pageContext.mockResolvedValue(context({ signedIn: true, ...extra })) const visitor = () => mocks.pageContext.mockResolvedValue(context({ signedIn: false })) const refusal = (status: number): AppError => new AppError({ type: "/problems/refused", code: "refused", title: "Refused", status }) const organizations = (...slugs: string[]) => ({ organizations: slugs.map((slug) => ({ slug, name: slug, role: "owner" })), }) const thrown = (run: Promise): Promise => run.then( () => { throw new Error("expected a refusal") }, (error: unknown) => error, ) const hrefOf = (error: unknown): string | undefined => (error as { options?: { href?: string } }).options?.href beforeEach(() => { vi.clearAllMocks() mocks.cookie = "" mocks.config.mockReturnValue({ provider: "oidc", devLogin: true }) }) describe("the entry page", () => { it("offers a visitor the way in", async () => { visitor() mocks.config.mockReturnValue({ provider: "oidc", oidc: {} }) const data = await homeData() expect(data).toMatchObject({ signedIn: false, signIn: { provider: { href: "/auth/sign-in/start?ui_locales=en", label: "app.signIn" }, signUp: { href: "/auth/sign-in/start?ui_locales=en&intent=sign-up", label: "signin.createAccount" }, }, }) expect(mocks.call).not.toHaveBeenCalled() }) it("takes a person with one organization straight to it", async () => { person({ mayCreateOrganization: true }) mocks.call.mockResolvedValue(organizations("acme")) expect(hrefOf(await thrown(homeData()))).toBe("/acme") }) it("takes a person with none to onboarding, or says why when they may not found one", async () => { person({ mayCreateOrganization: true }) mocks.call.mockResolvedValue(organizations()) expect(hrefOf(await thrown(homeData()))).toBe("/onboarding") person({ mayCreateOrganization: false }) const data = await homeData() expect(data).toMatchObject({ signedIn: true, emptyHint: "app.askForInvitation" }) expect(data).not.toHaveProperty("create") }) it("lists several organizations with their role translated", async () => { person({ mayCreateOrganization: false }) mocks.call.mockResolvedValue(organizations("acme", "north")) expect(await homeData()).toEqual( expect.objectContaining({ organizations: [ { slug: "acme", name: "acme", role: "kit:owner" }, { slug: "north", name: "north", role: "kit:owner" }, ], }), ) }) }) describe("onboarding", () => { it("sends a visitor to sign in and back", async () => { visitor() expect(hrefOf(await thrown(onboardingData()))).toBe("/auth/sign-in?return_to=%2Fhere") }) it("is the forbidden page, 403, for a person who may not found an organization", async () => { person({ mayCreateOrganization: false }) const error = (await thrown(onboardingData())) as { data?: unknown } expect(isNotFound(error)).toBe(true) expect(error.data).toEqual({ forbidden: true }) expect(mocks.setResponseHeader).toHaveBeenCalledWith("x-conusai-page-status", "403") }) it("names the first workspace in the deployment's language, whatever the founder's is", async () => { person({ mayCreateOrganization: true, deploymentI18n: { message: (key: string) => `lt:${key}` } }) expect((await onboardingData()).workspace).toEqual({ name: "lt:onboarding.workspaceName", slug: "lt:onboarding.workspaceSlug", }) }) }) describe("/members", () => { it.each([ [[], "/"], [["acme"], "/acme/members"], ])("sends a person with organizations %j to %s", async (slugs, href) => { person() mocks.call.mockResolvedValue(organizations(...slugs)) const error = await thrown(membersEntryData()) expect(isRedirect(error)).toBe(true) expect(hrefOf(error)).toBe(href) }) it("lists several organizations", async () => { person() mocks.call.mockResolvedValue(organizations("a", "b")) expect((await membersEntryData()).organizations.map(({ slug }) => slug)).toEqual(["a", "b"]) }) }) describe("the account page", () => { it("uses only a remembered organization the person belongs to", async () => { person() mocks.call.mockImplementation(async (procedure: unknown) => procedure === "me" ? { displayName: "Ada", email: "ada@example.test", ...organizations("a", "b") } : { interactive: true, recentSignIn: false, deletion: null }, ) mocks.cookie = "app_workspace=b%2Fgeneral" expect(hrefOf(await thrown(accountData(true)))).toBe("/b/settings/account") mocks.cookie = "app_workspace=other%2Fgeneral" expect((await accountData(true)).page.title).toBe("Account") mocks.cookie = "app_workspace=b%2Fgeneral%2Fextra" expect((await accountData(true)).page.title).toBe("Account") }) it("uses the only organization when there is no workspace preference", async () => { person() mocks.call.mockImplementation(async (procedure: unknown) => procedure === "me" ? { displayName: "Ada", email: "ada@example.test", ...organizations("a") } : { interactive: true, recentSignIn: false, deletion: null }, ) expect(hrefOf(await thrown(accountData(true)))).toBe("/a/settings/account") }) it("sends a person back through the dev login to sign in again, when that is the deployment's way in", async () => { person() mocks.call.mockImplementation(async (procedure: unknown) => procedure === "me" ? { displayName: "Ada", email: "ada@example.test" } : { interactive: true, recentSignIn: false, deletion: null }, ) const data = await accountData() expect(data.deletion.reauthHref).toBe("/auth/dev-login?email=ada%40example.test&return_to=%2Fsettings%2Faccount") expect(() => JSON.stringify(data)).not.toThrow() }) }) describe("the sign-in pages", () => { it("take a string from the query or nothing: the router parses `?return_to=123` as a number", () => { expect(signInSearch({ return_to: 123, error: true })).toEqual({}) expect(signInSearch({ return_to: "/members", error: "denied" })).toEqual({ return_to: "/members", error: "denied" }) }) it("sends somebody already in where they were going, and only inside this app", async () => { person() expect(hrefOf(await thrown(signInData({ return_to: "/members" })))).toBe("/members") expect(hrefOf(await thrown(signInData({ return_to: "https://evil.example/" })))).toBe("/") }) it("keeps somebody who was sent back with an error, and says what went wrong", async () => { person() expect(await signInData({ error: "denied" })).toMatchObject({ error: "signin.error.denied" }) }) it("offers the identity provider when there is one, with where to come back to", async () => { visitor() mocks.config.mockReturnValue({ provider: "oidc", oidc: {} }) const data = await signInData({ return_to: "/members" }) expect(data.provider?.href).toBe("/auth/sign-in/start?ui_locales=en&return_to=%2Fmembers") expect(data).not.toHaveProperty("dev") }) it("offers the development login with no provider, and says so when there is neither", async () => { visitor() expect((await signInData({ return_to: "/members" })).dev?.returnTo).toBe("/members") mocks.config.mockReturnValue({ provider: "oidc", devLogin: false }) expect(await signInData({})).toMatchObject({ notConfigured: "signin.notConfigured" }) }) // kit:convex-better-auth:begin it("has no sign-up page in an OIDC deployment", async () => { visitor() expect(isNotFound(await thrown(signUpData({})))).toBe(true) }) it("offers the account form with the Convex Better Auth adapter", async () => { visitor() mocks.config.mockReturnValue({ provider: "convex-better-auth" }) expect((await signInData({})).betterAuth?.toSignUp.href).toBe("/auth/sign-up") expect(await signUpData({ return_to: "/members" })).toMatchObject({ returnTo: "/members", toSignIn: { href: "/auth/sign-in?return_to=%2Fmembers" }, }) }) // kit:convex-better-auth:end }) describe("an invitation", () => { const open = { invitationId: "inv-1", state: "open", email: "guest@example.test", organization: { slug: "acme", name: "Acme" }, role: "member", roleKey: "editor", expiresAt: "2026-11-10T09:00:00.000Z", } it("tells a visitor who it is for and until when, and offers Create your account and Sign in", async () => { mocks.pageContext.mockResolvedValue(context({ signedIn: false, path: "/invitations/acme/token", theme: "dark" })) mocks.call.mockResolvedValue(open) const data = await invitationData({ org: "acme", token: "token" }) expect(mocks.call).toHaveBeenCalledWith("invitation.read", { org: "acme", token: "token" }) expect(data).toMatchObject({ state: "open", lead: "invite.lead", expires: "invite.expires" }) expect(data.start).toEqual({ org: "acme", token: "token", locale: "en", theme: "dark", createAccount: "invite.createAccount", signIn: "invite.haveAccount", }) expect(data.signedIn).toBeUndefined() }) it("says plainly that a link was used, revoked or has expired, and that an unknown one is not valid", async () => { visitor() for (const state of ["used", "revoked", "expired"]) { mocks.call.mockResolvedValueOnce({ ...open, state }) const data = await invitationData({ org: "acme", token: "token" }) expect(data).toMatchObject({ state, lead: `invite.error.${state}` }) expect(data.start).toBeUndefined() } mocks.call.mockRejectedValueOnce(refusal(404)) expect(await invitationData({ org: "acme", token: "token" })).toMatchObject({ state: "invalid", lead: "invite.error.unknown", }) }) it("offers the invited address the accept button, and anybody else only who it is for", async () => { person({ user: { email: "Guest@Example.test" } }) mocks.call.mockResolvedValue(open) const mine = await invitationData({ org: "acme", token: "token" }) expect(mine.signedIn?.labels?.accept).toBe("invite.accept") expect(mine.signedIn?.otherAccount).toBeUndefined() person({ user: { email: "ada@example.test" } }) const theirs = await invitationData({ org: "acme", token: "token" }) expect(theirs.signedIn?.labels).toBeUndefined() expect(theirs.signedIn?.otherAccount).toBe("invite.otherAccount") }) it("asks a visitor to sign in first where the sign-in takes no invitation", async () => { mocks.config.mockReturnValue({ provider: "convex-better-auth", devLogin: false }) mocks.pageContext.mockResolvedValue(context({ signedIn: false, path: "/invitations/acme/token" })) mocks.call.mockResolvedValue(open) expect((await invitationData({ org: "acme", token: "token" })).signInFirst?.href).toBe( "/auth/sign-in?return_to=%2Finvitations%2Facme%2Ftoken", ) }) }) describe("a read refused", () => { it("is the page that belongs to it", async () => { person() mocks.call.mockRejectedValue(refusal(401)) expect(isRedirect(await thrown(homeData()))).toBe(true) mocks.call.mockRejectedValue(refusal(404)) expect(isNotFound(await thrown(homeData()))).toBe(true) }) })