From d6c0489fa3370389ff1e92e3b0a9f6b0e59ca144 Mon Sep 17 00:00:00 2001 From: Danny Coates Date: Sat, 24 Feb 2018 11:24:12 -0800 Subject: [PATCH] more frontend tests and some factoring based on them --- app/api.js | 115 ++++++++++++-------------- app/fileManager.js | 8 +- app/fileReceiver.js | 49 +++++------ app/fileSender.js | 22 +++-- test/frontend/tests/api-tests.js | 19 ++++- test/frontend/tests/workflow-tests.js | 47 +++++++++++ 6 files changed, 153 insertions(+), 107 deletions(-) diff --git a/app/api.js b/app/api.js index 585cf1e9..e314657d 100644 --- a/app/api.js +++ b/app/api.js @@ -91,10 +91,15 @@ export async function setPassword(id, owner_token, keychain) { return response.ok; } -export function uploadFile(encrypted, metadata, verifierB64, keychain) { +export function uploadFile( + encrypted, + metadata, + verifierB64, + keychain, + onprogress +) { const xhr = new XMLHttpRequest(); const upload = { - onprogress: function() {}, cancel: function() { xhr.abort(); }, @@ -122,7 +127,7 @@ export function uploadFile(encrypted, metadata, verifierB64, keychain) { fd.append('data', blob); xhr.upload.addEventListener('progress', function(event) { if (event.lengthComputable) { - upload.onprogress([event.loaded, event.total]); + onprogress([event.loaded, event.total]); } }); xhr.open('post', '/api/upload', true); @@ -132,79 +137,63 @@ export function uploadFile(encrypted, metadata, verifierB64, keychain) { return upload; } -function download(id, keychain) { +function download(id, keychain, onprogress, canceller) { const xhr = new XMLHttpRequest(); - const download = { - onprogress: function() {}, - cancel: function() { - xhr.abort(); - }, - result: new Promise(async function(resolve, reject) { - xhr.addEventListener('loadend', function() { - const authHeader = xhr.getResponseHeader('WWW-Authenticate'); - if (authHeader) { - keychain.nonce = parseNonce(authHeader); - } - if (xhr.status !== 200) { - return reject(new Error(xhr.status)); - } - - const blob = new Blob([xhr.response]); - const fileReader = new FileReader(); - fileReader.readAsArrayBuffer(blob); - fileReader.onload = function() { - resolve(this.result); - }; - }); - xhr.addEventListener('progress', function(event) { - if (event.lengthComputable && event.target.status === 200) { - download.onprogress([event.loaded, event.total]); - } - }); - const auth = await keychain.authHeader(); - xhr.open('get', `/api/download/${id}`); - xhr.setRequestHeader('Authorization', auth); - xhr.responseType = 'blob'; - xhr.send(); - }) + canceller.oncancel = function() { + xhr.abort(); }; + return new Promise(async function(resolve, reject) { + xhr.addEventListener('loadend', function() { + canceller.oncancel = function() {}; + const authHeader = xhr.getResponseHeader('WWW-Authenticate'); + if (authHeader) { + keychain.nonce = parseNonce(authHeader); + } + if (xhr.status !== 200) { + return reject(new Error(xhr.status)); + } - return download; + const blob = new Blob([xhr.response]); + const fileReader = new FileReader(); + fileReader.readAsArrayBuffer(blob); + fileReader.onload = function() { + resolve(this.result); + }; + }); + xhr.addEventListener('progress', function(event) { + if (event.lengthComputable && event.target.status === 200) { + onprogress([event.loaded, event.total]); + } + }); + const auth = await keychain.authHeader(); + xhr.open('get', `/api/download/${id}`); + xhr.setRequestHeader('Authorization', auth); + xhr.responseType = 'blob'; + xhr.send(); + }); } -async function tryDownload(id, keychain, onprogress, tries = 1) { - const dl = download(id, keychain); - dl.onprogress = onprogress; +async function tryDownload(id, keychain, onprogress, canceller, tries = 1) { try { - const result = await dl.result; + const result = await download(id, keychain, onprogress, canceller); return result; } catch (e) { if (e.message === '401' && --tries > 0) { - return tryDownload(id, keychain, onprogress, tries); + return tryDownload(id, keychain, onprogress, canceller, tries); } throw e; } } -export function downloadFile(id, keychain) { - let cancelled = false; - function updateProgress(p) { - if (cancelled) { - // This is a bit of a hack - // We piggyback off of the progress event as a chance to cancel. - // Otherwise wiring the xhr abort up while allowing retries - // gets pretty nasty. - // 'this' here is the object returned by download(id, keychain) - return this.cancel(); - } - dl.onprogress(p); - } - const dl = { - onprogress: function() {}, - cancel: function() { - cancelled = true; - }, - result: tryDownload(id, keychain, updateProgress, 2) +export function downloadFile(id, keychain, onprogress) { + const canceller = { + oncancel: function() {} // download() sets this + }; + function cancel() { + canceller.oncancel(); + } + return { + cancel, + result: tryDownload(id, keychain, onprogress, canceller, 2) }; - return dl; } diff --git a/app/fileManager.js b/app/fileManager.js index a788e1b8..04eb99a3 100644 --- a/app/fileManager.js +++ b/app/fileManager.js @@ -149,8 +149,6 @@ export default function(state, emitter) { const receiver = new FileReceiver(file); try { await receiver.getMetadata(); - receiver.on('progress', updateProgress); - receiver.on('decrypting', render); state.transfer = receiver; } catch (e) { if (e.message === '401') { @@ -164,14 +162,16 @@ export default function(state, emitter) { }); emitter.on('download', async file => { - state.transfer.on('progress', render); + state.transfer.on('progress', updateProgress); state.transfer.on('decrypting', render); const links = openLinksInNewTab(); const size = file.size; try { const start = Date.now(); metrics.startedDownload({ size: file.size, ttl: file.ttl }); - await state.transfer.download(); + const dl = state.transfer.download(); + render(); + await dl; const time = Date.now() - start; const speed = size / (time / 1000); await delay(1000); diff --git a/app/fileReceiver.js b/app/fileReceiver.js index 9976ed1a..27dccf6d 100644 --- a/app/fileReceiver.js +++ b/app/fileReceiver.js @@ -30,54 +30,44 @@ export default class FileReceiver extends Nanobus { } cancel() { - this.cancelled = true; - if (this.fileDownload) { - this.fileDownload.cancel(); + if (this.downloadRequest) { + this.downloadRequest.cancel(); } } reset() { - this.fileDownload = null; this.msg = 'fileSizeProgress'; this.state = 'initialized'; this.progress = [0, 1]; - this.cancelled = false; } async getMetadata() { const meta = await metadata(this.fileInfo.id, this.keychain); - if (meta) { - this.keychain.setIV(meta.iv); - this.fileInfo.name = meta.name; - this.fileInfo.type = meta.type; - this.fileInfo.iv = meta.iv; - this.fileInfo.size = meta.size; - this.state = 'ready'; - return; - } - this.state = 'invalid'; - return; + this.keychain.setIV(meta.iv); + this.fileInfo.name = meta.name; + this.fileInfo.type = meta.type; + this.fileInfo.iv = meta.iv; + this.fileInfo.size = meta.size; + this.state = 'ready'; } async download(noSave = false) { this.state = 'downloading'; - this.emit('progress', this.progress); - try { - const download = await downloadFile(this.fileInfo.id, this.keychain); - download.onprogress = p => { + this.downloadRequest = await downloadFile( + this.fileInfo.id, + this.keychain, + p => { this.progress = p; - this.emit('progress', p); - }; - this.fileDownload = download; - const ciphertext = await download.result; - this.fileDownload = null; + this.emit('progress'); + } + ); + try { + const ciphertext = await this.downloadRequest.result; + this.downloadRequest = null; this.msg = 'decryptingFile'; this.state = 'decrypting'; this.emit('decrypting'); const plaintext = await this.keychain.decryptFile(ciphertext); - if (this.cancelled) { - throw new Error(0); - } if (!noSave) { await saveFile({ plaintext, @@ -87,9 +77,8 @@ export default class FileReceiver extends Nanobus { } this.msg = 'downloadFinish'; this.state = 'complete'; - return; } catch (e) { - this.state = 'invalid'; + this.downloadRequest = null; throw e; } } diff --git a/app/fileSender.js b/app/fileSender.js index a6cbc0a5..a3fb7a25 100644 --- a/app/fileSender.js +++ b/app/fileSender.js @@ -9,11 +9,8 @@ export default class FileSender extends Nanobus { constructor(file) { super('FileSender'); this.file = file; - this.uploadRequest = null; - this.msg = 'importingFile'; - this.progress = [0, 1]; - this.cancelled = false; this.keychain = new Keychain(); + this.reset(); } get progressRatio() { @@ -31,6 +28,13 @@ export default class FileSender extends Nanobus { }; } + reset() { + this.uploadRequest = null; + this.msg = 'importingFile'; + this.progress = [0, 1]; + this.cancelled = false; + } + cancel() { this.cancelled = true; if (this.uploadRequest) { @@ -71,13 +75,13 @@ export default class FileSender extends Nanobus { encrypted, metadata, authKeyB64, - this.keychain + this.keychain, + p => { + this.progress = p; + this.emit('progress', p); + } ); this.msg = 'fileSizeProgress'; - this.uploadRequest.onprogress = p => { - this.progress = p; - this.emit('progress', p); - }; try { const result = await this.uploadRequest.result; const time = Date.now() - start; diff --git a/test/frontend/tests/api-tests.js b/test/frontend/tests/api-tests.js index e11c47b9..fb470add 100644 --- a/test/frontend/tests/api-tests.js +++ b/test/frontend/tests/api-tests.js @@ -16,11 +16,28 @@ describe('API', function() { const encrypted = await keychain.encryptFile(plaintext); const meta = await keychain.encryptMetadata(metadata); const verifierB64 = await keychain.authKeyB64(); - const up = api.uploadFile(encrypted, meta, verifierB64, keychain); + const p = function() {}; + const up = api.uploadFile(encrypted, meta, verifierB64, keychain, p); const result = await up.result; assert.ok(result.url); assert.ok(result.id); assert.ok(result.ownerToken); }); + + it('can be cancelled', async function() { + const keychain = new Keychain(); + const encrypted = await keychain.encryptFile(plaintext); + const meta = await keychain.encryptMetadata(metadata); + const verifierB64 = await keychain.authKeyB64(); + const p = function() {}; + const up = api.uploadFile(encrypted, meta, verifierB64, keychain, p); + up.cancel(); + try { + await up.result; + assert.fail('not cancelled'); + } catch (e) { + assert.equal(e.message, '0'); + } + }); }); }); diff --git a/test/frontend/tests/workflow-tests.js b/test/frontend/tests/workflow-tests.js index 539aa40d..23bc8efc 100644 --- a/test/frontend/tests/workflow-tests.js +++ b/test/frontend/tests/workflow-tests.js @@ -87,6 +87,53 @@ describe('Upload / Download flow', function() { assert.equal(fr.fileInfo.name, blob.name); }); + it('can cancel the upload', async function() { + const fs = new FileSender(blob); + const up = fs.upload(); + fs.cancel(); // before encrypting + try { + await up; + assert.fail('not cancelled'); + } catch (e) { + assert.equal(e.message, '0'); + } + fs.reset(); + fs.once('encrypting', () => fs.cancel()); + try { + await fs.upload(); + assert.fail('not cancelled'); + } catch (e) { + assert.equal(e.message, '0'); + } + fs.reset(); + fs.once('progress', () => fs.cancel()); + try { + await fs.upload(); + assert.fail('not cancelled'); + } catch (e) { + assert.equal(e.message, '0'); + } + }); + + it('can cancel the download', async function() { + const fs = new FileSender(blob); + const file = await fs.upload(); + const fr = new FileReceiver({ + secretKey: file.toJSON().secretKey, + id: file.id, + nonce: file.keychain.nonce, + requiresPassword: false + }); + await fr.getMetadata(); + fr.once('progress', () => fr.cancel()); + try { + await fr.download(noSave); + assert.fail('not cancelled'); + } catch (e) { + assert.equal(e.message, '0'); + } + }); + it('can allow multiple downloads', async function() { const fs = new FileSender(blob); const file = await fs.upload();