Bug 1022725: add a mock httpd.js Translation provider for tests. r=florian,felipe, a=gavin.
authorMike de Boer <mdeboer@mozilla.com>
Thu, 19 Jun 2014 16:56:02 +0200
changeset 207344 13c4804465d6b6a9cbbd1b19a804c0d7c71d144d
parent 207343 a4b0020e343c7ea6ac3d688e7b5f5c6979405b36
child 207345 7130f4a8e2bb0d8717599acc41283bd69540f4d8
push id3741
push userasasaki@mozilla.com
push dateMon, 21 Jul 2014 20:25:18 +0000
treeherdermozilla-beta@4d6f46f5af68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflorian, felipe, gavin
bugs1022725
milestone32.0a2
Bug 1022725: add a mock httpd.js Translation provider for tests. r=florian,felipe, a=gavin.
browser/components/translation/test/bing.sjs
browser/components/translation/test/browser.ini
browser/components/translation/test/browser_translation_bing.js
browser/components/translation/test/browser_translation_fhr.js
browser/components/translation/test/fixtures/bug1022725-fr.html
browser/components/translation/test/fixtures/result-da39a3ee5e.txt
testing/profiles/prefs_general.js
new file mode 100644
--- /dev/null
+++ b/browser/components/translation/test/bing.sjs
@@ -0,0 +1,218 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.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("BING-SERVER-MOCK: " + msg + "\n");
+}
+
+const statusCodes = {
+  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 = statusCodes[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) {
+  let ret = {};
+  for (let param of query.replace(/^[?&]/, "").split("&")) {
+    param = param.split("=");
+    if (!param[0])
+      continue;
+    ret[unescape(param[0])] = unescape(param[1]);
+  }
+  return ret;
+}
+
+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 sha1(str) {
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                    .createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  // `result` is an out parameter, `result.value` will contain the array length.
+  let result = {};
+  // `data` is an array of bytes.
+  let data = converter.convertToByteArray(str, result);
+  let ch = Cc["@mozilla.org/security/hash;1"]
+             .createInstance(Ci.nsICryptoHash);
+  ch.init(ch.SHA1);
+  ch.update(data, data.length);
+  let hash = ch.finish(false);
+
+  // Return the two-digit hexadecimal code for a byte.
+  function toHexString(charCode) {
+    return ("0" + charCode.toString(16)).slice(-2);
+  }
+
+  // Convert the binary hash data to a hex string.
+  return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
+}
+
+function parseXml(body) {
+  let DOMParser = Cc["@mozilla.org/xmlextras/domparser;1"]
+                    .createInstance(Ci.nsIDOMParser);
+  let xml = DOMParser.parseFromString(body, "text/xml");
+  if (xml.documentElement.localName == "parsererror")
+    throw new Error("Invalid XML");
+  return xml;
+}
+
+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) {
+  let err = new Error("Authorization failed");
+  err.code = 401;
+
+  if (!req.hasHeader("Authorization"))
+    throw new HTTPError(401, "No Authorization header provided.");
+
+  let auth = req.getHeader("Authorization");
+  if (!auth.startsWith("Bearer "))
+    throw new HTTPError(401, "Invalid Authorization header content: '" + auth + "'");
+}
+
+function reallyHandleRequest(req, res) {
+  log("method: " + req.method);
+  if (req.method != "POST") {
+    sendError(res, "Bing only deals with POST requests, not '" + req.method + "'.");
+    return;
+  }
+
+  let body = getRequestBody(req);
+  log("body: " + body);
+
+  // First, we'll see if we're dealing with an XML body:
+  let contentType = req.hasHeader("Content-Type") ? req.getHeader("Content-Type") : null;
+  log("contentType: " + contentType);
+
+  if (contentType == "text/xml") {
+    try {
+      // For all these requests the client needs to supply the correct
+      // authentication headers.
+      checkAuth(req);
+
+      let xml = parseXml(body);
+      let method = xml.documentElement.localName;
+      log("invoking method: " + method);
+      // If the requested method is supported, delegate it to its handler.
+      if (methodHandlers[method])
+        methodHandlers[method](res, xml);
+      else
+        throw new HTTPError(501);
+    } catch (ex) {
+      sendError(res, ex, ex.code);
+    }
+  } else {
+    // Not XML, so it must be a query-string.
+    let params = parseQuery(body);
+
+    // Delegate an authentication request to the correct handler.
+    if ("grant_type" in params && params.grant_type == "client_credentials")
+      methodHandlers.authenticate(res, params);
+    else
+      sendError(res, 501);
+  }
+}
+
+const methodHandlers = {
+  authenticate: function(res, params) {
+    // Validate a few required parameters.
+    if (params.scope != "http://api.microsofttranslator.com") {
+      sendError(res, "Invalid scope.");
+      return;
+    }
+    if (!params.client_id) {
+      sendError(res, "Missing client_id param.");
+      return;
+    }
+    if (!params.client_secret) {
+      sendError(res, "Missing client_secret param.");
+      return;
+    }
+
+    let content = JSON.stringify({
+      access_token: "test",
+      expires_in: 600
+    });
+
+    res.setStatusLine("1.1", 200, "OK");
+    res.setHeader("Content-Length", String(content.length));
+    res.setHeader("Content-Type", "application/json");
+    res.write(content);
+  },
+
+  TranslateArrayRequest: function(res, xml, body) {
+    let from = xml.querySelector("From").firstChild.nodeValue;
+    let to = xml.querySelector("To").firstChild.nodeValue
+    log("translating from '" + from + "' to '" + to + "'");
+
+    res.setStatusLine("1.1", 200, "OK");
+    res.setHeader("Content-Type", "text/xml");
+
+    let hash = sha1(body).substr(0, 10);
+    log("SHA1 hash of content: " + hash);
+    let inputStream = getInputStream(
+      "browser/browser/components/translation/test/fixtures/result-" + hash + ".txt");
+    res.bodyOutputStream.writeFrom(inputStream, inputStream.available());
+    inputStream.close();
+  }
+};
--- a/browser/components/translation/test/browser.ini
+++ b/browser/components/translation/test/browser.ini
@@ -1,6 +1,10 @@
 [DEFAULT]
+support-files =
+  bing.sjs
+  fixtures/bug1022725-fr.html
+  fixtures/result-da39a3ee5e.txt
 
+[browser_translation_bing.js]
 [browser_translation_fhr.js]
-skip-if = true # Needs to wait until bug 1022725.
 [browser_translation_infobar.js]
 [browser_translation_exceptions.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/translation/test/browser_translation_bing.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test the Bing Translator client against a mock Bing service, bing.sjs.
+
+"use strict";
+
+const kClientIdPref = "browser.translation.bing.clientIdOverride";
+const kClientSecretPref = "browser.translation.bing.apiKeyOverride";
+
+const {BingTranslator} = Cu.import("resource:///modules/translation/BingTranslator.jsm", {});
+const {TranslationDocument} = Cu.import("resource:///modules/translation/TranslationDocument.jsm", {});
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setCharPref(kClientIdPref, "testClient");
+  Services.prefs.setCharPref(kClientSecretPref, "testSecret");
+
+  // Deduce the Mochitest server address in use from a pref that was pre-processed.
+  let server = Services.prefs.getCharPref("browser.translation.bing.authURL")
+                             .replace("http://", "");
+  server = server.substr(0, server.indexOf("/"));
+  let tab = gBrowser.addTab("http://" + server +
+    "/browser/browser/components/translation/test/fixtures/bug1022725-fr.html");
+  gBrowser.selectedTab = tab;
+
+  registerCleanupFunction(function () {
+    gBrowser.removeTab(tab);
+    Services.prefs.clearUserPref(kClientIdPref);
+    Services.prefs.clearUserPref(kClientSecretPref);
+  });
+
+  let browser = tab.linkedBrowser;
+  browser.addEventListener("load", function onload() {
+    if (browser.currentURI.spec == "about:blank")
+      return;
+
+    browser.removeEventListener("load", onload, true);
+    let client = new BingTranslator(
+      new TranslationDocument(browser.contentDocument), "fr", "en");
+
+    client.translate().then(
+      result => {
+        // XXXmikedeboer; here you would continue the test/ content inspection.
+        ok(result, "There should be a result.");
+        finish();
+      },
+      error => {
+        ok(false, "Unexpected Client Error: " + error);
+        finish();
+      }
+    );
+  }, true);
+}
--- a/browser/components/translation/test/browser_translation_fhr.js
+++ b/browser/components/translation/test/browser_translation_fhr.js
@@ -52,17 +52,18 @@ function retrieveTranslationCounts() {
     let values = yield measurement.getValues();
 
     let day = values.days.getDay(new Date());
     if (!day) {
       // This should never happen except when the test runs at midnight.
       return [0, 0];
     }
 
-    return [day.get("pageTranslatedCount"), day.get("charactersTranslatedCount")];
+    // .get() may return `undefined`, which we can't compute.
+    return [day.get("pageTranslatedCount") || 0, day.get("charactersTranslatedCount") || 0];
   });
 }
 
 function translate(text, from, to) {
   return Task.spawn(function* task_translate() {
     // Create some content to translate.
     let tab = gBrowser.selectedTab =
       gBrowser.addTab("data:text/html;charset=utf-8," + text);
new file mode 100644
--- /dev/null
+++ b/browser/components/translation/test/fixtures/bug1022725-fr.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang="fr">
+  <head>
+    <!--
+     - Text retrieved from http://fr.wikipedia.org/wiki/Coupe_du_monde_de_football_de_2014
+     - at 06/13/2014, Creative Commons Attribution-ShareAlike License.
+     -->
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>test</title>
+  </head>
+  <body>
+    <h1>Coupe du monde de football de 2014</h1>
+    <div>La Coupe du monde de football de 2014 est la 20e édition de la Coupe du monde de football, compétition organisée par la FIFA et qui réunit les trente-deux meilleures sélections nationales. Sa phase finale a lieu à l'été 2014 au Brésil. Avec le pays organisateur, toutes les équipes championnes du monde depuis 1930 (Uruguay, Italie, Allemagne, Angleterre, Argentine, France et Espagne) se sont qualifiées pour cette compétition. Elle est aussi la première compétition internationale de la Bosnie-Herzégovine.</div>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/translation/test/fixtures/result-da39a3ee5e.txt
@@ -0,0 +1,22 @@
+<ArrayOfTranslateArrayResponse xmlns="http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
+  <TranslateArrayResponse>
+    <From>fr</From>
+    <OriginalTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
+      <a:int>34</a:int>
+    </OriginalTextSentenceLengths>
+    <TranslatedText>Football's 2014 World Cup</TranslatedText>
+    <TranslatedTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
+      <a:int>25</a:int>
+    </TranslatedTextSentenceLengths>
+  </TranslateArrayResponse>
+  <TranslateArrayResponse>
+    <From>fr</From>
+    <OriginalTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
+      <a:int>508</a:int>
+    </OriginalTextSentenceLengths>
+    <TranslatedText>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus diam sem, porttitor eget neque sit amet, ultricies posuere metus. Cras placerat rutrum risus, nec dignissim magna dictum vitae. Fusce eleifend fermentum lacinia. Nulla sagittis cursus nibh. Praesent adipiscing, elit at pulvinar dapibus, neque massa tincidunt sapien, eu consectetur lectus metus sit amet odio. Proin blandit consequat porttitor. Pellentesque vehicula justo sed luctus vestibulum. Donec metus.</TranslatedText>
+    <TranslatedTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
+      <a:int>475</a:int>
+    </TranslatedTextSentenceLengths>
+  </TranslateArrayResponse>
+</ArrayOfTranslateArrayResponse>
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -195,8 +195,12 @@ user_pref('toolkit.telemetry.server', 'h
 
 // We don't want to hit the real Firefox Accounts server for tests.  We don't
 // actually need a functioning FxA server, so just set it to something that
 // resolves and accepts requests, even if they all fail.
 user_pref('identity.fxaccounts.auth.uri', 'https://%(server)s/fxa-dummy/');
 
 // Enable logging of APZ test data (see bug 961289).
 user_pref('apz.test.logging_enabled', true);
+
+// Make sure Translation won't hit the network.
+user_pref("browser.translation.bing.authURL", "http://%(server)s/browser/browser/components/translation/test/bing.sjs");
+user_pref("browser.translation.bing.translateArrayURL", "http://%(server)s/browser/browser/components/translation/test/bing.sjs");