commit
4122f4dc3e
@ -1,2 +1,4 @@
|
|||||||
public/bundle.js
|
public/bundle.js
|
||||||
public/webcrypto-shim.js
|
public/webcrypto-shim.js
|
||||||
|
test/frontend/bundle.js
|
||||||
|
firefox
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ public/bundle.js
|
|||||||
public/version.json
|
public/version.json
|
||||||
static/*
|
static/*
|
||||||
!static/info.txt
|
!static/info.txt
|
||||||
|
test/frontend/bundle.js
|
||||||
|
@ -4,6 +4,13 @@ machine:
|
|||||||
services:
|
services:
|
||||||
- docker
|
- docker
|
||||||
- redis
|
- redis
|
||||||
|
environment:
|
||||||
|
PATH: "/home/ubuntu/send/firefox:$PATH"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
pre:
|
||||||
|
- npm i -g get-firefox geckodriver
|
||||||
|
- get-firefox --platform linux --extract --target /home/ubuntu/send
|
||||||
|
|
||||||
deployment:
|
deployment:
|
||||||
latest:
|
latest:
|
||||||
|
@ -58,61 +58,41 @@ class FileReceiver extends EventEmitter {
|
|||||||
true,
|
true,
|
||||||
['encrypt', 'decrypt']
|
['encrypt', 'decrypt']
|
||||||
)
|
)
|
||||||
])
|
]).then(([fdata, key]) => {
|
||||||
.then(([fdata, key]) => {
|
|
||||||
this.emit('decrypting', true);
|
this.emit('decrypting', true);
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
window.crypto.subtle
|
window.crypto.subtle.decrypt(
|
||||||
.decrypt(
|
|
||||||
{
|
{
|
||||||
name: 'AES-GCM',
|
name: 'AES-GCM',
|
||||||
iv: hexToArray(fdata.iv),
|
iv: hexToArray(fdata.iv),
|
||||||
additionalData: hexToArray(fdata.aad),
|
additionalData: hexToArray(fdata.aad)
|
||||||
tagLength: 128
|
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
fdata.data
|
fdata.data
|
||||||
)
|
).then(decrypted => {
|
||||||
.then(decrypted => {
|
|
||||||
this.emit('decrypting', false);
|
this.emit('decrypting', false);
|
||||||
return new Promise((resolve, reject) => {
|
return Promise.resolve(decrypted)
|
||||||
resolve(decrypted);
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
new Promise((resolve, reject) => {
|
fdata.filename,
|
||||||
resolve(fdata.filename);
|
hexToArray(fdata.aad)
|
||||||
}),
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
resolve(hexToArray(fdata.aad));
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
})
|
}).then(([decrypted, fname, proposedHash]) => {
|
||||||
.then(([decrypted, fname, proposedHash]) => {
|
|
||||||
this.emit('hashing', true);
|
this.emit('hashing', true);
|
||||||
return window.crypto.subtle
|
return window.crypto.subtle.digest('SHA-256', decrypted).then(calculatedHash => {
|
||||||
.digest('SHA-256', decrypted)
|
|
||||||
.then(calculatedHash => {
|
|
||||||
this.emit('hashing', false);
|
this.emit('hashing', false);
|
||||||
const integrity =
|
const integrity = new Uint8Array(calculatedHash).toString() === proposedHash.toString();
|
||||||
new Uint8Array(calculatedHash).toString() ===
|
|
||||||
proposedHash.toString();
|
|
||||||
if (!integrity) {
|
if (!integrity) {
|
||||||
return new Promise((resolve, reject) => {
|
this.emit('unsafe', true)
|
||||||
console.log('This file has been tampered with.');
|
return Promise.reject();
|
||||||
reject();
|
} else {
|
||||||
});
|
this.emit('safe', true);
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
new Promise((resolve, reject) => {
|
decrypted,
|
||||||
resolve(decrypted);
|
fname
|
||||||
}),
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
resolve(fname);
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
});
|
}
|
||||||
});
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ function arrayToHex(iv) {
|
|||||||
hexStr += iv[i].toString(16);
|
hexStr += iv[i].toString(16);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.hexStr = hexStr;
|
|
||||||
return hexStr;
|
return hexStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3037
package-lock.json
generated
3037
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,7 @@
|
|||||||
"raven": "^2.1.0",
|
"raven": "^2.1.0",
|
||||||
"raven-js": "^3.16.0",
|
"raven-js": "^3.16.0",
|
||||||
"redis": "^2.7.1",
|
"redis": "^2.7.1",
|
||||||
|
"selenium-webdriver": "^3.4.0",
|
||||||
"supertest": "^3.0.0",
|
"supertest": "^3.0.0",
|
||||||
"uglify-es": "3.0.19"
|
"uglify-es": "3.0.19"
|
||||||
},
|
},
|
||||||
@ -52,7 +53,8 @@
|
|||||||
"lint:css": "stylelint 'public/*.css'",
|
"lint:css": "stylelint 'public/*.css'",
|
||||||
"lint:js": "eslint .",
|
"lint:js": "eslint .",
|
||||||
"start": "node server/server",
|
"start": "node server/server",
|
||||||
"test": "mocha test/unit && mocha test/server",
|
"test": "mocha test/unit && mocha test/server && npm run test-browser && node test/frontend/driver.js",
|
||||||
|
"test-browser": "browserify test/frontend/frontend.bundle.js -o test/frontend/bundle.js -d",
|
||||||
"version": "node scripts/version"
|
"version": "node scripts/version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
test/frontend/.eslintrc.yml
Normal file
2
test/frontend/.eslintrc.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
env:
|
||||||
|
browser: true
|
22
test/frontend/driver.js
Normal file
22
test/frontend/driver.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const webdriver = require('selenium-webdriver');
|
||||||
|
const path = require('path');
|
||||||
|
const until = webdriver.until;
|
||||||
|
|
||||||
|
const driver = new webdriver.Builder()
|
||||||
|
.forBrowser('firefox')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
driver.get(path.join('file:///', __dirname, '/frontend.test.html'));
|
||||||
|
driver.wait(until.titleIs('Mocha Tests'), 1000);
|
||||||
|
driver.wait(until.titleMatches(/^[0-1]$/), 10000);
|
||||||
|
|
||||||
|
driver.getTitle().then(title => {
|
||||||
|
driver.quit().then(() => {
|
||||||
|
if (title === '0') {
|
||||||
|
console.log('Frontend tests have passed.');
|
||||||
|
} else {
|
||||||
|
throw new Error('Frontend tests are failing. ' +
|
||||||
|
'Please open the frontend.test.html file in a browser.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
22
test/frontend/frontend.bundle.js
Normal file
22
test/frontend/frontend.bundle.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
class FakeFile extends Blob {
|
||||||
|
constructor(name, data, opt) {
|
||||||
|
super(data, opt);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Raven = {
|
||||||
|
captureException: function(err) {
|
||||||
|
console.error(err, err.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.FakeFile = FakeFile;
|
||||||
|
window.FileSender = require('../../frontend/src/fileSender');
|
||||||
|
window.FileReceiver = require('../../frontend/src/fileReceiver');
|
||||||
|
window.sinon = require('sinon');
|
||||||
|
window.server = window.sinon.fakeServer.create();
|
||||||
|
window.assert = require('assert');
|
||||||
|
const utils = require('../../frontend/src/utils');
|
||||||
|
window.hexToArray = utils.hexToArray;
|
||||||
|
window.arrayToHex = utils.arrayToHex;
|
24
test/frontend/frontend.test.html
Normal file
24
test/frontend/frontend.test.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Mocha Tests</title>
|
||||||
|
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css">
|
||||||
|
<script src="bundle.js"></script>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="mocha"></div>
|
||||||
|
<script src="../../node_modules/mocha/mocha.js"></script>
|
||||||
|
<script>mocha.setup('bdd')</script>
|
||||||
|
|
||||||
|
<script src="frontend.test.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
mocha.checkLeaks();
|
||||||
|
mocha.globals(['jQuery']);
|
||||||
|
mocha.run(function(err) {
|
||||||
|
document.title = err;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
360
test/frontend/frontend.test.js
Normal file
360
test/frontend/frontend.test.js
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
const FileSender = window.FileSender;
|
||||||
|
const FileReceiver = window.FileReceiver;
|
||||||
|
const FakeFile = window.FakeFile;
|
||||||
|
const assert = window.assert;
|
||||||
|
const server = window.server;
|
||||||
|
const hexToArray = window.hexToArray;
|
||||||
|
const arrayToHex = window.arrayToHex;
|
||||||
|
const sinon = window.sinon;
|
||||||
|
|
||||||
|
let file;
|
||||||
|
let encryptedIV;
|
||||||
|
let fileHash;
|
||||||
|
let secretKey;
|
||||||
|
let originalBlob;
|
||||||
|
|
||||||
|
describe('File Sender', function() {
|
||||||
|
before(function() {
|
||||||
|
server.respondImmediately = true;
|
||||||
|
server.respondWith(
|
||||||
|
'POST',
|
||||||
|
'/upload',
|
||||||
|
function(request) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsArrayBuffer(request.requestBody.get('data'));
|
||||||
|
|
||||||
|
reader.onload = function(event) {
|
||||||
|
file = this.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseObj = JSON.parse(request.requestHeaders['X-File-Metadata']);
|
||||||
|
request.respond(
|
||||||
|
200,
|
||||||
|
{'Content-Type': 'application/json'},
|
||||||
|
JSON.stringify({url: 'some url',
|
||||||
|
id: responseObj.id,
|
||||||
|
delete: responseObj.delete})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should get a loading event emission', function() {
|
||||||
|
const file = new FakeFile('hello_world.txt', ['This is some data.'])
|
||||||
|
const fs = new FileSender(file);
|
||||||
|
let testLoading = true;
|
||||||
|
|
||||||
|
fs.on('loading', isStillLoading => {
|
||||||
|
assert(!(!testLoading && isStillLoading));
|
||||||
|
testLoading = isStillLoading;
|
||||||
|
})
|
||||||
|
|
||||||
|
return fs.upload()
|
||||||
|
.then(info => {
|
||||||
|
assert(info);
|
||||||
|
assert(!testLoading);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err, err.stack);
|
||||||
|
assert.fail();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should get a hashing event emission', function() {
|
||||||
|
const file = new FakeFile('hello_world.txt', ['This is some data.'])
|
||||||
|
const fs = new FileSender(file);
|
||||||
|
let testHashing = true;
|
||||||
|
|
||||||
|
fs.on('hashing', isStillHashing => {
|
||||||
|
assert(!(!testHashing && isStillHashing));
|
||||||
|
testHashing = isStillHashing;
|
||||||
|
})
|
||||||
|
|
||||||
|
return fs.upload()
|
||||||
|
.then(info => {
|
||||||
|
assert(info);
|
||||||
|
assert(!testHashing);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err, err.stack);
|
||||||
|
assert.fail();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should get a encrypting event emission', function() {
|
||||||
|
const file = new FakeFile('hello_world.txt', ['This is some data.'])
|
||||||
|
const fs = new FileSender(file);
|
||||||
|
let testEncrypting = true;
|
||||||
|
|
||||||
|
fs.on('encrypting', isStillEncrypting => {
|
||||||
|
assert(!(!testEncrypting && isStillEncrypting));
|
||||||
|
testEncrypting = isStillEncrypting;
|
||||||
|
})
|
||||||
|
|
||||||
|
return fs.upload()
|
||||||
|
.then(info => {
|
||||||
|
assert(info);
|
||||||
|
assert(!testEncrypting);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err, err.stack);
|
||||||
|
assert.fail();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should encrypt a file properly', function(done) {
|
||||||
|
const newFile = new FakeFile('hello_world.txt', ['This is some data.'])
|
||||||
|
const fs = new FileSender(newFile);
|
||||||
|
fs.upload().then(info => {
|
||||||
|
const key = info.secretKey;
|
||||||
|
secretKey = info.secretKey;
|
||||||
|
const IV = info.fileId;
|
||||||
|
encryptedIV = info.fileId;
|
||||||
|
|
||||||
|
const readRaw = new FileReader;
|
||||||
|
readRaw.onload = function(event) {
|
||||||
|
const rawArray = new Uint8Array(this.result);
|
||||||
|
originalBlob = rawArray;
|
||||||
|
|
||||||
|
window.crypto.subtle.digest('SHA-256', rawArray).then(hash => {
|
||||||
|
fileHash = hash;
|
||||||
|
window.crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
{
|
||||||
|
kty: 'oct',
|
||||||
|
k: key,
|
||||||
|
alg: 'A128GCM',
|
||||||
|
ext: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AES-GCM'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
)
|
||||||
|
.then(cryptoKey => {
|
||||||
|
window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: hexToArray(IV),
|
||||||
|
additionalData: hash,
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
cryptoKey,
|
||||||
|
rawArray
|
||||||
|
)
|
||||||
|
.then(encrypted => {
|
||||||
|
assert(new Uint8Array(encrypted).toString() ===
|
||||||
|
new Uint8Array(file).toString());
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
readRaw.readAsArrayBuffer(newFile);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('File Receiver', function() {
|
||||||
|
|
||||||
|
class FakeXHR {
|
||||||
|
constructor() {
|
||||||
|
this.response = file;
|
||||||
|
this.status = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
static setup() {
|
||||||
|
FakeXHR.prototype.open = sinon.spy();
|
||||||
|
FakeXHR.prototype.send = function () {
|
||||||
|
this.onload();
|
||||||
|
}
|
||||||
|
|
||||||
|
FakeXHR.prototype.originalXHR = window.XMLHttpRequest;
|
||||||
|
|
||||||
|
FakeXHR.prototype.getResponseHeader = function () {
|
||||||
|
return JSON.stringify({
|
||||||
|
aad: arrayToHex(new Uint8Array(fileHash)),
|
||||||
|
filename: 'hello_world.txt',
|
||||||
|
id: encryptedIV
|
||||||
|
})
|
||||||
|
}
|
||||||
|
window.XMLHttpRequest = FakeXHR;
|
||||||
|
}
|
||||||
|
|
||||||
|
static restore() {
|
||||||
|
// originalXHR is a sinon FakeXMLHttpRequest, since
|
||||||
|
// fakeServer.create() is called in frontend.bundle.js
|
||||||
|
window.XMLHttpRequest.prototype.originalXHR.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cb = function(done) {
|
||||||
|
if (file === undefined ||
|
||||||
|
encryptedIV === undefined ||
|
||||||
|
fileHash === undefined ||
|
||||||
|
secretKey === undefined) {
|
||||||
|
assert.fail('Please run file sending tests before trying to receive the files.');
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
FakeXHR.setup();
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
before(cb)
|
||||||
|
|
||||||
|
after(function() {
|
||||||
|
FakeXHR.restore();
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should decrypt properly', function() {
|
||||||
|
const fr = new FileReceiver();
|
||||||
|
location.hash = secretKey;
|
||||||
|
return fr.download().then(([decrypted, name]) => {
|
||||||
|
assert(name);
|
||||||
|
assert(new Uint8Array(decrypted).toString() ===
|
||||||
|
new Uint8Array(originalBlob).toString())
|
||||||
|
}).catch(err => {
|
||||||
|
console.log(err, err.stack);
|
||||||
|
assert.fail();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should emit decrypting events', function() {
|
||||||
|
const fr = new FileReceiver();
|
||||||
|
location.hash = secretKey;
|
||||||
|
|
||||||
|
let testDecrypting = true;
|
||||||
|
|
||||||
|
fr.on('decrypting', isStillDecrypting => {
|
||||||
|
assert(!(!testDecrypting && isStillDecrypting));
|
||||||
|
testDecrypting = isStillDecrypting;
|
||||||
|
});
|
||||||
|
|
||||||
|
fr.on('safe', isSafe => {
|
||||||
|
assert(isSafe);
|
||||||
|
})
|
||||||
|
|
||||||
|
return fr.download().then(([decrypted, name]) => {
|
||||||
|
assert(decrypted);
|
||||||
|
assert(name);
|
||||||
|
assert(!testDecrypting);
|
||||||
|
}).catch(err => {
|
||||||
|
console.log(err, err.stack);
|
||||||
|
assert.fail();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should emit hashing events', function() {
|
||||||
|
const fr = new FileReceiver();
|
||||||
|
location.hash = secretKey;
|
||||||
|
|
||||||
|
let testHashing = true;
|
||||||
|
|
||||||
|
fr.on('hashing', isStillHashing => {
|
||||||
|
assert(!(!testHashing && isStillHashing));
|
||||||
|
testHashing = isStillHashing;
|
||||||
|
});
|
||||||
|
|
||||||
|
fr.on('safe', isSafe => {
|
||||||
|
assert(isSafe);
|
||||||
|
})
|
||||||
|
|
||||||
|
return fr.download().then(([decrypted, name]) => {
|
||||||
|
assert(decrypted);
|
||||||
|
assert(name);
|
||||||
|
assert(!testHashing);
|
||||||
|
}).catch(err => {
|
||||||
|
assert.fail();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should catch fraudulent checksums', function(done) {
|
||||||
|
// Use the secret key and file hash of the previous file to encrypt,
|
||||||
|
// which has a different hash than this one (different strings).
|
||||||
|
const newFile = new FakeFile('hello_world.txt',
|
||||||
|
['This is some data, with a changed hash.'])
|
||||||
|
const readRaw = new FileReader();
|
||||||
|
|
||||||
|
readRaw.onload = function(event) {
|
||||||
|
const plaintext = new Uint8Array(this.result);
|
||||||
|
window.crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
{
|
||||||
|
kty: 'oct',
|
||||||
|
k: secretKey,
|
||||||
|
alg: 'A128GCM',
|
||||||
|
ext: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AES-GCM'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
)
|
||||||
|
.then(key => {
|
||||||
|
// The file hash used here is the hash of the fake
|
||||||
|
// file from the previous test; it's a phony checksum.
|
||||||
|
return window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: hexToArray(encryptedIV),
|
||||||
|
additionalData: fileHash,
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
plaintext
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.then(encrypted => {
|
||||||
|
file = encrypted;
|
||||||
|
const fr = new FileReceiver();
|
||||||
|
location.hash = secretKey;
|
||||||
|
|
||||||
|
fr.on('unsafe', isUnsafe => {
|
||||||
|
assert(isUnsafe)
|
||||||
|
})
|
||||||
|
|
||||||
|
fr.on('safe', () => {
|
||||||
|
// This event should not be emitted.
|
||||||
|
assert.fail();
|
||||||
|
})
|
||||||
|
|
||||||
|
fr.download().then(() => {
|
||||||
|
assert.fail();
|
||||||
|
done();
|
||||||
|
}).catch(err => {
|
||||||
|
assert(1);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
readRaw.readAsArrayBuffer(newFile);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not decrypt with an incorrect checksum', function() {
|
||||||
|
FakeXHR.prototype.getResponseHeader = function () {
|
||||||
|
return JSON.stringify({
|
||||||
|
aad: 'some_bad_hashz',
|
||||||
|
filename: 'hello_world.txt',
|
||||||
|
id: encryptedIV
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fr = new FileReceiver();
|
||||||
|
location.hash = secretKey;
|
||||||
|
|
||||||
|
return fr.download().then(([decrypted, name]) => {
|
||||||
|
assert(decrypted);
|
||||||
|
assert(name);
|
||||||
|
assert.fail();
|
||||||
|
}).catch(err => {
|
||||||
|
assert(1);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user