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",