import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import EmailVerifyView from '@/views/auth/EmailVerifyView.vue' const { pushMock, showSuccessMock, showErrorMock, registerMock, setTokenMock, clearPendingAuthSessionMock, getPublicSettingsMock, sendVerifyCodeMock, sendPendingOAuthVerifyCodeMock, persistOAuthTokenContextMock, apiClientPostMock, authStoreState, } = vi.hoisted(() => ({ pushMock: vi.fn(), showSuccessMock: vi.fn(), showErrorMock: vi.fn(), registerMock: vi.fn(), setTokenMock: vi.fn(), clearPendingAuthSessionMock: vi.fn(), getPublicSettingsMock: vi.fn(), sendVerifyCodeMock: vi.fn(), sendPendingOAuthVerifyCodeMock: vi.fn(), persistOAuthTokenContextMock: vi.fn(), apiClientPostMock: vi.fn(), authStoreState: { pendingAuthSession: null as null | { token: string token_field: 'pending_auth_token' | 'pending_oauth_token' provider: string redirect?: string adoption_required?: boolean suggested_display_name?: string suggested_avatar_url?: string } }, })) vi.mock('vue-router', () => ({ useRouter: () => ({ push: pushMock, }), })) vi.mock('vue-i18n', () => ({ createI18n: () => ({ global: { t: (key: string) => key, }, }), useI18n: () => ({ t: (key: string, params?: Record) => { if (key === 'auth.accountCreatedSuccess') { return `Account created for ${params?.siteName ?? 'Sub2API'}` } return key }, locale: { value: 'en' }, }), })) vi.mock('@/stores', () => ({ useAuthStore: () => ({ pendingAuthSession: authStoreState.pendingAuthSession, register: (...args: any[]) => registerMock(...args), setToken: (...args: any[]) => setTokenMock(...args), clearPendingAuthSession: (...args: any[]) => clearPendingAuthSessionMock(...args), }), useAppStore: () => ({ showSuccess: (...args: any[]) => showSuccessMock(...args), showError: (...args: any[]) => showErrorMock(...args), }), })) vi.mock('@/api/auth', async () => { const actual = await vi.importActual('@/api/auth') return { ...actual, getPublicSettings: (...args: any[]) => getPublicSettingsMock(...args), sendVerifyCode: (...args: any[]) => sendVerifyCodeMock(...args), sendPendingOAuthVerifyCode: (...args: any[]) => sendPendingOAuthVerifyCodeMock(...args), persistOAuthTokenContext: (...args: any[]) => persistOAuthTokenContextMock(...args), } }) vi.mock('@/api/client', () => ({ apiClient: { post: (...args: any[]) => apiClientPostMock(...args), }, })) describe('EmailVerifyView', () => { beforeEach(() => { pushMock.mockReset() showSuccessMock.mockReset() showErrorMock.mockReset() registerMock.mockReset() setTokenMock.mockReset() clearPendingAuthSessionMock.mockReset() getPublicSettingsMock.mockReset() sendVerifyCodeMock.mockReset() sendPendingOAuthVerifyCodeMock.mockReset() persistOAuthTokenContextMock.mockReset() apiClientPostMock.mockReset() authStoreState.pendingAuthSession = null sessionStorage.clear() getPublicSettingsMock.mockResolvedValue({ turnstile_enabled: false, turnstile_site_key: '', site_name: 'Sub2API', registration_email_suffix_whitelist: [], }) sendVerifyCodeMock.mockResolvedValue({ countdown: 60 }) sendPendingOAuthVerifyCodeMock.mockResolvedValue({ countdown: 60 }) setTokenMock.mockResolvedValue({}) }) it('uses the pending oauth verify-code endpoint when register data carries a pending auth session', async () => { authStoreState.pendingAuthSession = { token: 'pending-token-1', token_field: 'pending_auth_token', provider: 'wechat', redirect: '/profile', } sessionStorage.setItem( 'register_data', JSON.stringify({ email: 'fresh@example.com', password: 'secret-123', }) ) mount(EmailVerifyView, { global: { stubs: { AuthLayout: { template: '
' }, Icon: true, TurnstileWidget: true, transition: false, }, }, }) await flushPromises() expect(sendPendingOAuthVerifyCodeMock).toHaveBeenCalledWith({ email: 'fresh@example.com', pending_auth_token: 'pending-token-1', }) expect(sendVerifyCodeMock).not.toHaveBeenCalled() }) it('skips the registration email suffix whitelist for pending oauth verification', async () => { authStoreState.pendingAuthSession = { token: 'pending-token-2', token_field: 'pending_auth_token', provider: 'oidc', redirect: '/profile', } getPublicSettingsMock.mockResolvedValue({ turnstile_enabled: false, turnstile_site_key: '', site_name: 'Sub2API', registration_email_suffix_whitelist: ['allowed.com'], }) sessionStorage.setItem( 'register_data', JSON.stringify({ email: 'fresh@example.com', password: 'secret-123', }) ) mount(EmailVerifyView, { global: { stubs: { AuthLayout: { template: '
' }, Icon: true, TurnstileWidget: true, transition: false, }, }, }) await flushPromises() expect(sendPendingOAuthVerifyCodeMock).toHaveBeenCalledWith({ email: 'fresh@example.com', pending_auth_token: 'pending-token-2', }) expect(showErrorMock).not.toHaveBeenCalled() }) it('submits pending auth account creation when session storage has no pending metadata but auth store does', async () => { authStoreState.pendingAuthSession = { token: 'pending-token-1', token_field: 'pending_auth_token', provider: 'wechat', redirect: '/profile', } sessionStorage.setItem( 'register_data', JSON.stringify({ email: 'fresh@example.com', password: 'secret-123', }) ) apiClientPostMock.mockResolvedValue({ data: { access_token: 'oauth-access-token', refresh_token: 'oauth-refresh-token', expires_in: 3600, token_type: 'Bearer', }, }) const wrapper = mount(EmailVerifyView, { global: { stubs: { AuthLayout: { template: '
' }, Icon: true, TurnstileWidget: true, transition: false, }, }, }) await flushPromises() await wrapper.get('#code').setValue('123456') await wrapper.get('form').trigger('submit.prevent') await flushPromises() expect(apiClientPostMock).toHaveBeenCalledWith('/auth/oauth/pending/create-account', { email: 'fresh@example.com', password: 'secret-123', verify_code: '123456', }) expect(persistOAuthTokenContextMock).toHaveBeenCalledWith({ access_token: 'oauth-access-token', refresh_token: 'oauth-refresh-token', expires_in: 3600, token_type: 'Bearer', }) expect(setTokenMock).toHaveBeenCalledWith('oauth-access-token') expect(clearPendingAuthSessionMock).toHaveBeenCalled() expect(pushMock).toHaveBeenCalledWith('/profile') expect(registerMock).not.toHaveBeenCalled() }) it('keeps the normal email registration flow unchanged', async () => { sessionStorage.setItem( 'register_data', JSON.stringify({ email: 'normal@example.com', password: 'secret-456', promo_code: 'PROMO', invitation_code: 'INVITE', }) ) registerMock.mockResolvedValue({}) const wrapper = mount(EmailVerifyView, { global: { stubs: { AuthLayout: { template: '
' }, Icon: true, TurnstileWidget: true, transition: false, }, }, }) await flushPromises() await wrapper.get('#code').setValue('654321') await wrapper.get('form').trigger('submit.prevent') await flushPromises() expect(registerMock).toHaveBeenCalledWith({ email: 'normal@example.com', password: 'secret-456', verify_code: '654321', turnstile_token: undefined, promo_code: 'PROMO', invitation_code: 'INVITE', }) expect(apiClientPostMock).not.toHaveBeenCalled() expect(pushMock).toHaveBeenCalledWith('/dashboard') }) })