FIX: Show activation modal if identity is missing (#117)

FIX: Show activation modal if identity is missing (#117)

User’s identity was missing if they had encryption enabled, but the current device was not activated. To make it easy to use, the user was prompted to activate the device when they navigated to an encrypted topic. This behavior changed when getIdentity started to return a rejected promise when user has no identity (it used to return an empty identity).

This commit removes the underscore prefix from exported function names. It used to mean that functions could be used only in tests, but now an error is thrown instead.

diff --git a/assets/javascripts/discourse/controllers/activate-encrypt.js b/assets/javascripts/discourse/controllers/activate-encrypt.js
index ce893c8..dce07c3 100644
--- a/assets/javascripts/discourse/controllers/activate-encrypt.js
+++ b/assets/javascripts/discourse/controllers/activate-encrypt.js
@@ -5,26 +5,28 @@ import I18n from "I18n";
 
 export default Controller.extend(ModalFunctionality, {
   onShow() {
-    const models = this.models || [];
-    models.push(this.model);
+    const widgets = this.widgets || [];
+    widgets.push(this.model.widget);
 
     this.setProperties({
-      models: models,
+      widgets,
       passphrase: "",
       error: "",
     });
   },
 
   onClose() {
-    const models = this.models || [];
-    models.forEach((model) => {
-      model.state.encryptState = "error";
-      model.state.error = I18n.t(
+    if (!this.widgets) {
+      return;
+    }
+
+    this.widgets.forEach((widget) => {
+      widget.state.encryptState = "error";
+      widget.state.error = I18n.t(
         "encrypt.preferences.status_enabled_but_inactive"
       );
-      model.scheduleRerender();
+      widget.scheduleRerender();
     });
-    this.set("models", null);
   },
 
   actions: {
@@ -34,12 +36,11 @@ export default Controller.extend(ModalFunctionality, {
       return activateEncrypt(this.currentUser, this.passphrase)
         .then(() => {
           this.appEvents.trigger("encrypt:status-changed");
-          this.models.forEach((model) => {
-            model.state.decrypting = true;
-            model.state.decrypted = false;
-            model.scheduleRerender();
+          this.widgets.forEach((widget) => {
+            widget.state.encryptState = "decrypting";
+            widget.scheduleRerender();
           });
-          this.set("models", null);
+          this.set("widgets", null);
           this.send("closeModal");
         })
         .catch(() =>
diff --git a/assets/javascripts/discourse/initializers/decrypt-posts.js b/assets/javascripts/discourse/initializers/decrypt-posts.js
index c951302..92ec77d 100644
--- a/assets/javascripts/discourse/initializers/decrypt-posts.js
+++ b/assets/javascripts/discourse/initializers/decrypt-posts.js
@@ -368,13 +368,7 @@ export default {
         state.ciphertext = ciphertext;
 
         getIdentity()
-          .then((identity) => {
-            if (!identity) {
-              // Absence of private key means user did not activate encryption.
-              showModal("activate-encrypt", { model: this });
-              return;
-            }
-
+          .then(() => {
             getTopicKey(topicId)
               .then((key) => {
                 decrypt(key, ciphertext)
@@ -435,9 +429,7 @@ export default {
               });
           })
           .catch(() => {
-            state.encryptState = "error";
-            state.error = I18n.t("encrypt.invalid_identity");
-            this.scheduleRerender();
+            showModal("activate-encrypt", { model: { widget: this } });
           });
       }
 
diff --git a/assets/javascripts/lib/database.js b/assets/javascripts/lib/database.js
index 638f10b..f2d7f31 100644
--- a/assets/javascripts/lib/database.js
+++ b/assets/javascripts/lib/database.js
@@ -1,3 +1,4 @@
+import { isTesting } from "discourse-common/config/environment";
 import {
   exportIdentity,
   importIdentity,
@@ -25,11 +26,13 @@ export let useLocalStorage = false;
 /**
  * Force usage of local storage instead of IndexedDb.
  *
- * Used in tests
- *
  * @param {Boolean} value Whether to use local storage.
  */
-export function _setUseLocalStorage(value) {
+export function setUseLocalStorage(value) {
+  if (!isTesting()) {
+    throw new Error("`setUseLocalStorage` can be called from tests only");
+  }
+
   useLocalStorage = value;
 }
 
@@ -41,11 +44,13 @@ export let indexedDb = window.indexedDB;
 /**
  * Sets IndexedDb backend
  *
- * Used in tests
- *
  * @param {Object} value
  */
-export function _setIndexedDb(value) {
+export function setIndexedDb(value) {
+  if (!isTesting()) {
+    throw new Error("`setIndexedDb` can be called from tests only");
+  }
+
   indexedDb = value;
 }
 
@@ -57,11 +62,13 @@ export let userAgent = window.navigator.userAgent;
 /**
  * Sets browser's user agent string
  *
- * Used in tests
- *
  * @param {String} value
  */
-export function _setUserAgent(value) {
+export function setUserAgent(value) {
+  if (!isTesting()) {
+    throw new Error("`setUserAgent` can be called from tests only");
+  }
+
   userAgent = value;
 }
 
diff --git a/assets/javascripts/lib/discourse.js b/assets/javascripts/lib/discourse.js
index 161bb48..49e6584 100644
--- a/assets/javascripts/lib/discourse.js
+++ b/assets/javascripts/lib/discourse.js
@@ -1,3 +1,4 @@
+import { isTesting } from "discourse-common/config/environment";
 import { ajax } from "discourse/lib/ajax";
 import {
   DB_NAME,
@@ -75,6 +76,17 @@ class TopicTitle {
 }
 
 /**
+ * Resets loaded user identity
+ */
+export function resetUserIdentity() {
+  if (!isTesting()) {
+    throw new Error("`resetUserIdentity` can be called from tests only");
+  }
+
+  userIdentity = null;
+}
+
+/**
  * Gets current user's identity from the database and caches it for future
  * usage.
  *
diff --git a/assets/stylesheets/common/encrypt.scss b/assets/stylesheets/common/encrypt.scss
index bffa2d3..ffee949 100644
--- a/assets/stylesheets/common/encrypt.scss
+++ b/assets/stylesheets/common/encrypt.scss
@@ -42,6 +42,12 @@ pre.exported-key-pair {
   word-break: break-all;
 }
 
+.modal.activate-encrypt-modal {
+  input#passphrase {
+    width: 100%;
+  }
+}
+
 .modal .modal-body .paper-key {
   font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono",
     "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace,
diff --git a/test/javascripts/acceptance/encrypt-test.js b/test/javascripts/acceptance/encrypt-test.js
index 684e6f4..ea30f06 100644
--- a/test/javascripts/acceptance/encrypt-test.js
+++ b/test/javascripts/acceptance/encrypt-test.js
@@ -12,6 +12,7 @@ import EncryptLibDiscourse, {
   ENCRYPT_ENABLED,
   getEncryptionStatus,
   getIdentity,
+  resetUserIdentity,
 } from "discourse/plugins/discourse-encrypt/lib/discourse";
 import {
   encrypt,
@@ -19,6 +20,7 @@ import {
   exportKey,
   generateIdentity,
   generateKey,
+  importIdentity,
 } from "discourse/plugins/discourse-encrypt/lib/protocol";
 import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
 import { default as userFixtures } from "discourse/tests/fixtures/user-fixtures";
@@ -26,6 +28,7 @@ import { parsePostData } from "discourse/tests/helpers/create-pretender";
 import {
   acceptance,
   count,
+  exists,
   query,
   queryAll,
   updateCurrentUser,
@@ -171,6 +174,8 @@ acceptance("Encrypt", function (needs) {
       }
       return this.send_(...arguments);
     };
+
+    resetUserIdentity();
   });
 
   needs.hooks.afterEach(() => {
@@ -353,7 +358,260 @@ acceptance("Encrypt", function (needs) {
     assert.rejects(loadDbIdentity());
   });
 
-  test("viewing encrypted topic works", async (assert) => {
+  test("viewing encrypted topic works when just enabled", async (assert) => {
+    await setEncryptionStatus(ENCRYPT_ENABLED);
+    globalAssert = assert;
+
+    const identities = JSON.parse(User.current().encrypt_private);
+    const identity = await importIdentity(identities["passphrase"], PASSPHRASE);
+    const topicKey = await generateKey();
+    const exportedTopicKey = await exportKey(topicKey, identity.encryptPublic);
+    const encryptedTitle = await encrypt(topicKey, { raw: "Top Secret Title" });
+    const encryptedRaw = await encrypt(topicKey, { raw: "Top Secret Post" });
+
+    server.get("/t/42.json", () => {
+      return [
+        200,
+        { "Content-Type": "application/json" },
+        {
+          post_stream: {
+            posts: [
+              {
+                id: 42,
+                name: null,
+                username: "bar",

[... diff too long, it was truncated ...]

GitHub sha: a3816e5f5ead59a5dc3855d9b63ecd40bca5f1e4

This commit appears in #117 which was approved by eviltrout. It was merged by udan11.