aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2022-09-16 16:55:53 +0200
committersonartech <sonartech@sonarsource.com>2022-09-22 20:03:32 +0000
commitc8b5142dfe1f772f212e740cf54dc487cabac83d (patch)
tree88f78635f62e5ccce875e4ad1cac773d1f703943 /server
parent9e2f4c8b43069a7580b453d7eaa8fa2c60202741 (diff)
downloadsonarqube-c8b5142dfe1f772f212e740cf54dc487cabac83d.tar.gz
sonarqube-c8b5142dfe1f772f212e740cf54dc487cabac83d.zip
SONAR-17027 Fix links when running in a webcontext
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/maintenance/components/App.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/maintenance/components/__tests__/App-test.tsx293
-rw-r--r--server/sonar-web/src/main/js/apps/maintenance/components/__tests__/__snapshots__/App-test.tsx.snap522
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/Login.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/LoginForm.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx144
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-test.tsx52
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginContainer-test.tsx69
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginForm-test.tsx56
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-it.tsx (renamed from server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx)58
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/OAuthProviders-test.tsx60
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/Unauthorized-it.tsx (renamed from server/sonar-web/src/main/js/apps/sessions/components/__tests__/Unauthorized-test.tsx)24
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-it.tsx.snap19
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-test.tsx.snap85
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginContainer-test.tsx.snap18
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginForm-test.tsx.snap228
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap13
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap84
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Unauthorized-test.tsx.snap39
22 files changed, 445 insertions, 1361 deletions
diff --git a/server/sonar-web/src/main/js/apps/maintenance/components/App.tsx b/server/sonar-web/src/main/js/apps/maintenance/components/App.tsx
index 7b943cf2000..4655bb62b1c 100644
--- a/server/sonar-web/src/main/js/apps/maintenance/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/maintenance/components/App.tsx
@@ -45,6 +45,9 @@ interface State {
wasStarting?: boolean;
}
+const DELAY_REDIRECT_PREV_PAGE = 2500;
+const DELAY_REFRESH_STATUS = 5000;
+
export default class App extends React.PureComponent<Props, State> {
interval?: number;
mounted = false;
@@ -109,13 +112,13 @@ export default class App extends React.PureComponent<Props, State> {
};
scheduleRefresh = () => {
- this.interval = window.setTimeout(this.fetchStatus, 5000);
+ this.interval = window.setTimeout(this.fetchStatus, DELAY_REFRESH_STATUS);
};
loadPreviousPage = () => {
setInterval(() => {
- window.location.href = getReturnUrl(this.props.location);
- }, 2500);
+ window.location.replace(getReturnUrl(this.props.location));
+ }, DELAY_REDIRECT_PREV_PAGE);
};
handleMigrateClick = () => {
@@ -150,6 +153,7 @@ export default class App extends React.PureComponent<Props, State> {
{translate('maintenance.sonarqube_is_offline.text')}
</p>
<p className="maintenance-text text-center">
+ {/* We don't use <Link> here, as we want to fully refresh the page. */}
<a href={getBaseUrl() + '/'}>{translate('maintenance.try_again')}</a>
</p>
</>
@@ -164,7 +168,7 @@ export default class App extends React.PureComponent<Props, State> {
{translate('maintenance.all_systems_opetational')}
</p>
<p className="maintenance-text text-center">
- <Link to={getBaseUrl() + '/'}>{translate('layout.home')}</Link>
+ <Link to="/">{translate('layout.home')}</Link>
</p>
</>
)}
@@ -189,6 +193,7 @@ export default class App extends React.PureComponent<Props, State> {
{translate('maintenance.sonarqube_is_down.text')}
</p>
<p className="maintenance-text text-center">
+ {/* We don't use <Link> here, as we want to fully refresh the page. */}
<a href={getBaseUrl() + '/'}>{translate('maintenance.try_again')}</a>
</p>
</>
@@ -238,7 +243,7 @@ export default class App extends React.PureComponent<Props, State> {
{translate('maintenance.database_is_up_to_date')}
</h1>
<p className="maintenance-text text-center">
- <Link to={getBaseUrl() + '/'}>{translate('layout.home')}</Link>
+ <Link to="/">{translate('layout.home')}</Link>
</p>
</>
)}
@@ -294,7 +299,7 @@ export default class App extends React.PureComponent<Props, State> {
{translate('maintenance.database_is_up_to_date')}
</h1>
<p className="maintenance-text text-center">
- <Link to={getBaseUrl() + '/'}>{translate('layout.home')}</Link>
+ <Link to="/">{translate('layout.home')}</Link>
</p>
</>
)}
diff --git a/server/sonar-web/src/main/js/apps/maintenance/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/maintenance/components/__tests__/App-test.tsx
index abf658535db..98f1e8fe9fc 100644
--- a/server/sonar-web/src/main/js/apps/maintenance/components/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/maintenance/components/__tests__/App-test.tsx
@@ -17,124 +17,245 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import * as React from 'react';
-import { click, waitAndUpdate } from '../../../../helpers/testUtils';
+import { getMigrationStatus, getSystemStatus, migrateDatabase } from '../../../../api/system';
+import { mockLocation } from '../../../../helpers/testMocks';
+import { renderApp } from '../../../../helpers/testReactTestingUtils';
import App from '../App';
jest.mock('../../../../api/system', () => ({
- getMigrationStatus: jest.fn(),
- getSystemStatus: jest.fn(),
- migrateDatabase: jest.fn()
+ getMigrationStatus: jest.fn().mockResolvedValue(null),
+ getSystemStatus: jest.fn().mockResolvedValue(null),
+ migrateDatabase: jest.fn().mockResolvedValue(null)
}));
+jest.mock('../../../../helpers/system', () => ({
+ ...jest.requireActual('../../../../helpers/system'),
+ getBaseUrl: jest.fn().mockReturnValue('/context')
+}));
+
+const originalLocation = window.location;
+const replace = jest.fn();
+
beforeAll(() => {
jest.useFakeTimers();
+
+ const location = {
+ ...window.location,
+ replace
+ };
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: location
+ });
});
afterAll(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
-});
-const getMigrationStatus = require('../../../../api/system').getMigrationStatus as jest.Mock;
-const getSystemStatus = require('../../../../api/system').getSystemStatus as jest.Mock;
-const migrateDatabase = require('../../../../api/system').migrateDatabase as jest.Mock;
-
-const location = { query: {} };
-
-beforeEach(() => {
- getMigrationStatus.mockClear();
- getSystemStatus.mockClear();
- migrateDatabase.mockClear();
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: originalLocation
+ });
});
-afterEach(() => {
- jest.clearAllTimers();
-});
+beforeEach(jest.clearAllMocks);
-describe('Maintenance Page', () => {
- ['UP', 'DOWN', 'STARTING', 'DB_MIGRATION_NEEDED', 'DB_MIGRATION_RUNNING'].forEach(status => {
- it(`should render ${status} status`, async () => {
- getSystemStatus.mockImplementationOnce(() => Promise.resolve({ status }));
- await checkApp(false);
- });
- });
+describe('Maintenance', () => {
+ it.each([
+ [
+ 'OFFLINE',
+ 'maintenance.is_offline',
+ 'maintenance.sonarqube_is_offline.text',
+ { name: 'maintenance.try_again', href: '/context/' }
+ ],
+ [
+ 'UP',
+ 'maintenance.is_up',
+ 'maintenance.all_systems_opetational',
+ { name: 'layout.home', href: '/' }
+ ],
+ ['STARTING', 'maintenance.is_starting'],
+ [
+ 'DOWN',
+ 'maintenance.is_down',
+ 'maintenance.sonarqube_is_down.text',
+ { name: 'maintenance.try_again', href: '/context/' }
+ ],
+ ['DB_MIGRATION_NEEDED', 'maintenance.is_under_maintenance'],
+ ['DB_MIGRATION_RUNNING', 'maintenance.is_under_maintenance']
+ ])(
+ 'should handle "%p" status correctly',
+ async (status, heading, body = undefined, linkInfo = undefined) => {
+ (getSystemStatus as jest.Mock).mockResolvedValueOnce({ status });
+ renderMaintenanceApp();
- it('should render OFFLINE status', async () => {
- getSystemStatus.mockImplementationOnce(() => Promise.reject(undefined));
- await checkApp(false);
- });
+ const title = await screen.findByRole('heading', { name: heading });
+ expect(title).toBeInTheDocument();
+ if (body) {
+ expect(screen.getByText(body)).toBeInTheDocument();
+ }
+ if (linkInfo) {
+ const link = screen.getByRole('link', { name: linkInfo.name });
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveAttribute('href', linkInfo.href);
+ }
+ }
+ );
it('should poll status', async () => {
- getSystemStatus.mockImplementationOnce(() =>
- Promise.resolve({ status: 'DB_MIGRATION_RUNNING' })
- );
- const wrapper = shallow(<App location={location} setup={false} />);
- await waitAndUpdate(wrapper);
- expect(getSystemStatus).toBeCalled();
+ (getSystemStatus as jest.Mock)
+ .mockResolvedValueOnce({ status: 'STARTING' })
+ .mockResolvedValueOnce({ status: 'DB_MIGRATION_RUNNING' })
+ .mockResolvedValueOnce({ status: 'UP' });
+
+ renderMaintenanceApp();
+
+ let title = await screen.findByRole('heading', { name: 'maintenance.is_starting' });
+ expect(title).toBeInTheDocument();
- getSystemStatus.mockClear();
- getSystemStatus.mockImplementationOnce(() =>
- Promise.resolve({ status: 'DB_MIGRATION_RUNNING' })
- );
jest.runOnlyPendingTimers();
- await waitAndUpdate(wrapper);
- expect(getSystemStatus).toBeCalled();
- getSystemStatus.mockClear();
- getSystemStatus.mockImplementationOnce(() =>
- Promise.resolve({ status: 'DB_MIGRATION_RUNNING' })
- );
+ title = await screen.findByRole('heading', { name: 'maintenance.is_under_maintenance' });
+ expect(title).toBeInTheDocument();
+
jest.runOnlyPendingTimers();
- await waitAndUpdate(wrapper);
- expect(getSystemStatus).toBeCalled();
- });
- it('should open previous page', async () => {
- getSystemStatus.mockImplementationOnce(() => Promise.resolve({ status: 'STARTING' }));
- const wrapper = shallow(<App location={location} setup={false} />);
- const loadPreviousPage = jest.fn();
- (wrapper.instance() as App).loadPreviousPage = loadPreviousPage;
- await waitAndUpdate(wrapper);
+ title = await screen.findByRole('heading', { name: 'maintenance.is_up' });
+ expect(title).toBeInTheDocument();
- getSystemStatus.mockImplementationOnce(() => Promise.resolve({ status: 'UP' }));
+ // Should redirect automatically.
jest.runOnlyPendingTimers();
- await waitAndUpdate(wrapper);
- expect(loadPreviousPage).toBeCalled();
+ expect(replace).toHaveBeenCalledWith('/return/to');
});
+
+ function renderMaintenanceApp(props: Partial<App['props']> = {}) {
+ return renderApp(
+ '/',
+ <App
+ location={mockLocation({ query: { return_to: '/return/to' } })}
+ setup={false}
+ {...props}
+ />
+ );
+ }
});
-describe('Setup Page', () => {
- ['NO_MIGRATION', 'NOT_SUPPORTED', 'MIGRATION_SUCCEEDED', 'MIGRATION_FAILED'].forEach(state => {
- it(`should render ${state} state`, async () => {
- getMigrationStatus.mockImplementationOnce(() =>
- Promise.resolve({ message: 'message', startedAt: '2017-01-02T00:00:00.000Z', state })
- );
- await checkApp(true);
+describe('Setup', () => {
+ it.each([
+ [
+ 'NO_MIGRATION',
+ 'maintenance.database_is_up_to_date',
+ undefined,
+ { name: 'layout.home', href: '/' }
+ ],
+ [
+ 'MIGRATION_REQUIRED',
+ 'maintenance.upgrade_database',
+ [
+ 'maintenance.upgrade_database.1',
+ 'maintenance.upgrade_database.2',
+ 'maintenance.upgrade_database.3'
+ ]
+ ],
+ [
+ 'NOT_SUPPORTED',
+ 'maintenance.migration_not_supported',
+ ['maintenance.migration_not_supported.text']
+ ],
+ [
+ 'MIGRATION_RUNNING',
+ 'maintenance.database_migration',
+ undefined,
+ undefined,
+ { message: 'MESSAGE', startedAt: '2022-12-01' }
+ ],
+ [
+ 'MIGRATION_SUCCEEDED',
+ 'maintenance.database_is_up_to_date',
+ undefined,
+ { name: 'layout.home', href: '/' }
+ ],
+ ['MIGRATION_FAILED', 'maintenance.upgrade_failed', ['maintenance.upgrade_failed.text']]
+ ])(
+ 'should handle "%p" state correctly',
+ async (state, heading, bodyText: string[] = [], linkInfo = undefined, payload = undefined) => {
+ (getMigrationStatus as jest.Mock).mockResolvedValueOnce({ state, ...payload });
+ renderSetupApp();
+
+ const title = await screen.findByRole('heading', { name: heading });
+ expect(title).toBeInTheDocument();
+ if (bodyText.length) {
+ bodyText.forEach(text => {
+ expect(screen.getByText(text)).toBeInTheDocument();
+ });
+ }
+ if (payload) {
+ expect(screen.getByText(payload.message)).toBeInTheDocument();
+ expect(screen.getByText('background_tasks.table.started')).toBeInTheDocument();
+ }
+ if (linkInfo) {
+ const link = screen.getByRole('link', { name: linkInfo.name });
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveAttribute('href', linkInfo.href);
+ }
+ }
+ );
+
+ it('should handle DB migration', async () => {
+ (migrateDatabase as jest.Mock).mockResolvedValueOnce({
+ message: 'MESSAGE',
+ startedAt: '2022-12-01',
+ state: 'MIGRATION_RUNNING'
});
+ (getMigrationStatus as jest.Mock)
+ .mockResolvedValueOnce({ state: 'MIGRATION_REQUIRED' })
+ .mockResolvedValueOnce({ state: 'MIGRATION_RUNNING' })
+ .mockResolvedValueOnce({ state: 'MIGRATION_SUCCEEDED' });
+
+ renderSetupApp();
+ const user = userEvent.setup();
+
+ jest.runOnlyPendingTimers();
+
+ let title = await screen.findByRole('heading', { name: 'maintenance.upgrade_database' });
+ expect(title).toBeInTheDocument();
+
+ // Trigger DB migration.
+ user.click(screen.getByRole('button'));
+
+ const message = await screen.findByText('MESSAGE');
+ expect(message).toBeInTheDocument();
+ expect(screen.getByText('background_tasks.table.started')).toBeInTheDocument();
+
+ // Trigger refresh; migration running.
+ jest.runOnlyPendingTimers();
+
+ title = await screen.findByRole('heading', { name: 'maintenance.database_migration' });
+ expect(title).toBeInTheDocument();
+
+ // Trigger refresh; migration done.
+ jest.runOnlyPendingTimers();
+
+ title = await screen.findByRole('heading', { name: 'maintenance.database_is_up_to_date' });
+ expect(title).toBeInTheDocument();
+
+ // Should redirect automatically.
+ jest.runOnlyPendingTimers();
+ expect(replace).toHaveBeenCalledWith('/return/to');
});
- it('should start migration', async () => {
- getMigrationStatus.mockImplementationOnce(() =>
- Promise.resolve({ state: 'MIGRATION_REQUIRED' })
- );
- migrateDatabase.mockImplementationOnce(() =>
- Promise.resolve({ startedAt: '2017-01-02T00:00:00.000Z', state: 'MIGRATION_RUNNING' })
+ function renderSetupApp(props: Partial<App['props']> = {}) {
+ return renderApp(
+ '/',
+ <App
+ location={mockLocation({ query: { return_to: '/return/to' } })}
+ setup={true}
+ {...props}
+ />
);
- const wrapper = shallow(<App location={location} setup={true} />);
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-
- click(wrapper.find('Button'));
- expect(migrateDatabase).toBeCalled();
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
- });
+ }
});
-
-async function checkApp(setup: boolean) {
- const wrapper = shallow(<App location={location} setup={setup} />);
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-}
diff --git a/server/sonar-web/src/main/js/apps/maintenance/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/maintenance/components/__tests__/__snapshots__/App-test.tsx.snap
deleted file mode 100644
index 0dc87dd0165..00000000000
--- a/server/sonar-web/src/main/js/apps/maintenance/components/__tests__/__snapshots__/App-test.tsx.snap
+++ /dev/null
@@ -1,522 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Maintenance Page should render DB_MIGRATION_NEEDED status 1`] = `
-<Fragment>
- <Helmet
- defaultTitle="maintenance.page"
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- />
- <div
- className="page-wrapper-simple"
- id="bd"
- >
- <div
- className="page-simple"
- id="nonav"
- >
- <h1
- className="maintenance-title"
- >
- <InstanceMessage
- message="maintenance.is_under_maintenance"
- />
- </h1>
- <p
- className="maintenance-text"
- >
- <FormattedMessage
- defaultMessage="maintenance.sonarqube_is_under_maintenance.1"
- id="maintenance.sonarqube_is_under_maintenance.1"
- values={
- Object {
- "link": <ForwardRef(Link)
- target="_blank"
- to="https://www.sonarlint.org/?referrer=sonarqube-maintenance"
- >
- maintenance.sonarqube_is_under_maintenance_link.1
- </ForwardRef(Link)>,
- }
- }
- />
- </p>
- <p
- className="maintenance-text"
- >
- <FormattedMessage
- defaultMessage="maintenance.sonarqube_is_under_maintenance.2"
- id="maintenance.sonarqube_is_under_maintenance.2"
- values={
- Object {
- "link": <ForwardRef(Link)
- target="_blank"
- to="https://redirect.sonarsource.com/doc/upgrading.html"
- >
- maintenance.sonarqube_is_under_maintenance_link.2
- </ForwardRef(Link)>,
- }
- }
- />
- </p>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`Maintenance Page should render DB_MIGRATION_RUNNING status 1`] = `
-<Fragment>
- <Helmet
- defaultTitle="maintenance.page"
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- />
- <div
- className="page-wrapper-simple"
- id="bd"
- >
- <div
- className="page-simple"
- id="nonav"
- >
- <h1
- className="maintenance-title"
- >
- <InstanceMessage
- message="maintenance.is_under_maintenance"
- />
- </h1>
- <p
- className="maintenance-text"
- >
- <FormattedMessage
- defaultMessage="maintenance.sonarqube_is_under_maintenance.1"
- id="maintenance.sonarqube_is_under_maintenance.1"
- values={
- Object {
- "link": <ForwardRef(Link)
- target="_blank"
- to="https://www.sonarlint.org/?referrer=sonarqube-maintenance"
- >
- maintenance.sonarqube_is_under_maintenance_link.1
- </ForwardRef(Link)>,
- }
- }
- />
- </p>
- <p
- className="maintenance-text"
- >
- <FormattedMessage
- defaultMessage="maintenance.sonarqube_is_under_maintenance.2"
- id="maintenance.sonarqube_is_under_maintenance.2"
- values={
- Object {
- "link": <ForwardRef(Link)
- target="_blank"
- to="https://redirect.sonarsource.com/doc/upgrading.html"
- >
- maintenance.sonarqube_is_under_maintenance_link.2
- </ForwardRef(Link)>,
- }
- }
- />
- </p>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`Maintenance Page should render DOWN status 1`] = `
-<Fragment>
- <Helmet
- defaultTitle="maintenance.page"
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- />
- <div
- className="page-wrapper-simple"
- id="bd"
- >
- <div
- className="page-simple"
- id="nonav"
- >
- <h1
- className="maintenance-title text-danger"
- >
- <InstanceMessage
- message="maintenance.is_down"
- />
- </h1>
- <p
- className="maintenance-text"
- >
- maintenance.sonarqube_is_down.text
- </p>
- <p
- className="maintenance-text text-center"
- >
- <a
- href="/"
- >
- maintenance.try_again
- </a>
- </p>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`Maintenance Page should render OFFLINE status 1`] = `
-<Fragment>
- <Helmet
- defaultTitle="maintenance.page"
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- />
- <div
- className="page-wrapper-simple"
- id="bd"
- >
- <div
- className="page-simple"
- id="nonav"
- >
- <h1
- className="maintenance-title text-danger"
- >
- <InstanceMessage
- message="maintenance.is_offline"
- />
- </h1>
- <p
- className="maintenance-text"
- >
- maintenance.sonarqube_is_offline.text
- </p>
- <p
- className="maintenance-text text-center"
- >
- <a
- href="/"
- >
- maintenance.try_again
- </a>
- </p>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`Maintenance Page should render STARTING status 1`] = `
-<Fragment>
- <Helmet
- defaultTitle="maintenance.page"
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- />
- <div
- className="page-wrapper-simple"
- id="bd"
- >
- <div
- className="page-simple"
- id="nonav"
- >
- <h1
- className="maintenance-title"
- >
- <InstanceMessage
- message="maintenance.is_starting"
- />
- </h1>
- <p
- className="maintenance-spinner"
- >
- <i
- className="spinner"
- />
- </p>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`Maintenance Page should render UP status 1`] = `
-<Fragment>
- <Helmet
- defaultTitle="maintenance.page"
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- />
- <div
- className="page-wrapper-simple"
- id="bd"
- >
- <div
- className="page-simple"
- id="nonav"
- >
- <h1
- className="maintenance-title"
- >
- <InstanceMessage
- message="maintenance.is_up"
- />
- </h1>
- <p
- className="maintenance-text text-center"
- >
- maintenance.all_systems_opetational
- </p>
- <p
- className="maintenance-text text-center"
- >
- <ForwardRef(Link)
- to="/"
- >
- layout.home
- </ForwardRef(Link)>
- </p>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`Setup Page should render MIGRATION_FAILED state 1`] = `
-<Fragment>
- <Helmet
- defaultTitle="maintenance.page"
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- />
- <div
- className="page-wrapper-simple"
- id="bd"
- >
- <div
- className="page-simple"
- id="nonav"
- >
- <h1
- className="maintenance-title text-danger"
- >
- maintenance.upgrade_failed
- </h1>
- <p
- className="maintenance-text"
- >
- maintenance.upgrade_failed.text
- </p>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`Setup Page should render MIGRATION_SUCCEEDED state 1`] = `
-<Fragment>
- <Helmet
- defaultTitle="maintenance.page"
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- />
- <div
- className="page-wrapper-simple"
- id="bd"
- >
- <div
- className="page-simple"
- id="nonav"
- >
- <h1
- className="maintenance-title text-success"
- >
- maintenance.database_is_up_to_date
- </h1>
- <p
- className="maintenance-text text-center"
- >
- <ForwardRef(Link)
- to="/"
- >
- layout.home
- </ForwardRef(Link)>
- </p>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`Setup Page should render NO_MIGRATION state 1`] = `
-<Fragment>
- <Helmet
- defaultTitle="maintenance.page"
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- />
- <div
- className="page-wrapper-simple"
- id="bd"
- >
- <div
- className="page-simple"
- id="nonav"
- >
- <h1
- className="maintenance-title"
- >
- maintenance.database_is_up_to_date
- </h1>
- <p
- className="maintenance-text text-center"
- >
- <ForwardRef(Link)
- to="/"
- >
- layout.home
- </ForwardRef(Link)>
- </p>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`Setup Page should render NOT_SUPPORTED state 1`] = `
-<Fragment>
- <Helmet
- defaultTitle="maintenance.page"
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- />
- <div
- className="page-wrapper-simple"
- id="bd"
- >
- <div
- className="page-simple"
- id="nonav"
- >
- <h1
- className="maintenance-title text-danger"
- >
- maintenance.migration_not_supported
- </h1>
- <p>
- maintenance.migration_not_supported.text
- </p>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`Setup Page should start migration 1`] = `
-<Fragment>
- <Helmet
- defaultTitle="maintenance.page"
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- />
- <div
- className="page-wrapper-simple"
- id="bd"
- >
- <div
- className="page-simple panel-warning"
- id="nonav"
- >
- <h1
- className="maintenance-title"
- >
- maintenance.upgrade_database
- </h1>
- <p
- className="maintenance-text"
- >
- maintenance.upgrade_database.1
- </p>
- <p
- className="maintenance-text"
- >
- maintenance.upgrade_database.2
- </p>
- <p
- className="maintenance-text"
- >
- maintenance.upgrade_database.3
- </p>
- <div
- className="maintenance-spinner"
- >
- <Button
- id="start-migration"
- onClick={[Function]}
- >
- maintenance.upgrade
- </Button>
- </div>
- </div>
- </div>
-</Fragment>
-`;
-
-exports[`Setup Page should start migration 2`] = `
-<Fragment>
- <Helmet
- defaultTitle="maintenance.page"
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- />
- <div
- className="page-wrapper-simple"
- id="bd"
- >
- <div
- className="page-simple"
- id="nonav"
- >
- <h1
- className="maintenance-title"
- >
- maintenance.database_migration
- </h1>
- <p
- className="maintenance-text text-center"
- >
- background_tasks.table.started
-
- <DateFromNow
- date="2017-01-02T00:00:00.000Z"
- />
- <br />
- <small
- className="text-muted"
- >
- <TimeFormatter
- date="2017-01-02T00:00:00.000Z"
- long={true}
- />
- </small>
- </p>
- <p
- className="maintenance-spinner"
- >
- <i
- className="spinner"
- />
- </p>
- </div>
- </div>
-</Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx b/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx
index 1de29b310b3..0cf55cf408b 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx
+++ b/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx
@@ -18,9 +18,10 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { Location, withRouter } from '../../../components/hoc/withRouter';
+import { Location } from '../../../components/hoc/withRouter';
import { Alert } from '../../../components/ui/Alert';
import { translate } from '../../../helpers/l10n';
+import { getReturnUrl } from '../../../helpers/urls';
import { IdentityProvider } from '../../../types/types';
import './Login.css';
import LoginForm from './LoginForm';
@@ -29,13 +30,13 @@ import OAuthProviders from './OAuthProviders';
export interface LoginProps {
identityProviders: IdentityProvider[];
onSubmit: (login: string, password: string) => Promise<void>;
- returnTo: string;
location: Location;
}
-export function Login(props: LoginProps) {
- const { identityProviders, returnTo, location } = props;
- const displayError = location.query.authorizationError;
+export default function Login(props: LoginProps) {
+ const { identityProviders, location } = props;
+ const returnTo = getReturnUrl(location);
+ const displayError = Boolean(location.query.authorizationError);
return (
<div className="login-page" id="login_form">
@@ -57,5 +58,3 @@ export function Login(props: LoginProps) {
</div>
);
}
-
-export default withRouter(Login);
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx b/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx
index 460ce54cd4a..3ada4d40012 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx
@@ -58,7 +58,7 @@ export class LoginContainer extends React.PureComponent<Props, State> {
}
handleSuccessfulLogin = () => {
- window.location.href = getReturnUrl(this.props.location);
+ window.location.replace(getReturnUrl(this.props.location));
};
handleSubmit = (id: string, password: string) => {
@@ -82,7 +82,7 @@ export class LoginContainer extends React.PureComponent<Props, State> {
<Login
identityProviders={identityProviders}
onSubmit={this.handleSubmit}
- returnTo={getReturnUrl(location)}
+ location={location}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/LoginForm.tsx b/server/sonar-web/src/main/js/apps/sessions/components/LoginForm.tsx
index ee96583d823..649efae5ecd 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/LoginForm.tsx
+++ b/server/sonar-web/src/main/js/apps/sessions/components/LoginForm.tsx
@@ -22,7 +22,6 @@ import Link from '../../../components/common/Link';
import { ButtonLink, SubmitButton } from '../../../components/controls/buttons';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { translate } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/system';
import './LoginForm.css';
interface Props {
@@ -125,7 +124,7 @@ export default class LoginForm extends React.PureComponent<Props, State> {
<SubmitButton disabled={this.state.loading}>
{translate('sessions.log_in')}
</SubmitButton>
- <Link className="spacer-left" to={`${getBaseUrl()}/`}>
+ <Link className="spacer-left" to="/">
{translate('cancel')}
</Link>
</div>
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx b/server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx
index 95bc68c8896..f87554c525d 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx
+++ b/server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx
@@ -24,7 +24,7 @@ import { addGlobalErrorMessage } from '../../../helpers/globalMessages';
import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
-export default class Logout extends React.PureComponent<{}> {
+export default class Logout extends React.PureComponent {
componentDidMount() {
logOut()
.then(() => {
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.tsx b/server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.tsx
index df989d43cf9..b8b61712f7c 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.tsx
+++ b/server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.tsx
@@ -21,7 +21,6 @@ import * as React from 'react';
import Link from '../../../components/common/Link';
import { getCookie } from '../../../helpers/cookies';
import { translate } from '../../../helpers/l10n';
-import { getBaseUrl } from '../../../helpers/system';
export default function Unauthorized() {
const message = decodeURIComponent(getCookie('AUTHENTICATION-ERROR') || '');
@@ -38,7 +37,7 @@ export default function Unauthorized() {
)}
<div className="big-spacer-top">
- <Link to={getBaseUrl() + '/'}>{translate('layout.home')}</Link>
+ <Link to="/">{translate('layout.home')}</Link>
</div>
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx
new file mode 100644
index 00000000000..70ac5f61ec6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx
@@ -0,0 +1,144 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { getIdentityProviders } from '../../../../api/users';
+import { addGlobalErrorMessage } from '../../../../helpers/globalMessages';
+import { mockLocation } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { LoginContainer } from '../LoginContainer';
+
+jest.mock('../../../../api/users', () => {
+ const { mockIdentityProvider } = jest.requireActual('../../../../helpers/testMocks');
+ return {
+ getIdentityProviders: jest
+ .fn()
+ .mockResolvedValue({ identityProviders: [mockIdentityProvider()] })
+ };
+});
+
+jest.mock('../../../../api/auth', () => ({
+ logIn: jest.fn((_id, password) => (password === 'valid' ? Promise.resolve() : Promise.reject()))
+}));
+
+jest.mock('../../../../helpers/globalMessages', () => ({
+ addGlobalErrorMessage: jest.fn()
+}));
+
+const originalLocation = window.location;
+const replace = jest.fn();
+
+beforeAll(() => {
+ const location = {
+ ...window.location,
+ replace
+ };
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: location
+ });
+});
+
+afterAll(() => {
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: originalLocation
+ });
+});
+
+beforeEach(jest.clearAllMocks);
+
+it('should behave correctly', async () => {
+ renderLoginContainer();
+ const user = userEvent.setup();
+
+ const heading = await screen.findByRole('heading', { name: 'login.login_to_sonarqube' });
+ expect(heading).toBeInTheDocument();
+
+ // OAuth provider.
+ const link = screen.getByRole('link', { name: 'Github login.login_with_x.Github' });
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveAttribute('href', '/sessions/init/github?return_to=%2Fsome%2Fpath');
+ expect(link).toMatchSnapshot('OAuthProvider link');
+
+ // Login form collapsed by default.
+ expect(screen.queryByLabelText('login')).not.toBeInTheDocument();
+
+ // Open login form, log in.
+ await user.click(screen.getByRole('button', { name: 'login.more_options' }));
+
+ const cancelLink = await screen.findByRole('link', { name: 'cancel' });
+ expect(cancelLink).toBeInTheDocument();
+ expect(cancelLink).toHaveAttribute('href', '/');
+
+ const loginField = screen.getByLabelText('login');
+ const passwordField = screen.getByLabelText('password');
+ const submitButton = screen.getByRole('button', { name: 'sessions.log_in' });
+
+ // Incorrect login.
+ await user.type(loginField, 'janedoe');
+ await user.type(passwordField, 'invalid');
+ // Don't use userEvent.click() here. This allows us to more easily see the loading state changes.
+ submitButton.click();
+ expect(submitButton).toBeDisabled(); // Loading.
+ await waitFor(() => {
+ expect(addGlobalErrorMessage).toHaveBeenCalledWith('login.authentication_failed');
+ });
+
+ // Correct login.
+ await user.clear(passwordField);
+ await user.type(passwordField, 'valid');
+ await user.click(submitButton);
+ expect(addGlobalErrorMessage).toHaveBeenCalledTimes(1);
+ expect(replace).toHaveBeenCalledWith('/some/path');
+});
+
+it('should not show any OAuth providers if none are configured', async () => {
+ (getIdentityProviders as jest.Mock).mockResolvedValueOnce({ identityProviders: [] });
+ renderLoginContainer();
+
+ const heading = await screen.findByRole('heading', { name: 'login.login_to_sonarqube' });
+ expect(heading).toBeInTheDocument();
+
+ // No OAuth providers, login form display by default.
+ expect(
+ screen.queryByRole('link', { name: 'login.login_with_x', exact: false })
+ ).not.toBeInTheDocument();
+ expect(screen.getByLabelText('login')).toBeInTheDocument();
+});
+
+it("should show a warning if there's an authorization error", async () => {
+ renderLoginContainer({
+ location: mockLocation({ query: { authorizationError: 'true' } })
+ });
+
+ const heading = await screen.findByRole('heading', { name: 'login.login_to_sonarqube' });
+ expect(heading).toBeInTheDocument();
+
+ expect(screen.getByRole('alert')).toBeInTheDocument();
+ expect(screen.getByText('login.unauthorized_access_alert')).toBeInTheDocument();
+});
+
+function renderLoginContainer(props: Partial<LoginContainer['props']> = {}) {
+ return renderComponent(
+ <LoginContainer location={mockLocation({ query: { return_to: '/some/path' } })} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-test.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-test.tsx
deleted file mode 100644
index 5f1071ea4dc..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-test.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { mockLocation } from '../../../../helpers/testMocks';
-import { Login, LoginProps } from '../Login';
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot('with identity providers');
- expect(shallowRender({ identityProviders: [] })).toMatchSnapshot(
- 'without any identity providers'
- );
- expect(
- shallowRender({ location: mockLocation({ query: { authorizationError: true } }) })
- ).toMatchSnapshot('with authorization error');
-});
-
-function shallowRender(props: Partial<LoginProps> = {}) {
- return shallow<LoginProps>(
- <Login
- identityProviders={[
- {
- backgroundColor: '#000',
- iconPath: '/some/path',
- key: 'foo',
- name: 'foo'
- }
- ]}
- location={mockLocation()}
- onSubmit={jest.fn()}
- returnTo=""
- {...props}
- />
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginContainer-test.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginContainer-test.tsx
deleted file mode 100644
index 510f7c4074f..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginContainer-test.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { logIn } from '../../../../api/auth';
-import { getIdentityProviders } from '../../../../api/users';
-import { mockLocation } from '../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import { LoginContainer } from '../LoginContainer';
-
-jest.mock('../../../../api/users', () => {
- const { mockIdentityProvider } = jest.requireActual('../../../../helpers/testMocks');
- return {
- getIdentityProviders: jest
- .fn()
- .mockResolvedValue({ identityProviders: [mockIdentityProvider()] })
- };
-});
-
-jest.mock('../../../../api/auth', () => ({
- logIn: jest.fn().mockResolvedValue({})
-}));
-
-beforeEach(jest.clearAllMocks);
-
-it('should render correctly', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- expect(wrapper).toMatchSnapshot();
- expect(getIdentityProviders).toBeCalled();
-});
-
-it('should not provide any options if no IdPs are present', async () => {
- (getIdentityProviders as jest.Mock).mockResolvedValue({});
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- expect(wrapper.type()).toBeNull();
- expect(getIdentityProviders).toBeCalled();
-});
-
-it('should handle submission', () => {
- (logIn as jest.Mock).mockResolvedValue(null);
- const wrapper = shallowRender();
- wrapper.instance().handleSubmit('user', 'pass');
- expect(logIn).toBeCalledWith('user', 'pass');
-});
-
-function shallowRender(props: Partial<LoginContainer['props']> = {}) {
- return shallow<LoginContainer>(<LoginContainer location={mockLocation()} {...props} />);
-}
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginForm-test.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginForm-test.tsx
deleted file mode 100644
index 5f3b1b64385..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginForm-test.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { change, click, submit, waitAndUpdate } from '../../../../helpers/testUtils';
-import LoginForm from '../LoginForm';
-
-it('logs in with simple credentials', () => {
- const onSubmit = jest.fn(() => Promise.resolve());
- const wrapper = shallow(<LoginForm onSubmit={onSubmit} />);
- expect(wrapper).toMatchSnapshot();
-
- change(wrapper.find('#login'), 'admin');
- change(wrapper.find('#password'), 'admin');
- submit(wrapper.find('form'));
-
- expect(onSubmit).toBeCalledWith('admin', 'admin');
-});
-
-it('should display a spinner and disabled button while loading', async () => {
- const onSubmit = jest.fn(() => Promise.resolve());
- const wrapper = shallow(<LoginForm onSubmit={onSubmit} />);
-
- change(wrapper.find('#login'), 'admin');
- change(wrapper.find('#password'), 'admin');
- submit(wrapper.find('form'));
- wrapper.update();
- expect(wrapper).toMatchSnapshot();
-
- await waitAndUpdate(wrapper);
-});
-
-it('expands more options', () => {
- const wrapper = shallow(<LoginForm collapsed={true} onSubmit={jest.fn()} />);
- expect(wrapper).toMatchSnapshot();
-
- click(wrapper.find('.js-more-options'));
- expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-it.tsx
index 9ff8fa85c6f..a211ec6e648 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx
+++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-it.tsx
@@ -17,11 +17,12 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { shallow } from 'enzyme';
+import { screen, waitFor } from '@testing-library/react';
import * as React from 'react';
import { logOut } from '../../../../api/auth';
+import RecentHistory from '../../../../app/components/RecentHistory';
import { addGlobalErrorMessage } from '../../../../helpers/globalMessages';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import Logout from '../Logout';
jest.mock('../../../../api/auth', () => ({
@@ -32,11 +33,24 @@ jest.mock('../../../../helpers/globalMessages', () => ({
addGlobalErrorMessage: jest.fn()
}));
+jest.mock('../../../../helpers/system', () => ({
+ getBaseUrl: jest.fn().mockReturnValue('/context')
+}));
+
+jest.mock('../../../../app/components/RecentHistory', () => ({
+ __esModule: true,
+ default: {
+ clear: jest.fn()
+ }
+}));
+
const originalLocation = window.location;
+const replace = jest.fn();
+
beforeAll(() => {
const location = {
...window.location,
- replace: jest.fn()
+ replace
};
Object.defineProperty(window, 'location', {
writable: true,
@@ -44,10 +58,6 @@ beforeAll(() => {
});
});
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
afterAll(() => {
Object.defineProperty(window, 'location', {
writable: true,
@@ -55,29 +65,27 @@ afterAll(() => {
});
});
-it('should logout correctly', async () => {
- (logOut as jest.Mock).mockResolvedValue(true);
+beforeEach(jest.clearAllMocks);
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
+it('should behave correctly', async () => {
+ renderLogout();
- expect(logOut).toHaveBeenCalled();
- expect(window.location.replace).toHaveBeenCalledWith('/');
- expect(addGlobalErrorMessage).not.toHaveBeenCalled();
+ expect(screen.getByText('logging_out')).toBeInTheDocument();
+ await waitFor(() => {
+ expect(replace).toHaveBeenCalledWith('/context/');
+ });
+ expect(RecentHistory.clear).toHaveBeenCalled();
});
-it('should not redirect if logout fails', async () => {
- (logOut as jest.Mock).mockRejectedValue(false);
+it('should correctly handle a failing log out', async () => {
+ (logOut as jest.Mock).mockRejectedValueOnce(false);
+ renderLogout();
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- expect(logOut).toHaveBeenCalled();
- expect(window.location.replace).not.toHaveBeenCalled();
- expect(addGlobalErrorMessage).toHaveBeenCalled();
- expect(wrapper).toMatchSnapshot();
+ await waitFor(() => {
+ expect(addGlobalErrorMessage).toHaveBeenCalledWith('login.logout_failed');
+ });
});
-function shallowRender() {
- return shallow(<Logout />);
+function renderLogout() {
+ return renderComponent(<Logout />);
}
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/OAuthProviders-test.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/OAuthProviders-test.tsx
deleted file mode 100644
index be169509357..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/OAuthProviders-test.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import OAuthProviders from '../OAuthProviders';
-
-const identityProviders = [
- {
- backgroundColor: '#000',
- iconPath: '/some/path',
- key: 'foo',
- name: 'Foo'
- },
- {
- backgroundColor: '#00F',
- helpMessage: 'Help message!',
- iconPath: '/icon/path',
- key: 'bar',
- name: 'Bar'
- }
-];
-
-it('should render correctly', () => {
- const wrapper = shallow(<OAuthProviders identityProviders={identityProviders} returnTo="" />);
- expect(wrapper).toMatchSnapshot();
- wrapper.find('OAuthProvider').forEach(node => expect(node.dive()).toMatchSnapshot());
-});
-
-it('should use the custom label formatter', () => {
- const wrapper = shallow(
- <OAuthProviders
- formatLabel={name => 'custom_format.' + name}
- identityProviders={[identityProviders[0]]}
- returnTo=""
- />
- );
- expect(
- wrapper
- .find('OAuthProvider')
- .first()
- .dive()
- ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Unauthorized-test.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Unauthorized-it.tsx
index 392ac1d59c7..ddbf360f5b3 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Unauthorized-test.tsx
+++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Unauthorized-it.tsx
@@ -17,14 +17,30 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { shallow } from 'enzyme';
+
+import { screen } from '@testing-library/react';
import * as React from 'react';
+import { getCookie } from '../../../../helpers/cookies';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import Unauthorized from '../Unauthorized';
jest.mock('../../../../helpers/cookies', () => ({
- getCookie: jest.fn().mockReturnValue('Foo')
+ getCookie: jest.fn()
}));
-it('render', () => {
- expect(shallow(<Unauthorized />)).toMatchSnapshot();
+it('should render correctly', () => {
+ renderUnauthorized();
+ expect(screen.getByText('unauthorized.message')).toBeInTheDocument();
+ expect(screen.queryByText('REASON')).not.toBeInTheDocument();
+ expect(screen.getByRole('link', { name: 'layout.home' })).toBeInTheDocument();
+});
+
+it('should correctly get the reason from the cookie', () => {
+ (getCookie as jest.Mock).mockReturnValueOnce('REASON');
+ renderUnauthorized();
+ expect(screen.getByText('unauthorized.reason REASON')).toBeInTheDocument();
});
+
+function renderUnauthorized() {
+ return renderComponent(<Unauthorized />);
+}
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-it.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-it.tsx.snap
new file mode 100644
index 00000000000..96449a1dc42
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-it.tsx.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should behave correctly: OAuthProvider link 1`] = `
+<a
+ class="identity-provider-link"
+ href="/sessions/init/github?return_to=%2Fsome%2Fpath"
+ style="background-color: rgb(0, 0, 0);"
+>
+ <img
+ alt="Github"
+ height="20"
+ src="/path/icon.svg"
+ width="20"
+ />
+ <span>
+ login.login_with_x.Github
+ </span>
+</a>
+`;
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-test.tsx.snap
deleted file mode 100644
index 22b570e17b1..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-test.tsx.snap
+++ /dev/null
@@ -1,85 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: with authorization error 1`] = `
-<div
- className="login-page"
- id="login_form"
->
- <h1
- className="login-title text-center huge-spacer-bottom"
- >
- login.login_to_sonarqube
- </h1>
- <Alert
- className="huge-spacer-bottom"
- display="block"
- variant="error"
- >
- login.unauthorized_access_alert
- </Alert>
- <OAuthProviders
- identityProviders={
- Array [
- Object {
- "backgroundColor": "#000",
- "iconPath": "/some/path",
- "key": "foo",
- "name": "foo",
- },
- ]
- }
- returnTo=""
- />
- <LoginForm
- collapsed={true}
- onSubmit={[MockFunction]}
- />
-</div>
-`;
-
-exports[`should render correctly: with identity providers 1`] = `
-<div
- className="login-page"
- id="login_form"
->
- <h1
- className="login-title text-center huge-spacer-bottom"
- >
- login.login_to_sonarqube
- </h1>
- <OAuthProviders
- identityProviders={
- Array [
- Object {
- "backgroundColor": "#000",
- "iconPath": "/some/path",
- "key": "foo",
- "name": "foo",
- },
- ]
- }
- returnTo=""
- />
- <LoginForm
- collapsed={true}
- onSubmit={[MockFunction]}
- />
-</div>
-`;
-
-exports[`should render correctly: without any identity providers 1`] = `
-<div
- className="login-page"
- id="login_form"
->
- <h1
- className="login-title text-center huge-spacer-bottom"
- >
- login.login_to_sonarqube
- </h1>
- <LoginForm
- collapsed={false}
- onSubmit={[MockFunction]}
- />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginContainer-test.tsx.snap
deleted file mode 100644
index c37fa66d43a..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginContainer-test.tsx.snap
+++ /dev/null
@@ -1,18 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<withRouter(Login)
- identityProviders={
- Array [
- Object {
- "backgroundColor": "#000000",
- "iconPath": "/path/icon.svg",
- "key": "github",
- "name": "Github",
- },
- ]
- }
- onSubmit={[Function]}
- returnTo="/"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginForm-test.tsx.snap
deleted file mode 100644
index 10720b42939..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginForm-test.tsx.snap
+++ /dev/null
@@ -1,228 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`expands more options 1`] = `
-<div
- className="text-center"
->
- <ButtonLink
- aria-expanded={false}
- className="small js-more-options"
- onClick={[Function]}
- >
- login.more_options
- </ButtonLink>
-</div>
-`;
-
-exports[`expands more options 2`] = `
-<form
- className="login-form"
- onSubmit={[Function]}
->
- <div
- className="big-spacer-bottom"
- >
- <label
- className="login-label"
- htmlFor="login"
- >
- login
- </label>
- <input
- autoFocus={true}
- className="login-input"
- id="login"
- maxLength={255}
- name="login"
- onChange={[Function]}
- placeholder="login"
- required={true}
- type="text"
- value=""
- />
- </div>
- <div
- className="big-spacer-bottom"
- >
- <label
- className="login-label"
- htmlFor="password"
- >
- password
- </label>
- <input
- className="login-input"
- id="password"
- name="password"
- onChange={[Function]}
- placeholder="password"
- required={true}
- type="password"
- value=""
- />
- </div>
- <div>
- <div
- className="text-right overflow-hidden"
- >
- <DeferredSpinner
- className="spacer-right"
- loading={false}
- />
- <SubmitButton
- disabled={false}
- >
- sessions.log_in
- </SubmitButton>
- <ForwardRef(Link)
- className="spacer-left"
- to="/"
- >
- cancel
- </ForwardRef(Link)>
- </div>
- </div>
-</form>
-`;
-
-exports[`logs in with simple credentials 1`] = `
-<form
- className="login-form"
- onSubmit={[Function]}
->
- <div
- className="big-spacer-bottom"
- >
- <label
- className="login-label"
- htmlFor="login"
- >
- login
- </label>
- <input
- autoFocus={true}
- className="login-input"
- id="login"
- maxLength={255}
- name="login"
- onChange={[Function]}
- placeholder="login"
- required={true}
- type="text"
- value=""
- />
- </div>
- <div
- className="big-spacer-bottom"
- >
- <label
- className="login-label"
- htmlFor="password"
- >
- password
- </label>
- <input
- className="login-input"
- id="password"
- name="password"
- onChange={[Function]}
- placeholder="password"
- required={true}
- type="password"
- value=""
- />
- </div>
- <div>
- <div
- className="text-right overflow-hidden"
- >
- <DeferredSpinner
- className="spacer-right"
- loading={false}
- />
- <SubmitButton
- disabled={false}
- >
- sessions.log_in
- </SubmitButton>
- <ForwardRef(Link)
- className="spacer-left"
- to="/"
- >
- cancel
- </ForwardRef(Link)>
- </div>
- </div>
-</form>
-`;
-
-exports[`should display a spinner and disabled button while loading 1`] = `
-<form
- className="login-form"
- onSubmit={[Function]}
->
- <div
- className="big-spacer-bottom"
- >
- <label
- className="login-label"
- htmlFor="login"
- >
- login
- </label>
- <input
- autoFocus={true}
- className="login-input"
- id="login"
- maxLength={255}
- name="login"
- onChange={[Function]}
- placeholder="login"
- required={true}
- type="text"
- value="admin"
- />
- </div>
- <div
- className="big-spacer-bottom"
- >
- <label
- className="login-label"
- htmlFor="password"
- >
- password
- </label>
- <input
- className="login-input"
- id="password"
- name="password"
- onChange={[Function]}
- placeholder="password"
- required={true}
- type="password"
- value="admin"
- />
- </div>
- <div>
- <div
- className="text-right overflow-hidden"
- >
- <DeferredSpinner
- className="spacer-right"
- loading={true}
- />
- <SubmitButton
- disabled={true}
- >
- sessions.log_in
- </SubmitButton>
- <ForwardRef(Link)
- className="spacer-left"
- to="/"
- >
- cancel
- </ForwardRef(Link)>
- </div>
- </div>
-</form>
-`;
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap
deleted file mode 100644
index 49bf97a3c59..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap
+++ /dev/null
@@ -1,13 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should not redirect if logout fails 1`] = `
-<div
- className="page page-limited"
->
- <div
- className="text-center"
- >
- logging_out
- </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap
deleted file mode 100644
index 156489e9ba5..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap
+++ /dev/null
@@ -1,84 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Styled(div)
- className="oauth-providers"
->
- <OAuthProvider
- format={[Function]}
- identityProvider={
- Object {
- "backgroundColor": "#000",
- "iconPath": "/some/path",
- "key": "foo",
- "name": "Foo",
- }
- }
- key="foo"
- returnTo=""
- />
- <OAuthProvider
- format={[Function]}
- identityProvider={
- Object {
- "backgroundColor": "#00F",
- "helpMessage": "Help message!",
- "iconPath": "/icon/path",
- "key": "bar",
- "name": "Bar",
- }
- }
- key="bar"
- returnTo=""
- />
-</Styled(div)>
-`;
-
-exports[`should render correctly 2`] = `
-<Styled(div)>
- <IdentityProviderLink
- backgroundColor="#000"
- iconPath="/some/path"
- name="Foo"
- url="/sessions/init/foo?return_to="
- >
- <span>
- login.login_with_x.Foo
- </span>
- </IdentityProviderLink>
-</Styled(div)>
-`;
-
-exports[`should render correctly 3`] = `
-<Styled(div)>
- <IdentityProviderLink
- backgroundColor="#00F"
- iconPath="/icon/path"
- name="Bar"
- url="/sessions/init/bar?return_to="
- >
- <span>
- login.login_with_x.Bar
- </span>
- </IdentityProviderLink>
- <HelpTooltip
- className="oauth-providers-help"
- overlay="Help message!"
- />
-</Styled(div)>
-`;
-
-exports[`should use the custom label formatter 1`] = `
-<Styled(div)>
- <IdentityProviderLink
- backgroundColor="#000"
- iconPath="/some/path"
- name="Foo"
- url="/sessions/init/foo?return_to="
- >
- <span>
- custom_format.Foo
- </span>
- </IdentityProviderLink>
-</Styled(div)>
-`;
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Unauthorized-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Unauthorized-test.tsx.snap
deleted file mode 100644
index 2e4e329391b..00000000000
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Unauthorized-test.tsx.snap
+++ /dev/null
@@ -1,39 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render 1`] = `
-<div
- className="page-wrapper-simple"
- id="bd"
->
- <div
- className="page-simple"
- id="nonav"
- >
- <div
- className="text-center"
- >
- <p
- id="unauthorized"
- >
- unauthorized.message
- </p>
- <p
- className="spacer-top"
- >
- unauthorized.reason
-
- Foo
- </p>
- <div
- className="big-spacer-top"
- >
- <ForwardRef(Link)
- to="/"
- >
- layout.home
- </ForwardRef(Link)>
- </div>
- </div>
- </div>
-</div>
-`;