Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shell 126 fix new persona next number calculation #285

Merged
merged 10 commits into from
Jul 12, 2023
Prev Previous commit
Next Next commit
test: add test for logout and improve other tests
refs: SHELL-121
beawar committed Jul 6, 2023
commit f2077d599b13a3e60236563d3be155474f7e688b
3 changes: 2 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ export default {
'!src/**/types/*', // exclude types
'!src/**/*.d.ts', // exclude declarations
'!src/test/*', // exclude test folder
'!**/__mocks__/**/*', // exclude manual mocks
'!src/workers/*' // FIXME: exclude worker folder which throws error because of the esm syntax
],

@@ -106,7 +107,7 @@ export default {
},

// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
modulePathIgnorePatterns: ['<rootDir>/.*/__mocks__'],

// Activates notifications for test results
// notify: false,
31 changes: 12 additions & 19 deletions src/boot/loader.test.tsx
Original file line number Diff line number Diff line change
@@ -3,20 +3,22 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ResponseResolver, rest, RestContext, RestRequest } from 'msw';
import { act, screen, waitFor } from '@testing-library/react';
import React from 'react';
import server from '../mocks/server';
import { getComponentsJson, GetComponentsJsonResponseBody } from '../mocks/handlers/components';
import { setup } from '../test/utils';

import { act, screen, waitFor } from '@testing-library/react';
import { ResponseResolver, rest, RestContext, RestRequest } from 'msw';

import { Loader } from './loader';
import { LOGIN_V3_CONFIG_PATH } from '../constants';
import { LoginConfigStore } from '../../types/loginConfig';
import { getLoginConfig } from '../mocks/handlers/login-config';
import { LOGIN_V3_CONFIG_PATH } from '../constants';
import { getComponentsJson, GetComponentsJsonResponseBody } from '../mocks/handlers/components';
import { getInfoRequest } from '../mocks/handlers/getInfoRequest';
import { getLoginConfig } from '../mocks/handlers/login-config';
import server from '../mocks/server';
import { controlConsoleError, setup } from '../test/utils';

jest.mock('../workers');
jest.mock('../reporting/functions');
jest.mock<typeof import('../workers')>('../workers');
jest.mock<typeof import('../reporting/functions')>('../reporting/functions');

describe('Loader', () => {
test('If only getComponents request fails, the LoaderFailureModal appears', async () => {
@@ -40,16 +42,7 @@ describe('Loader', () => {

test('If only getInfo request fails, the LoaderFailureModal appears', async () => {
// TODO remove when SHELL-117 will be implemented
const actualConsoleError = console.error;
console.error = jest.fn<ReturnType<typeof console.error>, Parameters<typeof console.error>>(
(error, ...restParameter) => {
if (error === 'Unexpected end of JSON input') {
console.log('Controlled error', error, ...restParameter);
} else {
actualConsoleError(error, ...restParameter);
}
}
);
controlConsoleError('Unexpected end of JSON input');
// using getComponents and loginConfig default handlers
server.use(
rest.post('/service/soap/GetInfoRequest', (req, res, ctx) =>
1 change: 1 addition & 0 deletions src/jest-env-setup.ts
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import { act, configure } from '@testing-library/react';
import dotenv from 'dotenv';
import failOnConsole from 'jest-fail-on-console';
import { noop } from 'lodash';

import server from './mocks/server';

dotenv.config();
9 changes: 7 additions & 2 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -5,17 +5,22 @@
*/

import { type RequestHandler, rest } from 'msw';

import { getComponentsJson } from './handlers/components';
import { endSessionRequest } from './handlers/endSessionRequest';
import { getInfoRequest } from './handlers/getInfoRequest';
import { LOGIN_V3_CONFIG_PATH } from '../constants';
import { getLoginConfig } from './handlers/login-config';
import { getRightsRequest } from './handlers/getRightsRequest';
import { getLoginConfig } from './handlers/login-config';
import { rootHandler } from './handlers/rootHandler';
import { LOGIN_V3_CONFIG_PATH } from '../constants';

const handlers: RequestHandler[] = [
rest.get('/static/iris/components.json', getComponentsJson),
rest.post('/service/soap/GetInfoRequest', getInfoRequest),
rest.post('/service/soap/GetRightsRequest', getRightsRequest),
rest.post('/service/soap/EndSessionRequest', endSessionRequest),
rest.get(LOGIN_V3_CONFIG_PATH, getLoginConfig),
rest.get('/', rootHandler),
rest.get('/i18n/en.json', (request, response, context) => response(context.json({})))
];

31 changes: 31 additions & 0 deletions src/mocks/handlers/endSessionRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { type ResponseResolver, type RestContext, type RestRequest } from 'msw';

type EndSessionRequestBody = {
EndSessionRequest: {
_jsns: string;
};
};

type EndSessionResponseBody = {
Body: {
EndSessionResponse: Record<string, unknown>;
};
};

export const endSessionRequest: ResponseResolver<
RestRequest<EndSessionRequestBody, never>,
RestContext,
EndSessionResponseBody
> = (request, response, context) =>
response(
context.json({
Body: {
EndSessionResponse: {}
}
})
);
13 changes: 13 additions & 0 deletions src/mocks/handlers/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ResponseResolver, RestContext, RestRequest } from 'msw';

export const logout: ResponseResolver<RestRequest, RestContext> = (req, res, ctx) =>
res(
ctx.status(304, 'Temporary Redirect'),
ctx.json({}),
ctx.set('location', 'https://localhost/static/login/')
);
16 changes: 16 additions & 0 deletions src/mocks/handlers/rootHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ResponseResolver, RestContext, RestRequest } from 'msw';

import { logout } from './logout';

export const rootHandler: ResponseResolver<RestRequest, RestContext> = (req, res, ctx) => {
if (req.url.searchParams.get('loginOp') === 'logout') {
return logout(req, res, ctx);
}

return req.passthrough();
};
76 changes: 76 additions & 0 deletions src/network/fetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { waitFor } from '@testing-library/react';
import { noop } from 'lodash';
import { DefaultBodyType, PathParams, rest } from 'msw';

import { getSoapFetch } from './fetch';
import * as networkUtils from './utils';
import { type ErrorSoapResponse } from '../../types';
import { SHELL_APP_ID } from '../constants';
import server from '../mocks/server';

jest.mock<typeof import('../workers')>('../workers');

describe('Fetch', () => {
test('should redirect to login if user is not authenticated', async () => {
server.use(
rest.post<DefaultBodyType, PathParams, Pick<ErrorSoapResponse, 'Body'>>(
'/service/soap/SomeRequest',
(req, res, ctx) =>
res(
ctx.json({
Body: {
Fault: {
Reason: { Text: 'Controlled error: auth required' },
Detail: {
Error: {
Code: 'service.AUTH_REQUIRED',
Detail: ''
}
}
}
}
})
)
)
);

const goToLoginFn = jest.spyOn(networkUtils, 'goToLogin').mockImplementation(noop);

await getSoapFetch(SHELL_APP_ID)('Some', {});
await waitFor(() => expect(goToLoginFn).toHaveBeenCalled());
});

test('should redirect to login if user session is expired', async () => {
server.use(
rest.post<DefaultBodyType, PathParams, Pick<ErrorSoapResponse, 'Body'>>(
'/service/soap/SomeRequest',
(req, res, ctx) =>
res(
ctx.json({
Body: {
Fault: {
Reason: { Text: 'Controlled error: auth expired' },
Detail: {
Error: {
Code: 'service.AUTH_EXPIRED',
Detail: ''
}
}
}
}
})
)
)
);

const goToLoginFn = jest.spyOn(networkUtils, 'goToLogin').mockImplementation(noop);

await getSoapFetch(SHELL_APP_ID)('Some', {});
await waitFor(() => expect(goToLoginFn).toHaveBeenCalled());
});
});
8 changes: 5 additions & 3 deletions src/network/get-components.test.ts
Original file line number Diff line number Diff line change
@@ -4,13 +4,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { rest } from 'msw';
import server from '../mocks/server';

import { getComponents } from './get-components';
import { CarbonioModule } from '../../types';
import { GetComponentsJsonResponseBody } from '../mocks/handlers/components';
import server from '../mocks/server';
import { useAppStore } from '../store/app';
import { getComponents } from './get-components';

jest.mock('../workers');
jest.mock<typeof import('../workers')>('../workers');

describe('Get components', () => {
test('Setup apps and request data for the logged account', async () => {
const shellModule: CarbonioModule = {
5 changes: 4 additions & 1 deletion src/network/logout.ts
Original file line number Diff line number Diff line change
@@ -4,9 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { SHELL_APP_ID } from '../constants';
import { getSoapFetch } from './fetch';
import { goTo, goToLogin } from './utils';
import { SHELL_APP_ID } from '../constants';
import { useLoginConfigStore } from '../store/login/store';

export function logout(): Promise<void> {
@@ -18,5 +18,8 @@ export function logout(): Promise<void> {
.then(() => {
const customLogoutUrl = useLoginConfigStore.getState().carbonioWebUiLogoutURL;
customLogoutUrl ? goTo(customLogoutUrl) : goToLogin();
})
.catch((error) => {
console.error(error);
});
}
7 changes: 7 additions & 0 deletions src/reporting/__mocks__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * from './store';
export * from './functions';
22 changes: 22 additions & 0 deletions src/reporting/__mocks__/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2022 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { type Hub } from '@sentry/browser';
import { create } from 'zustand';

import { CarbonioModule } from '../../../types';

type ReporterState = {
clients: Record<string, Hub>;
setClients: (apps: Array<CarbonioModule>) => void;
};

export const useReporter = create<ReporterState>()(() => ({
clients: {},
setClients: (): void => {
// do nothing
}
}));
55 changes: 55 additions & 0 deletions src/settings/components/general-settings/logout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react';

import { act, screen, waitFor } from '@testing-library/react';

import { Logout } from './logout';
import { waitForRequest } from '../../../mocks/server';
import * as networkUtils from '../../../network/utils';
import { useLoginConfigStore } from '../../../store/login/store';
import { setup } from '../../../test/utils';

jest.mock<typeof import('../../../workers')>('../../../workers');
jest.mock<typeof import('../../../reporting')>('../../../reporting');

describe('Logout', () => {
test('should redirect to custom logout url on manual logout', async () => {
const customLogout = 'custom.logout.url';
const goToFn = jest.spyOn(networkUtils, 'goTo').mockImplementation();
const goToLoginFn = jest.spyOn(networkUtils, 'goToLogin').mockImplementation();
useLoginConfigStore.setState((s) => ({ ...s, carbonioWebUiLogoutURL: customLogout }));
const { user } = setup(<Logout />);
const logout = waitForRequest('get', '/?loginOp=logout');
await user.click(screen.getByRole('button', { name: /logout/i }));
await logout;
act(() => {
jest.runOnlyPendingTimers();
});
await waitFor(() => expect(goToFn).toHaveBeenCalled());
expect(goToFn).toHaveBeenCalledTimes(1);
expect(goToFn).toHaveBeenCalledWith(customLogout);
expect(goToLoginFn).not.toHaveBeenCalled();
});

test('should redirect to login if no custom logout url is set', async () => {
const customLogout = 'custom.logout.url';
const goToFn = jest.spyOn(networkUtils, 'goTo').mockImplementation();
const goToLoginFn = jest.spyOn(networkUtils, 'goToLogin').mockImplementation();
useLoginConfigStore.setState((s) => ({ ...s, carbonioWebUiLogoutURL: customLogout }));
const { user } = setup(<Logout />);
const logout = waitForRequest('get', '/?loginOp=logout');
await user.click(screen.getByRole('button', { name: /logout/i }));
await logout;
act(() => {
jest.runOnlyPendingTimers();
});
await waitFor(() => expect(goToFn).toHaveBeenCalled());
expect(goToFn).toHaveBeenCalledTimes(1);
expect(goToFn).toHaveBeenCalledWith(customLogout);
expect(goToLoginFn).not.toHaveBeenCalled();
});
});
16 changes: 2 additions & 14 deletions src/shell/hooks/useLocalStorage.test.tsx
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { screen, within } from '@testing-library/react';
import { exportForTest, useLocalStorage } from './useLocalStorage';
import { setup } from '../../test/utils';
import { controlConsoleError, setup } from '../../test/utils';

describe('use local storage', () => {
const TestComponent = <T,>({
@@ -165,19 +165,7 @@ describe('use local storage', () => {
const initial = 'initial';
const updatesLocalStorage = [initial];
const updatesLocalStorageStore = [undefined, initial];
const actualConsoleError = console.error;
console.error = jest.fn<ReturnType<typeof console.error>, Parameters<typeof console.error>>(
(error, ...rest) => {
if (
error instanceof Error &&
error.message === 'Unexpected token o in JSON at position 1'
) {
console.log('Controlled error', error.message);
} else {
actualConsoleError(error, ...rest);
}
}
);
controlConsoleError('Unexpected token o in JSON at position 1');
setup(
<TestComponent initialValue={initial} updatedValue={'updated'} localStorageKey={lsKey} />
);
15 changes: 9 additions & 6 deletions src/shell/shell-primary-bar.test.tsx
Original file line number Diff line number Diff line change
@@ -4,19 +4,22 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react';

import { act, screen, within } from '@testing-library/react';
import { Route, Switch, useParams, useRouteMatch } from 'react-router-dom';
import { Button, Text } from '@zextras/carbonio-design-system';
import { setup } from '../test/utils';
import { Route, Switch, useParams, useRouteMatch } from 'react-router-dom';

import AppViewContainer from './app-view-container';
import ShellPrimaryBar from './shell-primary-bar';
import { useAppStore } from '../store/app';
import { PrimaryBarView } from '../../types';
import { usePushHistoryCallback } from '../history/hooks';
import AppViewContainer from './app-view-container';
import { DefaultViewsRegister } from '../boot/bootstrapper';
import { usePushHistoryCallback } from '../history/hooks';
import { ModuleSelector } from '../search/module-selector';
import { useAppStore } from '../store/app';
import { setup } from '../test/utils';

jest.mock('../workers');
jest.mock<typeof import('../workers')>('../workers');
jest.mock<typeof import('../reporting')>('../reporting');

const ShellWrapper = (): JSX.Element => (
<>
27 changes: 27 additions & 0 deletions src/test/account-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import produce from 'immer';

import { LOGGED_USER } from './constants';
import { Account } from '../../types';
import { useAccountStore } from '../store/account';

export const mockedAccount: Account = {
name: LOGGED_USER.name,
rights: { targets: [] },
signatures: { signature: [] },
id: LOGGED_USER.id,
displayName: LOGGED_USER.attrs.displayName,
identities: LOGGED_USER.identities
};

export function setupAccountStore(account = mockedAccount): void {
useAccountStore.setState(
produce((state) => {
state.account = account;
})
);
}
3 changes: 2 additions & 1 deletion src/test/constants.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import { Border } from '../shell/hooks/useResize';
const DEFAULT_ID = 'logged-user-id';
export const LOGGED_USER = {
id: DEFAULT_ID,
name: 'Logged User',
name: 'LoggedUser',
prefs: {},
attrs: {
displayName: 'Logged User'
@@ -127,6 +127,7 @@ export const PALETTE = {
};

export const ICONS = {
accountUtilityMenu: 'PersonOutline',
checkboxChecked: 'icon: CheckmarkSquare',
checkboxUnchecked: 'icon: Square',
close: 'Close',
19 changes: 19 additions & 0 deletions src/test/utils.tsx
Original file line number Diff line number Diff line change
@@ -165,3 +165,22 @@ export const setup = (
...options?.renderOptions
})
});

export function controlConsoleError(expectedMessage: string): void {
// eslint-disable-next-line no-console
const actualConsoleError = console.error;
// eslint-disable-next-line no-console
console.error = jest.fn<ReturnType<typeof console.error>, Parameters<typeof console.error>>(
(error, ...restParameter) => {
if (
(typeof error === 'string' && error === expectedMessage) ||
(error instanceof Error && error.message === expectedMessage)
) {
// eslint-disable-next-line no-console
console.error('Controlled error', error, ...restParameter);
} else {
actualConsoleError(error, ...restParameter);
}
}
);
}
69 changes: 69 additions & 0 deletions src/utility-bar/bar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react';

import { act, screen, waitFor } from '@testing-library/react';

import { ShellUtilityBar } from './bar';
import { mockedAccount, setupAccountStore } from '../test/account-utils';
import { ICONS } from '../test/constants';
import { setup } from '../test/utils';
import * as networkUtils from '../network/utils';
import { useLoginConfigStore } from '../store/login/store';
import { Logout } from '../settings/components/general-settings/logout';
import { waitForRequest } from '../mocks/server';

jest.mock<typeof import('../workers')>('../workers');
jest.mock<typeof import('../reporting')>('../reporting');

describe('Shell utility bar', () => {
test('should render the utility menu for the account', async () => {
setupAccountStore();
const { getByRoleWithIcon, user } = setup(<ShellUtilityBar />);

const accountUtilityMenu = getByRoleWithIcon('button', { icon: ICONS.accountUtilityMenu });
expect(accountUtilityMenu).toBeVisible();
await user.click(accountUtilityMenu);
await screen.findByText(mockedAccount.displayName);
expect(screen.getByText(mockedAccount.displayName)).toBeVisible();
expect(screen.getByText(mockedAccount.name)).toBeVisible();
expect(screen.getByText(/feedback/i)).toBeVisible();
expect(screen.getByText(/update view/i)).toBeVisible();
});

test.each(['Feedback', 'Update view', 'Documentation', 'Logout'])(
'should show the entry "%s" inside the account utility menu',
async () => {
setupAccountStore();
const { getByRoleWithIcon, user } = setup(<ShellUtilityBar />);

const accountUtilityMenu = getByRoleWithIcon('button', { icon: ICONS.accountUtilityMenu });
expect(accountUtilityMenu).toBeVisible();
await user.click(accountUtilityMenu);
await screen.findByText(mockedAccount.displayName);
expect(screen.getByText(/feedback/i)).toBeVisible();
}
);

test('should redirect to logout if user clicks on logout', async () => {
const customLogout = 'custom.logout.url';
const goToFn = jest.spyOn(networkUtils, 'goTo').mockImplementation();
const goToLoginFn = jest.spyOn(networkUtils, 'goToLogin').mockImplementation();
useLoginConfigStore.setState((s) => ({ ...s, carbonioWebUiLogoutURL: customLogout }));
const { user, getByRoleWithIcon } = setup(<ShellUtilityBar />);
const logout = waitForRequest('get', '/?loginOp=logout');
await user.click(getByRoleWithIcon('button', { icon: ICONS.accountUtilityMenu }));
await user.click(screen.getByText(/logout/i));
await logout;
act(() => {
jest.runOnlyPendingTimers();
});
await waitFor(() => expect(goToFn).toHaveBeenCalled());
expect(goToFn).toHaveBeenCalledTimes(1);
expect(goToFn).toHaveBeenCalledWith(customLogout);
expect(goToLoginFn).not.toHaveBeenCalled();
});
});