Bug 1069816: add unit tests for the GoogleImporter class. r=abr
authorMike de Boer <mdeboer@mozilla.com>
Thu, 02 Oct 2014 12:37:41 +0200
changeset 225565 4ba581107e6f84c822fcdc7e5f4f7337f1eb71f7
parent 225564 c17712a1a6537483348c9209e26f32aabf5bb843
child 225566 e15661e2c496ef3497db2e1d018fa260851a8e05
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersabr
bugs1069816
milestone34.0a2
Bug 1069816: add unit tests for the GoogleImporter class. r=abr
browser/components/loop/test/mochitest/browser.ini
browser/components/loop/test/mochitest/browser_CardDavImporter.js
browser/components/loop/test/mochitest/browser_GoogleImporter.js
browser/components/loop/test/mochitest/fixtures/google_auth.txt
browser/components/loop/test/mochitest/fixtures/google_contacts.txt
browser/components/loop/test/mochitest/fixtures/google_token.txt
browser/components/loop/test/mochitest/google_service.sjs
browser/components/loop/test/mochitest/head.js
--- a/browser/components/loop/test/mochitest/browser.ini
+++ b/browser/components/loop/test/mochitest/browser.ini
@@ -1,16 +1,21 @@
 [DEFAULT]
 support-files =
+    fixtures/google_auth.txt
+    fixtures/google_contacts.txt
+    fixtures/google_token.txt
+    google_service.sjs
     head.js
     loop_fxa.sjs
     ../../../../base/content/test/general/browser_fxa_oauth.html
 
 [browser_CardDavImporter.js]
 [browser_fxa_login.js]
+[browser_GoogleImporter.js]
 +skip-if = e10s
 [browser_loop_fxa_server.js]
 [browser_LoopContacts.js]
 [browser_mozLoop_appVersionInfo.js]
 [browser_mozLoop_prefs.js]
 [browser_mozLoop_doNotDisturb.js]
 [browser_mozLoop_softStart.js]
 skip-if = buildapp == 'mulet'
--- a/browser/components/loop/test/mochitest/browser_CardDavImporter.js
+++ b/browser/components/loop/test/mochitest/browser_CardDavImporter.js
@@ -1,53 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const {CardDavImporter} = Cu.import("resource:///modules/loop/CardDavImporter.jsm", {});
 
-const mockDb = {
-  _store: { },
-  _next_guid: 1,
-
-  add: function(details, callback) {
-    if (!("id" in details)) {
-      callback(new Error("No 'id' field present"));
-      return;
-    }
-    details._guid = this._next_guid++;
-    this._store[details._guid] = details;
-    callback(null, details);
-  },
-  remove: function(guid, callback) {
-    if (!guid in this._store) {
-      callback(new Error("Could not find _guid '" + guid + "' in database"));
-      return;
-    }
-    delete this._store[guid];
-    callback(null);
-  },
-  get: function(guid, callback) {
-    callback(null, this._store[guid]);
-  },
-  getByServiceId: function(serviceId, callback) {
-    for (let guid in this._store) {
-      if (serviceId === this._store[guid].id) {
-        callback(null, this._store[guid]);
-        return;
-      }
-    }
-    callback(null, null);
-  },
-  removeAll: function(callback) {
-    this._store = {};
-    this._next_guid = 1;
-    callback(null);
-  }
-};
-
 const kAuth = {
   "method": "basic",
   "user": "username",
   "password": "p455w0rd"
 }
 
 
 // "pid" for "provider ID"
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/browser_GoogleImporter.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {GoogleImporter} = Cu.import("resource:///modules/loop/GoogleImporter.jsm", {});
+
+let importer = new GoogleImporter();
+
+function promiseImport() {
+  return new Promise(function(resolve, reject) {
+    importer.startImport({}, function(err, stats) {
+      if (err) {
+        reject(err);
+      } else {
+        resolve(stats);
+      }
+    }, mockDb, window);
+  });
+}
+
+add_task(function* test_GoogleImport() {
+  let stats;
+  // An error may throw and the test will fail when that happens.
+  stats = yield promiseImport();
+
+  // Assert the world.
+  Assert.equal(stats.total, 5, "Five contacts should get processed");
+  Assert.equal(stats.success, 5, "Five contacts should be imported");
+
+  yield promiseImport();
+  Assert.equal(Object.keys(mockDb._store).length, 5, "Database should contain only five contact after reimport");
+
+  let c = mockDb._store[mockDb._next_guid - 5];
+  Assert.equal(c.name[0], "John Smith", "Full name should match");
+  Assert.equal(c.givenName[0], "John", "Given name should match");
+  Assert.equal(c.familyName[0], "Smith", "Family name should match");
+  Assert.equal(c.email[0].type, "other", "Email type should match");
+  Assert.equal(c.email[0].value, "john.smith@example.com", "Email should match");
+  Assert.equal(c.email[0].pref, true, "Pref should match");
+  Assert.equal(c.category[0], "google", "Category should match");
+  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/0", "UID should match and be scoped to provider");
+
+  c = mockDb._store[mockDb._next_guid - 4];
+  Assert.equal(c.name[0], "Jane Smith", "Full name should match");
+  Assert.equal(c.givenName[0], "Jane", "Given name should match");
+  Assert.equal(c.familyName[0], "Smith", "Family name should match");
+  Assert.equal(c.email[0].type, "other", "Email type should match");
+  Assert.equal(c.email[0].value, "jane.smith@example.com", "Email should match");
+  Assert.equal(c.email[0].pref, true, "Pref should match");
+  Assert.equal(c.category[0], "google", "Category should match");
+  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/1", "UID should match and be scoped to provider");
+
+  c = mockDb._store[mockDb._next_guid - 3];
+  Assert.equal(c.name[0], "Davy Randall Jones", "Full name should match");
+  Assert.equal(c.givenName[0], "Davy Randall", "Given name should match");
+  Assert.equal(c.familyName[0], "Jones", "Family name should match");
+  Assert.equal(c.email[0].type, "other", "Email type should match");
+  Assert.equal(c.email[0].value, "davy.jones@example.com", "Email should match");
+  Assert.equal(c.email[0].pref, true, "Pref should match");
+  Assert.equal(c.category[0], "google", "Category should match");
+  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/2", "UID should match and be scoped to provider");
+
+  c = mockDb._store[mockDb._next_guid - 2];
+  Assert.equal(c.name[0], "noname@example.com", "Full name should match");
+  Assert.equal(c.email[0].type, "other", "Email type should match");
+  Assert.equal(c.email[0].value, "noname@example.com", "Email should match");
+  Assert.equal(c.email[0].pref, true, "Pref should match");
+  Assert.equal(c.category[0], "google", "Category should match");
+  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/3", "UID should match and be scoped to provider");
+
+  c = mockDb._store[mockDb._next_guid - 1];
+  Assert.equal(c.name[0], "lycnix", "Full name should match");
+  Assert.equal(c.email[0].type, "other", "Email type should match");
+  Assert.equal(c.email[0].value, "lycnix", "Email should match");
+  Assert.equal(c.email[0].pref, true, "Pref should match");
+  Assert.equal(c.category[0], "google", "Category should match");
+  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/7", "UID should match and be scoped to provider");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/fixtures/google_auth.txt
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+<head><title>Success code=test-code</title></head>
+<body>Le Code.</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/fixtures/google_contacts.txt
@@ -0,0 +1,94 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<feed gd:etag="W/&quot;DUQNRHc8cCt7I2A9XRdSF04.&quot;" xmlns="http://www.w3.org/2005/Atom" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gContact="http://schemas.google.com/contact/2008" xmlns:gd="http://schemas.google.com/g/2005" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/">
+  <id>tester@mochi.com</id>
+  <updated>2014-09-26T13:16:35.978Z</updated>
+  <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
+  <title>Mochi Tester's Contacts</title>
+  <link href="http://www.google.com/" rel="alternate" type="text/html"/>
+  <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml"/>
+  <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#post" type="application/atom+xml"/>
+  <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/batch" rel="http://schemas.google.com/g/2005#batch" type="application/atom+xml"/>
+  <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full?max-results=25" rel="self" type="application/atom+xml"/>
+  <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full?start-index=26&amp;max-results=25" rel="next" type="application/atom+xml"/>
+  <author>
+    <name>Mochi Tester</name>
+    <email>tester@mochi.com</email>
+  </author>
+  <generator uri="http://www.google.com/m8/feeds" version="1.0">Contacts</generator>
+  <openSearch:totalResults>25</openSearch:totalResults>
+  <openSearch:startIndex>1</openSearch:startIndex>
+  <openSearch:itemsPerPage>10000000</openSearch:itemsPerPage>
+  <entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
+    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/0</id>
+    <updated>2012-08-17T23:50:36.892Z</updated>
+    <app:edited xmlns:app="http://www.w3.org/2007/app">2012-08-17T23:50:36.892Z</app:edited>
+    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
+    <title>John Smith</title>
+    <link gd:etag="&quot;Ug92D34SfCt7I2BmLHJTRgVzTlgrJXEAU08.&quot;" href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/0" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
+    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/0" rel="self" type="application/atom+xml"/>
+    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/0" rel="edit" type="application/atom+xml"/>
+    <gd:name>
+      <gd:fullName>John Smith</gd:fullName>
+      <gd:givenName>John</gd:givenName>
+      <gd:familyName>Smith</gd:familyName>
+    </gd:name>
+    <gd:email address="john.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
+    <gContact:website href="http://www.google.com/profiles/109576547678240773721" rel="profile"/>
+  </entry>
+  <entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
+    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/1</id>
+    <updated>2012-08-17T23:50:36.892Z</updated>
+    <app:edited xmlns:app="http://www.w3.org/2007/app">2012-08-17T23:50:36.892Z</app:edited>
+    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
+    <title>Jane Smith</title>
+    <link gd:etag="&quot;WA9BY1xFWit7I2BhLEkieCxLHEYTGCYuNxo.&quot;" href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/1" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
+    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/1" rel="self" type="application/atom+xml"/>
+    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/1" rel="edit" type="application/atom+xml"/>
+    <gd:name>
+      <gd:fullName>Jane Smith</gd:fullName>
+      <gd:givenName>Jane</gd:givenName>
+      <gd:familyName>Smith</gd:familyName>
+    </gd:name>
+    <gd:email address="jane.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
+    <gContact:website href="http://www.google.com/profiles/112886528199784431028" rel="profile"/>
+  </entry>
+  <entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
+    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/2</id>
+    <updated>2012-08-17T23:50:36.892Z</updated>
+    <app:edited xmlns:app="http://www.w3.org/2007/app">2012-08-17T23:50:36.892Z</app:edited>
+    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
+    <title>Davy Randall Jones</title>
+    <link gd:etag="&quot;KiV2PkYRfCt7I2BuD1AzEBFxD1VcGjwBUyA.&quot;" href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/2" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
+    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/2" rel="self" type="application/atom+xml"/>
+    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/2" rel="edit" type="application/atom+xml"/>
+    <gd:name>
+      <gd:fullName>Davy Randall Jones</gd:fullName>
+      <gd:givenName>Davy Randall</gd:givenName>
+      <gd:familyName>Jones</gd:familyName>
+    </gd:name>
+    <gd:email address="davy.jones@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
+    <gContact:website href="http://www.google.com/profiles/109710625881478599011" rel="profile"/>
+  </entry>
+  <entry gd:etag="&quot;Q3w7ezVSLit7I2A9WB5WGUkNRgE.&quot;">
+    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/3</id>
+    <updated>2007-08-01T05:45:52.203Z</updated>
+    <app:edited xmlns:app="http://www.w3.org/2007/app">2007-08-01T05:45:52.203Z</app:edited>
+    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
+    <title/>
+    <link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/3" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
+    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="self" type="application/atom+xml"/>
+    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="edit" type="application/atom+xml"/>
+    <gd:email address="noname@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
+  </entry>
+  <entry gd:etag="&quot;Q3w7ezVSLit7I2A9WB5WGUkNRgE.&quot;">
+    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/7</id>
+    <updated>2007-08-01T05:45:52.203Z</updated>
+    <app:edited xmlns:app="http://www.w3.org/2007/app">2007-08-01T05:45:52.203Z</app:edited>
+    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
+    <title/>
+    <link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/7" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
+    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="self" type="application/atom+xml"/>
+    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="edit" type="application/atom+xml"/>
+    <gd:email address="lycnix" primary="true" rel="http://schemas.google.com/g/2005#other"/>
+  </entry>
+</feed>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/fixtures/google_token.txt
@@ -0,0 +1,3 @@
+{
+    "access_token": "test-token"
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/google_service.sjs
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, Constructor: CC} = Components;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+                             "nsIBinaryInputStream",
+                             "setInputStream");
+
+function handleRequest(req, res) {
+  try {
+    reallyHandleRequest(req, res);
+  } catch (ex) {
+    res.setStatusLine("1.0", 200, "AlmostOK");
+    let msg = "Error handling request: " + ex + "\n" + ex.stack;
+    log(msg);
+    res.write(msg);
+  }
+}
+
+function log(msg) {
+  // dump("GOOGLE-SERVER-MOCK: " + msg + "\n");
+}
+
+const kBasePath = "browser/browser/components/loop/test/mochitest/fixtures/";
+
+const kStatusCodes = {
+  400: "Bad Request",
+  401: "Unauthorized",
+  403: "Forbidden",
+  404: "Not Found",
+  405: "Method Not Allowed",
+  500: "Internal Server Error",
+  501: "Not Implemented",
+  503: "Service Unavailable"
+};
+
+function HTTPError(code = 500, message) {
+  this.code = code;
+  this.name = kStatusCodes[code] || "HTTPError";
+  this.message = message || this.name;
+}
+HTTPError.prototype = new Error();
+HTTPError.prototype.constructor = HTTPError;
+
+function sendError(res, err) {
+  if (!(err instanceof HTTPError)) {
+    err = new HTTPError(typeof err == "number" ? err : 500,
+                        err.message || typeof err == "string" ? err : "");
+  }
+  res.setStatusLine("1.1", err.code, err.name);
+  res.write(err.message);
+}
+
+function parseQuery(query, params = {}) {
+  for (let param of query.replace(/^[?&]/, "").split(/(?:&|\?)/)) {
+    param = param.split("=");
+    if (!param[0])
+      continue;
+    params[unescape(param[0])] = unescape(param[1]);
+  }
+  return params;
+}
+
+function getRequestBody(req) {
+  let avail;
+  let bytes = [];
+  let body = new BinaryInputStream(req.bodyInputStream);
+
+  while ((avail = body.available()) > 0)
+    Array.prototype.push.apply(bytes, body.readByteArray(avail));
+
+  return String.fromCharCode.apply(null, bytes);
+}
+
+function getInputStream(path) {
+  let file = Cc["@mozilla.org/file/directory_service;1"]
+               .getService(Ci.nsIProperties)
+               .get("CurWorkD", Ci.nsILocalFile);
+  for (let part of path.split("/"))
+    file.append(part);
+  let fileStream  = Cc["@mozilla.org/network/file-input-stream;1"]
+                      .createInstance(Ci.nsIFileInputStream);
+  fileStream.init(file, 1, 0, false);
+  return fileStream;
+}
+
+function checkAuth(req) {
+  if (!req.hasHeader("Authorization"))
+    throw new HTTPError(401, "No Authorization header provided.");
+
+  let auth = req.getHeader("Authorization");
+  if (auth != "Bearer test-token")
+    throw new HTTPError(401, "Invalid Authorization header content: '" + auth + "'");
+}
+
+function reallyHandleRequest(req, res) {
+  log("method: " + req.method);
+
+  let body = getRequestBody(req);
+  log("body: " + body);
+
+  let contentType = req.hasHeader("Content-Type") ? req.getHeader("Content-Type") : null;
+  log("contentType: " + contentType);
+
+  let params = parseQuery(req.queryString);
+  parseQuery(body, params);
+  log("params: " + JSON.stringify(params));
+
+  // Delegate an authentication request to the correct handler.
+  if ("action" in params) {
+    methodHandlers[params.action](req, res, params);
+  } else {
+    sendError(res, 501);
+  }
+}
+
+function respondWithFile(res, fileName, mimeType) {
+  res.setStatusLine("1.1", 200, "OK");
+  res.setHeader("Content-Type", mimeType);
+
+  let inputStream = getInputStream(kBasePath + fileName);
+  res.bodyOutputStream.writeFrom(inputStream, inputStream.available());
+  inputStream.close();
+}
+
+const methodHandlers = {
+  auth: function(req, res, params) {
+    respondWithFile(res, "google_auth.txt", "text/html");
+  },
+
+  token: function(req, res, params) {
+    respondWithFile(res, "google_token.txt", "application/json");
+  },
+
+  contacts: function(req, res, params) {
+    try {
+      checkAuth(req);
+    } catch (ex) {
+      sendError(res, ex, ex.code);
+      return;
+    }
+
+    respondWithFile(res, "google_contacts.txt", "text/xml");
+  }
+};
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -193,8 +193,56 @@ let mockPushHandler = {
 
   /**
    * Test-only API to simplify notifying a push notification result.
    */
   notify: function(version) {
     this._notificationCallback(version);
   }
 };
+
+const mockDb = {
+  _store: { },
+  _next_guid: 1,
+
+  add: function(details, callback) {
+    if (!("id" in details)) {
+      callback(new Error("No 'id' field present"));
+      return;
+    }
+    details._guid = this._next_guid++;
+    this._store[details._guid] = details;
+    callback(null, details);
+  },
+  remove: function(guid, callback) {
+    if (!guid in this._store) {
+      callback(new Error("Could not find _guid '" + guid + "' in database"));
+      return;
+    }
+    delete this._store[guid];
+    callback(null);
+  },
+  getAll: function(callback) {
+    callback(null, this._store);
+  },
+  get: function(guid, callback) {
+    callback(null, this._store[guid]);
+  },
+  getByServiceId: function(serviceId, callback) {
+    for (let guid in this._store) {
+      if (serviceId === this._store[guid].id) {
+        callback(null, this._store[guid]);
+        return;
+      }
+    }
+    callback(null, null);
+  },
+  removeAll: function(callback) {
+    this._store = {};
+    this._next_guid = 1;
+    callback(null);
+  },
+  promise: function(method, ...params) {
+    return new Promise(resolve => {
+      this[method](...params, (err, res) => err ? reject(err) : resolve(res));
+    });
+  }
+};