#!/usr/bin/env node /* Copyright 2016 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* eslint-disable no-var */ "use strict"; var assert = require("assert"); var fs = require("fs"); var vm = require("vm"); var SRC_DIR = __dirname + "/../../"; var telemetryJsPath = "extensions/chromium/telemetry.js"; var telemetryJsSource = fs.readFileSync(SRC_DIR + telemetryJsPath); var telemetryScript = new vm.Script(telemetryJsSource, { filename: telemetryJsPath, displayErrors: true, }); var LOG_URL = /LOG_URL = '([^']+)'/.exec(telemetryJsSource)[1]; // Create a minimal extension global that mocks the extension environment that // is used by telemetry.js function createExtensionGlobal() { var window = {}; // Whenever a "request" was made, the extra headers are appended to this list. var test_requests = (window.test_requests = []); // Extension API mocks. window.window = window; window.chrome = {}; window.chrome.extension = {}; window.chrome.extension.inIncognitoContext = false; window.chrome.runtime = {}; window.chrome.runtime.id = "oemmndcbldboiebfnladdacbdfmadadm"; window.chrome.runtime.getManifest = function () { return { version: "1.0.0" }; }; function createStorageAPI() { var storageArea = {}; storageArea.get = function (key, callback) { assert.equal(key, "disableTelemetry"); // chrome.storage.*. is async, but we make it synchronous to ease testing. callback(storageArea.mock_data); }; return storageArea; } window.chrome.storage = {}; window.chrome.storage.managed = createStorageAPI(); window.chrome.storage.local = createStorageAPI(); window.chrome.storage.sync = createStorageAPI(); // Other DOM. window.navigator = {}; window.navigator.onLine = true; window.navigator.userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/50.0.2661.94 Safari/537.36"; window.localStorage = {}; var getRandomValues_state = 0; window.crypto = {}; window.crypto.getRandomValues = function (buf) { var state = getRandomValues_state++; for (var i = 0; i < buf.length; ++i) { // Totally random byte ;) buf[i] = 0x42 + state; } return buf; }; // Network-related mocks. window.Request = {}; window.Request.prototype = { get mode() { throw new TypeError("Illegal invocation"); }, }; window.fetch = function (url, options) { assert.equal(url, LOG_URL); assert.equal(options.method, "POST"); assert.equal(options.mode, "cors"); assert.ok(!options.body); test_requests.push(options.headers); }; window.Headers = function (headers) { headers = JSON.parse(JSON.stringify(headers)); // Clone. Object.keys(headers).forEach(function (k) { headers[k] = String(headers[k]); }); return headers; }; window.XMLHttpRequest = function () { var invoked = { open: false, send: false, }; var headers = {}; return { open(method, url) { assert.equal(invoked.open, false); invoked.open = true; assert.equal(method, "POST"); assert.equal(url, LOG_URL); }, setRequestHeader(k, v) { assert.equal(invoked.open, true); headers[k] = String(v); }, send(body) { assert.equal(invoked.open, true); assert.equal(invoked.send, false); invoked.send = true; assert.ok(!body); test_requests.push(headers); }, }; }; // Time-related logic. var timers = []; window.setInterval = function (callback, ms) { assert.equal(typeof callback, "function"); timers.push(callback); }; window.Date = { test_now_value: Date.now(), now() { return window.Date.test_now_value; }, }; window.test_fireTimers = function () { assert.ok(timers.length); timers.forEach(function (timer) { timer(); }); }; return window; } // Simulate an update of the browser. function updateBrowser(window) { window.navigator.userAgent = window.navigator.userAgent.replace( /Chrome\/(\d+)/, function (_, v) { return "Chrome/" + (parseInt(v) + 1); } ); } var tests = [ function test_first_run() { // Default settings, run extension for the first time. var window = createExtensionGlobal(); telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); }, function test_first_run_incognito() { // The extension should not send any requests when in incognito mode. var window = createExtensionGlobal(); window.chrome.extension.inIncognitoContext = true; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, []); }, function test_storage_managed_unavailable() { var window = createExtensionGlobal(); delete window.chrome.storage.managed; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); }, function test_managed_pref() { var window = createExtensionGlobal(); window.chrome.storage.managed.mock_data = { disableTelemetry: true, }; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, []); }, function test_local_pref() { var window = createExtensionGlobal(); window.chrome.storage.local.mock_data = { disableTelemetry: true, }; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, []); }, function test_managed_pref_is_overridden() { var window = createExtensionGlobal(); window.chrome.storage.managed.mock_data = { disableTelemetry: true, }; window.chrome.storage.sync.mock_data = { disableTelemetry: false, }; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); }, function test_run_extension_again() { var window = createExtensionGlobal(); telemetryScript.runInNewContext(window); telemetryScript.runInNewContext(window); // Only one request should be sent because of rate-limiting. assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); // Simulate that quite some hours passed, but it's still rate-limited. window.Date.test_now_value += 11 * 36e5; telemetryScript.runInNewContext(window); // Only one request should be sent because of rate-limiting. assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); // Another hour passes and the request should not be rate-limited any more. window.Date.test_now_value += 1 * 36e5; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); }, function test_running_for_a_while() { var window = createExtensionGlobal(); telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); // Simulate that the timer fired 11 hours since the last ping. The request // should still be rate-limited. window.Date.test_now_value += 11 * 36e5; window.test_fireTimers(); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); // Another hour passes and the request should not be rate-limited any more. window.Date.test_now_value += 1 * 36e5; window.test_fireTimers(); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); }, function test_browser_update() { var window = createExtensionGlobal(); telemetryScript.runInNewContext(window); updateBrowser(window); telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, { // Generate a new ID for better privacy. "Deduplication-Id": "4343434343", "Extension-Version": "1.0.0", }, ]); }, function test_browser_update_between_pref_toggle() { var window = createExtensionGlobal(); telemetryScript.runInNewContext(window); window.chrome.storage.local.mock_data = { disableTelemetry: true, }; updateBrowser(window); telemetryScript.runInNewContext(window); window.chrome.storage.local.mock_data = { disableTelemetry: false, }; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, { // Generate a new ID for better privacy, even if the update happened // while telemetry was disabled. "Deduplication-Id": "4343434343", "Extension-Version": "1.0.0", }, ]); }, function test_extension_update() { var window = createExtensionGlobal(); telemetryScript.runInNewContext(window); window.chrome.runtime.getManifest = function () { return { version: "1.0.1" }; }; window.Date.test_now_value += 12 * 36e5; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, { // The ID did not change because the browser version did not change. "Deduplication-Id": "4242424242", "Extension-Version": "1.0.1", }, ]); }, function test_unofficial_build() { var window = createExtensionGlobal(); var didWarn = false; window.console = {}; window.console.warn = function () { didWarn = true; }; window.chrome.runtime.id = "abcdefghijklmnopabcdefghijklmnop"; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, []); assert.ok(didWarn); }, function test_fetch_is_supported() { var window = createExtensionGlobal(); // XMLHttpRequest should not be called when fetch is available. So removing // the XMLHttpRequest API should not change behavior. delete window.XMLHttpRequest; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); }, function test_fetch_not_supported() { var window = createExtensionGlobal(); delete window.fetch; delete window.Request; delete window.Headers; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); }, function test_fetch_mode_not_supported() { var window = createExtensionGlobal(); delete window.Request.prototype.mode; window.fetch = function () { throw new Error("Unexpected call to fetch!"); }; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); }, function test_network_offline() { var window = createExtensionGlobal(); // Simulate that the network is down for sure. window.navigator.onLine = false; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, []); // Simulate that the network might be up. window.navigator.onLine = true; telemetryScript.runInNewContext(window); assert.deepEqual(window.test_requests, [ { "Deduplication-Id": "4242424242", "Extension-Version": "1.0.0", }, ]); }, ]; var test_i = 0; (function next() { var test = tests[test_i++]; if (!test) { console.log("All tests completed."); return; } console.log("Running test " + test_i + "/" + tests.length + ": " + test.name); test(); process.nextTick(next); })();