test(auth): reproduce Analytics return-path regression
This commit is contained in:
@@ -31,9 +31,9 @@ const mockedApi = api as jest.Mocked<typeof api>;
|
||||
|
||||
let consoleErrorSpy: jest.SpyInstance;
|
||||
|
||||
function renderLoginPage(initialMode: "login" | "register" = "login", from?: string) {
|
||||
function renderLoginPage(initialMode: "login" | "register" = "login", from?: string, fromState?: unknown, search = "") {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={[{ pathname: "/login", state: from ? { from } : undefined }]}>
|
||||
<MemoryRouter initialEntries={[{ pathname: "/login", search, state: from ? { from, fromState } : undefined }]}>
|
||||
<I18nProvider>
|
||||
<ToastProvider>
|
||||
<LoginPage initialMode={initialMode} />
|
||||
@@ -227,6 +227,58 @@ describe('LoginPage', () => {
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/dashboard', { replace: true }));
|
||||
});
|
||||
|
||||
it.each([
|
||||
'https://evil.example/analytics', '//evil.example/analytics', '/\t/evil.example',
|
||||
'/%2f%2fevil.example', '/%252f%252fevil.example', '/%5cevil.example',
|
||||
'/analytics?months=%ZZ', '/jobs/../login', '/%6cogin', '/register',
|
||||
'/analytics%0a', '/analytics?months=6\n',
|
||||
])('uses the safe fallback for the untrusted return target %j', async (from) => {
|
||||
mockedApi.post.mockResolvedValueOnce({ data: { authenticated: true, provider: 'local' } } as any);
|
||||
renderLoginPage('login', from);
|
||||
await userEvent.type(await screen.findByLabelText('Username or email'), 'person@example.com');
|
||||
await userEvent.type(screen.getByLabelText('Current password'), 'hunter2');
|
||||
await click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/dashboard', { replace: true }));
|
||||
});
|
||||
|
||||
it('preserves encoded internal query and fragment without decoding the navigation target', async () => {
|
||||
mockedApi.post.mockResolvedValueOnce({ data: { authenticated: true, provider: 'local' } } as any);
|
||||
const from = '/analytics?months=%36&context=%C3%B8#trend';
|
||||
renderLoginPage('login', from);
|
||||
await userEvent.type(await screen.findByLabelText('Username or email'), 'person@example.com');
|
||||
await userEvent.type(screen.getByLabelText('Current password'), 'hunter2');
|
||||
await click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith(from, { replace: true }));
|
||||
});
|
||||
|
||||
it('restores valid owned-workspace navigation context separately from the sign-in destination', async () => {
|
||||
mockedApi.post.mockResolvedValueOnce({ data: { authenticated: true, provider: 'local' } } as any);
|
||||
const fromState = { from: '/jobs?q=Engineer', focusJobId: 42 };
|
||||
renderLoginPage('login', '/jobs/42?section=documents#files', fromState);
|
||||
await userEvent.type(await screen.findByLabelText('Username or email'), 'person@example.com');
|
||||
await userEvent.type(screen.getByLabelText('Current password'), 'hunter2');
|
||||
await click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/jobs/42?section=documents#files', { replace: true, state: fromState }));
|
||||
});
|
||||
|
||||
it('does not replay an unsafe nested return context after authentication', async () => {
|
||||
mockedApi.post.mockResolvedValueOnce({ data: { authenticated: true, provider: 'local' } } as any);
|
||||
renderLoginPage('login', '/jobs/42', { from: '/\t/evil.example', focusJobId: 42 });
|
||||
await userEvent.type(await screen.findByLabelText('Username or email'), 'person@example.com');
|
||||
await userEvent.type(screen.getByLabelText('Current password'), 'hunter2');
|
||||
await click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/jobs/42', { replace: true }));
|
||||
});
|
||||
|
||||
it('abandons prior return context on the established account/session disposal entry', async () => {
|
||||
mockedApi.post.mockResolvedValueOnce({ data: { authenticated: true, provider: 'local' } } as any);
|
||||
renderLoginPage('login', '/jobs/42', { from: '/jobs?q=Private', focusJobId: 42 }, '?sessionChanged=1');
|
||||
await userEvent.type(await screen.findByLabelText('Username or email'), 'person@example.com');
|
||||
await userEvent.type(screen.getByLabelText('Current password'), 'hunter2');
|
||||
await click(screen.getByRole('button', { name: 'Sign in' }));
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/dashboard', { replace: true }));
|
||||
});
|
||||
|
||||
it('opens the separate forgot-password page with the typed email prefilled', async () => {
|
||||
renderLoginPage();
|
||||
await screen.findByLabelText('Username or email');
|
||||
|
||||
Reference in New Issue
Block a user