kopia lustrzana https://gitlab.com/rysiekpl/libresilient
Rewriting tests into Deno
rodzic
edd7a0c2ec
commit
34b87e5ad2
|
@ -1,4 +1,2 @@
|
||||||
node_modules/
|
|
||||||
radata/
|
|
||||||
coverage/
|
coverage/
|
||||||
junit.xml
|
junit.xml
|
||||||
|
|
|
@ -1,42 +1,36 @@
|
||||||
# You can override the included template(s) by including variable overrides
|
|
||||||
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
|
|
||||||
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
|
|
||||||
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
|
|
||||||
# Note that environment variables can be set in several places
|
|
||||||
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
|
|
||||||
cache:
|
|
||||||
key: "${CI_COMMIT_REF_SLUG}"
|
|
||||||
paths:
|
|
||||||
- node_modules/
|
|
||||||
|
|
||||||
libresilient-test:
|
libresilient-test:
|
||||||
image: node:17.0
|
image: denoland/deno:debian-1.37.0
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- npm ci --include=dev
|
# preparations
|
||||||
- npx jest --collectCoverage --coverageDirectory="./coverage" --coverage --ci --reporters=default --reporters=jest-junit
|
- apt update && apt-get install -y bsdextrautils
|
||||||
coverage: "/All files[^|]*\\|[^|]*\\s+([\\d\\.]+)/"
|
- deno install --allow-read --allow-write https://deno.land/x/lcov_cobertura/mod.ts
|
||||||
|
# run the tests!
|
||||||
|
- deno test --quiet --allow-read --importmap=./__tests__/importmap.json --coverage=./coverage/ --trace-ops
|
||||||
|
#
|
||||||
|
# sigh! we are loading the service-worker.js script once per each test, and Deno
|
||||||
|
# does not allow us to do it nicely - so we are adding a '?<num>' at the end of
|
||||||
|
# the filename each time.
|
||||||
|
#
|
||||||
|
# this works but causes the coverage data to be generated as if these were actually
|
||||||
|
# different files, so we need this little hack here to fix that
|
||||||
|
- sed -i -r -e 's/service-worker\.js\?[0-9]+"/service-worker.js"/' coverage/*.json
|
||||||
|
# convert coverage to cobertura XML by way of LCOV
|
||||||
|
- deno coverage --lcov ./coverage/ > ./coverage/coverage.lcov
|
||||||
|
- /usr/local/bin/lcov_cobertura coverage/coverage.lcov > coverage/coverage.xml
|
||||||
|
# pretty-print some stats directly in the job log
|
||||||
|
- deno coverage ./coverage/ | grep '^cover' | column -t -R 4,5
|
||||||
|
- egrep '^<coverage' coverage/coverage.xml | tr ' ' '\n' | egrep '(line|branch)-rate' | tr -d '>"' | tr '=' ' ' | awk '{ printf "%s %5.2f%%\n", $1, $2*100 }' - | sed -r -e 's/branch-rate/Branches/' -r -e 's/line-rate/Lines/' | column -t -R2
|
||||||
|
coverage: '/^Lines +[0-9]+\.[0-9]{2}%$/'
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
junit:
|
|
||||||
- junit.xml
|
|
||||||
coverage_report:
|
coverage_report:
|
||||||
coverage_format: cobertura
|
coverage_format: cobertura
|
||||||
path: coverage/cobertura-coverage.xml
|
path: coverage/coverage.xml
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
- linux
|
- linux
|
||||||
|
|
||||||
lrcli-test:
|
|
||||||
image: denoland/deno:1.28.3
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- deno test --quiet --allow-read --import-map=__denotests__/importmap.json __denotests__/
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- linux
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
|
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
import {
|
|
||||||
assert,
|
|
||||||
assertThrows,
|
|
||||||
assertRejects,
|
|
||||||
assertEquals
|
|
||||||
} from "https://deno.land/std@0.167.0/testing/asserts.ts";
|
|
||||||
|
|
||||||
Deno.test("plugin loads", async () => {
|
|
||||||
const bi = await import('../../plugins/basic-integrity/cli.js')
|
|
||||||
assert("name" in bi)
|
|
||||||
assert(bi.name == "basic-integrity")
|
|
||||||
assert("description" in bi)
|
|
||||||
assert("actions" in bi)
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test("get-integrity action defined", async () => {
|
|
||||||
const bi = await import('../../plugins/basic-integrity/cli.js')
|
|
||||||
assert("get-integrity" in bi.actions)
|
|
||||||
const gi = bi.actions["get-integrity"]
|
|
||||||
assert("run" in gi)
|
|
||||||
assert("description" in gi)
|
|
||||||
assert("arguments" in gi)
|
|
||||||
const gia = gi.arguments
|
|
||||||
assert("_" in gia)
|
|
||||||
assert("algorithm" in gia)
|
|
||||||
assert("output" in gia)
|
|
||||||
assert("name" in gia._)
|
|
||||||
assert("description" in gia._)
|
|
||||||
assert("description" in gia.algorithm)
|
|
||||||
assert("collect" in gia.algorithm)
|
|
||||||
assert(gia.algorithm.collect)
|
|
||||||
assert("string" in gia.algorithm)
|
|
||||||
assert(gia.algorithm.string)
|
|
||||||
assert("description" in gia.output)
|
|
||||||
assert("collect" in gia.output)
|
|
||||||
assert(!gia.output.collect)
|
|
||||||
assert("string" in gia.output)
|
|
||||||
assert(gia.output.string)
|
|
||||||
});
|
|
||||||
|
|
||||||
// this is a separate test in order to catch any changing defaults
|
|
||||||
Deno.test("get-integrity action defaults", async () => {
|
|
||||||
const bi = await import('../../plugins/basic-integrity/cli.js')
|
|
||||||
const gia = bi.actions["get-integrity"].arguments
|
|
||||||
assert("default" in gia.algorithm)
|
|
||||||
assert(gia.algorithm.default == "SHA-256")
|
|
||||||
assert("default" in gia.output)
|
|
||||||
assert(gia.output.default == "json")
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test("get-integrity verifies arguments are sane", async () => {
|
|
||||||
const bi = await import('../../plugins/basic-integrity/cli.js')
|
|
||||||
const gi = bi.actions["get-integrity"]
|
|
||||||
assertRejects(gi.run, Error, "Expected non-empty list of files to generate digests of.")
|
|
||||||
assertRejects(async ()=>{
|
|
||||||
await gi.run(['no-such-file'])
|
|
||||||
}, Error, "No such file or directory")
|
|
||||||
assertRejects(async ()=>{
|
|
||||||
await gi.run(['irrelevant'], [])
|
|
||||||
}, Error, "Expected non-empty list of algorithms to use.")
|
|
||||||
assertRejects(async ()=>{
|
|
||||||
await gi.run(['irrelevant'], ['SHA-384'], false)
|
|
||||||
}, Error, "Expected either 'json' or 'text' as output type to generate.")
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test("get-integrity handles paths in a sane way", async () => {
|
|
||||||
const bi = await import('../../plugins/basic-integrity/cli.js')
|
|
||||||
const gi = bi.actions["get-integrity"]
|
|
||||||
assertEquals(await gi.run(['./']), '{}')
|
|
||||||
assertEquals(await gi.run(['./__denotests__/mocks/hello.txt']), '{"./__denotests__/mocks/hello.txt":["sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="]}')
|
|
||||||
assertEquals(await gi.run(['./', './__denotests__/mocks/hello.txt']), '{"./__denotests__/mocks/hello.txt":["sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="]}')
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test("get-integrity handles algos argument in a sane way", async () => {
|
|
||||||
const bi = await import('../../plugins/basic-integrity/cli.js')
|
|
||||||
const gi = bi.actions["get-integrity"]
|
|
||||||
assertRejects(async ()=>{
|
|
||||||
await gi.run(['./__denotests__/mocks/hello.txt'], ['BAD-ALG'])
|
|
||||||
}, Error, 'Unrecognized algorithm name')
|
|
||||||
assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-256']), '{"./__denotests__/mocks/hello.txt":["sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="]}')
|
|
||||||
assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-384']), '{"./__denotests__/mocks/hello.txt":["sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9"]}')
|
|
||||||
assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-512']), '{"./__denotests__/mocks/hello.txt":["sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="]}')
|
|
||||||
assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-256', 'SHA-384', 'SHA-512']), '{"./__denotests__/mocks/hello.txt":["sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=","sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9","sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="]}')
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test("get-integrity handles output argument in a sane way", async () => {
|
|
||||||
const bi = await import('../../plugins/basic-integrity/cli.js')
|
|
||||||
const gi = bi.actions["get-integrity"]
|
|
||||||
assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-256'], 'text'), './__denotests__/mocks/hello.txt: sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=\n')
|
|
||||||
assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-384'], 'text'), './__denotests__/mocks/hello.txt: sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9\n')
|
|
||||||
assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-512'], 'text'), './__denotests__/mocks/hello.txt: sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==\n')
|
|
||||||
assertEquals(await gi.run(['./__denotests__/mocks/hello.txt'], ['SHA-256', 'SHA-384', 'SHA-512'], 'text'), './__denotests__/mocks/hello.txt: sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek= sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9 sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==\n')
|
|
||||||
});
|
|
|
@ -1,297 +0,0 @@
|
||||||
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: alt-fetch", () => {
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
Object.assign(global, makeServiceWorkerEnv());
|
|
||||||
jest.resetModules();
|
|
||||||
global.LibResilientPluginConstructors = new Map()
|
|
||||||
init = {
|
|
||||||
name: 'alt-fetch',
|
|
||||||
endpoints: [
|
|
||||||
'https://alt.resilient.is/test.json',
|
|
||||||
'https://error.resilientis/test.json',
|
|
||||||
'https://timeout.resilientis/test.json'
|
|
||||||
]}
|
|
||||||
LR = {
|
|
||||||
log: (component, ...items)=>{
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should register in LibResilientPluginConstructors", () => {
|
|
||||||
require("../../../plugins/alt-fetch/index.js");
|
|
||||||
expect(LibResilientPluginConstructors.get('alt-fetch')().name).toEqual('alt-fetch');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should fail with bad config", () => {
|
|
||||||
init = {
|
|
||||||
name: 'alt-fetch',
|
|
||||||
endpoints: "this is incorrect"
|
|
||||||
}
|
|
||||||
require("../../../plugins/alt-fetch/index.js")
|
|
||||||
expect.assertions(1)
|
|
||||||
expect(()=>{
|
|
||||||
LibResilientPluginConstructors.get('alt-fetch')(LR, init)
|
|
||||||
}).toThrow(Error);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should fetch the content, trying all configured endpoints (if fewer or equal to concurrency setting)", async () => {
|
|
||||||
require("../../../plugins/alt-fetch/index.js");
|
|
||||||
|
|
||||||
global.fetch.mockImplementation((url, init) => {
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(3);
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should fetch the content, trying <concurrency> random endpoints out of all configured (if more than concurrency setting)", async () => {
|
|
||||||
|
|
||||||
init = {
|
|
||||||
name: 'alt-fetch',
|
|
||||||
endpoints: [
|
|
||||||
'https://alt.resilient.is/',
|
|
||||||
'https://error.resilient.is/',
|
|
||||||
'https://timeout.resilient.is/',
|
|
||||||
'https://alt2.resilient.is/',
|
|
||||||
'https://alt3.resilient.is/',
|
|
||||||
'https://alt4.resilient.is/'
|
|
||||||
]}
|
|
||||||
|
|
||||||
require("../../../plugins/alt-fetch/index.js");
|
|
||||||
|
|
||||||
global.fetch.mockImplementation((url, init) => {
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(3);
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should fetch the content, trying all endpoints (if fewer than concurrency setting)", async () => {
|
|
||||||
|
|
||||||
init = {
|
|
||||||
name: 'alt-fetch',
|
|
||||||
endpoints: [
|
|
||||||
'https://alt.resilient.is/',
|
|
||||||
'https://error.resilient.is/'
|
|
||||||
]}
|
|
||||||
|
|
||||||
require("../../../plugins/alt-fetch/index.js");
|
|
||||||
|
|
||||||
global.fetch.mockImplementation((url, init) => {
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(2);
|
|
||||||
expect(fetch).toHaveBeenNthCalledWith(1, 'https://alt.resilient.is/test.json', {"cache": "reload"});
|
|
||||||
expect(fetch).toHaveBeenNthCalledWith(2, 'https://error.resilient.is/test.json', {"cache": "reload"});
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should pass the Request() init data to fetch() for all used endpoints", async () => {
|
|
||||||
|
|
||||||
init = {
|
|
||||||
name: 'alt-fetch',
|
|
||||||
endpoints: [
|
|
||||||
'https://alt.resilient.is/',
|
|
||||||
'https://error.resilient.is/',
|
|
||||||
'https://timeout.resilient.is/',
|
|
||||||
'https://alt2.resilient.is/',
|
|
||||||
'https://alt3.resilient.is/',
|
|
||||||
'https://alt4.resilient.is/'
|
|
||||||
]}
|
|
||||||
|
|
||||||
require("../../../plugins/alt-fetch/index.js");
|
|
||||||
|
|
||||||
global.fetch.mockImplementation((url, init) => {
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json', initTest);
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(3);
|
|
||||||
expect(fetch).toHaveBeenNthCalledWith(1, expect.stringContaining('/test.json'), initTest);
|
|
||||||
expect(fetch).toHaveBeenNthCalledWith(2, expect.stringContaining('/test.json'), initTest);
|
|
||||||
expect(fetch).toHaveBeenNthCalledWith(3, expect.stringContaining('/test.json'), initTest);
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should set the LibResilient headers", async () => {
|
|
||||||
require("../../../plugins/alt-fetch/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(3);
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
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('alt-fetch')
|
|
||||||
expect(response.headers.has('X-LibResilient-Etag')).toEqual(true)
|
|
||||||
expect(response.headers.get('X-LibResilient-ETag')).toEqual('TestingETagHeader')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should set the LibResilient ETag based on Last-Modified header (if ETag is not available in the original response)", async () => {
|
|
||||||
require("../../../plugins/alt-fetch/index.js");
|
|
||||||
|
|
||||||
global.fetch.mockImplementation((url, init) => {
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'Last-Modified': 'TestingLastModifiedHeader'
|
|
||||||
},
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(3);
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
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('alt-fetch')
|
|
||||||
expect(response.headers.has('X-LibResilient-Etag')).toEqual(true)
|
|
||||||
expect(response.headers.get('X-LibResilient-ETag')).toEqual('TestingLastModifiedHeader')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error when HTTP status is >= 400", async () => {
|
|
||||||
|
|
||||||
global.fetch.mockImplementation((url, init) => {
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
["Not Found"],
|
|
||||||
{type: "text/plain"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 404,
|
|
||||||
statusText: "Not Found",
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
require("../../../plugins/alt-fetch/index.js");
|
|
||||||
|
|
||||||
expect.assertions(1)
|
|
||||||
expect(LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json')).rejects.toThrow(Error)
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,168 +0,0 @@
|
||||||
const makeServiceWorkerEnv = require('service-worker-mock');
|
|
||||||
|
|
||||||
global.fetch = require('node-fetch');
|
|
||||||
jest.mock('node-fetch')
|
|
||||||
|
|
||||||
global.fetch.mockImplementation((url, init) => {
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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: any-of", () => {
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
Object.assign(global, makeServiceWorkerEnv());
|
|
||||||
jest.resetModules();
|
|
||||||
global.LibResilientPluginConstructors = new Map()
|
|
||||||
LR = {
|
|
||||||
log: (component, ...items)=>{
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
require("../../../plugins/fetch/index.js");
|
|
||||||
init = {
|
|
||||||
name: 'any-of',
|
|
||||||
uses: [
|
|
||||||
LibResilientPluginConstructors.get('fetch')(LR),
|
|
||||||
{
|
|
||||||
name: 'reject-all',
|
|
||||||
description: 'Rejects all',
|
|
||||||
version: '0.0.1',
|
|
||||||
fetch: url=>Promise.reject('Reject All!')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
self.log = function(component, ...items) {
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should register in LibResilientPluginConstructors", () => {
|
|
||||||
require("../../../plugins/any-of/index.js");
|
|
||||||
expect(LibResilientPluginConstructors.get('any-of')(LR, init).name).toEqual('any-of');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error when there aren't any wrapped plugins configured", async () => {
|
|
||||||
require("../../../plugins/any-of/index.js");
|
|
||||||
init = {
|
|
||||||
name: 'any-of',
|
|
||||||
uses: []
|
|
||||||
}
|
|
||||||
|
|
||||||
expect.assertions(2);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('any-of')(LR, init).fetch('https://resilient.is/test.json')
|
|
||||||
} catch (e) {
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('No wrapped plugins configured!')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should return data from a wrapped plugin", async () => {
|
|
||||||
require("../../../plugins/any-of/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('any-of')(LR, init).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalled();
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should pass Request() init data onto wrapped plugins", async () => {
|
|
||||||
require("../../../plugins/any-of/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
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('any-of')(LR, init).fetch('https://resilient.is/test.json', initTest);
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalled();
|
|
||||||
expect(fetch).toHaveBeenCalledWith('https://resilient.is/test.json', initTest);
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error when HTTP status is >= 400", async () => {
|
|
||||||
|
|
||||||
global.fetch.mockImplementation((url, init) => {
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
["Not Found"],
|
|
||||||
{type: "text/plain"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 404,
|
|
||||||
statusText: "Not Found",
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
require("../../../plugins/any-of/index.js");
|
|
||||||
|
|
||||||
expect.assertions(2);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('any-of')(LR, init).fetch('https://resilient.is/test.json')
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Array) {
|
|
||||||
expect(e[0].toString()).toMatch('Error')
|
|
||||||
} else {
|
|
||||||
expect(e).toBeInstanceOf(AggregateError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(fetch).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,181 +0,0 @@
|
||||||
const makeServiceWorkerEnv = require('service-worker-mock');
|
|
||||||
|
|
||||||
describe("plugin: basic-integrity", () => {
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
Object.assign(global, makeServiceWorkerEnv());
|
|
||||||
jest.resetModules();
|
|
||||||
|
|
||||||
global.LibResilientPluginConstructors = new Map()
|
|
||||||
LR = {
|
|
||||||
log: (component, ...items)=>{
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
global.resolvingFetch = jest.fn((url, init)=>{
|
|
||||||
return Promise.resolve(
|
|
||||||
new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: url
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
init = {
|
|
||||||
name: 'basic-integrity',
|
|
||||||
uses: [
|
|
||||||
{
|
|
||||||
name: 'resolve-all',
|
|
||||||
description: 'Resolves all',
|
|
||||||
version: '0.0.1',
|
|
||||||
fetch: resolvingFetch
|
|
||||||
}
|
|
||||||
],
|
|
||||||
integrity: {
|
|
||||||
"https://resilient.is/test.json": "sha384-kn5dhxz4RpBmx7xC7Dmq2N43PclV9U/niyh+4Km7oz5W0FaWdz3Op+3K0Qxz8y3z"
|
|
||||||
},
|
|
||||||
requireIntegrity: true
|
|
||||||
}
|
|
||||||
self.log = function(component, ...items) {
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should register in LibResilientPluginConstructors", () => {
|
|
||||||
require("../../../plugins/basic-integrity/index.js");
|
|
||||||
expect(LibResilientPluginConstructors.get('basic-integrity')(LR, init).name).toEqual('basic-integrity');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error when there aren't any wrapped plugins configured", async () => {
|
|
||||||
require("../../../plugins/basic-integrity/index.js");
|
|
||||||
init = {
|
|
||||||
name: 'basic-integrity',
|
|
||||||
uses: []
|
|
||||||
}
|
|
||||||
|
|
||||||
expect.assertions(2);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('basic-integrity')(LR, init).fetch('https://resilient.is/test.json')
|
|
||||||
} catch (e) {
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('Expected exactly one plugin to wrap')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error when there are more than one wrapped plugins configured", async () => {
|
|
||||||
require("../../../plugins/basic-integrity/index.js");
|
|
||||||
init = {
|
|
||||||
name: 'basic-integrity',
|
|
||||||
uses: [{
|
|
||||||
name: 'plugin-1'
|
|
||||||
},{
|
|
||||||
name: 'plugin-2'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
expect.assertions(2);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('basic-integrity')(LR, init).fetch('https://resilient.is/test.json')
|
|
||||||
} catch (e) {
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('Expected exactly one plugin to wrap')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
test("it should return data from the wrapped plugin", async () => {
|
|
||||||
require("../../../plugins/basic-integrity/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('basic-integrity')(LR, init).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalled();
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
test("it should provide the wrapped plugin with integrity data for a configured URL", async () => {
|
|
||||||
require("../../../plugins/basic-integrity/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('basic-integrity')(LR, init).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledWith(
|
|
||||||
'https://resilient.is/test.json',
|
|
||||||
{
|
|
||||||
integrity: init.integrity['https://resilient.is/test.json']
|
|
||||||
});
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should error out for an URL with no integrity data, when requireIntegrity is true", async () => {
|
|
||||||
require("../../../plugins/basic-integrity/index.js");
|
|
||||||
|
|
||||||
expect.assertions(3)
|
|
||||||
try {
|
|
||||||
const response = await LibResilientPluginConstructors.get('basic-integrity')(LR, init).fetch('https://resilient.is/test2.json');
|
|
||||||
} catch (e) {
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('Integrity data required but not provided for')
|
|
||||||
}
|
|
||||||
expect(resolvingFetch).not.toHaveBeenCalled()
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should return data from the wrapped plugin with no integrity data if requireIntegrity is false", async () => {
|
|
||||||
require("../../../plugins/basic-integrity/index.js");
|
|
||||||
|
|
||||||
init.integrity = {}
|
|
||||||
init.requireIntegrity = false
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('basic-integrity')(LR, init).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalled();
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/test.json', {});
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should return data from the wrapped plugin with no integrity data configured when requireIntegrity is true and integrity data is provided in Request() init data", async () => {
|
|
||||||
require("../../../plugins/basic-integrity/index.js");
|
|
||||||
|
|
||||||
init.integrity = {}
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors
|
|
||||||
.get('basic-integrity')(LR, init)
|
|
||||||
.fetch('https://resilient.is/test.json', {
|
|
||||||
integrity: "sha256-Aj9x0DWq9GUL1L8HibLCMa8YLKnV7IYAfpYurqrFwiQ="
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalled();
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/test.json', {integrity: "sha256-Aj9x0DWq9GUL1L8HibLCMa8YLKnV7IYAfpYurqrFwiQ="});
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should return data from the wrapped plugin with integrity data both configured and coming from Request() init", async () => {
|
|
||||||
require("../../../plugins/basic-integrity/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors
|
|
||||||
.get('basic-integrity')(LR, init)
|
|
||||||
.fetch('https://resilient.is/test.json', {
|
|
||||||
integrity: "sha256-Aj9x0DWq9GUL1L8HibLCMa8YLKnV7IYAfpYurqrFwiQ="
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalled();
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/test.json', {integrity: "sha256-Aj9x0DWq9GUL1L8HibLCMa8YLKnV7IYAfpYurqrFwiQ= sha384-kn5dhxz4RpBmx7xC7Dmq2N43PclV9U/niyh+4Km7oz5W0FaWdz3Op+3K0Qxz8y3z"});
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,279 +0,0 @@
|
||||||
/**
|
|
||||||
* @jest-environment jsdom
|
|
||||||
*/
|
|
||||||
|
|
||||||
const makeServiceWorkerEnv = require('service-worker-mock');
|
|
||||||
|
|
||||||
global.fetch = require('node-fetch');
|
|
||||||
jest.mock('node-fetch')
|
|
||||||
|
|
||||||
global.fetch.mockImplementation((url, init) => {
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("plugin: cache", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
Object.assign(global, makeServiceWorkerEnv());
|
|
||||||
jest.resetModules();
|
|
||||||
global.LibResilientPluginConstructors = new Map()
|
|
||||||
LR = {
|
|
||||||
log: (component, ...items)=>{
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should register in LibResilientPluginConstructors", () => {
|
|
||||||
require("../../../plugins/cache/index.js");
|
|
||||||
expect(LibResilientPluginConstructors.get('cache')().name).toEqual('cache');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should error out if resource is not found", () => {
|
|
||||||
require("../../../plugins/cache/index.js");
|
|
||||||
|
|
||||||
expect.assertions(1)
|
|
||||||
return expect(LibResilientPluginConstructors.get('cache')(LR).fetch('https://resilient.is/test.json')).rejects.toThrow(Error)
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should stash a url successfully", () => {
|
|
||||||
require("../../../plugins/cache/index.js");
|
|
||||||
expect.assertions(7);
|
|
||||||
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
|
||||||
return cachePlugin.stash('https://resilient.is/test.json').then((result)=>{
|
|
||||||
expect(result).toEqual(undefined)
|
|
||||||
return cachePlugin.fetch('https://resilient.is/test.json')
|
|
||||||
}).then(fetchResult => {
|
|
||||||
expect(fetchResult.status).toEqual(200)
|
|
||||||
expect(fetchResult.statusText).toEqual('OK')
|
|
||||||
expect(fetchResult.url).toEqual('https://resilient.is/test.json')
|
|
||||||
expect(fetchResult.headers.has('Etag')).toEqual(true)
|
|
||||||
expect(fetchResult.headers.get('ETag')).toEqual('TestingETagHeader')
|
|
||||||
return fetchResult.json().then(json => {
|
|
||||||
expect(json).toEqual({ test: "success" })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should clear a url successfully", () => {
|
|
||||||
require("../../../plugins/cache/index.js");
|
|
||||||
expect.assertions(3);
|
|
||||||
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
|
||||||
return cachePlugin.stash('https://resilient.is/test.json').then((result)=>{
|
|
||||||
expect(result).toBe(undefined)
|
|
||||||
return cachePlugin.unstash('https://resilient.is/test.json')
|
|
||||||
}).then(result => {
|
|
||||||
expect(result).toEqual(true)
|
|
||||||
return expect(cachePlugin.fetch('https://resilient.is/test.json')).rejects.toThrow(Error)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should stash an array of urls successfully", () => {
|
|
||||||
require("../../../plugins/cache/index.js");
|
|
||||||
expect.assertions(13);
|
|
||||||
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
|
||||||
return cachePlugin.stash(['https://resilient.is/test.json', 'https://resilient.is/test2.json']).then((result)=>{
|
|
||||||
expect(result).toEqual([undefined, undefined])
|
|
||||||
return cachePlugin.fetch('https://resilient.is/test.json')
|
|
||||||
}).then(fetchResult => {
|
|
||||||
expect(fetchResult.status).toEqual(200)
|
|
||||||
expect(fetchResult.statusText).toEqual('OK')
|
|
||||||
expect(fetchResult.url).toEqual('https://resilient.is/test.json')
|
|
||||||
expect(fetchResult.headers.has('Etag')).toEqual(true)
|
|
||||||
expect(fetchResult.headers.get('ETag')).toEqual('TestingETagHeader')
|
|
||||||
return fetchResult.json().then(json => {
|
|
||||||
expect(json).toEqual({ test: "success" })
|
|
||||||
})
|
|
||||||
}).then(() => {
|
|
||||||
return cachePlugin.fetch('https://resilient.is/test2.json')
|
|
||||||
}).then(fetchResult => {
|
|
||||||
expect(fetchResult.status).toEqual(200)
|
|
||||||
expect(fetchResult.statusText).toEqual('OK')
|
|
||||||
expect(fetchResult.url).toEqual('https://resilient.is/test2.json')
|
|
||||||
expect(fetchResult.headers.has('Etag')).toEqual(true)
|
|
||||||
expect(fetchResult.headers.get('ETag')).toEqual('TestingETagHeader')
|
|
||||||
return fetchResult.json().then(json => {
|
|
||||||
expect(json).toEqual({ test: "success" })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should clear an array of urls successfully", () => {
|
|
||||||
require("../../../plugins/cache/index.js");
|
|
||||||
expect.assertions(4);
|
|
||||||
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
|
||||||
return cachePlugin.stash(['https://resilient.is/test.json', 'https://resilient.is/test2.json']).then((result)=>{
|
|
||||||
expect(result).toEqual([undefined, undefined])
|
|
||||||
return cachePlugin.unstash(['https://resilient.is/test.json', 'https://resilient.is/test2.json'])
|
|
||||||
}).then(result => {
|
|
||||||
expect(result).toEqual([true, true])
|
|
||||||
return expect(cachePlugin.fetch('https://resilient.is/test.json')).rejects.toThrow(Error)
|
|
||||||
}).then(()=>{
|
|
||||||
return expect(cachePlugin.fetch('https://resilient.is/test2.json')).rejects.toThrow(Error)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
test("it should error out when stashing a Response without a url/key", () => {
|
|
||||||
require("../../../plugins/cache/index.js");
|
|
||||||
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
response.url=''
|
|
||||||
|
|
||||||
expect.assertions(1);
|
|
||||||
return expect(LibResilientPluginConstructors.get('cache')(LR).stash(response)).rejects.toThrow(Error)
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should stash a Response successfully", () => {
|
|
||||||
require("../../../plugins/cache/index.js");
|
|
||||||
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: 'https://resilient.is/test.json'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect.assertions(7);
|
|
||||||
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
|
||||||
return cachePlugin.stash(response).then((result)=>{
|
|
||||||
expect(result).toEqual(undefined)
|
|
||||||
return cachePlugin.fetch('https://resilient.is/test.json')
|
|
||||||
}).then(fetchResult => {
|
|
||||||
expect(fetchResult.status).toEqual(200)
|
|
||||||
expect(fetchResult.statusText).toEqual('OK')
|
|
||||||
expect(fetchResult.url).toEqual('https://resilient.is/test.json')
|
|
||||||
expect(fetchResult.headers.has('Etag')).toEqual(true)
|
|
||||||
expect(fetchResult.headers.get('ETag')).toEqual('TestingETagHeader')
|
|
||||||
return fetchResult.json().then(json => {
|
|
||||||
expect(json).toEqual({ test: "success" })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should stash a Response with an explicit key successfully", () => {
|
|
||||||
require("../../../plugins/cache/index.js");
|
|
||||||
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: 'https://resilient.is/test.json'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect.assertions(7);
|
|
||||||
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
|
||||||
return cachePlugin.stash(response, 'special-key').then((result)=>{
|
|
||||||
expect(result).toEqual(undefined)
|
|
||||||
return cachePlugin.fetch('special-key')
|
|
||||||
}).then(fetchResult => {
|
|
||||||
expect(fetchResult.status).toEqual(200)
|
|
||||||
expect(fetchResult.statusText).toEqual('OK')
|
|
||||||
expect(fetchResult.url).toEqual('https://resilient.is/test.json')
|
|
||||||
expect(fetchResult.headers.has('Etag')).toEqual(true)
|
|
||||||
expect(fetchResult.headers.get('ETag')).toEqual('TestingETagHeader')
|
|
||||||
return fetchResult.json().then(json => {
|
|
||||||
expect(json).toEqual({ test: "success" })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should stash a Response with no url set but with an explicit key successfully", () => {
|
|
||||||
require("../../../plugins/cache/index.js");
|
|
||||||
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
response.url = ''
|
|
||||||
|
|
||||||
expect.assertions(6);
|
|
||||||
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
|
||||||
return cachePlugin.stash(response, 'special-key').then((result)=>{
|
|
||||||
expect(result).toEqual(undefined)
|
|
||||||
return cachePlugin.fetch('special-key')
|
|
||||||
}).then(fetchResult => {
|
|
||||||
expect(fetchResult.status).toEqual(200)
|
|
||||||
expect(fetchResult.statusText).toEqual('OK')
|
|
||||||
expect(fetchResult.headers.has('Etag')).toEqual(true)
|
|
||||||
expect(fetchResult.headers.get('ETag')).toEqual('TestingETagHeader')
|
|
||||||
return fetchResult.json().then(json => {
|
|
||||||
expect(json).toEqual({ test: "success" })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should clear a Response successfully", () => {
|
|
||||||
require("../../../plugins/cache/index.js");
|
|
||||||
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: 'https://resilient.is/test.json'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect.assertions(3);
|
|
||||||
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
|
||||||
return cachePlugin.stash(response).then((result)=>{
|
|
||||||
expect(result).toBe(undefined)
|
|
||||||
return cachePlugin.unstash(response)
|
|
||||||
}).then(result => {
|
|
||||||
expect(result).toEqual(true)
|
|
||||||
return expect(cachePlugin.fetch('https://resilient.is/test.json')).rejects.toThrow(Error)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,343 +0,0 @@
|
||||||
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",
|
|
||||||
headers: {
|
|
||||||
'Last-Modified': 'TestingLastModifiedHeader'
|
|
||||||
},
|
|
||||||
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) {}
|
|
||||||
|
|
||||||
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"}})
|
|
||||||
})
|
|
||||||
|
|
||||||
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')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should pass the Request() init data to fetch() for all used endpoints", async () => {
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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', initTest);
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenNthCalledWith(2, expect.stringContaining('/test.json'), initTest);
|
|
||||||
expect(fetch).toHaveBeenNthCalledWith(3, expect.stringContaining('/test.json'), initTest);
|
|
||||||
expect(fetch).toHaveBeenNthCalledWith(4, expect.stringContaining('/test.json'), initTest);
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should set the LibResilient headers, setting X-LibResilient-ETag based on Last-Modified (if ETag is unavailable in the original response)", 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);
|
|
||||||
expect(await response.json()).toEqual(global.fetchResponse[0])
|
|
||||||
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')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error when HTTP status is >= 400", async () => {
|
|
||||||
|
|
||||||
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 {
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
["Not Found"],
|
|
||||||
{type: "text/plain"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 404,
|
|
||||||
statusText: "Not Found",
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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"]
|
|
||||||
expect.assertions(1)
|
|
||||||
expect(LibResilientPluginConstructors.get('dnslink-fetch')(LR, init).fetch('https://resilient.is/test.json')).rejects.toThrow(Error)
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,206 +0,0 @@
|
||||||
const makeServiceWorkerEnv = require('service-worker-mock');
|
|
||||||
|
|
||||||
describe("plugin: dnslink-ipfs", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
Object.assign(global, makeServiceWorkerEnv());
|
|
||||||
jest.resetModules();
|
|
||||||
init = {
|
|
||||||
name: 'dnslink-ipfs',
|
|
||||||
gunPubkey: 'stub'
|
|
||||||
}
|
|
||||||
global.LibResilientPluginConstructors = new Map()
|
|
||||||
LR = {
|
|
||||||
log: jest.fn((component, ...items)=>{
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
global.Ipfs = {
|
|
||||||
ipfsFixtureAddress: 'QmiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFS',
|
|
||||||
create: ()=>{
|
|
||||||
return Promise.resolve({
|
|
||||||
cat: (path)=>{
|
|
||||||
return {
|
|
||||||
sourceUsed: false,
|
|
||||||
next: ()=>{
|
|
||||||
if (path.endsWith('nonexistent.path')) {
|
|
||||||
throw new Error('Error: file does not exist')
|
|
||||||
}
|
|
||||||
var prevSourceUsed = self.sourceUsed
|
|
||||||
self.sourceUsed = true
|
|
||||||
var val = undefined
|
|
||||||
if (!prevSourceUsed) {
|
|
||||||
var val = Uint8Array.from(
|
|
||||||
Array
|
|
||||||
.from(JSON.stringify({
|
|
||||||
test: "success",
|
|
||||||
path: path
|
|
||||||
}))
|
|
||||||
.map(
|
|
||||||
letter => letter.charCodeAt(0)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return Promise.resolve({
|
|
||||||
done: prevSourceUsed,
|
|
||||||
value: val
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
resolve: (path)=>{
|
|
||||||
var result = path.replace(
|
|
||||||
'/ipns/' + self.location.origin.replace('https://', ''),
|
|
||||||
'/ipfs/' + Ipfs.ipfsFixtureAddress
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
next: ()=> {
|
|
||||||
return Promise.resolve({
|
|
||||||
done: false,
|
|
||||||
value: result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.Ipfs = global.Ipfs
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should register in LibResilientPlugins", () => {
|
|
||||||
require("../../../plugins/dnslink-ipfs/index.js");
|
|
||||||
expect(LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init).name).toEqual('dnslink-ipfs');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("IPFS setup should be initiated", async ()=>{
|
|
||||||
self.importScripts = jest.fn()
|
|
||||||
require("../../../plugins/dnslink-ipfs/index.js");
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init).fetch('/test.json')
|
|
||||||
} catch {}
|
|
||||||
expect(self.importScripts).toHaveBeenNthCalledWith(1, './lib/ipfs.js')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("fetching should error out for unpublished content", async ()=>{
|
|
||||||
require("../../../plugins/dnslink-ipfs/index.js");
|
|
||||||
|
|
||||||
expect.assertions(1)
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init).fetch(self.location.origin + '/nonexistent.path')
|
|
||||||
} catch(e) {
|
|
||||||
expect(e).toEqual(new Error('Error: file does not exist'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: probably not necessary in the long run?
|
|
||||||
test("fetching a path ending in <path>/ should instead fetch <path>/index.html", async ()=>{
|
|
||||||
require("../../../plugins/dnslink-ipfs/index.js");
|
|
||||||
|
|
||||||
try {
|
|
||||||
var response = await LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init).fetch(self.location.origin + '/test/')
|
|
||||||
} catch(e) {
|
|
||||||
}
|
|
||||||
var blob = await response.blob()
|
|
||||||
expect(JSON.parse(new TextDecoder().decode(blob.parts[0]))).toEqual({test: "success", path: "/ipfs/" + global.Ipfs.ipfsFixtureAddress + '/test/index.html'})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("content types should be guessed correctly when fetching", async ()=>{
|
|
||||||
require("../../../plugins/dnslink-ipfs/index.js");
|
|
||||||
var dnslinkIpfsPlugin = LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init)
|
|
||||||
try {
|
|
||||||
await dnslinkIpfsPlugin.fetch(self.location.origin + '/test/')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('dnslink-ipfs', " +-- guessed contentType : text/html")
|
|
||||||
LR.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dnslinkIpfsPlugin.fetch(self.location.origin + '/test.htm')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('dnslink-ipfs', " +-- guessed contentType : text/html")
|
|
||||||
LR.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dnslinkIpfsPlugin.fetch(self.location.origin + '/test.css')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('dnslink-ipfs', " +-- guessed contentType : text/css")
|
|
||||||
LR.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dnslinkIpfsPlugin.fetch(self.location.origin + '/test.js')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('dnslink-ipfs', " +-- guessed contentType : text/javascript")
|
|
||||||
LR.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dnslinkIpfsPlugin.fetch(self.location.origin + '/test.json')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('dnslink-ipfs', " +-- guessed contentType : application/json")
|
|
||||||
LR.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dnslinkIpfsPlugin.fetch(self.location.origin + '/test.svg')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('dnslink-ipfs', " +-- guessed contentType : image/svg+xml")
|
|
||||||
LR.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dnslinkIpfsPlugin.fetch(self.location.origin + '/test.ico')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('dnslink-ipfs', " +-- guessed contentType : image/x-icon")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("fetching should work", async ()=>{
|
|
||||||
require("../../../plugins/dnslink-ipfs/index.js");
|
|
||||||
|
|
||||||
let response = await LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init).fetch(self.location.origin + '/test.json')
|
|
||||||
expect(response.body.type).toEqual('application/json')
|
|
||||||
var blob = await response.blob()
|
|
||||||
expect(JSON.parse(new TextDecoder().decode(blob.parts[0]))).toEqual({test: "success", path: "/ipfs/" + global.Ipfs.ipfsFixtureAddress + '/test.json'})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("publish() should throw an error", async ()=>{
|
|
||||||
require("../../../plugins/dnslink-ipfs/index.js");
|
|
||||||
|
|
||||||
expect.assertions(1)
|
|
||||||
try {
|
|
||||||
LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init).publish()
|
|
||||||
} catch(e) {
|
|
||||||
expect(e).toEqual(new Error("Not implemented yet."))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("IPFS load error should be handled", async ()=>{
|
|
||||||
|
|
||||||
global.Ipfs.create = ()=>{
|
|
||||||
throw new Error('Testing IPFS loading failure')
|
|
||||||
}
|
|
||||||
require("../../../plugins/dnslink-ipfs/index.js");
|
|
||||||
|
|
||||||
expect.assertions(1)
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init).fetch('/test.json')
|
|
||||||
} catch(e) {
|
|
||||||
expect(e).toEqual(new Error("Error: Testing IPFS loading failure"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("importScripts being undefined should be handled", async ()=>{
|
|
||||||
self.importScripts = undefined
|
|
||||||
require("../../../plugins/dnslink-ipfs/index.js");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init).fetch('/test.json')
|
|
||||||
} catch(e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(LR.log).toHaveBeenCalledWith("dnslink-ipfs", "Importing IPFS-related libraries...")
|
|
||||||
expect(LR.log).toHaveBeenCalledWith("dnslink-ipfs", "assuming these scripts are already included:")
|
|
||||||
expect(LR.log).toHaveBeenCalledWith("dnslink-ipfs", "+--", "./lib/ipfs.js")
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,109 +0,0 @@
|
||||||
const makeServiceWorkerEnv = require('service-worker-mock');
|
|
||||||
|
|
||||||
global.fetch = require('node-fetch');
|
|
||||||
jest.mock('node-fetch')
|
|
||||||
|
|
||||||
describe("plugin: fetch", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
global.fetch.mockImplementation((url, init) => {
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
[JSON.stringify({ test: "success" })],
|
|
||||||
{type: "application/json"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.assign(global, makeServiceWorkerEnv());
|
|
||||||
jest.resetModules();
|
|
||||||
global.LibResilientPluginConstructors = new Map()
|
|
||||||
LR = {
|
|
||||||
log: (component, ...items)=>{
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should register in LibResilientPluginConstructors", () => {
|
|
||||||
require("../../../plugins/fetch/index.js");
|
|
||||||
expect(LibResilientPluginConstructors.get('fetch')().name).toEqual('fetch');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should return data from fetch()", async () => {
|
|
||||||
require("../../../plugins/fetch/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('fetch')(LR).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalled();
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should pass the Request() init data to fetch()", async () => {
|
|
||||||
require("../../../plugins/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
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('fetch')(LR).fetch('https://resilient.is/test.json', initTest);
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledWith('https://resilient.is/test.json', initTest);
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should set the LibResilient headers", async () => {
|
|
||||||
require("../../../plugins/fetch/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('fetch')(LR).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalled();
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
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('fetch')
|
|
||||||
expect(response.headers.has('X-LibResilient-Etag')).toEqual(true)
|
|
||||||
expect(response.headers.get('X-LibResilient-ETag')).toEqual('TestingETagHeader')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error when HTTP status is >= 400", async () => {
|
|
||||||
|
|
||||||
global.fetch.mockImplementation((url, init) => {
|
|
||||||
const response = new Response(
|
|
||||||
new Blob(
|
|
||||||
["Not Found"],
|
|
||||||
{type: "text/plain"}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
status: 404,
|
|
||||||
statusText: "Not Found",
|
|
||||||
url: url
|
|
||||||
});
|
|
||||||
return Promise.resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
require("../../../plugins/fetch/index.js");
|
|
||||||
|
|
||||||
expect.assertions(1)
|
|
||||||
expect(LibResilientPluginConstructors.get('fetch')(LR).fetch('https://resilient.is/test.json')).rejects.toThrow(Error)
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,198 +0,0 @@
|
||||||
const makeServiceWorkerEnv = require('service-worker-mock');
|
|
||||||
|
|
||||||
describe("plugin: gun-ipfs", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
Object.assign(global, makeServiceWorkerEnv());
|
|
||||||
jest.resetModules();
|
|
||||||
init = {
|
|
||||||
name: 'gun-ipfs',
|
|
||||||
gunPubkey: 'stub'
|
|
||||||
}
|
|
||||||
global.LibResilientPluginConstructors = new Map()
|
|
||||||
LR = {
|
|
||||||
log: jest.fn((component, ...items)=>{
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
global.Ipfs = {
|
|
||||||
create: ()=>{
|
|
||||||
return Promise.resolve({
|
|
||||||
get: ()=>{
|
|
||||||
return {
|
|
||||||
next: ()=>{
|
|
||||||
sourceUsed = true
|
|
||||||
return Promise.resolve({
|
|
||||||
value: {
|
|
||||||
path: 'some-ipfs-looking-address',
|
|
||||||
content: {
|
|
||||||
next: ()=>{
|
|
||||||
sourceUsed = !sourceUsed
|
|
||||||
return Promise.resolve({
|
|
||||||
done: sourceUsed,
|
|
||||||
value: Uint8Array.from(
|
|
||||||
Array
|
|
||||||
.from('{test: "success"}')
|
|
||||||
.map(
|
|
||||||
letter => letter.charCodeAt(0)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.Ipfs = global.Ipfs
|
|
||||||
self.gunUser = jest.fn(()=>{
|
|
||||||
return {
|
|
||||||
get: () => {
|
|
||||||
return {
|
|
||||||
get: ()=>{
|
|
||||||
return {
|
|
||||||
once: (arg)=>{ arg(undefined) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
global.Gun = jest.fn((nodes)=>{
|
|
||||||
return {
|
|
||||||
user: self.gunUser
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should register in LibResilientPlugins", () => {
|
|
||||||
require("../../../plugins/gun-ipfs/index.js");
|
|
||||||
expect(LibResilientPluginConstructors.get('gun-ipfs')(LR, init).name).toEqual('gun-ipfs');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("IPFS setup should be initiated", async ()=>{
|
|
||||||
self.importScripts = jest.fn()
|
|
||||||
require("../../../plugins/gun-ipfs/index.js");
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('gun-ipfs')(LR, init).fetch('/test.json')
|
|
||||||
} catch {}
|
|
||||||
expect(self.importScripts).toHaveBeenNthCalledWith(1, './lib/ipfs.js')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Gun setup should be initiated", async ()=>{
|
|
||||||
self.importScripts = jest.fn()
|
|
||||||
require("../../../plugins/gun-ipfs/index.js");
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('gun-ipfs')(LR, init).fetch('/test.json')
|
|
||||||
} catch {}
|
|
||||||
expect(self.importScripts).toHaveBeenNthCalledWith(2, "./lib/gun.js", "./lib/sea.js", "./lib/webrtc.js")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("fetching should error out for unpublished content", async ()=>{
|
|
||||||
require("../../../plugins/gun-ipfs/index.js");
|
|
||||||
|
|
||||||
expect.assertions(1)
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('gun-ipfs')(LR, init).fetch(self.location.origin + '/test.json')
|
|
||||||
} catch(e) {
|
|
||||||
expect(e).toEqual(new Error('IPFS address is undefined for: /test.json'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("fetching a path ending in <path>/ should instead fetch <path>/index.html", async ()=>{
|
|
||||||
require("../../../plugins/gun-ipfs/index.js");
|
|
||||||
|
|
||||||
expect.assertions(1)
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('gun-ipfs')(LR, init).fetch(self.location.origin + '/test/')
|
|
||||||
} catch(e) {
|
|
||||||
expect(e).toEqual(new Error('IPFS address is undefined for: /test/index.html'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("content types should be guessed correctly when fetching", async ()=>{
|
|
||||||
require("../../../plugins/gun-ipfs/index.js");
|
|
||||||
var gunipfsPlugin = LibResilientPluginConstructors.get('gun-ipfs')(LR, init)
|
|
||||||
try {
|
|
||||||
await gunipfsPlugin.fetch(self.location.origin + '/test/')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : text/html")
|
|
||||||
LR.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await gunipfsPlugin.fetch(self.location.origin + '/test.htm')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : text/html")
|
|
||||||
LR.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await gunipfsPlugin.fetch(self.location.origin + '/test.css')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : text/css")
|
|
||||||
LR.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await gunipfsPlugin.fetch(self.location.origin + '/test.js')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : text/javascript")
|
|
||||||
LR.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await gunipfsPlugin.fetch(self.location.origin + '/test.json')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : application/json")
|
|
||||||
LR.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await gunipfsPlugin.fetch(self.location.origin + '/test.svg')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : image/svg+xml")
|
|
||||||
LR.log.mockClear()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await gunipfsPlugin.fetch(self.location.origin + '/test.ico')
|
|
||||||
} catch(e) {}
|
|
||||||
expect(LR.log).toHaveBeenCalledWith('gun-ipfs', " +-- guessed contentType : image/x-icon")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("fetching should work (stub!)", async ()=>{
|
|
||||||
self.gunUser = jest.fn(()=>{
|
|
||||||
return {
|
|
||||||
get: () => {
|
|
||||||
return {
|
|
||||||
get: ()=>{
|
|
||||||
return {
|
|
||||||
once: (arg)=>{ arg('some-ipfs-looking-address') }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
require("../../../plugins/gun-ipfs/index.js");
|
|
||||||
|
|
||||||
//await self.Ipfs.create()
|
|
||||||
let response = await LibResilientPluginConstructors.get('gun-ipfs')(LR, init).fetch(self.location.origin + '/test.json')
|
|
||||||
expect(response.body.type).toEqual('application/json')
|
|
||||||
expect(String.fromCharCode.apply(null, response.body.parts[0])).toEqual('{test: "success"}')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("publishContent should error out if passed anything else than string or array of string", async ()=>{
|
|
||||||
require("../../../plugins/gun-ipfs/index.js");
|
|
||||||
var gunipfsPlugin = LibResilientPluginConstructors.get('gun-ipfs')(LR, init)
|
|
||||||
expect(()=>{
|
|
||||||
gunipfsPlugin.publish({
|
|
||||||
url: self.location.origin + '/test.json'
|
|
||||||
})
|
|
||||||
}).toThrow('Handling a Response: not implemented yet')
|
|
||||||
expect(()=>{
|
|
||||||
gunipfsPlugin.publish(true)
|
|
||||||
}).toThrow('Only accepts: string, Array of string, Response.')
|
|
||||||
expect(()=>{
|
|
||||||
gunipfsPlugin.publish([true, 5])
|
|
||||||
}).toThrow('Only accepts: string, Array of string, Response.')
|
|
||||||
})
|
|
||||||
});
|
|
|
@ -1,208 +0,0 @@
|
||||||
describe("plugin: integrity-check", () => {
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
global.nodeFetch = require('node-fetch')
|
|
||||||
global.Request = global.nodeFetch.Request
|
|
||||||
global.Response = global.nodeFetch.Response
|
|
||||||
global.crypto = require('crypto').webcrypto
|
|
||||||
global.Blob = require('buffer').Blob;
|
|
||||||
jest.resetModules();
|
|
||||||
self = global
|
|
||||||
global.btoa = (bin) => {
|
|
||||||
return Buffer.from(bin, 'binary').toString('base64')
|
|
||||||
}
|
|
||||||
|
|
||||||
global.LibResilientPluginConstructors = new Map()
|
|
||||||
LR = {
|
|
||||||
log: (component, ...items)=>{
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
global.resolvingFetch = jest.fn((url, init)=>{
|
|
||||||
return Promise.resolve(
|
|
||||||
new Response(
|
|
||||||
['{"test": "success"}'],
|
|
||||||
{
|
|
||||||
type: "application/json",
|
|
||||||
status: 200,
|
|
||||||
statusText: "OK",
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: url
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
init = {
|
|
||||||
name: 'integrity-check',
|
|
||||||
uses: [
|
|
||||||
{
|
|
||||||
name: 'resolve-all',
|
|
||||||
description: 'Resolves all',
|
|
||||||
version: '0.0.1',
|
|
||||||
fetch: resolvingFetch
|
|
||||||
}
|
|
||||||
],
|
|
||||||
requireIntegrity: false
|
|
||||||
}
|
|
||||||
requestInit = {
|
|
||||||
integrity: "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="
|
|
||||||
}
|
|
||||||
self.log = function(component, ...items) {
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should register in LibResilientPluginConstructors", () => {
|
|
||||||
require("../../../plugins/integrity-check/index.js");
|
|
||||||
expect(LibResilientPluginConstructors.get('integrity-check')(LR, init).name).toEqual('integrity-check');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error when there aren't any wrapped plugins configured", async () => {
|
|
||||||
require("../../../plugins/integrity-check/index.js");
|
|
||||||
init = {
|
|
||||||
name: 'integrity-check',
|
|
||||||
uses: []
|
|
||||||
}
|
|
||||||
|
|
||||||
expect.assertions(2);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json')
|
|
||||||
} catch (e) {
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('Expected exactly one plugin to wrap')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error when there are more than one wrapped plugins configured", async () => {
|
|
||||||
require("../../../plugins/integrity-check/index.js");
|
|
||||||
init = {
|
|
||||||
name: 'integrity-check',
|
|
||||||
uses: [{
|
|
||||||
name: 'plugin-1'
|
|
||||||
},{
|
|
||||||
name: 'plugin-2'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
expect.assertions(2);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json')
|
|
||||||
} catch (e) {
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('Expected exactly one plugin to wrap')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error when an unsupported digest algorithm is used", async () => {
|
|
||||||
require("../../../plugins/integrity-check/index.js");
|
|
||||||
|
|
||||||
expect.assertions(1);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json', {
|
|
||||||
integrity: "sha000-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.toString()).toMatch('No digest matched')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should return data from the wrapped plugin when no integrity data is available and requireIntegrity is false", async () => {
|
|
||||||
require("../../../plugins/integrity-check/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalled();
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should reject no integrity data is available but requireIntegrity is true", async () => {
|
|
||||||
require("../../../plugins/integrity-check/index.js");
|
|
||||||
init.requireIntegrity = true
|
|
||||||
|
|
||||||
expect.assertions(2);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json')
|
|
||||||
} catch (e) {
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('Integrity data required but not provided for')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should check integrity and return data from the wrapped plugin if SHA-256 integrity data matches", async () => {
|
|
||||||
require("../../../plugins/integrity-check/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json', requestInit);
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalled();
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should check integrity and return data from the wrapped plugin if SHA-384 integrity data matches", async () => {
|
|
||||||
require("../../../plugins/integrity-check/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json', {
|
|
||||||
integrity: "sha384-x4iqiH3PIPD51TibGEhTju/WhidcIEcnrpdklYEtIS87f96c4nLyj6CuwUp8kyOo"
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalled();
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should check integrity and return data from the wrapped plugin if SHA-512 integrity data matches", async () => {
|
|
||||||
require("../../../plugins/integrity-check/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json', {
|
|
||||||
integrity: "sha512-o+J3lPk7DU8xOJaNfZI5T4Upmaoc9XOVxOWPCFAy4pTgvS8LrJZ8iNis/2ZaryU4bB33cNSXQBxUDvwDxknEBQ=="
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalled();
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should check integrity of the data returned from the wrapped plugin and reject if it doesn't match", async () => {
|
|
||||||
require("../../../plugins/integrity-check/index.js");
|
|
||||||
|
|
||||||
expect.assertions(1);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json', {
|
|
||||||
integrity: "sha256-INCORRECTINCORRECTINCORRECTINCORRECTINCORREC"
|
|
||||||
});
|
|
||||||
} catch(e) {
|
|
||||||
expect(e.toString()).toMatch('No digest matched')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should check integrity of the data returned from the wrapped plugin and resolve if at least one of multiple integrity hash matches", async () => {
|
|
||||||
require("../../../plugins/integrity-check/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json', {
|
|
||||||
integrity: "sha256-INCORRECTINCORRECTINCORRECTINCORRECTINCORREC sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalled();
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should check integrity of the data returned from the wrapped plugin and reject if all out of multiple integrity hash do not match", async () => {
|
|
||||||
require("../../../plugins/integrity-check/index.js");
|
|
||||||
|
|
||||||
expect.assertions(1);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json', {
|
|
||||||
integrity: "sha256-INCORRECTINCORRECTINCORRECTINCORRECTINCORREC sha256-WRONGWRONGWRONGWRONGWRONGWRONGWRONGWRONGWRON"
|
|
||||||
});
|
|
||||||
} catch(e) {
|
|
||||||
expect(e.toString()).toMatch('No digest matched')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,22 +0,0 @@
|
||||||
const makeServiceWorkerEnv = require('service-worker-mock');
|
|
||||||
|
|
||||||
describe("plugin: ipns-ipfs", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
Object.assign(global, makeServiceWorkerEnv());
|
|
||||||
jest.resetModules();
|
|
||||||
global.LibResilientPluginConstructors = new Map()
|
|
||||||
init = {
|
|
||||||
name: 'ipns-ipfs',
|
|
||||||
ipnsPubkey: 'stub'
|
|
||||||
}
|
|
||||||
LR = {
|
|
||||||
log: (component, ...items)=>{
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
test("it should register in LibResilientPlugins", () => {
|
|
||||||
require("../../../plugins/ipns-ipfs/index.js");
|
|
||||||
expect(LibResilientPluginConstructors.get('ipns-ipfs')(LR, init).name).toEqual('ipns-ipfs');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,102 +0,0 @@
|
||||||
const makeServiceWorkerEnv = require('service-worker-mock');
|
|
||||||
|
|
||||||
global.fetch = require('node-fetch');
|
|
||||||
jest.mock('node-fetch')
|
|
||||||
|
|
||||||
describe("plugin: redirect", () => {
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
Object.assign(global, makeServiceWorkerEnv());
|
|
||||||
jest.resetModules();
|
|
||||||
global.LibResilientPluginConstructors = new Map()
|
|
||||||
init = {
|
|
||||||
name: 'redirect',
|
|
||||||
redirectStatus: 302,
|
|
||||||
redirectStatusText: "Found",
|
|
||||||
redirectTo: "https://redirected.example.org/subdir/"
|
|
||||||
}
|
|
||||||
LR = {
|
|
||||||
log: (component, ...items)=>{
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should register in LibResilientPluginConstructors", () => {
|
|
||||||
init = {
|
|
||||||
name: 'redirect',
|
|
||||||
redirectTo: 'https://example.org/'
|
|
||||||
}
|
|
||||||
require("../../../plugins/redirect/index.js");
|
|
||||||
expect(LibResilientPluginConstructors.get('redirect')(LR, init).name).toEqual('redirect');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should fail with incorrect redirectTo config value", () => {
|
|
||||||
init = {
|
|
||||||
name: 'redirect',
|
|
||||||
redirectTo: false
|
|
||||||
}
|
|
||||||
require("../../../plugins/redirect/index.js")
|
|
||||||
expect.assertions(1)
|
|
||||||
expect(()=>{
|
|
||||||
LibResilientPluginConstructors.get('redirect')(LR, init)
|
|
||||||
}).toThrow(Error);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should fail with incorrect redirectStatus config value", () => {
|
|
||||||
init = {
|
|
||||||
name: 'redirect',
|
|
||||||
redirectTo: 'https://example.org/',
|
|
||||||
redirectStatus: 'incorrect'
|
|
||||||
}
|
|
||||||
require("../../../plugins/redirect/index.js")
|
|
||||||
expect.assertions(1)
|
|
||||||
expect(()=>{
|
|
||||||
LibResilientPluginConstructors.get('redirect')(LR, init)
|
|
||||||
}).toThrow(Error);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should fail with incorrect redirectStatusText config value", () => {
|
|
||||||
init = {
|
|
||||||
name: 'redirect',
|
|
||||||
redirectTo: 'https://example.org/',
|
|
||||||
redirectStatusText: false
|
|
||||||
}
|
|
||||||
require("../../../plugins/redirect/index.js")
|
|
||||||
expect.assertions(1)
|
|
||||||
expect(()=>{
|
|
||||||
LibResilientPluginConstructors.get('redirect')(LR, init)
|
|
||||||
}).toThrow(Error);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should register in LibResilientPluginConstructors without error even if all config data is incorrect, as long as enabled is false", () => {
|
|
||||||
init = {
|
|
||||||
name: 'redirect',
|
|
||||||
redirectTo: false,
|
|
||||||
redirectStatus: "incorrect",
|
|
||||||
redirectStatusText: false,
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
require("../../../plugins/redirect/index.js");
|
|
||||||
expect(LibResilientPluginConstructors.get('redirect')(LR, init).name).toEqual('redirect');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should return a 302 Found redirect for any request", async () => {
|
|
||||||
init = {
|
|
||||||
name: 'redirect',
|
|
||||||
redirectTo: "https://redirected.example.org/subdirectory/"
|
|
||||||
}
|
|
||||||
|
|
||||||
require("../../../plugins/redirect/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('redirect')(LR, init).fetch('https://resilient.is/test.json');
|
|
||||||
|
|
||||||
|
|
||||||
//expect().toEqual()
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
expect(response.status).toEqual(302)
|
|
||||||
expect(response.statusText).toEqual('Found')
|
|
||||||
expect(response.headers.get('location')).toEqual('https://redirected.example.org/subdirectory/test.json')
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,366 +0,0 @@
|
||||||
const { subtle } = require('crypto').webcrypto;
|
|
||||||
|
|
||||||
describe("plugin: signed-integrity", () => {
|
|
||||||
|
|
||||||
var keypair = null
|
|
||||||
|
|
||||||
async function generateECDSAKeypair() {
|
|
||||||
if (keypair == null) {
|
|
||||||
keypair = await subtle.generateKey({
|
|
||||||
name: "ECDSA",
|
|
||||||
namedCurve: "P-384"
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
["sign", "verify"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return keypair;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getArmouredKey(key) {
|
|
||||||
return JSON.stringify(await subtle.exportKey('jwk', key))
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
global.nodeFetch = require('node-fetch')
|
|
||||||
global.Request = global.nodeFetch.Request
|
|
||||||
global.Response = global.nodeFetch.Response
|
|
||||||
global.crypto = require('crypto').webcrypto
|
|
||||||
global.Blob = require('buffer').Blob;
|
|
||||||
jest.resetModules();
|
|
||||||
self = global
|
|
||||||
global.subtle = subtle
|
|
||||||
global.btoa = (bin) => {
|
|
||||||
return Buffer.from(bin, 'binary').toString('base64')
|
|
||||||
}
|
|
||||||
global.atob = (ascii) => {
|
|
||||||
return Buffer.from(ascii, 'base64').toString('binary')
|
|
||||||
}
|
|
||||||
|
|
||||||
global.LibResilientPluginConstructors = new Map()
|
|
||||||
LR = {
|
|
||||||
log: (component, ...items)=>{
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// debug
|
|
||||||
console.log("pubkey: ", await getArmouredKey((await generateECDSAKeypair()).publicKey))
|
|
||||||
|
|
||||||
// ES384: ECDSA using P-384 and SHA-384
|
|
||||||
var header = btoa('{"alg": "ES384"}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
||||||
var payload = btoa('{"integrity": "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
||||||
|
|
||||||
// get a signature
|
|
||||||
var signature = await subtle.sign(
|
|
||||||
{
|
|
||||||
name: "ECDSA",
|
|
||||||
hash: {name: "SHA-384"}
|
|
||||||
},
|
|
||||||
(await generateECDSAKeypair()).privateKey,
|
|
||||||
(header + '.' + payload)
|
|
||||||
)
|
|
||||||
// prepare it for inclusion in the JWT
|
|
||||||
signature = btoa(signature).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
||||||
|
|
||||||
// need to test with bad algo!
|
|
||||||
var noneHeader = btoa('{"alg": "none"}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
||||||
|
|
||||||
// get an invalid signature
|
|
||||||
// an ECDSA signature for {alg: none} header makes zero sense
|
|
||||||
var noneSignature = await subtle.sign(
|
|
||||||
{
|
|
||||||
name: "ECDSA",
|
|
||||||
hash: {name: "SHA-384"}
|
|
||||||
},
|
|
||||||
(await generateECDSAKeypair()).privateKey,
|
|
||||||
(noneHeader + '.' + payload)
|
|
||||||
)
|
|
||||||
// prepare it for inclusion in the JWT
|
|
||||||
noneSignature = btoa(noneSignature).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
||||||
|
|
||||||
// prepare stuff for invalid JWT JSON test
|
|
||||||
var invalidPayload = btoa('not a valid JSON string').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
||||||
// get an valid signature for invalid payload
|
|
||||||
var invalidPayloadSignature = await subtle.sign(
|
|
||||||
{
|
|
||||||
name: "ECDSA",
|
|
||||||
hash: {name: "SHA-384"}
|
|
||||||
},
|
|
||||||
(await generateECDSAKeypair()).privateKey,
|
|
||||||
(header + '.' + invalidPayload)
|
|
||||||
)
|
|
||||||
// prepare it for inclusion in the JWT
|
|
||||||
invalidPayloadSignature = btoa(invalidPayloadSignature).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
||||||
|
|
||||||
// prepare stuff for JWT payload without integrity test
|
|
||||||
var noIntegrityPayload = btoa('{"no": "integrity"}').replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
||||||
// get an valid signature for invalid payload
|
|
||||||
var noIntegrityPayloadSignature = await subtle.sign(
|
|
||||||
{
|
|
||||||
name: "ECDSA",
|
|
||||||
hash: {name: "SHA-384"}
|
|
||||||
},
|
|
||||||
(await generateECDSAKeypair()).privateKey,
|
|
||||||
(header + '.' + noIntegrityPayload)
|
|
||||||
)
|
|
||||||
// prepare it for inclusion in the JWT
|
|
||||||
noIntegrityPayloadSignature = btoa(noIntegrityPayloadSignature).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '')
|
|
||||||
|
|
||||||
global.resolvingFetch = jest.fn((url, init)=>{
|
|
||||||
var content = '{"test": "success"}'
|
|
||||||
var status = 200
|
|
||||||
var statusText = "OK"
|
|
||||||
|
|
||||||
if (url == 'https://resilient.is/test.json.integrity') {
|
|
||||||
content = header + '.' + payload + '.' + signature
|
|
||||||
// testing 404 not found on the integrity URL
|
|
||||||
} else if (url == 'https://resilient.is/not-found.json.integrity') {
|
|
||||||
content = '{"test": "fail"}'
|
|
||||||
status = 404
|
|
||||||
statusText = "Not Found"
|
|
||||||
// testing invalid base64-encoded data
|
|
||||||
} else if (url == 'https://resilient.is/invalid-base64.json.integrity') {
|
|
||||||
// for this test to work correctly the length must be (n*4)+1
|
|
||||||
content = header + '.' + payload + '.' + 'badbase64'
|
|
||||||
// testing "alg: none" on the integrity JWT
|
|
||||||
} else if (url == 'https://resilient.is/alg-none.json.integrity') {
|
|
||||||
content = noneHeader + '.' + payload + '.'
|
|
||||||
// testing bad signature on the integrity JWT
|
|
||||||
} else if (url == 'https://resilient.is/bad-signature.json.integrity') {
|
|
||||||
content = header + '.' + payload + '.' + noneSignature
|
|
||||||
// testing invalid payload
|
|
||||||
} else if (url == 'https://resilient.is/invalid-payload.json.integrity') {
|
|
||||||
content = header + '.' + invalidPayload + '.' + invalidPayloadSignature
|
|
||||||
// testing payload without integrity data
|
|
||||||
} else if (url == 'https://resilient.is/no-integrity.json.integrity') {
|
|
||||||
content = header + '.' + noIntegrityPayload + '.' + noIntegrityPayloadSignature
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(
|
|
||||||
new Response(
|
|
||||||
[content],
|
|
||||||
{
|
|
||||||
type: "application/json",
|
|
||||||
status: status,
|
|
||||||
statusText: statusText,
|
|
||||||
headers: {
|
|
||||||
'ETag': 'TestingETagHeader'
|
|
||||||
},
|
|
||||||
url: url
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
init = {
|
|
||||||
name: 'signed-integrity',
|
|
||||||
uses: [
|
|
||||||
{
|
|
||||||
name: 'resolve-all',
|
|
||||||
description: 'Resolves all',
|
|
||||||
version: '0.0.1',
|
|
||||||
fetch: resolvingFetch
|
|
||||||
}
|
|
||||||
],
|
|
||||||
requireIntegrity: false,
|
|
||||||
publicKey: await subtle.exportKey('jwk', (await generateECDSAKeypair()).publicKey)
|
|
||||||
}
|
|
||||||
requestInit = {
|
|
||||||
integrity: "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="
|
|
||||||
}
|
|
||||||
self.log = function(component, ...items) {
|
|
||||||
console.debug(component + ' :: ', ...items)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("it should register in LibResilientPluginConstructors", () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
expect(LibResilientPluginConstructors.get('signed-integrity')(LR, init).name).toEqual('signed-integrity');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error when there aren't any wrapped plugins configured", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
init = {
|
|
||||||
name: 'signed-integrity',
|
|
||||||
uses: []
|
|
||||||
}
|
|
||||||
|
|
||||||
expect.assertions(2);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json')
|
|
||||||
} catch (e) {
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('Expected exactly one plugin to wrap')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error if the configured public key is impossible to load", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
|
|
||||||
init.publicKey = 'NOTAKEY'
|
|
||||||
|
|
||||||
expect.assertions(2);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json')
|
|
||||||
} catch (e) {
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('Unable to load the public key')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should throw an error when there are more than one wrapped plugins configured", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
init = {
|
|
||||||
name: 'signed-integrity',
|
|
||||||
uses: [{
|
|
||||||
name: 'plugin-1'
|
|
||||||
},{
|
|
||||||
name: 'plugin-2'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
expect.assertions(2);
|
|
||||||
try {
|
|
||||||
await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json')
|
|
||||||
} catch (e) {
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('Expected exactly one plugin to wrap')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should fetch content when integrity data provided without trying to fetch the integrity data URL", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json', {
|
|
||||||
integrity: "sha384-x4iqiH3PIPD51TibGEhTju/WhidcIEcnrpdklYEtIS87f96c4nLyj6CuwUp8kyOo"
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should fetch content when integrity data not provided, by also fetching the integrity data URL", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json', {});
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledTimes(2);
|
|
||||||
expect(resolvingFetch).toHaveBeenNthCalledWith(1, 'https://resilient.is/test.json.integrity')
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should fetch content when integrity data not provided, and integrity data URL 404s", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/not-found.json', {});
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledTimes(2);
|
|
||||||
expect(resolvingFetch).toHaveBeenNthCalledWith(1, 'https://resilient.is/not-found.json.integrity')
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/not-found.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should refuse to fetch content when integrity data not provided and integrity data URL 404s, but requireIntegrity is set to true", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
|
|
||||||
var newInit = init
|
|
||||||
newInit.requireIntegrity = true
|
|
||||||
|
|
||||||
expect.assertions(4);
|
|
||||||
try {
|
|
||||||
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, newInit).fetch('https://resilient.is/not-found.json', {});
|
|
||||||
} catch (e) {
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/not-found.json.integrity')
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('No integrity data available, though required.')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT is invalid", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
|
|
||||||
expect.assertions(4);
|
|
||||||
try {
|
|
||||||
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/invalid-base64.json', {});
|
|
||||||
} catch (e) {
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/invalid-base64.json.integrity')
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('Invalid base64-encoded string')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT uses alg: none", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
|
|
||||||
expect.assertions(4);
|
|
||||||
try {
|
|
||||||
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/alg-none.json', {});
|
|
||||||
} catch (e) {
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/alg-none.json.integrity')
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('JWT seems invalid (one or more sections are empty)')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT signature check fails", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
|
|
||||||
expect.assertions(4);
|
|
||||||
try {
|
|
||||||
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/bad-signature.json', {});
|
|
||||||
} catch (e) {
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/bad-signature.json.integrity')
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('JWT signature validation failed')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT payload is unparseable", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
|
|
||||||
expect.assertions(4);
|
|
||||||
try {
|
|
||||||
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/invalid-payload.json', {});
|
|
||||||
} catch (e) {
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/invalid-payload.json.integrity')
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('JWT payload parsing failed')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT payload does not contain integrity data", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
|
|
||||||
expect.assertions(4);
|
|
||||||
try {
|
|
||||||
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/no-integrity.json', {});
|
|
||||||
} catch (e) {
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledTimes(1);
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledWith('https://resilient.is/no-integrity.json.integrity')
|
|
||||||
expect(e).toBeInstanceOf(Error)
|
|
||||||
expect(e.toString()).toMatch('JWT payload did not contain integrity data')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should fetch and verify content, when integrity data not provided, by fetching the integrity data URL and using integrity data from it", async () => {
|
|
||||||
require("../../../plugins/signed-integrity/index.js");
|
|
||||||
|
|
||||||
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json', {});
|
|
||||||
|
|
||||||
expect(resolvingFetch).toHaveBeenCalledTimes(2);
|
|
||||||
expect(resolvingFetch).toHaveBeenNthCalledWith(1, 'https://resilient.is/test.json.integrity')
|
|
||||||
expect(resolvingFetch).toHaveBeenNthCalledWith(2, 'https://resilient.is/test.json', {integrity: "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="})
|
|
||||||
expect(await response.json()).toEqual({test: "success"})
|
|
||||||
expect(response.url).toEqual('https://resilient.is/test.json')
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
Plik diff jest za duży
Load Diff
Plik diff jest za duży
Load Diff
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"test": true
|
|
||||||
}
|
|
|
@ -0,0 +1,237 @@
|
||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
afterEach,
|
||||||
|
beforeEach
|
||||||
|
} 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";
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
window.fetch = spy(window.resolvingFetch)
|
||||||
|
window.init = {
|
||||||
|
name: 'alt-fetch',
|
||||||
|
endpoints: [
|
||||||
|
'https://alt.resilient.is/',
|
||||||
|
'https://error.resilient.is/',
|
||||||
|
'https://timeout.resilient.is/'
|
||||||
|
]}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(()=>{
|
||||||
|
window.fetch = null
|
||||||
|
window.init = null
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('browser: alt-fetch plugin', async () => {
|
||||||
|
window.LibResilientPluginConstructors = new Map()
|
||||||
|
window.LR = {
|
||||||
|
log: (component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.resolvingFetch = (url, init) => {
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(
|
||||||
|
new Blob(
|
||||||
|
[JSON.stringify({ test: "success" })],
|
||||||
|
{type: "application/json"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
window.fetch = null
|
||||||
|
window.init = null
|
||||||
|
await import("../../../plugins/alt-fetch/index.js");
|
||||||
|
|
||||||
|
it("should register in LibResilientPluginConstructors", async () => {
|
||||||
|
assertEquals(
|
||||||
|
LibResilientPluginConstructors
|
||||||
|
.get('alt-fetch')(LR, init)
|
||||||
|
.name,
|
||||||
|
'alt-fetch');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with bad config", () => {
|
||||||
|
init = {
|
||||||
|
name: 'alt-fetch',
|
||||||
|
endpoints: "this is incorrect"
|
||||||
|
}
|
||||||
|
assertThrows(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors.get('alt-fetch')(LR, init)
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'endpoints not confgured'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch the content, trying all configured endpoints (if fewer or equal to concurrency setting)", async () => {
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json');
|
||||||
|
|
||||||
|
// default concurrency setting is 3
|
||||||
|
assertSpyCalls(fetch, 3);
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fetch the content, trying <concurrency> random endpoints out of all configured (if more than concurrency setting)", async () => {
|
||||||
|
|
||||||
|
init = {
|
||||||
|
name: 'alt-fetch',
|
||||||
|
endpoints: [
|
||||||
|
'https://alt.resilient.is/',
|
||||||
|
'https://error.resilient.is/',
|
||||||
|
'https://timeout.resilient.is/',
|
||||||
|
'https://alt2.resilient.is/',
|
||||||
|
'https://alt3.resilient.is/',
|
||||||
|
'https://alt4.resilient.is/'
|
||||||
|
]}
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json');
|
||||||
|
|
||||||
|
// default concurrency setting is 3
|
||||||
|
assertSpyCalls(fetch, 3);
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fetch the content, trying all endpoints (if fewer than concurrency setting)", async () => {
|
||||||
|
|
||||||
|
init = {
|
||||||
|
name: 'alt-fetch',
|
||||||
|
endpoints: [
|
||||||
|
'https://alt.resilient.is/',
|
||||||
|
'https://error.resilient.is/'
|
||||||
|
]}
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json');
|
||||||
|
|
||||||
|
// default concurrency setting is 3
|
||||||
|
assertSpyCalls(fetch, 2);
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://alt.resilient.is/test.json', {"cache": "reload"}]
|
||||||
|
})
|
||||||
|
assertSpyCall(fetch, 1, {
|
||||||
|
args: ['https://error.resilient.is/test.json', {"cache": "reload"}]
|
||||||
|
})
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
})
|
||||||
|
|
||||||
|
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",
|
||||||
|
redirect: "follow-stub",
|
||||||
|
integrity: "integrity-stub"
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json', initTest);
|
||||||
|
|
||||||
|
// default concurrency setting is 3
|
||||||
|
assertSpyCalls(fetch, 3);
|
||||||
|
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://alt.resilient.is/test.json', initTest]
|
||||||
|
})
|
||||||
|
assertSpyCall(fetch, 1, {
|
||||||
|
args: ['https://error.resilient.is/test.json', initTest]
|
||||||
|
})
|
||||||
|
assertSpyCall(fetch, 2, {
|
||||||
|
args: ['https://timeout.resilient.is/test.json', initTest]
|
||||||
|
})
|
||||||
|
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should set the LibResilient headers", async () => {
|
||||||
|
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json');
|
||||||
|
|
||||||
|
// default concurrency setting is 3
|
||||||
|
assertSpyCalls(fetch, 3);
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
|
||||||
|
assertEquals(response.headers.has('X-LibResilient-Method'), true)
|
||||||
|
assertEquals(response.headers.get('X-LibResilient-Method'), 'alt-fetch')
|
||||||
|
assertEquals(response.headers.has('X-LibResilient-Etag'), true)
|
||||||
|
assertEquals(response.headers.get('X-LibResilient-ETag'), 'TestingETagHeader')
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set the LibResilient ETag based on Last-Modified header (if ETag is not available in the original response)", async () => {
|
||||||
|
window.fetch = spy((url, init) => {
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(
|
||||||
|
new Blob(
|
||||||
|
[JSON.stringify({ test: "success" })],
|
||||||
|
{type: "application/json"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
'Last-Modified': 'TestingLastModifiedHeader'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json');
|
||||||
|
|
||||||
|
assertSpyCalls(fetch, 3);
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
|
||||||
|
assertEquals(response.headers.has('X-LibResilient-Method'), true)
|
||||||
|
assertEquals(response.headers.get('X-LibResilient-Method'), 'alt-fetch')
|
||||||
|
assertEquals(response.headers.has('X-LibResilient-Etag'), true)
|
||||||
|
assertEquals(response.headers.get('X-LibResilient-ETag'), 'TestingLastModifiedHeader')
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when HTTP status is >= 400", async () => {
|
||||||
|
window.fetch = spy((url, init) => {
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(
|
||||||
|
new Blob(
|
||||||
|
["Not Found"],
|
||||||
|
{type: "text/plain"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
statusText: "Not Found"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
assertRejects(
|
||||||
|
async () => {
|
||||||
|
return await LibResilientPluginConstructors
|
||||||
|
.get('alt-fetch')(LR)
|
||||||
|
.fetch('https://resilient.is/test.json') },
|
||||||
|
Error,
|
||||||
|
'All promises were rejected'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
})
|
|
@ -0,0 +1,153 @@
|
||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
afterEach,
|
||||||
|
beforeEach
|
||||||
|
} 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";
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
window.fetch = spy(window.resolvingFetch)
|
||||||
|
window.init = {
|
||||||
|
name: 'any-of',
|
||||||
|
uses: [
|
||||||
|
LibResilientPluginConstructors.get('fetch')(LR),
|
||||||
|
{
|
||||||
|
name: 'reject-all',
|
||||||
|
description: 'Rejects all',
|
||||||
|
version: '0.0.1',
|
||||||
|
fetch: url=>Promise.reject('Reject All!')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(()=>{
|
||||||
|
window.fetch = null
|
||||||
|
window.init = null
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('browser: any-of plugin', async () => {
|
||||||
|
window.LibResilientPluginConstructors = new Map()
|
||||||
|
window.LR = {
|
||||||
|
log: (component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.resolvingFetch = (url, init) => {
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(
|
||||||
|
new Blob(
|
||||||
|
[JSON.stringify({ test: "success" })],
|
||||||
|
{type: "application/json"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
window.fetch = null
|
||||||
|
window.init = null
|
||||||
|
await import("../../../plugins/any-of/index.js");
|
||||||
|
// we need the fetch plugin in our testing
|
||||||
|
await import("../../../plugins/fetch/index.js");
|
||||||
|
|
||||||
|
it("should register in LibResilientPluginConstructors", async () => {
|
||||||
|
assertEquals(
|
||||||
|
LibResilientPluginConstructors
|
||||||
|
.get('any-of')(LR, init)
|
||||||
|
.name,
|
||||||
|
'any-of');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when there aren't any wrapped plugins configured", async () => {
|
||||||
|
init = {
|
||||||
|
name: 'any-of',
|
||||||
|
uses: []
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows(()=>{
|
||||||
|
return LibResilientPluginConstructors
|
||||||
|
.get('any-of')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json')
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'No wrapped plugins configured!'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return data from a wrapped plugin", async () => {
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('any-of')(LR, init).fetch('https://resilient.is/test.json');
|
||||||
|
|
||||||
|
assertSpyCalls(fetch, 1)
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass Request() init data onto wrapped plugins", async () => {
|
||||||
|
|
||||||
|
var initTest = {
|
||||||
|
method: "GET",
|
||||||
|
headers: new Headers({"x-stub": "STUB"}),
|
||||||
|
mode: "mode-stub",
|
||||||
|
credentials: "credentials-stub",
|
||||||
|
cache: "cache-stub",
|
||||||
|
referrer: "referrer-stub",
|
||||||
|
redirect: "follow-stub",
|
||||||
|
integrity: "integrity-stub"
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('any-of')(LR, init).fetch('https://resilient.is/test.json', initTest);
|
||||||
|
|
||||||
|
assertSpyCalls(fetch, 1);
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/test.json', initTest]
|
||||||
|
})
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when HTTP status is >= 400", async () => {
|
||||||
|
|
||||||
|
window.fetch = spy((url, init) => {
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(
|
||||||
|
new Blob(
|
||||||
|
["Not Found"],
|
||||||
|
{type: "text/plain"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
statusText: "Not Found"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
assertRejects(
|
||||||
|
async () => {
|
||||||
|
return await LibResilientPluginConstructors
|
||||||
|
.get('any-of')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json') },
|
||||||
|
AggregateError,
|
||||||
|
'All promises were rejected'
|
||||||
|
)
|
||||||
|
assertSpyCalls(fetch, 1);
|
||||||
|
});
|
||||||
|
})
|
|
@ -0,0 +1,238 @@
|
||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
afterEach,
|
||||||
|
beforeEach
|
||||||
|
} 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";
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
window.resolvingFetchSpy = spy(window.resolvingFetch)
|
||||||
|
window.init = {
|
||||||
|
name: 'basic-integrity',
|
||||||
|
uses: [
|
||||||
|
{
|
||||||
|
name: 'resolve-all',
|
||||||
|
description: 'Resolves all',
|
||||||
|
version: '0.0.1',
|
||||||
|
fetch: window.resolvingFetchSpy
|
||||||
|
}
|
||||||
|
],
|
||||||
|
integrity: {
|
||||||
|
"https://resilient.is/test.json": "sha384-kn5dhxz4RpBmx7xC7Dmq2N43PclV9U/niyh+4Km7oz5W0FaWdz3Op+3K0Qxz8y3z"
|
||||||
|
},
|
||||||
|
requireIntegrity: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(()=>{
|
||||||
|
window.init = null
|
||||||
|
window.resolvingFetchSpy = null
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('browser: basic-integrity plugin', async () => {
|
||||||
|
window.LibResilientPluginConstructors = new Map()
|
||||||
|
window.LR = {
|
||||||
|
log: (component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.resolvingFetch = (url, init) => {
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(
|
||||||
|
new Blob(
|
||||||
|
[JSON.stringify({ test: "success" })],
|
||||||
|
{type: "application/json"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
window.resolvingFetchSpy = null
|
||||||
|
await import("../../../plugins/basic-integrity/index.js");
|
||||||
|
|
||||||
|
it("should register in LibResilientPluginConstructors", async () => {
|
||||||
|
assertEquals(
|
||||||
|
LibResilientPluginConstructors
|
||||||
|
.get('basic-integrity')(LR, init)
|
||||||
|
.name,
|
||||||
|
'basic-integrity');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when there aren't any wrapped plugins configured", async () => {
|
||||||
|
init = {
|
||||||
|
name: 'basic-integrity',
|
||||||
|
uses: []
|
||||||
|
}
|
||||||
|
assertThrows( () => {
|
||||||
|
return LibResilientPluginConstructors
|
||||||
|
.get('basic-integrity')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json')
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Expected exactly one plugin to wrap'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when there are more than one wrapped plugins configured", async () => {
|
||||||
|
init = {
|
||||||
|
name: 'basic-integrity',
|
||||||
|
uses: [{
|
||||||
|
name: 'plugin-1'
|
||||||
|
},{
|
||||||
|
name: 'plugin-2'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows( () => {
|
||||||
|
return LibResilientPluginConstructors
|
||||||
|
.get('basic-integrity')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json')
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Expected exactly one plugin to wrap'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return data from the wrapped plugin", async () => {
|
||||||
|
const response = await LibResilientPluginConstructors.get('basic-integrity')(LR, init).fetch('https://resilient.is/test.json');
|
||||||
|
assertSpyCalls(resolvingFetchSpy, 1);
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should provide the wrapped plugin with integrity data for a configured URL", async () => {
|
||||||
|
const response = await LibResilientPluginConstructors.get('basic-integrity')(LR, init).fetch('https://resilient.is/test.json');
|
||||||
|
assertSpyCall(
|
||||||
|
resolvingFetchSpy,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
'https://resilient.is/test.json',
|
||||||
|
{
|
||||||
|
integrity: init.integrity['https://resilient.is/test.json']
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should error out for an URL with no integrity data, when requireIntegrity is true", async () => {
|
||||||
|
assertThrows( () => {
|
||||||
|
return LibResilientPluginConstructors
|
||||||
|
.get('basic-integrity')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test2.json')
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Integrity data required but not provided for'
|
||||||
|
)
|
||||||
|
assertSpyCalls(resolvingFetchSpy, 0)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return data from the wrapped plugin with no integrity data if requireIntegrity is false", async () => {
|
||||||
|
init.integrity = {}
|
||||||
|
init.requireIntegrity = false
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('basic-integrity')(LR, init).fetch('https://resilient.is/test.json');
|
||||||
|
|
||||||
|
assertSpyCalls(resolvingFetchSpy, 1)
|
||||||
|
assertSpyCall(
|
||||||
|
resolvingFetchSpy,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
'https://resilient.is/test.json',
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return data from the wrapped plugin with no integrity data configured when requireIntegrity is true and integrity data is provided in Request() init data", async () => {
|
||||||
|
init.integrity = {}
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors
|
||||||
|
.get('basic-integrity')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json', {
|
||||||
|
integrity: "sha256-Aj9x0DWq9GUL1L8HibLCMa8YLKnV7IYAfpYurqrFwiQ="
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSpyCalls(resolvingFetchSpy, 1)
|
||||||
|
assertSpyCall(
|
||||||
|
resolvingFetchSpy,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
'https://resilient.is/test.json',
|
||||||
|
{
|
||||||
|
integrity: "sha256-Aj9x0DWq9GUL1L8HibLCMa8YLKnV7IYAfpYurqrFwiQ="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return data from the wrapped plugin with integrity data both configured and coming from Request() init", async () => {
|
||||||
|
const response = await LibResilientPluginConstructors
|
||||||
|
.get('basic-integrity')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json', {
|
||||||
|
integrity: "sha256-Aj9x0DWq9GUL1L8HibLCMa8YLKnV7IYAfpYurqrFwiQ="
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSpyCalls(resolvingFetchSpy, 1)
|
||||||
|
assertSpyCall(
|
||||||
|
resolvingFetchSpy,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
'https://resilient.is/test.json',
|
||||||
|
{
|
||||||
|
integrity: "sha256-Aj9x0DWq9GUL1L8HibLCMa8YLKnV7IYAfpYurqrFwiQ= sha384-kn5dhxz4RpBmx7xC7Dmq2N43PclV9U/niyh+4Km7oz5W0FaWdz3Op+3K0Qxz8y3z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
});
|
||||||
|
|
||||||
|
// as per documentation, this plugin is not supposed to actualy *verify* integrity of fetched resources!
|
||||||
|
it("should return data from the wrapped plugin even with incorrect integrity data provided", async () => {
|
||||||
|
init.integrity = {}
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors
|
||||||
|
.get('basic-integrity')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json', {
|
||||||
|
integrity: "sha256-INCORRECTINCORRECTINCORRECTINCORRECTINCORRECT"
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSpyCalls(resolvingFetchSpy, 1)
|
||||||
|
assertSpyCall(
|
||||||
|
resolvingFetchSpy,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
'https://resilient.is/test.json',
|
||||||
|
{
|
||||||
|
integrity: "sha256-INCORRECTINCORRECTINCORRECTINCORRECTINCORRECT"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
|
@ -0,0 +1,95 @@
|
||||||
|
import {
|
||||||
|
assert,
|
||||||
|
assertRejects,
|
||||||
|
assertEquals
|
||||||
|
} from "https://deno.land/std@0.167.0/testing/asserts.ts";
|
||||||
|
|
||||||
|
Deno.test("plugin loads", async () => {
|
||||||
|
const bi = await import('../cli.js')
|
||||||
|
assert("name" in bi)
|
||||||
|
assert(bi.name == "basic-integrity")
|
||||||
|
assert("description" in bi)
|
||||||
|
assert("actions" in bi)
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("get-integrity action defined", async () => {
|
||||||
|
const bi = await import('../cli.js')
|
||||||
|
assert("get-integrity" in bi.actions)
|
||||||
|
const gi = bi.actions["get-integrity"]
|
||||||
|
assert("run" in gi)
|
||||||
|
assert("description" in gi)
|
||||||
|
assert("arguments" in gi)
|
||||||
|
const gia = gi.arguments
|
||||||
|
assert("_" in gia)
|
||||||
|
assert("algorithm" in gia)
|
||||||
|
assert("output" in gia)
|
||||||
|
assert("name" in gia._)
|
||||||
|
assert("description" in gia._)
|
||||||
|
assert("description" in gia.algorithm)
|
||||||
|
assert("collect" in gia.algorithm)
|
||||||
|
assert(gia.algorithm.collect)
|
||||||
|
assert("string" in gia.algorithm)
|
||||||
|
assert(gia.algorithm.string)
|
||||||
|
assert("description" in gia.output)
|
||||||
|
assert("collect" in gia.output)
|
||||||
|
assert(!gia.output.collect)
|
||||||
|
assert("string" in gia.output)
|
||||||
|
assert(gia.output.string)
|
||||||
|
});
|
||||||
|
|
||||||
|
// this is a separate test in order to catch any changing defaults
|
||||||
|
Deno.test("get-integrity action defaults", async () => {
|
||||||
|
const bi = await import('../cli.js')
|
||||||
|
const gia = bi.actions["get-integrity"].arguments
|
||||||
|
assert("default" in gia.algorithm)
|
||||||
|
assert(gia.algorithm.default == "SHA-256")
|
||||||
|
assert("default" in gia.output)
|
||||||
|
assert(gia.output.default == "json")
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("get-integrity verifies arguments are sane", async () => {
|
||||||
|
const bi = await import('../cli.js')
|
||||||
|
const gi = bi.actions["get-integrity"]
|
||||||
|
assertRejects(gi.run, Error, "Expected non-empty list of files to generate digests of.")
|
||||||
|
assertRejects(async ()=>{
|
||||||
|
await gi.run(['no-such-file'])
|
||||||
|
}, Error, "No such file or directory")
|
||||||
|
assertRejects(async ()=>{
|
||||||
|
await gi.run(['irrelevant'], [])
|
||||||
|
}, Error, "Expected non-empty list of algorithms to use.")
|
||||||
|
assertRejects(async ()=>{
|
||||||
|
await gi.run(['irrelevant'], ['SHA-384'], false)
|
||||||
|
}, Error, "Expected either 'json' or 'text' as output type to generate.")
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("get-integrity handles paths in a sane way", async () => {
|
||||||
|
const bi = await import('../cli.js')
|
||||||
|
const gi = bi.actions["get-integrity"]
|
||||||
|
const mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
|
||||||
|
assertEquals(await gi.run(['./']), '{}')
|
||||||
|
assertEquals(await gi.run([mh]), '{"' + mh + '":["sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="]}')
|
||||||
|
assertEquals(await gi.run(['./', mh]), '{"' + mh + '":["sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="]}')
|
||||||
|
}, );
|
||||||
|
|
||||||
|
Deno.test("get-integrity handles algos argument in a sane way", async () => {
|
||||||
|
const bi = await import('../cli.js')
|
||||||
|
const gi = bi.actions["get-integrity"]
|
||||||
|
const mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
|
||||||
|
assertRejects(async ()=>{
|
||||||
|
await gi.run([mh], ['BAD-ALG'])
|
||||||
|
}, Error, 'Unrecognized algorithm name')
|
||||||
|
assertEquals(await gi.run([mh], ['SHA-256']), '{"' + mh + '":["sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="]}')
|
||||||
|
assertEquals(await gi.run([mh], ['SHA-384']), '{"' + mh + '":["sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9"]}')
|
||||||
|
assertEquals(await gi.run([mh], ['SHA-512']), '{"' + mh + '":["sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="]}')
|
||||||
|
assertEquals(await gi.run([mh], ['SHA-256', 'SHA-384', 'SHA-512']), '{"' + mh + '":["sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=","sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9","sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="]}')
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("get-integrity handles output argument in a sane way", async () => {
|
||||||
|
const bi = await import('../cli.js')
|
||||||
|
const gi = bi.actions["get-integrity"]
|
||||||
|
const mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
|
||||||
|
assertEquals(await gi.run([mh], ['SHA-256'], 'text'), '' + mh + ': sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=\n')
|
||||||
|
assertEquals(await gi.run([mh], ['SHA-384'], 'text'), '' + mh + ': sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9\n')
|
||||||
|
assertEquals(await gi.run([mh], ['SHA-512'], 'text'), '' + mh + ': sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==\n')
|
||||||
|
assertEquals(await gi.run([mh], ['SHA-256', 'SHA-384', 'SHA-512'], 'text'), '' + mh + ': sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek= sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9 sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==\n')
|
||||||
|
});
|
|
@ -0,0 +1,442 @@
|
||||||
|
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',
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
},
|
||||||
|
url: url
|
||||||
|
});
|
||||||
|
return Promise.resolve(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* prototype of the plugin init object
|
||||||
|
*/
|
||||||
|
window.initPrototype = {
|
||||||
|
name: 'cache'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.fetch = spy(window.resolvingFetch)
|
||||||
|
window.fetchResponse = [
|
||||||
|
{test: "success"},
|
||||||
|
"application/json"
|
||||||
|
]
|
||||||
|
window.init = {
|
||||||
|
...window.initPrototype
|
||||||
|
}
|
||||||
|
// clear the caches
|
||||||
|
await caches
|
||||||
|
.has('v1')
|
||||||
|
.then(async (hasv1) => {
|
||||||
|
if (hasv1) {
|
||||||
|
await caches.delete('v1')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('browser: cache 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/cache/index.js");
|
||||||
|
|
||||||
|
it("should register in LibResilientPluginConstructors", () => {
|
||||||
|
assertEquals(
|
||||||
|
LibResilientPluginConstructors
|
||||||
|
.get('cache')(LR, init)
|
||||||
|
.name,
|
||||||
|
'cache'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should error out if resource is not found in cache", async () => {
|
||||||
|
// nothing got stashed so nothing is there to be retrieved
|
||||||
|
await assertRejects(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors
|
||||||
|
.get('cache')(LR)
|
||||||
|
.fetch('https://resilient.is/test.json')
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Resource not found in cache'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should stash and retrieve an url successfully", async () => {
|
||||||
|
|
||||||
|
let cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
||||||
|
|
||||||
|
// stash
|
||||||
|
let stashResult = await cachePlugin
|
||||||
|
.stash('https://resilient.is/test.json')
|
||||||
|
assertEquals(stashResult, undefined, "stashResult should be undefined");
|
||||||
|
|
||||||
|
// retrieve and verify
|
||||||
|
let fetchResult = await cachePlugin
|
||||||
|
.fetch('https://resilient.is/test.json')
|
||||||
|
|
||||||
|
assert(fetchResult.ok, "fetchResult.ok is false")
|
||||||
|
assertEquals(fetchResult.status, 200, "fetchResult.status is not 200")
|
||||||
|
assertEquals(fetchResult.statusText, 'OK', "fetchResult.statusText is not 'OK'")
|
||||||
|
|
||||||
|
assert(fetchResult.headers.has('etag'), "fetchResult.headers does not contain 'ETag'")
|
||||||
|
assertEquals(fetchResult.headers.get('ETag'), 'TestingETagHeader')
|
||||||
|
|
||||||
|
let fetchedJSON = await fetchResult.json()
|
||||||
|
assertEquals(
|
||||||
|
fetchedJSON,
|
||||||
|
{ test: "success" },
|
||||||
|
"fetchedJSON is incorrect: " + JSON.stringify(fetchedJSON))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clear a url from cache successfully", async () => {
|
||||||
|
|
||||||
|
let cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
||||||
|
|
||||||
|
// stash
|
||||||
|
let stashResult = await cachePlugin
|
||||||
|
.stash('https://resilient.is/test.json')
|
||||||
|
assertEquals(stashResult, undefined, "stashResult should be undefined");
|
||||||
|
|
||||||
|
// unstash
|
||||||
|
let unstashResult = await cachePlugin
|
||||||
|
.unstash('https://resilient.is/test.json')
|
||||||
|
assertEquals(unstashResult, true, "unstashResult should be true")
|
||||||
|
|
||||||
|
// verify
|
||||||
|
await assertRejects(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors
|
||||||
|
.get('cache')(LR)
|
||||||
|
.fetch('https://resilient.is/test.json')
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Resource not found in cache'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should stash an array of urls successfully", async () => {
|
||||||
|
|
||||||
|
let cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
||||||
|
let urls = [
|
||||||
|
'https://resilient.is/test.json',
|
||||||
|
'https://resilient.is/test2.json'
|
||||||
|
]
|
||||||
|
|
||||||
|
// stash
|
||||||
|
let stashResult = await cachePlugin.stash(urls)
|
||||||
|
assertEquals(
|
||||||
|
stashResult,
|
||||||
|
undefined,
|
||||||
|
"stashResult should be undefined");
|
||||||
|
|
||||||
|
// retrieve and verify
|
||||||
|
await Promise.all(
|
||||||
|
urls.map(async (url)=>{
|
||||||
|
let fetchResult = await cachePlugin.fetch(url)
|
||||||
|
assert(fetchResult.ok, "fetchResult.ok is false")
|
||||||
|
assertEquals(fetchResult.status, 200, "fetchResult.status is not 200")
|
||||||
|
assertEquals(fetchResult.statusText, 'OK', "fetchResult.statusText is not 'OK'")
|
||||||
|
assert(fetchResult.headers.has('etag'), "fetchResult.headers does not contain 'ETag'")
|
||||||
|
assertEquals(fetchResult.headers.get('ETag'), 'TestingETagHeader')
|
||||||
|
|
||||||
|
let fetchedJSON = await fetchResult.json()
|
||||||
|
assertEquals(
|
||||||
|
fetchedJSON,
|
||||||
|
{ test: "success" },
|
||||||
|
"fetchedJSON is incorrect: " + JSON.stringify(fetchedJSON))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clear an array of urls successfully", async () => {
|
||||||
|
|
||||||
|
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
||||||
|
let urls = [
|
||||||
|
'https://resilient.is/test.json',
|
||||||
|
'https://resilient.is/test2.json'
|
||||||
|
]
|
||||||
|
|
||||||
|
// stash
|
||||||
|
let stashResult = await cachePlugin.stash(urls)
|
||||||
|
assertEquals(
|
||||||
|
stashResult,
|
||||||
|
undefined,
|
||||||
|
"stashResult should be undefined");
|
||||||
|
|
||||||
|
// unstash
|
||||||
|
let unstashResult = await cachePlugin
|
||||||
|
.unstash(urls)
|
||||||
|
assertEquals(unstashResult, [true, true], "unstashResult should be [true, true]")
|
||||||
|
|
||||||
|
// verify
|
||||||
|
await Promise.all(
|
||||||
|
urls.map(async (url)=>{
|
||||||
|
await assertRejects(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors
|
||||||
|
.get('cache')(LR)
|
||||||
|
.fetch(url)
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Resource not found in cache',
|
||||||
|
'url should not have been in cache: ' + url
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should error out when stashing a Response without a url/key", async () => {
|
||||||
|
|
||||||
|
const response = new Response(
|
||||||
|
new Blob(
|
||||||
|
[JSON.stringify({ test: "success" })],
|
||||||
|
{type: "application/json"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertRejects(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors
|
||||||
|
.get('cache')(LR)
|
||||||
|
.stash(response)
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'No URL to work with!'
|
||||||
|
)
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should stash a Response successfully", async () => {
|
||||||
|
|
||||||
|
const response = new Response(
|
||||||
|
new Blob(
|
||||||
|
[JSON.stringify({ test: "success" })],
|
||||||
|
{type: "application/json"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Response.url is read-only, so we need to hack around that
|
||||||
|
Object.defineProperty(
|
||||||
|
response,
|
||||||
|
"url",
|
||||||
|
{ value: 'https://resilient.is/test.json' });
|
||||||
|
|
||||||
|
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
||||||
|
|
||||||
|
let stashResult = await cachePlugin.stash(response)
|
||||||
|
assertEquals(
|
||||||
|
stashResult,
|
||||||
|
undefined,
|
||||||
|
"stashResult should be undefined");
|
||||||
|
|
||||||
|
// retrieve and verify
|
||||||
|
let fetchResult = await cachePlugin
|
||||||
|
.fetch('https://resilient.is/test.json')
|
||||||
|
|
||||||
|
assert(fetchResult.ok, "fetchResult.ok is false")
|
||||||
|
assertEquals(fetchResult.status, 200, "fetchResult.status is not 200")
|
||||||
|
assertEquals(fetchResult.statusText, 'OK', "fetchResult.statusText is not 'OK'")
|
||||||
|
// assertEquals(fetchResult.url, 'https://resilient.is/test.json', "fetchResult.url is not correct") TODO
|
||||||
|
|
||||||
|
assert(fetchResult.headers.has('etag'), "fetchResult.headers does not contain 'ETag'")
|
||||||
|
assertEquals(fetchResult.headers.get('ETag'), 'TestingETagHeader')
|
||||||
|
|
||||||
|
let fetchedJSON = await fetchResult.json()
|
||||||
|
assertEquals(
|
||||||
|
fetchedJSON,
|
||||||
|
{ test: "success" },
|
||||||
|
"fetchedJSON is incorrect: " + JSON.stringify(fetchedJSON))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should stash a Response with an explicit key successfully", async () => {
|
||||||
|
|
||||||
|
const response = new Response(
|
||||||
|
new Blob(
|
||||||
|
[JSON.stringify({ test: "success" })],
|
||||||
|
{type: "application/json"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Response.url is read-only, so we need to hack around that
|
||||||
|
Object.defineProperty(
|
||||||
|
response,
|
||||||
|
"url",
|
||||||
|
{ value: 'https://resilient.is/test.json' });
|
||||||
|
|
||||||
|
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
||||||
|
|
||||||
|
let stashResult = await cachePlugin
|
||||||
|
.stash(response, 'https://example.com/special-key')
|
||||||
|
assertEquals(
|
||||||
|
stashResult,
|
||||||
|
undefined,
|
||||||
|
"stashResult should be undefined");
|
||||||
|
|
||||||
|
// retrieve and verify
|
||||||
|
let fetchResult = await cachePlugin
|
||||||
|
.fetch('https://example.com/special-key')
|
||||||
|
|
||||||
|
assert(fetchResult.ok, "fetchResult.ok is false")
|
||||||
|
assertEquals(fetchResult.status, 200, "fetchResult.status is not 200")
|
||||||
|
assertEquals(fetchResult.statusText, 'OK', "fetchResult.statusText is not 'OK'")
|
||||||
|
// assertEquals(fetchResult.url, 'https://resilient.is/test.json', "fetchResult.url is not correct") TODO
|
||||||
|
|
||||||
|
assert(fetchResult.headers.has('etag'), "fetchResult.headers does not contain 'ETag'")
|
||||||
|
assertEquals(fetchResult.headers.get('ETag'), 'TestingETagHeader')
|
||||||
|
|
||||||
|
let fetchedJSON = await fetchResult.json()
|
||||||
|
assertEquals(
|
||||||
|
fetchedJSON,
|
||||||
|
{ test: "success" },
|
||||||
|
"fetchedJSON is incorrect: " + JSON.stringify(fetchedJSON))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("it should stash a Response with no url set but with an explicit key successfully", async () => {
|
||||||
|
|
||||||
|
const response = new Response(
|
||||||
|
new Blob(
|
||||||
|
[JSON.stringify({ test: "success" })],
|
||||||
|
{type: "application/json"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
||||||
|
|
||||||
|
let stashResult = await cachePlugin
|
||||||
|
.stash(response, 'https://example.com/special-key')
|
||||||
|
assertEquals(
|
||||||
|
stashResult,
|
||||||
|
undefined,
|
||||||
|
"stashResult should be undefined");
|
||||||
|
|
||||||
|
// retrieve and verify
|
||||||
|
let fetchResult = await cachePlugin
|
||||||
|
.fetch('https://example.com/special-key')
|
||||||
|
|
||||||
|
assert(fetchResult.ok, "fetchResult.ok is false")
|
||||||
|
assertEquals(fetchResult.status, 200, "fetchResult.status is not 200")
|
||||||
|
assertEquals(fetchResult.statusText, 'OK', "fetchResult.statusText is not 'OK'")
|
||||||
|
// assertEquals(fetchResult.url, 'https://resilient.is/test.json', "fetchResult.url is not correct") TODO
|
||||||
|
|
||||||
|
assert(fetchResult.headers.has('etag'), "fetchResult.headers does not contain 'ETag'")
|
||||||
|
assertEquals(fetchResult.headers.get('ETag'), 'TestingETagHeader')
|
||||||
|
|
||||||
|
let fetchedJSON = await fetchResult.json()
|
||||||
|
assertEquals(
|
||||||
|
fetchedJSON,
|
||||||
|
{ test: "success" },
|
||||||
|
"fetchedJSON is incorrect: " + JSON.stringify(fetchedJSON))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clear a Response successfully", async () => {
|
||||||
|
|
||||||
|
const response = new Response(
|
||||||
|
new Blob(
|
||||||
|
[JSON.stringify({ test: "success" })],
|
||||||
|
{type: "application/json"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Response.url is read-only, so we need to hack around that
|
||||||
|
Object.defineProperty(
|
||||||
|
response,
|
||||||
|
"url",
|
||||||
|
{ value: 'https://resilient.is/test.json' });
|
||||||
|
|
||||||
|
var cachePlugin = LibResilientPluginConstructors.get('cache')(LR)
|
||||||
|
|
||||||
|
let stashResult = await cachePlugin
|
||||||
|
.stash(response)
|
||||||
|
assertEquals(
|
||||||
|
stashResult,
|
||||||
|
undefined,
|
||||||
|
"stashResult should be undefined");
|
||||||
|
|
||||||
|
// unstash
|
||||||
|
let unstashResult = await cachePlugin
|
||||||
|
.unstash('https://resilient.is/test.json')
|
||||||
|
assertEquals(unstashResult, true, "unstashResult should be true")
|
||||||
|
|
||||||
|
// verify
|
||||||
|
await assertRejects(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors
|
||||||
|
.get('cache')(LR)
|
||||||
|
.fetch('https://resilient.is/test.json')
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Resource not found in cache'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
})
|
|
@ -54,6 +54,36 @@
|
||||||
let cacheContent = (resource, key) => {
|
let cacheContent = (resource, key) => {
|
||||||
return caches.open('v1')
|
return caches.open('v1')
|
||||||
.then((cache) => {
|
.then((cache) => {
|
||||||
|
|
||||||
|
// polyfill cache.add()
|
||||||
|
// needed for tests and CLI, until Deno implements it natively
|
||||||
|
if (!("add" in cache)) {
|
||||||
|
cache.add = (url) => {
|
||||||
|
LR.log(pluginName, 'hit cache.add polyfill')
|
||||||
|
var result = fetch(url).then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("bad response!");
|
||||||
|
}
|
||||||
|
return cache.put(url, response);
|
||||||
|
});
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// polyfill cache.addAll()
|
||||||
|
// needed for tests and CLI, until Deno implements it natively
|
||||||
|
if (!("addAll" in cache)) {
|
||||||
|
cache.addAll = async (urls)=>{
|
||||||
|
LR.log(pluginName, 'hit cache.addAll polyfill')
|
||||||
|
await Promise.all(
|
||||||
|
urls.map((url)=>{
|
||||||
|
return cache.add(url)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof resource === 'string') {
|
if (typeof resource === 'string') {
|
||||||
// assume URL
|
// assume URL
|
||||||
LR.log(pluginName, "caching an URL: " + resource)
|
LR.log(pluginName, "caching an URL: " + resource)
|
||||||
|
|
|
@ -0,0 +1,516 @@
|
||||||
|
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:'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
})
|
|
@ -140,7 +140,7 @@
|
||||||
// remove the https://original.domain/ bit to get the relative path
|
// remove the https://original.domain/ bit to get the relative path
|
||||||
// TODO: this assumes that URLs we handle are always relative to the root
|
// TODO: this assumes that URLs we handle are always relative to the root
|
||||||
// TODO: of the original domain, this needs to be documented
|
// TODO: of the original domain, this needs to be documented
|
||||||
urlData = url.replace(/https?:\/\//, '').split('/')
|
var urlData = url.replace(/https?:\/\//, '').split('/')
|
||||||
var domain = urlData.shift()
|
var domain = urlData.shift()
|
||||||
var path = urlData.join('/')
|
var path = urlData.join('/')
|
||||||
LR.log(pluginName, '+-- fetching:\n',
|
LR.log(pluginName, '+-- fetching:\n',
|
||||||
|
|
|
@ -0,0 +1,255 @@
|
||||||
|
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-ipfs'
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ipfsPrototype = {
|
||||||
|
ipfsFixtureAddress: 'QmiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFS',
|
||||||
|
create: ()=>{
|
||||||
|
var sourceUsed = false
|
||||||
|
return Promise.resolve({
|
||||||
|
cat: (path)=>{
|
||||||
|
return {
|
||||||
|
next: ()=>{
|
||||||
|
if (path.endsWith('nonexistent.path')) {
|
||||||
|
throw new Error('Error: file does not exist')
|
||||||
|
}
|
||||||
|
let prevSourceUsed = sourceUsed
|
||||||
|
sourceUsed = true
|
||||||
|
var val = undefined
|
||||||
|
if (!prevSourceUsed) {
|
||||||
|
var val = Uint8Array.from(
|
||||||
|
Array
|
||||||
|
.from(JSON.stringify({
|
||||||
|
test: "success",
|
||||||
|
path: path
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
letter => letter.charCodeAt(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
done: prevSourceUsed,
|
||||||
|
value: val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
resolve: (path)=>{
|
||||||
|
var result = path.replace(
|
||||||
|
'/ipns/resilient.is',
|
||||||
|
'/ipfs/' + Ipfs.ipfsFixtureAddress
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
next: ()=> {
|
||||||
|
return Promise.resolve({
|
||||||
|
done: false,
|
||||||
|
value: result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
window.Ipfs = {
|
||||||
|
...window.ipfsPrototype
|
||||||
|
}
|
||||||
|
self.Ipfs = window.Ipfs
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('browser: dnslink-ipfs plugin', async () => {
|
||||||
|
window.LibResilientPluginConstructors = new Map()
|
||||||
|
window.LR = {
|
||||||
|
log: (component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.fetchResponse = []
|
||||||
|
window.resolvingFetch = null
|
||||||
|
window.fetch = null
|
||||||
|
window.Ipfs = null
|
||||||
|
|
||||||
|
await import("../../../plugins/dnslink-ipfs/index.js");
|
||||||
|
|
||||||
|
it("should register in LibResilientPluginConstructors", () => {
|
||||||
|
assertEquals(
|
||||||
|
LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init).name,
|
||||||
|
'dnslink-ipfs'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should initiate IPFS setup", async ()=>{
|
||||||
|
self.importScripts = spy(()=>{})
|
||||||
|
try {
|
||||||
|
await LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init).fetch('/test.json')
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
assertSpyCall(
|
||||||
|
importScripts,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
args: ['./lib/ipfs.js']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should error out when fetching unpublished content", async ()=>{
|
||||||
|
assertRejects(
|
||||||
|
async ()=>{
|
||||||
|
return await LibResilientPluginConstructors
|
||||||
|
.get('dnslink-ipfs')(LR, init)
|
||||||
|
.fetch('https://resilient.is/nonexistent.path')
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Error: file does not exist'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: probably not necessary in the long run?
|
||||||
|
it("should fetch <path>/index.html instead of a path ending in <path>/", async ()=>{
|
||||||
|
var response = await LibResilientPluginConstructors
|
||||||
|
.get('dnslink-ipfs')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test/')
|
||||||
|
assertEquals(
|
||||||
|
await response.json(),
|
||||||
|
{
|
||||||
|
test: "success",
|
||||||
|
path: "/ipfs/" + window.Ipfs.ipfsFixtureAddress + '/test/index.html'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should correctly guess content types when fetching", async ()=>{
|
||||||
|
|
||||||
|
let dnslinkIpfsPlugin = LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init)
|
||||||
|
let response = await dnslinkIpfsPlugin.fetch('https://resilient.is/test/')
|
||||||
|
assertEquals(response.headers.get("content-type"), 'text/html')
|
||||||
|
|
||||||
|
dnslinkIpfsPlugin = LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init)
|
||||||
|
response = await dnslinkIpfsPlugin.fetch('https://resilient.is/test.htm')
|
||||||
|
assertEquals(response.headers.get("content-type"), 'text/html')
|
||||||
|
|
||||||
|
dnslinkIpfsPlugin = LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init)
|
||||||
|
response = await dnslinkIpfsPlugin.fetch('https://resilient.is/test.css')
|
||||||
|
assertEquals(response.headers.get("content-type"), 'text/css')
|
||||||
|
|
||||||
|
dnslinkIpfsPlugin = LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init)
|
||||||
|
response = await dnslinkIpfsPlugin.fetch('https://resilient.is/test.js')
|
||||||
|
assertEquals(response.headers.get("content-type"), 'text/javascript')
|
||||||
|
|
||||||
|
dnslinkIpfsPlugin = LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init)
|
||||||
|
response = await dnslinkIpfsPlugin.fetch('https://resilient.is/test.json')
|
||||||
|
assertEquals(response.headers.get("content-type"), 'application/json')
|
||||||
|
|
||||||
|
dnslinkIpfsPlugin = LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init)
|
||||||
|
response = await dnslinkIpfsPlugin.fetch('https://resilient.is/test.svg')
|
||||||
|
assertEquals(response.headers.get("content-type"), 'image/svg+xml')
|
||||||
|
|
||||||
|
dnslinkIpfsPlugin = LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init)
|
||||||
|
response = await dnslinkIpfsPlugin.fetch('https://resilient.is/test.ico')
|
||||||
|
assertEquals(response.headers.get("content-type"), 'image/x-icon')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fetch content", async ()=>{
|
||||||
|
let response = await LibResilientPluginConstructors
|
||||||
|
.get('dnslink-ipfs')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json')
|
||||||
|
|
||||||
|
assertEquals(response.headers.get("content-type"), 'application/json')
|
||||||
|
assertEquals(
|
||||||
|
await response.json(),
|
||||||
|
{
|
||||||
|
test: "success",
|
||||||
|
path: "/ipfs/" + window.Ipfs.ipfsFixtureAddress + '/test.json'})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw an error on publish()", async ()=>{
|
||||||
|
assertThrows(
|
||||||
|
()=>{
|
||||||
|
LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init).publish()
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"Not implemented yet."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle IPFS load error correctly", async ()=>{
|
||||||
|
|
||||||
|
window.Ipfs.create = ()=>{
|
||||||
|
throw new Error('Testing IPFS loading failure')
|
||||||
|
}
|
||||||
|
|
||||||
|
assertRejects(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors
|
||||||
|
.get('dnslink-ipfs')(LR, init)
|
||||||
|
.fetch('/test.json')
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"Error: Testing IPFS loading failure"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle importScripts being undefined ", async ()=>{
|
||||||
|
self.importScripts = undefined
|
||||||
|
await LibResilientPluginConstructors.get('dnslink-ipfs')(LR, init).fetch('/test.json')
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,126 @@
|
||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
afterEach,
|
||||||
|
beforeEach
|
||||||
|
} from "https://deno.land/std@0.183.0/testing/bdd.ts";
|
||||||
|
|
||||||
|
import {
|
||||||
|
assert,
|
||||||
|
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";
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
window.fetch = spy(window.resolvingFetch)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(()=>{
|
||||||
|
window.fetch = null
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('browser: fetch plugin', async () => {
|
||||||
|
window.LibResilientPluginConstructors = new Map()
|
||||||
|
window.LR = {
|
||||||
|
log: (component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.resolvingFetch = (url, init) => {
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(
|
||||||
|
new Blob(
|
||||||
|
[JSON.stringify({ test: "success" })],
|
||||||
|
{type: "application/json"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
window.fetch = null
|
||||||
|
await import("../../../plugins/fetch/index.js");
|
||||||
|
|
||||||
|
it("should register in LibResilientPluginConstructors", () => {
|
||||||
|
assertEquals(LibResilientPluginConstructors.get('fetch')(LR).name, 'fetch');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return data from fetch()", async () => {
|
||||||
|
const response = await LibResilientPluginConstructors.get('fetch')(LR).fetch('https://resilient.is/test.json');
|
||||||
|
|
||||||
|
assertSpyCalls(fetch, 1);
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass the Request() init data to fetch()", async () => {
|
||||||
|
var initTest = {
|
||||||
|
method: "GET",
|
||||||
|
headers: new Headers({"x-stub": "STUB"}),
|
||||||
|
mode: "mode-stub",
|
||||||
|
credentials: "credentials-stub",
|
||||||
|
cache: "cache-stub",
|
||||||
|
referrer: "referrer-stub",
|
||||||
|
redirect: "redirect-stub",
|
||||||
|
integrity: "integrity-stub"
|
||||||
|
}
|
||||||
|
const response = await LibResilientPluginConstructors.get('fetch')(LR).fetch('https://resilient.is/test.json', initTest);
|
||||||
|
assertSpyCall(
|
||||||
|
fetch,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
'https://resilient.is/test.json',
|
||||||
|
initTest // TODO: does the initTest actually properly work here?
|
||||||
|
]
|
||||||
|
})
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set the LibResilient headers", async () => {
|
||||||
|
const response = await LibResilientPluginConstructors.get('fetch')(LR).fetch('https://resilient.is/test.json');
|
||||||
|
|
||||||
|
assertSpyCalls(fetch, 1);
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
|
||||||
|
assertEquals(response.headers.has('X-LibResilient-Method'), true)
|
||||||
|
assertEquals(response.headers.get('X-LibResilient-Method'), 'fetch')
|
||||||
|
assertEquals(response.headers.has('X-LibResilient-Etag'), true)
|
||||||
|
assertEquals(response.headers.get('X-LibResilient-ETag'), 'TestingETagHeader')
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when HTTP status is >= 400", async () => {
|
||||||
|
window.fetch = (url, init) => {
|
||||||
|
const response = new Response(
|
||||||
|
new Blob(
|
||||||
|
["Not Found"],
|
||||||
|
{type: "text/plain"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
statusText: "Not Found",
|
||||||
|
url: url
|
||||||
|
});
|
||||||
|
return Promise.resolve(response);
|
||||||
|
}
|
||||||
|
assertRejects(
|
||||||
|
async () => {
|
||||||
|
return await LibResilientPluginConstructors
|
||||||
|
.get('fetch')(LR)
|
||||||
|
.fetch('https://resilient.is/test.json') },
|
||||||
|
Error,
|
||||||
|
'HTTP Error: 404 Not Found'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
|
@ -0,0 +1,362 @@
|
||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
beforeEach,
|
||||||
|
beforeAll
|
||||||
|
} from "https://deno.land/std@0.183.0/testing/bdd.ts";
|
||||||
|
|
||||||
|
import {
|
||||||
|
assertEquals,
|
||||||
|
assertRejects,
|
||||||
|
assertThrows
|
||||||
|
} 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: 'gun-ipfs',
|
||||||
|
gunPubkey: 'stub'
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ipfsPrototype = {
|
||||||
|
ipfsFixtureAddress: 'QmiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFS',
|
||||||
|
create: ()=>{
|
||||||
|
var sourceUsed = false
|
||||||
|
return Promise.resolve({
|
||||||
|
get: (path)=>{
|
||||||
|
return {
|
||||||
|
next: ()=>{
|
||||||
|
if (path.endsWith('nonexistent.path')) {
|
||||||
|
throw new Error('Error: file does not exist')
|
||||||
|
}
|
||||||
|
let prevSourceUsed = sourceUsed
|
||||||
|
sourceUsed = true
|
||||||
|
var val = undefined
|
||||||
|
if (!prevSourceUsed) {
|
||||||
|
var val = Uint8Array.from(
|
||||||
|
Array
|
||||||
|
.from(JSON.stringify({
|
||||||
|
test: "success",
|
||||||
|
path: path
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
letter => letter.charCodeAt(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
done: prevSourceUsed,
|
||||||
|
value: val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
resolve: (path)=>{
|
||||||
|
var result = path.replace(
|
||||||
|
'/ipns/resilient.is',
|
||||||
|
'/ipfs/' + Ipfs.ipfsFixtureAddress
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
next: ()=> {
|
||||||
|
return Promise.resolve({
|
||||||
|
done: false,
|
||||||
|
value: result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gunUserFunction = ()=>{
|
||||||
|
return {
|
||||||
|
get: () => {
|
||||||
|
return {
|
||||||
|
get: (path)=>{
|
||||||
|
return {
|
||||||
|
once: (arg)=>{ arg('/ipfs/' + Ipfs.ipfsFixtureAddress + path) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.GunFunction = (nodes)=>{
|
||||||
|
return {
|
||||||
|
user: self.gunUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
window.Ipfs = {
|
||||||
|
...window.ipfsPrototype
|
||||||
|
}
|
||||||
|
self.Ipfs = window.Ipfs
|
||||||
|
window.gunUser = spy(window.gunUserFunction)
|
||||||
|
window.Gun = spy(window.GunFunction)
|
||||||
|
|
||||||
|
window.LR = {
|
||||||
|
log: spy((component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('browser: gun-ipfs plugin', async () => {
|
||||||
|
window.LibResilientPluginConstructors = new Map()
|
||||||
|
window.fetchResponse = []
|
||||||
|
window.resolvingFetch = null
|
||||||
|
window.fetch = null
|
||||||
|
window.Ipfs = null
|
||||||
|
window.Gun = null
|
||||||
|
window.gunUser = null
|
||||||
|
|
||||||
|
await import("../../../plugins/gun-ipfs/index.js");
|
||||||
|
|
||||||
|
it("should register in LibResilientPluginConstructors", () => {
|
||||||
|
assertEquals(
|
||||||
|
LibResilientPluginConstructors.get('gun-ipfs')(LR, init).name,
|
||||||
|
'gun-ipfs'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should initiate IPFS setup", async ()=>{
|
||||||
|
self.importScripts = spy(()=>{})
|
||||||
|
try {
|
||||||
|
await LibResilientPluginConstructors.get('gun-ipfs')(LR, init).fetch('/test.json')
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
assertSpyCall(
|
||||||
|
importScripts,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
args: ['./lib/ipfs.js']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should initiate Gun setup", async ()=>{
|
||||||
|
self.importScripts = spy(()=>{})
|
||||||
|
try {
|
||||||
|
await LibResilientPluginConstructors.get('gun-ipfs')(LR, init).fetch('/test.json')
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
assertSpyCall(
|
||||||
|
importScripts,
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
args: ['./lib/gun.js', "./lib/sea.js", "./lib/webrtc.js"]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should error out when fetching unpublished content", async ()=>{
|
||||||
|
assertRejects(
|
||||||
|
async ()=>{
|
||||||
|
return await LibResilientPluginConstructors
|
||||||
|
.get('gun-ipfs')(LR, init)
|
||||||
|
.fetch('https://resilient.is/nonexistent.path')
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Error: file does not exist'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: probably not necessary in the long run?
|
||||||
|
it("should fetch <path>/index.html instead of a path ending in <path>/", async ()=>{
|
||||||
|
await LibResilientPluginConstructors
|
||||||
|
.get('gun-ipfs')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test/')
|
||||||
|
assertSpyCall(
|
||||||
|
window.LR.log,
|
||||||
|
15,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
"gun-ipfs",
|
||||||
|
"path ends in '/', assuming 'index.html' should be appended."
|
||||||
|
]})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should correctly guess content types when fetching", async ()=>{
|
||||||
|
|
||||||
|
let gunIpfsPlugin = LibResilientPluginConstructors.get('gun-ipfs')(LR, init)
|
||||||
|
let response = await gunIpfsPlugin.fetch('https://resilient.is/test/')
|
||||||
|
assertSpyCall(
|
||||||
|
window.LR.log,
|
||||||
|
17,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
"gun-ipfs",
|
||||||
|
" +-- guessed contentType : text/html"
|
||||||
|
]})
|
||||||
|
|
||||||
|
window.LR.log = spy((component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
})
|
||||||
|
gunIpfsPlugin = LibResilientPluginConstructors.get('gun-ipfs')(LR, init)
|
||||||
|
response = await gunIpfsPlugin.fetch('https://resilient.is/test.htm')
|
||||||
|
assertSpyCall(
|
||||||
|
window.LR.log,
|
||||||
|
16,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
"gun-ipfs",
|
||||||
|
" +-- guessed contentType : text/html"
|
||||||
|
]})
|
||||||
|
|
||||||
|
window.LR.log = spy((component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
})
|
||||||
|
gunIpfsPlugin = LibResilientPluginConstructors.get('gun-ipfs')(LR, init)
|
||||||
|
response = await gunIpfsPlugin.fetch('https://resilient.is/test.css')
|
||||||
|
assertSpyCall(
|
||||||
|
window.LR.log,
|
||||||
|
16,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
"gun-ipfs",
|
||||||
|
" +-- guessed contentType : text/css"
|
||||||
|
]})
|
||||||
|
|
||||||
|
window.LR.log = spy((component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
})
|
||||||
|
gunIpfsPlugin = LibResilientPluginConstructors.get('gun-ipfs')(LR, init)
|
||||||
|
response = await gunIpfsPlugin.fetch('https://resilient.is/test.js')
|
||||||
|
assertSpyCall(
|
||||||
|
window.LR.log,
|
||||||
|
16,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
"gun-ipfs",
|
||||||
|
" +-- guessed contentType : text/javascript"
|
||||||
|
]})
|
||||||
|
|
||||||
|
window.LR.log = spy((component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
})
|
||||||
|
gunIpfsPlugin = LibResilientPluginConstructors.get('gun-ipfs')(LR, init)
|
||||||
|
response = await gunIpfsPlugin.fetch('https://resilient.is/test.json')
|
||||||
|
assertSpyCall(
|
||||||
|
window.LR.log,
|
||||||
|
16,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
"gun-ipfs",
|
||||||
|
" +-- guessed contentType : application/json"
|
||||||
|
]})
|
||||||
|
|
||||||
|
window.LR.log = spy((component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
})
|
||||||
|
gunIpfsPlugin = LibResilientPluginConstructors.get('gun-ipfs')(LR, init)
|
||||||
|
response = await gunIpfsPlugin.fetch('https://resilient.is/test.svg')
|
||||||
|
assertSpyCall(
|
||||||
|
window.LR.log,
|
||||||
|
16,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
"gun-ipfs",
|
||||||
|
" +-- guessed contentType : image/svg+xml"
|
||||||
|
]})
|
||||||
|
|
||||||
|
window.LR.log = spy((component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
})
|
||||||
|
gunIpfsPlugin = LibResilientPluginConstructors.get('gun-ipfs')(LR, init)
|
||||||
|
response = await gunIpfsPlugin.fetch('https://resilient.is/test.ico')
|
||||||
|
assertSpyCall(
|
||||||
|
window.LR.log,
|
||||||
|
16,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
"gun-ipfs",
|
||||||
|
" +-- guessed contentType : image/x-icon"
|
||||||
|
]})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should fetch content (stub!)", async ()=>{
|
||||||
|
|
||||||
|
let response = await LibResilientPluginConstructors
|
||||||
|
.get('gun-ipfs')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json')
|
||||||
|
|
||||||
|
assertSpyCall(
|
||||||
|
window.LR.log,
|
||||||
|
16,
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
"gun-ipfs",
|
||||||
|
" +-- guessed contentType : application/json"
|
||||||
|
]})
|
||||||
|
// TODO: implement actual content check once gun-ipfs plugin gets updated
|
||||||
|
// to work with current IPFS version
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should error out if publishContent() is passed anything else than string or array of string", async ()=>{
|
||||||
|
var gunipfsPlugin = LibResilientPluginConstructors.get('gun-ipfs')(LR, init)
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
()=>{
|
||||||
|
gunipfsPlugin.publish({
|
||||||
|
url: 'https://resilient.is/test.json'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Handling a Response: not implemented yet')
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
()=>{
|
||||||
|
gunipfsPlugin.publish(true)
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Only accepts: string, Array of string, Response.')
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
()=>{
|
||||||
|
gunipfsPlugin.publish([true, 5])
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Only accepts: string, Array of string, Response.')
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,253 @@
|
||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
beforeEach,
|
||||||
|
beforeAll
|
||||||
|
} from "https://deno.land/std@0.183.0/testing/bdd.ts";
|
||||||
|
|
||||||
|
import {
|
||||||
|
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.resolvingFetch = (url, init)=>{
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(
|
||||||
|
['{"test": "success"}'],
|
||||||
|
{
|
||||||
|
type: "application/json",
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
},
|
||||||
|
url: url
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* prototype of the plugin init object
|
||||||
|
*/
|
||||||
|
window.initPrototype = {
|
||||||
|
name: 'integrity-check',
|
||||||
|
uses: [
|
||||||
|
{
|
||||||
|
name: 'resolve-all',
|
||||||
|
description: 'Resolves all',
|
||||||
|
version: '0.0.1',
|
||||||
|
fetch: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* integrity data in init object to be passed to fetch()
|
||||||
|
* for the plugin to verify
|
||||||
|
*/
|
||||||
|
window.requestInit = {
|
||||||
|
sha256: {
|
||||||
|
integrity: "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="
|
||||||
|
},
|
||||||
|
sha384: {
|
||||||
|
integrity: "sha384-x4iqiH3PIPD51TibGEhTju/WhidcIEcnrpdklYEtIS87f96c4nLyj6CuwUp8kyOo"
|
||||||
|
},
|
||||||
|
sha512: {
|
||||||
|
integrity: "sha512-o+J3lPk7DU8xOJaNfZI5T4Upmaoc9XOVxOWPCFAy4pTgvS8LrJZ8iNis/2ZaryU4bB33cNSXQBxUDvwDxknEBQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.init = {
|
||||||
|
...window.initPrototype
|
||||||
|
}
|
||||||
|
window.init.uses[0].fetch = window.fetch
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('browser: integrity-check plugin', async () => {
|
||||||
|
window.LibResilientPluginConstructors = new Map()
|
||||||
|
window.LR = {
|
||||||
|
log: (component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.resolvingFetch = null
|
||||||
|
window.fetch = null
|
||||||
|
window.subtle = crypto.subtle
|
||||||
|
|
||||||
|
await import("../../../plugins/integrity-check/index.js");
|
||||||
|
|
||||||
|
it("should register in LibResilientPluginConstructors", () => {
|
||||||
|
assertEquals(
|
||||||
|
LibResilientPluginConstructors
|
||||||
|
.get('integrity-check')(LR, init).name,
|
||||||
|
'integrity-check');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when there aren't any wrapped plugins configured", () => {
|
||||||
|
init = {
|
||||||
|
name: 'integrity-check',
|
||||||
|
uses: []
|
||||||
|
}
|
||||||
|
assertThrows(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors.get('integrity-check')(LR, init)
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Expected exactly one plugin to wrap, but 0 configured.'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when there are more than one wrapped plugins configured", () => {
|
||||||
|
init = {
|
||||||
|
name: 'integrity-check',
|
||||||
|
uses: ['plugin-one', 'plugin-two']
|
||||||
|
}
|
||||||
|
assertThrows(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors.get('integrity-check')(LR, init)
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Expected exactly one plugin to wrap, but 2 configured.'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when an unsupported digest algorithm is used", async () => {
|
||||||
|
|
||||||
|
assertRejects(async ()=>{
|
||||||
|
return await LibResilientPluginConstructors
|
||||||
|
.get('integrity-check')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json', {
|
||||||
|
integrity: "sha000-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="
|
||||||
|
})
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'No digest matched for:'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("it should return data from the wrapped plugin when no integrity data is available and requireIntegrity is false (default)", async () => {
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json');
|
||||||
|
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
assertSpyCalls(fetch, 1)
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/test.json', {}]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject no integrity data is available but requireIntegrity is true", async () => {
|
||||||
|
|
||||||
|
init.requireIntegrity = true
|
||||||
|
|
||||||
|
assertRejects(async ()=>{
|
||||||
|
return await LibResilientPluginConstructors
|
||||||
|
.get('integrity-check')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json')
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Integrity data required but not provided for:'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should check integrity and return data from the wrapped plugin if SHA-256 integrity data matches", async () => {
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors
|
||||||
|
.get('integrity-check')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json', requestInit.sha256);
|
||||||
|
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
assertSpyCalls(fetch, 1)
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/test.json', requestInit.sha256]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should check integrity and return data from the wrapped plugin if SHA-384 integrity data matches", async () => {
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors
|
||||||
|
.get('integrity-check')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json', requestInit.sha384);
|
||||||
|
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
assertSpyCalls(fetch, 1)
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/test.json', requestInit.sha384]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should check integrity and return data from the wrapped plugin if SHA-512 integrity data matches", async () => {
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors
|
||||||
|
.get('integrity-check')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json', requestInit.sha512);
|
||||||
|
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
assertSpyCalls(fetch, 1)
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/test.json', requestInit.sha512]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should check integrity of the data returned from the wrapped plugin and reject if it doesn't match", async () => {
|
||||||
|
|
||||||
|
assertRejects(async ()=>{
|
||||||
|
return await LibResilientPluginConstructors
|
||||||
|
.get('integrity-check')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json', {
|
||||||
|
integrity: "sha256-INCORRECTINCORRECTINCORRECTINCORRECTINCORREC"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'No digest matched for:'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should check integrity of the data returned from the wrapped plugin and resolve if at least one of multiple integrity hash matches", async () => {
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('integrity-check')(LR, init).fetch('https://resilient.is/test.json', {
|
||||||
|
integrity: "sha256-INCORRECTINCORRECTINCORRECTINCORRECTINCORREC sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
assertSpyCalls(fetch, 1)
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/test.json',
|
||||||
|
{
|
||||||
|
integrity: "sha256-INCORRECTINCORRECTINCORRECTINCORRECTINCORREC sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should check integrity of the data returned from the wrapped plugin and reject if all out of multiple integrity hash do not match", async () => {
|
||||||
|
|
||||||
|
assertRejects(async ()=>{
|
||||||
|
return await LibResilientPluginConstructors
|
||||||
|
.get('integrity-check')(LR, init)
|
||||||
|
.fetch('https://resilient.is/test.json', {
|
||||||
|
integrity: "sha256-INCORRECTINCORRECTINCORRECTINCORRECTINCORREC sha256-WRONGWRONGWRONGWRONGWRONGWRONGWRONGWRONGWRON"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'No digest matched for:'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
})
|
|
@ -114,7 +114,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch data using the configured wrapped plugin
|
// fetch data using the configured wrapped plugin
|
||||||
responsePromise = config.uses[0].fetch(url, init)
|
var responsePromise = config.uses[0].fetch(url, init)
|
||||||
|
|
||||||
// if we have no integrity data, we really do not have anything to do
|
// if we have no integrity data, we really do not have anything to do
|
||||||
// apart from returning the response
|
// apart from returning the response
|
||||||
|
@ -144,7 +144,11 @@
|
||||||
// it's a string, we need an array
|
// it's a string, we need an array
|
||||||
nextIntegrityHash = nextIntegrityHash.split('-')
|
nextIntegrityHash = nextIntegrityHash.split('-')
|
||||||
// make sure the algo name is compatible with SubtleCrypto digest algo names
|
// make sure the algo name is compatible with SubtleCrypto digest algo names
|
||||||
nextIntegrityHash[0] = getAlgo(nextIntegrityHash[0])
|
try {
|
||||||
|
nextIntegrityHash[0] = getAlgo(nextIntegrityHash[0])
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(e)
|
||||||
|
}
|
||||||
LR.log(pluginName, `verifying integrity for: ${url}\n- algo: ${nextIntegrityHash[0]}\n- hash: ${nextIntegrityHash[1]}`)
|
LR.log(pluginName, `verifying integrity for: ${url}\n- algo: ${nextIntegrityHash[0]}\n- hash: ${nextIntegrityHash[1]}`)
|
||||||
return crypto
|
return crypto
|
||||||
.subtle
|
.subtle
|
||||||
|
@ -157,7 +161,7 @@
|
||||||
LR.log(pluginName, `successfully verified integrity for: ${url}\n- algo: ${nextIntegrityHash[0]}\n- hash: ${nextIntegrityHash[1]}`)
|
LR.log(pluginName, `successfully verified integrity for: ${url}\n- algo: ${nextIntegrityHash[0]}\n- hash: ${nextIntegrityHash[1]}`)
|
||||||
return responsePromise
|
return responsePromise
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject('Digest does not match.')
|
return Promise.reject(Error('Digest does not match.'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -165,7 +169,7 @@
|
||||||
Promise.reject())
|
Promise.reject())
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
return Promise.reject(`No digest matched for: ${url}`)
|
return Promise.reject(Error(`No digest matched for: ${url}`))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
beforeEach,
|
||||||
|
beforeAll
|
||||||
|
} from "https://deno.land/std@0.183.0/testing/bdd.ts";
|
||||||
|
|
||||||
|
import {
|
||||||
|
assertEquals
|
||||||
|
} from "https://deno.land/std@0.183.0/testing/asserts.ts";
|
||||||
|
|
||||||
|
import {
|
||||||
|
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: 'ipns-ipfs',
|
||||||
|
ipnsPubkey: 'stub'
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ipfsPrototype = {
|
||||||
|
ipfsFixtureAddress: 'QmiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFSiPFS',
|
||||||
|
create: ()=>{
|
||||||
|
console.log('*** Ipfs.create()')
|
||||||
|
var sourceUsed = false
|
||||||
|
return Promise.resolve({
|
||||||
|
cat: (path)=>{
|
||||||
|
return {
|
||||||
|
next: ()=>{
|
||||||
|
if (path.endsWith('nonexistent.path')) {
|
||||||
|
throw new Error('Error: file does not exist')
|
||||||
|
}
|
||||||
|
let prevSourceUsed = sourceUsed
|
||||||
|
sourceUsed = true
|
||||||
|
var val = undefined
|
||||||
|
if (!prevSourceUsed) {
|
||||||
|
var val = Uint8Array.from(
|
||||||
|
Array
|
||||||
|
.from(JSON.stringify({
|
||||||
|
test: "success",
|
||||||
|
path: path
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
letter => letter.charCodeAt(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
done: prevSourceUsed,
|
||||||
|
value: val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
resolve: (path)=>{
|
||||||
|
var result = path.replace(
|
||||||
|
'/ipns/resilient.is',
|
||||||
|
'/ipfs/' + Ipfs.ipfsFixtureAddress
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
next: ()=> {
|
||||||
|
return Promise.resolve({
|
||||||
|
done: false,
|
||||||
|
value: result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
window.Ipfs = {
|
||||||
|
...window.ipfsPrototype
|
||||||
|
}
|
||||||
|
self.Ipfs = window.Ipfs
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('browser: ipns-ipfs plugin', async () => {
|
||||||
|
window.LibResilientPluginConstructors = new Map()
|
||||||
|
window.LR = {
|
||||||
|
log: (component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.fetchResponse = []
|
||||||
|
window.resolvingFetch = null
|
||||||
|
window.fetch = null
|
||||||
|
window.Ipfs = null
|
||||||
|
|
||||||
|
await import("../../../plugins/ipns-ipfs/index.js");
|
||||||
|
|
||||||
|
it("should register in LibResilientPluginConstructors", () => {
|
||||||
|
assertEquals(
|
||||||
|
LibResilientPluginConstructors.get('ipns-ipfs')(LR, init).name,
|
||||||
|
'ipns-ipfs'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
|
@ -0,0 +1,136 @@
|
||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
afterEach,
|
||||||
|
beforeEach
|
||||||
|
} 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";
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
window.resolvingFetchSpy = spy(window.resolvingFetch)
|
||||||
|
window.init = {
|
||||||
|
name: 'redirect',
|
||||||
|
redirectStatus: 302,
|
||||||
|
redirectStatusText: "Found",
|
||||||
|
redirectTo: "https://redirected.example.org/subdir/"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(()=>{
|
||||||
|
window.init = null
|
||||||
|
window.resolvingFetchSpy = null
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('browser: redirect plugin', async () => {
|
||||||
|
window.LibResilientPluginConstructors = new Map()
|
||||||
|
window.LR = {
|
||||||
|
log: (component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.resolvingFetch = (url, init) => {
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(
|
||||||
|
new Blob(
|
||||||
|
[JSON.stringify({ test: "success" })],
|
||||||
|
{type: "application/json"}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
window.resolvingFetchSpy = null
|
||||||
|
await import("../../../plugins/redirect/index.js");
|
||||||
|
|
||||||
|
it("should register in LibResilientPluginConstructors", () => {
|
||||||
|
init = {
|
||||||
|
name: 'redirect',
|
||||||
|
redirectTo: 'https://example.org/'
|
||||||
|
}
|
||||||
|
assertEquals(LibResilientPluginConstructors.get('redirect')(LR, init).name, 'redirect');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with incorrect redirectTo config value", () => {
|
||||||
|
init = {
|
||||||
|
name: 'redirect',
|
||||||
|
redirectTo: false
|
||||||
|
}
|
||||||
|
assertThrows(
|
||||||
|
()=>{
|
||||||
|
LibResilientPluginConstructors.get('redirect')(LR, init)
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"redirectTo should be a string"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with incorrect redirectStatus config value", () => {
|
||||||
|
init = {
|
||||||
|
name: 'redirect',
|
||||||
|
redirectTo: 'https://example.org/',
|
||||||
|
redirectStatus: 'incorrect'
|
||||||
|
}
|
||||||
|
assertThrows(
|
||||||
|
()=>{
|
||||||
|
LibResilientPluginConstructors.get('redirect')(LR, init)
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"redirectStatus should be a number"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with incorrect redirectStatusText config value", () => {
|
||||||
|
init = {
|
||||||
|
name: 'redirect',
|
||||||
|
redirectTo: 'https://example.org/',
|
||||||
|
redirectStatusText: false
|
||||||
|
}
|
||||||
|
assertThrows(
|
||||||
|
()=>{
|
||||||
|
LibResilientPluginConstructors.get('redirect')(LR, init)
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"redirectStatusText should be a string"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should register in LibResilientPluginConstructors without error even if all config data is incorrect, as long as enabled is false", () => {
|
||||||
|
init = {
|
||||||
|
name: 'redirect',
|
||||||
|
redirectTo: false,
|
||||||
|
redirectStatus: "incorrect",
|
||||||
|
redirectStatusText: false,
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
assertEquals(LibResilientPluginConstructors.get('redirect')(LR, init).name, 'redirect');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a 302 Found redirect to a configured location for any request", async () => {
|
||||||
|
init = {
|
||||||
|
name: 'redirect',
|
||||||
|
redirectTo: "https://redirected.example.org/subdirectory/"
|
||||||
|
}
|
||||||
|
const response = await LibResilientPluginConstructors.get('redirect')(LR, init).fetch('https://resilient.is/test.json');
|
||||||
|
assertEquals(response.status, 302)
|
||||||
|
assertEquals(response.statusText, 'Found')
|
||||||
|
assertEquals(response.headers.get('location'), 'https://redirected.example.org/subdirectory/test.json')
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,411 @@
|
||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
beforeEach,
|
||||||
|
beforeAll
|
||||||
|
} from "https://deno.land/std@0.183.0/testing/bdd.ts";
|
||||||
|
|
||||||
|
import {
|
||||||
|
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";
|
||||||
|
|
||||||
|
async function generateECDSAKeypair() {
|
||||||
|
return await crypto.subtle.generateKey({
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: "P-384"
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["sign", "verify"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getArmouredKey(key) {
|
||||||
|
return JSON.stringify(await crypto.subtle.exportKey('jwk', key))
|
||||||
|
}
|
||||||
|
|
||||||
|
function jwtize(str) {
|
||||||
|
return btoa(str)
|
||||||
|
.replace(/\//g, '_')
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/=/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* k - keypair
|
||||||
|
* h - header
|
||||||
|
* p - payload
|
||||||
|
*/
|
||||||
|
async function getSignature(k, h, p) {
|
||||||
|
|
||||||
|
// we need a TextEncoder
|
||||||
|
var tenc = new TextEncoder()
|
||||||
|
var tencoded = tenc.encode(h + '.' + p)
|
||||||
|
|
||||||
|
// prepare a signature
|
||||||
|
var sig = new Uint8Array(await crypto.subtle.sign(
|
||||||
|
{
|
||||||
|
name: "ECDSA",
|
||||||
|
hash: {name: "SHA-384"}
|
||||||
|
},
|
||||||
|
k.privateKey,
|
||||||
|
tencoded
|
||||||
|
))
|
||||||
|
|
||||||
|
// prepare it for inclusion in a JWT
|
||||||
|
var sig = sig.reduce((str, cur)=>{return (str + String.fromCharCode(cur)) }, '')
|
||||||
|
return jwtize(sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async ()=>{
|
||||||
|
// our keypair
|
||||||
|
var keypair = await generateECDSAKeypair()
|
||||||
|
|
||||||
|
// ES384: ECDSA using P-384 and SHA-384
|
||||||
|
var header = jwtize('{"alg": "ES384"}')
|
||||||
|
var payload = jwtize('{"integrity": "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0="}')
|
||||||
|
|
||||||
|
// get a signature
|
||||||
|
var signature = await getSignature(keypair, header, payload)
|
||||||
|
|
||||||
|
// need to test with bad algo!
|
||||||
|
var noneHeader = jwtize('{"alg": "none"}')
|
||||||
|
|
||||||
|
// get an invalid signature
|
||||||
|
// an ECDSA signature for {alg: none} header makes zero sense
|
||||||
|
var noneSignature = await getSignature(keypair, noneHeader, payload)
|
||||||
|
|
||||||
|
// prepare stuff for invalid JWT JSON test
|
||||||
|
var invalidPayload = jwtize('not a valid JSON string')
|
||||||
|
// get an valid signature for invalid payload
|
||||||
|
var invalidPayloadSignature = await getSignature(keypair, header, invalidPayload)
|
||||||
|
|
||||||
|
// prepare stuff for JWT payload without integrity test
|
||||||
|
var noIntegrityPayload = jwtize('{"no": "integrity"}')
|
||||||
|
// get an valid signature for invalid payload
|
||||||
|
var noIntegrityPayloadSignature = await getSignature(keypair, header, noIntegrityPayload)
|
||||||
|
|
||||||
|
window.resolvingFetch = (url, init)=>{
|
||||||
|
var content = '{"test": "success"}'
|
||||||
|
var status = 200
|
||||||
|
var statusText = "OK"
|
||||||
|
|
||||||
|
if (url == 'https://resilient.is/test.json.integrity') {
|
||||||
|
content = header + '.' + payload + '.' + signature
|
||||||
|
// testing 404 not found on the integrity URL
|
||||||
|
} else if (url == 'https://resilient.is/not-found.json.integrity') {
|
||||||
|
content = '{"test": "fail"}'
|
||||||
|
status = 404
|
||||||
|
statusText = "Not Found"
|
||||||
|
// testing invalid base64-encoded data
|
||||||
|
} else if (url == 'https://resilient.is/invalid-base64.json.integrity') {
|
||||||
|
// for this test to work correctly the length must be (n*4)+1
|
||||||
|
content = header + '.' + payload + '.' + 'badbase64'
|
||||||
|
// testing "alg: none" on the integrity JWT
|
||||||
|
} else if (url == 'https://resilient.is/alg-none.json.integrity') {
|
||||||
|
content = noneHeader + '.' + payload + '.'
|
||||||
|
// testing bad signature on the integrity JWT
|
||||||
|
} else if (url == 'https://resilient.is/bad-signature.json.integrity') {
|
||||||
|
content = header + '.' + payload + '.' + noneSignature
|
||||||
|
// testing invalid payload
|
||||||
|
} else if (url == 'https://resilient.is/invalid-payload.json.integrity') {
|
||||||
|
content = header + '.' + invalidPayload + '.' + invalidPayloadSignature
|
||||||
|
// testing payload without integrity data
|
||||||
|
} else if (url == 'https://resilient.is/no-integrity.json.integrity') {
|
||||||
|
content = header + '.' + noIntegrityPayload + '.' + noIntegrityPayloadSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(
|
||||||
|
[content],
|
||||||
|
{
|
||||||
|
type: "application/json",
|
||||||
|
status: status,
|
||||||
|
statusText: statusText,
|
||||||
|
headers: {
|
||||||
|
'ETag': 'TestingETagHeader'
|
||||||
|
},
|
||||||
|
url: url
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.initPrototype = {
|
||||||
|
name: 'signed-integrity',
|
||||||
|
uses: [
|
||||||
|
{
|
||||||
|
name: 'resolve-all',
|
||||||
|
description: 'Resolves all',
|
||||||
|
version: '0.0.1',
|
||||||
|
fetch: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
integrity: {
|
||||||
|
"https://resilient.is/test.json": "sha384-kn5dhxz4RpBmx7xC7Dmq2N43PclV9U/niyh+4Km7oz5W0FaWdz3Op+3K0Qxz8y3z"
|
||||||
|
},
|
||||||
|
//requireIntegrity: false, // default is false
|
||||||
|
publicKey: await crypto.subtle.exportKey('jwk', keypair.publicKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.init = {
|
||||||
|
...window.initPrototype
|
||||||
|
}
|
||||||
|
window.init.uses[0].fetch = window.fetch
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('browser: signed-integrity plugin', async () => {
|
||||||
|
window.LibResilientPluginConstructors = new Map()
|
||||||
|
window.LR = {
|
||||||
|
log: (component, ...items)=>{
|
||||||
|
console.debug(component + ' :: ', ...items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.resolvingFetch = null
|
||||||
|
window.fetch = null
|
||||||
|
window.subtle = crypto.subtle
|
||||||
|
|
||||||
|
await import("../../../plugins/signed-integrity/index.js");
|
||||||
|
|
||||||
|
it("should register in LibResilientPluginConstructors", async () => {
|
||||||
|
assertEquals(
|
||||||
|
LibResilientPluginConstructors
|
||||||
|
.get('signed-integrity')(LR, init)
|
||||||
|
.name,
|
||||||
|
'signed-integrity');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when there aren't any wrapped plugins configured", () => {
|
||||||
|
init = {
|
||||||
|
name: 'signed-integrity',
|
||||||
|
uses: []
|
||||||
|
}
|
||||||
|
assertThrows(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors.get('signed-integrity')(LR, init)
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Expected exactly one plugin to wrap, but 0 configured.'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when there are more than one wrapped plugins configured", () => {
|
||||||
|
init = {
|
||||||
|
name: 'signed-integrity',
|
||||||
|
uses: ['plugin-one', 'plugin-two']
|
||||||
|
}
|
||||||
|
assertThrows(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors.get('signed-integrity')(LR, init)
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Expected exactly one plugin to wrap, but 2 configured.'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if the configured public key is impossible to load", async () => {
|
||||||
|
|
||||||
|
var testInit = {
|
||||||
|
...window.init
|
||||||
|
}
|
||||||
|
testInit.publicKey = 'NOTAKEY'
|
||||||
|
|
||||||
|
assertRejects(
|
||||||
|
async ()=>{
|
||||||
|
return await LibResilientPluginConstructors.get('signed-integrity')(LR, testInit).fetch('https://resilient.is/test.json')
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Unable to load the public key'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* we're only testing if signed-integrity plugin accepts that integrity data is set
|
||||||
|
* without pulling the .integrity file
|
||||||
|
*
|
||||||
|
* this will *not* result in the resource integrity *actually* being checked!
|
||||||
|
*/
|
||||||
|
it("should fetch content when integrity data provided without trying to fetch the integrity data URL", async () => {
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json', {
|
||||||
|
integrity: "sha384-x4iqiH3PIPD51TibGEhTju/WhidcIEcnrpdklYEtIS87f96c4nLyj6CuwUp8kyOo"
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
assertSpyCalls(fetch, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch content when integrity data not provided, by also fetching the integrity data URL", async () => {
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json', {});
|
||||||
|
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
assertSpyCalls(fetch, 2)
|
||||||
|
// the integrity file fetch has to happen first
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/test.json.integrity']
|
||||||
|
})
|
||||||
|
// the content fetch needs to have integrity data available
|
||||||
|
assertSpyCall(fetch, 1, {
|
||||||
|
args: [
|
||||||
|
"https://resilient.is/test.json",
|
||||||
|
{
|
||||||
|
integrity: "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0=",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch content when integrity data not provided, and integrity data URL 404s", async () => {
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/not-found.json', {});
|
||||||
|
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
assertSpyCalls(fetch, 2)
|
||||||
|
// the integrity file fetch has to happen first
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/not-found.json.integrity']
|
||||||
|
})
|
||||||
|
// the content fetch needs to have integrity data available
|
||||||
|
assertSpyCall(fetch, 1, {
|
||||||
|
args: [
|
||||||
|
"https://resilient.is/not-found.json",
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should refuse to fetch content when integrity data not provided and integrity data URL 404s, but requireIntegrity is set to true", async () => {
|
||||||
|
|
||||||
|
init.requireIntegrity = true
|
||||||
|
|
||||||
|
assertRejects(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/not-found.json', {})
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'No integrity data available, though required.'
|
||||||
|
)
|
||||||
|
assertSpyCalls(fetch, 1)
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/not-found.json.integrity']
|
||||||
|
})
|
||||||
|
|
||||||
|
//expect(e.toString()).toMatch('No integrity data available, though required.')
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT is invalid", async () => {
|
||||||
|
|
||||||
|
assertRejects(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/invalid-base64.json', {})
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'Invalid base64-encoded string!'
|
||||||
|
)
|
||||||
|
assertSpyCalls(fetch, 1)
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/invalid-base64.json.integrity']
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT uses alg: none", async () => {
|
||||||
|
|
||||||
|
assertRejects(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/alg-none.json', {})
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'JWT seems invalid (one or more sections are empty).'
|
||||||
|
)
|
||||||
|
|
||||||
|
assertSpyCalls(fetch, 1)
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/alg-none.json.integrity']
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT signature check fails", async () => {
|
||||||
|
|
||||||
|
assertRejects(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/bad-signature.json', {})
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'JWT signature validation failed! Somebody might be doing something nasty!'
|
||||||
|
)
|
||||||
|
|
||||||
|
assertSpyCalls(fetch, 1)
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/bad-signature.json.integrity']
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT payload is unparseable", async () => {
|
||||||
|
|
||||||
|
assertRejects(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/invalid-payload.json', {})
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'JWT payload parsing failed'
|
||||||
|
)
|
||||||
|
|
||||||
|
assertSpyCalls(fetch, 1)
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/invalid-payload.json.integrity']
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should refuse to fetch content when integrity data not provided and integrity data URL is fetched, but JWT payload does not contain integrity data", async () => {
|
||||||
|
|
||||||
|
assertRejects(
|
||||||
|
()=>{
|
||||||
|
return LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/no-integrity.json', {})
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
'JWT payload did not contain integrity data'
|
||||||
|
)
|
||||||
|
|
||||||
|
assertSpyCalls(fetch, 1)
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/no-integrity.json.integrity']
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch and verify content, when integrity data not provided, by fetching the integrity data URL and using integrity data from it", async () => {
|
||||||
|
|
||||||
|
const response = await LibResilientPluginConstructors.get('signed-integrity')(LR, init).fetch('https://resilient.is/test.json', {});
|
||||||
|
|
||||||
|
assertEquals(await response.json(), {test: "success"})
|
||||||
|
assertSpyCalls(fetch, 2)
|
||||||
|
// the integrity file fetch has to happen first
|
||||||
|
assertSpyCall(fetch, 0, {
|
||||||
|
args: ['https://resilient.is/test.json.integrity']
|
||||||
|
})
|
||||||
|
// the content fetch needs to have integrity data available
|
||||||
|
assertSpyCall(fetch, 1, {
|
||||||
|
args: [
|
||||||
|
"https://resilient.is/test.json",
|
||||||
|
{
|
||||||
|
integrity: "sha256-eiMrFuthzteJuj8fPwUMyNQMb2SMW7VITmmt2oAxGj0=",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
assert,
|
assert,
|
||||||
assertThrows,
|
|
||||||
assertRejects,
|
assertRejects,
|
||||||
assertEquals,
|
assertEquals,
|
||||||
assertStringIncludes,
|
assertStringIncludes,
|
||||||
|
@ -83,7 +82,7 @@ let verifySignedJWT = async (jwt, key) => {
|
||||||
|
|
||||||
|
|
||||||
Deno.test("plugin loads", async () => {
|
Deno.test("plugin loads", async () => {
|
||||||
const bi = await import('../../plugins/signed-integrity/cli.js')
|
const bi = await import('../cli.js')
|
||||||
assert("name" in bi)
|
assert("name" in bi)
|
||||||
assert(bi.name == "signed-integrity")
|
assert(bi.name == "signed-integrity")
|
||||||
assert("description" in bi)
|
assert("description" in bi)
|
||||||
|
@ -91,7 +90,7 @@ Deno.test("plugin loads", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("gen-integrity action defined", async () => {
|
Deno.test("gen-integrity action defined", async () => {
|
||||||
const bi = await import('../../plugins/signed-integrity/cli.js')
|
const bi = await import('../cli.js')
|
||||||
assert("gen-integrity" in bi.actions)
|
assert("gen-integrity" in bi.actions)
|
||||||
|
|
||||||
const gi = bi.actions["gen-integrity"]
|
const gi = bi.actions["gen-integrity"]
|
||||||
|
@ -133,7 +132,7 @@ Deno.test("gen-integrity action defined", async () => {
|
||||||
|
|
||||||
// this is a separate test in order to catch any changing defaults
|
// this is a separate test in order to catch any changing defaults
|
||||||
Deno.test("gen-integrity action defaults", async () => {
|
Deno.test("gen-integrity action defaults", async () => {
|
||||||
const bi = await import('../../plugins/signed-integrity/cli.js')
|
const bi = await import('../cli.js')
|
||||||
const gia = bi.actions["gen-integrity"].arguments
|
const gia = bi.actions["gen-integrity"].arguments
|
||||||
assert("default" in gia.algorithm)
|
assert("default" in gia.algorithm)
|
||||||
assert(gia.algorithm.default == "SHA-256")
|
assert(gia.algorithm.default == "SHA-256")
|
||||||
|
@ -144,7 +143,7 @@ Deno.test("gen-integrity action defaults", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("gen-integrity verifies arguments are sane", async () => {
|
Deno.test("gen-integrity verifies arguments are sane", async () => {
|
||||||
const bi = await import('../../plugins/signed-integrity/cli.js')
|
const bi = await import('../cli.js')
|
||||||
const gi = bi.actions["gen-integrity"]
|
const gi = bi.actions["gen-integrity"]
|
||||||
assertRejects(gi.run, Error, "Expected non-empty list of paths to process.")
|
assertRejects(gi.run, Error, "Expected non-empty list of paths to process.")
|
||||||
assertRejects(async ()=>{
|
assertRejects(async ()=>{
|
||||||
|
@ -172,39 +171,43 @@ Deno.test("gen-integrity verifies arguments are sane", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("gen-integrity handles paths correctly", async () => {
|
Deno.test("gen-integrity handles paths correctly", async () => {
|
||||||
const bi = await import('../../plugins/signed-integrity/cli.js')
|
const bi = await import('../cli.js')
|
||||||
const gi = bi.actions["gen-integrity"]
|
const gi = bi.actions["gen-integrity"]
|
||||||
|
const mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
|
||||||
|
const mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
|
||||||
assertRejects(async ()=>{
|
assertRejects(async ()=>{
|
||||||
await gi.run(['./'], 'non-existent')
|
await gi.run(['./'], 'non-existent')
|
||||||
}, Error, "Failed to load private key from 'non-existent'")
|
}, Error, "Failed to load private key from 'non-existent'")
|
||||||
assertRejects(async ()=>{
|
assertRejects(async ()=>{
|
||||||
await gi.run(['./'], './')
|
await gi.run(['./'], './')
|
||||||
}, Error, "ailed to load private key from './': Is a directory")
|
}, Error, "Failed to load private key from './': Is a directory")
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await gi.run(['./'], './__denotests__/mocks/keyfile.json'),
|
await gi.run(['./'], mk),
|
||||||
'{}'
|
'{}'
|
||||||
)
|
)
|
||||||
assertStringIncludes(
|
assertStringIncludes(
|
||||||
await gi.run(['./__denotests__/mocks/hello.txt'], './__denotests__/mocks/keyfile.json'),
|
await gi.run([mh], mk),
|
||||||
'"./__denotests__/mocks/hello.txt":"eyJhbGciOiAiRVMzODQifQ.eyJpbnRlZ3JpdHkiOiAic2hhMjU2LXVVMG51Wk5OUGdpbExsTFgybjJyK3NTRTcrTjZVNER1a0lqM3JPTHZ6ZWs9In0.'
|
'"' + mh + '":"eyJhbGciOiAiRVMzODQifQ.eyJpbnRlZ3JpdHkiOiAic2hhMjU2LXVVMG51Wk5OUGdpbExsTFgybjJyK3NTRTcrTjZVNER1a0lqM3JPTHZ6ZWs9In0.'
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("gen-integrity handles algos argument correctly", async () => {
|
Deno.test("gen-integrity handles algos argument correctly", async () => {
|
||||||
const bi = await import('../../plugins/signed-integrity/cli.js')
|
const bi = await import('../cli.js')
|
||||||
const gi = bi.actions["gen-integrity"]
|
const gi = bi.actions["gen-integrity"]
|
||||||
|
const mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
|
||||||
|
const mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
|
||||||
assertRejects(async ()=>{
|
assertRejects(async ()=>{
|
||||||
await gi.run(['./__denotests__/mocks/hello.txt'], './__denotests__/mocks/keyfile.json', ['BAD-ALG'])
|
await gi.run([mh], mk, ['BAD-ALG'])
|
||||||
}, Error, 'Unrecognized algorithm name')
|
}, Error, 'Unrecognized algorithm name')
|
||||||
|
|
||||||
// helper function
|
// helper function
|
||||||
let getGeneratedTestIntegrity = async (algos) => {
|
let getGeneratedTestIntegrity = async (algos) => {
|
||||||
let integrity = JSON.parse(await gi.run(
|
let integrity = JSON.parse(await gi.run(
|
||||||
['./__denotests__/mocks/hello.txt'],
|
[mh],
|
||||||
'./__denotests__/mocks/keyfile.json',
|
mk,
|
||||||
algos)
|
algos)
|
||||||
)
|
)
|
||||||
integrity = b64urlDecode(integrity["./__denotests__/mocks/hello.txt"].split('.')[1])
|
integrity = b64urlDecode(integrity[mh].split('.')[1])
|
||||||
return integrity
|
return integrity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,13 +218,15 @@ Deno.test("gen-integrity handles algos argument correctly", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("gen-integrity text output is correct", async () => {
|
Deno.test("gen-integrity text output is correct", async () => {
|
||||||
const bi = await import('../../plugins/signed-integrity/cli.js')
|
const bi = await import('../cli.js')
|
||||||
const gi = bi.actions["gen-integrity"]
|
const gi = bi.actions["gen-integrity"]
|
||||||
|
const mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
|
||||||
|
const mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
|
||||||
|
|
||||||
let getGeneratedTestIntegrity = async (algos) => {
|
let getGeneratedTestIntegrity = async (algos) => {
|
||||||
let result = await gi.run(
|
let result = await gi.run(
|
||||||
['./__denotests__/mocks/hello.txt'],
|
[mh],
|
||||||
'./__denotests__/mocks/keyfile.json',
|
mk,
|
||||||
algos,
|
algos,
|
||||||
'text'
|
'text'
|
||||||
)
|
)
|
||||||
|
@ -232,21 +237,21 @@ Deno.test("gen-integrity text output is correct", async () => {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await getGeneratedTestIntegrity(['SHA-256']),
|
await getGeneratedTestIntegrity(['SHA-256']),
|
||||||
[
|
[
|
||||||
"./__denotests__/mocks/hello.txt:",
|
mh + ":",
|
||||||
'{"integrity": "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="}'
|
'{"integrity": "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="}'
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await getGeneratedTestIntegrity(['SHA-384']),
|
await getGeneratedTestIntegrity(['SHA-384']),
|
||||||
[
|
[
|
||||||
"./__denotests__/mocks/hello.txt:",
|
mh + ":",
|
||||||
'{"integrity": "sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9"}'
|
'{"integrity": "sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9"}'
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await getGeneratedTestIntegrity(['SHA-512']),
|
await getGeneratedTestIntegrity(['SHA-512']),
|
||||||
[
|
[
|
||||||
"./__denotests__/mocks/hello.txt:",
|
mh + ":",
|
||||||
'{"integrity": "sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="}'
|
'{"integrity": "sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="}'
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -255,7 +260,7 @@ Deno.test("gen-integrity text output is correct", async () => {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await getGeneratedTestIntegrity(['SHA-256', 'SHA-384', 'SHA-512']),
|
await getGeneratedTestIntegrity(['SHA-256', 'SHA-384', 'SHA-512']),
|
||||||
[
|
[
|
||||||
"./__denotests__/mocks/hello.txt:",
|
mh + ":",
|
||||||
'{"integrity": "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek= sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9 sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="}'
|
'{"integrity": "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek= sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9 sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="}'
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -264,18 +269,21 @@ Deno.test("gen-integrity text output is correct", async () => {
|
||||||
// TODO: "files" output mode, which will require mocking file writing routines
|
// TODO: "files" output mode, which will require mocking file writing routines
|
||||||
|
|
||||||
Deno.test("gen-integrity signs the data correctly", async () => {
|
Deno.test("gen-integrity signs the data correctly", async () => {
|
||||||
const bi = await import('../../plugins/signed-integrity/cli.js')
|
const bi = await import('../cli.js')
|
||||||
const gi = bi.actions["gen-integrity"]
|
const gi = bi.actions["gen-integrity"]
|
||||||
let jwt = JSON.parse(await gi.run(['./__denotests__/mocks/hello.txt'], './__denotests__/mocks/keyfile.json'))
|
const mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
|
||||||
|
const mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
|
||||||
|
let jwt = JSON.parse(await gi.run([mh], mk))
|
||||||
assert(
|
assert(
|
||||||
await verifySignedJWT(
|
await verifySignedJWT(
|
||||||
jwt['./__denotests__/mocks/hello.txt'],
|
jwt[mh],
|
||||||
pubkey))
|
pubkey))
|
||||||
})
|
})
|
||||||
|
|
||||||
Deno.test("get-pubkey works correctly", async () => {
|
Deno.test("get-pubkey works correctly", async () => {
|
||||||
const bi = await import('../../plugins/signed-integrity/cli.js')
|
const bi = await import('../cli.js')
|
||||||
const gp = bi.actions["get-pubkey"]
|
const gp = bi.actions["get-pubkey"]
|
||||||
|
const mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
|
||||||
assertRejects(gp.run, Error, "No keyfile provided.")
|
assertRejects(gp.run, Error, "No keyfile provided.")
|
||||||
assertRejects(async ()=>{
|
assertRejects(async ()=>{
|
||||||
await gp.run('no-such-file')
|
await gp.run('no-such-file')
|
||||||
|
@ -284,17 +292,17 @@ Deno.test("get-pubkey works correctly", async () => {
|
||||||
await gp.run(['no-such-file'])
|
await gp.run(['no-such-file'])
|
||||||
}, Error, "No such file or directory")
|
}, Error, "No such file or directory")
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await gp.run('./__denotests__/mocks/keyfile.json'),
|
await gp.run(mk),
|
||||||
'{"kty":"EC","crv":"P-384","alg":"ES384","x":"rrFawYTuFo8ZjoDxaztUU-c_RAwjw1Y9Tp3j4nH4WsY2Zlizf40Mvz_0BUkVVZCw","y":"HaFct6PVK2CQ7ZT2SHClnN-knmGfjY_DFwc6qrAu1s0DFZ8fEUuNdmkTlj9T4NQw","key_ops":["verify"],"ext":true}'
|
'{"kty":"EC","crv":"P-384","alg":"ES384","x":"rrFawYTuFo8ZjoDxaztUU-c_RAwjw1Y9Tp3j4nH4WsY2Zlizf40Mvz_0BUkVVZCw","y":"HaFct6PVK2CQ7ZT2SHClnN-knmGfjY_DFwc6qrAu1s0DFZ8fEUuNdmkTlj9T4NQw","key_ops":["verify"],"ext":true}'
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await gp.run(['./__denotests__/mocks/keyfile.json', 'irrelevant']),
|
await gp.run([mk, 'irrelevant']),
|
||||||
'{"kty":"EC","crv":"P-384","alg":"ES384","x":"rrFawYTuFo8ZjoDxaztUU-c_RAwjw1Y9Tp3j4nH4WsY2Zlizf40Mvz_0BUkVVZCw","y":"HaFct6PVK2CQ7ZT2SHClnN-knmGfjY_DFwc6qrAu1s0DFZ8fEUuNdmkTlj9T4NQw","key_ops":["verify"],"ext":true}'
|
'{"kty":"EC","crv":"P-384","alg":"ES384","x":"rrFawYTuFo8ZjoDxaztUU-c_RAwjw1Y9Tp3j4nH4WsY2Zlizf40Mvz_0BUkVVZCw","y":"HaFct6PVK2CQ7ZT2SHClnN-knmGfjY_DFwc6qrAu1s0DFZ8fEUuNdmkTlj9T4NQw","key_ops":["verify"],"ext":true}'
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("gen-keypair works correctly", async () => {
|
Deno.test("gen-keypair works correctly", async () => {
|
||||||
const bi = await import('../../plugins/signed-integrity/cli.js')
|
const bi = await import('../cli.js')
|
||||||
const gk = bi.actions["gen-keypair"]
|
const gk = bi.actions["gen-keypair"]
|
||||||
const keypair = JSON.parse(await gk.run())
|
const keypair = JSON.parse(await gk.run())
|
||||||
assert('privateKey' in keypair)
|
assert('privateKey' in keypair)
|
|
@ -0,0 +1 @@
|
||||||
|
hello world
|
|
@ -92,6 +92,8 @@
|
||||||
+ '='.repeat(pad)
|
+ '='.repeat(pad)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// needed for making ArrayBuffers out of strings
|
||||||
|
let tenc = new TextEncoder()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getting content using the configured plugin,
|
* getting content using the configured plugin,
|
||||||
|
@ -123,7 +125,10 @@
|
||||||
let k = await getJWTPublicKey()
|
let k = await getJWTPublicKey()
|
||||||
|
|
||||||
// reality check: all parts of the JWT should be non-empty
|
// reality check: all parts of the JWT should be non-empty
|
||||||
if ( (jwt[0].length == 0) || (jwt[1].length == 0) || (jwt[2].length == 0) ) {
|
if ( (jwt.length < 3)
|
||||||
|
|| (jwt[0].length == 0)
|
||||||
|
|| (jwt[1].length == 0)
|
||||||
|
|| (jwt[2].length == 0) ) {
|
||||||
throw new Error('JWT seems invalid (one or more sections are empty).')
|
throw new Error('JWT seems invalid (one or more sections are empty).')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,8 +139,9 @@
|
||||||
Array
|
Array
|
||||||
.from(signature)
|
.from(signature)
|
||||||
.map(e=>e.charCodeAt(0))
|
.map(e=>e.charCodeAt(0))
|
||||||
).buffer
|
)
|
||||||
|
|
||||||
|
signature = signature.buffer
|
||||||
// verify the JWT
|
// verify the JWT
|
||||||
if (await subtle
|
if (await subtle
|
||||||
.verify(
|
.verify(
|
||||||
|
@ -145,7 +151,7 @@
|
||||||
},
|
},
|
||||||
k,
|
k,
|
||||||
signature,
|
signature,
|
||||||
(jwt[0] + '.' + jwt[1])
|
tenc.encode(jwt[0] + '.' + jwt[1])
|
||||||
)) {
|
)) {
|
||||||
// unpack it
|
// unpack it
|
||||||
var header = atob(b64urlDecode(jwt[0]))
|
var header = atob(b64urlDecode(jwt[0]))
|
||||||
|
|
|
@ -2,35 +2,6 @@
|
||||||
* LibResilient Service Worker.
|
* LibResilient Service Worker.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize the LibResilientPlugins array
|
// initialize the LibResilientPlugins array
|
||||||
if (!Array.isArray(self.LibResilientPlugins)) {
|
if (!Array.isArray(self.LibResilientPlugins)) {
|
||||||
self.LibResilientPlugins = new Array()
|
self.LibResilientPlugins = new Array()
|
||||||
|
@ -214,7 +185,7 @@ let initServiceWorker = async () => {
|
||||||
cacheConfigJSON(configURL, cresponse)
|
cacheConfigJSON(configURL, cresponse)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// we ain't got nothing useful -- justs set cdata to an empty object
|
// we ain't got nothing useful -- just set cdata to an empty object
|
||||||
cdata = {}
|
cdata = {}
|
||||||
self.log('service-worker', 'ignoring invalid config, using defaults.')
|
self.log('service-worker', 'ignoring invalid config, using defaults.')
|
||||||
}
|
}
|
||||||
|
@ -361,7 +332,7 @@ let initServiceWorker = async () => {
|
||||||
// i.e. if the plugin scripts have already been loaded
|
// i.e. if the plugin scripts have already been loaded
|
||||||
// FIXME: this does not currently dive into dependencies!
|
// FIXME: this does not currently dive into dependencies!
|
||||||
// FIXME: https://gitlab.com/rysiekpl/libresilient/-/issues/48
|
// FIXME: https://gitlab.com/rysiekpl/libresilient/-/issues/48
|
||||||
for (p in cdata.plugins) {
|
for (let p in cdata.plugins) {
|
||||||
var pname = cdata.plugins[p].name
|
var pname = cdata.plugins[p].name
|
||||||
|
|
||||||
// plugin constructor not available, meaning: we'd have to importScripts() it
|
// plugin constructor not available, meaning: we'd have to importScripts() it
|
||||||
|
@ -447,21 +418,29 @@ let decrementActiveFetches = (clientId) => {
|
||||||
* time - the timeout (in ms)
|
* time - the timeout (in ms)
|
||||||
* timeout_resolves - whether the Promise should resolve() or reject() when hitting the timeout (default: false (reject))
|
* timeout_resolves - whether the Promise should resolve() or reject() when hitting the timeout (default: false (reject))
|
||||||
* error_message - optional error message to use when rejecting (default: false (no error message))
|
* error_message - optional error message to use when rejecting (default: false (no error message))
|
||||||
|
*
|
||||||
|
* returns an array containing:
|
||||||
|
* - timeout-related Promise as element 0
|
||||||
|
* - timeoutID as element 1
|
||||||
*/
|
*/
|
||||||
let promiseTimeout = (time, timeout_resolves=false, error_message=false) => {
|
let promiseTimeout = (time, timeout_resolves=false, error_message=false) => {
|
||||||
return new Promise((resolve, reject)=>{
|
let timeout_id = null
|
||||||
setTimeout(()=>{
|
let timeout_promise = new Promise((resolve, reject)=>{
|
||||||
if (timeout_resolves) {
|
timeout_id = setTimeout(()=>{
|
||||||
resolve(time);
|
if (timeout_resolves) {
|
||||||
} else {
|
resolve(time);
|
||||||
if (error_message) {
|
} else {
|
||||||
reject(new Error(error_message))
|
if (error_message) {
|
||||||
} else {
|
reject(new Error(error_message))
|
||||||
reject(time)
|
} else {
|
||||||
}
|
reject(time)
|
||||||
}
|
}
|
||||||
},time);
|
}
|
||||||
|
}, time);
|
||||||
});
|
});
|
||||||
|
// we need both the promise and the timeout ID
|
||||||
|
// so that we can clearTimeout() if/when needed
|
||||||
|
return [timeout_promise, timeout_id]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -503,6 +482,7 @@ let LibResilientResourceInfo = class {
|
||||||
this.values.url = url
|
this.values.url = url
|
||||||
this.values.clientId = clientId
|
this.values.clientId = clientId
|
||||||
// we might not have a non-empty clientId if it's a cross-origin fetch
|
// we might not have a non-empty clientId if it's a cross-origin fetch
|
||||||
|
|
||||||
if (clientId) {
|
if (clientId) {
|
||||||
// get the client from Client API based on clientId
|
// get the client from Client API based on clientId
|
||||||
self.clients.get(clientId).then((client)=>{
|
self.clients.get(clientId).then((client)=>{
|
||||||
|
@ -644,15 +624,34 @@ let libresilientFetch = (plugin, url, init, reqInfo) => {
|
||||||
'\n+-- using method(s):', plugin.name
|
'\n+-- using method(s):', plugin.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let timeout_promise, timeout_id
|
||||||
|
[timeout_promise, timeout_id] = promiseTimeout(
|
||||||
|
self.LibResilientConfig.defaultPluginTimeout,
|
||||||
|
false,
|
||||||
|
`LibResilient request using ${plugin.name} timed out after ${self.LibResilientConfig.defaultPluginTimeout}ms.`
|
||||||
|
)
|
||||||
|
|
||||||
// race the plugin(s) vs. a timeout
|
// race the plugin(s) vs. a timeout
|
||||||
return Promise.race([
|
let race_promise = Promise
|
||||||
plugin.fetch(url, init),
|
.race([
|
||||||
promiseTimeout(
|
plugin.fetch(url, init),
|
||||||
self.LibResilientConfig.defaultPluginTimeout,
|
timeout_promise
|
||||||
false,
|
])
|
||||||
`LibResilient request using ${plugin.name} timed out after ${self.LibResilientConfig.defaultPluginTimeout}ms.`
|
|
||||||
)
|
// making sure there are no dangling promises etc
|
||||||
])
|
//
|
||||||
|
// this should happen asynchronously
|
||||||
|
race_promise
|
||||||
|
// make sure the timeout is cancelled as soon as the promise resolves
|
||||||
|
// we do not want any dangling promises/timeouts after all!
|
||||||
|
.then(()=>{
|
||||||
|
clearTimeout(timeout_id)
|
||||||
|
})
|
||||||
|
// no-op to make sure we don't end up with dangling rejected premises
|
||||||
|
.catch((e)=>{})
|
||||||
|
|
||||||
|
// return the racing promise
|
||||||
|
return race_promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -664,7 +663,7 @@ let libresilientFetch = (plugin, url, init, reqInfo) => {
|
||||||
*/
|
*/
|
||||||
let callOnLibResilientPlugin = (call, args) => {
|
let callOnLibResilientPlugin = (call, args) => {
|
||||||
// find the first plugin implementing the method
|
// find the first plugin implementing the method
|
||||||
for (i=0; i<self.LibResilientPlugins.length; i++) {
|
for (let i=0; i<self.LibResilientPlugins.length; i++) {
|
||||||
if (typeof self.LibResilientPlugins[i][call] === 'function') {
|
if (typeof self.LibResilientPlugins[i][call] === 'function') {
|
||||||
self.log('service-worker', 'Calling plugin ' + self.LibResilientPlugins[i].name + '.' + call + '()')
|
self.log('service-worker', 'Calling plugin ' + self.LibResilientPlugins[i].name + '.' + call + '()')
|
||||||
// call it
|
// call it
|
||||||
|
@ -744,7 +743,7 @@ let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doSt
|
||||||
reqInfo.update({state:"success"})
|
reqInfo.update({state:"success"})
|
||||||
|
|
||||||
// get the plugin that was used to fetch content
|
// get the plugin that was used to fetch content
|
||||||
plugin = self.LibResilientPlugins.find(p=>p.name===reqInfo.method)
|
let plugin = self.LibResilientPlugins.find(p=>p.name===reqInfo.method)
|
||||||
|
|
||||||
// if it's a stashing plugin...
|
// if it's a stashing plugin...
|
||||||
if (typeof plugin.stash === 'function') {
|
if (typeof plugin.stash === 'function') {
|
||||||
|
@ -801,7 +800,7 @@ let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doSt
|
||||||
// do we want to stash?
|
// do we want to stash?
|
||||||
if (doStash) {
|
if (doStash) {
|
||||||
// find the first stashing plugin
|
// find the first stashing plugin
|
||||||
for (i=0; i<self.LibResilientPlugins.length; i++) {
|
for (let i=0; i<self.LibResilientPlugins.length; i++) {
|
||||||
if (typeof self.LibResilientPlugins[i].stash === 'function') {
|
if (typeof self.LibResilientPlugins[i].stash === 'function') {
|
||||||
|
|
||||||
// ok, now we're in business
|
// ok, now we're in business
|
||||||
|
@ -858,7 +857,9 @@ let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doSt
|
||||||
|* === Setting up the event handlers === *|
|
|* === Setting up the event handlers === *|
|
||||||
\* ========================================================================= */
|
\* ========================================================================= */
|
||||||
self.addEventListener('install', async (event) => {
|
self.addEventListener('install', async (event) => {
|
||||||
event.waitUntil(initServiceWorker())
|
await event.waitUntil(
|
||||||
|
initServiceWorker()
|
||||||
|
)
|
||||||
// "COMMIT_UNKNOWN" will be replaced with commit ID
|
// "COMMIT_UNKNOWN" will be replaced with commit ID
|
||||||
self.log('service-worker', "0. Installed LibResilient Service Worker (commit: COMMIT_UNKNOWN).");
|
self.log('service-worker', "0. Installed LibResilient Service Worker (commit: COMMIT_UNKNOWN).");
|
||||||
});
|
});
|
||||||
|
@ -924,12 +925,13 @@ self.addEventListener('fetch', async event => {
|
||||||
|
|
||||||
// External requests go through a regular fetch()
|
// External requests go through a regular fetch()
|
||||||
if (!event.request.url.startsWith(self.location.origin)) {
|
if (!event.request.url.startsWith(self.location.origin)) {
|
||||||
self.log('service-worker', 'External request; current origin: ' + self.location.origin)
|
self.log('service-worker', 'External request, using standard fetch(); current origin: ' + self.location.origin)
|
||||||
return fetch(event.request);
|
return fetch(event.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-GET requests go through a regular fetch()
|
// Non-GET requests go through a regular fetch()
|
||||||
if (event.request.method !== 'GET') {
|
if (event.request.method !== 'GET') {
|
||||||
|
self.log('service-worker', 'Non-GET request, using standard fetch()')
|
||||||
return fetch(event.request);
|
return fetch(event.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue