DEV: Added tests.

DEV: Added tests.

From 49febbcf724332535032a72978314c1811621d00 Mon Sep 17 00:00:00 2001
From: Dan Ungureanu <dan@ungureanu.me>
Date: Fri, 30 Nov 2018 23:03:41 +0200
Subject: [PATCH] DEV: Added tests.


diff --git a/test/javascripts/acceptance/encrypt-test.js.es6 b/test/javascripts/acceptance/encrypt-test.js.es6
new file mode 100644
index 0000000..4f2f2d3
--- /dev/null
+++ b/test/javascripts/acceptance/encrypt-test.js.es6
@@ -0,0 +1,142 @@
+import { acceptance } from "helpers/qunit-helpers";
+import {
+  exportPrivateKey,
+  exportPublicKey,
+  generateKeyPair,
+  generateSalt,
+  generatePassphraseKey
+} from "discourse/plugins/discourse-encrypt/lib/keys";
+import { saveKeyPairToIndexedDb } from "discourse/plugins/discourse-encrypt/lib/keys_db";
+
+/**
+ * @var Secret passphrase used for testing purposes.
+ */
+const PASSPHRASE = "curren7U$er.pa$$Phr4se";
+
+/**
+ * @var Constant string that is used to check for plaintext leakage.
+ */
+const PLAINTEXT = "!PL41N73X7!";
+
+/**
+ * @var User keys.
+ */
+const keys = {};
+
+/**
+ * @var Global assert instance used to report plaintext leakage.
+ */
+let globalAssert;
+
+/**
+ * Generates a key pair.
+ *
+ * @param passphrase
+ *
+ * @return Tuple consisting of public and private key, as CryptoKey and string.
+ */
+async function getKeyPair(passsphrase) {
+  const salt = generateSalt();
+  const passphraseKey = await generatePassphraseKey(passsphrase, salt);
+  const [publicKey, privateKey] = await generateKeyPair();
+  const publicStr = await exportPublicKey(publicKey);
+  const privateStr = await exportPrivateKey(privateKey, passphraseKey);
+  return [publicKey, privateKey, publicStr, privateStr, salt];
+}
+
+/**
+ * Sets up encryption.
+ */
+async function setupEncryption() {
+  /*
+   * Setup current user.
+   */
+  const keyPair = await getKeyPair(PASSPHRASE);
+  const [publicKey, privateKey, publicStr, privateStr] = keyPair;
+
+  // Enable on server-side.
+  const user = Discourse.User.current();
+  user.set("custom_fields.encrypt_public_key", publicStr);
+  user.set("custom_fields.encrypt_private_key", privateStr);
+
+  // Activate on client-side.
+  await saveKeyPairToIndexedDb(publicKey, privateKey);
+
+  /*
+   * Setup other users.
+   */
+  keys[user.username] = keyPair;
+}
+
+acceptance("Encrypt", {
+  loggedIn: true,
+  settings: { encrypt_enabled: true },
+
+  beforeEach() {
+    // Hook `XMLHttpRequest` to search for leaked plaintext.
+    XMLHttpRequest.prototype.send_ = XMLHttpRequest.prototype.send;
+    XMLHttpRequest.prototype.send = function(body) {
+      if (body && globalAssert) {
+        globalAssert.notContains(body, PLAINTEXT, "does not leak plaintext");
+      }
+      return this.send_(...arguments);
+    };
+  },
+
+  afterEach() {
+    // Restore `XMLHttpRequest`.
+    XMLHttpRequest.prototype.send = XMLHttpRequest.prototype.send_;
+    delete XMLHttpRequest.prototype.send_;
+  },
+
+  pretend(server, helper) {
+    server.get("/encrypt/userkeys", request => {
+      const response = {};
+      request.queryParams["usernames"].forEach(u => (response[u] = keys[u][2]));
+      return helper.response(response);
+    });
+
+    // TODO: Autocomplete is not available during testing.
+    //
+    // server.get("/u/search/users", () => {
+    //   return helper.response({
+    //     users: [
+    //       {
+    //         username: "codinghorror",
+    //         name: "codinghorror",
+    //         avatar_template: "/images/avatar.png"
+    //       }
+    //     ],
+    //     groups: []
+    //   });
+    // });
+  }
+});
+
+test("posting does not leak plaintext", async assert => {
+  globalAssert = assert;
+  await setupEncryption();
+
+  const composerActions = selectKit(".composer-actions");
+
+  await visit("/");
+  await click("#create-topic");
+
+  await composerActions.expand();
+  await composerActions.selectRowByValue("reply_as_private_message");
+
+  // TODO: Autocomplete is not available during testing.
+  //
+  // await fillIn("#private-message-users", "codinghorror");
+  // await click("#private-message-users");
+  // await keyEvent("#private-message-users", "keydown", 8);
+  // await click(".autocomplete ul li a:first");
+
+  await click(".reply-details a");
+  await fillIn("#reply-title", `Some hidden message ${PLAINTEXT}`);
+  await fillIn(".d-editor-input", `Hello, world! ${PLAINTEXT}`.repeat(42));
+
+  await click("button.create");
+
+  globalAssert = null;
+});
diff --git a/test/javascripts/helpers/helpers.js.es6 b/test/javascripts/helpers/helpers.js.es6
new file mode 100644
index 0000000..32588bf
--- /dev/null
+++ b/test/javascripts/helpers/helpers.js.es6
@@ -0,0 +1,58 @@
+/*
+ * Checks if a string is not contained in a string.
+ *
+ * @param haystack
+ * @param needle
+ * @param message
+ */
+QUnit.assert.notContains = function(haystack, needle, message) {
+  this.pushResult({
+    result: haystack.indexOf(needle) === -1,
+    actual: haystack,
+    expected: "not to contain " + needle,
+    message
+  });
+};
+
+/*
+ * Checks if two array-like objects are equal.
+ *
+ * @param haystack
+ * @param needle
+ * @param message
+ */
+QUnit.assert.arrayEqual = function(actual, expected) {
+  if (actual.length !== expected.length) {
+    this.pushResult({
+      result: false,
+      actual: actual.length,
+      expected: expected.length,
+      message: "array lengths are equal"
+    });
+
+    return;
+  }
+
+  let result = true;
+
+  for (let i = 0; i < actual.length; ++i) {
+    if (actual[i] !== expected[i]) {
+      result = false;
+      this.pushResult({
+        result,
+        actual: actual[i],
+        expected: expected[i],
+        message: `index ${i} matches`
+      });
+    }
+  }
+
+  if (result) {
+    this.pushResult({
+      result,
+      actual: actual,
+      expected: expected,
+      message: "arrays match"
+    });
+  }
+};
diff --git a/test/javascripts/lib/base64-test.js.es6 b/test/javascripts/lib/base64-test.js.es6
new file mode 100644
index 0000000..5dea5b3
--- /dev/null
+++ b/test/javascripts/lib/base64-test.js.es6
@@ -0,0 +1,37 @@
+import {
+  base64ToBuffer,
+  bufferToBase64
+} from "discourse/plugins/discourse-encrypt/lib/base64";
+
+QUnit.module("discourse-encrypt:lib:base64");
+
+test("base64 to buffer", assert => {
+  let check = (actual, expected) =>
+    assert.arrayEqual(base64ToBuffer(actual), expected);
+
+  check("", []);
+  check("QQ==", [0x41]);
+  check("QUI=", [0x41, 0x42]);
+  check("QUJD", [0x41, 0x42, 0x43]);
+  check("QUJDRA==", [0x41, 0x42, 0x43, 0x44]);
+});
+
+test("buffer to base64", assert => {
+  let check = (actual, expected) =>
+    assert.equal(bufferToBase64(new Uint8Array(actual)), expected);
+
+  check([], "");
+  check([0x41], "QQ==");
+  check([0x41, 0x42], "QUI=");
+  check([0x41, 0x42, 0x43], "QUJD");
+  check([0x41, 0x42, 0x43, 0x44], "QUJDRA==");
+});
+
+test("buffer to base64 to buffer", assert => {
+  const array = [];
+  for (let i = 0; i < 32; ++i) {
+    const buffer = new Uint8Array(array);
+    assert.arrayEqual(base64ToBuffer(bufferToBase64(buffer)), buffer);
+    array.push(i);
+  }
+});
diff --git a/test/javascripts/lib/buffers-test.js.es6 b/test/javascripts/lib/buffers-test.js.es6
new file mode 100644
index 0000000..a7ebda7
--- /dev/null
+++ b/test/javascripts/lib/buffers-test.js.es6
@@ -0,0 +1,40 @@
+import {
+  stringToBuffer,
+  bufferToString
+} from "discourse/plugins/discourse-encrypt/lib/buffers";
+
+QUnit.module("discourse-encrypt:lib:buffers");
+
+test("string to buffer", assert => {
+  let check = (actual, expected) =>
+    assert.arrayEqual(new Uint16Array(stringToBuffer(actual)), expected);
+
+  check("", []);
+  check("A", [0x41]);
+  check("AB", [0x41, 0x42]);
+  check("ABC", [0x41, 0x42, 0x43]);
+  check("ABCD", [0x41, 0x42, 0x43, 0x44]);
+});
+
+test("buffer to string", assert => {
+  let check = (actual, expected) =>
+    assert.equal(bufferToString(new Uint16Array(actual)), expected);
+
+  check([], "");
+  check([0x41], "A");
+  check([0x41, 0x42], "AB");
+  check([0x41, 0x42,

GitHub