some frontend unit tests

This commit is contained in:
Danny Coates 2018-02-20 20:31:27 -08:00
parent 4929437283
commit 78728ce4ca
No known key found for this signature in database
GPG key ID: 4C442633C62E00CB
24 changed files with 708 additions and 417 deletions

View file

@ -1,2 +1,8 @@
env:
browser: true
browser: true
parserOptions:
sourceType: module
rules:
node/no-unsupported-features: off

View file

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

View file

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

View file

@ -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>

View file

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

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

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

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

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