Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* License text available at https://opensource.org/licenses/MIT
*/
import React from 'react';
import { Navigate, RouteProps } from 'react-router';
import { Navigate, RouteProps, useLocation } from 'react-router';

import config from 'chaire-lib-common/lib/config/shared/project.config';
import { Header } from '../pageParts';
Expand All @@ -21,6 +21,12 @@ export type PrivateRouteProps = RouteProps & {
const PrivateRoute = ({ permissions, component: Component, ...rest }: PrivateRouteProps) => {
const user = useSelector((state: { auth: AuthState }) => state.auth.user);
const isAuthenticated = useSelector((state: { auth: AuthState }) => !!state.auth.isAuthenticated);
const location = useLocation();

// Remember the requested URL so redirectAfterLogin (which reads
// location.state.referrer) can send the user back here after they log in,
// instead of always falling back to the home page (see issue #1946).
const referrer = `${location.pathname}${location.search}${location.hash}`;

return isAuthenticated && user ? (
permissions ? (
Expand All @@ -39,7 +45,7 @@ const PrivateRoute = ({ permissions, component: Component, ...rest }: PrivateRou
</React.Fragment>
)
) : (
<Navigate to="/login" />
<Navigate to="/login" state={{ referrer }} replace />
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright Polytechnique Montreal and contributors
*
* This file is licensed under the MIT License.
* License text available at https://opensource.org/licenses/MIT
*/
import * as React from 'react';
import { render, screen, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Provider } from 'react-redux';
import { MemoryRouter, Routes, Route, useLocation } from 'react-router';

import configureStore from '../../../store/configureStore';

// Header pulls in ESM-only deps (react-markdown) that jest does not transform;
// it is irrelevant to the redirect logic under test, so stub it out.
jest.mock('../../pageParts', () => ({ Header: () => null }));

import PrivateRoute from '../PrivateRoute';

// Login stand-in that exposes the referrer PrivateRoute stored in location.state,
// so tests can assert the originally requested URL is preserved (issue #1946).
const LoginStub = () => {
const location = useLocation();
const referrer = (location.state as { referrer?: string } | null)?.referrer;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As-tu besoin de caster ici ou location.state ne devrait-il pas déjà connaître le type?

return <div>login:{referrer ?? 'none'}</div>;
};

const renderAt = (initialEntry: string) => {
const store = configureStore({ auth: { isAuthenticated: false } });
return render(
<Provider store={store}>
<MemoryRouter initialEntries={[initialEntry]}>
<Routes>
<Route path="/protected" element={<PrivateRoute component={() => <div>protected</div>} />} />
<Route path="/login" element={<LoginStub />} />
</Routes>
</MemoryRouter>
</Provider>
);
};

afterEach(cleanup);

describe('PrivateRoute redirect to login', () => {
test.each([
['/protected', '/protected'],
['/protected?token=abc', '/protected?token=abc'],
['/protected#section', '/protected#section']
])('unauthenticated access to %s redirects to login with referrer', (entry, expectedReferrer) => {
renderAt(entry);
expect(screen.getByText(`login:${expectedReferrer}`)).toBeInTheDocument();
});
});
42 changes: 42 additions & 0 deletions packages/transition-frontend/ui-tests/login-redirect-tests.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright Polytechnique Montreal and contributors
*
* This file is licensed under the MIT License.
* License text available at https://opensource.org/licenses/MIT
*/

import { test } from '@playwright/test';
import * as testHelpers from './testHelpers';
import * as languageTestHelpers from './languageTestHelpers';

// An unauthenticated user opening a protected URL is sent to the login page, then must end up back on
// the originally requested URL after logging in instead of on the home page.
const context = {
page: null as any,
widgetTestCounters: {}
};

// A protected route distinct from the home page (/dashboard), so landing on it
// after login can only come from the preserved referrer, not the default fallback.
const protectedUrl = '/verify/playwrightReferrerToken';

// The tests share a single page and must run in order (navigate -> login -> assert).
test.describe.configure({ mode: 'serial' });

test.beforeAll(async ({ browser }) => {
context.page = await testHelpers.initializeTestPage(browser);
});

// Unauthenticated start lands on the login page; switch to english for stable labels.
testHelpers.hasUrlTest({ context, expectedUrl: '/login' });
languageTestHelpers.switchFromFrenchToEnglish({ context });

// Requesting a protected URL while logged out redirects to the login page.
testHelpers.goToUrlTest({ context, url: protectedUrl });
testHelpers.hasUrlTest({ context, expectedUrl: '/login' });

// After logging in, the user is redirected back to the originally requested URL.
testHelpers.loginTest({ context, loginMethod: 'clickLoginButton' });
testHelpers.hasUrlTest({ context, expectedUrl: protectedUrl });

testHelpers.logoutTest({ context });
12 changes: 12 additions & 0 deletions packages/transition-frontend/ui-tests/testHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type HasTitleTest = (params: { title: Title } & CommonTestParameters) => void;
type IsLanguageTest = (params: { expectedLanguage: AvailableLanguages } & CommonTestParameters) => void;
type SwitchLanguageTest = (params: { languageToSwitch: AvailableLanguages } & CommonTestParameters) => void;
type HasUrlTest = (params: { expectedUrl: Url } & CommonTestParameters) => void;
type GoToUrlTest = (params: { url: Url } & CommonTestParameters) => void;
type LoginTest = (params: { loginMethod: LoginMethods } & CommonTestParameters) => void;
type LogoutTest = (params: CommonTestParameters) => void;
type LeftMenuTest = (params: { section: LeftMenuSections; expectedRightPanelTitle: string } & CommonTestParameters) => void;
Expand Down Expand Up @@ -99,6 +100,17 @@ export const hasUrlTest: HasUrlTest = ({ context, expectedUrl }) => {
});
};

/**
* Navigate the current page to a specific URL (path relative to the base URL).
* @param {Object} options - The options for the test.
* @param {string} options.url - The URL (path) to navigate to.
*/
export const goToUrlTest: GoToUrlTest = ({ context, url }) => {
test(`Navigate to ${url} - ${getTestCounter(context, `goto-${url}`)}`, async () => {
await context.page.goto(url);
});
};

/**
* Test that the language is what we expect.
* @param {Object} options - The options for the test.
Expand Down
Loading