Bug 1101444 - [Loop] For each invalid inserted verification code, Device receives a valid verification code. r=spenrose, a=bajaj
authorFernando Jimenez <ferjmoreno@gmail.com>
Thu, 27 Nov 2014 11:53:43 +0100
changeset 204512 fcdb03d3674b2ba1986f319ab4044095b0c413e4
parent 204511 f7b776cd37bf6b129f71f34abb888255962b31ee
child 204513 fc137304880823d52e4833002892fe13b6128224
push id115
push userryanvm@gmail.com
push dateThu, 08 Jan 2015 14:26:44 +0000
treeherdermozilla-b2g32_v2_0m@83127168f548 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersspenrose, bajaj
bugs1101444
milestone32.0
Bug 1101444 - [Loop] For each invalid inserted verification code, Device receives a valid verification code. r=spenrose, a=bajaj
services/mobileid/MobileIdentityVerificationFlow.jsm
services/mobileid/tests/xpcshell/head.js
services/mobileid/tests/xpcshell/test_mobileid_manager.js
services/mobileid/tests/xpcshell/test_mobileid_verification_flow.js
services/mobileid/tests/xpcshell/xpcshell.ini
--- a/services/mobileid/MobileIdentityVerificationFlow.jsm
+++ b/services/mobileid/MobileIdentityVerificationFlow.jsm
@@ -32,78 +32,77 @@ MobileIdentityVerificationFlow.prototype
     return this.register()
     .then(
       (registerResult) => {
         log.debug("Register result ${}", registerResult);
         if (!registerResult || !registerResult.msisdnSessionToken) {
           return Promise.reject(ERROR_INTERNAL_UNEXPECTED);
         }
         this.sessionToken = registerResult.msisdnSessionToken;
-        return this._doVerification();
+        // We save the timestamp of the start of the verification timeout to be
+        // able to provide to the UI the remaining time on each retry.
+        if (!this.timer) {
+          log.debug("Creating verification code timer");
+          this.timerCreation = Date.now();
+          this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+          this.timer.initWithCallback(this.onVerificationCodeTimeout.bind(this),
+                                      VERIFICATIONCODE_TIMEOUT,
+                                      this.timer.TYPE_ONE_SHOT);
+        }
+
+        if (!this.verifyStrategy) {
+          return Promise.reject(ERROR_INTERNAL_INVALID_VERIFICATION_FLOW);
+        }
+
+        return this.verifyStrategy()
+        .then(() => {
+          return this._doVerification();
+        }, (reason) => {
+          this.verificationCodeDeferred.reject(reason);
+        });
       }
     )
   },
 
   _doVerification: function() {
     log.debug("_doVerification");
-    // We save the timestamp of the start of the verification timeout to be
-    // able to provide to the UI the remaining time on each retry.
-    if (!this.timer) {
-      log.debug("Creating verification code timer");
-      this.timerCreation = Date.now();
-      this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-      this.timer.initWithCallback(this.onVerificationCodeTimeout.bind(this),
-                                  VERIFICATIONCODE_TIMEOUT,
-                                  this.timer.TYPE_ONE_SHOT);
-    }
-
-    if (!this.verifyStrategy) {
-      return Promise.reject(ERROR_INTERNAL_INVALID_VERIFICATION_FLOW);
-    }
 
     this.verificationCodeDeferred = Promise.defer();
 
-    this.verifyStrategy()
-    .then(
-      () => {
-        // If the verification flow can be for an external phone number,
-        // we need to ask the user for the verification code.
-        // In that case we don't do a notification about the verification
-        // process being done until the user enters the verification code
-        // in the UI.
-        if (this.verificationOptions.external) {
-          let timeLeft = 0;
-          if (this.timer) {
-            timeLeft = this.timerCreation + VERIFICATIONCODE_TIMEOUT -
-                       Date.now();
+    // If the verification flow can be for an external phone number,
+    // we need to ask the user for the verification code.
+    // In that case we don't do a notification about the verification
+    // process being done until the user enters the verification code
+    // in the UI.
+    if (this.verificationOptions.external) {
+      let timeLeft = 0;
+      if (this.timer) {
+        timeLeft = this.timerCreation + VERIFICATIONCODE_TIMEOUT -
+                   Date.now();
+      }
+      this.ui.verificationCodePrompt(this.retries,
+                                     VERIFICATIONCODE_TIMEOUT / 1000,
+                                     timeLeft / 1000)
+      .then(
+        (verificationCode) => {
+          if (!verificationCode) {
+            return this.verificationCodeDeferred.reject(
+              ERROR_INTERNAL_INVALID_PROMPT_RESULT);
           }
-          this.ui.verificationCodePrompt(this.retries,
-                                         VERIFICATIONCODE_TIMEOUT / 1000,
-                                         timeLeft / 1000)
-          .then(
-            (verificationCode) => {
-              if (!verificationCode) {
-                return this.verificationCodeDeferred.reject(
-                  ERROR_INTERNAL_INVALID_PROMPT_RESULT);
-              }
-              // If the user got the verification code that means that the
-              // introduced phone number didn't belong to any of the inserted
-              // SIMs.
-              this.ui.verify();
-              this.verificationCodeDeferred.resolve(verificationCode);
-            }
-          );
-        } else {
+          // If the user got the verification code that means that the
+          // introduced phone number didn't belong to any of the inserted
+          // SIMs.
           this.ui.verify();
+          this.verificationCodeDeferred.resolve(verificationCode);
         }
-      },
-      (reason) => {
-        this.verificationCodeDeferred.reject(reason);
-      }
-    );
+      );
+    } else {
+      this.ui.verify();
+    }
+
     return this.verificationCodeDeferred.promise.then(
       this.onVerificationCode.bind(this)
     );
   },
 
   // When we receive a verification code from the UI, we check it against
   // the server. If the verification code is incorrect, we decrease the
   // number of retries left and allow the user to try again. If there is no
@@ -140,18 +139,21 @@ MobileIdentityVerificationFlow.prototype
         return this.verificationOptions;
       },
       (error) => {
         log.error("Verification code error " + error);
         this.retries--;
         log.error("Retries left " + this.retries);
         if (!this.retries) {
           this.ui.error(ERROR_NO_RETRIES_LEFT);
+          this.timer.cancel();
+          this.timer = null;
           return Promise.reject(ERROR_NO_RETRIES_LEFT);
         }
+        this.ui.error(ERROR_INVALID_VERIFICATION_CODE);
         this.verifying = false;
         if (this.queuedTimeout) {
           this.onVerificationCodeTimeout();
         }
         return this._doVerification();
       }
     );
   },
--- a/services/mobileid/tests/xpcshell/head.js
+++ b/services/mobileid/tests/xpcshell/head.js
@@ -1,19 +1,462 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
 "use strict";
 
+const Cm = Components.manager;
+
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 (function initMobileIdTestingInfrastructure() {
   do_get_profile();
 
   const PREF_FORCE_HTTPS = "services.mobileid.forcehttps";
   Services.prefs.setBoolPref(PREF_FORCE_HTTPS, false);
   Services.prefs.setCharPref("services.mobileid.loglevel", "Debug");
   Services.prefs.setCharPref("services.mobileid.server.uri",
                              "https://dummyurl.com");
 }).call(this);
+
+const DEBUG = false;
+
+const GET_ASSERTION_IPC_MSG = "MobileId:GetAssertion";
+const GET_ASSERTION_RETURN_OK = "MobileId:GetAssertion:Return:OK";
+const GET_ASSERTION_RETURN_KO = "MobileId:GetAssertion:Return:KO";
+
+// === Globals ===
+
+const ORIGIN = "app://afakeorigin";
+const APP_ID = 1;
+const PRINCIPAL = {
+  origin: ORIGIN,
+  appId: APP_ID
+};
+const PHONE_NUMBER = "+34666555444";
+const ANOTHER_PHONE_NUMBER = "+44123123123";
+const VERIFICATION_CODE = "123456";
+const SESSION_TOKEN = "aSessionToken";
+const ICC_ID = "aIccId";
+const ANOTHER_ICC_ID = "anotherIccId";
+const MNC = "aMnc";
+const ANOTHER_MNC = "anotherMnc";
+const MCC = "aMcc";
+const ANOTHER_MCC = "anotherMcc";
+const OPERATOR = "aOperator";
+const ANOTHER_OPERATOR = "anotherOperator";
+const RADIO_INTERFACE = {
+  rilContext: {
+    iccInfo: {
+      iccid: ICC_ID,
+      mcc: MCC,
+      mnc: MNC,
+      msisdn: PHONE_NUMBER,
+      operator: OPERATOR
+    }
+  },
+  voice: {
+    network: {
+      shortName: OPERATOR
+    },
+    roaming: false
+  },
+  data: {
+    network: {
+      shortName: OPERATOR
+    }
+  }
+};
+const ANOTHER_RADIO_INTERFACE = {
+  rilContext: {
+    iccInfo: {
+      iccid: ANOTHER_ICC_ID,
+      mcc: ANOTHER_MCC,
+      mnc: ANOTHER_MNC,
+      msisdn: ANOTHER_PHONE_NUMBER,
+      operator: ANOTHER_OPERATOR
+    }
+  },
+  voice: {
+    network: {
+      shortName: ANOTHER_OPERATOR
+    },
+    roaming: false
+  },
+  data: {
+    network: {
+      shortName: ANOTHER_OPERATOR
+    }
+  }
+};
+
+const INVALID_RADIO_INTERFACE = {
+  rilContext: {
+    iccInfo: {
+      iccid: null,
+      mcc: "",
+      mnc: "",
+      msisdn: "",
+      operator: ""
+    }
+  },
+  voice: {
+    network: {
+      shortName: ""
+    },
+    roaming: undefined
+  },
+  data: {
+    network: {
+      shortName: ""
+    }
+  }
+};
+
+const CERTIFICATE = "eyJhbGciOiJEUzI1NiJ9.eyJsYXN0QXV0aEF0IjoxNDA0NDY5NzkyODc3LCJ2ZXJpZmllZE1TSVNETiI6IiszMTYxNzgxNTc1OCIsInB1YmxpYy1rZXkiOnsiYWxnb3JpdGhtIjoiRFMiLCJ5IjoiNGE5YzkzNDY3MWZhNzQ3YmM2ZjMyNjE0YTg1MzUyZjY5NDcwMDdhNTRkMDAxMDY4OWU5ZjJjZjc0ZGUwYTEwZTRlYjlmNDk1ZGFmZTA0NGVjZmVlNDlkN2YwOGU4ODQyMDJiOTE5OGRhNWZhZWE5MGUzZjRmNzE1YzZjNGY4Yjc3MGYxZTU4YWZhNDM0NzVhYmFiN2VlZGE1MmUyNjk2YzFmNTljNzMzYjFlYzBhNGNkOTM1YWIxYzkyNzAxYjNiYTA5ZDRhM2E2MzNjNTJmZjE2NGYxMWY3OTg1YzlmZjY3ZThmZDFlYzA2NDU3MTdkMjBiNDE4YmM5M2YzYzVkNCIsInAiOiJmZjYwMDQ4M2RiNmFiZmM1YjQ1ZWFiNzg1OTRiMzUzM2Q1NTBkOWYxYmYyYTk5MmE3YThkYWE2ZGMzNGY4MDQ1YWQ0ZTZlMGM0MjlkMzM0ZWVlYWFlZmQ3ZTIzZDQ4MTBiZTAwZTRjYzE0OTJjYmEzMjViYTgxZmYyZDVhNWIzMDVhOGQxN2ViM2JmNGEwNmEzNDlkMzkyZTAwZDMyOTc0NGE1MTc5MzgwMzQ0ZTgyYTE4YzQ3OTMzNDM4Zjg5MWUyMmFlZWY4MTJkNjljOGY3NWUzMjZjYjcwZWEwMDBjM2Y3NzZkZmRiZDYwNDYzOGMyZWY3MTdmYzI2ZDAyZTE3IiwicSI6ImUyMWUwNGY5MTFkMWVkNzk5MTAwOGVjYWFiM2JmNzc1OTg0MzA5YzMiLCJnIjoiYzUyYTRhMGZmM2I3ZTYxZmRmMTg2N2NlODQxMzgzNjlhNjE1NGY0YWZhOTI5NjZlM2M4MjdlMjVjZmE2Y2Y1MDhiOTBlNWRlNDE5ZTEzMzdlMDdhMmU5ZTJhM2NkNWRlYTcwNGQxNzVmOGViZjZhZjM5N2Q2OWUxMTBiOTZhZmIxN2M3YTAzMjU5MzI5ZTQ4MjliMGQwM2JiYzc4OTZiMTViNGFkZTUzZTEzMDg1OGNjMzRkOTYyNjlhYTg5MDQxZjQwOTEzNmM3MjQyYTM4ODk1YzlkNWJjY2FkNGYzODlhZjFkN2E0YmQxMzk4YmQwNzJkZmZhODk2MjMzMzk3YSJ9LCJwcmluY2lwYWwiOiIwMzgxOTgyYS0xZTgzLTI1NjYtNjgzZS05MDRmNDA0NGM1MGRAbXNpc2RuLWRldi5zdGFnZS5tb3phd3MubmV0IiwiaWF0IjoxNDA0NDY5NzgyODc3LCJleHAiOjE0MDQ0OTEzOTI4NzcsImlzcyI6Im1zaXNkbi1kZXYuc3RhZ2UubW96YXdzLm5ldCJ9."
+
+// === Helpers ===
+
+function addPermission(aAction) {
+  let uri = Cc["@mozilla.org/network/io-service;1"]
+              .getService(Ci.nsIIOService)
+              .newURI(ORIGIN, null, null);
+  let _principal = Cc["@mozilla.org/scriptsecuritymanager;1"]
+                     .getService(Ci.nsIScriptSecurityManager)
+                     .getAppCodebasePrincipal(uri, APP_ID, false);
+  let pm = Cc["@mozilla.org/permissionmanager;1"]
+             .getService(Ci.nsIPermissionManager);
+  pm.addFromPrincipal(_principal, MOBILEID_PERM, aAction);
+}
+
+function removePermission() {
+  let uri = Cc["@mozilla.org/network/io-service;1"]
+              .getService(Ci.nsIIOService)
+              .newURI(ORIGIN, null, null);
+  let _principal = Cc["@mozilla.org/scriptsecuritymanager;1"]
+                     .getService(Ci.nsIScriptSecurityManager)
+                     .getAppCodebasePrincipal(uri, APP_ID, false);
+  let pm = Cc["@mozilla.org/permissionmanager;1"]
+             .getService(Ci.nsIPermissionManager);
+  pm.removeFromPrincipal(_principal, MOBILEID_PERM);
+}
+
+// === Mocks ===
+
+let Mock = function(aOptions) {
+  if (!aOptions) {
+    aOptions = {};
+  }
+  this._options = aOptions;
+  this._spied = {};
+};
+
+Mock.prototype = {
+  _: function(aMethod) {
+    DEBUG && do_print("_ " + aMethod + JSON.stringify(this._spied));
+    let self = this;
+    return {
+      callsLength: function(aNumberOfCalls) {
+        if (aNumberOfCalls == 0) {
+          do_check_eq(self._spied[aMethod], undefined);
+          return;
+        }
+        do_check_eq(self._spied[aMethod].length, aNumberOfCalls);
+      },
+      call: function(aCallNumber) {
+        return {
+          arg: function(aArgNumber, aValue) {
+            let _arg = self._spied[aMethod][aCallNumber - 1][aArgNumber - 1];
+            if (Array.isArray(aValue)) {
+              do_check_eq(_arg.length, aValue.length)
+              for (let i = 0; i < _arg.length; i++) {
+                do_check_eq(_arg[i], aValue[i]);
+              }
+              return;
+            }
+
+            if (typeof aValue === 'object') {
+              do_check_eq(JSON.stringify(_arg), JSON.stringify(aValue));
+              return;
+            }
+
+            do_check_eq(_arg, aValue);
+          }
+        }
+      }
+    }
+  },
+
+  _spy: function(aMethod, aArgs) {
+    DEBUG && do_print(aMethod + " - " + JSON.stringify(aArgs));
+    if (!this._spied[aMethod]) {
+      this._spied[aMethod] = [];
+    }
+    this._spied[aMethod].push(aArgs);
+  },
+
+  getSpiedCalls: function(aMethod) {
+    return this._spied[aMethod];
+  }
+};
+
+// UI Glue mock up.
+let MockUi = function(aOptions) {
+  Mock.call(this, aOptions);
+};
+
+MockUi.prototype = {
+  __proto__: Mock.prototype,
+
+  _startFlowResult: {
+    phoneNumber: PHONE_NUMBER,
+    mcc: MNC
+  },
+
+  _verifyCodePromptResult: {
+    verificationCode: VERIFICATION_CODE
+  },
+
+  startFlow: function() {
+    this._spy("startFlow", arguments);
+    return Promise.resolve(this._options.startFlowResult ||
+                           this._startFlowResult);
+  },
+
+  verificationCodePrompt: function() {
+    this._spy("verifyCodePrompt", arguments);
+    return Promise.resolve(this._options.verificationCodePromptResult ||
+                           this._verifyCodePromptResult);
+  },
+
+  verify: function() {
+    this._spy("verify", arguments);
+  },
+
+  error: function() {
+    this._spy("error", arguments);
+  },
+
+  verified: function() {
+    this._spy("verified", arguments);
+  },
+
+  set oncancel(aCallback) {
+  },
+
+  set onresendcode(aCallback) {
+  }
+};
+
+// Credentials store mock up.
+let MockCredStore = function(aOptions) {
+  Mock.call(this, aOptions);
+};
+
+MockCredStore.prototype = {
+  __proto__: Mock.prototype,
+
+  _getByOriginResult: null,
+
+  _getByMsisdnResult: null,
+
+  _getByIccIdResult: null,
+
+  getByOrigin: function() {
+    this._spy("getByOrigin", arguments);
+    let result = this._getByOriginResult;
+    if (this._options.getByOriginResult) {
+      if (Array.isArray(this._options.getByOriginResult)) {
+        result = this._options.getByOriginResult.length ?
+                 this._options.getByOriginResult.shift() : null;
+      } else {
+        result = this._options.getByOriginResult;
+      }
+    }
+    return Promise.resolve(result);
+  },
+
+  getByMsisdn: function() {
+    this._spy("getByMsisdn", arguments);
+    return Promise.resolve(this._options.getByMsisdnResult ||
+                           this._getByMsisdnResult);
+  },
+
+  getByIccId: function() {
+    this._spy("getByIccId", arguments);
+    return Promise.resolve(this._options.getByIccIdResult ||
+                           this._getByIccIdResult);
+  },
+
+  add: function() {
+    this._spy("add", arguments);
+    return Promise.resolve();
+  },
+
+  setDeviceIccIds: function() {
+    this._spy("setDeviceIccIds", arguments);
+    return Promise.resolve();
+  },
+
+  removeOrigin: function() {
+    this._spy("removeOrigin", arguments);
+    return Promise.resolve();
+  },
+
+  delete: function() {
+    this._spy("delete", arguments);
+    return Promise.resolve();
+  }
+};
+
+// Client mock up.
+let MockClient = function(aOptions) {
+  Mock.call(this, aOptions);
+};
+
+MockClient.prototype = {
+
+  __proto__: Mock.prototype,
+
+  _discoverResult: {
+    verificationMethods: ["sms/mt"],
+    verificationDetails: {
+      "sms/mt": {
+        mtSender: "123",
+        url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify"
+      }
+    }
+  },
+
+  _registerResult: {
+    msisdnSessionToken: SESSION_TOKEN
+  },
+
+  _smsMtVerifyResult: {},
+
+  _verifyCodeResult: {
+    msisdn: PHONE_NUMBER
+  },
+
+  _signResult: {
+    cert: CERTIFICATE
+  },
+
+  hawk: {
+    now: function() {
+      return Date.now();
+    }
+  },
+
+  discover: function() {
+    this._spy("discover", arguments);
+    return Promise.resolve(this._options.discoverResult ||
+                           this._discoverResult);
+  },
+
+  register: function() {
+    this._spy("register", arguments);
+    return Promise.resolve(this._options.registerResult ||
+                           this._registerResult);
+  },
+
+  smsMtVerify: function() {
+    this._spy("smsMtVerify", arguments);
+    return Promise.resolve(this._options.smsMtVerifyResult ||
+                           this._smsMtVerifyResult);
+  },
+
+  verifyCode: function() {
+    this._spy("verifyCode", arguments);
+    if (this._options.verifyCodeError) {
+      let error = Array.isArray(this._options.verifyCodeError) ?
+                  this._options.verifyCodeError.shift() :
+                  this._options.verifyCodeError;
+      if (!this._options.verifyCodeError.length) {
+        this._options.verifyCodeError = null;
+      }
+      return Promise.reject(error);
+    }
+    return Promise.resolve(this._options.verifyCodeResult ||
+                           this._verifyCodeResult);
+  },
+
+  sign: function() {
+    this._spy("sign", arguments);
+    if (this._options.signError) {
+      let error = Array.isArray(this._options.signError) ?
+                  this._options.signError.shift() :
+                  this._options.signError;
+      return Promise.reject(error);
+    }
+    return Promise.resolve(this._options.signResult || this._signResult);
+  }
+};
+
+// Override MobileIdentityUIGlue.
+const kMobileIdentityUIGlueUUID = "{05df0566-ca8a-4ec7-bc76-78626ebfbe9a}";
+const kMobileIdentityUIGlueContractID =
+  "@mozilla.org/services/mobileid-ui-glue;1";
+
+// Save original factory.
+/*const kMobileIdentityUIGlueFactory =
+  Cm.getClassObject(Cc[kMobileIdentityUIGlueContractID], Ci.nsIFactory);*/
+
+let fakeMobileIdentityUIGlueFactory = {
+  createInstance: function(aOuter, aIid) {
+    return MobileIdentityUIGlue.QueryInterface(aIid);
+  }
+};
+
+// MobileIdentityUIGlue fake component.
+let MobileIdentityUIGlue = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileIdentityUIGlue]),
+
+};
+
+(function registerFakeMobileIdentityUIGlue() {
+  Cm.QueryInterface(Ci.nsIComponentRegistrar)
+    .registerFactory(Components.ID(kMobileIdentityUIGlueUUID),
+                     "MobileIdentityUIGlue",
+                     kMobileIdentityUIGlueContractID,
+                     fakeMobileIdentityUIGlueFactory);
+})();
+
+// The tests rely on having an app registered. Otherwise, it will throw.
+// Override XULAppInfo.
+const XUL_APP_INFO_UUID = Components.ID("{84fdc459-d96d-421c-9bff-a8193233ae75}");
+const XUL_APP_INFO_CONTRACT_ID = "@mozilla.org/xre/app-info;1";
+
+let (XULAppInfo = {
+  vendor: "Mozilla",
+  name: "MobileIdTest",
+  ID: "{230de50e-4cd1-11dc-8314-0800200b9a66}",
+  version: "1",
+  appBuildID: "2007010101",
+  platformVersion: "",
+  platformBuildID: "2007010101",
+  inSafeMode: false,
+  logConsoleErrors: true,
+  OS: "XPCShell",
+  XPCOMABI: "noarch-spidermonkey",
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIXULAppInfo,
+    Ci.nsIXULRuntime,
+  ])
+}) {
+  let XULAppInfoFactory = {
+    createInstance: function (outer, iid) {
+      if (outer != null) {
+        throw Cr.NS_ERROR_NO_AGGREGATION;
+      }
+      return XULAppInfo.QueryInterface(iid);
+    }
+  };
+  Cm.QueryInterface(Ci.nsIComponentRegistrar)
+    .registerFactory(XUL_APP_INFO_UUID,
+                     "XULAppInfo",
+                     XUL_APP_INFO_CONTRACT_ID,
+                     XULAppInfoFactory);
+}
--- a/services/mobileid/tests/xpcshell/test_mobileid_manager.js
+++ b/services/mobileid/tests/xpcshell/test_mobileid_manager.js
@@ -1,442 +1,33 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const Cm = Components.manager;
-
 Cu.import("resource://gre/modules/Promise.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/MobileIdentityManager.jsm");
 Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
 
-const DEBUG = false;
-
-const GET_ASSERTION_IPC_MSG = "MobileId:GetAssertion";
-const GET_ASSERTION_RETURN_OK = "MobileId:GetAssertion:Return:OK";
-const GET_ASSERTION_RETURN_KO = "MobileId:GetAssertion:Return:KO";
-
-// === Globals ===
-
-const ORIGIN = "app://afakeorigin";
-const APP_ID = 1;
-const PRINCIPAL = {
-  origin: ORIGIN,
-  appId: APP_ID
-};
-const PHONE_NUMBER = "+34666555444";
-const ANOTHER_PHONE_NUMBER = "+44123123123";
-const VERIFICATION_CODE = "123456";
-const SESSION_TOKEN = "aSessionToken";
-const ICC_ID = "aIccId";
-const ANOTHER_ICC_ID = "anotherIccId";
-const MNC = "aMnc";
-const ANOTHER_MNC = "anotherMnc";
-const MCC = "aMcc";
-const ANOTHER_MCC = "anotherMcc";
-const OPERATOR = "aOperator";
-const ANOTHER_OPERATOR = "anotherOperator";
-const RADIO_INTERFACE = {
-  rilContext: {
-    iccInfo: {
-      iccid: ICC_ID,
-      mcc: MCC,
-      mnc: MNC,
-      msisdn: PHONE_NUMBER,
-      operator: OPERATOR
-    }
-  },
-  voice: {
-    network: {
-      shortName: OPERATOR
-    },
-    roaming: false
-  },
-  data: {
-    network: {
-      shortName: OPERATOR
-    }
-  }
-};
-const ANOTHER_RADIO_INTERFACE = {
-  rilContext: {
-    iccInfo: {
-      iccid: ANOTHER_ICC_ID,
-      mcc: ANOTHER_MCC,
-      mnc: ANOTHER_MNC,
-      msisdn: ANOTHER_PHONE_NUMBER,
-      operator: ANOTHER_OPERATOR
-    }
-  },
-  voice: {
-    network: {
-      shortName: ANOTHER_OPERATOR
-    },
-    roaming: false
-  },
-  data: {
-    network: {
-      shortName: ANOTHER_OPERATOR
-    }
-  }
-};
-
-const INVALID_RADIO_INTERFACE = {
-  rilContext: {
-    iccInfo: {
-      iccid: null,
-      mcc: "",
-      mnc: "",
-      msisdn: "",
-      operator: ""
-    }
-  },
-  voice: {
-    network: {
-      shortName: ""
-    },
-    roaming: undefined
-  },
-  data: {
-    network: {
-      shortName: ""
-    }
-  }
-};
-
-const CERTIFICATE = "eyJhbGciOiJEUzI1NiJ9.eyJsYXN0QXV0aEF0IjoxNDA0NDY5NzkyODc3LCJ2ZXJpZmllZE1TSVNETiI6IiszMTYxNzgxNTc1OCIsInB1YmxpYy1rZXkiOnsiYWxnb3JpdGhtIjoiRFMiLCJ5IjoiNGE5YzkzNDY3MWZhNzQ3YmM2ZjMyNjE0YTg1MzUyZjY5NDcwMDdhNTRkMDAxMDY4OWU5ZjJjZjc0ZGUwYTEwZTRlYjlmNDk1ZGFmZTA0NGVjZmVlNDlkN2YwOGU4ODQyMDJiOTE5OGRhNWZhZWE5MGUzZjRmNzE1YzZjNGY4Yjc3MGYxZTU4YWZhNDM0NzVhYmFiN2VlZGE1MmUyNjk2YzFmNTljNzMzYjFlYzBhNGNkOTM1YWIxYzkyNzAxYjNiYTA5ZDRhM2E2MzNjNTJmZjE2NGYxMWY3OTg1YzlmZjY3ZThmZDFlYzA2NDU3MTdkMjBiNDE4YmM5M2YzYzVkNCIsInAiOiJmZjYwMDQ4M2RiNmFiZmM1YjQ1ZWFiNzg1OTRiMzUzM2Q1NTBkOWYxYmYyYTk5MmE3YThkYWE2ZGMzNGY4MDQ1YWQ0ZTZlMGM0MjlkMzM0ZWVlYWFlZmQ3ZTIzZDQ4MTBiZTAwZTRjYzE0OTJjYmEzMjViYTgxZmYyZDVhNWIzMDVhOGQxN2ViM2JmNGEwNmEzNDlkMzkyZTAwZDMyOTc0NGE1MTc5MzgwMzQ0ZTgyYTE4YzQ3OTMzNDM4Zjg5MWUyMmFlZWY4MTJkNjljOGY3NWUzMjZjYjcwZWEwMDBjM2Y3NzZkZmRiZDYwNDYzOGMyZWY3MTdmYzI2ZDAyZTE3IiwicSI6ImUyMWUwNGY5MTFkMWVkNzk5MTAwOGVjYWFiM2JmNzc1OTg0MzA5YzMiLCJnIjoiYzUyYTRhMGZmM2I3ZTYxZmRmMTg2N2NlODQxMzgzNjlhNjE1NGY0YWZhOTI5NjZlM2M4MjdlMjVjZmE2Y2Y1MDhiOTBlNWRlNDE5ZTEzMzdlMDdhMmU5ZTJhM2NkNWRlYTcwNGQxNzVmOGViZjZhZjM5N2Q2OWUxMTBiOTZhZmIxN2M3YTAzMjU5MzI5ZTQ4MjliMGQwM2JiYzc4OTZiMTViNGFkZTUzZTEzMDg1OGNjMzRkOTYyNjlhYTg5MDQxZjQwOTEzNmM3MjQyYTM4ODk1YzlkNWJjY2FkNGYzODlhZjFkN2E0YmQxMzk4YmQwNzJkZmZhODk2MjMzMzk3YSJ9LCJwcmluY2lwYWwiOiIwMzgxOTgyYS0xZTgzLTI1NjYtNjgzZS05MDRmNDA0NGM1MGRAbXNpc2RuLWRldi5zdGFnZS5tb3phd3MubmV0IiwiaWF0IjoxNDA0NDY5NzgyODc3LCJleHAiOjE0MDQ0OTEzOTI4NzcsImlzcyI6Im1zaXNkbi1kZXYuc3RhZ2UubW96YXdzLm5ldCJ9."
-
-// === Helpers ===
-
-function addPermission(aAction) {
-  let uri = Cc["@mozilla.org/network/io-service;1"]
-              .getService(Ci.nsIIOService)
-              .newURI(ORIGIN, null, null);
-  let _principal = Cc["@mozilla.org/scriptsecuritymanager;1"]
-                     .getService(Ci.nsIScriptSecurityManager)
-                     .getAppCodebasePrincipal(uri, APP_ID, false);
-  let pm = Cc["@mozilla.org/permissionmanager;1"]
-             .getService(Ci.nsIPermissionManager);
-  pm.addFromPrincipal(_principal, MOBILEID_PERM, aAction);
-}
-
-function removePermission() {
-  let uri = Cc["@mozilla.org/network/io-service;1"]
-              .getService(Ci.nsIIOService)
-              .newURI(ORIGIN, null, null);
-  let _principal = Cc["@mozilla.org/scriptsecuritymanager;1"]
-                     .getService(Ci.nsIScriptSecurityManager)
-                     .getAppCodebasePrincipal(uri, APP_ID, false);
-  let pm = Cc["@mozilla.org/permissionmanager;1"]
-             .getService(Ci.nsIPermissionManager);
-  pm.removeFromPrincipal(_principal, MOBILEID_PERM);
-}
-
-// === Mocks ===
-
-let Mock = function(aOptions) {
-  if (!aOptions) {
-    aOptions = {};
-  }
-  this._options = aOptions;
-  this._spied = {};
-};
-
-Mock.prototype = {
-  _: function(aMethod) {
-    DEBUG && do_print("_ " + aMethod + JSON.stringify(this._spied));
-    let self = this;
-    return {
-      callsLength: function(aNumberOfCalls) {
-        if (aNumberOfCalls == 0) {
-          do_check_eq(self._spied[aMethod], undefined);
-          return;
-        }
-        do_check_eq(self._spied[aMethod].length, aNumberOfCalls);
-      },
-      call: function(aCallNumber) {
-        return {
-          arg: function(aArgNumber, aValue) {
-            let _arg = self._spied[aMethod][aCallNumber - 1][aArgNumber - 1];
-            if (Array.isArray(aValue)) {
-              do_check_eq(_arg.length, aValue.length)
-              for (let i = 0; i < _arg.length; i++) {
-                do_check_eq(_arg[i], aValue[i]);
-              }
-              return;
-            }
-
-            if (typeof aValue === 'object') {
-              do_check_eq(JSON.stringify(_arg), JSON.stringify(aValue));
-              return;
-            }
-
-            do_check_eq(_arg, aValue);
-          }
-        }
-      }
-    }
-  },
-
-  _spy: function(aMethod, aArgs) {
-    DEBUG && do_print(aMethod + " - " + JSON.stringify(aArgs));
-    if (!this._spied[aMethod]) {
-      this._spied[aMethod] = [];
-    }
-    this._spied[aMethod].push(aArgs);
-  },
-
-  getSpiedCalls: function(aMethod) {
-    return this._spied[aMethod];
-  }
-};
-
-// UI Glue mock up.
-let MockUi = function(aOptions) {
-  Mock.call(this, aOptions);
-};
-
-MockUi.prototype = {
-  __proto__: Mock.prototype,
-
-  _startFlowResult: {
-    phoneNumber: PHONE_NUMBER,
-    mcc: MNC
-  },
-
-  _verifyCodePromptResult: {
-    verificationCode: VERIFICATION_CODE
-  },
-
-  startFlow: function() {
-    this._spy("startFlow", arguments);
-    return Promise.resolve(this._options.startFlowResult ||
-                           this._startFlowResult);
-  },
-
-  verificationCodePrompt: function() {
-    this._spy("verifyCodePrompt", arguments);
-    return Promise.resolve(this._options.verificationCodePromptResult ||
-                           this._verifyCodePromptResult);
-  },
-
-  verify: function() {
-    this._spy("verify", arguments);
-  },
-
-  error: function() {
-    this._spy("error", arguments);
-  },
-
-  verified: function() {
-    this._spy("verified", arguments);
-  },
-
-  set oncancel(aCallback) {
-  },
-
-  set onresendcode(aCallback) {
-  }
-};
-
 // Save original credential store instance.
 const kMobileIdentityCredStore = MobileIdentityManager.credStore;
-
-// Credentials store mock up.
-let MockCredStore = function(aOptions) {
-  Mock.call(this, aOptions);
-};
-
-MockCredStore.prototype = {
-  __proto__: Mock.prototype,
-
-  _getByOriginResult: null,
-
-  _getByMsisdnResult: null,
-
-  _getByIccIdResult: null,
-
-  getByOrigin: function() {
-    this._spy("getByOrigin", arguments);
-    let result = this._getByOriginResult;
-    if (this._options.getByOriginResult) {
-      if (Array.isArray(this._options.getByOriginResult)) {
-        result = this._options.getByOriginResult.length ?
-                 this._options.getByOriginResult.shift() : null;
-      } else {
-        result = this._options.getByOriginResult;
-      }
-    }
-    return Promise.resolve(result);
-  },
-
-  getByMsisdn: function() {
-    this._spy("getByMsisdn", arguments);
-    return Promise.resolve(this._options.getByMsisdnResult ||
-                           this._getByMsisdnResult);
-  },
-
-  getByIccId: function() {
-    this._spy("getByIccId", arguments);
-    return Promise.resolve(this._options.getByIccIdResult ||
-                           this._getByIccIdResult);
-  },
-
-  add: function() {
-    this._spy("add", arguments);
-    return Promise.resolve();
-  },
-
-  setDeviceIccIds: function() {
-    this._spy("setDeviceIccIds", arguments);
-    return Promise.resolve();
-  },
-
-  removeOrigin: function() {
-    this._spy("removeOrigin", arguments);
-    return Promise.resolve();
-  },
-
-  delete: function() {
-    this._spy("delete", arguments);
-    return Promise.resolve();
-  }
-};
-
 // Save original client instance.
 const kMobileIdentityClient = MobileIdentityManager.client;
 
-// Client mock up.
-let MockClient = function(aOptions) {
-  Mock.call(this, aOptions);
-};
-
-MockClient.prototype = {
-
-  __proto__: Mock.prototype,
-
-  _discoverResult: {
-    verificationMethods: ["sms/mt"],
-    verificationDetails: {
-      "sms/mt": {
-        mtSender: "123",
-        url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify"
-      }
-    }
-  },
-
-  _registerResult: {
-    msisdnSessionToken: SESSION_TOKEN
-  },
-
-  _smsMtVerifyResult: {},
-
-  _verifyCodeResult: {
-    msisdn: PHONE_NUMBER
-  },
-
-  _signResult: {
-    cert: CERTIFICATE
-  },
-
-  hawk: {
-    now: function() {
-      return Date.now();
-    }
-  },
-
-  discover: function() {
-    this._spy("discover", arguments);
-    return Promise.resolve(this._options.discoverResult ||
-                           this._discoverResult);
-  },
-
-  register: function() {
-    this._spy("register", arguments);
-    return Promise.resolve(this._options.registerResult ||
-                           this._registerResult);
-  },
-
-  smsMtVerify: function() {
-    this._spy("smsMtVerify", arguments);
-    return Promise.resolve(this._options.smsMtVerifyResult ||
-                           this._smsMtVerifyResult);
-  },
-
-  verifyCode: function() {
-    this._spy("verifyCode", arguments);
-    return Promise.resolve(this._options.verifyCodeResult ||
-                           this._verifyCodeResult);
-  },
-
-  sign: function() {
-    this._spy("sign", arguments);
-    if (this._options.signError) {
-      let error = Array.isArray(this._options.signError) ?
-                  this._options.signError.shift() :
-                  this._options.signError;
-      return Promise.reject(error);
-    }
-    return Promise.resolve(this._options.signResult || this._signResult);
-  }
-};
-
-// The test rely on having an app registered. Otherwise, it will throw.
-// Override XULAppInfo.
-const XUL_APP_INFO_UUID = Components.ID("{84fdc459-d96d-421c-9bff-a8193233ae75}");
-const XUL_APP_INFO_CONTRACT_ID = "@mozilla.org/xre/app-info;1";
-
-let (XULAppInfo = {
-  vendor: "Mozilla",
-  name: "MobileIdTest",
-  ID: "{230de50e-4cd1-11dc-8314-0800200b9a66}",
-  version: "1",
-  appBuildID: "2007010101",
-  platformVersion: "",
-  platformBuildID: "2007010101",
-  inSafeMode: false,
-  logConsoleErrors: true,
-  OS: "XPCShell",
-  XPCOMABI: "noarch-spidermonkey",
-
-  QueryInterface: XPCOMUtils.generateQI([
-    Ci.nsIXULAppInfo,
-    Ci.nsIXULRuntime,
-  ])
-}) {
-  let XULAppInfoFactory = {
-    createInstance: function (outer, iid) {
-      if (outer != null) {
-        throw Cr.NS_ERROR_NO_AGGREGATION;
-      }
-      return XULAppInfo.QueryInterface(iid);
-    }
-  };
-  Cm.QueryInterface(Ci.nsIComponentRegistrar)
-    .registerFactory(XUL_APP_INFO_UUID,
-                     "XULAppInfo",
-                     XUL_APP_INFO_CONTRACT_ID,
-                     XULAppInfoFactory);
-}
-
 // === Global cleanup ===
-
 function cleanup() {
   MobileIdentityManager.credStore = kMobileIdentityCredStore;
   MobileIdentityManager.client = kMobileIdentityClient;
   MobileIdentityManager.ui = null;
   MobileIdentityManager._iccInfo = [];
   removePermission(ORIGIN);
 }
 
 // Unregister mocks and restore original code.
 do_register_cleanup(cleanup);
-
 // === Tests ===
 function run_test() {
   run_next_test();
 }
 
 add_test(function() {
   do_print("= Initial state =");
   do_check_neq(MobileIdentityManager, undefined);
new file mode 100644
--- /dev/null
+++ b/services/mobileid/tests/xpcshell/test_mobileid_verification_flow.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/MobileIdentityVerificationFlow.jsm");
+
+function verifyStrategy() {
+  return Promise.resolve();
+}
+
+function cleanupStrategy() {
+}
+
+function run_test() {
+  do_print("= Bug 1101444: Invalid verification code shouldn't restart " +
+           "verification flow =");
+
+  let client = new MockClient({
+    // This will emulate two invalid attempts. The third time it will work.
+    verifyCodeError: ["INVALID", "INVALID"]
+  });
+  let ui = new MockUi();
+
+  let verificationFlow = new MobileIdentityVerificationFlow({
+    external: true,
+    sessionToken: SESSION_TOKEN,
+    msisdn: PHONE_NUMBER
+  }, ui, client, verifyStrategy, cleanupStrategy);
+
+  verificationFlow.doVerification().then(() => {
+    // We should only do the registration process once. We only try registering
+    // again when the timeout fires, but not when we enter an invalid
+    // verification code.
+    client._("register").callsLength(1);
+    client._("verifyCode").callsLength(3);
+    // Because we do two invalid attempts, we should show the invalid code error twice.
+    ui._("error").callsLength(2);
+  });
+
+  do_test_finished();
+};
--- a/services/mobileid/tests/xpcshell/xpcshell.ini
+++ b/services/mobileid/tests/xpcshell/xpcshell.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 head = head.js
 tail =
 
+[test_mobileid_client.js]
 [test_mobileid_manager.js]
-[test_mobileid_client.js]
+[test_mobileid_verification_flow.js]