import { describe, it, beforeEach, beforeAll } 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"; beforeAll(async ()=>{ window.fetchResponse = [] window.resolvingFetch = (url, init) => { const response = new Response( new Blob( [JSON.stringify(window.fetchResponse[0])], {type: window.fetchResponse[1]} ), { status: 200, statusText: "OK", headers: { 'Last-Modified': 'TestingLastModifiedHeader' }, url: url }); return Promise.resolve(response); } /* * prototype of the plugin init object */ window.initPrototype = { name: 'dnslink-fetch' } }) /** * 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(()=>{ window.fetch = spy(window.resolvingFetch) window.fetchResponse = [ {test: "success"}, "application/json" ] window.init = { ...window.initPrototype } }) describe('browser: dnslink-fetch plugin', async () => { window.LibResilientPluginConstructors = new Map() window.LR = { log: (component, ...items)=>{ console.debug(component + ' :: ', ...items) } } window.fetchResponse = [] window.resolvingFetch = null window.fetch = null await import("../../../plugins/dnslink-fetch/index.js"); it("should register in LibResilientPluginConstructors", () => { assertEquals( LibResilientPluginConstructors.get('dnslink-fetch')(LR, init).name, 'dnslink-fetch' ); }); it("should fail with bad config", () => { init = { name: 'dnslink-fetch', dohProvider: false } assertThrows( ()=>{ return LibResilientPluginConstructors.get('dnslink-fetch')(LR, init) }, Error, 'dohProvider not confgured' ) }); it("should perform a fetch against the default dohProvider endpoint, with default ECS settings", async () => { // this will fail after the fetch() is done // but we only care about the fetch() being done in this test try { await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); } catch (e) {} assertSpyCall( fetch, 0, { args: [ "https://dns.hostux.net/dns-query?name=_dnslink.resilient.is&type=TXT&edns_client_subnet=0.0.0.0/0", {"headers": {"accept": "application/json"}} ] }) }) it("should perform a fetch against the configured dohProvider endpoint, with configured ECS settings", async () => { init.dohProvider = 'https://doh.example.org/resolve-example' init.ecsMasked = false // this will fail after the fetch() is done // but we only care about the fetch() being done in this test try { await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); } catch(e) {} assertSpyCall( fetch, 0, { args: [ "https://doh.example.org/resolve-example?name=_dnslink.resilient.is&type=TXT", {"headers": {"accept": "application/json"}} ] }) }) it("should throw an error if the DoH response is not a valid JSON", async () => { window.fetchResponse = ["not-json", "text/plain"] assertRejects( async ()=>{ return await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); }, Error, 'Response is not a valid JSON' ) }) it("should throw an error if the DoH response is does not have a Status field", async () => { window.fetchResponse = [{test: "success"}, "application/json"] assertRejects( async ()=>{ return await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); }, Error, 'DNS request failure, status code: undefined' ) }) it("should throw an error if the DoH response has Status other than 0", async () => { window.fetchResponse = [{Status: 999}, "application/json"] assertRejects( async ()=>{ return await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); }, Error, 'DNS request failure, status code: 999' ) }) it("should throw an error if the DoH response does not have an Answer field", async () => { window.fetchResponse = [{Status: 0}, "application/json"] assertRejects( async ()=>{ return await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); }, Error, 'DNS response did not contain a valid Answer section' ) }) it("should throw an error if the DoH response's Answer field is not an object", async () => { window.fetchResponse = [{Status: 0, Answer: 'invalid'}, "application/json"] assertRejects( async ()=>{ return await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); }, Error, 'DNS response did not contain a valid Answer section' ) }) it("should throw an error if the DoH response's Answer field is not an Array", async () => { window.fetchResponse = [{Status: 0, Answer: {}}, "application/json"] assertRejects( async ()=>{ return await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); }, Error, 'DNS response did not contain a valid Answer section' ) }) it("should throw an error if the DoH response's Answer field does not contain TXT records", async () => { window.fetchResponse = [{Status: 0, Answer: ['aaa', 'bbb']}, "application/json"] assertRejects( async ()=>{ return await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); }, Error, 'Answer section of the DNS response did not contain any TXT records' ) }) it("should throw an error if the DoH response's Answer elements do not contain valid endpoint data", async () => { window.fetchResponse = [{Status: 0, Answer: [{type: 16}, {type: 16}]}, "application/json"] assertRejects( async ()=>{ return await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); }, Error, 'No TXT record contained http or https endpoint definition' ) }) it("should throw an error if the DoH response's Answer elements do not contain valid endpoints", async () => { window.fetchResponse = [{Status: 0, Answer: [{type: 16, data: 'aaa'}, {type: 16, data: 'bbb'}]}, "application/json"] assertRejects( async ()=>{ return await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); }, Error, 'No TXT record contained http or https endpoint definition' ) }) it("should successfully resolve if the DoH response contains endpoint data", async () => { window.fetchResponse = [ {Status: 0, Answer: [ {type: 16, data: 'dnslink=/https/example.org'}, {type: 16, data: 'dnslink=/http/example.net/some/path'} ]}, "application/json" ] // this might fail after the fetch() is done // but we only care about the fetch() being done in this test try { await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); } catch(e) {} // 1 fetch to resolve DNSLink, // then 2 fetch requests to the two DNSLink-resolved endpoints assertSpyCalls(fetch, 3) assertSpyCall( fetch, 1, { args: [ "https://example.org/test.json", {cache: 'reload'} ] }) assertSpyCall( fetch, 2, { args: [ "http://example.net/some/path/test.json", {cache: 'reload'} ] }) }) it("should fetch the content, trying all DNSLink-resolved endpoints (if fewer or equal to concurrency setting)", async () => { window.fetchResponse = [ {Status: 0, Answer: [ {type: 16, data: 'dnslink=/https/example.org'}, {type: 16, data: 'dnslink=/http/example.net/some/path'} ]}, "application/json" ] const response = await LibResilientPluginConstructors.get('dnslink-fetch')(LR, init).fetch('https://resilient.is/test.json'); // 1 fetch to resolve DNSLink, // then 2 fetch requests to the two DNSLink-resolved endpoints assertSpyCalls(fetch, 3) assertEquals(await response.json(), window.fetchResponse[0]) assertSpyCall( fetch, 1, { args: [ "https://example.org/test.json", {cache: 'reload'} ] }) assertSpyCall( fetch, 2, { args: [ "http://example.net/some/path/test.json", {cache: 'reload'} ] }) }) it("should fetch the content, trying random endpoints out of all DNSLink-resolved endpoints (if more than concurrency setting)", async () => { init.concurrency = 3 window.fetchResponse = [ {Status: 0, Answer: [ {type: 16, data: 'dnslink=/https/example.org'}, {type: 16, data: 'dnslink=/http/example.net/some/path'}, {type: 16, data: 'dnslink=/https/example.net/some/path'}, {type: 16, data: 'dnslink=/https/example.net/some/other/path'} ]}, "application/json" ] const response = await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json'); // 1 fetch to resolve DNSLink, // then 3 fetch requests to random three of the five DNSLink-resolved endpoints assertSpyCalls(fetch, 4) assertEquals(await response.json(), window.fetchResponse[0]) }) it("should pass the Request() init data to fetch() for all used endpoints", async () => { var initTest = { method: "GET", 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 } window.fetchResponse = [ {Status: 0, Answer: [ {type: 16, data: 'dnslink=/https/example.org'}, {type: 16, data: 'dnslink=/http/example.net/some/path'}, {type: 16, data: 'dnslink=/https/example.net/some/path'} ]}, "application/json" ] const response = await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json', initTest); // 1 fetch to resolve DNSLink, // then 3 fetch requests to the three DNSLink-resolved endpoints assertSpyCalls(fetch, 4) assertEquals(await response.json(), window.fetchResponse[0]) assertSpyCall( fetch, 1, { args: [ "https://example.org/test.json", initTest ] }) assertSpyCall( fetch, 2, { args: [ "http://example.net/some/path/test.json", initTest ] }) assertSpyCall( fetch, 3, { args: [ "https://example.net/some/path/test.json", initTest ] }) }) it("should set the LibResilient headers, setting X-LibResilient-ETag based on Last-Modified (if ETag is unavailable in the original response)", async () => { window.fetchResponse = [ {Status: 0, Answer: [ {type: 16, data: 'dnslink=/https/example.org'}, {type: 16, data: 'dnslink=/http/example.net/some/path'} ]}, "application/json"] const response = await LibResilientPluginConstructors.get('dnslink-fetch')(LR, init).fetch('https://resilient.is/test.json'); // 1 fetch to resolve DNSLink, // then 3 fetch requests to the three DNSLink-resolved endpoints assertSpyCalls(fetch, 3) assertEquals(await response.json(), window.fetchResponse[0]) assert(response.headers.has('X-LibResilient-Method')) assert(response.headers.has('X-LibResilient-Etag')) assertEquals(response.headers.get('X-LibResilient-Method'), 'dnslink-fetch') assertEquals(response.headers.get('X-LibResilient-Etag'), 'TestingLastModifiedHeader') }); it("should throw an error when HTTP status is >= 400", async () => { window.resolvingFetch = (url, init) => { if (url.startsWith('https://dns.hostux.net/dns-query')) { const response = new Response( new Blob( [JSON.stringify(fetchResponse[0])], {type: fetchResponse[1]} ), { status: 200, statusText: "OK", headers: { 'Last-Modified': 'TestingLastModifiedHeader' }, url: url }); return Promise.resolve(response); } else { const response = new Response( new Blob( ["Not Found"], {type: "text/plain"} ), { status: 404, statusText: "Not Found", url: url }); return Promise.resolve(response); } } window.fetch = spy(window.resolvingFetch) window.fetchResponse = [ {Status: 0, Answer: [ {type: 16, data: 'dnslink=/https/example.org'}, {type: 16, data: 'dnslink=/http/example.net/some/path'} ]}, "application/json" ] assertRejects( async ()=>{ const response = await LibResilientPluginConstructors .get('dnslink-fetch')(LR, init) .fetch('https://resilient.is/test.json') console.log(response) }, Error, 'HTTP Error:' ) }); })