import { describe, it, beforeEach, beforeAll, afterEach } from "https://deno.land/std@0.183.0/testing/bdd.ts"; import { assert, assertThrows, assertRejects, assertEquals } from "https://deno.land/std@0.183.0/testing/asserts.ts"; import { assertSpyCall, assertSpyCalls, spy, } from "https://deno.land/std@0.183.0/testing/mock.ts"; /* * mocking the FetchEvent class * https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent */ class FetchEvent extends Event { request = null response = null constructor(request, init=null) { super('fetch') if (typeof request == "string") { if (request.indexOf('http') != 0) { request = window.location.origin + request } if (init == null) { request = new Request(request) } else { request = new Request(request, init) } } this.request = request } clientId = 'libresilient-tests' respondWith(a) { this.response = a } waitForResponse() { return new Promise(async (resolve, reject)=>{ while (this.response === null) { await new Promise(resolve => setTimeout(resolve, 1)) } resolve(this.response) }) } } beforeAll(async ()=>{ // default mocked response data let responseMockedData = { data: JSON.stringify({test: "success"}), type: "application/json", status: 200, statusText: "OK", headers: { 'Last-Modified': 'TestingLastModifiedHeader', 'ETag': 'TestingETagHeader' } } // get a Promise resolvint to a mocked Response object built based on supplied data window.getMockedResponse = (url, init, response_data={}) => { let rdata = { ...responseMockedData, ...response_data } let response = new Response( new Blob( [rdata.data], {type: rdata.type} ), { status: rdata.status, statusText: rdata.statusText, headers: rdata.headers }); // Response.url is read-only, so we have Object.defineProperty( response, "url", { value: url } ); return Promise.resolve(response); } // get a mocked fetch()-like function that returns a Promise resolving to the above window.getMockedFetch = (response_data={}) => { return (url, init)=>{ return window.getMockedResponse(url, init, response_data) } } // prepare a mocked transport plugin, returning a spied-on fetch()-like function window.prepareMockedTransportConfigJSON = (plugin_name, config_data, stale=false) => { // by default not stale let date = new Date().toUTCString() // very stale if need be if (stale === true) { date = new Date(0).toUTCString() } // prepare the function let mockedFetch = spy(window.getMockedFetch({ data: JSON.stringify(config_data), headers: { Date: date } })) // create the plugin window.LibResilientPluginConstructors.set(plugin_name, ()=>{ return { name: plugin_name, description: 'Resolve with config data (pretending to be fetch).', version: '0.0.1', fetch: mockedFetch } }) return mockedFetch } /** * caching a mocked config.json */ window.cacheMockedConfigJSON = async (cache_name, data, stale=false) => { // prepare the data let config_data = { data: JSON.stringify(data) } // do we want a stale cache? if (stale === true) { config_data.headers = { // very stale date 'Date': new Date(0).toUTCString() } } else { config_data.headers = { // current date 'Date': new Date().toUTCString() } } // cache it in the relevant cache return await caches .open(cache_name) .then(async (cache)=>{ await cache .put( self.config_url, await window .getMockedResponse(config_url, {}, config_data) ) }) } /* * prototype of the plugin init object */ window.initPrototype = { name: 'cache' } /* * mocking caches.match() * https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/match#browser_compatibility */ caches.match = async (url, options={}) => { let cn = 'v1' if ('cacheName' in options) { cn = options.cacheName } let cache = await caches.open(cn) return cache.match(url) } /* * mocking Event.waitUntil() * https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/waitUntil#browser_compatibility */ Event.prototype.waitUntil = async (promise) => { await promise } /* * mocking importScriptsPrototype */ window.importScriptsPrototype = (script) => { let plugin = null try { plugin = script.split('/')[2] } catch (e) {} if (plugin === null) { // ignoring errors here — these happen when we're not actually loading a plugin return false } window .LibResilientPluginConstructors .set( plugin, window.LibResilientPluginConstructorsPrototype.get(plugin) ) } window.LRLogPrototype = (component, ...items)=>{ console.debug(component + ' :: ', ...items) } /* * mocking window.clients */ window.clients = { get: async (id) => { // always return the same client, we care only about postMessage() working // and getting the messages return { // that's the only thing we need // this allows us to spy on client.postMessage() calls issued by the service worker postMessage: window.clients.prototypePostMessage } }, // the actual spy function must be possible to reference // but we want spy data per test, so we set it properly in beforeEach() prototypePostMessage: null } // we need to be able to reliably wait for SW installation // which is triggered by an "install" Event window.sw_install_ran = false // override addEventListener in order to override the callback // and to keep track of event listeners that we need to remove in afterEach() window.event_listeners = new Array() window.addEventListenerOrig = window.addEventListener window.addEventListener = async (evtype, func) => { // normally we want the handler to be what it says on the packaging let handler = func // but for "install" type event… we actually want to wrap it such that // we can then await for it if (evtype == 'install') { handler = async (ev) => { let result = await func(ev); window.sw_install_ran = true; return result; } } // adding to the list of installed event listeners window.event_listeners.push([evtype, handler]) // we're done return await window.addEventListenerOrig(evtype, handler) } // wait for SW installation window.waitForSWInstall = () => { return new Promise(async (resolve, reject)=>{ while (!window.sw_install_ran) { await new Promise(resolve => setTimeout(resolve, 1)) } resolve(true) }) } // wait for caching of a URL, looped up to `tries` times window.waitForCacheAction = (url, cache_name, action="add", tries=100) => { if (action != "add" && action != "remove" && action != "update") { throw new Error('waitForCacheAction()\'s action parameter can only be "add", "remove", or "update".') } console.log(`*** WAITING FOR CACHE (${cache_name}) ACTION:`, action, '\n - url:', url) return new Promise(async (resolve, reject)=>{ // get the cache object let cache = await caches.open(cache_name) let last_result = false // try to match until we succeed, or run out of tries for (let i=0; i A "CacheResponseResource" resource (rid 7) was created during // > the test, but not cleaned up during the test. Close the resource // > before the end of the test. return resolve(await cache_result.text()) } // waiting for cache content to be updated? } else if (action === 'update') { // we expect something to have already been in cache if (cache_result === undefined) { return reject('Nothing was in cache already, which is unexpected') } // save the "old" data if (last_result === false) { last_result = await cache_result.text() // we are waiting for an update of the content } else { let cur = await cache_result.text() if (last_result !== cur) { return resolve(cur) } } // waiting for content to be removed from cache? } else if (action === "remove") { if (cache_result === undefined) { return resolve(undefined) } // as above, we need to "use" the resource await cache_result.text() } await new Promise(resolve => setTimeout(resolve, 1)) } return reject("Ran out of tries"); }) } /* * importScripts mock relies on all plugins being loaded here * TODO: automagically load the list from the plugins directory */ let plugins = [ "alt-fetch", "any-of", "basic-integrity", "cache", "dnslink-fetch", "dnslink-ipfs", "fetch", "gun-ipfs", "integrity-check", "ipns-ipfs", "redirect", "signed-integrity", ] await Promise.all( plugins.map(async (plugin)=>{ await import(`../../plugins/${plugin}/index.js`) }) ) window.LibResilientPluginConstructorsPrototype = window.LibResilientPluginConstructors window.LibResilientPluginConstructors = new Map() }) /** * we need to do all of this before each test in order to reset the fetch() use counter * and make sure window.init is clean and not modified by previous tests */ beforeEach(async ()=>{ window.config_url = window.location.origin + 'config.json' window.fetch = spy(window.getMockedFetch()) /* * mocking our own ServiceWorker API, sigh! * https://github.com/denoland/deno/issues/5957#issuecomment-985494969 */ window.registration = { scope: "https://test.resilient.is/", unregister: spy(()=>{}) } window.init = { ...window.initPrototype } // clear the caches await caches .has('v1') .then(async (hasit) => { if (hasit) { await caches.delete('v1') } }) await caches .has('v1:verified') .then(async (hasit) => { if (hasit) { await caches.delete('v1:verified') } }) // make sure we're starting with a clean slate in LibResilientPluginConstructors window.LibResilientPluginConstructors = new Map() // keeping track of whether the SW got installed window.sw_install_ran = false // cleanup self.LibResilientConfig = null self.LibResilientPlugins = null // postMessage spy window.clients.prototypePostMessage = spy((msg)=>{console.log('*** got message', msg)}) // importScripts spy window.importScripts = spy(window.importScriptsPrototype) // LR.log spy window.LR.log = spy(window.LRLogPrototype) }) /** * after each test we need to do a bit of cleanup * * specifically, since we need to load the service-worker.js module anew * we want to clean up any side-effects of having loaded it for the previous test * and any side-effects of the previous test itself */ afterEach(async ()=>{ while (window.event_listeners.length) { await window.removeEventListener(...window.event_listeners.pop()); } window.test_id += 1; }) describe('service-worker', async () => { // mocking window.location // https://developer.mozilla.org/en-US/docs/Web/API/Window/location window.location = { origin: "https://test.resilient.is/" } window.LibResilientPluginConstructors = new Map() window.LR = { log: null } window.fetch = null window.importScripts = null window.test_id = 0 it("should use default LibResilientConfig values when config.json is missing", async () => { let mock_response_data = { data: JSON.stringify({text: "success"}), status: 404, statusText: "Not Found" } window.fetch = spy(window.getMockedFetch(mock_response_data)) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, "object") assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000) assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache']) assertEquals(self.LibResilientConfig.normalizeQueryParams, true) assertEquals(self.LibResilientConfig.useMimeSniffingLibrary, false) assertSpyCalls(self.fetch, 1) }) it("should throw a meaningful exception when an unhandled exception occurs during initialization", async () => { // this will generate an exception on line where a new Map() // is created from self.LibResilientPluginConstructors self.LibResilientPluginConstructors = true await import("../../service-worker.js?" + window.test_id); // the a failed install event handler in the service worker code // will run self.registration.unregister() to not leave the site in a broken state await self.dispatchEvent(new Event('install')) assertSpyCalls(self.registration.unregister, 1) }) it("should use default LibResilientConfig values when fetching config.json throws an exception", async () => { window.fetch = spy(() => { throw new Error('Testing exception') }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, "object") assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000) assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache']) assertEquals(self.LibResilientConfig.normalizeQueryParams, true) assertEquals(self.LibResilientConfig.useMimeSniffingLibrary, false) assertSpyCalls(self.fetch, 1) }) it("should use default LibResilientConfig values when fetching config.json returns undefined", async () => { window.fetch = spy(() => { return undefined; }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, "object") assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000) assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache']) assertEquals(self.LibResilientConfig.normalizeQueryParams, true) assertEquals(self.LibResilientConfig.useMimeSniffingLibrary, false) assertSpyCalls(self.fetch, 1) }) it("should fail if default LibResilientConfig values are invalid and fetched config.json is not valid JSON", async () => { let mock_response_data = { data: "Not JSON" } window.fetch = spy(window.getMockedFetch(mock_response_data)) self.LibResilientConfig = {} await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertSpyCalls(self.fetch, 1) assertSpyCalls(self.registration.unregister, 1) }) it("should use default LibResilientConfig values when the fetched config.json is not valid JSON", async () => { let mock_response_data = { data: "Not JSON" } window.fetch = spy(window.getMockedFetch(mock_response_data)) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, "object") assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000) assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache']) assertEquals(self.LibResilientConfig.normalizeQueryParams, true) assertEquals(self.LibResilientConfig.useMimeSniffingLibrary, false) assertSpyCalls(self.fetch, 1) }) it("should use default LibResilientConfig values when no valid 'plugins' field in config.json", async () => { let mock_response_data = { data: JSON.stringify({loggedComponents: ['service-worker', 'fetch'], plugins: 'not a valid array'}) } window.fetch = spy(window.getMockedFetch(mock_response_data)) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, "object") assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000) assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache']) assertEquals(self.LibResilientConfig.normalizeQueryParams, true) assertEquals(self.LibResilientConfig.useMimeSniffingLibrary, false) assertSpyCalls(self.fetch, 1) }) it("should use default LibResilientConfig values when no valid 'loggedComponents' field in config.json", async () => { let mock_response_data = { data: JSON.stringify({loggedComponents: 'not a valid array', plugins: [{name: "fetch"}]}) } window.fetch = spy(window.getMockedFetch(mock_response_data)) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, "object") assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000) assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache']) assertEquals(self.LibResilientConfig.normalizeQueryParams, true) assertEquals(self.LibResilientConfig.useMimeSniffingLibrary, false) assertSpyCalls(self.fetch, 1) }) it("should use default LibResilientConfig values when 'defaultPluginTimeout' field in config.json contains an invalid value", async () => { let mock_response_data = { data: JSON.stringify({loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 'not an integer'}) } window.fetch = spy(window.getMockedFetch(mock_response_data)) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, "object") assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000) assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache']) assertEquals(self.LibResilientConfig.normalizeQueryParams, true) assertEquals(self.LibResilientConfig.useMimeSniffingLibrary, false) assertSpyCalls(self.fetch, 1) }) it("should use default LibResilientConfig values when 'normalizeQueryParams' field in config.json contains an invalid value", async () => { let mock_response_data = { data: JSON.stringify({loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 5000, normalizeQueryParams: "not a boolean"}) } window.fetch = spy(window.getMockedFetch(mock_response_data)) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, "object") assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000) assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache']) assertEquals(self.LibResilientConfig.normalizeQueryParams, true) assertEquals(self.LibResilientConfig.useMimeSniffingLibrary, false) assertSpyCalls(self.fetch, 1) }) it("should use default LibResilientConfig values when 'useMimeSniffingLibrary' field in config.json contains an invalid value", async () => { let mock_response_data = { data: JSON.stringify({loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 5000, normalizeQueryParams: false, useMimeSniffingLibrary: "not a boolean"}) } window.fetch = spy(window.getMockedFetch(mock_response_data)) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, "object") assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000) assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache']) assertEquals(self.LibResilientConfig.normalizeQueryParams, true) assertEquals(self.LibResilientConfig.useMimeSniffingLibrary, false) assertSpyCalls(self.fetch, 1) }) it("should use config values from a valid fetched config.json file, caching it in both caches (v1, v1:verified)", async () => { let mock_response_data = { data: JSON.stringify({loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000, normalizeQueryParams: false, useMimeSniffingLibrary: true}) } window.fetch = spy(window.getMockedFetch(mock_response_data)) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, "object") assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) assertEquals(self.LibResilientConfig.normalizeQueryParams, false) assertEquals(self.LibResilientConfig.useMimeSniffingLibrary, true) assertSpyCalls(self.fetch, 1) assertEquals( await window.waitForCacheAction(self.config_url, 'v1'), mock_response_data.data ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), mock_response_data.data ); }) it("should 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' }], loggedComponents: ['service-worker'] } window.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 || [] } }) window.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 || [] } }) window.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 || [] } }) window.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 || [] } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, 'object') // basic stuff assertEquals(self.LibResilientPlugins.length, 4) assertEquals(self.LibResilientPlugins[0].name, 'plugin-1') assertEquals(self.LibResilientPlugins[1].name, 'plugin-2') assertEquals(self.LibResilientPlugins[2].name, 'plugin-3') assertEquals(self.LibResilientPlugins[3].name, 'plugin-4') // first plugin dependencies assertEquals(self.LibResilientPlugins[0].uses.length, 2) assertEquals(self.LibResilientPlugins[0].uses[0].name, 'plugin-2') assertEquals(self.LibResilientPlugins[0].uses[0].uses.length, 1) assertEquals(self.LibResilientPlugins[0].uses[0].uses[0].name, 'plugin-3') assertEquals(self.LibResilientPlugins[0].uses[0].uses[0].uses.length, 0) assertEquals(self.LibResilientPlugins[0].uses[1].name, 'plugin-3') assertEquals(self.LibResilientPlugins[0].uses[1].uses.length, 0) // second plugin dependencies assertEquals(self.LibResilientPlugins[1].uses.length, 1) assertEquals(self.LibResilientPlugins[1].uses[0].name, 'plugin-3') assertEquals(self.LibResilientPlugins[1].uses[0].uses.length, 0) // third plugin dependencies assertEquals(self.LibResilientPlugins[2].uses.length, 2) assertEquals(self.LibResilientPlugins[2].uses[0].name, 'plugin-1') assertEquals(self.LibResilientPlugins[2].uses[0].uses.length, 0) assertEquals(self.LibResilientPlugins[2].uses[1].name, 'plugin-2') assertEquals(self.LibResilientPlugins[2].uses[1].uses.length, 1) assertEquals(self.LibResilientPlugins[2].uses[1].uses[0].name, 'plugin-1') assertEquals(self.LibResilientPlugins[2].uses[1].uses[0].uses.length, 1) assertEquals(self.LibResilientPlugins[2].uses[1].uses[0].uses[0].name, 'plugin-4') assertEquals(self.LibResilientPlugins[2].uses[1].uses[0].uses[0].uses.length, 0) // fourth plugin dependencies assertEquals(self.LibResilientPlugins[3].uses.length, 0) }) it("should 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 }] } window.LibResilientPluginConstructors.set('plugin-enabled', ()=>{ return { name: 'plugin-enabled', description: 'Enabled plugin', version: '0.0.1', fetch: (url)=>{return true} } }) window.LibResilientPluginConstructors.set('plugin-disabled', ()=>{ return { name: 'plugin-disabled', description: 'Disabled plugin', version: '0.0.1', fetch: (url)=>{return true} } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientPlugins.length, 2) assertEquals(self.LibResilientPlugins[0].name, 'plugin-enabled') assertEquals(self.LibResilientPlugins[1].name, 'plugin-enabled') }) it("should 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'] } window.LibResilientPluginConstructors.set('plugin-enabled', (LRPC, config)=>{ return { name: 'plugin-enabled', description: 'Enabled plugin', version: '0.0.1', fetch: (url)=>{return true}, uses: config.uses || [] } }) window.LibResilientPluginConstructors.set('plugin-disabled', (LRPC, config)=>{ return { name: 'plugin-disabled', description: 'Disabled plugin', version: '0.0.1', fetch: (url)=>{return true}, uses: config.uses || [] } }) window.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 || [] } }) window.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 || [] } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientPlugins.length, 2) assertEquals(self.LibResilientPlugins[0].name, 'plugin-enabled') assertEquals(self.LibResilientPlugins[0].uses.length, 0) assertEquals(self.LibResilientPlugins[1].name, 'plugin-enabled') assertEquals(self.LibResilientPlugins[1].uses.length, 1) assertEquals(self.LibResilientPlugins[1].uses[0].name, 'dependency-enabled') assertEquals(self.LibResilientPlugins[1].uses[0].uses.length, 0) }) /* * testing config.json fetching, verification, and caching */ it("should use a valid config.json from v1 cache, with no fetch happening, caching it in v1:verified cache", async () => { // v1 cache data let v1_data = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000} // cache it in v1 cache, not stale await self.cacheMockedConfigJSON("v1", v1_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // no fetch assertSpyCalls(self.fetch, 0) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) // in both caches we expect the contents of v1 cache assertEquals( await window.waitForCacheAction(self.config_url, 'v1'), JSON.stringify(v1_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1_data), "v1:verified cache has incorrect config.json data" ); }) it("should use a valid config.json from v1:verified cache when not available in v1, with no fetch happening, caching it in v1 cache", async () => { // v1:verified cache data let v1verified_data = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 6000} // cache it in v1 cache, not stale await self.cacheMockedConfigJSON("v1:verified", v1verified_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // no fetch assertSpyCalls(self.fetch, 0) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) // in both caches we expect the contents of v1 cache assertEquals( await window.waitForCacheAction(self.config_url, 'v1'), JSON.stringify(v1verified_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1verified_data), "v1:verified cache has incorrect config.json data" ); }) it("should use a valid config.json from v1:verified cache when the one in v1 is invalid (structurally), with no fetch happening, caching it in v1 cache", async () => { // v1 cached data invalid (invalid structure) let v1_data = {loggedComponentsInvalid: ['service-worker', 'cache'], pluginsInvalid: [{name: "cache"}], defaultPluginTimeoutInvalid: 5000} await self.cacheMockedConfigJSON("v1", v1_data, false) // v1:verified cached data valid let v1verified_data = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 6000} await self.cacheMockedConfigJSON("v1:verified", v1verified_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // no fetch assertSpyCalls(self.fetch, 0) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) // in both caches we expect the contents of v1 cache; in v1 the contents need to be updated assertEquals( await window.waitForCacheAction(self.config_url, 'v1'), JSON.stringify(v1verified_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1verified_data), "v1:verified cache has incorrect config.json data" ); }) it("should use a valid config.json from v1:verified cache when the one in v1 is invalid (syntactically), with no fetch happening, caching it in v1 cache", async () => { // v1 cached data invalid (not JSON) let v1_data = "NOT JSON" await self.cacheMockedConfigJSON("v1", v1_data, false) // v1:verified cached data valid let v1verified_data = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 6000} await self.cacheMockedConfigJSON("v1:verified", v1verified_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // no fetch assertSpyCalls(self.fetch, 0) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) // in both caches we expect the contents of v1 cache; in v1 the contents need to be updated assertEquals( await window.waitForCacheAction(self.config_url, 'v1'), JSON.stringify(v1verified_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1verified_data), "v1:verified cache has incorrect config.json data" ); }) it("should use a valid config.json from v1:verified cache when the one in v1 is invalid (unavailable plugins), with no fetch happening, caching it in v1 cache", async () => { // v1 cached data valid let v1_data = {loggedComponents: ['service-worker', 'no-such-plugin'], plugins: [{name: "no-such-plugin"}], defaultPluginTimeout: 5000} await self.cacheMockedConfigJSON("v1", v1_data, false) // v1:verified cached data valid let v1verified_data = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 6000} await self.cacheMockedConfigJSON("v1:verified", v1verified_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // no fetch assertSpyCalls(self.fetch, 0) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) // in both caches we expect the contents of v1 cache; in v1 the contents need to be updated assertEquals( await window.waitForCacheAction(self.config_url, 'v1'), JSON.stringify(v1verified_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1verified_data), "v1:verified cache has incorrect config.json data" ); }) it("should use a stale config.json from v1 cache, with no fetch happening, caching it in v1:verified cache", async () => { // v1 cached data valid let v1_data = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000} await self.cacheMockedConfigJSON("v1", v1_data, true) // stale! // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // no fetch assertSpyCalls(self.fetch, 0) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) // in both caches we expect the contents of v1 cache; in v1 the contents need to be updated assertEquals( await window.waitForCacheAction(self.config_url, 'v1'), JSON.stringify(v1_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1_data), "v1:verified cache has incorrect config.json data" ); }) it("should use a stale config.json from v1 cache, caching it in v1:verified cache; then try to fetch a fresh version, caching it if valid in v1 cache", async () => { // v1 cached data valid let v1_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "mocked-fetch"}, {name: 'cache'}], defaultPluginTimeout: 5000} await self.cacheMockedConfigJSON("v1", v1_data, true) // stale! let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "cache"}, {name: "mocked-fetch"}], defaultPluginTimeout: 7000} let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // no fetch assertSpyCalls(mockedFetch, 1) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}, {name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch', 'cache']) assertEquals( await window.waitForCacheAction(self.config_url, 'v1', 'update'), JSON.stringify(fetch_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1_data), "v1:verified cache has incorrect config.json data" ); }) it("should use a stale config.json from v1 cache, then try to fetch a fresh version, ignoring it if invalid (structurally) and caching the v1 version in v1:verified cache", async () => { // v1 cached data valid let v1_data = {loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 5000} await self.cacheMockedConfigJSON("v1", v1_data, true) // stale! // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // no fetch assertSpyCalls(self.fetch, 1) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch']) // v1 cache should *not* be updated try { await window.waitForCacheAction(self.config_url, 'v1', 'update') } catch (e) { assertEquals(e, 'Ran out of tries') } assertEquals( await window.waitForCacheAction(self.config_url, 'v1'), JSON.stringify(v1_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1_data), "v1:verified cache has incorrect config.json data" ); }) it("should use a stale config.json from v1 cache, then try to fetch a fresh version, ignoring it if invalid (unavailable plugins) and caching the v1 version in v1:verified cache", async () => { // v1 cached data valid let v1_data = {loggedComponents: ['service-worker', 'mocked-fetch'], plugins: [{name: "mocked-fetch"}], defaultPluginTimeout: 5000} await self.cacheMockedConfigJSON("v1", v1_data, true) // stale! let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'no-such-plugin'], plugins: [{name: "mocked-fetch"}, {name: 'no-such-plugin'}], defaultPluginTimeout: 7000} let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // no fetch assertSpyCalls(mockedFetch, 1) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch']) // v1 cache should *not* be updated try { await window.waitForCacheAction(self.config_url, 'v1', 'update') } catch (e) { assertEquals(e, 'Ran out of tries') } assertEquals( await window.waitForCacheAction(self.config_url, 'v1'), JSON.stringify(v1_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1_data), "v1:verified cache has incorrect config.json data" ); }) it("should use a stale config.json from v1:verified cache; then try to fetch a fresh version, caching it if valid in v1 cache", async () => { // v1:verified cached data valid let v1verified_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "mocked-fetch"}, {name: 'cache'}], defaultPluginTimeout: 6000} await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "cache"}, {name: "mocked-fetch"}], defaultPluginTimeout: 7000} let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // fetch assertSpyCalls(mockedFetch, 1) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}, {name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch', 'cache']) assertEquals( await window.waitForCacheAction(self.config_url, 'v1', 'update'), JSON.stringify(fetch_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1verified_data), "v1:verified cache has incorrect config.json data" ); }) it("should use a stale config.json from v1:verified cache, then try to fetch a fresh version, ignoring it if invalid (structurally) and caching the v1:verified version in v1 cache", async () => { // v1:verified cached data valid let v1verified_data = {loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 6000} await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // fetch assertSpyCalls(self.fetch, 1) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch']) assertEquals( await window.waitForCacheAction(self.config_url, 'v1'), JSON.stringify(v1verified_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1verified_data), "v1:verified cache has incorrect config.json data" ); }) it("should use a stale config.json from v1:verified cache, then try to fetch a fresh version, ignoring it if invalid (unavailable plugins) and caching the v1:verified version in v1 cache", async () => { // v1:verified cached data valid let v1verified_data = {loggedComponents: ['service-worker', 'mocked-fetch'], plugins: [{name: "mocked-fetch"}], defaultPluginTimeout: 6000} await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'no-such-plugin'], plugins: [{name: "mocked-fetch"}, {name: 'no-such-plugin'}], defaultPluginTimeout: 7000} let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // mocked fetch assertSpyCalls(mockedFetch, 1) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch']) assertEquals( await window.waitForCacheAction(self.config_url, 'v1'), JSON.stringify(v1verified_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1verified_data), "v1:verified cache has incorrect config.json data" ); }) it("should ignore invalid (structurally) config from v1 cache and use a stale config.json from v1:verified cache; then try to fetch a fresh version, caching it if valid in v1 cache", async () => { // v1 cached data invalid (invalid structure) let v1_data = {loggedComponentsInvalid: ['service-worker', 'cache'], pluginsInvalid: [{name: "cache"}], defaultPluginTimeoutInvalid: 5000} await self.cacheMockedConfigJSON("v1", v1_data, false) // v1:verified cached data valid let v1verified_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "mocked-fetch"}, {name: 'cache'}], defaultPluginTimeout: 6000} await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "cache"}, {name: "mocked-fetch"}], defaultPluginTimeout: 7000} let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // fetch assertSpyCalls(mockedFetch, 1) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}, {name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch', 'cache']) assertEquals( await window.waitForCacheAction(self.config_url, 'v1', 'update'), JSON.stringify(fetch_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1verified_data), "v1:verified cache has incorrect config.json data" ); }) it("should ignore invalid (syntactically) config from v1 cache and use a stale config.json from v1:verified cache; then try to fetch a fresh version, caching it if valid in v1 cache", async () => { // v1 cached data invalid (not JSON) let v1_data = "NOT JSON" await self.cacheMockedConfigJSON("v1", v1_data, false) // v1:verified cached data valid let v1verified_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "mocked-fetch"}, {name: 'cache'}], defaultPluginTimeout: 6000} await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "cache"}, {name: "mocked-fetch"}], defaultPluginTimeout: 7000} let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // fetch assertSpyCalls(mockedFetch, 1) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}, {name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch', 'cache']) assertEquals( await window.waitForCacheAction(self.config_url, 'v1', 'update'), JSON.stringify(fetch_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1verified_data), "v1:verified cache has incorrect config.json data" ); }) it("should ignore invalid (unavailable plugins) config from v1 cache and use a stale config.json from v1:verified cache; then try to fetch a fresh version, caching it if valid in v1 cache", async () => { // v1 cached data invalid (unavailable plugin) let v1_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'no-such-plugin'], plugins: [{name: "mocked-fetch"}, {name: 'no-such-plugin'}], defaultPluginTimeout: 5000} await self.cacheMockedConfigJSON("v1", v1_data, false) // v1:verified cached data valid let v1verified_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "mocked-fetch"}, {name: 'cache'}], defaultPluginTimeout: 6000} await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "cache"}, {name: "mocked-fetch"}], defaultPluginTimeout: 7000} let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // fetch assertSpyCalls(mockedFetch, 1) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}, {name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch', 'cache']) assertEquals( await window.waitForCacheAction(self.config_url, 'v1', 'update'), JSON.stringify(fetch_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1verified_data), "v1:verified cache has incorrect config.json data" ); }) it("should ignore invalid config from v1 cache and use a stale config.json from v1:verified cache; then try to fetch a fresh version, ignoring it if invalid", async () => { // v1 cached data invalid (invalid structure) let v1_data = {loggedComponentsInvalid: ['service-worker', 'cache'], pluginsInvalid: [{name: "cache"}], defaultPluginTimeoutInvalid: 5000} await self.cacheMockedConfigJSON("v1", v1_data, false) // v1:verified cached data valid let v1verified_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "mocked-fetch"}, {name: 'cache'}], defaultPluginTimeout: 6000} await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! let fetch_data = "NOT JSON" let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // fetch assertSpyCalls(mockedFetch, 1) // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}, {name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch', 'cache']) assertEquals( await window.waitForCacheAction(self.config_url, 'v1'), JSON.stringify(v1verified_data), "v1 cache has incorrect config.json data" ); assertEquals( await window.waitForCacheAction(self.config_url, 'v1:verified'), JSON.stringify(v1verified_data), "v1:verified cache has incorrect config.json data" ); }) /* * end of testing config.json fetching, verification, and caching */ it("should ignore failed fetch by first configured plugin if followed by a successful fetch by a second one", async () => { window.LibResilientConfig = { plugins: [{ name: 'reject-all' },{ name: 'resolve-all' }], loggedComponents: [ 'service-worker' ] } let rejectingFetch = spy( (request, init)=>{ return Promise.reject('reject-all rejecting a request for: ' + request); } ) window.LibResilientPluginConstructors.set('reject-all', ()=>{ return { name: 'reject-all', description: 'Reject all requests.', version: '0.0.1', fetch: rejectingFetch } }) window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: fetch } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let fetch_event = new FetchEvent('test.json') window.dispatchEvent(fetch_event) let response = await fetch_event.waitForResponse() assertSpyCalls(window.fetch, 2); // two, because the first one is for config.json assertSpyCalls(rejectingFetch, 1); assertSpyCall(window.fetch, 1, { args: [ "https://test.resilient.is/test.json", { cache: undefined, integrity: undefined, method: "GET", redirect: "follow", referrer: undefined, }] }) assertEquals(await response.json(), { test: "success" }) }); it("should normalize query params in requested URLs by default", async () => { console.log(self.LibResilientConfig) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let fetch_event = new FetchEvent('test.json?b=bbb&a=aaa&d=ddd&c=ccc') window.dispatchEvent(fetch_event) let response = await fetch_event.waitForResponse() assertEquals( fetch.calls[1].args[0], "https://test.resilient.is/test.json?a=aaa&b=bbb&c=ccc&d=ddd" ) }) it("should not normalize query params in requested URLs if 'normalizeQueryParams' is set to false", async () => { self.LibResilientConfig = { plugins: [{ name: 'fetch' }], loggedComponents: [ 'service-worker' ], normalizeQueryParams: false } await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let fetch_event = new FetchEvent('test.json?b=bbb&a=aaa&d=ddd&c=ccc') window.dispatchEvent(fetch_event) let response = await fetch_event.waitForResponse() assertEquals( fetch.calls[1].args[0], "https://test.resilient.is/test.json?b=bbb&a=aaa&d=ddd&c=ccc" ) }) it("should pass the Request() init data to plugins", async () => { self.LibResilientConfig = { plugins: [{ name: 'reject-all' },{ name: 'resolve-all' }], loggedComponents: [ 'service-worker' ] } let rejectingFetch = spy( (request, init)=>{ return Promise.reject('reject-all rejecting a request for: ' + request); } ) window.LibResilientPluginConstructors.set('reject-all', ()=>{ return { name: 'reject-all', description: 'Reject all requests.', version: '0.0.1', fetch: rejectingFetch } }) window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: fetch } }) let 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", redirect: "error", //integrity: "" } await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let fetch_event = new FetchEvent('test.json', initTest) window.dispatchEvent(fetch_event) let response = await fetch_event.waitForResponse() assertSpyCalls(rejectingFetch, 1); assertSpyCalls(window.fetch, 2); // two, because the first one is for config.json assertEquals(await response.json(), { test: "success" }) assertSpyCall(rejectingFetch, 0, { args: [ "https://test.resilient.is/test.json", { cache: undefined, integrity: undefined, method: "GET", redirect: "error", referrer: undefined, }] }) assertSpyCall(window.fetch, 1, { args: [ "https://test.resilient.is/test.json", { cache: undefined, integrity: undefined, method: "GET", redirect: "error", referrer: undefined, }] }) }); it("should respect defaultPluginTimeout", async () => { window.LibResilientConfig = { defaultPluginTimeout: 100, plugins: [{ name: 'resolve-with-timeout' }], loggedComponents: [ 'service-worker', ] } let rwtCallback = spy() let rwt_timeout_id = null window.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)=>{ rwt_timeout_id = setTimeout(rwtCallback, 300) }) } } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let fetch_event = new FetchEvent('test.json') window.dispatchEvent(fetch_event) let err = null try { let response = await fetch_event.waitForResponse() } catch(e) { err = e } clearTimeout(rwt_timeout_id) assertEquals(err.toString(), "Error: LibResilient request using resolve-with-timeout timed out after 100ms.") }); it("external request should work and not go through the plugins", async () => { self.LibResilientConfig = { plugins: [{ name: 'reject-all' }], loggedComponents: [ 'service-worker' ] } window.LibResilientPluginConstructors.set('reject-all', ()=>{ return { name: 'reject-all', description: 'Reject all requests.', version: '0.0.1', fetch: (request, init)=>{ return Promise.reject(request); } } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let fetch_event = new FetchEvent('https://example.com/test.json') window.dispatchEvent(fetch_event) let response = await fetch_event.waitForResponse() assertEquals(await response.json(), { test: "success" }) }) it("should make POST requests not go through the plugins", async () => { self.LibResilientConfig = { plugins: [{ name: 'reject-all' }], loggedComponents: [ 'service-worker' ] } window.LibResilientPluginConstructors.set('reject-all', ()=>{ return { name: 'reject-all', description: 'Reject all requests.', version: '0.0.1', fetch: (request, init)=>{ return Promise.reject(request); } } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let fetch_event = new FetchEvent(window.location.origin + 'test.json', {method: "POST"}) window.dispatchEvent(fetch_event) let response = await fetch_event.waitForResponse() assertEquals(await response.json(), { test: "success" }) }) it("should stash content after a successful fetch", async () => { self.LibResilientConfig = { plugins: [{ name: 'fetch' },{ name: 'cache' }], loggedComponents: [ 'service-worker', 'fetch', 'cache' ] } await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let fetch_event = new FetchEvent(window.location.origin + 'test.json') window.dispatchEvent(fetch_event) let response = await fetch_event.waitForResponse() assertEquals(await response.json(), { test: "success" }) // stashing plugin's stash() is called asynchronously in the Service Worker, // if we don't make sure that the caching has completed, we will get an error. // so we wait until config.json is cached, and use that to verify it is in fact // cached assertEquals( JSON.parse( await window.waitForCacheAction(window.location.origin + 'test.json', 'v1')), { test: "success" } ); }); it("should skip stashing should if content was retrieved from a stashing plugin", async () => { self.LibResilientConfig = { plugins: [{ name: 'stashing-test' },{ name: 'reject-all' }], loggedComponents: [ 'service-worker' ] } // three little mocks let resolvingFetch = spy(window.getMockedFetch()) let rejectingFetch = spy((request, init)=>{ return Promise.reject(request); }) let stashingStash = spy() // two little plugins window.LibResilientPluginConstructors.set('stashing-test', ()=>{ return { name: 'stashing-test', description: 'Mock stashing plugin.', version: '0.0.1', fetch: resolvingFetch, stash: stashingStash } }) window.LibResilientPluginConstructors.set('reject-all', ()=>{ return { name: 'reject-all', description: 'Reject all requests.', version: '0.0.1', fetch: rejectingFetch } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let fetch_event = new FetchEvent(window.location.origin + 'test.json') window.dispatchEvent(fetch_event) let response = await fetch_event.waitForResponse() assertEquals(await response.json(), { test: "success" }) assertSpyCalls(resolvingFetch, 1) assertSpyCalls(stashingStash, 0) assertSpyCalls(rejectingFetch, 1) }); it("should stash content 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' ] } // three little mocks let resolvingFetch = spy(window.getMockedFetch()) let resolvingFetch2 = spy(window.getMockedFetch({ data: JSON.stringify({ test: "success2" }), headers: { 'X-LibResilient-ETag': 'NewTestingETagHeader' } })) let stashingStash = spy(async (response, url)=>{ assertEquals(await response.json(), { test: "success2" }) assertEquals(response.headers.get('X-LibResilient-ETag'), 'NewTestingETagHeader') }) window.LibResilientPluginConstructors.set('stashing-test', ()=>{ return { name: 'stashing-test', description: 'Mock stashing plugin.', version: '0.0.1', fetch: resolvingFetch, stash: stashingStash } }) window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: resolvingFetch2 } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let fetch_event = new FetchEvent(window.location.origin + 'test.json') window.dispatchEvent(fetch_event) let response = await fetch_event.waitForResponse() assertEquals(await response.json(), { test: "success" }) assertSpyCalls(resolvingFetch, 1) assertSpyCalls(stashingStash, 1) assertSpyCalls(resolvingFetch2, 1) assertSpyCall( window.clients.prototypePostMessage, 6, { args: [{ url: "https://test.resilient.is/test.json", fetchedDiffers: true }]} ) }); it("should stash content when explicitly asked to", async () => { self.LibResilientConfig = { plugins: [{ name: 'cache' }], loggedComponents: [ 'service-worker', 'cache' ] } await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let stashEvent = new Event('message') stashEvent.data = { stash: [await window.getMockedResponse(window.location.origin + 'test.json')] } // stash it! await self.dispatchEvent(stashEvent) // let's see if it got added to cache assertEquals( JSON.parse(await window.waitForCacheAction(window.location.origin + 'test.json', 'v1')), { test: "success" } ); }); it("should pass the Request() init data to a background plugin after a retrieval from a stashing plugin", async () => { self.LibResilientConfig = { plugins: [{ name: 'stashing-test' },{ name: 'resolve-all' }], loggedComponents: [ 'service-worker' ] } let resolvingFetch = spy(window.getMockedFetch({ headers: { 'X-LibResilient-Method': 'resolve-all', 'X-LibResilient-ETag': 'TestingETagHeader' } })) let resolvingFetch2 = spy(window.getMockedFetch({ data: JSON.stringify({ test: "success2" }), headers: { 'ETag': 'NewTestingETagHeader' } })) let stashingStash = spy(async (response, url)=>{ assertEquals(await response.json(), { test: "success2" }) assertEquals(response.headers.get('ETag'), 'NewTestingETagHeader') }) window.LibResilientPluginConstructors.set('stashing-test', ()=>{ return { name: 'stashing-test', description: 'Mock stashing plugin.', version: '0.0.1', fetch: resolvingFetch, stash: stashingStash } }) window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: resolvingFetch2 } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let initTest = { method: "GET", // TODO: ref. https://gitlab.com/rysiekpl/libresilient/-/issues/23 //headers: new Headers({"x-stub": "STUB"}), //mode: "mode-stub", //credentials: "same-origin", cache: undefined, referrer: undefined, redirect: "error", // this is the only signal we get here, really! integrity: undefined } let fetch_event = new FetchEvent(window.location.origin + 'test.json', initTest) window.dispatchEvent(fetch_event) let response = await fetch_event.waitForResponse() assertSpyCalls(resolvingFetch, 1); assertSpyCalls(resolvingFetch2, 1); assertEquals(await response.json(), { test: "success" }) assertSpyCall( resolvingFetch, 0, { args: [ window.location.origin + 'test.json', initTest ]} ) assertSpyCall( resolvingFetch2, 0, { args: [ window.location.origin + 'test.json', initTest ]} ) }); it("should unstash content when explicitly asked to", async () => { self.LibResilientConfig = { plugins: [{ name: 'cache' }], loggedComponents: [ 'service-worker', 'cache' ] } await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let stashEvent = new Event('message') stashEvent.data = { stash: [await window.getMockedResponse(window.location.origin + 'test.json')] } // stash it! await self.dispatchEvent(stashEvent) // let's see if it got added to cache assertEquals( JSON.parse(await window.waitForCacheAction(window.location.origin + 'test.json', 'v1')), { test: "success" } ); let unstashEvent = new Event("message") unstashEvent.data = { unstash: [window.location.origin + 'test.json'] } // unstash it! await self.dispatchEvent(unstashEvent) // let's see if it got removed from cache assertEquals( await window.waitForCacheAction(window.location.origin + 'test.json', 'v1', "remove"), undefined ); }); it("should handle publishing content explicitly", async () => { self.LibResilientConfig = { plugins: [{ name: 'publish-test' }], loggedComponents: [ 'service-worker' ] } let publishMock = spy() window.LibResilientPluginConstructors.set('publish-test', ()=>{ return { name: 'publish-test', description: 'Publish plugin fixture.', version: '0.0.1', publish: publishMock } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let publishEvent = new Event('message') publishEvent.data = { publish: [await window.getMockedResponse(window.location.origin + 'test.json')] } // publish it! await self.dispatchEvent(publishEvent) assertSpyCall(publishMock, 0, { args: [publishEvent.data.publish[0]] }) }) it("should be able to use plugins with dependencies correctly", async () => { self.LibResilientConfig = { plugins: [{ name: 'dependent-test', uses: [{ name: 'dependency1-test' },{ name: 'dependency2-test' }] }], loggedComponents: [ 'service-worker' ] } window.LibResilientPluginConstructors.set('dependent-test', ()=>{ return { name: 'dependent-test', description: 'Dependent plugin fixture.', version: '0.0.1', uses: [{ name: 'dependency1-test' },{ name: 'dependency2-test' }] } }) window.LibResilientPluginConstructors.set('dependency1-test', ()=>{ return { name: 'dependency1-test', description: 'First dependency plugin fixture.', version: '0.0.1' } }) window.LibResilientPluginConstructors.set('dependency2-test', ()=>{ return { name: 'dependency2-test', description: 'Second dependency plugin fixture.', version: '0.0.1' } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(self.LibResilientPlugins.map(p=>p.name), ['dependent-test']) assertEquals(self.LibResilientPlugins[0].uses.map(p=>p.name), ['dependency1-test', 'dependency2-test']) }) it("should be able to use multiple instances of the same plugin", async () => { self.LibResilientConfig = { plugins: [{ name: 'plugin-test', },{ name: 'plugin-test', },{ name: 'plugin-test', }], loggedComponents: [ 'service-worker' ] } var pver = 0 window.LibResilientPluginConstructors.set('plugin-test', ()=>{ pver += 1 return { name: 'plugin-test', description: 'Simple plugin stub.', version: '0.0.' + pver } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(self.LibResilientPlugins.map(p=>p.name), ['plugin-test', 'plugin-test', 'plugin-test']) assertEquals(self.LibResilientPlugins.map(p=>p.version), ['0.0.1', '0.0.2', '0.0.3']) }) it("should error out if all plugins fail", async () => { self.LibResilientConfig = { plugins: [{ name: 'reject-all' }], loggedComponents: [ 'service-worker' ] } window.LibResilientPluginConstructors.set('reject-all', ()=>{ return { name: 'reject-all', description: 'Reject all requests.', version: '0.0.1', fetch: (request, init)=>{ return Promise.reject(request); } } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() let fetch_event = new FetchEvent('test.json') window.dispatchEvent(fetch_event) assertRejects( ()=>{ return fetch_event.waitForResponse() }, fetch_event.request ) }) it("should send clientId back if event.resultingClientId is set", async () => { self.LibResilientConfig = { plugins: [{ name: 'resolve-all' }], loggedComponents: [ 'service-worker' ] } window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: window.fetch } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // we need a FetchEvent with a resultingClientId field set let fetch_event = new FetchEvent('test.json') fetch_event.resultingClientId = 'resulting-client-id-test' // do the fetch and wait for the result that we don't really care about window.dispatchEvent(fetch_event) await fetch_event.waitForResponse() // assert that resulting-client-id-test shows up in messages // posted from the service worker assertSpyCall(window.clients.prototypePostMessage, 0, { args: [{ clientId: "resulting-client-id-test", plugins: [ "resolve-all" ], serviceWorker: "COMMIT_UNKNOWN" }] }) }) it("guessMimeType() should correctly guess content type based on extension by default", async () => { // set things up await import("../../service-worker.js?" + window.test_id); self.pluginName = "service-worker-test" // extensions we support, with associated MIME types let ext_to_mime = new Map([ ['htm', 'text/html'], ['html', 'text/html'], ['css', 'text/css'], ['js', 'text/javascript'], ['json', 'application/json'], ['svg', 'image/svg+xml'], ['ico', 'image/x-icon'], ['gif', 'image/gif'], ['png', 'image/png'], ['jpg', 'image/jpeg'], ['jpeg', 'image/jpeg'], ['jpe', 'image/jpeg'], ['jfif', 'image/jpeg'], ['pjpeg', 'image/jpeg'], ['pjp', 'image/jpeg'], ['webp', 'image/webp'], ['avi', 'video/avi'], ['mp4', 'video/mp4'], ['mp2', 'video/mpeg'], ['mp3', 'audio/mpeg'], ['mpa', 'video/mpeg'], ['pdf', 'application/pdf'], ['txt', 'text/plain'], ['ics', 'text/calendar'], ['jsonld', 'application/ld+json'], ['mjs', 'text/javascript'], ['oga', 'audio/ogg'], ['ogv', 'video/ogg'], ['ogx', 'application/ogg'], ['opus', 'audio/opus'], ['otf', 'font/otf'], ['ts', 'video/mp2t'], ['ttf', 'font/ttf'], ['weba', 'audio/webm'], ['webm', 'video/webm'], ['webp', 'image/webp'], ['woff', 'font/woff'], ['woff2', 'font/woff2'], ['xhtml', 'application/xhtml+xml'], ['xml', 'application/xml'] ]) // check'em all for (let [ext, mime] of ext_to_mime.entries()) { assertEquals(await self.guessMimeType(ext, null), mime) } }) it("should attempt to load the external MIME type sniffing library if 'useMimeSniffingLibrary' is set to true", async () => { self.LibResilientConfig = { plugins: [{ name: 'resolve-all', }], useMimeSniffingLibrary: true, loggedComponents: ['service-worker'] } window.fileType = { fileTypeFromBuffer: (ext, content)=>{ console.log(`fileTypeFromBuffer(${ext}, ${content.length})`) } } window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: fetch } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertSpyCall(self.importScripts, 0, {args: ["./lib/file-type.js"]}) }) it("should default to extension-based MIME sniffing if 'useMimeSniffingLibrary' is set to true but loading the library failed", async () => { self.LibResilientConfig = { plugins: [{ name: 'resolve-all', }], useMimeSniffingLibrary: true, loggedComponents: ['service-worker'] } window.fileType = undefined window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: fetch } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(await self.guessMimeType("png", "test arg 2"), "image/png") }) it("should call the external library function in 'guessMimeType()', passing the second argument, if 'useMimeSniffingLibrary' is set to true", async () => { self.LibResilientConfig = { plugins: [{ name: 'resolve-all', }], useMimeSniffingLibrary: true, loggedComponents: ['service-worker'] } window.fileType = { fileTypeFromBuffer: spy(async (ext, content)=>{ return undefined }) } window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: fetch } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() await self.guessMimeType("test arg 1", "test arg 2") assertSpyCall(window.fileType.fileTypeFromBuffer, 0, {args: ["test arg 2"]}) }) it("should revert to guessing MIME type based on extension if the external library function in 'guessMimeType()' errors out", async () => { self.LibResilientConfig = { plugins: [{ name: 'resolve-all', }], useMimeSniffingLibrary: true, loggedComponents: ['service-worker'] } window.fileType = { fileTypeFromBuffer: spy(async (ext, content)=>{ throw new Error('test error') }) } window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: fetch } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(await self.guessMimeType("gif", "test arg 2"), "image/gif") assertSpyCalls(window.fileType.fileTypeFromBuffer, 1) }) it("should revert to guessing MIME type based on extension if the external library function in 'guessMimeType()' fails to guess the MIME type", async () => { self.LibResilientConfig = { plugins: [{ name: 'resolve-all', }], useMimeSniffingLibrary: true, loggedComponents: ['service-worker'] } window.fileType = { fileTypeFromBuffer: spy(async (ext, content)=>{ return undefined }) } window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: fetch } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(await self.guessMimeType("png", "test arg 2"), "image/png") assertSpyCalls(window.fileType.fileTypeFromBuffer, 1) }) it("should revert to guessing MIME type based on extension if the external library function in 'guessMimeType()' returns an unexpected value type", async () => { self.LibResilientConfig = { plugins: [{ name: 'resolve-all', }], useMimeSniffingLibrary: true, loggedComponents: ['service-worker'] } window.fileType = { fileTypeFromBuffer: spy(async (ext, content)=>{ return "this should not be a string" }) } window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: fetch } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(await self.guessMimeType("jpg", "test arg 2"), "image/jpeg") assertSpyCalls(window.fileType.fileTypeFromBuffer, 1) }) it("should revert to guessing MIME type based on extension if the external library function in 'guessMimeType()' returns an incorrectly formatted object", async () => { self.LibResilientConfig = { plugins: [{ name: 'resolve-all', }], useMimeSniffingLibrary: true, loggedComponents: ['service-worker'] } window.fileType = { fileTypeFromBuffer: spy(async (ext, content)=>{ return {bad: "data"} }) } window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: fetch } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(await self.guessMimeType("txt", "test arg 2"), "text/plain") assertSpyCalls(window.fileType.fileTypeFromBuffer, 1) }) it("should ignore extension if the external library function in 'guessMimeType()' returns valid data", async () => { self.LibResilientConfig = { plugins: [{ name: 'resolve-all', }], useMimeSniffingLibrary: true, loggedComponents: ['service-worker'] } window.fileType = { fileTypeFromBuffer: spy(async (ext, content)=>{ return {ext: "txt", mime: "text/plain"} }) } window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: fetch } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() assertEquals(await self.guessMimeType("png", "test arg 2"), "text/plain") assertSpyCalls(window.fileType.fileTypeFromBuffer, 1) }) it("should not guess a MIME type if both external library function and built-in extension-based heuristic fail", async () => { self.LibResilientConfig = { plugins: [{ name: 'resolve-all', }], useMimeSniffingLibrary: true, loggedComponents: ['service-worker'] } window.fileType = { fileTypeFromBuffer: spy(async (ext, content)=>{ return {bad: "data"} }) } window.LibResilientPluginConstructors.set('resolve-all', ()=>{ return { name: 'resolve-all', description: 'Resolve all requests.', version: '0.0.1', fetch: fetch } }) await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() // if we're not sure, according to RFC 7231 we should not set Content-Type // (or, set it to an empty string) // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.1.5 assertEquals(await self.guessMimeType("no-such-extension", "test arg 2"), "") assertSpyCalls(window.fileType.fileTypeFromBuffer, 1) }) })