tests/auth-sso-provisioning.unit.test.ts
Rama main · réplica de lectura del repositorio original.
import { AsyncLocalStorage } from 'node:async_hooks'
import { describe, expect, it, vi } from 'vitest'
import { allowOnboardingDashboardExploration } from '../src/utils/onboardingRedirect'
interface MockFetchResponse {
ok: boolean
json: () => Promise<Record<string, unknown>>
}
function createUsersQuery(userRecord: Record<string, unknown>) {
const query = {
limit: vi.fn(async () => ({
data: [],
error: null,
})),
maybeSingle: vi.fn(async () => ({
data: userRecord,
error: null,
})),
}
return {
select: vi.fn(() => ({
eq: vi.fn(() => query),
})),
}
}
function createTestContext() {
const userRecord = {
id: 'user-123',
email: 'user@managed.test',
first_name: 'Managed',
last_name: 'User',
image_url: null,
onboarding: { profile_completed_at: '2026-10-20T10:00:00.000Z' } as Record<string, unknown>,
}
const mainStore = {
auth: undefined as any,
user: undefined as any,
isAdmin: false,
plans: [] as any[],
resolvePlatformAdminStatus: vi.fn<() => Promise<boolean>>(),
}
const organizationStore = {
organizations: [] as Array<{ gid: string, role: string }>,
hasOrganizations: false,
fetchOrganizations: vi.fn(async () => {
organizationStore.organizations = [{ gid: 'org-123', role: 'read' }]
organizationStore.hasOrganizations = true
}),
dedupFetchOrganizations: vi.fn(async () => {}),
}
const mockGetClaims = vi.fn().mockResolvedValue({
data: {
claims: {
sub: 'user-123',
},
},
})
const mockGetSession = vi.fn().mockResolvedValue({
data: {
session: {
access_token: 'token-123',
user: {
id: 'user-123',
email: 'user@managed.test',
email_confirmed_at: '2026-04-15T10:00:00.000Z',
app_metadata: {
provider: 'sso:provider-123',
providers: ['sso:provider-123'],
},
},
},
},
})
const mockGetAuthenticatorAssuranceLevel = vi.fn().mockResolvedValue({
data: {
currentLevel: 'aal1',
nextLevel: 'aal1',
},
error: null,
})
const mockRpc = vi.fn().mockResolvedValue({
data: false,
error: null,
})
const mockSignOut = vi.fn().mockResolvedValue({ error: null })
const mockSetUser = vi.fn()
const mockSendEvent = vi.fn().mockResolvedValue(undefined)
const mockHideLoader = vi.fn()
const mockCreateSignedImageUrl = vi.fn(async (value: string) => value)
const mockGetPlans = vi.fn<() => Promise<any[]>>(async () => [])
const mockIsPlatformAdmin = vi.fn(async () => false)
const mockSetWebsitePaidUserCookie = vi.fn()
mainStore.resolvePlatformAdminStatus.mockImplementation(async () => {
const status = await mockIsPlatformAdmin()
mainStore.isAdmin = status
if (status)
mockSetWebsitePaidUserCookie(true)
return status
})
const mockFetch = vi.fn<(...args: unknown[]) => Promise<MockFetchResponse>>(async () => ({
ok: true,
json: async () => ({ success: true }),
}))
const mockApps: Array<{ app_id: string, need_onboarding: boolean }> = []
const mockFrom = vi.fn((table: string) => {
if (table === 'apps') {
return {
select: vi.fn(() => ({
eq: vi.fn(() => ({
limit: vi.fn(async () => ({ data: mockApps, error: null })),
})),
})),
}
}
return createUsersQuery(userRecord)
})
return {
userRecord,
mainStore,
mockCreateSignedImageUrl,
mockApps,
mockFetch,
mockFrom,
mockGetAuthenticatorAssuranceLevel,
mockGetClaims,
mockGetPlans,
mockHideLoader,
mockIsPlatformAdmin,
mockRpc,
mockSendEvent,
mockSetUser,
mockSetWebsitePaidUserCookie,
mockGetSession,
mockSignOut,
organizationStore,
}
}
type AuthGuardTestContext = ReturnType<typeof createTestContext>
const contextStorage = new AsyncLocalStorage<AuthGuardTestContext>()
function getContext() {
const context = contextStorage.getStore()
if (!context)
throw new Error('Missing auth guard test context')
return context
}
async function withTestContext(run: (context: AuthGuardTestContext) => Promise<void>) {
const context = createTestContext()
await contextStorage.run(context, async () => {
await run(context)
})
}
vi.mock('~/services/loader', () => ({
hideLoader: () => getContext().mockHideLoader(),
}))
vi.mock('~/services/posthog', () => ({
setUser: (...args: unknown[]) => getContext().mockSetUser(...args),
}))
vi.mock('~/services/storage', () => ({
createSignedImageUrl: (value: string) => getContext().mockCreateSignedImageUrl(value),
getImmediateImageUrl: (value?: string | null) => value ?? '',
}))
vi.mock('~/services/tracking', () => ({
sendEvent: (...args: unknown[]) => getContext().mockSendEvent(...args),
}))
vi.mock('~/services/websiteAuthCookie', () => ({
clearWebsitePaidUserCookie: vi.fn(),
setWebsitePaidUserCookie: (isPaidUser: boolean) => getContext().mockSetWebsitePaidUserCookie(isPaidUser),
}))
vi.mock('~/services/console', () => ({
getLocalConfig: () => ({ apiHost: 'https://supabase.capgo.test' }),
getPlans: () => getContext().mockGetPlans(),
isPlatformAdmin: () => getContext().mockIsPlatformAdmin(),
useConsole: () => {
const context = getContext()
return {
auth: {
getClaims: context.mockGetClaims,
getSession: context.mockGetSession,
getAuthenticatorAssuranceLevel: context.mockGetAuthenticatorAssuranceLevel,
mfa: {
getAuthenticatorAssuranceLevel: context.mockGetAuthenticatorAssuranceLevel,
},
signOut: context.mockSignOut,
},
rpc: context.mockRpc,
from: context.mockFrom,
}
},
defaultApiHost: 'https://api.capgo.test',
}))
vi.mock('~/stores/main', () => ({
useMainStore: () => getContext().mainStore,
}))
vi.mock('~/stores/organization', () => ({
isPendingOrganizationInvite: (org: { is_invite?: boolean | null, role: string }) => org.is_invite ?? org.role.startsWith('invite'),
useOrganizationStore: () => getContext().organizationStore,
}))
vi.stubGlobal('fetch', vi.fn((...args: unknown[]) => getContext().mockFetch(...args)))
const DASHBOARD_ROUTE = { path: '/dashboard', fullPath: '/dashboard', meta: { middleware: 'auth' }, query: {} }
const LOGIN_ROUTE = { path: '/login', fullPath: '/login', meta: {}, query: {} }
async function getGuard() {
const router = {
beforeEach: vi.fn(),
}
const { install } = await import('../src/modules/auth.ts')
install({ router } as any)
const guard = router.beforeEach.mock.calls[0]?.[0]
if (!guard)
throw new Error('Auth guard was not registered')
return guard
}
describe('auth guard SSO provisioning', () => {
it.concurrent('provisions an SSO session before redirecting to app onboarding and keeps the user on the target route', async () => {
await withTestContext(async (context) => {
const guard = await getGuard()
const next = vi.fn()
await guard(DASHBOARD_ROUTE, LOGIN_ROUTE, next)
expect(context.mockFetch).toHaveBeenCalledWith('https://api.capgo.test/private/sso/provision-user', expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
Authorization: 'Bearer token-123',
}),
}))
expect(context.organizationStore.fetchOrganizations).toHaveBeenCalled()
expect(next).toHaveBeenCalledWith()
expect(next).not.toHaveBeenCalledWith('/onboarding/app')
})
})
it.concurrent('redirects an eligible user to resume their pending app onboarding', async () => {
await withTestContext(async (context) => {
context.mockApps.push({ app_id: 'com.test.pending-onboarding', need_onboarding: true })
context.mockGetSession.mockResolvedValue({
data: {
session: {
access_token: 'token-123',
user: {
id: 'user-789',
email: 'user@managed.test',
created_at: '2026-08-04T14:01:00.000Z',
email_confirmed_at: '2026-04-15T10:00:00.000Z',
app_metadata: { provider: 'email', providers: ['email'] },
},
},
},
})
const guard = await getGuard()
const next = vi.fn()
await guard(DASHBOARD_ROUTE, LOGIN_ROUTE, next)
expect(next).toHaveBeenCalledWith({
path: '/app/com.test.pending-onboarding/getting-started',
})
})
})
it.concurrent('loads plans before redirecting a newly authenticated user into onboarding', async () => {
await withTestContext(async (context) => {
const loadedPlans = [{ name: 'Solo' }]
context.mockApps.push({ app_id: 'com.test.pending-onboarding', need_onboarding: true })
context.mockGetPlans.mockResolvedValue(loadedPlans)
context.mockGetSession.mockResolvedValue({
data: {
session: {
access_token: 'token-123',
user: {
id: 'user-plans-onboarding',
email: 'user@managed.test',
created_at: '2026-08-04T14:01:00.000Z',
email_confirmed_at: '2026-04-15T10:00:00.000Z',
app_metadata: { provider: 'email', providers: ['email'] },
},
},
},
})
const guard = await getGuard()
const next = vi.fn()
await guard(DASHBOARD_ROUTE, LOGIN_ROUTE, next)
expect(next).toHaveBeenCalledWith({
path: '/app/com.test.pending-onboarding/getting-started',
})
expect(context.mockGetPlans).toHaveBeenCalledOnce()
expect(context.mainStore.plans).toEqual(loadedPlans)
})
})
it.concurrent('tracks a successful login before redirecting the user into onboarding', async () => {
await withTestContext(async (context) => {
context.mockApps.push({ app_id: 'com.test.pending-onboarding', need_onboarding: true })
context.mockGetSession.mockResolvedValue({
data: {
session: {
access_token: 'token-123',
user: {
id: 'user-login-tracking',
email: 'user@managed.test',
created_at: '2026-08-04T14:01:00.000Z',
email_confirmed_at: '2026-04-15T10:00:00.000Z',
app_metadata: { provider: 'email', providers: ['email'] },
},
},
},
})
const guard = await getGuard()
const next = vi.fn()
await guard(DASHBOARD_ROUTE, LOGIN_ROUTE, next)
expect(next).toHaveBeenCalledWith({
path: '/app/com.test.pending-onboarding/getting-started',
})
expect(context.mockSendEvent).toHaveBeenCalledOnce()
expect(context.mockSendEvent).toHaveBeenCalledWith(expect.objectContaining({
channel: 'user-login',
event: 'User Login',
user_id: 'user-login-tracking',
}))
})
})
it.concurrent('retries loading plans on a later authenticated navigation when the store is empty', async () => {
await withTestContext(async (context) => {
const loadedPlans = [{ name: 'Solo' }]
context.mainStore.auth = {
id: 'user-123',
email: 'user@managed.test',
email_confirmed_at: '2026-04-15T10:00:00.000Z',
}
context.mockGetPlans.mockResolvedValue(loadedPlans)
const guard = await getGuard()
const next = vi.fn()
await guard(
{ path: '/settings/organization/plans', fullPath: '/settings/organization/plans', meta: { middleware: 'auth' }, query: {} },
{ path: '/dashboard', fullPath: '/dashboard', meta: { middleware: 'auth' }, query: {} },
next,
)
expect(next).toHaveBeenCalledWith()
expect(context.mockGetPlans).toHaveBeenCalledOnce()
expect(context.mainStore.plans).toEqual(loadedPlans)
})
})
it.concurrent('allows an eligible user to explore the dashboard until refresh', async () => {
await withTestContext(async (context) => {
context.mockApps.push({ app_id: 'com.test.pending-onboarding', need_onboarding: true })
context.mockGetSession.mockResolvedValue({
data: {
session: {
access_token: 'token-123',
user: {
id: 'user-456',
email: 'user@managed.test',
created_at: '2026-08-04T14:01:00.000Z',
email_confirmed_at: '2026-04-15T10:00:00.000Z',
app_metadata: { provider: 'email', providers: ['email'] },
},
},
},
})
allowOnboardingDashboardExploration('user-456')
const guard = await getGuard()
const next = vi.fn()
await guard(DASHBOARD_ROUTE, LOGIN_ROUTE, next)
expect(next).toHaveBeenCalledWith()
})
})
it.concurrent('keeps redirecting non-SSO users without organizations to app onboarding', async () => {
await withTestContext(async (context) => {
context.mockGetSession.mockResolvedValue({
data: {
session: {
access_token: 'token-123',
user: {
id: 'user-123',
email: 'user@managed.test',
email_confirmed_at: '2026-04-15T10:00:00.000Z',
app_metadata: {
provider: 'email',
providers: ['email'],
},
},
},
},
})
context.organizationStore.fetchOrganizations = vi.fn(async () => {
context.organizationStore.organizations = []
context.organizationStore.hasOrganizations = false
})
const guard = await getGuard()
const next = vi.fn()
await guard(DASHBOARD_ROUTE, LOGIN_ROUTE, next)
expect(context.mockFetch).not.toHaveBeenCalled()
expect(next).toHaveBeenCalledWith({
path: '/onboarding/app',
query: {
to: '/dashboard',
},
})
})
})
it.concurrent.each([
{ cachedAuth: false, result: 'admin' },
{ cachedAuth: true, result: 'admin' },
{ cachedAuth: false, result: 'member' },
{ cachedAuth: true, result: 'member' },
{ cachedAuth: false, result: 'error' },
{ cachedAuth: true, result: 'error' },
])('resolves $result access without organizations (cached auth: $cachedAuth)', async ({ cachedAuth, result }) => {
await withTestContext(async (context) => {
const user = {
id: 'user-123',
email: 'user@managed.test',
email_confirmed_at: '2026-04-15T10:00:00.000Z',
app_metadata: { provider: 'email', providers: ['email'] },
}
context.mockGetSession.mockResolvedValue({
data: { session: { access_token: 'token-123', user } },
})
if (cachedAuth)
context.mainStore.auth = user
context.mainStore.isAdmin = true
context.organizationStore.fetchOrganizations = vi.fn(async () => {
context.organizationStore.organizations = []
context.organizationStore.hasOrganizations = false
})
if (result === 'error')
context.mockIsPlatformAdmin.mockRejectedValue(new Error('Admin lookup failed'))
else
context.mockIsPlatformAdmin.mockResolvedValue(result === 'admin')
const guard = await getGuard()
const next = vi.fn()
await guard(DASHBOARD_ROUTE, LOGIN_ROUTE, next)
expect(context.mockIsPlatformAdmin).toHaveBeenCalledOnce()
expect(context.mainStore.isAdmin).toBe(result === 'admin')
if (result === 'admin') {
expect(next).toHaveBeenCalledWith()
expect(context.mockSetWebsitePaidUserCookie).toHaveBeenCalledWith(true)
}
else {
expect(next).toHaveBeenCalledWith({ path: '/onboarding/app', query: { to: '/dashboard' } })
expect(context.mockSetWebsitePaidUserCookie).not.toHaveBeenCalled()
}
})
})
it.concurrent('resolves platform-admin access for users with organizations', async () => {
await withTestContext(async (context) => {
const user = {
id: 'user-123',
email: 'user@managed.test',
email_confirmed_at: '2026-04-15T10:00:00.000Z',
app_metadata: { provider: 'email', providers: ['email'] },
}
context.mainStore.auth = user
context.organizationStore.organizations = [{ gid: 'org-123', role: 'read' }]
context.organizationStore.hasOrganizations = true
context.mockIsPlatformAdmin.mockResolvedValue(true)
const guard = await getGuard()
const next = vi.fn()
await guard(
{ path: '/dashboard', fullPath: '/dashboard', meta: { middleware: 'auth' }, query: {} },
{ path: '/dashboard', fullPath: '/dashboard', meta: { middleware: 'auth' }, query: {} },
next,
)
expect(context.mockIsPlatformAdmin).toHaveBeenCalledOnce()
expect(context.mainStore.isAdmin).toBe(true)
expect(next).toHaveBeenCalledWith()
})
})
it.concurrent('redirects accounts pending deletion to the recovery page instead of app onboarding', async () => {
await withTestContext(async (context) => {
context.mockRpc.mockResolvedValueOnce({
data: true,
error: null,
})
context.organizationStore.fetchOrganizations = vi.fn(async () => {
context.organizationStore.organizations = []
context.organizationStore.hasOrganizations = false
})
const guard = await getGuard()
const next = vi.fn()
await guard(DASHBOARD_ROUTE, LOGIN_ROUTE, next)
expect(context.organizationStore.fetchOrganizations).not.toHaveBeenCalled()
expect(next).toHaveBeenCalledWith({
path: '/accountDisabled',
query: {
to: '/dashboard',
},
})
})
})
it.concurrent('keeps disabled users on the recovery page when it is reloaded with a saved destination', async () => {
await withTestContext(async (context) => {
context.mockRpc.mockResolvedValueOnce({
data: true,
error: null,
})
const guard = await getGuard()
const next = vi.fn()
await guard(
{
path: '/accountDisabled',
fullPath: '/accountDisabled?to=/apps/app-123',
meta: { middleware: 'auth' },
query: { to: '/apps/app-123' },
},
{ path: '/login', fullPath: '/login', meta: {}, query: {} },
next,
)
expect(next).toHaveBeenCalledTimes(1)
expect(next).toHaveBeenCalledWith()
})
})
it.concurrent('continues navigation when the disabled-account RPC errors', async () => {
await withTestContext(async (context) => {
context.mockRpc.mockResolvedValueOnce({
data: null,
error: new Error('rpc failed'),
})
const guard = await getGuard()
const next = vi.fn()
await guard(DASHBOARD_ROUTE, LOGIN_ROUTE, next)
expect(context.organizationStore.fetchOrganizations).toHaveBeenCalled()
expect(next).toHaveBeenCalledWith()
expect(next).not.toHaveBeenCalledWith(expect.objectContaining({
path: '/accountDisabled',
}))
})
})
it.concurrent('sets the website paid user cookie for platform admins after login', async () => {
await withTestContext(async (context) => {
context.mockIsPlatformAdmin.mockResolvedValue(true)
const guard = await getGuard()
const next = vi.fn()
await guard(DASHBOARD_ROUTE, LOGIN_ROUTE, next)
expect(context.mockSetWebsitePaidUserCookie).toHaveBeenCalledWith(true)
expect(context.mainStore.isAdmin).toBe(true)
expect(next).toHaveBeenCalledWith()
})
})
it.concurrent('redirects active users away from the recovery page when the disabled-account check errors', async () => {
await withTestContext(async (context) => {
context.mainStore.auth = {
id: 'user-123',
email: 'user@managed.test',
email_confirmed_at: '2026-04-15T10:00:00.000Z',
}
context.mockRpc.mockResolvedValueOnce({
data: null,
error: new Error('rpc failed'),
})
const guard = await getGuard()
const next = vi.fn()
await guard(
{
path: '/accountDisabled',
fullPath: '/accountDisabled?to=/apps/app-123',
meta: { middleware: 'auth' },
query: { to: '/apps/app-123' },
},
{ path: '/apps/app-123', fullPath: '/apps/app-123', meta: { middleware: 'auth' }, query: {} },
next,
)
expect(next).toHaveBeenCalledTimes(1)
expect(next).toHaveBeenCalledWith('/apps/app-123')
})
})
it.concurrent('aborts navigation for managed SSO users when provisioning fails instead of redirecting to app onboarding', async () => {
await withTestContext(async (context) => {
context.mockFetch.mockResolvedValueOnce({
ok: false,
json: async () => ({ error: 'provider_lookup_failed' }),
})
context.organizationStore.fetchOrganizations = vi.fn(async () => {
context.organizationStore.organizations = []
context.organizationStore.hasOrganizations = false
})
const guard = await getGuard()
const next = vi.fn()
await guard(DASHBOARD_ROUTE, LOGIN_ROUTE, next)
expect(context.mockFetch).toHaveBeenCalled()
expect(next).toHaveBeenCalledWith(false)
expect(next).not.toHaveBeenCalledWith({
path: '/onboarding/app',
query: {
to: '/dashboard',
},
})
})
})
it.concurrent('aborts navigation when merged-session sign out fails instead of redirecting with a stale SSO session', async () => {
await withTestContext(async (context) => {
context.mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true, merged: true }),
})
context.mockSignOut.mockResolvedValueOnce({
error: new Error('sign out failed'),
})
const guard = await getGuard()
const next = vi.fn()
await guard(DASHBOARD_ROUTE, LOGIN_ROUTE, next)
expect(next).toHaveBeenCalledWith(false)
expect(next).not.toHaveBeenCalledWith('/login?message=sso_account_linked')
})
})
})
describe('mandatory name navigation', () => {
for (const alreadyAuthenticated of [false, true]) {
for (const path of ['/dashboard', '/onboarding/app', '/onboarding/organization', '/onboarding/invitation', '/apikeys']) {
it.concurrent(`blocks ${path} before names are saved (authenticated: ${alreadyAuthenticated})`, async () => {
await withTestContext(async (context) => {
Object.assign(context.userRecord, { first_name: '', last_name: '', onboarding: {} })
const noOrganizations = async () => {
context.organizationStore.organizations = []
context.organizationStore.hasOrganizations = false
}
context.organizationStore.fetchOrganizations.mockImplementation(noOrganizations)
context.organizationStore.dedupFetchOrganizations.mockImplementation(noOrganizations)
if (alreadyAuthenticated) {
context.mainStore.auth = { id: 'user-123' }
context.mainStore.user = context.userRecord
}
const guard = await getGuard()
const next = vi.fn()
await guard({ path, fullPath: path, meta: { middleware: 'auth' }, query: {} }, { path: '/login', meta: {}, query: {} }, next)
expect(next).toHaveBeenCalledWith({ path: '/onboarding/name', query: { to: path } })
expect(next).toHaveBeenCalledTimes(1)
})
})
}
}
for (const path of ['/login-cli', '/oauth/authorize']) {
it.concurrent(`lets ${path} finish without the name step`, async () => {
await withTestContext(async (context) => {
Object.assign(context.userRecord, { first_name: '', last_name: '', onboarding: {} })
const noOrganizations = async () => {
context.organizationStore.organizations = []
context.organizationStore.hasOrganizations = false
}
context.organizationStore.fetchOrganizations.mockImplementation(noOrganizations)
context.organizationStore.dedupFetchOrganizations.mockImplementation(noOrganizations)
const guard = await getGuard()
const next = vi.fn()
await guard({ path, fullPath: path, meta: { middleware: 'auth' }, query: {} }, { path: '/login', meta: {}, query: {} }, next)
expect(next).not.toHaveBeenCalledWith(expect.objectContaining({ path: '/onboarding/name' }))
})
})
}
it.concurrent('provisions SSO membership before checking names for an already-authenticated user', async () => {
await withTestContext(async (context) => {
Object.assign(context.userRecord, { first_name: '', last_name: '', onboarding: {} })
context.mainStore.auth = { id: 'user-123' }
context.mainStore.user = context.userRecord
const guard = await getGuard()
const next = vi.fn()
await guard({ path: '/dashboard', fullPath: '/dashboard', meta: { middleware: 'auth' }, query: {} }, { path: '/apps', meta: {}, query: {} }, next)
expect(context.mockFetch).toHaveBeenCalledWith('https://api.capgo.test/private/sso/provision-user', expect.anything())
expect(next).not.toHaveBeenCalledWith(expect.objectContaining({ path: '/onboarding/name' }))
expect(next).toHaveBeenCalledWith()
})
})
it.concurrent('keeps the name screen accessible even with pending invitations or app setup', async () => {
await withTestContext(async (context) => {
Object.assign(context.userRecord, { first_name: '', last_name: '', onboarding: {} })
context.mockApps.push({ app_id: 'com.pending', need_onboarding: true })
context.organizationStore.fetchOrganizations.mockImplementation(async () => {
context.organizationStore.organizations = [{ gid: 'org', role: 'invite_read' }]
context.organizationStore.hasOrganizations = false
})
const guard = await getGuard()
const next = vi.fn()
await guard({ path: '/onboarding/name', fullPath: '/onboarding/name', meta: { middleware: 'auth' }, query: {} }, { path: '/login', meta: {}, query: {} }, next)
expect(next).toHaveBeenCalledWith()
})
})
})
describe('initial profile gate failures and existing members', () => {
it.concurrent('offers a recoverable login without allowing a missing profile past the name gate', async () => {
await withTestContext(async (context) => {
context.mockFrom.mockImplementation(() => ({
select: () => ({ eq: () => ({ maybeSingle: async () => ({ data: null, error: { code: 'unavailable' } }) }) }),
insert: () => { throw new Error('profile unavailable') },
}) as any)
const guard = await getGuard()
const next = vi.fn()
await guard({ path: '/onboarding/app', fullPath: '/onboarding/app', meta: { middleware: 'auth' }, query: {} }, { path: '/login', meta: {}, query: {} }, next)
expect(next).toHaveBeenCalledWith({ path: '/login', query: { profile_unavailable: '1', to: '/onboarding/app' } })
expect(context.mainStore.auth?.id).toBe('user-123')
expect(context.mockSignOut).not.toHaveBeenCalled()
})
})
it.concurrent('does not send an existing organization member through initial names', async () => {
await withTestContext(async (context) => {
Object.assign(context.userRecord, { first_name: '', last_name: '', onboarding: {} })
const guard = await getGuard()
const next = vi.fn()
await guard({ path: '/apps', fullPath: '/apps', meta: { middleware: 'auth' }, query: {} }, { path: '/login', meta: {}, query: {} }, next)
expect(next).toHaveBeenCalledWith()
})
})
it.concurrent('does not block navigation for users without names when organizations fail to load', async () => {
await withTestContext(async (context) => {
Object.assign(context.userRecord, { first_name: '', last_name: '', onboarding: {} })
context.organizationStore.fetchOrganizations.mockRejectedValue(new Error('organizations unavailable'))
const guard = await getGuard()
const next = vi.fn()
await guard({ path: '/apps', fullPath: '/apps', meta: { middleware: 'auth' }, query: {} }, { path: '/login', meta: {}, query: {} }, next)
expect(next).not.toHaveBeenCalledWith(false)
expect(next).not.toHaveBeenCalledWith(expect.objectContaining({ path: '/onboarding/name' }))
})
})
})