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 231683 e14db252186c206eaf5109ca2572ade5323ce92c
parent 231682 1d4b89d2fe4a6de20ac96ecc44332b2b0c8c2dcb
child 231684 eee08fb2a4a4a793d9a7582c9a775d971f1fda81
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersabr
bugs1069816
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
testing/profiles/prefs_general.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));
+    });
+  }
+};
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -244,16 +244,18 @@ user_pref("dom.mozApps.debug", true);
 
 // Don't fetch or send directory tiles data from real servers
 user_pref("browser.newtabpage.directory.source", 'data:application/json,{"testing":1}');
 user_pref("browser.newtabpage.directory.ping", "");
 
 // Enable Loop
 user_pref("loop.enabled", true);
 user_pref("loop.throttled", false);
+user_pref("loop.oauth.google.URL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=");
+user_pref("loop.oauth.google.getContactsURL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=contacts");
 
 // Ensure UITour won't hit the network
 user_pref("browser.uitour.pinnedTabUrl", "http://%(server)s/uitour-dummy/pinnedTab");
 user_pref("browser.uitour.url", "http://%(server)s/uitour-dummy/tour");
 
 user_pref("media.eme.enabled", true);
 
 // Don't prompt about e10s