Rewriting tests into Deno

merge-requests/23/head
Michał "rysiek" Woźniak 2023-09-26 20:32:54 +00:00
rodzic edd7a0c2ec
commit 34b87e5ad2
43 zmienionych plików z 5192 dodań i 4559 usunięć

2
.gitignore vendored
Wyświetl plik

@ -1,4 +1,2 @@
node_modules/
radata/
coverage/
junit.xml

Wyświetl plik

@ -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:
image: node:17.0
image: denoland/deno:debian-1.37.0
stage: test
script:
- npm ci --include=dev
- npx jest --collectCoverage --coverageDirectory="./coverage" --coverage --ci --reporters=default --reporters=jest-junit
coverage: "/All files[^|]*\\|[^|]*\\s+([\\d\\.]+)/"
# preparations
- apt update && apt-get install -y bsdextrautils
- 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:
when: always
reports:
junit:
- junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
path: coverage/coverage.xml
tags:
- docker
- 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:
- test

Wyświetl plik

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

Wyświetl plik

@ -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)
});
});

Wyświetl plik

@ -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();
});
});

Wyświetl plik

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

Wyświetl plik

@ -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)
})
});
});

Wyświetl plik

@ -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)
});
});

Wyświetl plik

@ -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")
})
});

Wyświetl plik

@ -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)
});
});

Wyświetl plik

@ -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.')
})
});

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,3 +0,0 @@
{
"test": true
}

Wyświetl plik

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

Wyświetl plik

@ -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);
});
})

Wyświetl plik

@ -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"})
});
})

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -54,6 +54,36 @@
let cacheContent = (resource, key) => {
return caches.open('v1')
.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') {
// assume URL
LR.log(pluginName, "caching an URL: " + resource)

Wyświetl plik

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

Wyświetl plik

@ -140,7 +140,7 @@
// 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: 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 path = urlData.join('/')
LR.log(pluginName, '+-- fetching:\n',

Wyświetl plik

@ -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')
})
})

Wyświetl plik

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

Wyświetl plik

@ -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.')
})
})

Wyświetl plik

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

Wyświetl plik

@ -114,7 +114,7 @@
}
// 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
// apart from returning the response
@ -144,7 +144,11 @@
// it's a string, we need an array
nextIntegrityHash = nextIntegrityHash.split('-')
// 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]}`)
return crypto
.subtle
@ -157,7 +161,7 @@
LR.log(pluginName, `successfully verified integrity for: ${url}\n- algo: ${nextIntegrityHash[0]}\n- hash: ${nextIntegrityHash[1]}`)
return responsePromise
} else {
return Promise.reject('Digest does not match.')
return Promise.reject(Error('Digest does not match.'))
}
})
},
@ -165,7 +169,7 @@
Promise.reject())
})
.catch((err)=>{
return Promise.reject(`No digest matched for: ${url}`)
return Promise.reject(Error(`No digest matched for: ${url}`))
})
}

Wyświetl plik

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

Wyświetl plik

@ -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')
})
})

Wyświetl plik

@ -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=",
}
]
})
});
})

Wyświetl plik

@ -1,6 +1,5 @@
import {
assert,
assertThrows,
assertRejects,
assertEquals,
assertStringIncludes,
@ -83,7 +82,7 @@ let verifySignedJWT = async (jwt, key) => {
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(bi.name == "signed-integrity")
assert("description" in bi)
@ -91,7 +90,7 @@ Deno.test("plugin loads", 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)
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
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
assert("default" in gia.algorithm)
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 () => {
const bi = await import('../../plugins/signed-integrity/cli.js')
const bi = await import('../cli.js')
const gi = bi.actions["gen-integrity"]
assertRejects(gi.run, Error, "Expected non-empty list of paths to process.")
assertRejects(async ()=>{
@ -172,39 +171,43 @@ Deno.test("gen-integrity verifies arguments are sane", 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 mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
const mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
assertRejects(async ()=>{
await gi.run(['./'], 'non-existent')
}, Error, "Failed to load private key from 'non-existent'")
assertRejects(async ()=>{
await gi.run(['./'], './')
}, Error, "ailed to load private key from './': Is a directory")
}, Error, "Failed to load private key from './': Is a directory")
assertEquals(
await gi.run(['./'], './__denotests__/mocks/keyfile.json'),
await gi.run(['./'], mk),
'{}'
)
assertStringIncludes(
await gi.run(['./__denotests__/mocks/hello.txt'], './__denotests__/mocks/keyfile.json'),
'"./__denotests__/mocks/hello.txt":"eyJhbGciOiAiRVMzODQifQ.eyJpbnRlZ3JpdHkiOiAic2hhMjU2LXVVMG51Wk5OUGdpbExsTFgybjJyK3NTRTcrTjZVNER1a0lqM3JPTHZ6ZWs9In0.'
await gi.run([mh], mk),
'"' + mh + '":"eyJhbGciOiAiRVMzODQifQ.eyJpbnRlZ3JpdHkiOiAic2hhMjU2LXVVMG51Wk5OUGdpbExsTFgybjJyK3NTRTcrTjZVNER1a0lqM3JPTHZ6ZWs9In0.'
)
});
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 mh = import.meta.resolve('./mocks/hello.txt').replace(/^file:\/\//gi, "")
const mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
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')
// helper function
let getGeneratedTestIntegrity = async (algos) => {
let integrity = JSON.parse(await gi.run(
['./__denotests__/mocks/hello.txt'],
'./__denotests__/mocks/keyfile.json',
[mh],
mk,
algos)
)
integrity = b64urlDecode(integrity["./__denotests__/mocks/hello.txt"].split('.')[1])
integrity = b64urlDecode(integrity[mh].split('.')[1])
return integrity
}
@ -215,13 +218,15 @@ Deno.test("gen-integrity handles algos argument correctly", 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 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 result = await gi.run(
['./__denotests__/mocks/hello.txt'],
'./__denotests__/mocks/keyfile.json',
[mh],
mk,
algos,
'text'
)
@ -232,21 +237,21 @@ Deno.test("gen-integrity text output is correct", async () => {
assertEquals(
await getGeneratedTestIntegrity(['SHA-256']),
[
"./__denotests__/mocks/hello.txt:",
mh + ":",
'{"integrity": "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="}'
]
)
assertEquals(
await getGeneratedTestIntegrity(['SHA-384']),
[
"./__denotests__/mocks/hello.txt:",
mh + ":",
'{"integrity": "sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9"}'
]
)
assertEquals(
await getGeneratedTestIntegrity(['SHA-512']),
[
"./__denotests__/mocks/hello.txt:",
mh + ":",
'{"integrity": "sha512-MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw=="}'
]
)
@ -255,7 +260,7 @@ Deno.test("gen-integrity text output is correct", async () => {
assertEquals(
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=="}'
]
)
@ -264,18 +269,21 @@ Deno.test("gen-integrity text output is correct", async () => {
// TODO: "files" output mode, which will require mocking file writing routines
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"]
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(
await verifySignedJWT(
jwt['./__denotests__/mocks/hello.txt'],
jwt[mh],
pubkey))
})
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 mk = import.meta.resolve('./mocks/keyfile.json').replace(/^file:\/\//gi, "")
assertRejects(gp.run, Error, "No keyfile provided.")
assertRejects(async ()=>{
await gp.run('no-such-file')
@ -284,17 +292,17 @@ Deno.test("get-pubkey works correctly", async () => {
await gp.run(['no-such-file'])
}, Error, "No such file or directory")
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}'
)
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}'
)
});
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 keypair = JSON.parse(await gk.run())
assert('privateKey' in keypair)

Wyświetl plik

@ -0,0 +1 @@
hello world

Wyświetl plik

@ -92,6 +92,8 @@
+ '='.repeat(pad)
}
// needed for making ArrayBuffers out of strings
let tenc = new TextEncoder()
/**
* getting content using the configured plugin,
@ -123,7 +125,10 @@
let k = await getJWTPublicKey()
// 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).')
}
@ -134,8 +139,9 @@
Array
.from(signature)
.map(e=>e.charCodeAt(0))
).buffer
)
signature = signature.buffer
// verify the JWT
if (await subtle
.verify(
@ -145,7 +151,7 @@
},
k,
signature,
(jwt[0] + '.' + jwt[1])
tenc.encode(jwt[0] + '.' + jwt[1])
)) {
// unpack it
var header = atob(b64urlDecode(jwt[0]))

Wyświetl plik

@ -2,35 +2,6 @@
* 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
if (!Array.isArray(self.LibResilientPlugins)) {
self.LibResilientPlugins = new Array()
@ -214,7 +185,7 @@ let initServiceWorker = async () => {
cacheConfigJSON(configURL, cresponse)
} 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 = {}
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
// FIXME: this does not currently dive into dependencies!
// 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
// plugin constructor not available, meaning: we'd have to importScripts() it
@ -447,21 +418,29 @@ let decrementActiveFetches = (clientId) => {
* time - the timeout (in ms)
* 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))
*
* returns an array containing:
* - timeout-related Promise as element 0
* - timeoutID as element 1
*/
let promiseTimeout = (time, timeout_resolves=false, error_message=false) => {
return new Promise((resolve, reject)=>{
setTimeout(()=>{
if (timeout_resolves) {
resolve(time);
} else {
if (error_message) {
reject(new Error(error_message))
} else {
reject(time)
}
}
},time);
let timeout_id = null
let timeout_promise = new Promise((resolve, reject)=>{
timeout_id = setTimeout(()=>{
if (timeout_resolves) {
resolve(time);
} else {
if (error_message) {
reject(new Error(error_message))
} else {
reject(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.clientId = clientId
// we might not have a non-empty clientId if it's a cross-origin fetch
if (clientId) {
// get the client from Client API based on clientId
self.clients.get(clientId).then((client)=>{
@ -644,15 +624,34 @@ let libresilientFetch = (plugin, url, init, reqInfo) => {
'\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
return Promise.race([
plugin.fetch(url, init),
promiseTimeout(
self.LibResilientConfig.defaultPluginTimeout,
false,
`LibResilient request using ${plugin.name} timed out after ${self.LibResilientConfig.defaultPluginTimeout}ms.`
)
])
let race_promise = Promise
.race([
plugin.fetch(url, init),
timeout_promise
])
// 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) => {
// 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') {
self.log('service-worker', 'Calling plugin ' + self.LibResilientPlugins[i].name + '.' + call + '()')
// call it
@ -744,7 +743,7 @@ let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doSt
reqInfo.update({state:"success"})
// 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 (typeof plugin.stash === 'function') {
@ -801,7 +800,7 @@ let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doSt
// do we want to stash?
if (doStash) {
// 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') {
// ok, now we're in business
@ -858,7 +857,9 @@ let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doSt
|* === Setting up the event handlers === *|
\* ========================================================================= */
self.addEventListener('install', async (event) => {
event.waitUntil(initServiceWorker())
await event.waitUntil(
initServiceWorker()
)
// "COMMIT_UNKNOWN" will be replaced with commit ID
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()
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);
}
// Non-GET requests go through a regular fetch()
if (event.request.method !== 'GET') {
self.log('service-worker', 'Non-GET request, using standard fetch()')
return fetch(event.request);
}