feat(auth): add configurable email verification enforcement
Auth:RequireEmailVerification (default off) gates whether local register requires confirming email before login. OAuth new-user paths are untouched -- Google/Microsoft already assert a verified email. Adds verify-email and resend-verification-email endpoints, mirroring the existing reset-password enumeration-avoidance and rate-limiting patterns, plus a login-embedded resend affordance and a verify-email landing page on the frontend. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import VerifyEmailPage from './views/VerifyEmailPage';
|
||||
import { I18nProvider } from './i18n/I18nProvider';
|
||||
import { api, getApiErrorMessage } from './api';
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>;
|
||||
// CRA's jest config sets resetMocks: true, which wipes the initial implementation given to
|
||||
// jest.fn() in setupTests.ts before every test -- re-arm it here so error-derived text is testable.
|
||||
const mockedGetApiErrorMessage = getApiErrorMessage as jest.Mock;
|
||||
|
||||
function renderVerifyEmailPage(search: string) {
|
||||
window.history.pushState({}, '', `/verify-email${search}`);
|
||||
return render(
|
||||
<MemoryRouter initialEntries={[`/verify-email${search}`]} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||
<I18nProvider>
|
||||
<VerifyEmailPage />
|
||||
</I18nProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('VerifyEmailPage', () => {
|
||||
beforeEach(() => {
|
||||
mockedApi.post.mockReset();
|
||||
mockedGetApiErrorMessage.mockImplementation((e: any, fallback?: string) => {
|
||||
const data = e?.response?.data;
|
||||
return typeof data === 'string' && data.trim() ? data.trim() : fallback;
|
||||
});
|
||||
});
|
||||
|
||||
it('confirms the account and shows success when the link is valid', async () => {
|
||||
mockedApi.post.mockResolvedValueOnce({ data: {} } as any);
|
||||
|
||||
renderVerifyEmailPage('?userId=user-1&token=good-token');
|
||||
|
||||
expect(await screen.findByText('Your email has been verified. You can now sign in.')).toBeInTheDocument();
|
||||
expect(mockedApi.post).toHaveBeenCalledWith('/auth/verify-email', { userId: 'user-1', token: 'good-token' });
|
||||
});
|
||||
|
||||
it('shows an error when the link is invalid or expired', async () => {
|
||||
mockedApi.post.mockRejectedValueOnce({ response: { status: 400, data: 'Invalid or expired link.' } });
|
||||
|
||||
renderVerifyEmailPage('?userId=user-1&token=bad-token');
|
||||
|
||||
expect(await screen.findByText('Invalid or expired link.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows an error without calling the API when the link is missing userId/token', async () => {
|
||||
renderVerifyEmailPage('');
|
||||
|
||||
expect(await screen.findByText('Missing user/token in link.')).toBeInTheDocument();
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user