import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react'
import type { ReactNode } from 'react'
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import {
base,
configz,
createAccountServer,
createAccountStore,
HttpResponse,
http,
renderWithClient,
} from './account.test-utils'
import { AccountSecurityPage } from './security-page'
const success = vi.fn()
const errorToast = vi.fn()
vi.mock('sonner', () => ({
toast: { success: (...a: unknown[]) => success(...a), error: (...a: unknown[]) => errorToast(...a) },
}))
vi.mock('@tanstack/react-router', () => ({
Link: ({ children, className, to }: { children: ReactNode; className?: string; to: string }) => (
{children}
),
useNavigate: () => vi.fn(),
}))
const store = createAccountStore()
const server = createAccountServer(store)
async function openSecurityTab(name: 'MFA' | 'Passkeys' | 'Sessions' | 'Wallet') {
const existing = screen.queryByRole('button', { name: 'Done' })
if (existing) fireEvent.click(existing)
const labels = {
MFA: 'Manage authenticator',
Passkeys: 'Manage passkeys',
Sessions: 'Manage sessions',
Wallet: 'Manage wallet',
}
fireEvent.click(await screen.findByRole('button', { name: labels[name] }))
await waitFor(() => expect(screen.queryAllByText('Loading security settings')).toHaveLength(0))
}
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }))
afterEach(() => {
cleanup()
server.resetHandlers()
success.mockClear()
errorToast.mockClear()
Object.assign(store, createAccountStore())
})
afterAll(() => server.close())
describe('AccountSecurityPage', () => {
it('renders the security panels with password, MFA, passkeys, and sessions', async () => {
renderWithClient()
expect(await screen.findByRole('button', { name: /Change password/ })).toBeTruthy()
expect((await screen.findByRole('link', { name: 'Manage Connections' })).getAttribute('href')).toBe('/connections')
await openSecurityTab('MFA')
expect(screen.getByText('Multi-factor authentication')).toBeTruthy()
expect(screen.getByRole('button', { name: /Set up authenticator app/ })).toBeTruthy()
await openSecurityTab('Passkeys')
expect(screen.getByText('No passkeys have been added yet.')).toBeTruthy()
await openSecurityTab('Sessions')
expect(screen.getByText('No other active sessions.')).toBeTruthy()
})
it('shows active Provider sign-in and unlinks a wallet credential', async () => {
store.linkedAccounts = [
{
id: 'wallet-1',
accountId: '0x1111111111111111111111111111111111111111',
providerId: 'siwe',
createdAt: '2026-08-08T00:00:00.000Z',
},
]
let unlinked = false
server.use(
http.get(`${base}/api/account/provider-connections`, () =>
HttpResponse.json({
items: [
{
id: 'provider-connection-1',
connector: {
id: 'connector-1',
slug: 'github',
providerId: 'github',
providerType: 'social',
displayName: 'GitHub',
capabilities: {
signIn: { available: true },
agentAccess: { available: false },
connection: { method: 'sign_in' },
},
},
displayName: 'Octocat',
externalSubject: 'octocat',
capabilities: {
signIn: { available: true, active: true },
agentAccess: { available: false, active: false, authorizationCount: 0, resourceNames: [] },
},
createdAt: '2026-08-08T00:00:00.000Z',
updatedAt: '2026-08-08T00:00:00.000Z',
},
],
pagination: { page: Math.floor(0 / 50) + 1, pageSize: 50, totalItems: 1, totalPages: Math.ceil(1 / 50) },
}),
),
http.delete(`${base}/api/account/wallet-addresses/:accountId`, () => {
unlinked = true
return new HttpResponse(null, { status: 204 })
}),
)
renderWithClient()
expect(await screen.findByText('GitHub')).toBeTruthy()
await openSecurityTab('Wallet')
fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
fireEvent.click(await screen.findByRole('button', { name: 'Unlink wallet' }))
await waitFor(() => expect(unlinked).toBe(true))
})
it('surfaces a missing wallet provider when linking a wallet', async () => {
renderWithClient()
await openSecurityTab('Wallet')
fireEvent.click(await screen.findByRole('button', { name: 'Link wallet' }))
await waitFor(() => expect(errorToast).toHaveBeenCalled())
})
it('renders an error state when a security request fails', async () => {
server.use(http.get(`${base}/api/account/security`, () => HttpResponse.json({ error: 'no' }, { status: 500 })))
renderWithClient()
expect((await screen.findAllByText('no')).length).toBeGreaterThan(0)
})
it('keeps the Account Center shell visible while a Security tab loads', async () => {
let finishSecurityRequest!: () => void
server.use(
http.get(`${base}/api/account/security`, async () => {
await new Promise((resolve) => {
finishSecurityRequest = resolve
})
return HttpResponse.json({ security: store.security })
}),
)
renderWithClient()
expect(await screen.findByRole('heading', { name: 'Account settings' })).toBeTruthy()
expect((await screen.findAllByText('Loading security settings')).length).toBeGreaterThan(0)
expect(screen.getByRole('navigation', { name: 'Account Center' })).toBeTruthy()
expect(screen.getByRole('heading', { name: 'Account settings' })).toBeTruthy()
finishSecurityRequest()
expect(await screen.findByText('Authenticator app')).toBeTruthy()
})
it('shows the account-load error when the profile is missing', async () => {
server.use(http.get(`${base}/api/account/profile`, () => HttpResponse.json({ user: null })))
renderWithClient()
expect(await screen.findByText('Unable to load account center.')).toBeTruthy()
})
it('changes the password through the password dialog', async () => {
server.use(http.post(`${base}/api/account/password/change`, () => HttpResponse.json({ ok: true })))
renderWithClient()
fireEvent.click(await screen.findByRole('button', { name: /Change password/ }))
fireEvent.change(await screen.findByLabelText('Current password'), { target: { value: 'old-password' } })
fireEvent.change(screen.getByLabelText('New password'), { target: { value: 'new-password-1' } })
fireEvent.change(screen.getByLabelText('Confirm new password'), { target: { value: 'new-password-1' } })
fireEvent.click(screen.getByRole('dialog').querySelector('button[type="submit"]') as HTMLElement)
await waitFor(() => expect(success).toHaveBeenCalledWith('Password changed.'))
})
it('cancels the password dialog', async () => {
renderWithClient()
fireEvent.click(await screen.findByRole('button', { name: /Change password/ }))
expect(await screen.findByLabelText('Current password')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
await waitFor(() => expect(screen.queryByLabelText('Current password')).toBeNull())
})
it('surfaces a password change error inside the dialog', async () => {
server.use(
http.post(`${base}/api/account/password/change`, () =>
HttpResponse.json({ error: 'Wrong password.' }, { status: 400 }),
),
)
renderWithClient()
fireEvent.click(await screen.findByRole('button', { name: /Change password/ }))
fireEvent.change(await screen.findByLabelText('Current password'), { target: { value: 'bad' } })
fireEvent.change(screen.getByLabelText('New password'), { target: { value: 'new-password-1' } })
fireEvent.change(screen.getByLabelText('Confirm new password'), { target: { value: 'new-password-1' } })
fireEvent.click(screen.getByRole('dialog').querySelector('button[type="submit"]') as HTMLElement)
await waitFor(() => expect(screen.getByText('Wrong password.')).toBeTruthy())
})
it('rejects a mismatched password confirmation before submission', async () => {
let requests = 0
server.use(
http.post(`${base}/api/account/password/change`, () => {
requests += 1
return HttpResponse.json({ ok: true })
}),
)
renderWithClient()
fireEvent.click(await screen.findByRole('button', { name: /Change password/ }))
fireEvent.change(await screen.findByLabelText('Current password'), { target: { value: 'old-password' } })
fireEvent.change(screen.getByLabelText('New password'), { target: { value: 'new-password-1' } })
fireEvent.change(screen.getByLabelText('Confirm new password'), { target: { value: 'different-password' } })
fireEvent.click(screen.getByRole('dialog').querySelector('button[type="submit"]') as HTMLElement)
expect(await screen.findByText('New passwords do not match.')).toBeTruthy()
expect(requests).toBe(0)
})
it('enrolls TOTP then verifies the authenticator code', async () => {
server.use(
http.post(`${base}/api/account/security/mfa/totp-enrollment`, () =>
HttpResponse.json({ totpURI: 'otpauth://x', secret: 'SEKRET', backupCodes: ['code-1'] }),
),
http.post(`${base}/api/account/security/mfa/totp-verification`, () => HttpResponse.json({ ok: true })),
)
renderWithClient()
await openSecurityTab('MFA')
fireEvent.click(await screen.findByRole('button', { name: /Set up authenticator app/ }))
fireEvent.change(await screen.findByLabelText('Password'), { target: { value: 'pw' } })
fireEvent.click(screen.getByRole('dialog').querySelector('button[type="submit"]') as HTMLElement)
await waitFor(() => expect(success).toHaveBeenCalledWith('TOTP enrollment started.'))
expect(await screen.findByAltText('Authenticator app QR code')).toBeTruthy()
expect(await screen.findByText('SEKRET')).toBeTruthy()
fireEvent.change(await screen.findByLabelText('Authenticator code'), { target: { value: '123456' } })
fireEvent.click(screen.getByRole('button', { name: 'Verify code' }))
await waitFor(() => expect(success).toHaveBeenCalledWith('MFA enabled.'))
expect(await screen.findByRole('heading', { name: 'Save backup codes' })).toBeTruthy()
expect(screen.getByText('code-1')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Done' }))
await waitFor(() => expect(screen.queryByRole('heading', { name: 'Save backup codes' })).toBeNull())
})
it('verifies an MFA challenge for an enrolled account', async () => {
const enrolled = createAccountStore()
enrolled.security.mfa.enabled = true
Object.assign(store, enrolled)
server.use(http.post(`${base}/api/account/security/mfa/totp-verification`, () => HttpResponse.json({ ok: true })))
renderWithClient()
await openSecurityTab('MFA')
fireEvent.click(await screen.findByRole('button', { name: 'Verify code' }))
fireEvent.change(await screen.findByLabelText('Authenticator code'), { target: { value: '654321' } })
fireEvent.click(screen.getByRole('dialog').querySelector('button[type="submit"]') as HTMLElement)
await waitFor(() => expect(success).toHaveBeenCalledWith('MFA challenge verified.'))
})
it('disables MFA for an enrolled account', async () => {
const enrolled = createAccountStore()
enrolled.security.mfa.enabled = true
Object.assign(store, enrolled)
server.use(http.delete(`${base}/api/account/security/mfa/totp`, () => HttpResponse.json({ ok: true })))
renderWithClient()
await openSecurityTab('MFA')
fireEvent.click(await screen.findByRole('button', { name: 'Disable MFA' }))
fireEvent.change(await screen.findByLabelText('Password'), { target: { value: 'pw' } })
fireEvent.click(screen.getByRole('button', { name: 'Disable authenticator app' }))
await waitFor(() => expect(success).toHaveBeenCalledWith('MFA disabled.'))
})
it('removes a passkey through the destructive confirmation dialog', async () => {
const withPasskey = createAccountStore()
withPasskey.passkeys = [
{ id: 'pk-1', name: 'My Key', deviceType: 'platform', backedUp: true, createdAt: '2026-01-01T00:00:00.000Z' },
]
Object.assign(store, withPasskey)
server.use(http.delete(`${base}/api/account/security/passkeys/:id`, () => HttpResponse.json({ ok: true })))
renderWithClient()
await openSecurityTab('Passkeys')
expect(await screen.findByText('My Key')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Remove' }))
fireEvent.click(await screen.findByRole('button', { name: 'Remove passkey' }))
await waitFor(() => expect(success).toHaveBeenCalledWith('Passkey removed.'))
})
it('renames one of multiple passkeys and refreshes the drawer [spec: account-center/passkey-flow]', async () => {
store.passkeys = [
{ id: 'key-1', name: 'Laptop', deviceType: 'multiDevice', backedUp: true, createdAt: null },
{ id: 'key-2', name: 'Backup key', deviceType: 'singleDevice', backedUp: false, createdAt: null },
]
const rename = vi.fn()
server.use(
http.patch(`${base}/api/account/security/passkeys/:id`, async ({ request, params }) => {
const body = (await request.json()) as { name: string }
rename(params.id, body.name)
store.passkeys[0]!.name = body.name
return HttpResponse.json({ status: true })
}),
)
renderWithClient()
await openSecurityTab('Passkeys')
fireEvent.click(screen.getAllByRole('button', { name: 'Rename' })[0]!)
fireEvent.change(screen.getByLabelText('Passkey name'), { target: { value: 'Personal laptop' } })
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
await waitFor(() => expect(rename).toHaveBeenCalledWith('key-1', 'Personal laptop'))
expect(await screen.findByText('Personal laptop')).toBeTruthy()
expect(screen.getByText('Backup key')).toBeTruthy()
})
it('preserves an unnamed passkey and its draft when rename fails, then allows cancellation', async () => {
store.passkeys = [{ id: 'key-1', name: null, deviceType: 'singleDevice', backedUp: false, createdAt: null }]
server.use(
http.patch(`${base}/api/account/security/passkeys/:id`, () =>
HttpResponse.json({ message: 'Rename unavailable.' }, { status: 503 }),
),
)
renderWithClient()
await openSecurityTab('Passkeys')
fireEvent.click(screen.getByRole('button', { name: 'Rename' }))
expect((screen.getByLabelText('Passkey name') as HTMLInputElement).value).toBe('')
expect((screen.getByRole('button', { name: 'Save' }) as HTMLButtonElement).disabled).toBe(true)
fireEvent.change(screen.getByLabelText('Passkey name'), { target: { value: 'Replacement name' } })
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
expect((await screen.findByRole('alert')).textContent).toContain('Rename unavailable.')
expect((screen.getByLabelText('Passkey name') as HTMLInputElement).value).toBe('Replacement name')
expect(screen.getByRole('heading', { name: 'Unnamed passkey' })).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
expect(screen.queryByLabelText('Passkey name')).toBeNull()
expect(success).not.toHaveBeenCalled()
})
it('enrolls a passkey from the add-passkey dialog', async () => {
const create = vi.fn().mockResolvedValue({
id: 'cred-1',
type: 'public-key',
rawId: new Uint8Array([1]).buffer,
response: { attestationObject: new Uint8Array([2]).buffer, clientDataJSON: new Uint8Array([3]).buffer },
})
vi.stubGlobal('navigator', { ...navigator, credentials: { create } })
server.use(
http.get(`${base}/api/auth/passkey/generate-register-options`, () =>
HttpResponse.json({
publicKey: {
challenge: 'AQID',
user: { id: 'BAUG', name: 'jane', displayName: 'Jane' },
},
}),
),
http.post(`${base}/api/auth/passkey/verify-registration`, () => HttpResponse.json({ verified: true })),
)
renderWithClient()
await openSecurityTab('Passkeys')
fireEvent.click(await screen.findByRole('button', { name: /Add passkey/ }))
fireEvent.change(await screen.findByLabelText('Passkey name'), { target: { value: 'Laptop' } })
fireEvent.click(screen.getByRole('dialog').querySelector('button[type="submit"]') as HTMLElement)
await waitFor(() => expect(success).toHaveBeenCalledWith('Passkey enrolled.'))
vi.unstubAllGlobals()
})
it('revokes another session', async () => {
const withSession = createAccountStore()
withSession.sessions = [
{
id: 'sess-1',
userAgent: 'Mozilla/5.0 (Mac OS X) Chrome/120',
ipAddress: '1.2.3.4',
expiresAt: '2026-02-01T00:00:00.000Z',
current: false,
},
]
Object.assign(store, withSession)
server.use(http.delete(`${base}/api/account/security/sessions/:sessionId`, () => HttpResponse.json({ ok: true })))
renderWithClient()
await openSecurityTab('Sessions')
expect(await screen.findByText('Chrome on macOS')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Revoke' }))
fireEvent.click(await screen.findByRole('button', { name: 'Revoke session' }))
await waitFor(() => expect(success).toHaveBeenCalledWith('Session revoked.'))
})
it('marks the current session without presenting a duplicate revoke action', async () => {
const withSession = createAccountStore()
withSession.sessions = [
{ id: 'sess-current', userAgent: null, ipAddress: null, expiresAt: '2026-02-01T00:00:00.000Z', current: true },
]
Object.assign(store, withSession)
renderWithClient()
await openSecurityTab('Sessions')
expect((await screen.findAllByText('Unknown device')).length).toBeGreaterThan(0)
expect(screen.getByText('Current')).toBeTruthy()
expect(screen.queryByRole('button', { name: 'Revoke' })).toBeNull()
})
it('revokes all other sessions', async () => {
server.use(http.delete(`${base}/api/account/security/sessions`, () => HttpResponse.json({ ok: true })))
renderWithClient()
await openSecurityTab('Sessions')
fireEvent.click(await screen.findByRole('button', { name: 'Sign out others' }))
fireEvent.click(await screen.findByRole('button', { name: 'Revoke sessions' }))
await waitFor(() => expect(success).toHaveBeenCalledWith('Other sessions revoked.'))
})
it('keeps the verify dialog open when MFA challenge verification fails', async () => {
const enrolled = createAccountStore()
enrolled.security.mfa.enabled = true
Object.assign(store, enrolled)
server.use(
http.post(`${base}/api/account/security/mfa/totp-verification`, () =>
HttpResponse.json({ error: 'Invalid code.' }, { status: 400 }),
),
)
renderWithClient()
await openSecurityTab('MFA')
fireEvent.click(await screen.findByRole('button', { name: 'Verify code' }))
fireEvent.change(await screen.findByLabelText('Authenticator code'), { target: { value: '000000' } })
fireEvent.click(screen.getByRole('dialog').querySelector('button[type="submit"]') as HTMLElement)
await waitFor(() => expect(errorToast).toHaveBeenCalledWith('Invalid code.'))
expect(screen.getByLabelText('Authenticator code')).toBeTruthy()
})
it('keeps the disable dialog open when disabling MFA fails', async () => {
const enrolled = createAccountStore()
enrolled.security.mfa.enabled = true
Object.assign(store, enrolled)
server.use(
http.delete(`${base}/api/account/security/mfa/totp`, () =>
HttpResponse.json({ error: 'Wrong password.' }, { status: 400 }),
),
)
renderWithClient()
await openSecurityTab('MFA')
fireEvent.click(await screen.findByRole('button', { name: 'Disable MFA' }))
fireEvent.change(await screen.findByLabelText('Password'), { target: { value: 'bad' } })
fireEvent.click(screen.getByRole('button', { name: 'Disable authenticator app' }))
await waitFor(() => expect(errorToast).toHaveBeenCalledWith('Wrong password.'))
expect(screen.getByLabelText('Password')).toBeTruthy()
})
it('keeps the enroll dialog open when MFA verification after enrollment fails', async () => {
server.use(
http.post(`${base}/api/account/security/mfa/totp-enrollment`, () => HttpResponse.json({ secret: 'SEKRET' })),
http.post(`${base}/api/account/security/mfa/totp-verification`, () =>
HttpResponse.json({ error: 'Invalid code.' }, { status: 400 }),
),
)
renderWithClient()
await openSecurityTab('MFA')
fireEvent.click(await screen.findByRole('button', { name: /Set up authenticator app/ }))
fireEvent.change(await screen.findByLabelText('Password'), { target: { value: 'pw' } })
fireEvent.click(screen.getByRole('dialog').querySelector('button[type="submit"]') as HTMLElement)
expect(await screen.findByText('SEKRET')).toBeTruthy()
fireEvent.change(await screen.findByLabelText('Authenticator code'), { target: { value: '000000' } })
fireEvent.click(screen.getByRole('dialog').querySelector('button[type="submit"]') as HTMLElement)
await waitFor(() => expect(errorToast).toHaveBeenCalledWith('Invalid code.'))
expect(screen.getByText('SEKRET')).toBeTruthy()
})
it('keeps the passkey dialog open when enrollment fails', async () => {
vi.stubGlobal('navigator', {
...navigator,
credentials: { create: vi.fn().mockRejectedValue(new Error('Passkey registration was cancelled.')) },
})
server.use(
http.get(`${base}/api/auth/passkey/generate-register-options`, () =>
HttpResponse.json({ publicKey: { challenge: 'AQID', user: { id: 'BAUG', name: 'j', displayName: 'J' } } }),
),
)
renderWithClient()
await openSecurityTab('Passkeys')
fireEvent.click(await screen.findByRole('button', { name: /Add passkey/ }))
fireEvent.change(await screen.findByLabelText('Passkey name'), { target: { value: 'Laptop' } })
fireEvent.click(screen.getByRole('dialog').querySelector('button[type="submit"]') as HTMLElement)
await waitFor(() => expect(errorToast).toHaveBeenCalledWith('Passkey registration was cancelled.'))
expect(screen.getByLabelText('Passkey name')).toBeTruthy()
vi.unstubAllGlobals()
})
it('renders the QR code and enrollment URI in the setup panel', async () => {
server.use(
http.post(`${base}/api/account/security/mfa/totp-enrollment`, () =>
HttpResponse.json({ qrCode: 'data:image/png;base64,QR', otpAuthUri: 'otpauth://abc' }),
),
)
renderWithClient()
await openSecurityTab('MFA')
fireEvent.click(await screen.findByRole('button', { name: /Set up authenticator app/ }))
fireEvent.change(await screen.findByLabelText('Password'), { target: { value: 'pw' } })
fireEvent.click(screen.getByRole('dialog').querySelector('button[type="submit"]') as HTMLElement)
expect(await screen.findByAltText('Authenticator app QR code')).toBeTruthy()
expect(screen.getByText('otpauth://abc')).toBeTruthy()
})
it('renders QR-only enrollment details when no manual setup value is returned', async () => {
server.use(
http.post(`${base}/api/account/security/mfa/totp-enrollment`, () =>
HttpResponse.json({ qrCode: 'data:image/png;base64,QR' }),
),
)
renderWithClient()
await openSecurityTab('MFA')
fireEvent.click(await screen.findByRole('button', { name: /Set up authenticator app/ }))
fireEvent.change(await screen.findByLabelText('Password'), { target: { value: 'pw' } })
fireEvent.click(screen.getByRole('dialog').querySelector('button[type="submit"]') as HTMLElement)
expect(await screen.findByAltText('Authenticator app QR code')).toBeTruthy()
expect(screen.queryByText('Manual setup key')).toBeNull()
expect(screen.queryByText('Enrollment URI')).toBeNull()
})
it('cancels the TOTP enroll, verify, disable, and passkey dialogs', async () => {
const enrolled = createAccountStore()
enrolled.security.mfa.enabled = true
Object.assign(store, enrolled)
renderWithClient()
await openSecurityTab('MFA')
fireEvent.click(await screen.findByRole('button', { name: 'Verify code' }))
fireEvent.click(await screen.findByRole('button', { name: 'Cancel' }))
await waitFor(() => expect(screen.queryByLabelText('Authenticator code')).toBeNull())
fireEvent.click(screen.getByRole('button', { name: 'Disable MFA' }))
fireEvent.click(await screen.findByRole('button', { name: 'Cancel' }))
await waitFor(() => expect(screen.queryByLabelText('Password')).toBeNull())
await openSecurityTab('Passkeys')
fireEvent.click(screen.getByRole('button', { name: /Add passkey/ }))
fireEvent.click(await screen.findByRole('button', { name: 'Cancel' }))
await waitFor(() => expect(screen.queryByLabelText('Passkey name')).toBeNull())
expect(success).not.toHaveBeenCalled()
})
it('resets MFA and passkey editor state when dialogs are dismissed from their close control', async () => {
renderWithClient()
await openSecurityTab('MFA')
fireEvent.click(await screen.findByRole('button', { name: /Set up authenticator app/ }))
fireEvent.change(await screen.findByLabelText('Password'), { target: { value: 'temporary' } })
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
await waitFor(() => expect(screen.queryByLabelText('Password')).toBeNull())
await openSecurityTab('Passkeys')
fireEvent.click(screen.getByRole('button', { name: /Add passkey/ }))
fireEvent.change(await screen.findByLabelText('Passkey name'), { target: { value: 'Temporary key' } })
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
await waitFor(() => expect(screen.queryByLabelText('Passkey name')).toBeNull())
})
it('resets verify and disable credentials when enrolled MFA dialogs are dismissed', async () => {
const enrolled = createAccountStore()
enrolled.security.mfa.enabled = true
Object.assign(store, enrolled)
renderWithClient()
await openSecurityTab('MFA')
fireEvent.click(await screen.findByRole('button', { name: 'Verify code' }))
fireEvent.change(await screen.findByLabelText('Authenticator code'), { target: { value: '123456' } })
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
await waitFor(() => expect(screen.queryByLabelText('Authenticator code')).toBeNull())
fireEvent.click(screen.getByRole('button', { name: 'Disable MFA' }))
fireEvent.change(await screen.findByLabelText('Password'), { target: { value: 'temporary' } })
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
await waitFor(() => expect(screen.queryByLabelText('Password')).toBeNull())
})
it('cancels the TOTP enroll dialog and clears enrollment', async () => {
renderWithClient()
await openSecurityTab('MFA')
fireEvent.click(await screen.findByRole('button', { name: /Set up authenticator app/ }))
fireEvent.click(await screen.findByRole('button', { name: 'Cancel' }))
await waitFor(() => expect(screen.queryByLabelText('Password')).toBeNull())
})
it('hides the sessions panel when sessions view is disabled', async () => {
const disabled = configz()
disabled.accountCenter = { ...disabled.accountCenter, sessionsViewEnabled: false }
server.use(http.get(`${base}/api/configz`, () => HttpResponse.json(disabled)))
renderWithClient()
expect(await screen.findByRole('button', { name: 'Manage authenticator' })).toBeTruthy()
expect(screen.queryByRole('button', { name: 'Manage sessions' })).toBeNull()
})
it('hides the password panel when password change is disabled', async () => {
const disabled = configz()
disabled.accountCenter = { ...disabled.accountCenter, passwordChangeEnabled: false }
server.use(http.get(`${base}/api/configz`, () => HttpResponse.json(disabled)))
renderWithClient()
expect(await screen.findByRole('button', { name: 'Manage authenticator' })).toBeTruthy()
expect(screen.queryByRole('button', { name: /Change password/ })).toBeNull()
})
it('shows the plural passkey count meta with multiple passkeys', async () => {
const withPasskeys = createAccountStore()
withPasskeys.passkeys = [
{ id: 'pk-1', name: 'One', deviceType: 'platform', backedUp: true, createdAt: '2026-01-01T00:00:00.000Z' },
{ id: 'pk-2', name: null, deviceType: 'cross-platform', backedUp: false, createdAt: null },
]
Object.assign(store, withPasskeys)
renderWithClient()
await openSecurityTab('Passkeys')
expect(await screen.findByText('2 registered keys, managed individually.')).toBeTruthy()
expect(screen.getByText('Unnamed passkey')).toBeTruthy()
})
it('uses empty collection fallbacks when optional security payload fields are absent', async () => {
const sparseConfig = configz()
Object.assign(sparseConfig, {
identityProviders: undefined,
builtInProviders: { ...sparseConfig.builtInProviders, web3Wallet: undefined },
})
server.use(
http.get(`${base}/api/configz`, () => HttpResponse.json(sparseConfig)),
http.get(`${base}/api/account/security`, () => HttpResponse.json({})),
http.get(`${base}/api/account/security/passkeys`, () => HttpResponse.json({})),
http.get(`${base}/api/account/sessions`, () => HttpResponse.json({})),
http.get(`${base}/api/account/linked-accounts`, () => HttpResponse.json({})),
)
renderWithClient()
expect(await screen.findByText('No external sign-in Providers')).toBeTruthy()
await openSecurityTab('Passkeys')
expect(screen.getByText('No passkeys have been added yet.')).toBeTruthy()
await openSecurityTab('Sessions')
expect(screen.getByText('No other active sessions.')).toBeTruthy()
})
})