116 lines
5.9 KiB
TypeScript
116 lines
5.9 KiB
TypeScript
import React from 'react';
|
|
import '@testing-library/jest-dom';
|
|
import { act, 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';
|
|
import { translations } from './i18n/translations';
|
|
|
|
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, emailChange = false) {
|
|
const path = emailChange ? '/confirm-email-change' : '/verify-email';
|
|
window.history.pushState({}, '', `${path}${search}`);
|
|
return render(
|
|
<MemoryRouter initialEntries={[`${path}${search}`]}>
|
|
<I18nProvider>
|
|
<VerifyEmailPage emailChange={emailChange} />
|
|
</I18nProvider>
|
|
</MemoryRouter>,
|
|
);
|
|
}
|
|
|
|
describe('VerifyEmailPage', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
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();
|
|
});
|
|
|
|
it('confirms a pending email change and requires a fresh sign-in', async () => {
|
|
mockedApi.post.mockResolvedValueOnce({ data: {} } as any);
|
|
|
|
renderVerifyEmailPage('?userId=user-1&email=new%40example.com&token=change-token', true);
|
|
|
|
expect(await screen.findByText('Your email has changed. Sign in again with the new address.')).toBeInTheDocument();
|
|
expect(mockedApi.post).toHaveBeenCalledWith('/auth/email-change/confirm', { userId: 'user-1', email: 'new@example.com', token: 'change-token' });
|
|
});
|
|
|
|
it.each([false, true])('changing UI language never resubmits a pending confirmation: emailChange=%s', async emailChange => {
|
|
let finish!: (value: any) => void;
|
|
mockedApi.post.mockReturnValue(new Promise(resolve => { finish = resolve; }));
|
|
renderVerifyEmailPage('?userId=user-1&email=new%40example.test&token=one-use', emailChange);
|
|
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
|
act(() => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' })));
|
|
const pendingCalls = mockedApi.post.mock.calls.length;
|
|
await act(async () => finish({ data: {} }));
|
|
expect(pendingCalls).toBe(1);
|
|
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
|
expect(screen.getByRole('status')).toHaveTextContent(translations.nb[emailChange ? 'confirmEmailChangeSuccess' : 'verifyEmailSuccess']);
|
|
act(() => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'en' })));
|
|
expect(screen.getByRole('status')).toHaveTextContent(translations.en[emailChange ? 'confirmEmailChangeSuccess' : 'verifyEmailSuccess']);
|
|
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('localizes missing-link errors without a confirmation request', async () => {
|
|
renderVerifyEmailPage('');
|
|
expect(await screen.findByText(translations.en.missingVerifyLinkInfo)).toBeInTheDocument();
|
|
act(() => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' })));
|
|
expect(screen.getByRole('alert')).toHaveTextContent(translations.nb.missingVerifyLinkInfo);
|
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('localizes a failed confirmation fallback without retrying the one-time command', async () => {
|
|
mockedApi.post.mockRejectedValue({ response: { status: 503, data: '' } });
|
|
renderVerifyEmailPage('?userId=user-1&token=one-use');
|
|
expect(await screen.findByText(translations.en.verifyEmailFailed)).toBeInTheDocument();
|
|
await act(async () => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' })));
|
|
expect(screen.getByRole('alert')).toHaveTextContent(translations.nb.verifyEmailFailed);
|
|
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not let an older confirmation replace a different flow missing-link error', async () => {
|
|
let finish!: (value: any) => void;
|
|
mockedApi.post.mockReturnValue(new Promise(resolve => { finish = resolve; }));
|
|
const view = renderVerifyEmailPage('?userId=user-1&token=one-use');
|
|
window.history.replaceState({}, '', '/confirm-email-change?userId=user-1&token=one-use');
|
|
view.rerender(<MemoryRouter><I18nProvider><VerifyEmailPage emailChange /></I18nProvider></MemoryRouter>);
|
|
expect(await screen.findByText(translations.en.missingEmailChangeLinkInfo)).toBeInTheDocument();
|
|
await act(async () => finish({ data: {} }));
|
|
expect(screen.getByRole('alert')).toHaveTextContent(translations.en.missingEmailChangeLinkInfo);
|
|
expect(mockedApi.post).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|