diff options
Diffstat (limited to 'toolkit/components/telemetry/tests/unit/test_TelemetryController.js')
-rw-r--r-- | toolkit/components/telemetry/tests/unit/test_TelemetryController.js | 507 |
1 files changed, 507 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js new file mode 100644 index 000000000..b383de6bf --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js @@ -0,0 +1,507 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ +/* This testcase triggers two telemetry pings. + * + * Telemetry code keeps histograms of past telemetry pings. The first + * ping populates these histograms. One of those histograms is then + * checked in the second request. + */ + +Cu.import("resource://gre/modules/ClientID.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetryStorage.jsm", this); +Cu.import("resource://gre/modules/TelemetrySend.jsm", this); +Cu.import("resource://gre/modules/TelemetryArchive.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/Preferences.jsm"); + +const PING_FORMAT_VERSION = 4; +const DELETION_PING_TYPE = "deletion"; +const TEST_PING_TYPE = "test-ping-type"; + +const PLATFORM_VERSION = "1.9.2"; +const APP_VERSION = "1"; +const APP_NAME = "XPCShell"; + +const PREF_BRANCH = "toolkit.telemetry."; +const PREF_ENABLED = PREF_BRANCH + "enabled"; +const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled"; +const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; +const PREF_UNIFIED = PREF_BRANCH + "unified"; + +var gClientID = null; + +function sendPing(aSendClientId, aSendEnvironment) { + if (PingServer.started) { + TelemetrySend.setServer("http://localhost:" + PingServer.port); + } else { + TelemetrySend.setServer("http://doesnotexist"); + } + + let options = { + addClientId: aSendClientId, + addEnvironment: aSendEnvironment, + }; + return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options); +} + +function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) { + const MANDATORY_PING_FIELDS = [ + "type", "id", "creationDate", "version", "application", "payload" + ]; + + const APPLICATION_TEST_DATA = { + buildId: gAppInfo.appBuildID, + name: APP_NAME, + version: APP_VERSION, + displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY, + vendor: "Mozilla", + platformVersion: PLATFORM_VERSION, + xpcomAbi: "noarch-spidermonkey", + }; + + // Check that the ping contains all the mandatory fields. + for (let f of MANDATORY_PING_FIELDS) { + Assert.ok(f in aPing, f + " must be available."); + } + + Assert.equal(aPing.type, aType, "The ping must have the correct type."); + Assert.equal(aPing.version, PING_FORMAT_VERSION, "The ping must have the correct version."); + + // Test the application section. + for (let f in APPLICATION_TEST_DATA) { + Assert.equal(aPing.application[f], APPLICATION_TEST_DATA[f], + f + " must have the correct value."); + } + + // We can't check the values for channel and architecture. Just make + // sure they are in. + Assert.ok("architecture" in aPing.application, + "The application section must have an architecture field."); + Assert.ok("channel" in aPing.application, + "The application section must have a channel field."); + + // Check the clientId and environment fields, as needed. + Assert.equal("clientId" in aPing, aHasClientId); + Assert.equal("environment" in aPing, aHasEnvironment); +} + +add_task(function* test_setup() { + // Addon manager needs a profile directory + do_get_profile(); + loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + // Make sure we don't generate unexpected pings due to pref changes. + yield setEmptyPrefWatchlist(); + + Services.prefs.setBoolPref(PREF_ENABLED, true); + Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true); + + yield new Promise(resolve => + Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))); +}); + +add_task(function* asyncSetup() { + yield TelemetryController.testSetup(); +}); + +// Ensure that not overwriting an existing file fails silently +add_task(function* test_overwritePing() { + let ping = {id: "foo"}; + yield TelemetryStorage.savePing(ping, true); + yield TelemetryStorage.savePing(ping, false); + yield TelemetryStorage.cleanupPingFile(ping); +}); + +// Checks that a sent ping is correctly received by a dummy http server. +add_task(function* test_simplePing() { + PingServer.start(); + // Update the Telemetry Server preference with the address of the local server. + // Otherwise we might end up sending stuff to a non-existing server after + // |TelemetryController.testReset| is called. + Preferences.set(TelemetryController.Constants.PREF_SERVER, "http://localhost:" + PingServer.port); + + yield sendPing(false, false); + let request = yield PingServer.promiseNextRequest(); + + // Check that we have a version query parameter in the URL. + Assert.notEqual(request.queryString, ""); + + // Make sure the version in the query string matches the new ping format version. + let params = request.queryString.split("&"); + Assert.ok(params.find(p => p == ("v=" + PING_FORMAT_VERSION))); + + let ping = decodeRequestPayload(request); + checkPingFormat(ping, TEST_PING_TYPE, false, false); +}); + +add_task(function* test_disableDataUpload() { + const isUnified = Preferences.get(PREF_UNIFIED, false); + if (!isUnified) { + // Skipping the test if unified telemetry is off, as no deletion ping will + // be generated. + return; + } + + // Disable FHR upload: this should trigger a deletion ping. + Preferences.set(PREF_FHR_UPLOAD_ENABLED, false); + + let ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, DELETION_PING_TYPE, true, false); + // Wait on ping activity to settle. + yield TelemetrySend.testWaitOnOutgoingPings(); + + // Restore FHR Upload. + Preferences.set(PREF_FHR_UPLOAD_ENABLED, true); + + // Simulate a failure in sending the deletion ping by disabling the HTTP server. + yield PingServer.stop(); + + // Try to send a ping. It will be saved as pending and get deleted when disabling upload. + TelemetryController.submitExternalPing(TEST_PING_TYPE, {}); + + // Disable FHR upload to send a deletion ping again. + Preferences.set(PREF_FHR_UPLOAD_ENABLED, false); + + // Wait on sending activity to settle, as |TelemetryController.testReset()| doesn't do that. + yield TelemetrySend.testWaitOnOutgoingPings(); + // Wait for the pending pings to be deleted. Resetting TelemetryController doesn't + // trigger the shutdown, so we need to call it ourselves. + yield TelemetryStorage.shutdown(); + // Simulate a restart, and spin the send task. + yield TelemetryController.testReset(); + + // Disabling Telemetry upload must clear out all the pending pings. + let pendingPings = yield TelemetryStorage.loadPendingPingList(); + Assert.equal(pendingPings.length, 1, + "All the pending pings but the deletion ping should have been deleted"); + + // Enable the ping server again. + PingServer.start(); + // We set the new server using the pref, otherwise it would get reset with + // |TelemetryController.testReset|. + Preferences.set(TelemetryController.Constants.PREF_SERVER, "http://localhost:" + PingServer.port); + + // Stop the sending task and then start it again. + yield TelemetrySend.shutdown(); + // Reset the controller to spin the ping sending task. + yield TelemetryController.testReset(); + ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, DELETION_PING_TYPE, true, false); + + // Wait on ping activity to settle before moving on to the next test. If we were + // to shut down telemetry, even though the PingServer caught the expected pings, + // TelemetrySend could still be processing them (clearing pings would happen in + // a couple of ticks). Shutting down would cancel the request and save them as + // pending pings. + yield TelemetrySend.testWaitOnOutgoingPings(); + // Restore FHR Upload. + Preferences.set(PREF_FHR_UPLOAD_ENABLED, true); +}); + +add_task(function* test_pingHasClientId() { + const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID"; + + // Make sure we have no cached client ID for this test: we'll try to send + // a ping with it while Telemetry is being initialized. + Preferences.reset(PREF_CACHED_CLIENTID); + yield TelemetryController.testShutdown(); + yield ClientID._reset(); + yield TelemetryStorage.testClearPendingPings(); + // And also clear the counter histogram since we're here. + let h = Telemetry.getHistogramById("TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID"); + h.clear(); + + // Init telemetry and try to send a ping with a client ID. + let promisePingSetup = TelemetryController.testReset(); + yield sendPing(true, false); + Assert.equal(h.snapshot().sum, 1, + "We must have a ping waiting for the clientId early during startup."); + // Wait until we are fully initialized. Pings will be assembled but won't get + // sent before then. + yield promisePingSetup; + + let ping = yield PingServer.promiseNextPing(); + // Fetch the client ID after initializing and fetching the the ping, so we + // don't unintentionally trigger its loading. We'll still need the client ID + // to see if the ping looks sane. + gClientID = yield ClientID.getClientID(); + + checkPingFormat(ping, TEST_PING_TYPE, true, false); + Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported."); + + // Shutdown Telemetry so we can safely restart it. + yield TelemetryController.testShutdown(); + yield TelemetryStorage.testClearPendingPings(); + + // We should have cached the client ID now. Lets confirm that by checking it before + // the async ping setup is finished. + h.clear(); + promisePingSetup = TelemetryController.testReset(); + yield sendPing(true, false); + yield promisePingSetup; + + // Check that we received the cached client id. + Assert.equal(h.snapshot().sum, 0, "We must have used the cached clientId."); + ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, TEST_PING_TYPE, true, false); + Assert.equal(ping.clientId, gClientID, + "Telemetry should report the correct cached clientId."); + + // Check that sending a ping without relying on the cache, after the + // initialization, still works. + Preferences.reset(PREF_CACHED_CLIENTID); + yield TelemetryController.testShutdown(); + yield TelemetryStorage.testClearPendingPings(); + yield TelemetryController.testReset(); + yield sendPing(true, false); + ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, TEST_PING_TYPE, true, false); + Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported."); + Assert.equal(h.snapshot().sum, 0, "No ping should have been waiting for a clientId."); +}); + +add_task(function* test_pingHasEnvironment() { + // Send a ping with the environment data. + yield sendPing(false, true); + let ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, TEST_PING_TYPE, false, true); + + // Test a field in the environment build section. + Assert.equal(ping.application.buildId, ping.environment.build.buildId); +}); + +add_task(function* test_pingHasEnvironmentAndClientId() { + // Send a ping with the environment data and client id. + yield sendPing(true, true); + let ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, TEST_PING_TYPE, true, true); + + // Test a field in the environment build section. + Assert.equal(ping.application.buildId, ping.environment.build.buildId); + // Test that we have the correct clientId. + Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported."); +}); + +add_task(function* test_archivePings() { + let now = new Date(2009, 10, 18, 12, 0, 0); + fakeNow(now); + + // Disable ping upload so that pings don't get sent. + // With unified telemetry the FHR upload pref controls this, + // with non-unified telemetry the Telemetry enabled pref. + const isUnified = Preferences.get(PREF_UNIFIED, false); + const uploadPref = isUnified ? PREF_FHR_UPLOAD_ENABLED : PREF_ENABLED; + Preferences.set(uploadPref, false); + + // If we're using unified telemetry, disabling ping upload will generate a "deletion" + // ping. Catch it. + if (isUnified) { + let ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, DELETION_PING_TYPE, true, false); + } + + // Register a new Ping Handler that asserts if a ping is received, then send a ping. + PingServer.registerPingHandler(() => Assert.ok(false, "Telemetry must not send pings if not allowed to.")); + let pingId = yield sendPing(true, true); + + // Check that the ping was archived, even with upload disabled. + let ping = yield TelemetryArchive.promiseArchivedPingById(pingId); + Assert.equal(ping.id, pingId, "TelemetryController should still archive pings."); + + // Check that pings don't get archived if not allowed to. + now = new Date(2010, 10, 18, 12, 0, 0); + fakeNow(now); + Preferences.set(PREF_ARCHIVE_ENABLED, false); + pingId = yield sendPing(true, true); + let promise = TelemetryArchive.promiseArchivedPingById(pingId); + Assert.ok((yield promiseRejects(promise)), + "TelemetryController should not archive pings if the archive pref is disabled."); + + // Enable archiving and the upload so that pings get sent and archived again. + Preferences.set(uploadPref, true); + Preferences.set(PREF_ARCHIVE_ENABLED, true); + + now = new Date(2014, 6, 18, 22, 0, 0); + fakeNow(now); + // Restore the non asserting ping handler. + PingServer.resetPingHandler(); + pingId = yield sendPing(true, true); + + // Check that we archive pings when successfully sending them. + yield PingServer.promiseNextPing(); + ping = yield TelemetryArchive.promiseArchivedPingById(pingId); + Assert.equal(ping.id, pingId, + "TelemetryController should still archive pings if ping upload is enabled."); +}); + +// Test that we fuzz the submission time around midnight properly +// to avoid overloading the telemetry servers. +add_task(function* test_midnightPingSendFuzzing() { + const fuzzingDelay = 60 * 60 * 1000; + fakeMidnightPingFuzzingDelay(fuzzingDelay); + let now = new Date(2030, 5, 1, 11, 0, 0); + fakeNow(now); + + let waitForTimer = () => new Promise(resolve => { + fakePingSendTimer((callback, timeout) => { + resolve([callback, timeout]); + }, () => {}); + }); + + PingServer.clearRequests(); + yield TelemetryController.testReset(); + + // A ping after midnight within the fuzzing delay should not get sent. + now = new Date(2030, 5, 2, 0, 40, 0); + fakeNow(now); + PingServer.registerPingHandler((req, res) => { + Assert.ok(false, "No ping should be received yet."); + }); + let timerPromise = waitForTimer(); + yield sendPing(true, true); + let [timerCallback, timerTimeout] = yield timerPromise; + Assert.ok(!!timerCallback); + Assert.deepEqual(futureDate(now, timerTimeout), new Date(2030, 5, 2, 1, 0, 0)); + + // A ping just before the end of the fuzzing delay should not get sent. + now = new Date(2030, 5, 2, 0, 59, 59); + fakeNow(now); + timerPromise = waitForTimer(); + yield sendPing(true, true); + [timerCallback, timerTimeout] = yield timerPromise; + Assert.deepEqual(timerTimeout, 1 * 1000); + + // Restore the previous ping handler. + PingServer.resetPingHandler(); + + // Setting the clock to after the fuzzing delay, we should trigger the two ping sends + // with the timer callback. + now = futureDate(now, timerTimeout); + fakeNow(now); + yield timerCallback(); + const pings = yield PingServer.promiseNextPings(2); + for (let ping of pings) { + checkPingFormat(ping, TEST_PING_TYPE, true, true); + } + yield TelemetrySend.testWaitOnOutgoingPings(); + + // Moving the clock further we should still send pings immediately. + now = futureDate(now, 5 * 60 * 1000); + yield sendPing(true, true); + let ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, TEST_PING_TYPE, true, true); + yield TelemetrySend.testWaitOnOutgoingPings(); + + // Check that pings shortly before midnight are immediately sent. + now = fakeNow(2030, 5, 3, 23, 59, 0); + yield sendPing(true, true); + ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, TEST_PING_TYPE, true, true); + yield TelemetrySend.testWaitOnOutgoingPings(); + + // Clean-up. + fakeMidnightPingFuzzingDelay(0); + fakePingSendTimer(() => {}, () => {}); +}); + +add_task(function* test_changePingAfterSubmission() { + // Submit a ping with a custom payload. + let payload = { canary: "test" }; + let pingPromise = TelemetryController.submitExternalPing(TEST_PING_TYPE, payload, options); + + // Change the payload with a predefined value. + payload.canary = "changed"; + + // Wait for the ping to be archived. + const pingId = yield pingPromise; + + // Make sure our changes didn't affect the submitted payload. + let archivedCopy = yield TelemetryArchive.promiseArchivedPingById(pingId); + Assert.equal(archivedCopy.payload.canary, "test", + "The payload must not be changed after being submitted."); +}); + +add_task(function* test_telemetryEnabledUnexpectedValue() { + // Remove the default value for toolkit.telemetry.enabled from the default prefs. + // Otherwise, we wouldn't be able to set the pref to a string. + let defaultPrefBranch = Services.prefs.getDefaultBranch(null); + defaultPrefBranch.deleteBranch(PREF_ENABLED); + + // Set the preferences controlling the Telemetry status to a string. + Preferences.set(PREF_ENABLED, "false"); + // Check that Telemetry is not enabled. + yield TelemetryController.testReset(); + Assert.equal(Telemetry.canRecordExtended, false, + "Invalid values must not enable Telemetry recording."); + + // Delete the pref again. + defaultPrefBranch.deleteBranch(PREF_ENABLED); + + // Make sure that flipping it to true works. + Preferences.set(PREF_ENABLED, true); + yield TelemetryController.testReset(); + Assert.equal(Telemetry.canRecordExtended, true, + "True must enable Telemetry recording."); + + // Also check that the false works as well. + Preferences.set(PREF_ENABLED, false); + yield TelemetryController.testReset(); + Assert.equal(Telemetry.canRecordExtended, false, + "False must disable Telemetry recording."); +}); + +add_task(function* test_telemetryCleanFHRDatabase() { + const FHR_DBNAME_PREF = "datareporting.healthreport.dbName"; + const CUSTOM_DB_NAME = "unlikely.to.be.used.sqlite"; + const DEFAULT_DB_NAME = "healthreport.sqlite"; + + // Check that we're able to remove a FHR DB with a custom name. + const CUSTOM_DB_PATHS = [ + OS.Path.join(OS.Constants.Path.profileDir, CUSTOM_DB_NAME), + OS.Path.join(OS.Constants.Path.profileDir, CUSTOM_DB_NAME + "-wal"), + OS.Path.join(OS.Constants.Path.profileDir, CUSTOM_DB_NAME + "-shm"), + ]; + Preferences.set(FHR_DBNAME_PREF, CUSTOM_DB_NAME); + + // Write fake DB files to the profile directory. + for (let dbFilePath of CUSTOM_DB_PATHS) { + yield OS.File.writeAtomic(dbFilePath, "some data"); + } + + // Trigger the cleanup and check that the files were removed. + yield TelemetryStorage.removeFHRDatabase(); + for (let dbFilePath of CUSTOM_DB_PATHS) { + Assert.ok(!(yield OS.File.exists(dbFilePath)), "The DB must not be on the disk anymore: " + dbFilePath); + } + + // We should not break anything if there's no DB file. + yield TelemetryStorage.removeFHRDatabase(); + + // Check that we're able to remove a FHR DB with the default name. + Preferences.reset(FHR_DBNAME_PREF); + + const DEFAULT_DB_PATHS = [ + OS.Path.join(OS.Constants.Path.profileDir, DEFAULT_DB_NAME), + OS.Path.join(OS.Constants.Path.profileDir, DEFAULT_DB_NAME + "-wal"), + OS.Path.join(OS.Constants.Path.profileDir, DEFAULT_DB_NAME + "-shm"), + ]; + + // Write fake DB files to the profile directory. + for (let dbFilePath of DEFAULT_DB_PATHS) { + yield OS.File.writeAtomic(dbFilePath, "some data"); + } + + // Trigger the cleanup and check that the files were removed. + yield TelemetryStorage.removeFHRDatabase(); + for (let dbFilePath of DEFAULT_DB_PATHS) { + Assert.ok(!(yield OS.File.exists(dbFilePath)), "The DB must not be on the disk anymore: " + dbFilePath); + } +}); + +add_task(function* stopServer() { + yield PingServer.stop(); +}); |