Skip to content

Commit

Permalink
Merge pull request #1534 from folio-org/STCOR-875
Browse files Browse the repository at this point in the history
STCOR-875 sync keycloak-ramsons to master
  • Loading branch information
zburke authored Sep 20, 2024
2 parents f694821 + 28845c3 commit 5d30376
Show file tree
Hide file tree
Showing 44 changed files with 1,211 additions and 532 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
* Terminate the session when the fixed-length session expires. Refs STCOR-862.
* Ensure support for the passed `tenantId` value by `useChunkedCQLFetch` for manipulations in the context of a specific tenant. Refs STCOR-873.
* Provide `key` to elements in `<SessionEventContainer>`. Refs STCOR-874.
* Correctly populate `stripes.user.user` on reload. Refs STCOR-860.
* Correctly evaluate `stripes.okapi` before rendering `<RootWithIntl>`. Refs STCOR-864.
* Change main navigation's skip link label to "Skip to main content". Refs STCOR-863.
* Invalidate `QueryClient` cache on login/logout. Refs STCOR-832.
* Ensure support for the passed `tenantId` value by `useChunkedCQLFetch` for manipulations in the context of a specific tenant. Refs STCOR-873.
* When re-authenticating after logout timeout, return to previous location. Refs STCOR-849.
* Add `nl` (Dutch, Flemish) to the supported locales. Refs STCOR-878.

## [10.1.1](https://github.com/folio-org/stripes-core/tree/v10.1.1) (2024-03-25)
[Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.1.0...v10.1.1)
Expand Down Expand Up @@ -54,7 +61,6 @@
* Add `idName` and `limit` as passable props to `useChunkedCQLFetch`. Refs STCOR-821.
* Check for valid token before rotating during XHR send. Refs STCOR-817.
* Remove `autoComplete` from `<ForgotPassword>`, `<ForgotUsername>` fields. Refs STCOR-742.
* Use keycloak URLs in place of users-bl for tenant-switch. Refs US1153537.

## [10.0.3](https://github.com/folio-org/stripes-core/tree/v10.0.3) (2023-11-10)
[Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.0.2...v10.0.3)
Expand Down
7 changes: 4 additions & 3 deletions src/RootWithIntl.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { StripesContext } from './StripesContext';
import { CalloutContext } from './CalloutContext';
import AuthnLogin from './components/AuthnLogin';

const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAuth, history = {} }) => {
const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAuth, history = {}, queryClient }) => {
const connect = connectFor('@folio/core', stripes.epics, stripes.logger);
const connectedStripes = stripes.clone({ connect });

Expand All @@ -66,7 +66,7 @@ const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAut
<>
<MainContainer>
<AppCtxMenuProvider>
<MainNav stripes={connectedStripes} />
<MainNav stripes={connectedStripes} queryClient={queryClient} />
{typeof connectedStripes?.config?.staleBundleWarning === 'object' && <StaleBundleWarning />}
<HandlerManager
event={events.LOGIN}
Expand All @@ -75,7 +75,7 @@ const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAut
{ (typeof connectedStripes.okapi !== 'object' || connectedStripes.discovery.isFinished) && (
<ModuleContainer id="content">
<OverlayContainer />
{connectedStripes.config.useSecureTokens && <SessionEventContainer history={history} />}
{connectedStripes.config.useSecureTokens && <SessionEventContainer history={history} queryClient={queryClient} />}
<Switch>
<TitledRoute
name="home"
Expand Down Expand Up @@ -191,6 +191,7 @@ RootWithIntl.propTypes = {
isAuthenticated: PropTypes.bool,
disableAuth: PropTypes.bool.isRequired,
history: PropTypes.shape({}),
queryClient: PropTypes.object.isRequired,
};

export default RootWithIntl;
41 changes: 35 additions & 6 deletions src/RootWithIntl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,27 @@ import { Router as DefaultRouter } from 'react-router-dom';
import { createMemoryHistory } from 'history';

import AuthnLogin from './components/AuthnLogin';
import MainNav from './components/MainNav';
import MainContainer from './components/MainContainer';
import ModuleContainer from './components/ModuleContainer';
import {
Login,
MainNav,
MainContainer,
ModuleContainer,
OverlayContainer,
StaleBundleWarning,
SessionEventContainer,
} from './components';

import RootWithIntl from './RootWithIntl';
import Stripes from './Stripes';

jest.mock('./components/AuthnLogin', () => () => '<AuthnLogin>');
jest.mock('./components/Login', () => () => '<Login>');
jest.mock('./components/MainNav', () => () => '<MainNav>');
jest.mock('./components/ModuleContainer', () => () => '<ModuleContainer>');
jest.mock('./components/OverlayContainer', () => () => '<OverlayContainer>');
jest.mock('./components/ModuleContainer', () => ({ children }) => children);
jest.mock('./components/MainContainer', () => ({ children }) => children);
jest.mock('./components/StaleBundleWarning', () => () => '<StaleBundleWarning>');
jest.mock('./components/SessionEventContainer', () => () => '<SessionEventContainer>');

const defaultHistory = createMemoryHistory();

Expand Down Expand Up @@ -82,14 +93,32 @@ describe('RootWithIntl', () => {
const stripes = new Stripes({ epics: {}, logger: {}, bindings: {}, config: {}, store, discovery: { isFinished: true } });
await render(<Harness><RootWithIntl stripes={stripes} history={defaultHistory} isAuthenticated /></Harness>);

expect(screen.getByText(/<ModuleContainer>/)).toBeInTheDocument();
expect(screen.queryByText(/<Login>/)).toBeNull();
expect(screen.queryByText(/<MainNav>/)).toBeInTheDocument();
expect(screen.getByText(/<OverlayContainer>/)).toBeInTheDocument();
});

it('if discovery is finished', async () => {
const stripes = new Stripes({ epics: {}, logger: {}, bindings: {}, config: {}, store, okapi: {}, discovery: { isFinished: true } });
await render(<Harness><RootWithIntl stripes={stripes} history={defaultHistory} isAuthenticated /></Harness>);

expect(screen.getByText(/<ModuleContainer>/)).toBeInTheDocument();
expect(screen.queryByText(/<Login>/)).toBeNull();
expect(screen.queryByText(/<MainNav>/)).toBeInTheDocument();
expect(screen.getByText(/<OverlayContainer>/)).toBeInTheDocument();
});
});

it('renders StaleBundleWarning', async () => {
const stripes = new Stripes({ epics: {}, logger: {}, bindings: {}, config: { staleBundleWarning: {} }, store, okapi: {}, discovery: { isFinished: true } });
await render(<Harness><RootWithIntl stripes={stripes} history={defaultHistory} isAuthenticated /></Harness>);

expect(screen.getByText(/<StaleBundleWarning>/)).toBeInTheDocument();
});

it('renders SessionEventContainer', async () => {
const stripes = new Stripes({ epics: {}, logger: {}, bindings: {}, config: { useSecureTokens: true }, store, okapi: {}, discovery: { isFinished: true } });
await render(<Harness><RootWithIntl stripes={stripes} history={defaultHistory} isAuthenticated /></Harness>);

expect(screen.getByText(/<SessionEventContainer>/)).toBeInTheDocument();
});
});
10 changes: 2 additions & 8 deletions src/components/Logout/Logout.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router';
import { FormattedMessage } from 'react-intl';

Expand All @@ -13,16 +12,15 @@ import { getLocale, logout } from '../../loginServices';
* This corresponds to the '/logout' route, allowing that route to be directly
* accessible rather than only accessible through the menu action.
*
* @param {object} history
*/
const Logout = ({ history }) => {
const Logout = () => {
const stripes = useStripes();
const [didLogout, setDidLogout] = useState(false);

useEffect(
() => {
getLocale(stripes.okapi.url, stripes.store, stripes.okapi.tenant)
.then(logout(stripes.okapi.url, stripes.store, history))
.then(logout(stripes.okapi.url, stripes.store))
.then(setDidLogout(true));
},
// no dependencies because we only want to start the logout process once.
Expand All @@ -35,8 +33,4 @@ const Logout = ({ history }) => {
return didLogout ? <Redirect to="/" /> : <FormattedMessage id="stripes-core.logoutPending" />;
};

Logout.propTypes = {
history: PropTypes.object,
};

export default Logout;
14 changes: 12 additions & 2 deletions src/components/LogoutTimeout/LogoutTimeout.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import {

import OrganizationLogo from '../OrganizationLogo';
import { useStripes } from '../../StripesContext';
import { logout } from '../../loginServices';
import {
getUnauthorizedPathFromSession,
logout,
removeUnauthorizedPathFromSession,
} from '../../loginServices';

import styles from './LogoutTimeout.css';

Expand Down Expand Up @@ -48,6 +52,12 @@ const LogoutTimeout = () => {
[]
);

const handleClick = (_e) => {
removeUnauthorizedPathFromSession();
};

const redirectTo = getUnauthorizedPathFromSession() || '/';

if (!didLogout) {
return <LoadingView />;
}
Expand All @@ -68,7 +78,7 @@ const LogoutTimeout = () => {
</Row>
<Row center="xs">
<Col xs={12}>
<Button to="/"><FormattedMessage id="stripes-core.rtr.idleSession.logInAgain" /></Button>
<Button to={redirectTo} onClick={handleClick}><FormattedMessage id="stripes-core.rtr.idleSession.logInAgain" /></Button>
</Col>
</Row>
</div>
Expand Down
63 changes: 49 additions & 14 deletions src/components/LogoutTimeout/LogoutTimeout.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { render, screen } from '@folio/jest-config-stripes/testing-library/react';
import { userEvent } from '@folio/jest-config-stripes/testing-library/user-event';

import LogoutTimeout from './LogoutTimeout';
import { useStripes } from '../../StripesContext';
import { logout } from '../../loginServices';


import { getUnauthorizedPathFromSession, logout, setUnauthorizedPathToSession } from '../../loginServices';

jest.mock('../OrganizationLogo');
jest.mock('../../StripesContext');
Expand All @@ -14,23 +13,59 @@ jest.mock('react-router', () => ({

jest.mock('../../loginServices', () => ({
logout: jest.fn(() => Promise.resolve()),
getUnauthorizedPathFromSession: jest.fn(),
removeUnauthorizedPathFromSession: jest.fn(),
setUnauthorizedPathToSession: jest.fn(),
}));

describe('LogoutTimeout', () => {
it('if not authenticated, renders a timeout message', async () => {
const mockUseStripes = useStripes;
mockUseStripes.mockReturnValue({ okapi: { isAuthenticated: false } });
describe('if not authenticated', () => {
it('renders a timeout message', async () => {
const mockUseStripes = useStripes;
mockUseStripes.mockReturnValue({ okapi: { isAuthenticated: false } });

render(<LogoutTimeout />);
screen.getByText('stripes-core.rtr.idleSession.sessionExpiredSoSad');
});

it('clears previous path from storage after clicking "log in again"', async () => {
const previousPath = '/monkey?bagel';
setUnauthorizedPathToSession(previousPath);
const user = userEvent.setup();
const mockUseStripes = useStripes;
mockUseStripes.mockReturnValue({ okapi: { isAuthenticated: false } });

render(<LogoutTimeout />);

render(<LogoutTimeout />);
screen.getByText('stripes-core.rtr.idleSession.sessionExpiredSoSad');
await user.click(screen.getByRole('button'));
expect(getUnauthorizedPathFromSession()).toBeFalsy();
});
});

it('if authenticated, calls logout then renders a timeout message', async () => {
const mockUseStripes = useStripes;
mockUseStripes.mockReturnValue({ okapi: { isAuthenticated: true } });
describe('if not authenticated', () => {
it('calls logout then renders a timeout message', async () => {
const mockUseStripes = useStripes;
mockUseStripes.mockReturnValue({ okapi: { isAuthenticated: true } });

render(<LogoutTimeout />);
expect(logout).toHaveBeenCalled();
screen.getByText('stripes-core.rtr.idleSession.sessionExpiredSoSad');
});

it('clears previous path from storage after clicking "log in again"', async () => {
const previousPath = '/monkey?bagel';
setUnauthorizedPathToSession(previousPath);
const user = userEvent.setup();
const mockUseStripes = useStripes;
mockUseStripes.mockReturnValue({ okapi: { isAuthenticated: true } });

render(<LogoutTimeout />);

expect(logout).toHaveBeenCalled();
screen.getByText('stripes-core.rtr.idleSession.sessionExpiredSoSad');

render(<LogoutTimeout />);
expect(logout).toHaveBeenCalled();
screen.getByText('stripes-core.rtr.idleSession.sessionExpiredSoSad');
await user.click(screen.getByRole('button'));
expect(getUnauthorizedPathFromSession()).toBeFalsy();
});
});
});
6 changes: 5 additions & 1 deletion src/components/MainNav/MainNav.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ class MainNav extends Component {
}).isRequired,
modules: PropTypes.shape({
app: PropTypes.arrayOf(PropTypes.object),
})
}),
queryClient: PropTypes.object.isRequired,
};

constructor(props) {
Expand Down Expand Up @@ -93,6 +94,9 @@ class MainNav extends Component {
}
}
});

// remove QueryProvider cache to be 100% sure we're starting from a clean slate.
this.props.queryClient.removeQueries();
}

componentDidUpdate(prevProps) {
Expand Down
1 change: 1 addition & 0 deletions src/components/Root/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class Root extends Component {
isAuthenticated={isAuthenticated}
disableAuth={disableAuth}
history={history}
queryClient={this.reactQueryClient}
/>
</IntlProvider>
</QueryClientProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import SessionEventContainer, {
thisWindowRtrError,
thisWindowRtrIstTimeout,
} from './SessionEventContainer';
import { SESSION_NAME } from '../../loginServices';
import {
setUnauthorizedPathToSession,
SESSION_NAME,
} from '../../loginServices';
import { RTR_TIMEOUT_EVENT } from '../Root/constants';

import { toggleRtrModal } from '../../okapiActions';
Expand Down Expand Up @@ -70,7 +73,11 @@ describe('SessionEventContainer event listeners', () => {
it('thisWindowRtrError', async () => {
const history = { push: jest.fn() };

const setUnauthorizedPathToSessionMock = setUnauthorizedPathToSession;
setUnauthorizedPathToSessionMock.mockReturnValue(null);

thisWindowRtrError(null, { okapi: { url: 'http' } }, history);
expect(setUnauthorizedPathToSession).toHaveBeenCalled();
expect(history.push).toHaveBeenCalledWith('/logout-timeout');
});

Expand Down
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export { default as BadRequestScreen } from './BadRequestScreen';
export { default as NoPermissionScreen } from './NoPermissionScreen';
export { default as ResetPasswordNotAvailableScreen } from './ResetPasswordNotAvailableScreen';
export { default as SessionEventContainer } from './SessionEventContainer';
export { default as StaleBundleWarning } from './StaleBundleWarning';

export * from './ModuleHierarchy';
export * from './Namespace';
8 changes: 8 additions & 0 deletions src/helpers/hideEmail/hideEmail.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import hideEmail from './hideEmail';

describe('hideEmail', () => {
it('masks email addresses', () => {
expect(hideEmail('[email protected]')).toEqual('te**@e******.***');
expect(hideEmail('[email protected]')).toEqual('mo****@b****.*****');
});
});
Loading

0 comments on commit 5d30376

Please sign in to comment.