diff --git a/.eslintignore b/.eslintignore index c3446d5d0..65b9ea36c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,6 +11,7 @@ js/libloki.js js/util_worker.js js/libsignal-protocol-worker.js libtextsecure/components.js +libloki/test/test.js libtextsecure/test/test.js test/test.js @@ -25,4 +26,3 @@ test/blanket_mocha.js # TypeScript generated files ts/**/*.js - diff --git a/.gitignore b/.gitignore index 586046d86..cd183624e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ js/libtextsecure.js js/libloki.js libtextsecure/components.js libtextsecure/test/test.js +libloki/test/test.js stylesheets/*.css test/test.js diff --git a/Gruntfile.js b/Gruntfile.js index 5c392b946..a97bf9860 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -91,6 +91,14 @@ module.exports = grunt => { src: ['libloki/libloki-protocol.js'], dest: 'js/libloki.js', }, + lokitest: { + src: [ + 'node_modules/mocha/mocha.js', + 'node_modules/chai/chai.js', + 'libloki/test/_test.js', + ], + dest: 'libloki/test/test.js', + }, libtextsecuretest: { src: [ 'node_modules/jquery/dist/jquery.js', @@ -355,6 +363,17 @@ module.exports = grunt => { } ); + grunt.registerTask( + 'loki-unit-tests', + 'Run loki unit tests w/Electron', + function thisNeeded() { + const environment = grunt.option('env') || 'test-loki'; + const done = this.async(); + + runTests(environment, done); + } + ); + grunt.registerMultiTask( 'test-release', 'Test packaged releases', @@ -442,7 +461,8 @@ module.exports = grunt => { 'locale-patch', ]); grunt.registerTask('dev', ['default', 'watch']); - grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']); + grunt.registerTask('test', ['unit-tests', 'lib-unit-tests', 'loki-unit-tests']); + grunt.registerTask('test-loki', ['loki-unit-tests']); grunt.registerTask('date', ['gitinfo', 'getExpireTime']); grunt.registerTask('default', [ 'exec:build-protobuf', diff --git a/libloki/test/.eslintrc b/libloki/test/.eslintrc new file mode 100644 index 000000000..29971976c --- /dev/null +++ b/libloki/test/.eslintrc @@ -0,0 +1,26 @@ +{ + "env": { + "browser": true, + "node": false, + "mocha": true + }, + "parserOptions": { + "sourceType": "script" + }, + "rules": { + "strict": "off", + "more/no-then": "off" + }, + "globals": { + "assert": true, + "assertEqualArrayBuffers": true, + "dcodeIO": true, + "getString": true, + "hexToArrayBuffer": true, + "MockServer": true, + "MockSocket": true, + "clearDatabase": true, + "PROTO_ROOT": true, + "stringToArrayBuffer": true + } +} diff --git a/libloki/test/_test.js b/libloki/test/_test.js new file mode 100644 index 000000000..af7726e93 --- /dev/null +++ b/libloki/test/_test.js @@ -0,0 +1,66 @@ +/* global mocha, chai, assert, Whisper */ + +mocha.setup('bdd'); +window.assert = chai.assert; +window.PROTO_ROOT = '../../protos'; + +const OriginalReporter = mocha._reporter; + +const SauceReporter = function Constructor(runner) { + const failedTests = []; + + runner.on('end', () => { + window.mochaResults = runner.stats; + window.mochaResults.reports = failedTests; + }); + + runner.on('fail', (test, err) => { + const flattenTitles = item => { + const titles = []; + while (item.parent.title) { + titles.push(item.parent.title); + // eslint-disable-next-line no-param-reassign + item = item.parent; + } + return titles.reverse(); + }; + failedTests.push({ + name: test.title, + result: false, + message: err.message, + stack: err.stack, + titles: flattenTitles(test), + }); + }); + + // eslint-disable-next-line no-new + new OriginalReporter(runner); +}; + +SauceReporter.prototype = OriginalReporter.prototype; + +mocha.reporter(SauceReporter); + +// Override the database id. +window.Whisper = window.Whisper || {}; +window.Whisper.Database = window.Whisper.Database || {}; +Whisper.Database.id = 'test'; + +/* + * global helpers for tests + */ +window.assertEqualArrayBuffers = (ab1, ab2) => { + assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2)); +}; + +window.hexToArrayBuffer = str => { + const ret = new ArrayBuffer(str.length / 2); + const array = new Uint8Array(ret); + for (let i = 0; i < str.length / 2; i += 1) + array[i] = parseInt(str.substr(i * 2, 2), 16); + return ret; +}; + +window.clearDatabase = async () => { + await window.Signal.Data.removeAll(); +}; diff --git a/libloki/test/index.html b/libloki/test/index.html new file mode 100644 index 000000000..2245c97e2 --- /dev/null +++ b/libloki/test/index.html @@ -0,0 +1,39 @@ + + + + + libloki test runner + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libloki/test/libloki-protocol_test.js b/libloki/test/libloki-protocol_test.js new file mode 100644 index 000000000..c165b6d27 --- /dev/null +++ b/libloki/test/libloki-protocol_test.js @@ -0,0 +1,60 @@ +/* global libsignal, libloki, textsecure, StringView */ + +'use strict'; + +describe('ConversationCollection', () => { + let fallbackCipher; + let identityKey; + let testKey; + let address; + const store = textsecure.storage.protocol; + + beforeEach(async () => { + clearDatabase(); + identityKey = await libsignal.KeyHelper.generateIdentityKeyPair(); + store.put('identityKey', identityKey); + const key = libsignal.crypto.getRandomBytes(32); + const pubKeyString = StringView.arrayBufferToHex(key); + address = new libsignal.SignalProtocolAddress( + pubKeyString, + 1 // sourceDevice + ); + testKey = { + pubKey: libsignal.crypto.getRandomBytes(33), + privKey: libsignal.crypto.getRandomBytes(32), + }; + fallbackCipher = new libloki.FallBackSessionCipher(address); + textsecure.storage.put('maxPreKeyId', 0); + textsecure.storage.put('signedKeyId', 2); + await store.storeSignedPreKey(1, testKey); + }); + + it('should encrypt fallback cipher messages as friend requests', async () => { + const buffer = new ArrayBuffer(10); + const { type } = await fallbackCipher.encrypt(buffer); + assert(type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST); + }); + + it('should should generate a new prekey bundle for a new contact', async () => { + const pubKey = libsignal.crypto.getRandomBytes(32); + const pubKeyString = StringView.arrayBufferToHex(pubKey); + const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1); + const newBundle = await libloki.getPreKeyBundleForNumber(pubKeyString); + const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1); + assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1); + + const testKeyArray = new Uint8Array(testKey.pubKey); + assert.isDefined(newBundle); + assert.isDefined(newBundle.identityKey); + assert.isDefined(newBundle.deviceId); + assert.isDefined(newBundle.preKeyId); + assert.isDefined(newBundle.signedKeyId); + assert.isDefined(newBundle.preKey); + assert.isDefined(newBundle.signedKey); + assert.isDefined(newBundle.signature); + const signedKeyArray = new Uint8Array(newBundle.signedKey.toArrayBuffer()); + assert.strictEqual(testKeyArray.byteLength, signedKeyArray.byteLength); + for (let i = 0 ; i !== testKeyArray.byteLength ; i += 1) + assert.strictEqual(testKeyArray[i], signedKeyArray[i]); + }); +}); diff --git a/libtextsecure/test/in_memory_signal_protocol_store.js b/libtextsecure/test/in_memory_signal_protocol_store.js index 9b8ce813a..712208853 100644 --- a/libtextsecure/test/in_memory_signal_protocol_store.js +++ b/libtextsecure/test/in_memory_signal_protocol_store.js @@ -152,4 +152,16 @@ SignalProtocolStore.prototype = { resolve(deviceIds); }); }, + async loadPreKeyForContactIdentityKeyString(contactIdentityKeyString) { + return new Promise(resolve => { + const key = this.get(`25519KeypreKey${contactIdentityKeyString}`); + if (!key) resolve(undefined); + resolve({ + pubKey: key.publicKey, + privKey: key.privateKey, + keyId: key.id, + recipient: key.recipient, + }); + }); + }, }; diff --git a/main.js b/main.js index 1e794c6da..62c41fc52 100644 --- a/main.js +++ b/main.js @@ -318,6 +318,10 @@ function createWindow() { mainWindow.loadURL( prepareURL([__dirname, 'libtextsecure', 'test', 'index.html']) ); + } else if (config.environment === 'test-loki') { + mainWindow.loadURL( + prepareURL([__dirname, 'libloki', 'test', 'index.html']) + ); } else { mainWindow.loadURL(prepareURL([__dirname, 'background.html'])); } @@ -341,6 +345,7 @@ function createWindow() { if ( config.environment === 'test' || config.environment === 'test-lib' || + config.environment === 'test-loki' || (mainWindow.readyForShutdown && windowState.shouldQuit()) ) { return; @@ -611,7 +616,11 @@ app.on('ready', async () => { const userDataPath = await getRealPath(app.getPath('userData')); const installPath = await getRealPath(app.getAppPath()); - if (process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'test-lib') { + if ( + process.env.NODE_ENV !== 'test' && + process.env.NODE_ENV !== 'test-lib' && + process.env.NODE_ENV !== 'test-loki' + ) { installFileHandler({ protocol: electronProtocol, userDataPath, @@ -777,7 +786,8 @@ app.on('window-all-closed', () => { if ( process.platform !== 'darwin' || config.environment === 'test' || - config.environment === 'test-lib' + config.environment === 'test-lib' || + config.environment === 'test-loki' ) { app.quit(); } diff --git a/package.json b/package.json index a0de59a19..dcaf564a0 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "prepare-import-build": "node prepare_import_build.js", "publish-to-apt": "NAME=$npm_package_name VERSION=$npm_package_version ./aptly.sh", "test": "yarn test-node && yarn test-electron", + "test-loki": "NODE_ENV=test-loki yarn run start", "test-electron": "yarn grunt test", "test-node": "mocha --recursive test/app test/modules ts/test", "test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test",