some frontend unit tests
This commit is contained in:
parent
4929437283
commit
78728ce4ca
24 changed files with 708 additions and 417 deletions
|
@ -1,2 +1,8 @@
|
|||
env:
|
||||
browser: true
|
||||
browser: true
|
||||
|
||||
parserOptions:
|
||||
sourceType: module
|
||||
|
||||
rules:
|
||||
node/no-unsupported-features: off
|
|
@ -1,22 +0,0 @@
|
|||
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'));
|
||||
driver.wait(until.titleMatches(/^[0-9]$/));
|
||||
|
||||
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.'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,22 +0,0 @@
|
|||
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('../../app/fileSender');
|
||||
window.FileReceiver = require('../../app/fileReceiver');
|
||||
window.sinon = require('sinon');
|
||||
window.server = window.sinon.fakeServer.create();
|
||||
window.assert = require('assert');
|
||||
const utils = require('../../app/utils');
|
||||
window.b64ToArray = utils.b64ToArray;
|
||||
window.arrayToB64 = utils.arrayToB64;
|
|
@ -1,24 +0,0 @@
|
|||
<!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>
|
|
@ -1,230 +0,0 @@
|
|||
const FileSender = window.FileSender;
|
||||
const FileReceiver = window.FileReceiver;
|
||||
const FakeFile = window.FakeFile;
|
||||
const assert = window.assert;
|
||||
const server = window.server;
|
||||
const b64ToArray = window.b64ToArray;
|
||||
const sinon = window.sinon;
|
||||
|
||||
let file;
|
||||
let encryptedIV;
|
||||
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 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
|
||||
.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: b64ToArray(IV),
|
||||
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({
|
||||
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 ||
|
||||
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;
|
||||
});
|
||||
|
||||
return fr
|
||||
.download()
|
||||
.then(([decrypted, name]) => {
|
||||
assert(decrypted);
|
||||
assert(name);
|
||||
assert(!testDecrypting);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err, err.stack);
|
||||
assert.fail();
|
||||
});
|
||||
});
|
||||
});
|
16
test/frontend/index.js
Normal file
16
test/frontend/index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function kv(f) {
|
||||
return `require('./tests/${f}')`;
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
const files = fs.readdirSync(path.join(__dirname, 'tests'));
|
||||
const code = files.map(kv).join(';\n');
|
||||
return {
|
||||
code,
|
||||
dependencies: files.map(f => require.resolve('./tests/' + f)),
|
||||
cacheable: false
|
||||
};
|
||||
};
|
47
test/frontend/routes.js
Normal file
47
test/frontend/routes.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
const html = require('choo/html');
|
||||
const assets = require('../../common/assets');
|
||||
|
||||
module.exports = function(app) {
|
||||
app.get('/mocha.css', function(req, res) {
|
||||
res.sendFile(require.resolve('mocha/mocha.css'));
|
||||
});
|
||||
app.get('/mocha.js', function(req, res) {
|
||||
res.sendFile(require.resolve('mocha/mocha.js'));
|
||||
});
|
||||
app.get('/test', function(req, res) {
|
||||
res.send(
|
||||
html`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="/mocha.css" />
|
||||
<script src="/mocha.js"></script>
|
||||
<script>
|
||||
const reporters = mocha.constructor.reporters;
|
||||
function Combo(runner) {
|
||||
reporters.HTML.call(this, runner)
|
||||
reporters.JSON.call(this, runner)
|
||||
}
|
||||
Object.setPrototypeOf(Combo.prototype, reporters.HTML.prototype)
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
reporter: Combo
|
||||
})
|
||||
</script>
|
||||
<script src="/jsconfig.js"></script>
|
||||
<script src="${assets.get('runtime.js')}"></script>
|
||||
<script src="${assets.get('vendor.js')}"></script>
|
||||
<script src="${assets.get('tests.js')}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha"></div>
|
||||
<script>
|
||||
mocha.checkLeaks();
|
||||
const runner = mocha.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`.toString()
|
||||
);
|
||||
});
|
||||
};
|
63
test/frontend/runner.js
Normal file
63
test/frontend/runner.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
/* eslint-disable no-undef, no-process-exit */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
const puppeteer = require('puppeteer');
|
||||
const webpack = require('webpack');
|
||||
const config = require('../../webpack.config');
|
||||
const middleware = require('webpack-dev-middleware');
|
||||
const express = require('express');
|
||||
const devRoutes = require('../../server/dev');
|
||||
const app = express();
|
||||
|
||||
const wpm = middleware(webpack(config), { logLevel: 'silent' });
|
||||
app.use(wpm);
|
||||
devRoutes(app, { middleware: wpm });
|
||||
|
||||
function onConsole(msg) {
|
||||
// excluding 'log' because mocha uses it to write the json output
|
||||
if (msg.type() !== 'log') {
|
||||
console.error(msg.text());
|
||||
}
|
||||
}
|
||||
|
||||
const server = app.listen(async function() {
|
||||
let exitCode = -1;
|
||||
const browser = await puppeteer.launch();
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
page.on('console', onConsole);
|
||||
page.on('pageerror', console.log.bind(console));
|
||||
await page.goto(`http://127.0.0.1:${server.address().port}/test`);
|
||||
await page.waitFor(() => typeof runner.testResults !== 'undefined', {
|
||||
timeout: 5000
|
||||
});
|
||||
const results = await page.evaluate(() => runner.testResults);
|
||||
const coverage = await page.evaluate(() => __coverage__);
|
||||
if (coverage) {
|
||||
const dir = path.resolve(__dirname, '../../.nyc_output');
|
||||
mkdirp.sync(dir);
|
||||
fs.writeFileSync(
|
||||
path.resolve(dir, 'frontend.json'),
|
||||
JSON.stringify(coverage)
|
||||
);
|
||||
}
|
||||
const stats = results.stats;
|
||||
exitCode = stats.failures;
|
||||
console.log(`${stats.passes} passing (${stats.duration}ms)\n`);
|
||||
if (stats.failures) {
|
||||
console.log('Failures:\n');
|
||||
for (const f of results.failures) {
|
||||
console.log(`${f.fullTitle}`);
|
||||
console.log(` ${f.err.stack}\n`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
browser.close();
|
||||
server.close(() => {
|
||||
process.exit(exitCode);
|
||||
});
|
||||
}
|
||||
});
|
26
test/frontend/tests/api-tests.js
Normal file
26
test/frontend/tests/api-tests.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import assert from 'assert';
|
||||
import * as api from '../../../app/api';
|
||||
import Keychain from '../../../app/keychain';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const plaintext = encoder.encode('hello world!');
|
||||
const metadata = {
|
||||
name: 'test.txt',
|
||||
type: 'text/plain'
|
||||
};
|
||||
|
||||
describe('API', function() {
|
||||
describe('uploadFile', function() {
|
||||
it('returns file info on success', async function() {
|
||||
const keychain = new Keychain();
|
||||
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 result = await up.result;
|
||||
assert.ok(result.url);
|
||||
assert.ok(result.id);
|
||||
assert.ok(result.ownerToken);
|
||||
});
|
||||
});
|
||||
});
|
17
test/frontend/tests/fileSender-tests.js
Normal file
17
test/frontend/tests/fileSender-tests.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import assert from 'assert';
|
||||
import FileSender from '../../../app/fileSender';
|
||||
|
||||
// FileSender uses a File in real life but a Blob works for testing
|
||||
const blob = new Blob(['hello world!'], { type: 'text/plain' });
|
||||
blob.name = 'text.txt';
|
||||
|
||||
describe('FileSender', function() {
|
||||
describe('upload', function() {
|
||||
it('returns an OwnedFile on success', async function() {
|
||||
const fs = new FileSender(blob);
|
||||
const file = await fs.upload();
|
||||
assert.ok(file.id);
|
||||
assert.equal(file.name, blob.name);
|
||||
});
|
||||
});
|
||||
});
|
41
test/frontend/tests/keychain-tests.js
Normal file
41
test/frontend/tests/keychain-tests.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import assert from 'assert';
|
||||
import Keychain from '../../../app/keychain';
|
||||
|
||||
describe('Keychain', function() {
|
||||
describe('setPassword', function() {
|
||||
it('changes the authKey', async function() {
|
||||
const k = new Keychain();
|
||||
const original = await k.authKeyB64();
|
||||
k.setPassword('foo', 'some://url');
|
||||
const pwd = await k.authKeyB64();
|
||||
assert.notEqual(pwd, original);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encrypt / decrypt file', function() {
|
||||
it('can decrypt text it encrypts', async function() {
|
||||
const enc = new TextEncoder();
|
||||
const dec = new TextDecoder();
|
||||
const text = 'hello world!';
|
||||
const k = new Keychain();
|
||||
const ciphertext = await k.encryptFile(enc.encode(text));
|
||||
assert.notEqual(dec.decode(ciphertext), text);
|
||||
const plaintext = await k.decryptFile(ciphertext);
|
||||
assert.equal(dec.decode(plaintext), text);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encrypt / decrypt metadata', function() {
|
||||
it('can decrypt metadata it encrypts', async function() {
|
||||
const k = new Keychain();
|
||||
const meta = {
|
||||
name: 'foo',
|
||||
type: 'bar/baz'
|
||||
};
|
||||
const ciphertext = await k.encryptMetadata(meta);
|
||||
const result = await k.decryptMetadata(ciphertext);
|
||||
assert.equal(result.name, meta.name);
|
||||
assert.equal(result.type, meta.type);
|
||||
});
|
||||
});
|
||||
});
|
133
test/frontend/tests/workflow-tests.js
Normal file
133
test/frontend/tests/workflow-tests.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
import assert from 'assert';
|
||||
import FileSender from '../../../app/fileSender';
|
||||
import FileReceiver from '../../../app/fileReceiver';
|
||||
|
||||
const headless = /Headless/.test(navigator.userAgent);
|
||||
const noSave = !headless; // only run the saveFile code if headless
|
||||
|
||||
// FileSender uses a File in real life but a Blob works for testing
|
||||
const blob = new Blob(['hello world!'], { type: 'text/plain' });
|
||||
blob.name = 'test.txt';
|
||||
|
||||
describe('Upload / Download flow', function() {
|
||||
it('can only download once by default', 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();
|
||||
await fr.download(noSave);
|
||||
try {
|
||||
await fr.download(noSave);
|
||||
assert.fail('downloaded again');
|
||||
} catch (e) {
|
||||
assert.equal(e.message, '404');
|
||||
}
|
||||
});
|
||||
|
||||
it('downloads with the correct password', async function() {
|
||||
const fs = new FileSender(blob);
|
||||
const file = await fs.upload();
|
||||
await file.setPassword('magic');
|
||||
const fr = new FileReceiver({
|
||||
secretKey: file.toJSON().secretKey,
|
||||
id: file.id,
|
||||
url: file.url,
|
||||
nonce: file.keychain.nonce,
|
||||
requiresPassword: true,
|
||||
password: 'magic'
|
||||
});
|
||||
await fr.getMetadata();
|
||||
await fr.download(noSave);
|
||||
assert.equal(fr.state, 'complete');
|
||||
});
|
||||
|
||||
it('blocks invalid passwords from downloading', async function() {
|
||||
const fs = new FileSender(blob);
|
||||
const file = await fs.upload();
|
||||
await file.setPassword('magic');
|
||||
const fr = new FileReceiver({
|
||||
secretKey: file.toJSON().secretKey,
|
||||
id: file.id,
|
||||
url: file.url,
|
||||
nonce: file.keychain.nonce,
|
||||
requiresPassword: true,
|
||||
password: 'password'
|
||||
});
|
||||
try {
|
||||
await fr.getMetadata();
|
||||
assert.fail('got metadata with bad password');
|
||||
} catch (e) {
|
||||
assert.equal(e.message, '401');
|
||||
}
|
||||
try {
|
||||
// We can't decrypt without IV from metadata
|
||||
// but let's try to download anyway
|
||||
await fr.download();
|
||||
assert.fail('downloaded file with bad password');
|
||||
} catch (e) {
|
||||
assert.equal(e.message, '401');
|
||||
}
|
||||
});
|
||||
|
||||
it('retries a bad nonce', async function() {
|
||||
const fs = new FileSender(blob);
|
||||
const file = await fs.upload();
|
||||
const fr = new FileReceiver({
|
||||
secretKey: file.toJSON().secretKey,
|
||||
id: file.id,
|
||||
nonce: null, // oops
|
||||
requiresPassword: false
|
||||
});
|
||||
await fr.getMetadata();
|
||||
assert.equal(fr.fileInfo.name, blob.name);
|
||||
});
|
||||
|
||||
it('can allow multiple downloads', 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 file.changeLimit(2);
|
||||
await fr.getMetadata();
|
||||
await fr.download(noSave);
|
||||
await file.updateDownloadCount();
|
||||
assert.equal(file.dtotal, 1);
|
||||
|
||||
await fr.download(noSave);
|
||||
await file.updateDownloadCount();
|
||||
assert.equal(file.dtotal, 2);
|
||||
try {
|
||||
await fr.download(noSave);
|
||||
assert.fail('downloaded too many times');
|
||||
} catch (e) {
|
||||
assert.equal(e.message, '404');
|
||||
}
|
||||
});
|
||||
|
||||
it('can delete the file before 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 file.del();
|
||||
try {
|
||||
await fr.getMetadata();
|
||||
assert.fail('file still exists');
|
||||
} catch (e) {
|
||||
assert.equal(e.message, '404');
|
||||
}
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue