From 6c6a85e34122656758976e625da06fbfc0b8dd69 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Tue, 7 Nov 2023 19:54:34 -0500 Subject: [PATCH] STCOR-759 read okapi config from micro-stripes-config (#1366) A service worker's global state is reset after each sleep/wake cycle, meaning the `okapiUrl` and `okapiTenant` values so lovingly sent to the service worker during registration are likely to be promptly forgotten as soon as the browser is idle for a few minutes and decides it would be good to clean up inactive processes. Here, those values are directly imported from a virtual module created at build-time by stripes-webpack, which forwards the values from the stripes-config object (most likely, the `stripes.config.js` file) and allows them to be compiled directly into the generated `service-worker.js` asset. An alternative approach would be to pass in those values as URL parameters when the service worker is registered. h/t @mkuklis and @johnc-80 who did the heavy lifting here, both in thinking through the potential solutions and actually figuring out how to implement this in our highly customized build process. * Requires folio-org/stripes-webpack/pull/132 Refs STCOR-759 --------- Co-authored-by: Michal Kuklis --- index.js | 3 ++ src/loginServices.js | 12 +++-- src/loginServices.test.js | 2 + src/service-worker.js | 19 ++++---- src/service-worker.test.js | 47 +++++++++++++++++-- src/serviceWorkerRegistration.js | 11 ++--- src/serviceWorkerRegistration.test.js | 6 +-- test/bigtest/tests/login-test.js | 2 +- test/jest/__mock__/index.js | 1 + test/jest/__mock__/microStripesConfig.mock.js | 5 ++ 10 files changed, 76 insertions(+), 32 deletions(-) create mode 100644 test/jest/__mock__/microStripesConfig.mock.js diff --git a/index.js b/index.js index b0fc43f26..877ba3222 100644 --- a/index.js +++ b/index.js @@ -45,4 +45,7 @@ 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/src/loginServices.js b/src/loginServices.js index 7b32350b3..b99107f93 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -179,7 +179,7 @@ function dispatchLocale(url, store, tenant) { .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) { @@ -258,7 +258,7 @@ export function getPlugins(okapiUrl, store, tenant) { .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, }), {}); @@ -291,7 +291,7 @@ export function getBindings(okapiUrl, store, tenant) { } else { response.json().then((json) => { const configs = json.configs; - if (configs.length > 0) { + if (Array.isArray(configs) && configs.length > 0) { const string = configs[0].value; try { const tmp = JSON.parse(string); @@ -387,10 +387,12 @@ export async function logout(okapiUrl, store) { /** * postTokenExpiration * send SW a TOKEN_EXPIRATION message + * @param {object} tokenExpiration shaped like { atExpires, rtExpires} where both are millisecond timestamps + * * @returns {Promise} */ -const postTokenExpiration = (tokenExpiration) => { - if ('serviceWorker' in navigator) { +export const postTokenExpiration = (tokenExpiration) => { + if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { return navigator.serviceWorker.ready .then((reg) => { const sw = reg.active; diff --git a/src/loginServices.test.js b/src/loginServices.test.js index 6d117df33..d46935ac9 100644 --- a/src/loginServices.test.js +++ b/src/loginServices.test.js @@ -96,6 +96,7 @@ describe('createOkapiSession', () => { const postMessage = jest.fn(); navigator.serviceWorker = { + controller: true, ready: Promise.resolve({ active: { postMessage, @@ -309,6 +310,7 @@ describe('validateUser', () => { const postMessage = jest.fn(); navigator.serviceWorker = { + controller: true, ready: Promise.resolve({ active: { postMessage, diff --git a/src/service-worker.js b/src/service-worker.js index 6748fa9e1..88ab7a6ca 100644 --- a/src/service-worker.js +++ b/src/service-worker.js @@ -4,6 +4,13 @@ /** * TLDR: perform refresh-token-rotation for Okapi-bound requests. * + * Critical reading: + * @see https://web.dev/articles/service-worker-mindset#watch_out_for_global_state + * @see https://web.dev/articles/service-worker-lifecycle#shift-reload + * + * The (rather opaque) specification: + * @see https://www.w3.org/TR/service-workers/ + * * The gory details: * This service worker acts as a proxy betwen the browser and the network, * intercepting all fetch requests. Those not bound for Okapi are simply @@ -43,15 +50,11 @@ * */ +import { okapiUrl, okapiTenant } from 'micro-stripes-config'; /** { atExpires, rtExpires } both are JS millisecond timestamps */ let tokenExpiration = null; -/** string FQDN including protocol, e.g. https://some-okapi.somewhere.org */ -let okapiUrl = null; - -let okapiTenant = null; - /** whether to emit console logs */ let shouldLog = false; @@ -455,12 +458,6 @@ self.addEventListener('message', (event) => { if (event.data.source === '@folio/stripes-core') { if (shouldLog) console.info('-- (rtr-sw) reading', event.data); - // OKAPI_CONFIG - if (event.data.type === 'OKAPI_CONFIG') { - okapiUrl = event.data.value.url; - okapiTenant = event.data.value.tenant; - } - // LOGGER_CONFIG if (event.data.type === 'LOGGER_CONFIG') { shouldLog = !!event.data.value.categories?.split(',').some(cat => cat === 'rtr-sw'); diff --git a/src/service-worker.test.js b/src/service-worker.test.js index c2de64d88..bd4d70592 100644 --- a/src/service-worker.test.js +++ b/src/service-worker.test.js @@ -203,7 +203,7 @@ describe('isOkapiRequest', () => { }); describe('passThroughLogout', () => { - it('succeeds', async () => { + it('resolves on success', async () => { const val = { monkey: 'bagel' }; global.fetch = jest.fn(() => ( Promise.resolve({ @@ -215,14 +215,17 @@ describe('passThroughLogout', () => { expect(await res.json()).toMatchObject(val); }); - it('succeeds even when it fails', async () => { + it('rejects on failure', async () => { window.Response = jest.fn(); const val = {}; - global.fetch = jest.fn(() => Promise.reject(Promise.resolve(new Response({})))); + global.fetch = jest.fn(() => Promise.reject(Promise.resolve(new Response(JSON.stringify({}))))); const event = { request: 'monkey' }; - const res = await passThroughLogout(event); - expect(await res).toMatchObject(val); + try { + await passThroughLogout(event); + } catch (e) { + expect(e).toMatchObject(val); + } }); }); @@ -506,6 +509,40 @@ describe('rtr', () => { 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', () => { diff --git a/src/serviceWorkerRegistration.js b/src/serviceWorkerRegistration.js index 6d1faf85e..a2128ba4b 100644 --- a/src/serviceWorkerRegistration.js +++ b/src/serviceWorkerRegistration.js @@ -1,5 +1,3 @@ -/* eslint no-console: 0 */ - /** * registerSW * * register SW @@ -57,11 +55,12 @@ export const registerServiceWorker = async (okapiConfig, config, logger) => { } // talk to me, goose - if (navigator.serviceWorker.controller) { - logger.log('rtr', 'This page is currently controlled by: ', navigator.serviceWorker.controller); - } navigator.serviceWorker.oncontrollerchange = () => { - logger.log('rtr', 'This page is now controlled by: ', navigator.serviceWorker.controller); + if (navigator.serviceWorker.controller) { + logger.log('rtr', 'This page is currently controlled by: ', navigator.serviceWorker.controller); + } else { + logger.log('rtr', 'SERVICE WORKER NOT ACTIVE'); + } }; } }; diff --git a/src/serviceWorkerRegistration.test.js b/src/serviceWorkerRegistration.test.js index 46678bb3e..3a78d4f36 100644 --- a/src/serviceWorkerRegistration.test.js +++ b/src/serviceWorkerRegistration.test.js @@ -27,13 +27,11 @@ describe('registerServiceWorker', () => { await registerServiceWorker(okapiConfig, config, l); - const oConfig = { source: '@folio/stripes-core', type: 'OKAPI_CONFIG', value: okapiConfig }; const lConfig = { source: '@folio/stripes-core', type: 'LOGGER_CONFIG', value: { categories: config.logCategories } }; - expect(sw.postMessage).toHaveBeenNthCalledWith(1, oConfig); - expect(sw.postMessage).toHaveBeenNthCalledWith(2, lConfig); + expect(sw.postMessage).toHaveBeenCalledWith(lConfig); expect(typeof navigator.serviceWorker.oncontrollerchange).toBe('function'); - expect(l.log).toHaveBeenCalledTimes(4); + expect(l.log).toHaveBeenCalledTimes(3); }); }; diff --git a/test/bigtest/tests/login-test.js b/test/bigtest/tests/login-test.js index e5ab939e3..b0f218612 100644 --- a/test/bigtest/tests/login-test.js +++ b/test/bigtest/tests/login-test.js @@ -348,7 +348,7 @@ describe('Login', () => { // // we'll need to cover these components with jest/RTL tests // eventually. - describe.skip('with valid credentials', () => { + describe('with valid credentials', () => { beforeEach(async () => { const { username, password, submit } = login; diff --git a/test/jest/__mock__/index.js b/test/jest/__mock__/index.js index 15fc172e2..1b511c30a 100644 --- a/test/jest/__mock__/index.js +++ b/test/jest/__mock__/index.js @@ -1,3 +1,4 @@ +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 new file mode 100644 index 000000000..ace72c7b7 --- /dev/null +++ b/test/jest/__mock__/microStripesConfig.mock.js @@ -0,0 +1,5 @@ +jest.mock('micro-stripes-config', () => ({ + okapiUrl: 'https://los-alamos-barbie-has-a-nice-tan.oh-wa.it', + okapiTenant: 'kenough', +}), +{ virtual: true });