From 2576bcadc0f9bc16c114708112b30cf09b6237c9 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Fri, 10 Nov 2023 15:34:10 -0500 Subject: [PATCH] Revert RTR (#1371) Revert all PRs related to STCOR-571, which implemented RTR in a service worker. This includes the following: * Revert "STCOR-759 return non-okapi request fetches as-is (#1369)" This reverts commit a323456fb14760b0c5fcb5558039ac62013cd40e. * Revert "STCOR-759 read okapi config from micro-stripes-config (#1366)" This reverts commit 6c6a85e34122656758976e625da06fbfc0b8dd69. * Revert "STCOR-756 correctly evaluate token lifespan (#1363)" This reverts commit 607dc5c5d78fc5d7eb3d8b083b6122ad53181d78. * Revert "STCOR-574 rotate tokens after 80% of their TTL (#1361)" This reverts commit dd718191f9a23572c5e085ab7355408a09bf1b63. * Revert "STCOR-671 handle access-control via cookies (#1346)" This reverts commit 27d29484c907f059c090aaa3a8fda2af753e8049. Also, add a dummy service-worker.js file to appease stripes-webpack. This removes the need to do extra clean-up work in that repository. --- CHANGELOG.md | 4 - index.js | 4 - package.json | 1 - src/App.js | 7 - src/RootWithIntl.js | 8 +- src/Stripes.js | 4 +- src/components/MainNav/MainNav.js | 13 +- src/components/Root/Root.js | 19 +- src/createApolloClient.js | 4 +- src/discoverServices.js | 11 +- src/loginServices.js | 279 ++------- src/loginServices.test.js | 202 +------ src/mainActions.js | 4 + src/okapiActions.js | 23 +- src/okapiActions.test.js | 15 - src/okapiReducer.js | 12 +- src/okapiReducer.test.js | 8 +- src/queries/useConfigurations.test.js | 14 - src/queries/useOkapiEnv.test.js | 14 - src/service-worker.test.js | 569 ------------------ src/serviceWorkerRegistration.js | 79 --- src/serviceWorkerRegistration.test.js | 132 ---- src/useOkapiKy.js | 7 +- src/useOkapiKy.test.js | 3 + src/withOkapiKy.js | 4 +- test/bigtest/helpers/setup-application.js | 11 +- test/bigtest/network/config.js | 14 +- .../network/scenarios/fifthAttemptToLogin.js | 2 +- .../network/scenarios/invalidResponseBody.js | 2 +- .../network/scenarios/lockedAccount.js | 2 +- .../network/scenarios/multipleErrors.js | 2 +- test/bigtest/network/scenarios/serverError.js | 2 +- .../network/scenarios/thirdAttemptToLogin.js | 2 +- .../network/scenarios/wrongPassword.js | 2 +- .../network/scenarios/wrongUsername.js | 2 +- test/bigtest/tests/login-test.js | 8 - test/bigtest/tests/session-timeout-test.js | 2 +- test/jest/__mock__/index.js | 1 - test/jest/__mock__/microStripesConfig.mock.js | 5 - 39 files changed, 164 insertions(+), 1333 deletions(-) delete mode 100644 src/service-worker.test.js delete mode 100644 src/serviceWorkerRegistration.js delete mode 100644 src/serviceWorkerRegistration.test.js delete mode 100644 test/jest/__mock__/microStripesConfig.mock.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aae6d1c4..27bc69a50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,10 +25,7 @@ * Avoid private path when import `validateUser` function. Refs STCOR-749. * Ensure `` is not cut off when app name is long. Refs STCOR-752. * Use cookies and RTR instead of directly handling the JWT. Refs STCOR-671, FOLIO-3627. -* Shrink the token lifespan so we are less likely to use an expired one. Refs STCOR-754. -* Correctly evaluate token lifespan; use consistent protocol for service worker messages. Refs STCOR-756. * Allow console to be preserved on logout. STCOR-761. -* Do not catch and reject non-okapi request errors. Refs STCOR-759, UILDP-129. ## [10.0.0](https://github.com/folio-org/stripes-core/tree/v10.0.0) (2023-10-11) [Full Changelog](https://github.com/folio-org/stripes-core/compare/v9.0.0...v10.0.0) @@ -54,7 +51,6 @@ * *BREAKING* bump `react-intl` to `v6.4.4`. Refs STCOR-744. * Bump `stylelint` to `v15` and `stylelint-config-standard` to `v34`. Refs STCOR-745. * Read ky timeout from stripes-config value. Refs STCOR-594. -* *BREAKING* use cookies and RTR instead of directly handling the JWT. Refs STCOR-671, FOLIO-3627. ## [9.0.0](https://github.com/folio-org/stripes-core/tree/v9.0.0) (2023-01-30) [Full Changelog](https://github.com/folio-org/stripes-core/compare/v8.3.0...v9.0.0) diff --git a/index.js b/index.js index 877ba3222..f9598cb04 100644 --- a/index.js +++ b/index.js @@ -45,7 +45,3 @@ export { userLocaleConfig } from './src/loginServices'; export * from './src/consortiaServices'; export { default as queryLimit } from './src/queryLimit'; export { default as init } from './src/init'; - -/* RTR and service worker */ -export { postTokenExpiration } from './src/loginServices'; -export { registerServiceWorker, unregisterServiceWorker } from './src/serviceWorkerRegistration'; diff --git a/package.json b/package.json index 36631f51c..1c171d274 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "graphql": "^16.0.0", "history": "^4.6.3", "hoist-non-react-statics": "^3.3.0", - "inactivity-timer": "^1.0.0", "jwt-decode": "^3.1.2", "ky": "^0.23.0", "localforage": "^1.5.6", diff --git a/src/App.js b/src/App.js index 8d7503b59..437037235 100644 --- a/src/App.js +++ b/src/App.js @@ -11,7 +11,6 @@ import gatherActions from './gatherActions'; import { destroyStore } from './mainActions'; import Root from './components/Root'; -import { registerServiceWorker } from './serviceWorkerRegistration'; export default class StripesCore extends Component { static propTypes = { @@ -31,12 +30,6 @@ export default class StripesCore extends Component { this.epics = configureEpics(connectErrorEpic); this.store = configureStore(initialState, this.logger, this.epics); this.actionNames = gatherActions(); - - // register a service worker, providing okapi and stripes config details. - // the service worker functions as a proxy between between the browser - // and the network, intercepting ALL fetch requests to make sure they - // are accompanied by a valid access-token. - registerServiceWorker(okapiConfig, config, this.logger); } componentWillUnmount() { diff --git a/src/RootWithIntl.js b/src/RootWithIntl.js index aa54a9e9d..1f67251b5 100644 --- a/src/RootWithIntl.js +++ b/src/RootWithIntl.js @@ -46,13 +46,13 @@ class RootWithIntl extends React.Component { logger: PropTypes.object.isRequired, clone: PropTypes.func.isRequired, }).isRequired, - isAuthenticated: PropTypes.bool, + token: PropTypes.string, disableAuth: PropTypes.bool.isRequired, history: PropTypes.shape({}), }; static defaultProps = { - isAuthenticated: false, + token: '', history: {}, }; @@ -66,7 +66,7 @@ class RootWithIntl extends React.Component { render() { const { - isAuthenticated, + token, disableAuth, history, } = this.props; @@ -85,7 +85,7 @@ class RootWithIntl extends React.Component { > - { isAuthenticated || disableAuth ? + { token || disableAuth ? <> diff --git a/src/Stripes.js b/src/Stripes.js index db4b4913d..560397a39 100644 --- a/src/Stripes.js +++ b/src/Stripes.js @@ -49,7 +49,7 @@ export const stripesShape = PropTypes.shape({ ]), okapiReady: PropTypes.bool, tenant: PropTypes.string.isRequired, - isAuthenticated: PropTypes.bool, + token: PropTypes.string, translations: PropTypes.object, url: PropTypes.string.isRequired, withoutOkapi: PropTypes.bool, @@ -57,10 +57,10 @@ export const stripesShape = PropTypes.shape({ plugins: PropTypes.object, setBindings: PropTypes.func.isRequired, setCurrency: PropTypes.func.isRequired, - setIsAuthenticated: PropTypes.func.isRequired, setLocale: PropTypes.func.isRequired, setSinglePlugin: PropTypes.func.isRequired, setTimezone: PropTypes.func.isRequired, + setToken: PropTypes.func.isRequired, store: PropTypes.shape({ dispatch: PropTypes.func.isRequired, getState: PropTypes.func.isRequired, diff --git a/src/components/MainNav/MainNav.js b/src/components/MainNav/MainNav.js index d1b7a6e07..86a886fa6 100644 --- a/src/components/MainNav/MainNav.js +++ b/src/components/MainNav/MainNav.js @@ -4,6 +4,7 @@ import { isEqual, find } from 'lodash'; import { compose } from 'redux'; import { injectIntl } from 'react-intl'; import { withRouter } from 'react-router'; +import localforage from 'localforage'; import { branding, config } from 'stripes-config'; @@ -11,7 +12,9 @@ import { Icon } from '@folio/stripes-components'; import { withModules } from '../Modules'; import { LastVisitedContext } from '../LastVisited'; -import { getLocale, logout as sessionLogout } from '../../loginServices'; +import { clearOkapiToken, clearCurrentUser } from '../../okapiActions'; +import { resetStore } from '../../mainActions'; +import { getLocale } from '../../loginServices'; import { updateQueryResource, getLocationQuery, @@ -120,8 +123,12 @@ class MainNav extends Component { returnToLogin() { const { okapi } = this.store.getState(); - return getLocale(okapi.url, this.store, okapi.tenant) - .then(sessionLogout(okapi.url, this.store)); + return getLocale(okapi.url, this.store, okapi.tenant).then(() => { + this.store.dispatch(clearOkapiToken()); + this.store.dispatch(clearCurrentUser()); + this.store.dispatch(resetStore()); + localforage.removeItem('okapiSess'); + }); } // return the user to the login screen, but after logging in they will be brought to the default screen. diff --git a/src/components/Root/Root.js b/src/components/Root/Root.js index 75725d37a..b4b549cc6 100644 --- a/src/components/Root/Root.js +++ b/src/components/Root/Root.js @@ -20,8 +20,8 @@ import initialReducers from '../../initialReducers'; import enhanceReducer from '../../enhanceReducer'; import createApolloClient from '../../createApolloClient'; import createReactQueryClient from '../../createReactQueryClient'; -import { setSinglePlugin, setBindings, setIsAuthenticated, setTimezone, setCurrency, updateCurrentUser } from '../../okapiActions'; -import { addServiceWorkerListeners, loadTranslations, checkOkapiSession } from '../../loginServices'; +import { setSinglePlugin, setBindings, setOkapiToken, setTimezone, setCurrency, updateCurrentUser } from '../../okapiActions'; +import { loadTranslations, checkOkapiSession } from '../../loginServices'; import { getQueryResourceKey, getCurrentModule } from '../../locationService'; import Stripes from '../../Stripes'; import RootWithIntl from '../../RootWithIntl'; @@ -40,7 +40,7 @@ class Root extends Component { constructor(...args) { super(...args); - const { modules, history, okapi, store } = this.props; + const { modules, history, okapi } = this.props; this.reducers = { ...initialReducers }; this.epics = {}; @@ -64,9 +64,6 @@ class Root extends Component { this.apolloClient = createApolloClient(okapi); this.reactQueryClient = createReactQueryClient(); - - // service-worker message listeners - addServiceWorkerListeners(okapi, store); } getChildContext() { @@ -110,7 +107,7 @@ class Root extends Component { } render() { - const { logger, store, epics, config, okapi, actionNames, isAuthenticated, disableAuth, currentUser, currentPerms, locale, defaultTranslations, timezone, currency, plugins, bindings, discovery, translations, history, serverDown } = this.props; + const { logger, store, epics, config, okapi, actionNames, token, disableAuth, currentUser, currentPerms, locale, defaultTranslations, timezone, currency, plugins, bindings, discovery, translations, history, serverDown } = this.props; if (serverDown) { return
Error: server is down.
; @@ -128,7 +125,7 @@ class Root extends Component { config, okapi, withOkapi: this.withOkapi, - setIsAuthenticated: (val) => { store.dispatch(setIsAuthenticated(val)); }, + setToken: (val) => { store.dispatch(setOkapiToken(val)); }, actionNames, locale, timezone, @@ -169,7 +166,7 @@ class Root extends Component { > @@ -194,7 +191,7 @@ Root.propTypes = { getState: PropTypes.func.isRequired, replaceReducer: PropTypes.func.isRequired, }), - isAuthenticated: PropTypes.bool, + token: PropTypes.string, disableAuth: PropTypes.bool.isRequired, logger: PropTypes.object.isRequired, currentPerms: PropTypes.object, @@ -252,13 +249,13 @@ function mapStateToProps(state) { currentPerms: state.okapi.currentPerms, currentUser: state.okapi.currentUser, discovery: state.discovery, - isAuthenticated: state.okapi.isAuthenticated, locale: state.okapi.locale, okapi: state.okapi, okapiReady: state.okapi.okapiReady, plugins: state.okapi.plugins, serverDown: state.okapi.serverDown, timezone: state.okapi.timezone, + token: state.okapi.token, translations: state.okapi.translations, }; } diff --git a/src/createApolloClient.js b/src/createApolloClient.js index 393afb8bb..9819bda6c 100644 --- a/src/createApolloClient.js +++ b/src/createApolloClient.js @@ -1,10 +1,10 @@ import { InMemoryCache, ApolloClient } from '@apollo/client'; -const createClient = ({ url, tenant }) => (new ApolloClient({ +const createClient = ({ url, tenant, token }) => (new ApolloClient({ uri: `${url}/graphql`, - credentials: 'include', headers: { 'X-Okapi-Tenant': tenant, + 'X-Okapi-Token': token, }, cache: new InMemoryCache(), })); diff --git a/src/discoverServices.js b/src/discoverServices.js index 29360f498..7c5e33812 100644 --- a/src/discoverServices.js +++ b/src/discoverServices.js @@ -1,8 +1,9 @@ import { some } from 'lodash'; -function getHeaders(tenant) { +function getHeaders(tenant, token) { return { 'X-Okapi-Tenant': tenant, + 'X-Okapi-Token': token, 'Content-Type': 'application/json' }; } @@ -11,9 +12,7 @@ function fetchOkapiVersion(store) { const okapi = store.getState().okapi; return fetch(`${okapi.url}/_/version`, { - headers: getHeaders(okapi.tenant), - credentials: 'include', - mode: 'cors', + headers: getHeaders(okapi.tenant, okapi.token) }).then((response) => { // eslint-disable-line consistent-return if (response.status >= 400) { store.dispatch({ type: 'DISCOVERY_FAILURE', code: response.status }); @@ -32,9 +31,7 @@ function fetchModules(store) { const okapi = store.getState().okapi; return fetch(`${okapi.url}/_/proxy/tenants/${okapi.tenant}/modules?full=true`, { - headers: getHeaders(okapi.tenant), - credentials: 'include', - mode: 'cors', + headers: getHeaders(okapi.tenant, okapi.token) }).then((response) => { // eslint-disable-line consistent-return if (response.status >= 400) { store.dispatch({ type: 'DISCOVERY_FAILURE', code: response.status }); diff --git a/src/loginServices.js b/src/loginServices.js index b99107f93..d2acceae8 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -1,10 +1,9 @@ import localforage from 'localforage'; -import { config, translations } from 'stripes-config'; +import { translations } from 'stripes-config'; import rtlDetect from 'rtl-detect'; import moment from 'moment'; import { discoverServices } from './discoverServices'; -import { resetStore } from './mainActions'; import { clearCurrentUser, @@ -15,18 +14,16 @@ import { setPlugins, setBindings, setTranslations, - setIsAuthenticated, + clearOkapiToken, setAuthError, checkSSO, setOkapiReady, setServerDown, setSessionData, - setTokenExpiration, setLoginData, updateCurrentUser, } from './okapiActions'; import processBadResponse from './processBadResponse'; -import configureLogger from './configureLogger'; // export supported locales, i.e. the languages we provide translations for export const supportedLocales = [ @@ -66,20 +63,16 @@ export const supportedNumberingSystems = [ 'arab', // Arabic-Hindi (٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩) ]; -/** name for the session key in local storage */ -const SESSION_NAME = 'okapiSess'; - // export config values for storing user locale export const userLocaleConfig = { 'configName': 'localeSettings', 'module': '@folio/stripes-core', }; -const logger = configureLogger(config); - -function getHeaders(tenant) { +function getHeaders(tenant, token) { return { 'X-Okapi-Tenant': tenant, + 'X-Okapi-Token': token, 'Content-Type': 'application/json', }; } @@ -171,15 +164,12 @@ export function loadTranslations(store, locale, defaultTranslations = {}) { * @returns {Promise} */ function dispatchLocale(url, store, tenant) { - return fetch(url, { - headers: getHeaders(tenant), - credentials: 'include', - mode: 'cors', - }) + return fetch(url, + { headers: getHeaders(tenant, store.getState().okapi.token) }) .then((response) => { if (response.status === 200) { response.json().then((json) => { - if (json.configs?.length) { + if (json.configs.length) { const localeValues = JSON.parse(json.configs[0].value); const { locale, timezone, currency } = localeValues; if (locale) { @@ -250,15 +240,12 @@ export function getUserLocale(okapiUrl, store, tenant, userId) { * @returns {Promise} */ export function getPlugins(okapiUrl, store, tenant) { - return fetch(`${okapiUrl}/configurations/entries?query=(module==PLUGINS)`, { - headers: getHeaders(tenant), - credentials: 'include', - mode: 'cors', - }) + return fetch(`${okapiUrl}/configurations/entries?query=(module==PLUGINS)`, + { headers: getHeaders(tenant, store.getState().okapi.token) }) .then((response) => { if (response.status < 400) { response.json().then((json) => { - const configs = json.configs?.reduce((acc, val) => ({ + const configs = json.configs.reduce((acc, val) => ({ ...acc, [val.configName]: val.value, }), {}); @@ -279,11 +266,8 @@ export function getPlugins(okapiUrl, store, tenant) { * @returns {Promise} */ export function getBindings(okapiUrl, store, tenant) { - return fetch(`${okapiUrl}/configurations/entries?query=(module==ORG and configName==bindings)`, { - headers: getHeaders(tenant), - credentials: 'include', - mode: 'cors', - }) + return fetch(`${okapiUrl}/configurations/entries?query=(module==ORG and configName==bindings)`, + { headers: getHeaders(tenant, store.getState().okapi.token) }) .then((response) => { let bindings = {}; if (response.status >= 400) { @@ -291,7 +275,7 @@ export function getBindings(okapiUrl, store, tenant) { } else { response.json().then((json) => { const configs = json.configs; - if (Array.isArray(configs) && configs.length > 0) { + if (configs.length > 0) { const string = configs[0].value; try { const tmp = JSON.parse(string); @@ -363,62 +347,13 @@ export function spreadUserWithPerms(userWithPerms) { return { user, perms }; } -/** - * logout - * dispatch events to clear the store, then clear the session too. - * - * @param {object} redux store - * - * @returns {Promise} - */ -export async function logout(okapiUrl, store) { - store.dispatch(setIsAuthenticated(false)); - store.dispatch(clearCurrentUser()); - store.dispatch(resetStore()); - return fetch(`${okapiUrl}/authn/logout`, { - method: 'POST', - mode: 'cors', - credentials: 'include' - }) - .then(localforage.removeItem(SESSION_NAME)) - .then(localforage.removeItem('loginResponse')); -} - -/** - * postTokenExpiration - * send SW a TOKEN_EXPIRATION message - * @param {object} tokenExpiration shaped like { atExpires, rtExpires} where both are millisecond timestamps - * - * @returns {Promise} - */ -export const postTokenExpiration = (tokenExpiration) => { - if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { - return navigator.serviceWorker.ready - .then((reg) => { - const sw = reg.active; - if (sw) { - const message = { source: '@folio/stripes-core', type: 'TOKEN_EXPIRATION', value: { tokenExpiration } }; - logger.log('rtr', '<= sending', message); - sw.postMessage(message); - } else { - logger.log('rtr', 'error, could not send TOKEN_EXPIRATION message; no ServiceWorker is active'); - } - }); - } - - logger.log('rtr', 'error, could not send TOKEN_EXPIRATION message; navigator.serviceWorker is empty'); - return Promise.resolve(); -}; - /** * createOkapiSession * Remap the given data into a session object shaped like: * { * user: { id, username, personal } - * tenant: string, * perms: { permNameA: true, permNameB: true, ... } - * isAuthenticated: boolean, - * tokenExpiration: { atExpires, rtExpires } + * token: token * } * Dispatch the session object, then return a Promise that fetches * and dispatches tenant resources. @@ -426,11 +361,12 @@ export const postTokenExpiration = (tokenExpiration) => { * @param {*} okapiUrl * @param {*} store * @param {*} tenant + * @param {*} token * @param {*} data * * @returns {Promise} */ -export function createOkapiSession(okapiUrl, store, tenant, data) { +export function createOkapiSession(okapiUrl, store, tenant, token, data) { // clear any auth-n errors store.dispatch(setAuthError(null)); @@ -442,81 +378,54 @@ export function createOkapiSession(okapiUrl, store, tenant, data) { store.dispatch(setCurrentPerms(perms)); - // if we can't parse tokenExpiration data, e.g. because data comes from `/bl-users/_self` - // which doesn't provide it, then set an invalid AT value and a near-future (+10 minutes) RT value. - // the invalid AT will prompt an RTR cycle which will either give us new AT/RT values - // (if the RT was valid) or throw an RTR_ERROR (if the RT was not valid). - const tokenExpiration = { - atExpires: data.tokenExpiration?.accessTokenExpiration ? new Date(data.tokenExpiration.accessTokenExpiration).getTime() : -1, - rtExpires: data.tokenExpiration?.refreshTokenExpiration ? new Date(data.tokenExpiration.refreshTokenExpiration).getTime() : Date.now() + (10 * 60 * 1000), - }; - const sessionTenant = data.tenant || tenant; const okapiSess = { - isAuthenticated: true, + token, user, perms, tenant: sessionTenant, - tokenExpiration, }; - // provide token-expiration info to the service worker - return postTokenExpiration(tokenExpiration) - .then(localforage.setItem('loginResponse', data)) - .then(() => localforage.setItem(SESSION_NAME, okapiSess)) + return localforage.setItem('loginResponse', data) + .then(() => localforage.setItem('okapiSess', okapiSess)) .then(() => { - store.dispatch(setIsAuthenticated(true)); store.dispatch(setSessionData(okapiSess)); return loadResources(okapiUrl, store, sessionTenant, user.id); }); } /** - * handleServiceWorkerMessage - * Handle messages posted by service workers - * * TOKEN_EXPIRATION: update the redux store - * * RTR_ERROR: logout + * validateUser + * return a promise that fetches from bl-users/self. + * if successful, dispatch the result to create a session + * if not, clear the session and token. * - * @param {Event} event - * @param {object} store redux-store + * @param {string} okapiUrl + * @param {redux store} store + * @param {string} tenant + * @param {object} session + * + * @returns {Promise} */ -export const handleServiceWorkerMessage = (event, store) => { - // only accept events whose origin matches this window's origin, - // i.e. if this is a same-origin event. Browsers allow cross-origin - // message exchange, but we're only interested in the events we control. - if ((!event.origin) || (event.origin !== window.location.origin)) { - return; - } - - if (event.data.source === '@folio/stripes-core') { - // RTR happened: update token expiration timestamps in our store - if (event.data.type === 'TOKEN_EXPIRATION') { - store.dispatch(setTokenExpiration({ - atExpires: new Date(event.data.value.tokenExpiration.atExpires).toISOString(), - rtExpires: new Date(event.data.value.tokenExpiration.rtExpires).toISOString(), - })); - } +export function validateUser(okapiUrl, store, tenant, session) { + const { token, user, perms, tenant: sessionTenant = tenant } = session; - // RTR failed: we have no cookies; logout - if (event.data.type === 'RTR_ERROR') { - logger.log('rtr', 'rtr error; logging out', event.data.error); - store.dispatch(setIsAuthenticated(false)); + return fetch(`${okapiUrl}/bl-users/_self`, { headers: getHeaders(sessionTenant, token) }).then((resp) => { + if (resp.ok) { + return resp.json().then((data) => { + store.dispatch(setLoginData(data)); + store.dispatch(setSessionData({ token, user, perms, tenant: sessionTenant })); + return loadResources(okapiUrl, store, sessionTenant, user.id); + }); + } else { store.dispatch(clearCurrentUser()); - store.dispatch(resetStore()); - localforage.removeItem(SESSION_NAME) - .then(localforage.removeItem('loginResponse')); + store.dispatch(clearOkapiToken()); + return localforage.removeItem('okapiSess'); } - } -}; - -export function addServiceWorkerListeners(okapiConfig, store) { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.addEventListener('message', (e) => { - handleServiceWorkerMessage(e, store); - }); - } else { - logger.log('rtr', 'error; navigator.serviceWorker is empty'); - } + }).catch((error) => { + store.dispatch(setServerDown()); + return error; + }); } /** @@ -593,7 +502,7 @@ function processSSOLoginResponse(resp) { * @returns {Promise} resolving to the response's JSON */ export function handleLoginError(dispatch, resp) { - return localforage.removeItem(SESSION_NAME) + return localforage.removeItem('okapiSess') .then(() => processBadResponse(dispatch, resp)) .then(responseBody => { dispatch(setOkapiReady()); @@ -609,16 +518,18 @@ export function handleLoginError(dispatch, resp) { * @param {redux store} store * @param {string} tenant * @param {Response} resp HTTP response + * @param {string} ssoToken * * @returns {Promise} resolving with login response body, rejecting with, ummmmm */ -export function processOkapiSession(okapiUrl, store, tenant, resp) { +export function processOkapiSession(okapiUrl, store, tenant, resp, ssoToken) { + const token = resp.headers.get('X-Okapi-Token') || ssoToken; const { dispatch } = store; if (resp.ok) { return resp.json() .then(json => { - return createOkapiSession(okapiUrl, store, tenant, json) + return createOkapiSession(okapiUrl, store, tenant, token, json) .then(() => json); }) .then((json) => { @@ -630,64 +541,6 @@ export function processOkapiSession(okapiUrl, store, tenant, resp) { } } -/** - * validateUser - * return a promise that fetches from bl-users/self. - * if successful, dispatch the result to create a session - * if not, clear the session and token. - * - * @param {string} okapiUrl - * @param {redux store} store - * @param {string} tenant - * @param {object} session - * - * @returns {Promise} - */ -export function validateUser(okapiUrl, store, tenant, session) { - const { user, perms, tenant: sessionTenant = tenant } = session; - return fetch(`${okapiUrl}/bl-users/_self`, { - headers: getHeaders(sessionTenant), - credentials: 'include', - mode: 'cors', - }).then((resp) => { - if (resp.ok) { - return resp.json().then((data) => { - // clear any auth-n errors - store.dispatch(setAuthError(null)); - store.dispatch(setLoginData(data)); - - // If the request succeeded, we know the AT must be valid, but the - // response body from this endpoint doesn't include token-expiration - // data. So ... we set a near-future RT and an already-expired AT. - // On the next request, the expired AT will prompt an RTR cycle and - // we'll get real expiration values then. - const tokenExpiration = { - atExpires: -1, - rtExpires: Date.now() + (10 * 60 * 1000), - }; - // provide token-expiration info to the service-worker - return postTokenExpiration(tokenExpiration) - .then(() => { - store.dispatch(setSessionData({ - isAuthenticated: true, - user, - perms, - tenant: sessionTenant, - tokenExpiration, - })); - return loadResources(okapiUrl, store, sessionTenant, user.id); - }); - }); - } else { - return logout(okapiUrl, store); - } - }).catch((error) => { - console.error(error); // eslint-disable-line no-console - store.dispatch(setServerDown()); - return error; - }); -} - /** * checkOkapiSession * 1. Pull the session from local storage; if non-empty validate it, dispatching load-resources actions. @@ -699,7 +552,7 @@ export function validateUser(okapiUrl, store, tenant, session) { * @param {string} tenant */ export function checkOkapiSession(okapiUrl, store, tenant) { - localforage.getItem(SESSION_NAME) + localforage.getItem('okapiSess') .then((sess) => { return sess !== null ? validateUser(okapiUrl, store, tenant, sess) : null; }) @@ -723,12 +576,10 @@ export function checkOkapiSession(okapiUrl, store, tenant) { * @returns {Promise} */ export function requestLogin(okapiUrl, store, tenant, data) { - return fetch(`${okapiUrl}/bl-users/login-with-expiry?expandPermissions=true&fullPermissions=true`, { - body: JSON.stringify(data), - credentials: 'include', - headers: { 'X-Okapi-Tenant': tenant, 'Content-Type': 'application/json' }, + return fetch(`${okapiUrl}/bl-users/login?expandPermissions=true&fullPermissions=true`, { method: 'POST', - mode: 'cors', + headers: { 'X-Okapi-Tenant': tenant, 'Content-Type': 'application/json' }, + body: JSON.stringify(data), }) .then(resp => processOkapiSession(okapiUrl, store, tenant, resp)); } @@ -738,13 +589,14 @@ export function requestLogin(okapiUrl, store, tenant, data) { * retrieve currently-authenticated user * @param {string} okapiUrl * @param {string} tenant + * @param {string} token * * @returns {Promise} Promise resolving to the response of the request */ -function fetchUserWithPerms(okapiUrl, tenant) { +function fetchUserWithPerms(okapiUrl, tenant, token) { return fetch( `${okapiUrl}/bl-users/_self?expandPermissions=true&fullPermissions=true`, - { headers: getHeaders(tenant) }, + { headers: getHeaders(tenant, token) }, ); } @@ -754,12 +606,13 @@ function fetchUserWithPerms(okapiUrl, tenant) { * @param {string} okapiUrl * @param {redux store} store * @param {string} tenant + * @param {string} token * * @returns {Promise} Promise resolving to the response-body (JSON) of the request */ -export function requestUserWithPerms(okapiUrl, store, tenant) { - return fetchUserWithPerms(okapiUrl, tenant) - .then(resp => processOkapiSession(okapiUrl, store, tenant, resp)); +export function requestUserWithPerms(okapiUrl, store, tenant, token) { + return fetchUserWithPerms(okapiUrl, tenant, token) + .then(resp => processOkapiSession(okapiUrl, store, tenant, resp, token)); } /** @@ -795,10 +648,10 @@ export function requestSSOLogin(okapiUrl, tenant) { * @returns {Promise} */ export function updateUser(store, data) { - return localforage.getItem(SESSION_NAME) + return localforage.getItem('okapiSess') .then((sess) => { sess.user = { ...sess.user, ...data }; - return localforage.setItem(SESSION_NAME, sess); + return localforage.setItem('okapiSess', sess); }) .then(() => { store.dispatch(updateCurrentUser(data)); @@ -815,9 +668,9 @@ export function updateUser(store, data) { * @returns {Promise} */ export async function updateTenant(okapi, tenant) { - const okapiSess = await localforage.getItem(SESSION_NAME); - const userWithPermsResponse = await fetchUserWithPerms(okapi.url, tenant); + const okapiSess = await localforage.getItem('okapiSess'); + const userWithPermsResponse = await fetchUserWithPerms(okapi.url, tenant, okapi.token); const userWithPerms = await userWithPermsResponse.json(); - await localforage.setItem(SESSION_NAME, { ...okapiSess, tenant, ...spreadUserWithPerms(userWithPerms) }); + await localforage.setItem('okapiSess', { ...okapiSess, tenant, ...spreadUserWithPerms(userWithPerms) }); } diff --git a/src/loginServices.test.js b/src/loginServices.test.js index d46935ac9..1c7f6a6f0 100644 --- a/src/loginServices.test.js +++ b/src/loginServices.test.js @@ -1,21 +1,18 @@ import localforage from 'localforage'; import { + spreadUserWithPerms, createOkapiSession, handleLoginError, - handleServiceWorkerMessage, loadTranslations, processOkapiSession, - spreadUserWithPerms, supportedLocales, supportedNumberingSystems, - updateTenant, updateUser, + updateTenant, validateUser, } from './loginServices'; -import { resetStore } from './mainActions'; - import { clearCurrentUser, setCurrentPerms, @@ -25,35 +22,19 @@ import { // setPlugins, // setBindings, // setTranslations, + clearOkapiToken, setAuthError, // checkSSO, - setIsAuthenticated, setOkapiReady, setServerDown, - // setSessionData, - setTokenExpiration, + setSessionData, setLoginData, updateCurrentUser, } from './okapiActions'; import { defaultErrors } from './constants'; -// reassign console.log to keep things quiet -const consoleInterruptor = {}; -beforeAll(() => { - consoleInterruptor.log = global.console.log; - consoleInterruptor.error = global.console.error; - consoleInterruptor.warn = global.console.warn; - console.log = () => { }; - console.error = () => { }; - console.warn = () => { }; -}); -afterAll(() => { - global.console.log = consoleInterruptor.log; - global.console.error = consoleInterruptor.error; - global.console.warn = consoleInterruptor.warn; -}); jest.mock('localforage', () => ({ getItem: jest.fn(() => Promise.resolve({ user: {} })), @@ -83,8 +64,9 @@ const mockFetchCleanUp = () => { delete global.fetch; }; + describe('createOkapiSession', () => { - it('clears authentication errors and sends a TOKEN_EXPIRATION message', async () => { + it('clears authentication errors', async () => { const store = { dispatch: jest.fn(), getState: () => ({ @@ -94,50 +76,23 @@ describe('createOkapiSession', () => { }), }; - const postMessage = jest.fn(); - navigator.serviceWorker = { - controller: true, - ready: Promise.resolve({ - active: { - postMessage, - } - }) - }; - - const te = { - accessTokenExpiration: '2023-11-06T18:05:33Z', - refreshTokenExpiration: '2023-10-30T18:15:33Z', - }; - const data = { user: { id: 'user-id', }, permissions: { permissions: [{ permissionName: 'a' }, { permissionName: 'b' }] - }, - tokenExpiration: te, + } }; const permissionsMap = { a: true, b: true }; + mockFetchSuccess([]); - await createOkapiSession('url', store, 'tenant', data); + await createOkapiSession('url', store, 'tenant', 'token', data); expect(store.dispatch).toHaveBeenCalledWith(setAuthError(null)); expect(store.dispatch).toHaveBeenCalledWith(setLoginData(data)); expect(store.dispatch).toHaveBeenCalledWith(setCurrentPerms(permissionsMap)); - const message = { - source: '@folio/stripes-core', - type: 'TOKEN_EXPIRATION', - value: { - tokenExpiration: { - atExpires: new Date('2023-11-06T18:05:33Z').getTime(), - rtExpires: new Date('2023-10-30T18:15:33Z').getTime(), - }, - } - }; - expect(postMessage).toHaveBeenCalledWith(message); - mockFetchCleanUp(); }); }); @@ -241,7 +196,7 @@ describe('processOkapiSession', () => { mockFetchSuccess(); - await processOkapiSession('url', store, 'tenant', resp); + await processOkapiSession('url', store, 'tenant', resp, 'token'); expect(store.dispatch).toHaveBeenCalledWith(setAuthError(null)); expect(store.dispatch).toHaveBeenCalledWith(setOkapiReady()); @@ -258,7 +213,7 @@ describe('processOkapiSession', () => { } }; - await processOkapiSession('url', store, 'tenant', resp); + await processOkapiSession('url', store, 'tenant', resp, 'token'); expect(store.dispatch).toHaveBeenCalledWith(setOkapiReady()); expect(store.dispatch).toHaveBeenCalledWith(setAuthError([defaultErrors.DEFAULT_LOGIN_CLIENT_ERROR])); @@ -299,45 +254,20 @@ describe('validateUser', () => { const tenant = 'tenant'; const data = { monkey: 'bagel' }; + const token = 'token'; const user = { id: 'id' }; const perms = []; const session = { + token, user, perms, }; mockFetchSuccess(data); - const postMessage = jest.fn(); - navigator.serviceWorker = { - controller: true, - ready: Promise.resolve({ - active: { - postMessage, - } - }) - }; - - // set a fixed system time so date math is stable - const now = new Date('2023-10-30T19:34:56.000Z'); - jest.useFakeTimers().setSystemTime(now); - await validateUser('url', store, tenant, session); - - expect(store.dispatch).nthCalledWith(1, setAuthError(null)); - expect(store.dispatch).nthCalledWith(2, setLoginData(data)); - - const message = { - source: '@folio/stripes-core', - type: 'TOKEN_EXPIRATION', - value: { - tokenExpiration: { - atExpires: -1, - rtExpires: new Date(now).getTime() + (10 * 60 * 1000), - }, - }, - }; - expect(postMessage).toHaveBeenCalledWith(message); + expect(store.dispatch).toHaveBeenCalledWith(setLoginData(data)); + expect(store.dispatch).toHaveBeenCalledWith(setSessionData({ token, user, perms, tenant })); mockFetchCleanUp(); }); @@ -350,22 +280,21 @@ describe('validateUser', () => { const tenant = 'tenant'; const sessionTenant = 'sessionTenant'; const data = { monkey: 'bagel' }; + const token = 'token'; const user = { id: 'id' }; const perms = []; const session = { + token, user, perms, tenant: sessionTenant, }; mockFetchSuccess(data); - navigator.serviceWorker = { - ready: Promise.resolve({}) - }; await validateUser('url', store, tenant, session); - expect(store.dispatch).nthCalledWith(1, setAuthError(null)); - expect(store.dispatch).nthCalledWith(2, setLoginData(data)); + expect(store.dispatch).toHaveBeenCalledWith(setLoginData(data)); + expect(store.dispatch).toHaveBeenCalledWith(setSessionData({ token, user, perms, tenant: sessionTenant })); mockFetchCleanUp(); }); @@ -381,6 +310,7 @@ describe('validateUser', () => { await validateUser('url', store, 'tenant', {}); expect(store.dispatch).toHaveBeenCalledWith(clearCurrentUser()); + expect(store.dispatch).toHaveBeenCalledWith(clearOkapiToken()); mockFetchCleanUp(); }); }); @@ -426,95 +356,3 @@ describe('updateTenant', () => { }); }); }); - - -describe('handleServiceWorkerMessage', () => { - const store = { - dispatch: jest.fn(), - getState: () => ({ - okapi: { - currentPerms: [], - } - }), - }; - - beforeEach(() => { - delete window.location; - }); - - describe('ignores cross-origin events', () => { - it('mismatched event origin', () => { - window.location = new URL('https://www.barbie.com'); - const event = { origin: '' }; - - handleServiceWorkerMessage(event, store); - expect(store.dispatch).not.toHaveBeenCalled(); - }); - - it('missing event origin', () => { - window.location = new URL('https://www.barbie.com'); - const event = { origin: 'https://www.openheimer.com' }; - - handleServiceWorkerMessage(event, store); - expect(store.dispatch).not.toHaveBeenCalled(); - }); - }); - - describe('handles same-origin events', () => { - it('only handles events if data.source is "@folio/stripes-core"', () => { - window.location = new URL('https://www.barbie.com'); - const event = { - origin: 'https://www.barbie.com', - data: { - source: 'monkey-bagel' - } - }; - - handleServiceWorkerMessage(event, store); - expect(store.dispatch).not.toHaveBeenCalled(); - }); - - it('on RTR, dispatches new token-expiration data', () => { - window.location = new URL('https://www.barbie.com'); - const tokenExpiration = { - atExpires: '2023-11-06T18:05:33.000Z', - rtExpires: '2023-10-30T18:15:33.000Z', - }; - - const event = { - origin: 'https://www.barbie.com', - data: { - source: '@folio/stripes-core', - type: 'TOKEN_EXPIRATION', - value: { tokenExpiration }, - } - }; - - handleServiceWorkerMessage(event, store); - expect(store.dispatch).toHaveBeenCalledWith(setTokenExpiration({ ...tokenExpiration })); - }); - - it('on RTR error, ends session', () => { - window.location = new URL('https://www.oppenheimer.com'); - const tokenExpiration = { - atExpires: '2023-11-06T18:05:33.000Z', - rtExpires: '2023-10-30T18:15:33.000Z', - }; - - const event = { - origin: 'https://www.oppenheimer.com', - data: { - source: '@folio/stripes-core', - type: 'RTR_ERROR', - tokenExpiration, - } - }; - - handleServiceWorkerMessage(event, store); - expect(store.dispatch).toHaveBeenCalledWith(setIsAuthenticated(false)); - expect(store.dispatch).toHaveBeenCalledWith(clearCurrentUser()); - expect(store.dispatch).toHaveBeenCalledWith(resetStore()); - }); - }); -}); - diff --git a/src/mainActions.js b/src/mainActions.js index bc41df8d2..91f063fbb 100644 --- a/src/mainActions.js +++ b/src/mainActions.js @@ -18,6 +18,10 @@ function destroyStore() { }; } +// We export a single named function rather than using a default +// export, to remain consistent with okapiActions.js +// +// eslint-disable-next-line import/prefer-default-export export { resetStore, destroyStore, diff --git a/src/okapiActions.js b/src/okapiActions.js index 77834a767..fe3bed7a1 100644 --- a/src/okapiActions.js +++ b/src/okapiActions.js @@ -61,10 +61,16 @@ function setBindings(bindings) { }; } -function setIsAuthenticated(b) { +function setOkapiToken(token) { return { - type: 'SET_IS_AUTHENTICATED', - isAuthenticated: Boolean(b), + type: 'SET_OKAPI_TOKEN', + token, + }; +} + +function clearOkapiToken() { + return { + type: 'CLEAR_OKAPI_TOKEN', }; } @@ -122,31 +128,24 @@ function updateCurrentUser(data) { }; } -function setTokenExpiration(tokenExpiration) { - return { - type: 'SET_TOKEN_EXPIRATION', - tokenExpiration, - }; -} - export { checkSSO, clearCurrentUser, + clearOkapiToken, setAuthError, setBindings, setCurrency, setCurrentPerms, setCurrentUser, - setIsAuthenticated, setLocale, setLoginData, setOkapiReady, + setOkapiToken, setPlugins, setServerDown, setSessionData, setSinglePlugin, setTimezone, - setTokenExpiration, setTranslations, updateCurrentUser, }; diff --git a/src/okapiActions.test.js b/src/okapiActions.test.js index 9ac82f56d..2376aed7e 100644 --- a/src/okapiActions.test.js +++ b/src/okapiActions.test.js @@ -1,23 +1,8 @@ import { - setIsAuthenticated, setLoginData, updateCurrentUser, } from './okapiActions'; -describe('setIsAuthenticated', () => { - it('handles truthy values', () => { - expect(setIsAuthenticated('truthy').isAuthenticated).toBe(true); - expect(setIsAuthenticated(1).isAuthenticated).toBe(true); - expect(setIsAuthenticated(true).isAuthenticated).toBe(true); - }); - - it('handles falsey values', () => { - expect(setIsAuthenticated('').isAuthenticated).toBe(false); - expect(setIsAuthenticated(0).isAuthenticated).toBe(false); - expect(setIsAuthenticated(false).isAuthenticated).toBe(false); - }); -}); - describe('setLoginData', () => { it('receives given data in "loginData"', () => { const av = { monkey: 'bagel' }; diff --git a/src/okapiReducer.js b/src/okapiReducer.js index a5f581192..aaa34563f 100644 --- a/src/okapiReducer.js +++ b/src/okapiReducer.js @@ -1,9 +1,11 @@ export default function okapiReducer(state = {}, action) { switch (action.type) { + case 'SET_OKAPI_TOKEN': + return Object.assign({}, state, { token: action.token }); + case 'CLEAR_OKAPI_TOKEN': + return Object.assign({}, state, { token: null }); case 'SET_CURRENT_USER': return Object.assign({}, state, { currentUser: action.currentUser }); - case 'SET_IS_AUTHENTICATED': - return Object.assign({}, state, { isAuthenticated: action.isAuthenticated }); case 'SET_LOCALE': return Object.assign({}, state, { locale: action.locale }); case 'SET_TIMEZONE': @@ -20,15 +22,13 @@ export default function okapiReducer(state = {}, action) { return Object.assign({}, state, { currentPerms: action.currentPerms }); case 'SET_LOGIN_DATA': return Object.assign({}, state, { loginData: action.loginData }); - case 'SET_TOKEN_EXPIRATION': - return Object.assign({}, state, { loginData: { ...state.loginData, tokenExpiration: action.tokenExpiration } }); case 'CLEAR_CURRENT_USER': return Object.assign({}, state, { currentUser: {}, currentPerms: {} }); case 'SET_SESSION_DATA': { - const { isAuthenticated, perms, tenant, user } = action.session; + const { perms, user, token, tenant } = action.session; const sessionTenant = tenant || state.tenant; - return { ...state, currentUser: user, currentPerms: perms, isAuthenticated, tenant: sessionTenant }; + return { ...state, currentUser: user, currentPerms: perms, token, tenant: sessionTenant }; } case 'SET_AUTH_FAILURE': return Object.assign({}, state, { authFailure: action.message }); diff --git a/src/okapiReducer.test.js b/src/okapiReducer.test.js index de9cd2827..fc67ace6e 100644 --- a/src/okapiReducer.test.js +++ b/src/okapiReducer.test.js @@ -1,12 +1,6 @@ import okapiReducer from './okapiReducer'; describe('okapiReducer', () => { - it('SET_IS_AUTHENTICATED', () => { - const isAuthenticated = true; - const o = okapiReducer({}, { type: 'SET_IS_AUTHENTICATED', isAuthenticated: true }); - expect(o).toMatchObject({ isAuthenticated }); - }); - it('SET_LOGIN_DATA', () => { const loginData = 'loginData'; const o = okapiReducer({}, { type: 'SET_LOGIN_DATA', loginData }); @@ -24,6 +18,7 @@ describe('okapiReducer', () => { const initialState = { perms: [], user: {}, + token: 'qwerty', tenant: 'central', }; const session = { @@ -34,6 +29,7 @@ describe('okapiReducer', () => { username: 'admin', } }, + token: 'ytrewq', tenant: 'institutional', }; const o = okapiReducer(initialState, { type: 'SET_SESSION_DATA', session }); diff --git a/src/queries/useConfigurations.test.js b/src/queries/useConfigurations.test.js index a40725cff..83baeef4b 100644 --- a/src/queries/useConfigurations.test.js +++ b/src/queries/useConfigurations.test.js @@ -11,20 +11,6 @@ import useOkapiKy from '../useOkapiKy'; jest.mock('../useOkapiKy'); jest.mock('../StripesContext'); -// reassign console.log to keep things quiet -const consoleInterruptor = {}; -beforeAll(() => { - consoleInterruptor.log = global.console.log; - consoleInterruptor.error = global.console.error; - console.log = () => { }; - console.error = () => { }; -}); - -afterAll(() => { - global.console.log = consoleInterruptor.log; - global.console.error = consoleInterruptor.error; -}); - // set query retries to false. otherwise, react-query will thoughtfully // (but unhelpfully, in the context of testing) retry a failed query // several times causing the test to timeout when what we really want diff --git a/src/queries/useOkapiEnv.test.js b/src/queries/useOkapiEnv.test.js index 28efc91cf..3101a000c 100644 --- a/src/queries/useOkapiEnv.test.js +++ b/src/queries/useOkapiEnv.test.js @@ -11,20 +11,6 @@ import useOkapiKy from '../useOkapiKy'; jest.mock('../useOkapiKy'); jest.mock('../StripesContext'); -// reassign console.log to keep things quiet -const consoleInterruptor = {}; -beforeAll(() => { - consoleInterruptor.log = global.console.log; - consoleInterruptor.error = global.console.error; - console.log = () => { }; - console.error = () => { }; -}); - -afterAll(() => { - global.console.log = consoleInterruptor.log; - global.console.error = consoleInterruptor.error; -}); - // set query retries to false. otherwise, react-query will thoughtfully // (but unhelpfully, in the context of testing) retry a failed query // several times causing the test to timeout when what we really want diff --git a/src/service-worker.test.js b/src/service-worker.test.js deleted file mode 100644 index 79dd26e5b..000000000 --- a/src/service-worker.test.js +++ /dev/null @@ -1,569 +0,0 @@ -import { - handleTokenExpiration, - isLogoutRequest, - isOkapiRequest, - isPermissibleRequest, - isValidAT, - isValidRT, - messageToClient, - passThrough, - passThroughLogout, - rtr, - TTL_WINDOW, -} from './service-worker'; - -// reassign console.log to keep things quiet -const consoleInterruptor = {}; -beforeAll(() => { - consoleInterruptor.log = global.console.log; - consoleInterruptor.error = global.console.error; - console.log = () => { }; - console.error = () => { }; -}); - -afterAll(() => { - global.console.log = consoleInterruptor.log; - global.console.error = consoleInterruptor.error; -}); - -describe('isValidAT', () => { - it('returns true for valid ATs', () => { - expect(isValidAT({ atExpires: Date.now() + 1000 })).toBe(true); - }); - - it('returns false for expired ATs', () => { - expect(isValidAT({ atExpires: Date.now() - 1000 })).toBe(false); - }); - - it('returns false when AT info is missing', () => { - expect(isValidAT({ monkey: 'bagel' })).toBe(false); - }); -}); - -describe('isValidRT', () => { - it('returns true for valid RTs', () => { - expect(isValidRT({ rtExpires: Date.now() + 1000 })).toBe(true); - }); - - it('returns false for expired RTs', () => { - expect(isValidRT({ rtExpires: Date.now() - 1000 })).toBe(false); - }); - - it('returns false when RT info is missing', () => { - expect(isValidRT({ monkey: 'bagel' })).toBe(false); - }); -}); - -describe('messageToClient', () => { - let self = null; - const client = { - postMessage: jest.fn(), - }; - - describe('when clients are absent, ignores events', () => { - beforeEach(() => { - ({ self } = window); - delete window.self; - - window.self = { - clients: { - get: jest.fn().mockReturnValue(Promise.resolve(undefined)), - }, - }; - }); - - afterEach(() => { - window.self = self; - }); - - it('event.clientId is absent', async () => { - messageToClient({}); - expect(window.self.clients.get).not.toHaveBeenCalled(); - }); - - it('self.clients.get(event.clientId) is empty', async () => { - const event = { clientId: 'monkey' }; - messageToClient(event, 'message'); - expect(window.self.clients.get).toHaveBeenCalledWith(event.clientId); - expect(client.postMessage).not.toHaveBeenCalled(); - }); - }); - - describe('when clients are present, posts a message', () => { - beforeEach(() => { - ({ self } = window); - delete window.self; - - window.self = { - clients: { - get: jest.fn().mockReturnValue(Promise.resolve(client)), - }, - }; - }); - - afterEach(() => { - window.self = self; - }); - - it('posts a message', async () => { - const event = { clientId: 'monkey' }; - const message = { thunder: 'chicken' }; - - await messageToClient(event, message); - expect(window.self.clients.get).toHaveBeenCalledWith(event.clientId); - expect(client.postMessage).toBeCalledWith({ ...message, source: '@folio/stripes-core' }); - }); - }); -}); - -describe('isPermissibleRequest', () => { - describe('when AT is valid', () => { - it('when AT is valid, accepts any endpoint', () => { - const req = { url: 'monkey' }; - const te = { atExpires: (Date.now() / TTL_WINDOW) + 1000, rtExpires: (Date.now() / TTL_WINDOW) + 1000 }; - expect(isPermissibleRequest(req, te, '')).toBe(true); - }); - }); - - describe('when AT is invalid or missing', () => { - describe('accepts known endpoints that do not require authorization', () => { - it('/bl-users/forgotten/password', () => { - const req = { url: '/bl-users/forgotten/password' }; - const te = {}; - - expect(isPermissibleRequest(req, te, '')).toBe(true); - }); - - it('/bl-users/forgotten/username', () => { - const req = { url: '/bl-users/forgotten/username' }; - const te = {}; - - expect(isPermissibleRequest(req, te, '')).toBe(true); - }); - - it('/bl-users/login-with-expiry', () => { - const req = { url: '/bl-users/login-with-expiry' }; - const te = {}; - - expect(isPermissibleRequest(req, te, '')).toBe(true); - }); - - it('/bl-users/password-reset', () => { - const req = { url: '/bl-users/password-reset' }; - const te = {}; - - expect(isPermissibleRequest(req, te, '')).toBe(true); - }); - - it('/saml/check', () => { - const req = { url: '/saml/check' }; - const te = {}; - - expect(isPermissibleRequest(req, te, '')).toBe(true); - }); - }); - - it('rejects unknown endpoints', () => { - const req = { url: '/monkey/bagel/is/not/known/to/stripes/at/least/i/hope/not' }; - const te = {}; - - expect(isPermissibleRequest(req, te, '')).toBe(false); - }); - }); -}); - -describe('isLogoutRequest', () => { - describe('accepts logout endpoints', () => { - it('/authn/logout', () => { - const req = { url: '/authn/logout' }; - - expect(isLogoutRequest(req, '')).toBe(true); - }); - }); - - it('rejects unknown endpoints', () => { - const req = { url: '/monkey/bagel/is/not/known/to/stripes/at/least/i/hope/not' }; - const te = {}; - - expect(isLogoutRequest(req, te, '')).toBe(false); - }); -}); - -describe('isOkapiRequest', () => { - it('accepts requests whose origin matches okapi\'s', () => { - const oUrl = 'https://domain.edu'; - const req = { url: `${oUrl}/some/endpoint` }; - expect(isOkapiRequest(req, oUrl)).toBe(true); - }); - - it('rejects requests whose origin does not match okapi\'s', () => { - const req = { url: 'https://foo.edu/some/endpoint' }; - expect(isOkapiRequest(req, 'https://bar.edu')).toBe(false); - }); -}); - -describe('passThroughLogout', () => { - it('resolves on success', async () => { - const val = { monkey: 'bagel' }; - global.fetch = jest.fn(() => ( - Promise.resolve({ - json: () => Promise.resolve(val), - }) - )); - const event = { request: 'monkey' }; - const res = await passThroughLogout(event); - expect(await res.json()).toMatchObject(val); - }); - - it('rejects on failure', async () => { - window.Response = jest.fn(); - const val = {}; - global.fetch = jest.fn(() => Promise.reject(Promise.resolve(new Response(JSON.stringify({}))))); - - const event = { request: 'monkey' }; - try { - await passThroughLogout(event); - } catch (e) { - expect(e).toMatchObject(val); - } - }); -}); - -describe('passThrough', () => { - describe('non-okapi requests break on through, break on through, break on through to the other side', () => { - it('successful requests receive a response', async () => { - const req = { - url: 'https://barbie-is-the-greatest-action-movie-of-all-time.fight.me' - }; - const event = { - request: { - clone: () => req, - } - }; - const tokenExpiration = {}; - const oUrl = 'https://okapi.edu'; - - const response = 'kenough'; - global.fetch = jest.fn(() => Promise.resolve(response)); - - const res = await passThrough(event, tokenExpiration, oUrl); - expect(res).toBe(response); - }); - - it('failed requests receive a rejection', async () => { - const req = { - url: 'https://barbie-is-the-greatest-action-movie-of-all-time.fight.me' - }; - const event = { - request: { - clone: () => req, - } - }; - const tokenExpiration = {}; - const oUrl = 'https://okapi.edu'; - - const error = 'not kenough'; - global.fetch = jest.fn(() => Promise.reject(error)); - - try { - await passThrough(event, tokenExpiration, oUrl); - } catch (e) { - expect(e).toEqual(error); - } - }); - }); - - describe('okapi requests are subject to RTR', () => { - it('requests to logout succeed', async () => { - const oUrl = 'https://trinity.edu'; - const req = { url: `${oUrl}/authn/logout` }; - const event = { - request: { - clone: () => req, - } - }; - const tokenExpiration = {}; - - const response = 'oppenheimer'; - global.fetch = jest.fn(() => Promise.resolve(response)); - - const res = await passThrough(event, tokenExpiration, oUrl); - expect(res).toEqual(response); - }); - - // request was valid, response is success; we should receive response - it('requests with valid ATs succeed with success response', async () => { - const oUrl = 'https://trinity.edu'; - const req = { url: `${oUrl}/manhattan` }; - const event = { - request: { - clone: () => req, - } - }; - const tokenExpiration = { atExpires: (Date.now() / TTL_WINDOW) + 10000 }; - - const response = { ok: true }; - global.fetch = jest.fn(() => Promise.resolve(response)); - - const res = await passThrough(event, tokenExpiration, oUrl); - expect(res).toEqual(response); - }); - - // request was valid, response is error; we should receive response - it('requests with valid ATs succeed with error response', async () => { - const oUrl = 'https://trinity.edu'; - const req = { url: `${oUrl}/manhattan` }; - const event = { - request: { - clone: () => req, - } - }; - const tokenExpiration = { atExpires: (Date.now() / TTL_WINDOW) + 10000 }; - - const response = { - ok: false, - status: 403, - headers: { 'content-type': 'text/plain' }, - clone: () => ({ - text: () => Promise.resolve('Access for user \'barbie\' (c0ffeeee-dead-beef-dead-coffeecoffee) requires permission: pink.is.the.new.black') - }), - }; - global.fetch = jest.fn(() => Promise.resolve(response)); - - const res = await passThrough(event, tokenExpiration, oUrl); - expect(res).toEqual(response); - }); - - it('requests with false-valid AT data succeed via RTR', async () => { - const oUrl = 'https://trinity.edu'; - const req = { url: `${oUrl}/manhattan` }; - const event = { - request: { - clone: () => req, - } - }; - const tokenExpiration = { - atExpires: (Date.now() / TTL_WINDOW) + 1000, // at says it's valid, but ok == false - rtExpires: (Date.now() / TTL_WINDOW) + 1000 - }; - - const response = 'los alamos'; - global.fetch = jest.fn() - .mockReturnValueOnce(Promise.resolve({ - status: 403, - headers: { 'content-type': 'text/plain' }, - clone: () => ({ - text: () => Promise.resolve('Token missing, access requires permission:'), - }), - })) - .mockReturnValueOnce(Promise.resolve({ - ok: true, - json: () => Promise.resolve({ - accessTokenExpiration: Date.now(), - refreshTokenExpiration: Date.now(), - }) - })) - .mockReturnValueOnce(Promise.resolve(response)); - const res = await passThrough(event, tokenExpiration, oUrl); - expect(res).toEqual(response); - }); - - it('requests with valid RTs succeed', async () => { - const oUrl = 'https://trinity.edu'; - const req = { url: `${oUrl}/manhattan` }; - const event = { - request: { - clone: () => req, - } - }; - const tokenExpiration = { - atExpires: Date.now() - 1000, - rtExpires: (Date.now() / TTL_WINDOW) + 1000 - }; - - const response = 'los alamos'; - - global.fetch = jest.fn() - .mockReturnValueOnce(Promise.resolve({ - ok: true, - json: () => Promise.resolve({ - accessTokenExpiration: Date.now(), - refreshTokenExpiration: Date.now(), - }) - })) - .mockReturnValueOnce(Promise.resolve(response)); - const res = await passThrough(event, tokenExpiration, oUrl); - expect(res).toEqual(response); - }); - - it('requests with false-valid RTs fail softly', async () => { - const oUrl = 'https://trinity.edu'; - const req = { url: `${oUrl}/manhattan` }; - const event = { - request: { - clone: () => req, - } - }; - const tokenExpiration = { - atExpires: Date.now() - 1000, - rtExpires: Date.now() + 1000 // rt says it's valid but ok == false - }; - - const error = {}; - window.Response = jest.fn(); - - global.fetch = jest.fn() - .mockReturnValueOnce(Promise.resolve({ - ok: false, - json: () => Promise.resolve('RTR response failure') - })) - .mockReturnValueOnce(Promise.resolve(error)); - - try { - await passThrough(event, tokenExpiration, oUrl); - } catch (e) { - expect(e).toMatchObject(error); - } - }); - - it('requests with invalid RTs fail softly', async () => { - const oUrl = 'https://trinity.edu'; - const req = { url: `${oUrl}/manhattan` }; - const event = { - request: { - clone: () => req, - } - }; - const tokenExpiration = { - atExpires: Date.now() - 1000, - rtExpires: Date.now() - 1000 - }; - - const error = {}; - window.Response = jest.fn(); - - global.fetch = jest.fn() - .mockReturnValueOnce(Promise.resolve({ - ok: false, - json: () => Promise.reject(new Error('RTR response failure')), - })) - .mockReturnValueOnce(Promise.resolve(error)); - - try { - await passThrough(event, tokenExpiration, oUrl); - } catch (e) { - expect(e).toMatchObject(error); - } - }); - }); -}); - -describe('rtr', () => { - it('on error with JSON, returns it', async () => { - const oUrl = 'https://trinity.edu'; - const req = { url: `${oUrl}/manhattan` }; - const event = { - request: { - clone: () => req, - } - }; - - const error = { message: 'los', code: 'alamos' }; - window.Response = jest.fn(); - - global.fetch = jest.fn() - .mockReturnValueOnce(Promise.resolve({ - ok: false, - json: () => Promise.resolve({ errors: [error] }) - })); - - try { - await rtr(event); - } catch (e) { - expect(e.message).toMatch(error.message); - expect(e.message).toMatch(error.code); - } - }); - - it('on unknown error, throws a generic error', async () => { - const oUrl = 'https://trinity.edu'; - const req = { url: `${oUrl}/manhattan` }; - const event = { - request: { - clone: () => req, - } - }; - - const error = 'RTR response failure'; - window.Response = jest.fn(); - - global.fetch = jest.fn() - .mockReturnValueOnce(Promise.resolve({ - ok: false, - json: () => Promise.resolve(error) - })); - - try { - await rtr(event); - } catch (e) { - expect(e.message).toMatch(error); - } - }); - - it.skip('foo', async () => { - const foo = handleTokenExpiration; - const bar = messageToClient; - - handleTokenExpiration = jest.fn(); - messageToClient = jest.fn(); - - const oUrl = 'https://trinity.edu'; - const req = { url: `${oUrl}/manhattan` }; - const event = { - request: { - clone: () => req, - } - }; - - window.Response = jest.fn(); - - global.fetch = jest.fn() - .mockReturnValueOnce(Promise.resolve({ - ok: true, - json: () => Promise.resolve({ - accessTokenExpiration: Date.now(), - refreshTokenExpiration: Date.now(), - }) - })); - - await rtr(event); - expect(handleTokenExpiration).toHaveBeenCalled(); - expect(messageToClient).toHaveBeenCalled(); - - handleTokenExpiration = foo; - messageToClient = bar; - }); -}); - -describe('handleTokenExpiration', () => { - const testWindow = (token) => { - const now = Date.now(); - const window = 1000; - const data = { - tokenExpiration: { - [token]: now + window, - }, - }; - - const result = handleTokenExpiration(data); - expect(parseFloat(result[token] - now).toPrecision(2)).toEqual(parseFloat(TTL_WINDOW * window).toPrecision(2)); - }; - - it(`shrinks AT's validity window to ${parseFloat(TTL_WINDOW * 100).toPrecision(2)}% of original size`, () => { - testWindow('atExpires'); - }); - - it(`shrinks RT's validity window to ${parseFloat(TTL_WINDOW * 100).toPrecision(2)}% of original size`, () => { - testWindow('rtExpires'); - }); -}); diff --git a/src/serviceWorkerRegistration.js b/src/serviceWorkerRegistration.js deleted file mode 100644 index a2128ba4b..000000000 --- a/src/serviceWorkerRegistration.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * registerSW - * * register SW - * * send SW okapi details via an OKAPI_CONFIG message. - * * send SW log category details via a LOGGER_CONFIG message. - * Note that although normally a page must be reloaded after a service worker - * has been installed in order for the page to be controlled, this one - * immediately claims control. Otherwise, no RTR would occur until after a - * reload. - * - * @param {object} okapiConfig okapi object from stripes.config.js - * @param {object} config config object from stripes.config.js - * @param {object} logger stripes logger - * @return void - */ -export const registerServiceWorker = async (okapiConfig, config, logger) => { - if ('serviceWorker' in navigator) { - try { - let sw = null; - // - // register - // - const registration = await navigator.serviceWorker.register(new URL('./service-worker.js', window.location.origin), { scope: '/' }) - .then(reg => { - return reg.update(); - }); - if (registration.installing) { - sw = registration.installing; - logger.log('rtr', 'Service worker installing'); - } else if (registration.waiting) { - sw = registration.waiting; - logger.log('rtr', 'Service worker installed'); - } else if (registration.active) { - sw = registration.active; - logger.log('rtr', 'Service worker active'); - } - - // - // send SW okapi config details and a logger. - // the corresponding listener is configured in App.js in order for it - // to recieve some additional config values (i.e. the redux store) - // which are necessary for processing failures (so we can clear out - // said store on logout). - // - if (sw) { - logger.log('rtr', 'sending OKAPI_CONFIG'); - sw.postMessage({ source: '@folio/stripes-core', type: 'OKAPI_CONFIG', value: okapiConfig }); - logger.log('rtr', 'sending LOGGER', logger); - sw.postMessage({ source: '@folio/stripes-core', type: 'LOGGER_CONFIG', value: { categories: config.logCategories } }); - } else { - console.error('(rtr) service worker not available'); - } - } catch (error) { - console.error(`(rtr) service worker registration failed with ${error}`); - } - - // talk to me, goose - navigator.serviceWorker.oncontrollerchange = () => { - if (navigator.serviceWorker.controller) { - logger.log('rtr', 'This page is currently controlled by: ', navigator.serviceWorker.controller); - } else { - logger.log('rtr', 'SERVICE WORKER NOT ACTIVE'); - } - }; - } -}; - -export const unregisterServiceWorker = async () => { - console.log('unregister'); - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready - .then((reg) => { - reg.unregister(); - }) - .catch((error) => { - console.error(error.message); - }); - } -}; diff --git a/src/serviceWorkerRegistration.test.js b/src/serviceWorkerRegistration.test.js deleted file mode 100644 index 3a78d4f36..000000000 --- a/src/serviceWorkerRegistration.test.js +++ /dev/null @@ -1,132 +0,0 @@ -import { - registerServiceWorker, - unregisterServiceWorker -} from './serviceWorkerRegistration'; - -describe('registerServiceWorker', () => { - describe('on success', () => { - const stateTest = (state) => { - it(state, async () => { - const sw = { - postMessage: jest.fn(), - }; - - navigator.serviceWorker = { - register: () => Promise.resolve({ - update: () => ({ [state]: sw }) - }), - controller: 'malibu-trinity', - }; - - const l = { - log: jest.fn(), - }; - - const okapiConfig = { 'barbie': 'oppenheimer' }; - const config = { logCategories: 'kenough,trinity' }; - - await registerServiceWorker(okapiConfig, config, l); - - const lConfig = { source: '@folio/stripes-core', type: 'LOGGER_CONFIG', value: { categories: config.logCategories } }; - - expect(sw.postMessage).toHaveBeenCalledWith(lConfig); - expect(typeof navigator.serviceWorker.oncontrollerchange).toBe('function'); - expect(l.log).toHaveBeenCalledTimes(3); - }); - }; - - const states = ['installing', 'waiting', 'active']; - states.forEach((state) => stateTest(state)); - }); - - describe('on failure', () => { - const consoleInterruptor = {}; - beforeAll(() => { - consoleInterruptor.error = global.console.error; - console.error = jest.fn(); - }); - - afterAll(() => { - global.console.error = consoleInterruptor.error; - }); - - it('registration is not in expected state', async () => { - navigator.serviceWorker = { - register: () => Promise.resolve({ - update: () => ({ }) - }), - }; - - const l = { - log: jest.fn(), - }; - - const okapiConfig = { 'barbie': 'oppenheimer' }; - const config = { logCategories: 'kenough,trinity' }; - - await registerServiceWorker(okapiConfig, config, l); - expect(console.error).toHaveBeenCalledWith('(rtr) service worker not available'); - }); - - it('registration throws', async () => { - const error = Error('Trinity Ken has a nice tan. Oh. Wait.'); - navigator.serviceWorker = { - register: () => { - throw error; - } - }; - - const l = { - log: jest.fn(), - }; - - const okapiConfig = { 'barbie': 'oppenheimer' }; - const config = { logCategories: 'kenough,trinity' }; - - await registerServiceWorker(okapiConfig, config, l); - expect(console.error).toHaveBeenCalledWith(`(rtr) service worker registration failed with ${error}`); - }); - }); -}); - -describe('unregisterServiceWorker', () => { - const consoleInterruptor = {}; - beforeEach(() => { - consoleInterruptor.log = global.console.log; - consoleInterruptor.error = global.console.error; - console.log = jest.fn(); - console.error = jest.fn(); - }); - - afterEach(() => { - global.console.log = consoleInterruptor.log; - global.console.error = consoleInterruptor.error; - }); - - it('on success', async () => { - const unregister = jest.fn(); - navigator.serviceWorker = { - ready: Promise.resolve({ - unregister, - }) - }; - - await unregisterServiceWorker(); - expect(unregister).toHaveBeenCalled(); - }); - - it('on failure', async () => { - const error = 'Los Alamos Ken has a nice tan. Oh. Wait.'; - const unregister = jest.fn(); - navigator.serviceWorker = { - ready: Promise.reject(new Error(error)) - }; - - await unregisterServiceWorker(); - expect(unregister).not.toHaveBeenCalled(); - - // logging will show that console.error _is_ called, - // yet jest always says there are 0 calls here. wha...? - // expect(console.error).toHaveBeenCalled(); - }); -}); diff --git a/src/useOkapiKy.js b/src/useOkapiKy.js index 921530cbb..22fdef09e 100644 --- a/src/useOkapiKy.js +++ b/src/useOkapiKy.js @@ -2,20 +2,19 @@ import ky from 'ky'; import { useStripes } from './StripesContext'; export default ({ tenant } = {}) => { - const { locale = 'en', timeout = 30000, tenant: currentTenant, url } = useStripes().okapi; + const { locale = 'en', timeout = 30000, tenant: currentTenant, token, url } = useStripes().okapi; return ky.create({ - credentials: 'include', + prefixUrl: url, hooks: { beforeRequest: [ request => { request.headers.set('Accept-Language', locale); request.headers.set('X-Okapi-Tenant', tenant ?? currentTenant); + request.headers.set('X-Okapi-Token', token); } ] }, - mode: 'cors', - prefixUrl: url, retry: 0, timeout, }); diff --git a/src/useOkapiKy.test.js b/src/useOkapiKy.test.js index 5959efdc8..1a58e3208 100644 --- a/src/useOkapiKy.test.js +++ b/src/useOkapiKy.test.js @@ -15,6 +15,7 @@ describe('useOkapiKy', () => { locale: 'klingon', tenant: 'tenant', timeout: 271828, + token: 'token', url: 'https://whatever.com' }; @@ -35,6 +36,7 @@ describe('useOkapiKy', () => { expect(r.headers.set).toHaveBeenCalledWith('Accept-Language', okapi.locale); expect(r.headers.set).toHaveBeenCalledWith('X-Okapi-Tenant', okapi.tenant); + expect(r.headers.set).toHaveBeenCalledWith('X-Okapi-Token', okapi.token); }); it('provides default values if stripes lacks them', async () => { @@ -61,6 +63,7 @@ describe('useOkapiKy', () => { const okapi = { tenant: 'tenant', timeout: 271828, + token: 'token', url: 'https://whatever.com' }; diff --git a/src/withOkapiKy.js b/src/withOkapiKy.js index bd692c916..522ab6056 100644 --- a/src/withOkapiKy.js +++ b/src/withOkapiKy.js @@ -9,6 +9,7 @@ const withOkapiKy = (WrappedComponent) => { stripes: PropTypes.shape({ okapi: PropTypes.shape({ tenant: PropTypes.string.isRequired, + token: PropTypes.string.isRequired, url: PropTypes.string.isRequired, }).isRequired, }).isRequired, @@ -16,13 +17,14 @@ const withOkapiKy = (WrappedComponent) => { constructor(props) { super(); - const { tenant, url } = props.stripes.okapi; + const { tenant, token, url } = props.stripes.okapi; this.okapiKy = ky.create({ prefixUrl: url, hooks: { beforeRequest: [ request => { request.headers.set('X-Okapi-Tenant', tenant); + request.headers.set('X-Okapi-Token', token); } ] } diff --git a/test/bigtest/helpers/setup-application.js b/test/bigtest/helpers/setup-application.js index d2dd67a4e..3f2121b9d 100644 --- a/test/bigtest/helpers/setup-application.js +++ b/test/bigtest/helpers/setup-application.js @@ -41,6 +41,7 @@ export default function setupApplication({ // when auth is disabled, add a fake user to the store if (disableAuth) { initialState.okapi = { + token: 'test', currentUser: assign({ id: 'test', username: 'testuser', @@ -50,8 +51,7 @@ export default function setupApplication({ addresses: [], servicePoints: [] }, currentUser), - currentPerms: permissions, - isAuthenticated: true, + currentPerms: permissions }; } else { initialState.okapi = { @@ -74,14 +74,9 @@ export default function setupApplication({ if (userLoggedIn) { localforage.setItem('okapiSess', { - isAuthenticated: true, + token: initialState.okapi.token, user: initialState.okapi.currentUser, perms: initialState.okapi.currentPerms, - tenant: 'tenant', - tokenExpiration: { - atExpires: Date.now() + (10 * 60 * 1000), - rtExpires: Date.now() + (10 * 60 * 1000), - }, }); } diff --git a/test/bigtest/network/config.js b/test/bigtest/network/config.js index 5229d629d..82e58f915 100644 --- a/test/bigtest/network/config.js +++ b/test/bigtest/network/config.js @@ -29,13 +29,6 @@ export default function configure() { launchDescriptor : {} }]); - this.get('/service-worker.js', { - monkey: 'bagel' - }); - this.get('/_/env', { - monkey: 'bagel' - }); - this.get('/saml/check', { ssoEnabled: false }); @@ -50,10 +43,11 @@ export default function configure() { }); this.post('/bl-users/password-reset/reset', {}, 401); - this.post('/authn/logout', {}, 204); - this.post('/bl-users/login-with-expiry', () => { - return new Response(201, {}, { + this.post('/bl-users/login', () => { + return new Response(201, { + 'X-Okapi-Token': `myOkapiToken:${Date.now()}` + }, { user: { id: 'test', username: 'testuser', diff --git a/test/bigtest/network/scenarios/fifthAttemptToLogin.js b/test/bigtest/network/scenarios/fifthAttemptToLogin.js index 32e72aa49..def93a69f 100644 --- a/test/bigtest/network/scenarios/fifthAttemptToLogin.js +++ b/test/bigtest/network/scenarios/fifthAttemptToLogin.js @@ -1,5 +1,5 @@ export default (server) => { - server.post('bl-users/login-with-expiry', { + server.post('bl-users/login', { errorMessage: JSON.stringify( { errors: [ { diff --git a/test/bigtest/network/scenarios/invalidResponseBody.js b/test/bigtest/network/scenarios/invalidResponseBody.js index 65908f776..6f821cf84 100644 --- a/test/bigtest/network/scenarios/invalidResponseBody.js +++ b/test/bigtest/network/scenarios/invalidResponseBody.js @@ -1,5 +1,5 @@ export default (server) => { - server.post('bl-users/login-with-expiry', { + server.post('bl-users/login', { errorMessage: JSON.stringify(['test']) }, 422); }; diff --git a/test/bigtest/network/scenarios/lockedAccount.js b/test/bigtest/network/scenarios/lockedAccount.js index 7d73b1257..498b91c5d 100644 --- a/test/bigtest/network/scenarios/lockedAccount.js +++ b/test/bigtest/network/scenarios/lockedAccount.js @@ -1,5 +1,5 @@ export default (server) => { - server.post('bl-users/login-with-expiry', { + server.post('bl-users/login', { errorMessage: JSON.stringify( { errors: [ { diff --git a/test/bigtest/network/scenarios/multipleErrors.js b/test/bigtest/network/scenarios/multipleErrors.js index a0a512553..b70f89628 100644 --- a/test/bigtest/network/scenarios/multipleErrors.js +++ b/test/bigtest/network/scenarios/multipleErrors.js @@ -1,5 +1,5 @@ export default (server) => { - server.post('bl-users/login-with-expiry', { + server.post('bl-users/login', { errorMessage: JSON.stringify( { errors: [ { diff --git a/test/bigtest/network/scenarios/serverError.js b/test/bigtest/network/scenarios/serverError.js index 43160f128..f9902294d 100644 --- a/test/bigtest/network/scenarios/serverError.js +++ b/test/bigtest/network/scenarios/serverError.js @@ -1,3 +1,3 @@ export default (server) => { - server.post('bl-users/login-with-expiry', {}, 500); + server.post('bl-users/login', {}, 500); }; diff --git a/test/bigtest/network/scenarios/thirdAttemptToLogin.js b/test/bigtest/network/scenarios/thirdAttemptToLogin.js index 8cd063303..3d005ce0a 100644 --- a/test/bigtest/network/scenarios/thirdAttemptToLogin.js +++ b/test/bigtest/network/scenarios/thirdAttemptToLogin.js @@ -1,5 +1,5 @@ export default (server) => { - server.post('bl-users/login-with-expiry', { + server.post('bl-users/login', { errorMessage: JSON.stringify( { errors: [ { diff --git a/test/bigtest/network/scenarios/wrongPassword.js b/test/bigtest/network/scenarios/wrongPassword.js index 02282ba49..c0529673b 100644 --- a/test/bigtest/network/scenarios/wrongPassword.js +++ b/test/bigtest/network/scenarios/wrongPassword.js @@ -1,5 +1,5 @@ export default (server) => { - server.post('bl-users/login-with-expiry', { + server.post('bl-users/login', { errorMessage: JSON.stringify( { errors: [ { diff --git a/test/bigtest/network/scenarios/wrongUsername.js b/test/bigtest/network/scenarios/wrongUsername.js index 8e3015bee..993ee8253 100644 --- a/test/bigtest/network/scenarios/wrongUsername.js +++ b/test/bigtest/network/scenarios/wrongUsername.js @@ -1,5 +1,5 @@ export default (server) => { - server.post('bl-users/login-with-expiry', { + server.post('bl-users/login', { errorMessage: JSON.stringify( { errors: [ { diff --git a/test/bigtest/tests/login-test.js b/test/bigtest/tests/login-test.js index b0f218612..126c6d829 100644 --- a/test/bigtest/tests/login-test.js +++ b/test/bigtest/tests/login-test.js @@ -340,14 +340,6 @@ describe('Login', () => { }); }); - // the login workflow invokes navigator.serviceWorker.ready, - // a browser property that returns a Promise that waits until - // the service worker resolves, but in Karma-land we don't - // configure the service-worker. hence, this will time out, - // every time. - // - // we'll need to cover these components with jest/RTL tests - // eventually. describe('with valid credentials', () => { beforeEach(async () => { const { username, password, submit } = login; diff --git a/test/bigtest/tests/session-timeout-test.js b/test/bigtest/tests/session-timeout-test.js index f6e8046e4..702f2a1b5 100644 --- a/test/bigtest/tests/session-timeout-test.js +++ b/test/bigtest/tests/session-timeout-test.js @@ -5,7 +5,7 @@ import setupApplication from '../helpers/setup-core-application'; import LoginInteractor from '../interactors/login'; import translations from '../../../translations/stripes-core/en'; -describe.skip('Session timeout test', () => { +describe('Session timeout test', () => { const login = new LoginInteractor('form[class^="form--"]'); setupApplication({ diff --git a/test/jest/__mock__/index.js b/test/jest/__mock__/index.js index 1b511c30a..15fc172e2 100644 --- a/test/jest/__mock__/index.js +++ b/test/jest/__mock__/index.js @@ -1,4 +1,3 @@ -import './microStripesConfig.mock'; import './stripesConfig.mock'; import './intl.mock'; import './stripesIcon.mock'; diff --git a/test/jest/__mock__/microStripesConfig.mock.js b/test/jest/__mock__/microStripesConfig.mock.js deleted file mode 100644 index ace72c7b7..000000000 --- a/test/jest/__mock__/microStripesConfig.mock.js +++ /dev/null @@ -1,5 +0,0 @@ -jest.mock('micro-stripes-config', () => ({ - okapiUrl: 'https://los-alamos-barbie-has-a-nice-tan.oh-wa.it', - okapiTenant: 'kenough', -}), -{ virtual: true });