kopia lustrzana https://gitlab.com/rysiekpl/libresilient
517 wiersze
18 KiB
JavaScript
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:'
|
|
)
|
|
});
|
|
})
|