]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17027 Fix links when running in a webcontext
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Fri, 16 Sep 2022 14:55:53 +0000 (16:55 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 22 Sep 2022 20:03:32 +0000 (20:03 +0000)
24 files changed:
server/sonar-web/src/main/js/apps/maintenance/components/App.tsx
server/sonar-web/src/main/js/apps/maintenance/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/maintenance/components/__tests__/__snapshots__/App-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/Login.tsx
server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx
server/sonar-web/src/main/js/apps/sessions/components/LoginForm.tsx
server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx
server/sonar-web/src/main/js/apps/sessions/components/Unauthorized.tsx
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginContainer-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginForm-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/OAuthProviders-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Unauthorized-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Unauthorized-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-it.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Login-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginContainer-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginForm-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Unauthorized-test.tsx.snap [deleted file]

index 7b943cf2000f71451b22286008af7f878ce48004..4655bb62b1c81413244faaaf29717c7ce25ea22c 100644 (file)
@@ -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>
               </>
             )}
index abf658535db0ac01ba30c757a51cd46b46050388..98f1e8fe9fc9d282eccc55381ef19564bd0638a3 100644 (file)
  * 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 (file)
index 0dc87dd..0000000
+++ /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>
-`;
index 1de29b310b3a6e313d052ffeefe3dda2092549a7..0cf55cf408b3851e62f168811bc654566e2c5e59 100644 (file)
  * 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);
index 460ce54cd4ab490a1fafd107010194b1636fd4a4..3ada4d4001277931df56cad56bb6761dba2ec2e6 100644 (file)
@@ -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}
       />
     );
   }
index ee96583d8239d74836de46f79329a55b4880c280..649efae5ecd98812306b517394d63309f01ca87e 100644 (file)
@@ -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>
index 95bc68c8896c864a16606e99d7649c51e4ff59f3..f87554c525d69eca47940f6e28fb23b3936b7260 100644 (file)
@@ -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(() => {
index df989d43cf92d8beccb5cd6843f77c219b58c9e0..b8b61712f7c57c6dc197b634cd1516fdd9c378fb 100644 (file)
@@ -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 (file)
index 0000000..70ac5f6
--- /dev/null
@@ -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 (file)
index 5f1071e..0000000
+++ /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 (file)
index 510f7c4..0000000
+++ /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 (file)
index 5f3b1b6..0000000
+++ /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-it.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-it.tsx
new file mode 100644 (file)
index 0000000..a211ec6
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * 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 * as React from 'react';
+import { logOut } from '../../../../api/auth';
+import RecentHistory from '../../../../app/components/RecentHistory';
+import { addGlobalErrorMessage } from '../../../../helpers/globalMessages';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import Logout from '../Logout';
+
+jest.mock('../../../../api/auth', () => ({
+  logOut: jest.fn().mockResolvedValue(true)
+}));
+
+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
+  };
+  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 () => {
+  renderLogout();
+
+  expect(screen.getByText('logging_out')).toBeInTheDocument();
+  await waitFor(() => {
+    expect(replace).toHaveBeenCalledWith('/context/');
+  });
+  expect(RecentHistory.clear).toHaveBeenCalled();
+});
+
+it('should correctly handle a failing log out', async () => {
+  (logOut as jest.Mock).mockRejectedValueOnce(false);
+  renderLogout();
+
+  await waitFor(() => {
+    expect(addGlobalErrorMessage).toHaveBeenCalledWith('login.logout_failed');
+  });
+});
+
+function renderLogout() {
+  return renderComponent(<Logout />);
+}
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-test.tsx
deleted file mode 100644 (file)
index 9ff8fa8..0000000
+++ /dev/null
@@ -1,83 +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 { logOut } from '../../../../api/auth';
-import { addGlobalErrorMessage } from '../../../../helpers/globalMessages';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import Logout from '../Logout';
-
-jest.mock('../../../../api/auth', () => ({
-  logOut: jest.fn().mockResolvedValue(true)
-}));
-
-jest.mock('../../../../helpers/globalMessages', () => ({
-  addGlobalErrorMessage: jest.fn()
-}));
-
-const originalLocation = window.location;
-beforeAll(() => {
-  const location = {
-    ...window.location,
-    replace: jest.fn()
-  };
-  Object.defineProperty(window, 'location', {
-    writable: true,
-    value: location
-  });
-});
-
-beforeEach(() => {
-  jest.clearAllMocks();
-});
-
-afterAll(() => {
-  Object.defineProperty(window, 'location', {
-    writable: true,
-    value: originalLocation
-  });
-});
-
-it('should logout correctly', async () => {
-  (logOut as jest.Mock).mockResolvedValue(true);
-
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
-  expect(logOut).toHaveBeenCalled();
-  expect(window.location.replace).toHaveBeenCalledWith('/');
-  expect(addGlobalErrorMessage).not.toHaveBeenCalled();
-});
-
-it('should not redirect if logout fails', async () => {
-  (logOut as jest.Mock).mockRejectedValue(false);
-
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
-  expect(logOut).toHaveBeenCalled();
-  expect(window.location.replace).not.toHaveBeenCalled();
-  expect(addGlobalErrorMessage).toHaveBeenCalled();
-  expect(wrapper).toMatchSnapshot();
-});
-
-function shallowRender() {
-  return shallow(<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 (file)
index be16950..0000000
+++ /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-it.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Unauthorized-it.tsx
new file mode 100644 (file)
index 0000000..ddbf360
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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 } 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()
+}));
+
+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__/Unauthorized-test.tsx b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/Unauthorized-test.tsx
deleted file mode 100644 (file)
index 392ac1d..0000000
+++ /dev/null
@@ -1,30 +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 Unauthorized from '../Unauthorized';
-
-jest.mock('../../../../helpers/cookies', () => ({
-  getCookie: jest.fn().mockReturnValue('Foo')
-}));
-
-it('render', () => {
-  expect(shallow(<Unauthorized />)).toMatchSnapshot();
-});
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 (file)
index 0000000..96449a1
--- /dev/null
@@ -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 (file)
index 22b570e..0000000
+++ /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 (file)
index c37fa66..0000000
+++ /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 (file)
index 10720b4..0000000
+++ /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 (file)
index 49bf97a..0000000
+++ /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 (file)
index 156489e..0000000
+++ /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 (file)
index 2e4e329..0000000
+++ /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>
-`;