This commit is contained in:
Danny Coates 2017-08-02 16:50:58 -07:00
parent a7de951115
commit caeba94e04
No known key found for this signature in database
GPG Key ID: 4C442633C62E00CB
8 changed files with 345 additions and 322 deletions

View File

@ -1,8 +1,6 @@
const EventEmitter = require('events'); const EventEmitter = require('events');
const { arrayToHex } = require('./utils'); const { arrayToHex } = require('./utils');
const Raven = window.Raven;
class FileSender extends EventEmitter { class FileSender extends EventEmitter {
constructor(file) { constructor(file) {
super(); super();
@ -38,15 +36,14 @@ class FileSender extends EventEmitter {
const self = this; const self = this;
self.emit('loading', true); self.emit('loading', true);
return Promise.all([ return Promise.all([
window.crypto.subtle window.crypto.subtle.generateKey(
.generateKey( {
{ name: 'AES-GCM',
name: 'AES-GCM', length: 128
length: 128 },
}, true,
true, ['encrypt', 'decrypt']
['encrypt', 'decrypt'] ),
),
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.readAsArrayBuffer(this.file); reader.readAsArrayBuffer(this.file);

View File

@ -1,12 +1,7 @@
/* global MAXFILESIZE EXPIRE_SECONDS */ /* global MAXFILESIZE EXPIRE_SECONDS */
require('./common'); require('./common');
const FileSender = require('./fileSender'); const FileSender = require('./fileSender');
const { const { notify, findMetric, sendEvent, ONE_DAY_IN_MS } = require('./utils');
notify,
findMetric,
sendEvent,
ONE_DAY_IN_MS
} = require('./utils');
const bytes = require('bytes'); const bytes = require('bytes');
const Storage = require('./storage'); const Storage = require('./storage');
const storage = new Storage(localStorage); const storage = new Storage(localStorage);

View File

@ -1,12 +1,8 @@
/*** index.html ***/ /*** index.html ***/
html { html {
background: url('resources/send_bg.svg'); background: url('resources/send_bg.svg');
font-family: -apple-system, font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', Helvetica,
BlinkMacSystemFont, Arial, sans-serif;
'SF Pro Text',
Helvetica,
Arial,
sans-serif;
font-weight: 200; font-weight: 200;
background-size: 110%; background-size: 110%;
background-repeat: no-repeat; background-repeat: no-repeat;

View File

@ -14,7 +14,7 @@ const filename = path.join(__dirname, '..', 'public', 'version.json');
const filedata = { const filedata = {
commit, commit,
source: pkg.homepage, source: pkg.homepage,
version: process.env.CIRCLE_TAG || `v${ pkg.version }` version: process.env.CIRCLE_TAG || `v${pkg.version}`
}; };
fs.writeFileSync(filename, JSON.stringify(filedata, null, 2) + '\n'); fs.writeFileSync(filename, JSON.stringify(filedata, null, 2) + '\n');

View File

@ -9,7 +9,7 @@ window.Raven = {
captureException: function(err) { captureException: function(err) {
console.error(err, err.stack); console.error(err, err.stack);
} }
} };
window.FakeFile = FakeFile; window.FakeFile = FakeFile;
window.FileSender = require('../../frontend/src/fileSender'); window.FileSender = require('../../frontend/src/fileSender');

View File

@ -15,83 +15,84 @@ let originalBlob;
describe('File Sender', function() { describe('File Sender', function() {
before(function() { before(function() {
server.respondImmediately = true; server.respondImmediately = true;
server.respondWith( server.respondWith('POST', '/upload', function(request) {
'POST', const reader = new FileReader();
'/upload', reader.readAsArrayBuffer(request.requestBody.get('data'));
function(request) {
const reader = new FileReader();
reader.readAsArrayBuffer(request.requestBody.get('data'));
reader.onload = function(event) { reader.onload = function(event) {
file = this.result; file = this.result;
} };
const responseObj = JSON.parse(request.requestHeaders['X-File-Metadata']); const responseObj = JSON.parse(request.requestHeaders['X-File-Metadata']);
request.respond( request.respond(
200, 200,
{'Content-Type': 'application/json'}, { 'Content-Type': 'application/json' },
JSON.stringify({url: 'some url', JSON.stringify({
id: responseObj.id, url: 'some url',
delete: responseObj.delete}) id: responseObj.id,
) delete: responseObj.delete
} })
) );
}) });
});
it('Should get a loading event emission', function() { it('Should get a loading event emission', function() {
const file = new FakeFile('hello_world.txt', ['This is some data.']) const file = new FakeFile('hello_world.txt', ['This is some data.']);
const fs = new FileSender(file); const fs = new FileSender(file);
let testLoading = true; let testLoading = true;
fs.on('loading', isStillLoading => { fs.on('loading', isStillLoading => {
assert(!(!testLoading && isStillLoading)); assert(!(!testLoading && isStillLoading));
testLoading = isStillLoading; testLoading = isStillLoading;
});
return fs
.upload()
.then(info => {
assert(info);
assert(!testLoading);
}) })
.catch(err => {
return fs.upload() console.log(err, err.stack);
.then(info => { assert.fail();
assert(info); });
assert(!testLoading); });
})
.catch(err => {
console.log(err, err.stack);
assert.fail();
});
})
it('Should get a hashing event emission', function() { it('Should get a hashing event emission', function() {
const file = new FakeFile('hello_world.txt', ['This is some data.']) const file = new FakeFile('hello_world.txt', ['This is some data.']);
const fs = new FileSender(file); const fs = new FileSender(file);
let testHashing = true; let testHashing = true;
fs.on('hashing', isStillHashing => { fs.on('hashing', isStillHashing => {
assert(!(!testHashing && isStillHashing)); assert(!(!testHashing && isStillHashing));
testHashing = isStillHashing; testHashing = isStillHashing;
}) });
return fs.upload() return fs
.then(info => { .upload()
assert(info); .then(info => {
assert(!testHashing); assert(info);
}) assert(!testHashing);
.catch(err => { })
console.log(err, err.stack); .catch(err => {
assert.fail(); console.log(err, err.stack);
}); assert.fail();
}) });
});
it('Should get a encrypting event emission', function() { it('Should get a encrypting event emission', function() {
const file = new FakeFile('hello_world.txt', ['This is some data.']) const file = new FakeFile('hello_world.txt', ['This is some data.']);
const fs = new FileSender(file); const fs = new FileSender(file);
let testEncrypting = true; let testEncrypting = true;
fs.on('encrypting', isStillEncrypting => { fs.on('encrypting', isStillEncrypting => {
assert(!(!testEncrypting && isStillEncrypting)); assert(!(!testEncrypting && isStillEncrypting));
testEncrypting = isStillEncrypting; testEncrypting = isStillEncrypting;
}) });
return fs.upload() return fs
.upload()
.then(info => { .then(info => {
assert(info); assert(info);
assert(!testEncrypting); assert(!testEncrypting);
@ -100,67 +101,68 @@ describe('File Sender', function() {
console.log(err, err.stack); console.log(err, err.stack);
assert.fail(); assert.fail();
}); });
}) });
it('Should encrypt a file properly', function(done) { it('Should encrypt a file properly', function(done) {
const newFile = new FakeFile('hello_world.txt', ['This is some data.']) const newFile = new FakeFile('hello_world.txt', ['This is some data.']);
const fs = new FileSender(newFile); const fs = new FileSender(newFile);
fs.upload().then(info => { fs.upload().then(info => {
const key = info.secretKey; const key = info.secretKey;
secretKey = info.secretKey; secretKey = info.secretKey;
const IV = info.fileId; const IV = info.fileId;
encryptedIV = info.fileId; encryptedIV = info.fileId;
const readRaw = new FileReader; const readRaw = new FileReader();
readRaw.onload = function(event) { readRaw.onload = function(event) {
const rawArray = new Uint8Array(this.result); const rawArray = new Uint8Array(this.result);
originalBlob = rawArray; originalBlob = rawArray;
window.crypto.subtle.digest('SHA-256', rawArray).then(hash => { window.crypto.subtle.digest('SHA-256', rawArray).then(hash => {
fileHash = hash; fileHash = hash;
window.crypto.subtle.importKey( window.crypto.subtle
'jwk', .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', kty: 'oct',
iv: hexToArray(IV), k: key,
additionalData: hash, alg: 'A128GCM',
tagLength: 128 ext: true
}, },
cryptoKey, {
rawArray name: 'AES-GCM'
},
true,
['encrypt', 'decrypt']
) )
.then(encrypted => { .then(cryptoKey => {
assert(new Uint8Array(encrypted).toString() === window.crypto.subtle
new Uint8Array(file).toString()); .encrypt(
done(); {
}) 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); readRaw.readAsArrayBuffer(newFile);
}) });
}) });
}); });
describe('File Receiver', function() { describe('File Receiver', function() {
class FakeXHR { class FakeXHR {
constructor() { constructor() {
this.response = file; this.response = file;
@ -169,19 +171,19 @@ describe('File Receiver', function() {
static setup() { static setup() {
FakeXHR.prototype.open = sinon.spy(); FakeXHR.prototype.open = sinon.spy();
FakeXHR.prototype.send = function () { FakeXHR.prototype.send = function() {
this.onload(); this.onload();
} };
FakeXHR.prototype.originalXHR = window.XMLHttpRequest; FakeXHR.prototype.originalXHR = window.XMLHttpRequest;
FakeXHR.prototype.getResponseHeader = function () { FakeXHR.prototype.getResponseHeader = function() {
return JSON.stringify({ return JSON.stringify({
aad: arrayToHex(new Uint8Array(fileHash)), aad: arrayToHex(new Uint8Array(fileHash)),
filename: 'hello_world.txt', filename: 'hello_world.txt',
id: encryptedIV id: encryptedIV
}) });
} };
window.XMLHttpRequest = FakeXHR; window.XMLHttpRequest = FakeXHR;
} }
@ -191,38 +193,47 @@ describe('File Receiver', function() {
window.XMLHttpRequest.prototype.originalXHR.restore(); window.XMLHttpRequest.prototype.originalXHR.restore();
} }
} }
const cb = function(done) { const cb = function(done) {
if (file === undefined || if (
encryptedIV === undefined || file === undefined ||
fileHash === undefined || encryptedIV === undefined ||
secretKey === undefined) { fileHash === undefined ||
assert.fail('Please run file sending tests before trying to receive the files.'); secretKey === undefined
) {
assert.fail(
'Please run file sending tests before trying to receive the files.'
);
done(); done();
} }
FakeXHR.setup(); FakeXHR.setup();
done(); done();
} };
before(cb) before(cb);
after(function() { after(function() {
FakeXHR.restore(); FakeXHR.restore();
}) });
it('Should decrypt properly', function() { it('Should decrypt properly', function() {
const fr = new FileReceiver(); const fr = new FileReceiver();
location.hash = secretKey; location.hash = secretKey;
return fr.download().then(([decrypted, name]) => { return fr
assert(name); .download()
assert(new Uint8Array(decrypted).toString() === .then(([decrypted, name]) => {
new Uint8Array(originalBlob).toString()) assert(name);
}).catch(err => { assert(
console.log(err, err.stack); new Uint8Array(decrypted).toString() ===
assert.fail(); new Uint8Array(originalBlob).toString()
}) );
}) })
.catch(err => {
console.log(err, err.stack);
assert.fail();
});
});
it('Should emit decrypting events', function() { it('Should emit decrypting events', function() {
const fr = new FileReceiver(); const fr = new FileReceiver();
@ -237,17 +248,20 @@ describe('File Receiver', function() {
fr.on('safe', isSafe => { fr.on('safe', isSafe => {
assert(isSafe); assert(isSafe);
}) });
return fr.download().then(([decrypted, name]) => { return fr
assert(decrypted); .download()
assert(name); .then(([decrypted, name]) => {
assert(!testDecrypting); assert(decrypted);
}).catch(err => { assert(name);
console.log(err, err.stack); assert(!testDecrypting);
assert.fail(); })
}) .catch(err => {
}) console.log(err, err.stack);
assert.fail();
});
});
it('Should emit hashing events', function() { it('Should emit hashing events', function() {
const fr = new FileReceiver(); const fr = new FileReceiver();
@ -262,99 +276,109 @@ describe('File Receiver', function() {
fr.on('safe', isSafe => { fr.on('safe', isSafe => {
assert(isSafe); assert(isSafe);
}) });
return fr.download().then(([decrypted, name]) => { return fr
assert(decrypted); .download()
assert(name); .then(([decrypted, name]) => {
assert(!testHashing); assert(decrypted);
}).catch(err => { assert(name);
assert.fail(); assert(!testHashing);
}) })
}) .catch(err => {
assert.fail();
});
});
it('Should catch fraudulent checksums', function(done) { it('Should catch fraudulent checksums', function(done) {
// Use the secret key and file hash of the previous file to encrypt, // Use the secret key and file hash of the previous file to encrypt,
// which has a different hash than this one (different strings). // which has a different hash than this one (different strings).
const newFile = new FakeFile('hello_world.txt', const newFile = new FakeFile('hello_world.txt', [
['This is some data, with a changed hash.']) 'This is some data, with a changed hash.'
]);
const readRaw = new FileReader(); const readRaw = new FileReader();
readRaw.onload = function(event) { readRaw.onload = function(event) {
const plaintext = new Uint8Array(this.result); const plaintext = new Uint8Array(this.result);
window.crypto.subtle.importKey( window.crypto.subtle
'jwk', .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', kty: 'oct',
iv: hexToArray(encryptedIV), k: secretKey,
additionalData: fileHash, alg: 'A128GCM',
tagLength: 128 ext: true
}, },
key, {
plaintext name: 'AES-GCM'
},
true,
['encrypt', 'decrypt']
) )
}) .then(key => {
.then(encrypted => { // The file hash used here is the hash of the fake
file = encrypted; // file from the previous test; it's a phony checksum.
const fr = new FileReceiver(); return window.crypto.subtle.encrypt(
location.hash = secretKey; {
name: 'AES-GCM',
fr.on('unsafe', isUnsafe => { iv: hexToArray(encryptedIV),
assert(isUnsafe) additionalData: fileHash,
tagLength: 128
},
key,
plaintext
);
}) })
.then(encrypted => {
file = encrypted;
const fr = new FileReceiver();
location.hash = secretKey;
fr.on('safe', () => { fr.on('unsafe', isUnsafe => {
// This event should not be emitted. assert(isUnsafe);
assert.fail(); });
})
fr.download().then(() => { fr.on('safe', () => {
assert.fail(); // This event should not be emitted.
done(); assert.fail();
}).catch(err => { });
assert(1);
done(); fr
}) .download()
}) .then(() => {
} assert.fail();
done();
})
.catch(err => {
assert(1);
done();
});
});
};
readRaw.readAsArrayBuffer(newFile); readRaw.readAsArrayBuffer(newFile);
}) });
it('Should not decrypt with an incorrect checksum', function() { it('Should not decrypt with an incorrect checksum', function() {
FakeXHR.prototype.getResponseHeader = function () { FakeXHR.prototype.getResponseHeader = function() {
return JSON.stringify({ return JSON.stringify({
aad: 'some_bad_hashz', aad: 'some_bad_hashz',
filename: 'hello_world.txt', filename: 'hello_world.txt',
id: encryptedIV id: encryptedIV
}) });
} };
const fr = new FileReceiver(); const fr = new FileReceiver();
location.hash = secretKey; location.hash = secretKey;
return fr.download().then(([decrypted, name]) => { return fr
assert(decrypted); .download()
assert(name); .then(([decrypted, name]) => {
assert.fail(); assert(decrypted);
}).catch(err => { assert(name);
assert(1); assert.fail();
}) })
}) .catch(err => {
assert(1);
}) });
});
});

View File

@ -4,7 +4,6 @@ const proxyquire = require('proxyquire');
const request = require('supertest'); const request = require('supertest');
const fs = require('fs'); const fs = require('fs');
const logStub = {}; const logStub = {};
logStub.info = sinon.stub(); logStub.info = sinon.stub();
logStub.error = sinon.stub(); logStub.error = sinon.stub();
@ -38,17 +37,21 @@ describe('Server integration tests', function() {
storage.flushall(); storage.flushall();
storage.quit(); storage.quit();
server.close(); server.close();
}) });
function upload() { function upload() {
return request(server).post('/upload') return request(server)
.field('fname', 'test_upload.txt') .post('/upload')
.set('X-File-Metadata', JSON.stringify({ .field('fname', 'test_upload.txt')
aad: '11111', .set(
id: '111111111111111111111111', 'X-File-Metadata',
filename: 'test_upload.txt' JSON.stringify({
})) aad: '11111',
.attach('file', './test/test_upload.txt') id: '111111111111111111111111',
filename: 'test_upload.txt'
})
)
.attach('file', './test/test_upload.txt');
} }
it('Responds with a 200 when the service is up', function() { it('Responds with a 200 when the service is up', function() {
@ -56,115 +59,123 @@ describe('Server integration tests', function() {
}); });
it('Rejects with a 404 when a file id is not valid', function() { it('Rejects with a 404 when a file id is not valid', function() {
return request(server).post('/upload/123') return request(server)
.field('fname', 'test_upload.txt') .post('/upload/123')
.set('X-File-Metadata', JSON.stringify({ .field('fname', 'test_upload.txt')
'silly': 'text' .set(
})) 'X-File-Metadata',
.attach('file', './test/test_upload.txt') JSON.stringify({
.expect(404) silly: 'text'
}) })
)
.attach('file', './test/test_upload.txt')
.expect(404);
});
it('Accepts a file and stores it when properly uploaded', function(done) { it('Accepts a file and stores it when properly uploaded', function(done) {
upload().then(res => { upload().then(res => {
assert(res.body.hasOwnProperty('delete')); assert(res.body.hasOwnProperty('delete'));
uuid = res.body.delete; uuid = res.body.delete;
assert(res.body.hasOwnProperty('url')); assert(res.body.hasOwnProperty('url'));
assert(res.body.hasOwnProperty('id')); assert(res.body.hasOwnProperty('id'));
fileId = res.body.id; fileId = res.body.id;
fs.access('./static/' + fileId, fs.constants.F_OK, err => { fs.access('./static/' + fileId, fs.constants.F_OK, err => {
if (err) { if (err) {
done(new Error('The file does not exist')); done(new Error('The file does not exist'));
} else { } else {
done(); done();
} }
}) });
}) });
}) });
it('Responds with a 200 if a file exists', function() { it('Responds with a 200 if a file exists', function() {
return request(server).get('/exists/' + fileId) return request(server).get('/exists/' + fileId).expect(200);
.expect(200) });
})
it('Exists in the redis server', function() { it('Exists in the redis server', function() {
return storage.exists(fileId) return storage
.then(() => assert(1)) .exists(fileId)
.catch(err => assert.fail()) .then(() => assert(1))
}) .catch(err => assert.fail());
});
it('Fails delete if the delete token does not match', function() { it('Fails delete if the delete token does not match', function() {
return request(server).post('/delete/' + fileId) return request(server)
.send({ delete_token: 11 }) .post('/delete/' + fileId)
.expect(404); .send({ delete_token: 11 })
}) .expect(404);
});
it('Fails delete if the id is invalid', function() { it('Fails delete if the id is invalid', function() {
return request(server).post('/delete/1') return request(server).post('/delete/1').expect(404);
.expect(404); });
})
it('Successfully deletes if the id is valid and the delete token matches', function(done) { it('Successfully deletes if the id is valid and the delete token matches', function(
request(server).post('/delete/' + fileId) done
.send({ delete_token: uuid }) ) {
.expect(200) request(server)
.then(() => { .post('/delete/' + fileId)
fs.access('./static/' + fileId, fs.constants.F_OK, err => { .send({ delete_token: uuid })
if (err) { .expect(200)
done(); .then(() => {
} else { fs.access('./static/' + fileId, fs.constants.F_OK, err => {
done(new Error('The file does not exist')); if (err) {
} done();
}) } else {
}) done(new Error('The file does not exist'));
}) }
});
});
});
it('Responds with a 404 if a file does not exist', function() { it('Responds with a 404 if a file does not exist', function() {
return request(server).get('/exists/notfound') return request(server).get('/exists/notfound').expect(404);
.expect(404) });
})
it('Uploads properly after a delete', function(done) { it('Uploads properly after a delete', function(done) {
upload().then(res => { upload().then(res => {
assert(res.body.hasOwnProperty('delete')); assert(res.body.hasOwnProperty('delete'));
uuid = res.body.delete; uuid = res.body.delete;
assert(res.body.hasOwnProperty('url')); assert(res.body.hasOwnProperty('url'));
assert(res.body.hasOwnProperty('id')); assert(res.body.hasOwnProperty('id'));
fileId = res.body.id; fileId = res.body.id;
fs.access('./static/' + fileId, fs.constants.F_OK, err => { fs.access('./static/' + fileId, fs.constants.F_OK, err => {
if (err) { if (err) {
done(new Error('The file does not exist')); done(new Error('The file does not exist'));
} else { } else {
done(); done();
} }
}) });
}) });
}) });
it('Responds with a 200 for the download page', function() { it('Responds with a 200 for the download page', function() {
return request(server).get('/download/' + fileId) return request(server).get('/download/' + fileId).expect(200);
.expect(200); });
})
it('Downloads a file properly', function() { it('Downloads a file properly', function() {
return request(server).get('/assets/download/' + fileId) return request(server).get('/assets/download/' + fileId).then(res => {
.then(res => { assert(res.header.hasOwnProperty('content-disposition'));
assert(res.header.hasOwnProperty('content-disposition')); assert(res.header.hasOwnProperty('content-type'));
assert(res.header.hasOwnProperty('content-type')) assert(res.header.hasOwnProperty('content-length'));
assert(res.header.hasOwnProperty('content-length')) assert(res.header.hasOwnProperty('x-file-metadata'));
assert(res.header.hasOwnProperty('x-file-metadata')) assert.equal(
assert.equal(res.header['content-disposition'], 'attachment; filename=test_upload.txt') res.header['content-disposition'],
assert.equal(res.header['content-type'], 'application/octet-stream') 'attachment; filename=test_upload.txt'
}) );
}) assert.equal(res.header['content-type'], 'application/octet-stream');
});
});
it('The file is deleted after one download', function() { it('The file is deleted after one download', function() {
assert(!fs.existsSync('./static/' + fileId)); assert(!fs.existsSync('./static/' + fileId));
}) });
it('No longer exists in the redis server', function() { it('No longer exists in the redis server', function() {
return storage.exists(fileId) return storage
.then(() => assert.fail()) .exists(fileId)
.catch(err => assert(1)) .then(() => assert.fail())
}) .catch(err => assert(1));
});
}); });

View File

@ -110,9 +110,9 @@ describe('Testing Set using aws', function() {
it('Should pass when the file is successfully uploaded', function() { it('Should pass when the file is successfully uploaded', function() {
const buf = Buffer.alloc(10); const buf = Buffer.alloc(10);
sinon.stub(crypto, 'randomBytes').returns(buf); sinon.stub(crypto, 'randomBytes').returns(buf);
s3Stub.upload.returns({promise: () => Promise.resolve()}); s3Stub.upload.returns({ promise: () => Promise.resolve() });
return storage return storage
.set('123', {on: sinon.stub()}, 'Filename.moz', {}) .set('123', { on: sinon.stub() }, 'Filename.moz', {})
.then(() => { .then(() => {
assert(expire.calledOnce); assert(expire.calledOnce);
assert(expire.calledWith('123', 86400)); assert(expire.calledWith('123', 86400));
@ -121,9 +121,9 @@ describe('Testing Set using aws', function() {
}); });
it('Should fail if there was an error during uploading', function() { it('Should fail if there was an error during uploading', function() {
s3Stub.upload.returns({promise: () => Promise.reject()}); s3Stub.upload.returns({ promise: () => Promise.reject() });
return storage return storage
.set('123', {on: sinon.stub()}, 'Filename.moz', 'url.com') .set('123', { on: sinon.stub() }, 'Filename.moz', 'url.com')
.then(_reply => assert.fail()) .then(_reply => assert.fail())
.catch(err => assert(1)); .catch(err => assert(1));
}); });