diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2022-09-16 16:55:53 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-09-22 20:03:32 +0000 |
commit | c8b5142dfe1f772f212e740cf54dc487cabac83d (patch) | |
tree | 88f78635f62e5ccce875e4ad1cac773d1f703943 /server | |
parent | 9e2f4c8b43069a7580b453d7eaa8fa2c60202741 (diff) | |
download | sonarqube-c8b5142dfe1f772f212e740cf54dc487cabac83d.tar.gz sonarqube-c8b5142dfe1f772f212e740cf54dc487cabac83d.zip |
SONAR-17027 Fix links when running in a webcontext
Diffstat (limited to 'server')
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> -`; |