libresilient/plugins/dnslink-fetch/__tests__/browser.test.js

517 wiersze
18 KiB
JavaScript

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 <concurrency> 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:'
)
});
})