2022-10-22 01:05:05 +00:00
const makeServiceWorkerEnv = require ( 'service-worker-mock' ) ;
global . fetch = require ( 'node-fetch' ) ;
jest . mock ( 'node-fetch' )
/ *
* we need a Promise . any ( ) polyfill
* so here it is
* https : //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any
*
* TODO : remove once Promise . any ( ) is implemented broadly
* /
if ( typeof Promise . any === 'undefined' ) {
Promise . any = async ( promises ) => {
// Promise.all() is the polar opposite of Promise.any()
// in that it returns as soon as there is a first rejection
// but without it, it returns an array of resolved results
return Promise . all (
promises . map ( p => {
return new Promise ( ( resolve , reject ) =>
// swap reject and resolve, so that we can use Promise.all()
// and get the result we need
Promise . resolve ( p ) . then ( reject , resolve )
) ;
} )
// now, swap errors and values back
) . then (
err => Promise . reject ( err ) ,
val => Promise . resolve ( val )
) ;
} ;
}
describe ( "plugin: dnslink-fetch" , ( ) => {
beforeEach ( ( ) => {
Object . assign ( global , makeServiceWorkerEnv ( ) ) ;
jest . resetModules ( ) ;
global . LibResilientPluginConstructors = new Map ( )
init = {
name : 'dnslink-fetch'
}
LR = {
log : jest . fn ( ( component , ... items ) => {
console . debug ( component + ' :: ' , ... items )
} )
}
global . fetchResponse = [ ]
global . fetch . mockImplementation ( ( url , init ) => {
const response = new Response (
new Blob (
[ JSON . stringify ( fetchResponse [ 0 ] ) ] ,
{ type : fetchResponse [ 1 ] }
) ,
{
status : 200 ,
statusText : "OK" ,
2022-10-22 01:42:37 +00:00
headers : {
'Last-Modified' : 'TestingLastModifiedHeader'
} ,
2022-10-22 01:05:05 +00:00
url : url
} ) ;
return Promise . resolve ( response ) ;
} ) ;
} )
test ( "it should register in LibResilientPluginConstructors" , ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
expect ( LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( ) . name ) . toEqual ( 'dnslink-fetch' ) ;
} ) ;
test ( "it should fail with bad config" , ( ) => {
init = {
name : 'dnslink-fetch' ,
dohProvider : false
}
require ( "../../../plugins/dnslink-fetch/index.js" )
expect . assertions ( 1 )
expect ( ( ) => {
LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init )
} ) . toThrow ( Error ) ;
} ) ;
test ( "it should perform a fetch against the default dohProvider endpoint, with default ECS settings" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
try {
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
} catch ( e ) { }
2022-10-22 11:56:03 +00:00
expect ( global . fetch ) . toHaveBeenCalledWith ( "https://dns.hostux.net/dns-query?name=_dnslink.resilient.is&type=TXT&edns_client_subnet=0.0.0.0/0" , { "headers" : { "accept" : "application/json" } } )
2022-10-22 01:05:05 +00:00
} )
test ( "it should perform a fetch against the configured dohProvider endpoint, with configured ECS settings" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
let init = {
name : 'dnslink-fetch' ,
dohProvider : 'https://doh.example.org/resolve-example' ,
ecsMasked : false
}
try {
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
} catch ( e ) { }
expect ( global . fetch ) . toHaveBeenCalledWith ( "https://doh.example.org/resolve-example?name=_dnslink.resilient.is&type=TXT" , { "headers" : { "accept" : "application/json" } } )
} )
test ( "it should throw an error if the DoH response is not a valid JSON" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
global . fetchResponse = [ "not-json" , "text/plain" ]
expect . assertions ( 1 )
try {
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
} catch ( e ) {
expect ( e ) . toEqual ( new Error ( 'Response is not a valid JSON' ) )
}
} )
test ( "it should throw an error if the DoH response is does not have a Status field" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
global . fetchResponse = [ { test : "success" } , "application/json" ]
expect . assertions ( 1 )
try {
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
} catch ( e ) {
expect ( e ) . toEqual ( new Error ( 'DNS request failure, status code: undefined' ) )
}
} )
test ( "it should throw an error if the DoH response has Status other than 0" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
global . fetchResponse = [ { Status : 999 } , "application/json" ]
expect . assertions ( 1 )
try {
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
} catch ( e ) {
expect ( e ) . toEqual ( new Error ( 'DNS request failure, status code: 999' ) )
}
} )
test ( "it should throw an error if the DoH response does not have an Answer field" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
global . fetchResponse = [ { Status : 0 } , "application/json" ]
expect . assertions ( 1 )
try {
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
} catch ( e ) {
expect ( e ) . toEqual ( new Error ( 'DNS response did not contain a valid Answer section' ) )
}
} )
test ( "it should throw an error if the DoH response's Answer field is not an object" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
global . fetchResponse = [ { Status : 0 , Answer : 'invalid' } , "application/json" ]
expect . assertions ( 1 )
try {
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
} catch ( e ) {
expect ( e ) . toEqual ( new Error ( 'DNS response did not contain a valid Answer section' ) )
}
} )
test ( "it should throw an error if the DoH response's Answer field is not an Array" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
global . fetchResponse = [ { Status : 0 , Answer : { } } , "application/json" ]
expect . assertions ( 1 )
try {
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
} catch ( e ) {
expect ( e ) . toEqual ( new Error ( 'DNS response did not contain a valid Answer section' ) )
}
} )
test ( "it should throw an error if the DoH response's Answer field does not contain TXT records" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
global . fetchResponse = [ { Status : 0 , Answer : [ 'aaa' , 'bbb' ] } , "application/json" ]
expect . assertions ( 1 )
try {
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
} catch ( e ) {
expect ( e ) . toEqual ( new Error ( 'Answer section of the DNS response did not contain any TXT records' ) )
}
} )
test ( "it should throw an error if the DoH response's Answer elements do not contain valid endpoint data" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
global . fetchResponse = [ { Status : 0 , Answer : [ { type : 16 } , { type : 16 } ] } , "application/json" ]
expect . assertions ( 1 )
try {
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
} catch ( e ) {
expect ( e ) . toEqual ( new Error ( 'No TXT record contained http or https endpoint definition' ) )
}
} )
test ( "it should throw an error if the DoH response's Answer elements do not contain valid endpoints" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
global . fetchResponse = [ { Status : 0 , Answer : [ { type : 16 , data : 'aaa' } , { type : 16 , data : 'bbb' } ] } , "application/json" ]
expect . assertions ( 1 )
try {
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
} catch ( e ) {
expect ( e ) . toEqual ( new Error ( 'No TXT record contained http or https endpoint definition' ) )
}
} )
test ( "it should successfully resolve if the DoH response contains endpoint data" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
global . fetchResponse = [ { Status : 0 , Answer : [ { type : 16 , data : 'dnslink=/https/example.org' } , { type : 16 , data : 'dnslink=/http/example.net/some/path' } ] } , "application/json" ]
try {
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
} catch ( e ) { }
expect ( LR . log ) . toHaveBeenCalledWith ( "dnslink-fetch" , "+-- alternative endpoints from DNSLink:\n - " , "https://example.org\n - http://example.net/some/path" )
} )
test ( "it should fetch the content, trying all DNSLink-resolved endpoints (if fewer or equal to concurrency setting)" , async ( ) => {
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
global . 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' ) ;
expect ( fetch ) . toHaveBeenCalledTimes ( 3 ) ; // 1 fetch to resolve DNSLink, then 2 fetch requests to the two DNSLink-resolved endpoints
expect ( fetch ) . toHaveBeenNthCalledWith ( 2 , 'https://example.org/test.json' , { "cache" : "reload" } ) ;
expect ( fetch ) . toHaveBeenNthCalledWith ( 3 , 'http://example.net/some/path/test.json' , { "cache" : "reload" } ) ;
expect ( await response . json ( ) ) . toEqual ( global . fetchResponse [ 0 ] )
expect ( response . url ) . toEqual ( 'https://resilient.is/test.json' )
} )
test ( "it should fetch the content, trying <concurrency> random endpoints out of all DNSLink-resolved endpoints (if more than concurrency setting)" , async ( ) => {
let init = {
name : 'dnslink-fetch' ,
concurrency : 2
}
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
global . 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' ) ;
expect ( fetch ) . toHaveBeenCalledTimes ( 3 ) ; // 1 fetch to resolve DNSLink, then <concurrency> fetch requests to the two DNSLink-resolved endpoints
expect ( await response . json ( ) ) . toEqual ( global . fetchResponse [ 0 ] )
expect ( response . url ) . toEqual ( 'https://resilient.is/test.json' )
} )
2022-10-22 01:42:37 +00:00
test ( "it should pass the Request() init data to fetch() for all used endpoints" , async ( ) => {
2022-10-22 01:05:05 +00:00
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
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
}
2022-10-22 01:42:37 +00:00
global . 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" ]
2022-10-22 01:05:05 +00:00
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' , initTest ) ;
2022-10-22 01:42:37 +00:00
expect ( fetch ) . toHaveBeenCalledTimes ( 4 ) ; // 1 fetch to resolve DNSLink, then <concurrency> (default: 3) fetch requests to the two DNSLink-resolved endpoints
expect ( await response . json ( ) ) . toEqual ( global . fetchResponse [ 0 ] )
expect ( response . url ) . toEqual ( 'https://resilient.is/test.json' )
2022-10-22 01:05:05 +00:00
expect ( fetch ) . toHaveBeenNthCalledWith ( 2 , expect . stringContaining ( '/test.json' ) , initTest ) ;
expect ( fetch ) . toHaveBeenNthCalledWith ( 3 , expect . stringContaining ( '/test.json' ) , initTest ) ;
2022-10-22 01:42:37 +00:00
expect ( fetch ) . toHaveBeenNthCalledWith ( 4 , expect . stringContaining ( '/test.json' ) , initTest ) ;
2022-10-22 01:05:05 +00:00
} )
2022-10-22 01:42:37 +00:00
test ( "it should set the LibResilient headers, setting X-LibResilient-ETag based on Last-Modified (if ETag is unavailable in the original response)" , async ( ) => {
2022-10-22 01:05:05 +00:00
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
2022-10-22 01:42:37 +00:00
global . fetchResponse = [ { Status : 0 , Answer : [ { type : 16 , data : 'dnslink=/https/example.org' } , { type : 16 , data : 'dnslink=/http/example.net/some/path' } ] } , "application/json" ]
2022-10-22 01:05:05 +00:00
const response = await LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ;
expect ( fetch ) . toHaveBeenCalledTimes ( 3 ) ;
2022-10-22 01:42:37 +00:00
expect ( await response . json ( ) ) . toEqual ( global . fetchResponse [ 0 ] )
2022-10-22 01:05:05 +00:00
expect ( response . url ) . toEqual ( 'https://resilient.is/test.json' )
expect ( response . headers . has ( 'X-LibResilient-Method' ) ) . toEqual ( true )
expect ( response . headers . get ( 'X-LibResilient-Method' ) ) . toEqual ( 'dnslink-fetch' )
expect ( response . headers . has ( 'X-LibResilient-Etag' ) ) . toEqual ( true )
expect ( response . headers . get ( 'X-LibResilient-ETag' ) ) . toEqual ( 'TestingLastModifiedHeader' )
} ) ;
2022-10-22 01:42:37 +00:00
test ( "it should throw an error when HTTP status is >= 400" , async ( ) => {
2022-10-22 01:05:05 +00:00
2022-10-22 01:42:37 +00:00
global . fetch . mockImplementation ( ( url , init ) => {
if ( url . startsWith ( 'https://dns.google/resolve' ) ) {
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 {
2022-10-22 01:05:05 +00:00
const response = new Response (
new Blob (
[ "Not Found" ] ,
{ type : "text/plain" }
) ,
{
status : 404 ,
statusText : "Not Found" ,
url : url
} ) ;
return Promise . resolve ( response ) ;
2022-10-22 01:42:37 +00:00
}
} ) ;
2022-10-22 01:05:05 +00:00
require ( "../../../plugins/dnslink-fetch/index.js" ) ;
2022-10-22 01:42:37 +00:00
global . fetchResponse = [ { Status : 0 , Answer : [ { type : 16 , data : 'dnslink=/https/example.org' } , { type : 16 , data : 'dnslink=/http/example.net/some/path' } ] } , "application/json" ]
2022-10-22 01:05:05 +00:00
expect . assertions ( 1 )
expect ( LibResilientPluginConstructors . get ( 'dnslink-fetch' ) ( LR , init ) . fetch ( 'https://resilient.is/test.json' ) ) . rejects . toThrow ( Error )
} ) ;
} ) ;