veza/apps/web/src/features/auth/pages/VerifyEmailPage.test.tsx

373 lines
11 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import { VerifyEmailPage } from './VerifyEmailPage';
import { verifyEmail, resendVerificationEmail } from '../services/authService';
// Mock dependencies
vi.mock('../services/authService', () => ({
verifyEmail: vi.fn(),
resendVerificationEmail: vi.fn(),
}));
const mockNavigate = vi.fn();
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return {
...actual,
useNavigate: () => mockNavigate,
};
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter>{children}</MemoryRouter>
);
describe('VerifyEmailPage', () => {
beforeEach(() => {
vi.clearAllMocks();
mockNavigate.mockClear();
localStorage.clear();
});
afterEach(() => {
vi.clearAllTimers();
});
it('should render verifying state when token is present', async () => {
vi.mocked(verifyEmail).mockResolvedValue(undefined);
2025-12-13 02:34:34 +00:00
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={['/verify-email?token=test-token']}>
{children}
</MemoryRouter>
);
render(<VerifyEmailPage />, { wrapper: wrapperWithToken });
// Should show verifying state initially
await waitFor(() => {
expect(screen.getByText("Vérification de l'email")).toBeInTheDocument();
});
});
it('should display error message when token is missing', () => {
render(<VerifyEmailPage />, { wrapper });
expect(screen.getByText("Vérification de l'email")).toBeInTheDocument();
2025-12-13 02:34:34 +00:00
expect(
screen.getByText('Lien de vérification invalide ou manquant'),
).toBeInTheDocument();
});
it('should extract token from URL and verify email', async () => {
vi.mocked(verifyEmail).mockResolvedValue(undefined);
2025-12-13 02:34:34 +00:00
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={['/verify-email?token=abc123']}>
{children}
</MemoryRouter>
);
render(<VerifyEmailPage />, { wrapper: wrapperWithToken });
// Wait for verification to complete
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(verifyEmail).toHaveBeenCalledWith('abc123');
},
{ timeout: 3000 },
);
});
it('should display success message after successful verification', async () => {
vi.mocked(verifyEmail).mockResolvedValue(undefined);
2025-12-13 02:34:34 +00:00
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={['/verify-email?token=test-token']}>
{children}
</MemoryRouter>
);
render(<VerifyEmailPage />, { wrapper: wrapperWithToken });
// Wait for verifyEmail to be called
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(verifyEmail).toHaveBeenCalled();
},
{ timeout: 2000 },
);
// Wait for success message
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(screen.getByText('Email vérifié')).toBeInTheDocument();
},
{ timeout: 2000 },
);
});
it('should display error message when verification fails', async () => {
const error = { message: 'Token invalide ou expiré' };
vi.mocked(verifyEmail).mockRejectedValue(error);
2025-12-13 02:34:34 +00:00
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={['/verify-email?token=invalid-token']}>
{children}
</MemoryRouter>
);
render(<VerifyEmailPage />, { wrapper: wrapperWithToken });
// Wait for error message
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(
screen.getByText('Token invalide ou expiré'),
).toBeInTheDocument();
},
{ timeout: 3000 },
);
});
it('should redirect to login after successful verification', async () => {
vi.useFakeTimers();
vi.mocked(verifyEmail).mockResolvedValue(undefined);
2025-12-13 02:34:34 +00:00
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={['/verify-email?token=test-token']}>
{children}
</MemoryRouter>
);
render(<VerifyEmailPage />, { wrapper: wrapperWithToken });
// Wait for success
await waitFor(() => {
expect(screen.getByText('Email vérifié')).toBeInTheDocument();
});
// Fast-forward the timer (3 seconds)
await act(async () => {
vi.advanceTimersByTime(3000);
});
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true });
});
vi.useRealTimers();
});
it('should display retry button when verification fails', async () => {
const error = { message: 'Verification failed' };
vi.mocked(verifyEmail).mockRejectedValue(error);
2025-12-13 02:34:34 +00:00
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={['/verify-email?token=test-token']}>
{children}
</MemoryRouter>
);
render(<VerifyEmailPage />, { wrapper: wrapperWithToken });
// Wait for error state
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(screen.getByText('Réessayer')).toBeInTheDocument();
},
{ timeout: 3000 },
);
});
it('should call verifyEmail when retry button is clicked', async () => {
const user = userEvent.setup();
const error = { message: 'Verification failed' };
2025-12-13 02:34:34 +00:00
vi.mocked(verifyEmail)
.mockRejectedValueOnce(error)
.mockResolvedValueOnce(undefined);
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={['/verify-email?token=test-token']}>
{children}
</MemoryRouter>
);
render(<VerifyEmailPage />, { wrapper: wrapperWithToken });
// Wait for error state
await waitFor(() => {
expect(screen.getByText('Réessayer')).toBeInTheDocument();
});
// Click retry button
const retryButton = screen.getByText('Réessayer');
await act(async () => {
await user.click(retryButton);
});
// Should call verifyEmail again
await waitFor(() => {
expect(verifyEmail).toHaveBeenCalledTimes(2);
});
});
it('should display resend email button', async () => {
const error = { message: 'Verification failed' };
vi.mocked(verifyEmail).mockRejectedValue(error);
2025-12-13 02:34:34 +00:00
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={['/verify-email?token=test-token']}>
{children}
</MemoryRouter>
);
render(<VerifyEmailPage />, { wrapper: wrapperWithToken });
// Wait for error state
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(
screen.getByText(/Renvoyer l'email de vérification/),
).toBeInTheDocument();
},
{ timeout: 3000 },
);
});
it('should call resendVerificationEmail when resend button is clicked', async () => {
const user = userEvent.setup();
const error = { message: 'Verification failed' };
vi.mocked(verifyEmail).mockRejectedValue(error);
vi.mocked(resendVerificationEmail).mockResolvedValue(undefined);
2025-12-13 02:34:34 +00:00
localStorage.setItem('pendingVerificationEmail', 'test@example.com');
2025-12-13 02:34:34 +00:00
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={['/verify-email?token=test-token']}>
{children}
</MemoryRouter>
);
render(<VerifyEmailPage />, { wrapper: wrapperWithToken });
// Wait for error state
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(
screen.getByText(/Renvoyer l'email de vérification/),
).toBeInTheDocument();
},
{ timeout: 3000 },
);
// Click resend button
const resendButton = screen.getByText(/Renvoyer l'email de vérification/);
await act(async () => {
await user.click(resendButton);
});
// Should call resendVerificationEmail
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(resendVerificationEmail).toHaveBeenCalledWith(
'test@example.com',
);
},
{ timeout: 3000 },
);
});
it('should set cooldown after resending email', async () => {
const user = userEvent.setup();
const error = { message: 'Verification failed' };
vi.mocked(verifyEmail).mockRejectedValue(error);
vi.mocked(resendVerificationEmail).mockResolvedValue(undefined);
2025-12-13 02:34:34 +00:00
localStorage.setItem('pendingVerificationEmail', 'test@example.com');
2025-12-13 02:34:34 +00:00
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={['/verify-email?token=test-token']}>
{children}
</MemoryRouter>
);
render(<VerifyEmailPage />, { wrapper: wrapperWithToken });
// Wait for error state
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(
screen.getByText(/Renvoyer l'email de vérification/),
).toBeInTheDocument();
},
{ timeout: 2000 },
);
// Click resend button
const resendButton = screen.getByText(/Renvoyer l'email de vérification/);
await act(async () => {
await user.click(resendButton);
});
// Verify that resendVerificationEmail was called
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(resendVerificationEmail).toHaveBeenCalledWith(
'test@example.com',
);
},
{ timeout: 2000 },
);
});
it('should handle resend when email is not found', async () => {
const user = userEvent.setup();
const error = { message: 'Verification failed' };
vi.mocked(verifyEmail).mockRejectedValue(error);
2025-12-13 02:34:34 +00:00
// No email in localStorage
localStorage.clear();
2025-12-13 02:34:34 +00:00
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={['/verify-email?token=test-token']}>
{children}
</MemoryRouter>
);
render(<VerifyEmailPage />, { wrapper: wrapperWithToken });
// Wait for error state
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(
screen.getByText(/Renvoyer l'email de vérification/),
).toBeInTheDocument();
},
{ timeout: 2000 },
);
// Click resend button
const resendButton = screen.getByText(/Renvoyer l'email de vérification/);
await act(async () => {
await user.click(resendButton);
});
// The component should handle the missing email gracefully
// resendVerificationEmail should NOT be called since email is missing
// Wait a bit for async operations
await new Promise((resolve) => setTimeout(resolve, 200));
expect(resendVerificationEmail).not.toHaveBeenCalled();
});
it('should display footer links', () => {
render(<VerifyEmailPage />, { wrapper });
const loginLink = screen.getByText('Retour à la connexion');
expect(loginLink).toBeInTheDocument();
expect(loginLink.closest('a')).toHaveAttribute('href', '/login');
});
});