Add Session context and redirect on auth

This commit is contained in:
Marliana Lara
2021-04-14 12:48:57 -04:00
parent a286324ab7
commit a097602d7f
10 changed files with 287 additions and 161 deletions

View File

@@ -13,6 +13,7 @@ import { i18n } from '@lingui/core';
import { Card, PageSection } from '@patternfly/react-core'; import { Card, PageSection } from '@patternfly/react-core';
import { ConfigProvider, useAuthorizedPath } from './contexts/Config'; import { ConfigProvider, useAuthorizedPath } from './contexts/Config';
import { SessionProvider, useSession } from './contexts/Session';
import AppContainer from './components/AppContainer'; import AppContainer from './components/AppContainer';
import Background from './components/Background'; import Background from './components/Background';
import ContentError from './components/ContentError'; import ContentError from './components/ContentError';
@@ -82,16 +83,23 @@ const AuthorizedRoutes = ({ routeConfig }) => {
); );
}; };
const ProtectedRoute = ({ children, ...rest }) => const ProtectedRoute = ({ children, ...rest }) => {
isAuthenticated(document.cookie) ? ( const { authRedirectTo, setAuthRedirectTo } = useSession();
<Route {...rest}> const { pathname } = useLocation();
<ErrorBoundary FallbackComponent={ErrorFallback}>
{children} if (isAuthenticated(document.cookie)) {
</ErrorBoundary> return (
</Route> <Route {...rest}>
) : ( <ErrorBoundary FallbackComponent={ErrorFallback}>
<Redirect to="/login" /> {children}
); </ErrorBoundary>
</Route>
)
}
if (authRedirectTo !== null) setAuthRedirectTo(pathname);
return <Redirect to="/login" />;
};
function App() { function App() {
let language = getLanguageWithoutRegionCode(navigator); let language = getLanguageWithoutRegionCode(navigator);
@@ -109,24 +117,26 @@ function App() {
return ( return (
<I18nProvider i18n={i18n}> <I18nProvider i18n={i18n}>
<Background> <Background>
<Switch> <SessionProvider>
<Route exact strict path="/*/"> <Switch>
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} /> <Route exact strict path="/*/">
</Route> <Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
<Route path="/login"> </Route>
<Login isAuthenticated={isAuthenticated} /> <Route path="/login">
</Route> <Login isAuthenticated={isAuthenticated} />
<Route exact path="/"> </Route>
<Redirect to="/home" /> <Route exact path="/">
</Route> <Redirect to="/home" />
<ProtectedRoute> </Route>
<ConfigProvider> <ProtectedRoute>
<AppContainer navRouteConfig={getRouteConfig()}> <ConfigProvider>
<AuthorizedRoutes routeConfig={getRouteConfig()} /> <AppContainer navRouteConfig={getRouteConfig()}>
</AppContainer> <AuthorizedRoutes routeConfig={getRouteConfig()} />
</ConfigProvider> </AppContainer>
</ProtectedRoute> </ConfigProvider>
</Switch> </ProtectedRoute>
</Switch>
</SessionProvider>
</Background> </Background>
</I18nProvider> </I18nProvider>
); );

View File

@@ -1,17 +1,27 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../testUtils/enzymeHelpers'; import { mountWithContexts } from '../testUtils/enzymeHelpers';
import * as SessionContext from './contexts/Session';
import App from './App'; import App from './App';
jest.mock('./api'); jest.mock('./api');
describe('<App />', () => { describe('<App />', () => {
test('renders ok', async () => { test('renders ok', async () => {
const contextValues = {
setAuthRedirectTo: jest.fn(),
isSessionExpired: false,
};
jest
.spyOn(SessionContext, 'useSession')
.mockImplementation(() => contextValues);
let wrapper; let wrapper;
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<App />); wrapper = mountWithContexts(<App />);
}); });
expect(wrapper.length).toBe(1); expect(wrapper.length).toBe(1);
jest.clearAllMocks();
wrapper.unmount();
}); });
}); });

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useCallback, useRef } from 'react'; import React, { useState, useEffect } from 'react';
import { useHistory, withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { import {
Button, Button,
Nav, Nav,
@@ -11,133 +11,38 @@ import {
PageHeaderToolsItem, PageHeaderToolsItem,
PageSidebar, PageSidebar,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { t } from '@lingui/macro'; import { t, Plural } from '@lingui/macro';
import styled from 'styled-components'; import styled from 'styled-components';
import { MeAPI, RootAPI } from '../../api';
import { useConfig, useAuthorizedPath } from '../../contexts/Config'; import { useConfig, useAuthorizedPath } from '../../contexts/Config';
import { SESSION_TIMEOUT_KEY } from '../../constants'; import { useSession } from '../../contexts/Session';
import { isAuthenticated } from '../../util/auth';
import issuePendoIdentity from '../../util/issuePendoIdentity'; import issuePendoIdentity from '../../util/issuePendoIdentity';
import About from '../About'; import About from '../About';
import AlertModal from '../AlertModal';
import BrandLogo from './BrandLogo'; import BrandLogo from './BrandLogo';
import NavExpandableGroup from './NavExpandableGroup'; import NavExpandableGroup from './NavExpandableGroup';
import PageHeaderToolbar from './PageHeaderToolbar'; import PageHeaderToolbar from './PageHeaderToolbar';
import AlertModal from '../AlertModal';
// The maximum supported timeout for setTimeout(), in milliseconds,
// is the highest number you can represent as a signed 32bit
// integer (approximately 25 days)
const MAX_TIMEOUT = 2 ** (32 - 1) - 1;
// The number of seconds the session timeout warning is displayed
// before the user is logged out. Increasing this number (up to
// the total session time, which is 1800s by default) will cause
// the session timeout warning to display sooner.
const SESSION_WARNING_DURATION = 10;
const PageHeader = styled(PFPageHeader)` const PageHeader = styled(PFPageHeader)`
& .pf-c-page__header-brand-link { & .pf-c-page__header-brand-link {
color: inherit; color: inherit;
&:hover { &:hover {
color: inherit; color: inherit;
} }
} }
`; `;
/**
* The useStorage hook integrates with the browser's localStorage api.
* It accepts a storage key as its only argument and returns a state
* variable and setter function for that state variable.
*
* This utility behaves much like the standard useState hook with some
* key differences:
* 1. You don't pass it an initial value. Instead, the provided key
* is used to retrieve the initial value from local storage. If
* the key doesn't exist in local storage, null is returned.
* 2. Behind the scenes, this hook registers an event listener with
* the Web Storage api to establish a two-way binding between the
* state variable and its corresponding local storage value. This
* means that updates to the state variable with the setter
* function will produce a corresponding update to the local
* storage value and vice-versa.
* 3. When local storage is shared across browser tabs, the data
* binding is also shared across browser tabs. This means that
* updates to the state variable using the setter function on
* one tab will also update the state variable on any other tab
* using this hook with the same key and vice-versa.
*/
function useStorage(key) {
const [storageVal, setStorageVal] = useState(
window.localStorage.getItem(key)
);
window.addEventListener('storage', () => {
const newVal = window.localStorage.getItem(key);
if (newVal !== storageVal) {
setStorageVal(newVal);
}
});
const setValue = val => {
window.localStorage.setItem(key, val);
setStorageVal(val);
};
return [storageVal, setValue];
}
function AppContainer({ navRouteConfig = [], children }) { function AppContainer({ navRouteConfig = [], children }) {
const history = useHistory();
const config = useConfig(); const config = useConfig();
const { logout, handleSessionContinue, sessionCountdown } = useSession();
const isReady = !!config.license_info; const isReady = !!config.license_info;
const isSidebarVisible = useAuthorizedPath(); const isSidebarVisible = useAuthorizedPath();
const [isAboutModalOpen, setIsAboutModalOpen] = useState(false); const [isAboutModalOpen, setIsAboutModalOpen] = useState(false);
const sessionTimeoutId = useRef();
const sessionIntervalId = useRef();
const [sessionTimeout, setSessionTimeout] = useStorage(SESSION_TIMEOUT_KEY);
const [timeoutWarning, setTimeoutWarning] = useState(false);
const [timeRemaining, setTimeRemaining] = useState(null);
const handleAboutModalOpen = () => setIsAboutModalOpen(true); const handleAboutModalOpen = () => setIsAboutModalOpen(true);
const handleAboutModalClose = () => setIsAboutModalOpen(false); const handleAboutModalClose = () => setIsAboutModalOpen(false);
const handleSessionTimeout = () => setTimeoutWarning(true);
const handleLogout = useCallback(async () => {
await RootAPI.logout();
setSessionTimeout(null);
}, [setSessionTimeout]);
const handleSessionContinue = () => {
MeAPI.read();
setTimeoutWarning(false);
};
useEffect(() => {
if (!isAuthenticated(document.cookie)) history.replace('/login');
const calcRemaining = () =>
parseInt(sessionTimeout, 10) - new Date().getTime();
const updateRemaining = () => setTimeRemaining(calcRemaining());
setTimeoutWarning(false);
clearTimeout(sessionTimeoutId.current);
clearInterval(sessionIntervalId.current);
sessionTimeoutId.current = setTimeout(
handleSessionTimeout,
Math.min(calcRemaining() - SESSION_WARNING_DURATION * 1000, MAX_TIMEOUT)
);
sessionIntervalId.current = setInterval(updateRemaining, 1000);
return () => {
clearTimeout(sessionTimeoutId.current);
clearInterval(sessionIntervalId.current);
};
}, [history, sessionTimeout]);
useEffect(() => {
if (timeRemaining !== null && timeRemaining <= 1) {
handleLogout();
}
}, [handleLogout, timeRemaining]);
useEffect(() => { useEffect(() => {
if ('analytics_status' in config) { if ('analytics_status' in config) {
@@ -159,7 +64,7 @@ function AppContainer({ navRouteConfig = [], children }) {
loggedInUser={config?.me} loggedInUser={config?.me}
isAboutDisabled={!config?.version} isAboutDisabled={!config?.version}
onAboutClick={handleAboutModalOpen} onAboutClick={handleAboutModalOpen}
onLogoutClick={handleLogout} onLogoutClick={logout}
/> />
} }
/> />
@@ -172,7 +77,7 @@ function AppContainer({ navRouteConfig = [], children }) {
<PageHeaderTools> <PageHeaderTools>
<PageHeaderToolsGroup> <PageHeaderToolsGroup>
<PageHeaderToolsItem> <PageHeaderToolsItem>
<Button onClick={handleLogout} variant="tertiary" ouiaId="logout"> <Button onClick={logout} variant="tertiary" ouiaId="logout">
{t`Logout`} {t`Logout`}
</Button> </Button>
</PageHeaderToolsItem> </PageHeaderToolsItem>
@@ -219,8 +124,8 @@ function AppContainer({ navRouteConfig = [], children }) {
<AlertModal <AlertModal
ouiaId="session-expiration-modal" ouiaId="session-expiration-modal"
title={t`Your session is about to expire`} title={t`Your session is about to expire`}
isOpen={timeoutWarning && sessionTimeout > 0 && timeRemaining !== null} isOpen={sessionCountdown && sessionCountdown > 0}
onClose={handleLogout} onClose={logout}
showClose={false} showClose={false}
variant="warning" variant="warning"
actions={[ actions={[
@@ -236,15 +141,17 @@ function AppContainer({ navRouteConfig = [], children }) {
ouiaId="session-expiration-logout-button" ouiaId="session-expiration-logout-button"
key="logout" key="logout"
variant="secondary" variant="secondary"
onClick={handleLogout} onClick={logout}
> >
{t`Logout`} {t`Logout`}
</Button>, </Button>,
]} ]}
> >
{t`You will be logged out in ${Number( <Plural
Math.max(Math.floor(timeRemaining / 1000), 0) value={sessionCountdown}
)} seconds due to inactivity.`} one="You will be logged out in # second due to inactivity"
other="You will be logged out in # seconds due to inactivity"
/>
</AlertModal> </AlertModal>
</> </>
); );

View File

@@ -181,12 +181,17 @@ describe('<AppContainer />', () => {
test('logout makes expected call to api client', async () => { test('logout makes expected call to api client', async () => {
const userMenuButton = 'UserIcon'; const userMenuButton = 'UserIcon';
const logoutButton = '#logout-button button'; const logoutButton = '#logout-button button';
const logout = jest.fn();
let wrapper; let wrapper;
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<AppContainer />); wrapper = mountWithContexts(<AppContainer />, {
context: {
session: {
logout,
},
},
});
}); });
// open the user menu // open the user menu
expect(wrapper.find(logoutButton)).toHaveLength(0); expect(wrapper.find(logoutButton)).toHaveLength(0);
wrapper.find(userMenuButton).simulate('click'); wrapper.find(userMenuButton).simulate('click');
@@ -194,6 +199,6 @@ describe('<AppContainer />', () => {
// logout // logout
wrapper.find(logoutButton).simulate('click'); wrapper.find(logoutButton).simulate('click');
expect(RootAPI.logout).toHaveBeenCalledTimes(1); expect(logout).toHaveBeenCalledTimes(1);
}); });
}); });

View File

@@ -10,15 +10,12 @@ import {
EmptyStateBody, EmptyStateBody,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { ExclamationTriangleIcon } from '@patternfly/react-icons'; import { ExclamationTriangleIcon } from '@patternfly/react-icons';
import { RootAPI } from '../../api'; import { useSession } from '../../contexts/Session';
import ErrorDetail from '../ErrorDetail'; import ErrorDetail from '../ErrorDetail';
async function logout() {
await RootAPI.logout();
window.location.replace('#/login');
}
function ContentError({ error, children, isNotFound }) { function ContentError({ error, children, isNotFound }) {
const { logout } = useSession();
if (error && error.response && error.response.status === 401) { if (error && error.response && error.response.status === 401) {
if (!error.response.headers['session-timeout']) { if (!error.response.headers['session-timeout']) {
logout(); logout();

View File

@@ -3,13 +3,14 @@ import { useRouteMatch } from 'react-router-dom';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ConfigAPI, MeAPI, RootAPI } from '../api'; import { ConfigAPI, MeAPI } from '../api';
import useRequest, { useDismissableError } from '../util/useRequest'; import useRequest, { useDismissableError } from '../util/useRequest';
import AlertModal from '../components/AlertModal'; import AlertModal from '../components/AlertModal';
import ErrorDetail from '../components/ErrorDetail'; import ErrorDetail from '../components/ErrorDetail';
import { useSession } from './Session';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const ConfigContext = React.createContext([{}, () => {}]); export const ConfigContext = React.createContext({});
ConfigContext.displayName = 'ConfigContext'; ConfigContext.displayName = 'ConfigContext';
export const Config = ConfigContext.Consumer; export const Config = ConfigContext.Consumer;
@@ -22,6 +23,8 @@ export const useConfig = () => {
}; };
export const ConfigProvider = ({ children }) => { export const ConfigProvider = ({ children }) => {
const { logout } = useSession();
const { error: configError, isLoading, request, result: config } = useRequest( const { error: configError, isLoading, request, result: config } = useRequest(
useCallback(async () => { useCallback(async () => {
const [ const [
@@ -45,9 +48,9 @@ export const ConfigProvider = ({ children }) => {
useEffect(() => { useEffect(() => {
if (error?.response?.status === 401) { if (error?.response?.status === 401) {
RootAPI.logout(); logout();
} }
}, [error]); }, [error, logout]);
const value = useMemo(() => ({ ...config, request, isLoading }), [ const value = useMemo(() => ({ ...config, request, isLoading }), [
config, config,

View File

@@ -0,0 +1,163 @@
import React, {
useContext,
useEffect,
useMemo,
useState,
useRef,
useCallback,
} from 'react';
import { useHistory } from 'react-router-dom';
import { RootAPI, MeAPI } from '../api';
import { SESSION_TIMEOUT_KEY } from '../constants';
import { isAuthenticated } from '../util/auth';
// The maximum supported timeout for setTimeout(), in milliseconds,
// is the highest number you can represent as a signed 32bit
// integer (approximately 25 days)
const MAX_TIMEOUT = 2 ** (32 - 1) - 1;
// The number of seconds the session timeout warning is displayed
// before the user is logged out. Increasing this number (up to
// the total session time, which is 1800s by default) will cause
// the session timeout warning to display sooner.
const SESSION_WARNING_DURATION = 10;
/**
* The useStorage hook integrates with the browser's localStorage api.
* It accepts a storage key as its only argument and returns a state
* variable and setter function for that state variable.
*
* This utility behaves much like the standard useState hook with some
* key differences:
* 1. You don't pass it an initial value. Instead, the provided key
* is used to retrieve the initial value from local storage. If
* the key doesn't exist in local storage, null is returned.
* 2. Behind the scenes, this hook registers an event listener with
* the Web Storage api to establish a two-way binding between the
* state variable and its corresponding local storage value. This
* means that updates to the state variable with the setter
* function will produce a corresponding update to the local
* storage value and vice-versa.
* 3. When local storage is shared across browser tabs, the data
* binding is also shared across browser tabs. This means that
* updates to the state variable using the setter function on
* one tab will also update the state variable on any other tab
* using this hook with the same key and vice-versa.
*/
function useStorage(key) {
const [storageVal, setStorageVal] = useState(
window.localStorage.getItem(key)
);
window.addEventListener('storage', () => {
const newVal = window.localStorage.getItem(key);
if (newVal !== storageVal) {
setStorageVal(newVal);
}
});
const setValue = val => {
window.localStorage.setItem(key, val);
setStorageVal(val);
};
return [storageVal, setValue];
}
const SessionContext = React.createContext({});
SessionContext.displayName = 'SessionContext';
function SessionProvider({ children }) {
const history = useHistory();
const isSessionExpired = useRef(false);
const sessionTimeoutId = useRef();
const sessionIntervalId = useRef();
const [sessionTimeout, setSessionTimeout] = useStorage(SESSION_TIMEOUT_KEY);
const [sessionCountdown, setSessionCountdown] = useState(0);
const [authRedirectTo, setAuthRedirectTo] = useState('/home');
const logout = useCallback(async () => {
if (!isSessionExpired.current) setAuthRedirectTo(null);
await RootAPI.logout();
setSessionTimeout(null);
setSessionCountdown(0);
clearTimeout(sessionTimeoutId.current);
clearInterval(sessionIntervalId.current);
}, [setSessionTimeout, setSessionCountdown]);
useEffect(() => {
if (!isAuthenticated(document.cookie)) {
history.replace('/login');
return () => {};
}
const calcRemaining = () =>
parseInt(sessionTimeout, 10) - new Date().getTime();
const handleSessionTimeout = () => {
let countDown = SESSION_WARNING_DURATION;
setSessionCountdown(countDown);
sessionIntervalId.current = setInterval(() => {
if (countDown > 0) {
setSessionCountdown(--countDown);
} else {
isSessionExpired.current = true;
logout();
}
}, 1000);
};
clearTimeout(sessionTimeoutId.current);
clearInterval(sessionIntervalId.current);
isSessionExpired.current = false;
sessionTimeoutId.current = setTimeout(
handleSessionTimeout,
Math.min(calcRemaining() - SESSION_WARNING_DURATION * 1000, MAX_TIMEOUT)
);
return () => {
clearTimeout(sessionTimeoutId.current);
clearInterval(sessionIntervalId.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [history, sessionTimeout]);
const handleSessionContinue = useCallback(async () => {
await MeAPI.read();
setSessionCountdown(0);
clearTimeout(sessionTimeoutId.current);
clearInterval(sessionIntervalId.current);
}, []);
const value = useMemo(
() => ({
logout,
authRedirectTo,
setAuthRedirectTo,
handleSessionContinue,
sessionCountdown,
isSessionExpired,
}),
[
logout,
authRedirectTo,
setAuthRedirectTo,
handleSessionContinue,
sessionCountdown,
isSessionExpired,
]
);
return (
<SessionContext.Provider value={value}>{children}</SessionContext.Provider>
);
}
function useSession() {
const context = useContext(SessionContext);
if (context === undefined) {
throw new Error('useSession must be used within a SessionProvider');
}
return context;
}
export { SessionContext, SessionProvider, useSession };

View File

@@ -6,6 +6,7 @@ import { Formik } from 'formik';
import styled from 'styled-components'; import styled from 'styled-components';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import { import {
Alert,
Brand, Brand,
LoginMainFooterLinksItem, LoginMainFooterLinksItem,
LoginForm, LoginForm,
@@ -28,6 +29,7 @@ import useRequest, { useDismissableError } from '../../util/useRequest';
import { AuthAPI, RootAPI } from '../../api'; import { AuthAPI, RootAPI } from '../../api';
import AlertModal from '../../components/AlertModal'; import AlertModal from '../../components/AlertModal';
import ErrorDetail from '../../components/ErrorDetail'; import ErrorDetail from '../../components/ErrorDetail';
import { useSession } from '../../contexts/Session';
const loginLogoSrc = '/static/media/logo-login.svg'; const loginLogoSrc = '/static/media/logo-login.svg';
@@ -37,7 +39,9 @@ const Login = styled(PFLogin)`
} }
`; `;
function AWXLogin({ alt, isAuthenticated }) { function AWXLogin({ alt, i18n, isAuthenticated }) {
const { authRedirectTo, isSessionExpired, setAuthRedirectTo } = useSession();
const { const {
isLoading: isCustomLoginInfoLoading, isLoading: isCustomLoginInfoLoading,
error: customLoginInfoError, error: customLoginInfoError,
@@ -110,6 +114,7 @@ function AWXLogin({ alt, isAuthenticated }) {
const handleSubmit = async values => { const handleSubmit = async values => {
dismissAuthError(); dismissAuthError();
await authenticate(values); await authenticate(values);
setAuthRedirectTo('/home');
}; };
if (isCustomLoginInfoLoading) { if (isCustomLoginInfoLoading) {
@@ -120,7 +125,7 @@ function AWXLogin({ alt, isAuthenticated }) {
return null; return null;
} }
if (isAuthenticated(document.cookie)) { if (isAuthenticated(document.cookie)) {
return <Redirect to="/" />; return <Redirect to={authRedirectTo || '/'} />;
} }
let helperText; let helperText;
@@ -151,6 +156,13 @@ function AWXLogin({ alt, isAuthenticated }) {
subtitle={t`Please log in`} subtitle={t`Please log in`}
/> />
<LoginMainBody> <LoginMainBody>
{isSessionExpired.current ? (
<Alert
variant="warning"
isInline
title={t`Your session has expired. Please log in to continue where you left off.`}
/>
) : null}
<Formik <Formik
initialValues={{ initialValues={{
password: '', password: '',

View File

@@ -35,3 +35,12 @@ jest.doMock('./contexts/Config', () => ({
useConfig: () => React.useContext(MockConfigContext), useConfig: () => React.useContext(MockConfigContext),
useAuthorizedPath: jest.fn(), useAuthorizedPath: jest.fn(),
})); }));
// ?
const MockSessionContext = React.createContext({});
jest.doMock('./contexts/Session', () => ({
__esModule: true,
SessionContext: MockSessionContext,
SessionProvider: MockSessionContext.Provider,
useSession: () => React.useContext(MockSessionContext),
}));

View File

@@ -10,6 +10,7 @@ import { I18nProvider } from '@lingui/react';
import { i18n } from '@lingui/core'; import { i18n } from '@lingui/core';
import { en } from 'make-plural/plurals'; import { en } from 'make-plural/plurals';
import english from '../src/locales/en/messages'; import english from '../src/locales/en/messages';
import { SessionProvider } from '../src/contexts/Session';
import { ConfigProvider } from '../src/contexts/Config'; import { ConfigProvider } from '../src/contexts/Config';
i18n.loadLocaleData({ en: { plurals: en } }); i18n.loadLocaleData({ en: { plurals: en } });
@@ -57,10 +58,14 @@ const defaultContexts = {
}, },
toJSON: () => '/router/', toJSON: () => '/router/',
}, },
session: {
isSessionExpired: false,
logout: () => {},
},
}; };
function wrapContexts(node, context) { function wrapContexts(node, context) {
const { config, router } = context; const { config, router, session } = context;
class Wrap extends React.Component { class Wrap extends React.Component {
render() { render() {
// eslint-disable-next-line react/no-this-in-sfc // eslint-disable-next-line react/no-this-in-sfc
@@ -69,17 +74,21 @@ function wrapContexts(node, context) {
if (router.history) { if (router.history) {
return ( return (
<I18nProvider i18n={i18n}> <I18nProvider i18n={i18n}>
<ConfigProvider value={config}> <SessionProvider value={session}>
<Router history={router.history}>{component}</Router> <ConfigProvider value={config}>
</ConfigProvider> <Router history={router.history}>{component}</Router>
</ConfigProvider>
</SessionProvider>
</I18nProvider> </I18nProvider>
); );
} }
return ( return (
<I18nProvider i18n={i18n}> <I18nProvider i18n={i18n}>
<ConfigProvider value={config}> <SessionProvider value={session}>
<MemoryRouter>{component}</MemoryRouter> <ConfigProvider value={config}>
</ConfigProvider> <MemoryRouter>{component}</MemoryRouter>
</ConfigProvider>
</SessionProvider>
</I18nProvider> </I18nProvider>
); );
} }
@@ -122,6 +131,7 @@ export function mountWithContexts(node, options = {}) {
}).isRequired, }).isRequired,
history: shape({}), history: shape({}),
}), }),
session: shape({}),
...options.childContextTypes, ...options.childContextTypes,
}; };
return mount(wrapContexts(node, context), { context, childContextTypes }); return mount(wrapContexts(node, context), { context, childContextTypes });