const makeServiceWorkerEnv = require('service-worker-mock'); global.fetch = require('node-fetch'); jest.mock('node-fetch') describe("service-worker", () => { beforeEach(() => { global.fetch.mockImplementation((url, init) => { return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: url }) ); }); Object.assign(global, makeServiceWorkerEnv()); global.self = new ServiceWorkerGlobalScope() jest.resetModules(); self.LibResilientPlugins = new Array() self.importScripts = jest.fn((url)=>{ console.debug(`importScripts('../${url}')`) try { require('../' + url); } catch(e) {} }) // TODO: pretty ugly, but necessary for some reason... global.LibResilientPluginConstructors = new Map() self.LibResilientPluginConstructors = global.LibResilientPluginConstructors }) test("reality-check: Promise.any() polyfill should work", async () => { self.LibResilientPlugins = false self.LibResilientConfig = { plugins: [], loggedComponents: [ 'service-worker' ] } expect.assertions(4) // we want to make sure to actually test the polyfill Promise.any = undefined expect(typeof Promise.any).toEqual('undefined') require("../service-worker.js"); expect(typeof Promise.any).toEqual('function') expect(await Promise.any([ Promise.resolve('test resolve 1'), Promise.reject('test reject 2') ])).toEqual('test resolve 1') try { await Promise.any([ Promise.reject('test reject 1'), Promise.reject('test reject 2') ]) } catch (e) { expect(e).toEqual([ "test reject 1", "test reject 2" ]) } }) test("basic set-up: LibResilientPlugins", async () => { self.LibResilientPlugins = false self.LibResilientConfig = { plugins: [], loggedComponents: [ 'service-worker' ] } require("../service-worker.js"); expect(self.LibResilientPlugins).toBeInstanceOf(Array) }) test("basic set-up: use default LibResilientConfig values when config.json missing", async () => { self.LibResilientConfig = null global.fetch.mockImplementation((url, init) => { return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "fail" })], {type: "application/json"} ), { status: 404, statusText: "Not Found", headers: { 'ETag': 'TestingETagHeader' }, url: url }) ); }); try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000) expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}]) expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache']) expect(fetch).toHaveBeenCalled(); }) test("basic set-up: use default LibResilientConfig values when config.json not valid JSON", async () => { self.LibResilientConfig = null global.fetch.mockImplementation((url, init) => { return Promise.resolve( new Response( new Blob( ["not a JSON"], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: url }) ); }); try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000) expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}]) expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache']) expect(fetch).toHaveBeenCalled(); }) test("basic set-up: use default LibResilientConfig values when no valid 'plugins' field in config.json", async () => { self.LibResilientConfig = null global.fetch.mockImplementation((url, init) => { return Promise.resolve( new Response( new Blob( [JSON.stringify({loggedComponents: ['service-worker', 'fetch'], plugins: 'not a valid array'})], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: url }) ); }); try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000) expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}]) expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache']) expect(fetch).toHaveBeenCalled(); }) test("basic set-up: use default LibResilientConfig values when no valid 'loggedComponents' field in config.json", async () => { self.LibResilientConfig = null global.fetch.mockImplementation((url, init) => { return Promise.resolve( new Response( new Blob( [JSON.stringify({loggedComponents: 'not a valid array', plugins: [{name: "fetch"}]})], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: url }) ); }); try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000) expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}]) expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache']) expect(fetch).toHaveBeenCalled(); }) test("basic set-up: use default LibResilientConfig values when 'defaultPluginTimeout' field in config.json contains an invalid value", async () => { self.LibResilientConfig = null global.fetch.mockImplementation((url, init) => { return Promise.resolve( new Response( new Blob( [JSON.stringify({loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 'not an integer'})], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: url }) ); }); try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000) expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}]) expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache']) expect(fetch).toHaveBeenCalled(); }) test("basic set-up: use config values from a valid config.json file", async () => { self.LibResilientConfig = null global.fetch.mockImplementation((url, init) => { return Promise.resolve( new Response( new Blob( [JSON.stringify({loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000})], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: url }) ); }); try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(5000) expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "cache"}]) expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'cache']) expect(fetch).toHaveBeenCalled(); }) test("basic set-up: instantiate a complex tree of configured plugins", async () => { self.LibResilientConfig = { plugins: [{ name: 'plugin-1', uses: [{ name: 'plugin-2', uses: [{ name: 'plugin-3' }] },{ name: 'plugin-3' }] },{ name: 'plugin-2', uses: [{ name: 'plugin-3' }] },{ name: 'plugin-3', uses: [{ name: 'plugin-1' },{ name: 'plugin-2', uses: [{ name: 'plugin-1', uses: [{ name: 'plugin-4' }] }] }] },{ name: 'plugin-4' } ] } global.LibResilientPluginConstructors.set('plugin-1', (LRPC, config)=>{ return { name: 'plugin-1', description: 'Plugin Type 1', version: '0.0.1', fetch: (url)=>{return true}, uses: config.uses || [] } }) global.LibResilientPluginConstructors.set('plugin-2', (LRPC, config)=>{ return { name: 'plugin-2', description: 'Plugin Type 2', version: '0.0.1', fetch: (url)=>{return true}, uses: config.uses || [] } }) global.LibResilientPluginConstructors.set('plugin-3', (LRPC, config)=>{ return { name: 'plugin-3', description: 'Plugin Type 3', version: '0.0.1', fetch: (url)=>{return true}, uses: config.uses || [] } }) global.LibResilientPluginConstructors.set('plugin-4', (LRPC, config)=>{ return { name: 'plugin-4', description: 'Plugin Type 4', version: '0.0.1', fetch: (url)=>{return true}, uses: config.uses || [] } }) try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(typeof self.LibResilientConfig).toEqual('object') // basic stuff expect(self.LibResilientPlugins.length).toEqual(4) expect(self.LibResilientPlugins[0].name).toEqual('plugin-1') expect(self.LibResilientPlugins[1].name).toEqual('plugin-2') expect(self.LibResilientPlugins[2].name).toEqual('plugin-3') expect(self.LibResilientPlugins[3].name).toEqual('plugin-4') // first plugin dependencies expect(self.LibResilientPlugins[0].uses.length).toEqual(2) expect(self.LibResilientPlugins[0].uses[0].name).toEqual('plugin-2') expect(self.LibResilientPlugins[0].uses[0].uses.length).toEqual(1) expect(self.LibResilientPlugins[0].uses[0].uses[0].name).toEqual('plugin-3') expect(self.LibResilientPlugins[0].uses[0].uses[0].uses.length).toEqual(0) expect(self.LibResilientPlugins[0].uses[1].name).toEqual('plugin-3') expect(self.LibResilientPlugins[0].uses[1].uses.length).toEqual(0) // second plugin dependencies expect(self.LibResilientPlugins[1].uses.length).toEqual(1) expect(self.LibResilientPlugins[1].uses[0].name).toEqual('plugin-3') expect(self.LibResilientPlugins[1].uses[0].uses.length).toEqual(0) // third plugin dependencies expect(self.LibResilientPlugins[2].uses.length).toEqual(2) expect(self.LibResilientPlugins[2].uses[0].name).toEqual('plugin-1') expect(self.LibResilientPlugins[2].uses[0].uses.length).toEqual(0) expect(self.LibResilientPlugins[2].uses[1].name).toEqual('plugin-2') expect(self.LibResilientPlugins[2].uses[1].uses.length).toEqual(1) expect(self.LibResilientPlugins[2].uses[1].uses[0].name).toEqual('plugin-1') expect(self.LibResilientPlugins[2].uses[1].uses[0].uses.length).toEqual(1) expect(self.LibResilientPlugins[2].uses[1].uses[0].uses[0].name).toEqual('plugin-4') expect(self.LibResilientPlugins[2].uses[1].uses[0].uses[0].uses.length).toEqual(0) // fourth plugin dependencies expect(self.LibResilientPlugins[3].uses.length).toEqual(0) }) test("basic set-up: instantiate configured plugins; explicitly disabled plugins should not be instantiated", async () => { self.LibResilientConfig = { plugins: [{ name: 'plugin-enabled' },{ name: 'plugin-disabled', enabled: false },{ name: 'plugin-enabled', enabled: true }] } global.LibResilientPluginConstructors.set('plugin-enabled', ()=>{ return { name: 'plugin-enabled', description: 'Enabled plugin', version: '0.0.1', fetch: (url)=>{return true} } }) global.LibResilientPluginConstructors.set('plugin-disabled', ()=>{ return { name: 'plugin-disabled', description: 'Disabled plugin', version: '0.0.1', fetch: (url)=>{return true} } }) try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientPlugins.length).toEqual(2) expect(self.LibResilientPlugins[0].name).toEqual('plugin-enabled') expect(self.LibResilientPlugins[1].name).toEqual('plugin-enabled') }) test("basic set-up: instantiate configured plugins; explicitly disabled dependencies should not be instantiated", async () => { self.LibResilientConfig = { plugins: [{ name: 'plugin-disabled', enabled: false, uses: [{ name: 'dependency-enabled' }] },{ name: 'plugin-enabled', uses: [{ name: 'dependency-disabled', enabled: false }] },{ name: 'plugin-enabled', uses: [{ name: 'dependency-enabled', enabled: true }] }], loggedComponents: ['service-worker'] } global.LibResilientPluginConstructors.set('plugin-enabled', (LRPC, config)=>{ return { name: 'plugin-enabled', description: 'Enabled plugin', version: '0.0.1', fetch: (url)=>{return true}, uses: config.uses || [] } }) global.LibResilientPluginConstructors.set('plugin-disabled', (LRPC, config)=>{ return { name: 'plugin-disabled', description: 'Disabled plugin', version: '0.0.1', fetch: (url)=>{return true}, uses: config.uses || [] } }) global.LibResilientPluginConstructors.set('dependency-disabled', (LRPC, config)=>{ return { name: 'dependency-disabled', description: 'Disabled dependency plugin', version: '0.0.1', fetch: (url)=>{return true}, uses: config.uses || [] } }) global.LibResilientPluginConstructors.set('dependency-enabled', (LRPC, config)=>{ return { name: 'dependency-enabled', description: 'Enabled dependency plugin', version: '0.0.1', fetch: (url)=>{return true}, uses: config.uses || [] } }) try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientPlugins.length).toEqual(2) expect(self.LibResilientPlugins[0].name).toEqual('plugin-enabled') expect(self.LibResilientPlugins[0].uses.length).toEqual(0) expect(self.LibResilientPlugins[1].name).toEqual('plugin-enabled') expect(self.LibResilientPlugins[1].uses.length).toEqual(1) expect(self.LibResilientPlugins[1].uses[0].name).toEqual('dependency-enabled') expect(self.LibResilientPlugins[1].uses[0].uses.length).toEqual(0) }) test("basic set-up: a valid config.json file gets cached", async () => { self.LibResilientConfig = null var configData = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000} global.fetch.mockImplementation((url, init) => { return Promise.resolve( new Response( new Blob( [JSON.stringify(configData)], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: url }) ); }); try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(5000) expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "cache"}]) expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'cache']) expect(fetch).toHaveBeenCalled(); expect (await caches.open('v1').then((cache)=>{ return cache.match(self.location.origin + '/config.json') }).then((response)=>{ return response.json() }).then((json)=>{ return json })).toStrictEqual({loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000}) }) test("basic set-up: a cached valid config.json file gets used, no fetch happens", async () => { self.LibResilientConfig = null var configData = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000} var configUrl = '/config.json' var configResponse = new Response( new Blob( [JSON.stringify(configData)], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: configUrl }) await caches .open('v1') .then((cache)=>{ return cache.put(configUrl, configResponse) }) try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(5000) expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "cache"}]) expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'cache']) expect(fetch).not.toHaveBeenCalled(); }) test("basic set-up: a stale cached valid config.json file gets used, no fetch happens, fresh config.json is retrieved using the configured plugins and cached", async () => { self.LibResilientConfig = null var configData = {loggedComponents: ['service-worker', 'cache', 'fetch'], plugins: [{name: "fetch"},{name: "cache"}], defaultPluginTimeout: 5000} var configUrl = '/config.json' var configResponse = new Response( new Blob( [JSON.stringify(configData)], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader', // very stale date 'Date': new Date(0).toUTCString() }, url: configUrl }) await caches .open('v1') .then((cache)=>{ return cache.put(configUrl, configResponse) }) var newConfigData = {loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 2000} let resolveConfigFetch = jest.fn((request, init)=>{ return Promise.resolve( new Response( new Blob( [JSON.stringify(newConfigData)], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader', // very current date 'Date': new Date().toUTCString() }, url: configUrl }) ) }) global.LibResilientPluginConstructors.set('fetch', ()=>{ return { name: 'fetch', description: 'Resolve with config data (pretending to be fetch).', version: '0.0.1', fetch: resolveConfigFetch } }) try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') // verify current config (the one from the pre-cached stale `config.json`) expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(5000) expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}]) expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'cache', 'fetch']) expect(fetch).not.toHaveBeenCalled(); expect(resolveConfigFetch).toHaveBeenCalled(); // verify that the *new* config got cached cdata = await caches .open('v1') .then((cache)=>{ return cache.match(configUrl) }) .then((cresponse)=>{ return cresponse.json() }) expect(cdata).toStrictEqual(newConfigData) }) test("basic set-up: a stale cached valid config.json file gets used, no fetch happens; invalid config.json retrieved using the configured plugins is not cached", async () => { self.LibResilientConfig = null var configData = {loggedComponents: ['service-worker', 'cache', 'resolve-config'], plugins: [{name: "cache"}, {name: "resolve-config"}], defaultPluginTimeout: 5000} var configUrl = '/config.json' var configResponse = new Response( new Blob( [JSON.stringify(configData)], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader', // very stale date 'Date': new Date(0).toUTCString() }, url: configUrl }) await caches .open('v1') .then((cache)=>{ return cache.put(configUrl, configResponse) }) var newConfigData = {loggedComponentsInvalid: ['service-worker', 'resolve-config'], pluginsInvalid: [{name: "resolve-config"}], defaultPluginTimeoutInvalid: 2000} let resolveConfigFetch = jest.fn((request, init)=>{ return Promise.resolve( new Response( new Blob( [JSON.stringify(newConfigData)], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader', // very current date 'Date': new Date().toUTCString() }, url: configUrl }) ) }) global.LibResilientPluginConstructors.set('resolve-config', ()=>{ return { name: 'resolve-config', description: 'Resolve with config data.', version: '0.0.1', fetch: resolveConfigFetch } }) try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') // verify current config (the one from the pre-cached stale `config.json`) expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(5000) expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "cache"}, {name: "resolve-config"}]) expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'cache', 'resolve-config']) expect(fetch).not.toHaveBeenCalled(); expect(resolveConfigFetch).toHaveBeenCalled(); // verify that the *new* config got cached cdata = await caches .open('v1') .then((cache)=>{ return cache.match(configUrl) }) .then((cresponse)=>{ return cresponse.json() }) expect(cdata).toStrictEqual(configData) }) test("basic set-up: a stale cached valid config.json file gets used, no fetch happens; valid config.json (configuring additional plugins) is retrieved using the configured plugins other than fetch, and is not cached", async () => { self.LibResilientConfig = null var configData = {loggedComponents: ['service-worker', 'resolve-config'], plugins: [{name: "resolve-config"}], defaultPluginTimeout: 5000} var configUrl = '/config.json' var configResponse = new Response( new Blob( [JSON.stringify(configData)], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader', // very stale date 'Date': new Date(0).toUTCString() }, url: configUrl }) await caches .open('v1') .then((cache)=>{ return cache.put(configUrl, configResponse) }) var newConfigData = {loggedComponents: ['service-worker', 'resolve-config', 'cache'], plugins: [{name: "resolve-config"}, {name: "cache"}], defaultPluginTimeout: 2000} let resolveConfigFetch = jest.fn((request, init)=>{ return Promise.resolve( new Response( new Blob( [JSON.stringify(newConfigData)], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader', // very current date 'Date': new Date().toUTCString() }, url: configUrl }) ) }) global.LibResilientPluginConstructors.set('resolve-config', ()=>{ return { name: 'resolve-config', description: 'Resolve with config data.', version: '0.0.1', fetch: resolveConfigFetch } }) try { require("../service-worker.js"); } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') // verify current config (the one from the pre-cached stale `config.json`) expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(configData.defaultPluginTimeout) expect(self.LibResilientConfig.plugins).toStrictEqual(configData.plugins) expect(self.LibResilientConfig.loggedComponents).toStrictEqual(configData.loggedComponents) expect(fetch).not.toHaveBeenCalled(); expect(resolveConfigFetch).toHaveBeenCalled(); // verify that the *new* config got cached cdata = await caches .open('v1') .then((cache)=>{ return cache.match(configUrl) }) .then((cresponse)=>{ return cresponse.json() }) expect(cdata).toStrictEqual(configData) }) test("failed fetch by first configured plugin should not affect a successful fetch by a second one", async () => { self.LibResilientConfig = { plugins: [{ name: 'reject-all' },{ name: 'resolve-all' }], loggedComponents: [ 'service-worker' ] } let rejectingFetch = jest.fn((request, init)=>{ return Promise.reject(request); }) let resolvingFetch = jest.fn((request, init)=>{ return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: self.location.origin + '/test.json' }) ) }) global.LibResilientPluginConstructors.set('reject-all', ()=>{ return { name: 'reject-all', description: 'Reject all requests.', version: '0.0.1', fetch: rejectingFetch } }) global.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: resolvingFetch } }) require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') var response = await self.trigger('fetch', new Request('/test.json')) expect(rejectingFetch).toHaveBeenCalled(); expect(resolvingFetch).toHaveBeenCalled(); expect(await response.json()).toEqual({ test: "success" }) }); test("plugins should receive the Request() init data", async () => { self.LibResilientConfig = { plugins: [{ name: 'reject-all' },{ name: 'resolve-all' }], loggedComponents: [ 'service-worker' ] } let rejectingFetch = jest.fn((request, init)=>{ return Promise.reject(request); }) let resolvingFetch = jest.fn((request, init)=>{ return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: self.location.origin + '/test.json' }) ) }) global.LibResilientPluginConstructors.set('reject-all', ()=>{ return { name: 'reject-all', description: 'Reject all requests.', version: '0.0.1', fetch: rejectingFetch } }) global.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: resolvingFetch } }) var initTest = { method: "GET", // TODO: ref. https://gitlab.com/rysiekpl/libresilient/-/issues/23 //headers: new Headers({"x-stub": "STUB"}), //mode: "mode-stub", //credentials: "credentials-stub", cache: "cache-stub", referrer: "referrer-stub", // these are not implemented by service-worker-mock // https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20 redirect: undefined, integrity: undefined, cache: undefined } require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') var response = await self.trigger('fetch', new Request('/test.json', initTest)) expect(rejectingFetch).toHaveBeenCalled(); expect(resolvingFetch).toHaveBeenCalled(); expect(await response.json()).toEqual({ test: "success" }) expect(rejectingFetch).toHaveBeenCalledWith('https://www.test.com/test.json', initTest) expect(resolvingFetch).toHaveBeenCalledWith('https://www.test.com/test.json', initTest) }); test("defaultPluginTimeout should be respected", async () => { self.LibResilientConfig = { defaultPluginTimeout: 100, plugins: [{ name: 'resolve-with-timeout' }], loggedComponents: [ 'service-worker', ] } let rwtCallback = jest.fn() global.LibResilientPluginConstructors.set('resolve-with-timeout', ()=>{ return { name: 'resolve-with-timeout', description: 'Resolve all requests after a timeout.', version: '0.0.1', fetch: (request, init)=>{ return new Promise((resolve, reject)=>{ setTimeout(rwtCallback, 300) }) } } }) require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') var response = self.trigger('fetch', new Request('/test.json')) expect.assertions(2) try { await response } catch(e) { expect(e.toString()).toBe("Error: LibResilient request using resolve-with-timeout timed out after 100ms.") } expect(rwtCallback).not.toHaveBeenCalled() }); test("making an external request should work and not go through the plugins", async () => { global.fetch.mockImplementation((request, init) => { return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, method: 'GET', url: request.url }) ); }); self.LibResilientConfig = { plugins: [{ name: 'reject-all' }], loggedComponents: [ 'service-worker' ] } global.LibResilientPluginConstructors.set('reject-all', ()=>{ return { name: 'reject-all', description: 'Reject all requests.', version: '0.0.1', fetch: (request, init)=>{ return Promise.reject(request); } } }) require("../service-worker.js"); var response = await self.trigger('fetch', new Request('https://example.com/test.json')) expect(await response.json()).toEqual({ test: "success" }) }) test("making a POST request should work and not go through the plugins", async () => { global.fetch.mockImplementation((request, init) => { return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, method: 'POST', url: request.url }) ); }); self.LibResilientConfig = { plugins: [{ name: 'reject-all' }], loggedComponents: [ 'service-worker' ] } global.LibResilientPluginConstructors.set('reject-all', ()=>{ return { name: 'reject-all', description: 'Reject all requests.', version: '0.0.1', fetch: (request, init)=>{ return Promise.reject(request); } } }) require("../service-worker.js"); var response = await self.trigger('fetch', new Request('/test.json', {method: "POST"})) expect(response.method).toEqual('POST') expect(await response.json()).toEqual({ test: "success" }) }) test("stashing content after a successful fetch should work", async () => { self.LibResilientConfig = { plugins: [{ name: 'fetch' },{ name: 'cache' }], loggedComponents: [ 'service-worker', 'fetch', 'cache' ] } require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') var response = await self.trigger('fetch', new Request('/test.json')) expect(await response.json()).toEqual({ test: "success" }) expect (await caches.open('v1').then((cache)=>{ return cache.keys() }).then((keys)=>{ return keys[0].url })).toEqual(self.location.origin + '/test.json') expect (await caches.open('v1').then((cache)=>{ return cache.match(self.location.origin + '/test.json') }).then((response)=>{ return response.json() }).then((json)=>{ return json })).toEqual({ test: "success" }) }); test("stashing should be skipped if content was retrieved from a stashing plugin", async () => { self.LibResilientConfig = { plugins: [{ name: 'stashing-test' },{ name: 'reject-all' }], loggedComponents: [ 'service-worker' ] } let resolvingFetch = jest.fn((request, init)=>{ return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'X-LibResilient-Method': 'resolve-all', 'X-LibResilient-ETag': 'TestingETagHeader' }, url: self.location.origin + '/test.json' }) ) }) let rejectingFetch = jest.fn((request, init)=>{ return Promise.reject(request); }) let stashingStash = jest.fn() global.LibResilientPluginConstructors.set('stashing-test', ()=>{ return { name: 'stashing-test', description: 'Mock stashing plugin.', version: '0.0.1', fetch: resolvingFetch, stash: stashingStash } }) global.LibResilientPluginConstructors.set('reject-all', ()=>{ return { name: 'reject-all', description: 'Reject all requests.', version: '0.0.1', fetch: rejectingFetch } }) require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') var response = await self.trigger('fetch', new Request('/test.json')) expect(resolvingFetch).toHaveBeenCalled(); expect(stashingStash).not.toHaveBeenCalled(); expect(rejectingFetch).toHaveBeenCalled(); expect(await response.json()).toEqual({ test: "success" }) }); test("content should be stashed if it was retrieved from a job after retrieval from a stashing plugin, and it differs from the stashed version", async () => { self.LibResilientConfig = { plugins: [{ name: 'stashing-test' },{ name: 'resolve-all' }], loggedComponents: [ 'service-worker' ] } let resolvingFetch = jest.fn((request, init)=>{ return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'X-LibResilient-Method': 'resolve-all', 'X-LibResilient-ETag': 'TestingETagHeader' }, url: self.location.origin + '/test.json' }) ) }) let resolvingFetch2 = jest.fn((request, init)=>{ return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "success2" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'NewTestingETagHeader' }, url: self.location.origin + '/test.json' }) ) }) let stashingStash = jest.fn(async (response, url)=>{ expect(await response.json()).toEqual({ test: "success2" }) expect(response.headers.get('ETag')).toEqual('NewTestingETagHeader') }) global.LibResilientPluginConstructors.set('stashing-test', ()=>{ return { name: 'stashing-test', description: 'Mock stashing plugin.', version: '0.0.1', fetch: resolvingFetch, stash: stashingStash } }) global.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: resolvingFetch2 } }) var testClient = new Client() self.clients.clients.push(testClient) var fetchedDiffersFound = false testClient.addEventListener('message', event => { if (event.data.fetchedDiffers) { fetchedDiffersFound = true } }) require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') var response = await self.trigger('fetch', { request: new Request('/test.json'), clientId: testClient.id }) expect(resolvingFetch).toHaveBeenCalled(); expect(await response.json()).toEqual({ test: "success" }) expect(resolvingFetch2).toHaveBeenCalled(); expect(stashingStash).toHaveBeenCalled(); expect(fetchedDiffersFound).toEqual(true) }); test("content should be stashed if it was retrieved from a job after retrieval from a stashing plugin, even it does not differ from the stashed version", async () => { self.LibResilientConfig = { plugins: [{ name: 'stashing-test' },{ name: 'resolve-all' }], loggedComponents: [ 'service-worker' ] } let resolvingFetch = jest.fn((request, init)=>{ return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'X-LibResilient-Method': 'resolve-all', 'X-LibResilient-ETag': 'TestingETagHeader' }, url: self.location.origin + '/test.json' }) ) }) let stashingStash = jest.fn() global.LibResilientPluginConstructors.set('stashing-test', ()=>{ return { name: 'stashing-test', description: 'Mock stashing plugin.', version: '0.0.1', fetch: resolvingFetch, stash: stashingStash } }) global.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: resolvingFetch } }) require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') var response = await self.trigger('fetch', new Request('/test.json')) expect(resolvingFetch).toHaveBeenCalledTimes(2); expect(await response.json()).toEqual({ test: "success" }) expect(stashingStash).toHaveBeenCalled(); }); test("stashing content explicitly should work", async () => { self.LibResilientConfig = { plugins: [{ name: 'cache' }], loggedComponents: [ 'service-worker', 'cache' ] } require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') await self.trigger( 'message', { data:{ stash: [new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: self.location.origin + '/test.json' })] } }) // needed here also await new Promise(resolve => setTimeout(resolve, 100)); expect (await caches.open('v1').then((cache)=>{ return cache.keys() }).then((keys)=>{ return keys[0].url })).toEqual(self.location.origin + '/test.json') expect (await caches.open('v1').then((cache)=>{ return cache.match(self.location.origin + '/test.json') }).then((response)=>{ return response.json() }).then((json)=>{ return json })).toEqual({ test: "success" }) }); test("after a retrieval from a stashing plugin, background plugin should receive the Request() init data", async () => { self.LibResilientConfig = { plugins: [{ name: 'stashing-test' },{ name: 'resolve-all' }], loggedComponents: [ 'service-worker' ] } let resolvingFetch = jest.fn((request, init)=>{ return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'X-LibResilient-Method': 'resolve-all', 'X-LibResilient-ETag': 'TestingETagHeader' }, url: self.location.origin + '/test.json' }) ) }) let resolvingFetch2 = jest.fn((request, init)=>{ return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "success2" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'NewTestingETagHeader' }, url: self.location.origin + '/test.json' }) ) }) let stashingStash = jest.fn(async (response, url)=>{ expect(await response.json()).toEqual({ test: "success2" }) expect(response.headers.get('ETag')).toEqual('NewTestingETagHeader') }) global.LibResilientPluginConstructors.set('stashing-test', ()=>{ return { name: 'stashing-test', description: 'Mock stashing plugin.', version: '0.0.1', fetch: resolvingFetch, stash: stashingStash } }) global.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: resolvingFetch2 } }) var testClient = new Client() self.clients.clients.push(testClient) var fetchedDiffersFound = false testClient.addEventListener('message', event => { if (event.data.fetchedDiffers) { fetchedDiffersFound = true } }) require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') var initTest = { method: "GET", // TODO: ref. https://gitlab.com/rysiekpl/libresilient/-/issues/23 //headers: new Headers({"x-stub": "STUB"}), //mode: "mode-stub", //credentials: "credentials-stub", cache: "cache-stub", referrer: "referrer-stub", // these are not implemented by service-worker-mock // https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20 redirect: undefined, integrity: undefined, cache: undefined } var response = await self.trigger('fetch', { request: new Request('/test.json', initTest), clientId: testClient.id }) expect(resolvingFetch).toHaveBeenCalled(); expect(await response.json()).toEqual({ test: "success" }) expect(resolvingFetch).toHaveBeenCalledWith('https://www.test.com/test.json', initTest); expect(resolvingFetch2).toHaveBeenCalledWith('https://www.test.com/test.json', initTest); }); test("unstashing content explicitly should work", async () => { self.LibResilientConfig = { plugins: [{ name: 'cache' }], loggedComponents: [ 'service-worker', 'cache' ] } require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') await self.trigger( 'message', { data:{ stash: [new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: self.location.origin + '/test.json' })] } }) // needed here also await new Promise(resolve => setTimeout(resolve, 100)); expect (await caches.open('v1').then((cache)=>{ return cache.keys() }).then((keys)=>{ return keys[0].url })).toEqual(self.location.origin + '/test.json') expect (await caches.open('v1').then((cache)=>{ return cache.match(self.location.origin + '/test.json') }).then((response)=>{ return response.json() }).then((json)=>{ return json })).toEqual({ test: "success" }) // now unstash await self.trigger( 'message', { data:{ unstash: [self.location.origin + '/test.json'] } }) expect (await caches.open('v1').then((cache)=>{ return cache.keys() })).toEqual([]) }); test("publishing content explicitly should work (stub)", async () => { self.LibResilientConfig = { plugins: [{ name: 'publish-test' }], loggedComponents: [ 'service-worker' ] } var result = false global.LibResilientPluginConstructors.set('publish-test', ()=>{ return { name: 'publish-test', description: 'Publish plugin fixture.', version: '0.0.1', publish: (request)=>{ result = 'publish-test success: ' + request.url } } }) require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') await self.trigger( 'message', { data:{ publish: [new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: self.location.origin + '/test.json' })] } }) expect(result).toEqual('publish-test success: ' + self.location.origin + '/test.json') }) test("using plugins with dependencies should work", async () => { self.LibResilientConfig = { plugins: [{ name: 'dependent-test', uses: [{ name: 'dependency1-test' },{ name: 'dependency2-test' }] }], loggedComponents: [ 'service-worker' ] } global.LibResilientPluginConstructors.set('dependent-test', ()=>{ return { name: 'dependent-test', description: 'Dependent plugin fixture.', version: '0.0.1', uses: [{ name: 'dependency1-test' },{ name: 'dependency2-test' }] } }) global.LibResilientPluginConstructors.set('dependency1-test', ()=>{ return { name: 'dependency1-test', description: 'First dependency plugin fixture.', version: '0.0.1' } }) global.LibResilientPluginConstructors.set('dependency2-test', ()=>{ return { name: 'dependency2-test', description: 'Second dependency plugin fixture.', version: '0.0.1' } }) require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(self.LibResilientPlugins.map(p=>p.name)).toEqual(['dependent-test']) expect(self.LibResilientPlugins[0].uses.map(p=>p.name)).toEqual(['dependency1-test', 'dependency2-test']) }) test("using multiple instances of the same plugin should work", async () => { self.LibResilientConfig = { plugins: [{ name: 'plugin-test', },{ name: 'plugin-test', },{ name: 'plugin-test', }], loggedComponents: [ 'service-worker' ] } var pver = 0 global.LibResilientPluginConstructors.set('plugin-test', ()=>{ pver += 1 return { name: 'plugin-test', description: 'Simple plugin stub.', version: '0.0.' + pver } }) require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect(self.LibResilientPlugins.map(p=>p.name)).toEqual(['plugin-test', 'plugin-test', 'plugin-test']) expect(self.LibResilientPlugins.map(p=>p.version)).toEqual(['0.0.1', '0.0.2', '0.0.3']) }) test("should error out if all plugins fail", async () => { self.LibResilientConfig = { plugins: [{ name: 'reject-all' }], loggedComponents: [ 'service-worker' ] } global.LibResilientPluginConstructors.set('reject-all', ()=>{ return { name: 'reject-all', description: 'Reject all requests.', version: '0.0.1', fetch: (request, init)=>{ return Promise.reject(request); } } }) require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') expect.assertions(1) try { await self.trigger('fetch', new Request('/test.json', {method: "GET"})) } catch(e) { expect(e).toEqual(self.location.origin + '/test.json') } }) test("should send clientId back if event.resultingClientId is set", async () => { self.LibResilientConfig = { plugins: [{ name: 'resolve-all' }], loggedComponents: [ 'service-worker' ] } global.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: (request, init)=>{ return Promise.resolve( new Response( new Blob( [JSON.stringify({ test: "success" })], {type: "application/json"} ), { status: 200, statusText: "OK", headers: { 'ETag': 'TestingETagHeader' }, url: self.location.origin + '/test.json' }) ) } } }) var testClient = new Client() self.clients.clients.push(testClient) // monkey-patching addEventListener so that we can add // the resultingClientId field to the event in a fetch callback self.oldAddEventListener = self.addEventListener self.addEventListener = (eventName, callback) => { if (eventName === 'fetch') { return self.oldAddEventListener(eventName, event => { event.resultingClientId = testClient.id return callback(event) }) } else { return self.oldAddEventListener(eventName, callback) } } expect.hasAssertions() testClient.addEventListener('message', event => { if (event.data.clientId) { expect(event.data.clientId).toEqual(testClient.id) } }) require("../service-worker.js"); await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') await self.trigger('fetch', new Request('/test.json')) }) });