import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import { MemoryRouter, Route, Routes } from 'react-router-dom' import { HelmetProvider } from 'react-helmet-async' import { VerifyEmailPage } from '../VerifyEmailPage' import { authApi } from '@/api/auth' import { useAuthStore } from '@/store/authStore' import type { User } from '@/types' vi.mock('@/api/auth', () => ({ authApi: { verifyEmail: vi.fn(), sendVerificationEmail: vi.fn(), me: vi.fn(), }, })) vi.mock('@/lib/toast', () => ({ toast: { success: vi.fn(), error: vi.fn(), }, })) function makeUser(overrides: Partial = {}): User { return { id: 'user-1', email: 'test@example.com', name: 'Test User', role: 'engineer', is_super_admin: false, is_active: true, must_change_password: false, account_id: 'acct-1', account_role: 'engineer', team_id: null, created_at: '2026-05-01T00:00:00Z', last_login: null, phone: null, job_title: null, timezone: 'UTC', avatar_url: null, email_verified_at: null, ...overrides, } } function renderPage(initialPath: string) { return render( } /> dashboard} /> , ) } describe('VerifyEmailPage', () => { beforeEach(() => { vi.useFakeTimers({ shouldAdvanceTime: true }) useAuthStore.setState({ user: null, token: null, isAuthenticated: false, }) vi.mocked(authApi.me).mockResolvedValue( makeUser({ email_verified_at: '2026-05-06T00:00:00Z' }), ) }) afterEach(() => { vi.useRealTimers() vi.clearAllMocks() }) it('shows success and redirects on valid token', async () => { useAuthStore.setState({ user: makeUser() }) // Override fetchUser to avoid hitting axios/XHR in jsdom — the page calls // it after a successful verify to refresh `email_verified_at`. useAuthStore.setState({ fetchUser: vi.fn().mockResolvedValue(undefined) }) vi.mocked(authApi.verifyEmail).mockResolvedValue(undefined) renderPage('/verify-email?token=valid-token') await waitFor(() => { expect(authApi.verifyEmail).toHaveBeenCalledWith('valid-token') }) await waitFor(() => { expect(screen.getByText(/Email verified/i)).toBeInTheDocument() }) // Advance past the redirect delay. vi.advanceTimersByTime(2000) await waitFor(() => { expect(screen.getByText('dashboard')).toBeInTheDocument() }) }) it('shows already-verified state when user is already verified', async () => { useAuthStore.setState({ user: makeUser({ email_verified_at: '2026-05-05T00:00:00Z' }), }) renderPage('/verify-email?token=any-token') await waitFor(() => { expect( screen.getByText(/already verified/i), ).toBeInTheDocument() }) // The verify endpoint must NOT have been called when the user is already // verified — that would burn a perfectly good token for no reason. expect(authApi.verifyEmail).not.toHaveBeenCalled() }) it('only calls verifyEmail once even if the effect re-runs (strict-mode guard)', async () => { useAuthStore.setState({ user: makeUser() }) useAuthStore.setState({ fetchUser: vi.fn().mockResolvedValue(undefined) }) vi.mocked(authApi.verifyEmail).mockResolvedValue(undefined) const { rerender } = render( } /> dashboard} /> , ) // Force a re-render to simulate React 19 strict-mode double-invoke. rerender( } /> dashboard} /> , ) await waitFor(() => { expect(authApi.verifyEmail).toHaveBeenCalled() }) expect(authApi.verifyEmail).toHaveBeenCalledTimes(1) }) it('shows an error state with a resend CTA on invalid token', async () => { useAuthStore.setState({ user: makeUser() }) vi.mocked(authApi.verifyEmail).mockRejectedValue( Object.assign(new Error('boom'), { response: { data: { detail: 'Token expired' } }, }), ) renderPage('/verify-email?token=stale-token') await waitFor(() => { expect(screen.getByText(/Verification failed/i)).toBeInTheDocument() }) expect(screen.getByText(/Token expired/i)).toBeInTheDocument() expect(screen.getByTestId('resend-button')).toBeInTheDocument() }) })