src/features/account/account-center.test.tsx
Rama main · réplica de lectura del repositorio original.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import type { ReactNode } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
AccountConnectionsPage,
AccountDataPrivacyPage,
AccountProfilePage,
AccountSecurityPage,
} from '@/features/account/account-center'
import { AccountCenterLayout } from '@/features/account/account-surface'
import {
asRecord,
base64UrlToBuffer,
bufferToBase64Url,
formatSessionDevice,
readChainId,
readFirstString,
readRedirectUrl,
readRequiredString,
readString,
} from '@/features/account/utils'
vi.mock('@tanstack/react-router', () => ({
Link: ({ children, className, to }: { children: ReactNode; className?: string; to: string }) => (
<a className={className} href={to}>
{children}
</a>
),
useNavigate: () => vi.fn(),
}))
afterEach(() => {
cleanup()
vi.restoreAllMocks()
})
describe('account pages', () => {
it('normalizes account helper values', () => {
expect(bufferToBase64Url(new Uint8Array([251, 255]).buffer)).toBe('-_8')
expect(Array.from(new Uint8Array(base64UrlToBuffer('-_8')))).toEqual([251, 255])
expect(asRecord({ id: 'value' })).toEqual({ id: 'value' })
expect(asRecord(null)).toEqual({})
expect(readString('value')).toBe('value')
expect(readString('')).toBeNull()
expect(readFirstString(['first'])).toBe('first')
expect(readFirstString([1])).toBeNull()
expect(readChainId(1)).toBe(1)
expect(readChainId('0x2105')).toBe(8453)
expect(readChainId('10')).toBe(10)
expect(() => readChainId(null)).toThrow('Wallet did not return a chain ID.')
expect(readRequiredString('challenge', 'challenge')).toBe('challenge')
expect(() => readRequiredString('', 'challenge')).toThrow('Passkey registration option challenge is required.')
expect(readRedirectUrl({ url: '/profile' })).toBe('/profile')
expect(readRedirectUrl({ redirectTo: '/settings' })).toBe('/settings')
expect(readRedirectUrl({ callbackURL: '/callback' })).toBe('/callback')
expect(readRedirectUrl({ url: 1 })).toBeNull()
expect(formatSessionDevice(null)).toBe('Unknown device')
expect(formatSessionDevice('Custom Agent')).toBe('Custom Agent')
expect(formatSessionDevice('Mozilla/5.0 (Mac OS X) Chrome/120')).toBe('Chrome on macOS')
})
it('profile page only loads profile-owned account data [spec: account-center/account-section-routes]', async () => {
const requests = mockAccountFetch()
renderWithClient(<AccountProfilePage />)
expect(await screen.findByRole('heading', { name: 'Account settings' })).toBeTruthy()
await waitFor(() =>
expect(requests).toEqual(['/api/configz', '/api/account/profile', '/api/account/developer-console-access']),
)
})
it('security page loads summaries once and reuses data when opening a drawer', async () => {
const requests = mockAccountFetch()
renderWithClient(<AccountSecurityPage />)
expect(await screen.findByRole('heading', { name: 'Account settings' })).toBeTruthy()
await waitFor(() => expect(requests).toContain('/api/account/security/passkeys'))
fireEvent.click(await screen.findByRole('button', { name: 'Manage passkeys' }))
expect(await screen.findByRole('heading', { name: 'Your passkeys' })).toBeTruthy()
expect(requests.filter((path) => path === '/api/account/security/passkeys')).toHaveLength(1)
expect(screen.queryByRole('button', { name: 'Delete account' })).toBeNull()
})
it('data and privacy page owns export and deletion actions [spec: account-center/account-data-export]', async () => {
const requests = mockAccountFetch()
renderWithClient(<AccountDataPrivacyPage />)
expect(await screen.findByRole('heading', { name: 'Data & privacy' })).toBeTruthy()
expect(screen.getByText('Export account data')).toBeTruthy()
expect(screen.getByRole('button', { name: 'Download data' })).toBeTruthy()
expect(screen.getByRole('button', { name: 'Delete account' })).toBeTruthy()
await waitFor(() =>
expect(requests).toEqual(['/api/configz', '/api/account/profile', '/api/account/developer-console-access']),
)
})
it('connections page loads connection-owned account data', async () => {
const requests = mockAccountFetch()
renderWithClient(<AccountConnectionsPage />)
expect(await screen.findByRole('heading', { name: 'Access management' })).toBeTruthy()
await waitFor(() =>
expect(requests).toEqual([
'/api/configz',
'/api/account/profile',
'/api/account/developer-console-access',
'/api/account/provider-connectors?page=1&pageSize=100',
'/api/account/provider-connections?page=1&pageSize=100',
]),
)
})
})
function renderWithClient(ui: ReactNode) {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
render(
<QueryClientProvider client={queryClient}>
<AccountCenterLayout section="overview">{ui}</AccountCenterLayout>
</QueryClientProvider>,
)
}
function mockAccountFetch() {
const requests: string[] = []
vi.spyOn(window, 'fetch').mockImplementation((input) => {
const path = String(input)
requests.push(path)
if (path === '/api/configz') return Promise.resolve(jsonResponse(configz()))
if (path === '/api/account/profile') {
return Promise.resolve(
jsonResponse({
user: profile(),
}),
)
}
if (path === '/api/account/developer-console-access') {
return Promise.resolve(
jsonResponse({
canCreateOrganization: true,
showOrganizations: false,
platformOperator: false,
consoleOrganizations: [],
}),
)
}
const pagination = { page: 1, pageSize: 50, totalItems: 0, totalPages: 0 }
if (path === '/api/account/security') return Promise.resolve(jsonResponse({ security }))
if (path === '/api/account/security/passkeys') return Promise.resolve(jsonResponse({ passkeys: [] }))
if (path === '/api/account/sessions') return Promise.resolve(jsonResponse({ items: [], pagination }))
if (path === '/api/account/linked-accounts') {
return Promise.resolve(jsonResponse({ items: linkedAccounts, pagination }))
}
if (path.startsWith('/api/account/provider-connectors')) {
return Promise.resolve(jsonResponse({ items: [], pagination }))
}
if (path.startsWith('/api/account/provider-connections')) {
return Promise.resolve(jsonResponse({ items: [], pagination }))
}
if (path === '/api/account/application-authorizations')
return Promise.resolve(jsonResponse({ items: [], pagination }))
if (path === '/api/account/agents') return Promise.resolve(jsonResponse({ items: [], pagination }))
if (path === '/api/account/api-resources') return Promise.resolve(jsonResponse({ items: [], pagination }))
if (path === '/api/account/account-connections') return Promise.resolve(jsonResponse({ items: [], pagination }))
return Promise.resolve(jsonResponse({}))
})
return requests
}
function jsonResponse(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
})
}
function configz() {
return {
onboarding: { required: false, href: '/onboarding' },
signIn: {
passwordEnabled: true,
signupEnabled: true,
socialLoginEnabled: true,
emailOtpEnabled: true,
usernameEnabled: true,
identifierFirst: false,
},
builtInProviders: {
email: { enabled: true },
phone: { enabled: false },
web3Wallet: { enabled: true, allowSignUp: true, chains: [1] },
passkey: { allowSignUp: true },
oneTap: {
enabled: false,
clientId: '',
autoSelect: false,
cancelOnTapOutside: true,
uxMode: 'popup',
context: 'signin',
promptBaseDelayMs: 0,
promptMaxAttempts: 1,
},
},
branding: { logoUrl: null, faviconUrl: null, primaryColor: null, backgroundColor: null, customCss: null },
identityProviders: [
{ slug: 'github', providerType: 'social', providerId: 'github', displayName: 'GitHub', icon: 'github' },
],
links: { termsUri: null, privacyUri: null, supportEmail: null },
copy: { productName: 'Realmroot', headline: 'Sign in', description: 'Secure access' },
auth: {
basePath: '/api/auth',
signInEmailPath: '/api/auth/sign-in/email',
signInUsernamePath: '/api/auth/sign-in/username',
signUpEmailPath: '/api/auth/sign-up/email',
signOutPath: '/api/auth/sign-out',
sendVerificationEmailPath: '/api/auth/send-verification-email',
verifyEmailPath: '/api/auth/verify-email',
emailOtpPath: '/api/auth/email-otp/send-verification-otp',
emailOtpSignInPath: '/api/auth/sign-in/email-otp',
emailOtpVerificationPath: '/api/auth/email-otp/verify-email',
emailOtpPasswordResetRequestPath: '/api/auth/email-otp/request-password-reset',
emailOtpPasswordResetPath: '/api/auth/email-otp/reset-password',
},
oidc: {
issuer: 'https://auth.example.com',
discoveryUrl: '/.well-known/openid-configuration',
authorizationEndpoint: '/oauth2/authorize',
tokenEndpoint: '/oauth2/token',
jwksUri: '/jwks',
userInfoEndpoint: '/userinfo',
endSessionEndpoint: '/logout',
},
security: { mfaRequired: false, sessionExpiresInSeconds: 3600, passkeysEnabled: true },
navigation: { externalLinks: [] },
accountCenter: {
profileEditingEnabled: true,
displayNameEditable: true,
usernameEditable: true,
avatarEditable: true,
emailChangeEnabled: true,
passwordChangeEnabled: true,
connectedAccountsEnabled: true,
sessionsViewEnabled: true,
dangerZoneEnabled: false,
},
captcha: { enabled: false, provider: 'turnstile', siteKey: '' },
}
}
function profile() {
return {
id: 'user-1',
email: 'jane@example.com',
emailVerified: true,
displayName: 'Jane Stone',
username: 'jane',
avatarAssetId: null,
image: null,
bio: null,
location: null,
links: [],
role: 'user',
}
}
const security = {
mfa: { enabled: false, factors: [] },
passkeys: { enabled: true, count: 0 },
policy: { mfa: { mode: 'optional' }, passkeys: { enabled: true, rpName: 'Realmroot' } },
}
const linkedAccounts = [
{ id: 'account-1', accountId: 'account-1', providerId: 'github', createdAt: '2026-01-01T00:00:00.000Z' },
]