Backed out changeset 8673477f5fc2 (bug 862563) for Windows mochitest-5 failures

This commit is contained in:
Wes Kocher 2014-07-11 17:00:08 -07:00
parent 1346d5a2f8
commit 0c43bada3a
15 changed files with 694 additions and 414 deletions

View File

@ -62,6 +62,11 @@ let gDataNotificationInfoBar = {
accessKey: gNavigatorBundle.getString("dataReportingNotification.button.accessKey"),
popup: null,
callback: function () {
// Clicking the button to go to the preferences tab constitutes
// acceptance of the data upload policy for Firefox Health Report.
// This will ensure the checkbox is checked. The user has the option of
// unchecking it.
request.onUserAccept("info-bar-button-pressed");
this._actionTaken = true;
window.openAdvancedPreferences("dataChoicesTab");
}.bind(this),
@ -76,14 +81,16 @@ let gDataNotificationInfoBar = {
buttons,
function onEvent(event) {
if (event == "removed") {
if (!this._actionTaken) {
request.onUserAccept("info-bar-dismissed");
}
Services.obs.notifyObservers(null, "datareporting:notify-data-policy:close", null);
}
}.bind(this)
);
// It is important to defer calling onUserNotifyComplete() until we're
// actually sure the notification was displayed. If we ever called
// onUserNotifyComplete() without showing anything to the user, that
// would be very good for user choice. It may also have legal impact.
// Tell the notification request we have displayed the notification.
request.onUserNotifyComplete();
},
@ -95,16 +102,18 @@ let gDataNotificationInfoBar = {
}
},
onNotifyDataPolicy: function (request) {
try {
this._displayDataPolicyInfoBar(request);
} catch (ex) {
request.onUserNotifyFailed(ex);
}
},
observe: function(subject, topic, data) {
switch (topic) {
case "datareporting:notify-data-policy:request":
let request = subject.wrappedJSObject.object;
try {
this._displayDataPolicyInfoBar(request);
} catch (ex) {
request.onUserNotifyFailed(ex);
return;
}
this.onNotifyDataPolicy(subject.wrappedJSObject.object);
break;
case "datareporting:notify-data-policy:close":

View File

@ -2,49 +2,30 @@
* 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/. */
let originalPolicy = null;
/**
* Display a datareporting notification to the user.
*
* @param {String} name
*/
function sendNotifyRequest(name) {
let ns = {};
Cu.import("resource://gre/modules/services/datareporting/policy.jsm", ns);
Cu.import("resource://gre/modules/Preferences.jsm", ns);
Components.utils.import("resource://gre/modules/services/datareporting/policy.jsm", ns);
Components.utils.import("resource://gre/modules/Preferences.jsm", ns);
let service = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
let service = Components.classes["@mozilla.org/datareporting/service;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject;
ok(service.healthReporter, "Health Reporter instance is available.");
Cu.import("resource://gre/modules/Promise.jsm", ns);
let deferred = ns.Promise.defer();
if (!originalPolicy) {
originalPolicy = service.policy;
}
let policyPrefs = new ns.Preferences("testing." + name + ".");
ok(service._prefs, "Health Reporter prefs are available.");
let hrPrefs = service._prefs;
let policy = new ns.DataReportingPolicy(policyPrefs, hrPrefs, service);
policy.dataSubmissionPolicyBypassNotification = false;
service.policy = policy;
policy.firstRunDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
service.healthReporter.onInit().then(function onSuccess () {
is(policy.ensureUserNotified(), false, "User not notified about data policy on init.");
ok(policy._userNotifyPromise, "_userNotifyPromise defined.");
policy._userNotifyPromise.then(
deferred.resolve.bind(deferred),
deferred.reject.bind(deferred)
);
}.bind(this), deferred.reject.bind(deferred));
is(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED, "Policy is in unnotified state.");
return [policy, deferred.promise];
service.healthReporter.onInit().then(function onInit() {
is(policy.ensureNotifyResponse(new Date()), false, "User has not responded to policy.");
});
return policy;
}
/**
@ -74,7 +55,6 @@ function waitForNotificationClose(notification, cb) {
let dumpAppender, rootLogger;
function test() {
registerCleanupFunction(cleanup);
waitForExplicitFinish();
let ns = {};
@ -84,41 +64,29 @@ function test() {
dumpAppender.level = ns.Log.Level.All;
rootLogger.addAppender(dumpAppender);
closeAllNotifications().then(function onSuccess () {
let notification = document.getElementById("global-notificationbox");
let notification = document.getElementById("global-notificationbox");
let policy;
notification.addEventListener("AlertActive", function active() {
notification.removeEventListener("AlertActive", active, true);
is(notification.allNotifications.length, 1, "Notification Displayed.");
notification.addEventListener("AlertActive", function active() {
notification.removeEventListener("AlertActive", active, true);
executeSoon(function afterNotification() {
waitForNotificationClose(notification.currentNotification, function onClose() {
is(notification.allNotifications.length, 0, "No notifications remain.");
is(policy.dataSubmissionPolicyAcceptedVersion, 1, "Version pref set.");
ok(policy.dataSubmissionPolicyNotifiedDate.getTime() > -1, "Date pref set.");
test_multiple_windows();
});
notification.currentNotification.close();
executeSoon(function afterNotification() {
is(policy.notifyState, policy.STATE_NOTIFY_WAIT, "Policy is waiting for user response.");
ok(!policy.dataSubmissionPolicyAccepted, "Data submission policy not yet accepted.");
waitForNotificationClose(notification.currentNotification, function onClose() {
is(policy.notifyState, policy.STATE_NOTIFY_COMPLETE, "Closing info bar completes user notification.");
ok(policy.dataSubmissionPolicyAccepted, "Data submission policy accepted.");
is(policy.dataSubmissionPolicyResponseType, "accepted-info-bar-dismissed",
"Reason for acceptance was info bar dismissal.");
is(notification.allNotifications.length, 0, "No notifications remain.");
test_multiple_windows();
});
}, true);
let [policy, promise] = sendNotifyRequest("single_window_notified");
is(policy.dataSubmissionPolicyAcceptedVersion, 0, "No version should be set on init.");
is(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0, "No date should be set on init.");
is(policy.userNotifiedOfCurrentPolicy, false, "User not notified about datareporting policy.");
promise.then(function () {
is(policy.dataSubmissionPolicyAcceptedVersion, 1, "Policy version set.");
is(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0, true, "Policy date set.");
is(policy.userNotifiedOfCurrentPolicy, true, "User notified about datareporting policy.");
}.bind(this), function (err) {
throw err;
notification.currentNotification.close();
});
}, true);
}.bind(this), function onError (err) {
throw err;
});
policy = sendNotifyRequest("single_window_notified");
}
function test_multiple_windows() {
@ -130,7 +98,7 @@ function test_multiple_windows() {
let notification2 = window2.document.getElementById("global-notificationbox");
ok(notification2, "2nd window has a global notification box.");
let [policy, promise] = sendNotifyRequest("multiple_window_behavior");
let policy;
let displayCount = 0;
let prefWindowClosed = false;
let mutationObserversRemoved = false;
@ -161,8 +129,8 @@ function test_multiple_windows() {
dump("Finishing multiple window test.\n");
rootLogger.removeAppender(dumpAppender);
dumpAppender = null;
rootLogger = null;
delete dumpAppender;
delete rootLogger;
finish();
}
let closeCount = 0;
@ -175,8 +143,12 @@ function test_multiple_windows() {
}
ok(true, "Closing info bar on one window closed them on all.");
is(policy.userNotifiedOfCurrentPolicy, true, "Data submission policy accepted.");
is(policy.notifyState, policy.STATE_NOTIFY_COMPLETE,
"Closing info bar with multiple windows completes notification.");
ok(policy.dataSubmissionPolicyAccepted, "Data submission policy accepted.");
is(policy.dataSubmissionPolicyResponseType, "accepted-info-bar-button-pressed",
"Policy records reason for acceptance was button press.");
is(notification1.allNotifications.length, 0, "No notifications remain on main window.");
is(notification2.allNotifications.length, 0, "No notifications remain on 2nd window.");
@ -220,20 +192,7 @@ function test_multiple_windows() {
executeSoon(onAlertDisplayed);
}, true);
promise.then(null, function onError(err) {
throw err;
});
policy = sendNotifyRequest("multiple_window_behavior");
});
}
function cleanup () {
// In case some test fails.
if (originalPolicy) {
let service = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
service.policy = originalPolicy;
}
return closeAllNotifications();
}

View File

@ -7,26 +7,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
function closeAllNotifications () {
let notificationBox = document.getElementById("global-notificationbox");
if (!notificationBox || !notificationBox.currentNotification) {
return Promise.resolve();
}
let deferred = Promise.defer();
for (let notification of notificationBox.allNotifications) {
waitForNotificationClose(notification, function () {
if (notificationBox.allNotifications.length === 0) {
deferred.resolve();
}
});
notification.close();
}
return deferred.promise;
}
function whenDelayedStartupFinished(aWindow, aCallback) {
Services.obs.addObserver(function observer(aSubject, aTopic) {
if (aWindow == aSubject) {

View File

@ -29,6 +29,7 @@ function test() {
}
function testBasic(win, doc, policy) {
is(policy.dataSubmissionPolicyAccepted, false, "Data submission policy not accepted.");
is(policy.healthReportUploadEnabled, true, "Health Report upload enabled on app first run.");
let checkbox = doc.getElementById("submitHealthReportBox");

View File

@ -13,8 +13,6 @@ function runPaneTest(fn) {
.policy;
ok(policy, "Policy object defined");
resetPreferences();
fn(win, policy);
}
@ -23,30 +21,11 @@ function runPaneTest(fn) {
"chrome,titlebar,toolbar,centerscreen,dialog=no", "paneAdvanced");
}
let logDetails = {
dumpAppender: null,
rootLogger: null,
};
function test() {
waitForExplicitFinish();
resetPreferences();
registerCleanupFunction(resetPreferences);
let ld = logDetails;
registerCleanupFunction(() => {
ld.rootLogger.removeAppender(ld.dumpAppender);
delete ld.dumpAppender;
delete ld.rootLogger;
});
let ns = {};
Cu.import("resource://gre/modules/Log.jsm", ns);
ld.rootLogger = ns.Log.repository.rootLogger;
ld.dumpAppender = new ns.Log.DumpAppender();
ld.dumpAppender.level = ns.Log.Level.All;
ld.rootLogger.addAppender(ld.dumpAppender);
Services.prefs.lockPref("datareporting.healthreport.uploadEnabled");
runPaneTest(testUploadDisabled);
}
@ -64,8 +43,7 @@ function testUploadDisabled(win, policy) {
function testBasic(win, policy) {
let doc = win.document;
resetPreferences();
is(policy.dataSubmissionPolicyAccepted, false, "Data submission policy not accepted.");
is(policy.healthReportUploadEnabled, true, "Health Report upload enabled on app first run.");
let checkbox = doc.getElementById("submitHealthReportBox");
@ -85,10 +63,6 @@ function testBasic(win, policy) {
}
function resetPreferences() {
let service = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
service.policy._prefs.resetBranch("datareporting.policy.");
service.policy.dataSubmissionPolicyBypassNotification = true;
Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled");
}

View File

@ -6,7 +6,6 @@
function test() {
requestLongerTimeout(2);
waitForExplicitFinish();
resetPreferences();
try {
let cm = Components.classes["@mozilla.org/categorymanager;1"]
@ -102,10 +101,3 @@ function test() {
}
function resetPreferences() {
let service = Components.classes["@mozilla.org/datareporting/service;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject;
service.policy._prefs.resetBranch("datareporting.policy.");
service.policy.dataSubmissionPolicyBypassNotification = true;
}

View File

@ -175,7 +175,6 @@ DataReportingService.prototype = Object.freeze({
// The instance installs its own shutdown observers. So, we just
// fire and forget: it will clean itself up.
let reporter = this.healthReporter;
this.policy.ensureUserNotified();
}.bind(this),
}, delayInterval, this.timer.TYPE_ONE_SHOT);

View File

@ -3,10 +3,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
pref("datareporting.policy.dataSubmissionEnabled", true);
pref("datareporting.policy.firstRunTime", "0");
pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
pref("datareporting.policy.dataSubmissionPolicyBypassAcceptance", false);
pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "0");
pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 0);
pref("datareporting.policy.dataSubmissionPolicyBypassNotification", false);
pref("datareporting.policy.dataSubmissionPolicyResponseType", "");
pref("datareporting.policy.dataSubmissionPolicyResponseTime", "0");
pref("datareporting.policy.firstRunTime", "0");
pref("datareporting.policy.currentPolicyVersion", 2);
pref("datareporting.policy.minimumPolicyVersion", 1);
pref("datareporting.policy.minimumPolicyVersion.channel-beta", 2);

View File

@ -26,27 +26,22 @@ this.MockPolicyListener = function MockPolicyListener() {
}
MockPolicyListener.prototype = {
onRequestDataUpload: function (request) {
onRequestDataUpload: function onRequestDataUpload(request) {
this._log.info("onRequestDataUpload invoked.");
this.requestDataUploadCount++;
this.lastDataRequest = request;
},
onRequestRemoteDelete: function (request) {
onRequestRemoteDelete: function onRequestRemoteDelete(request) {
this._log.info("onRequestRemoteDelete invoked.");
this.requestRemoteDeleteCount++;
this.lastRemoteDeleteRequest = request;
},
onNotifyDataPolicy: function (request, rejectMessage=null) {
this._log.info("onNotifyDataPolicy invoked.");
onNotifyDataPolicy: function onNotifyDataPolicy(request) {
this._log.info("onNotifyUser invoked.");
this.notifyUserCount++;
this.lastNotifyRequest = request;
if (rejectMessage) {
request.onUserNotifyFailed(rejectMessage);
} else {
request.onUserNotifyComplete();
}
},
};

View File

@ -3,8 +3,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This file is in transition. Most of its content needs to be moved under
* /services/healthreport.
* This file is in transition. It was originally conceived to fulfill the
* needs of only Firefox Health Report. It is slowly being morphed into
* fulfilling the needs of all data reporting facilities in Gecko applications.
* As a result, some things feel a bit weird.
*
* DataReportingPolicy is both a driver for data reporting notification
* (a true policy) and the driver for FHR data submission. The latter should
* eventually be split into its own type and module.
*/
"use strict";
@ -14,7 +20,6 @@
this.EXPORTED_SYMBOLS = [
"DataSubmissionRequest", // For test use only.
"DataReportingPolicy",
"DATAREPORTING_POLICY_VERSION",
];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
@ -27,11 +32,6 @@ Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://gre/modules/UpdateChannel.jsm");
// The current policy version number. If the version number stored in the prefs
// is smaller than this, data upload will be disabled until the user is re-notified
// about the policy changes.
const DATAREPORTING_POLICY_VERSION = 1;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
// Used as a sanity lower bound for dates stored in prefs. This module was
@ -41,15 +41,33 @@ const OLDEST_ALLOWED_YEAR = 2012;
/**
* Represents a request to display data policy.
*
* Instances of this are created when the policy is requesting the user's
* approval to agree to the data submission policy.
*
* Receivers of these instances are expected to call one or more of the on*
* functions when events occur.
*
* When one of these requests is received, the first thing a callee should do
* is present notification to the user of the data policy. When the notice
* is displayed to the user, the callee should call `onUserNotifyComplete`.
* This begins a countdown timer that upon completion will signal implicit
* acceptance of the policy. If for whatever reason the callee could not
* display a notice, it should call `onUserNotifyFailed`.
*
* If for whatever reason the callee could not display a notice,
* it should call `onUserNotifyFailed`.
* Once the user is notified of the policy, the callee has the option of
* signaling explicit user acceptance or rejection of the policy. They do this
* by calling `onUserAccept` or `onUserReject`, respectively. These functions
* are essentially proxies to
* DataReportingPolicy.{recordUserAcceptance,recordUserRejection}.
*
* If the user never explicitly accepts or rejects the policy, it will be
* implicitly accepted after a specified duration of time. The notice is
* expected to remain displayed even after implicit acceptance (in case the
* user is away from the device). So, no event signaling implicit acceptance
* is exposed.
*
* Receivers of instances of this type should treat it as a black box with
* the exception of the on* functions.
*
* @param policy
* (DataReportingPolicy) The policy instance this request came from.
@ -60,13 +78,17 @@ function NotifyPolicyRequest(policy, deferred) {
this.policy = policy;
this.deferred = deferred;
}
NotifyPolicyRequest.prototype = Object.freeze({
NotifyPolicyRequest.prototype = {
/**
* Called when the user is notified of the policy.
*
* This starts a countdown timer that will eventually signify implicit
* acceptance of the data policy.
*/
onUserNotifyComplete: function () {
return this.deferred.resolve();
},
onUserNotifyComplete: function onUserNotified() {
this.deferred.resolve();
return this.deferred.promise;
},
/**
* Called when there was an error notifying the user about the policy.
@ -74,10 +96,32 @@ NotifyPolicyRequest.prototype = Object.freeze({
* @param error
* (Error) Explains what went wrong.
*/
onUserNotifyFailed: function (error) {
return this.deferred.reject(error);
onUserNotifyFailed: function onUserNotifyFailed(error) {
this.deferred.reject(error);
},
});
/**
* Called when the user agreed to the data policy.
*
* @param reason
* (string) How the user agreed to the policy.
*/
onUserAccept: function onUserAccept(reason) {
this.policy.recordUserAcceptance(reason);
},
/**
* Called when the user rejected the data policy.
*
* @param reason
* (string) How the user rejected the policy.
*/
onUserReject: function onUserReject(reason) {
this.policy.recordUserRejection(reason);
},
};
Object.freeze(NotifyPolicyRequest.prototype);
/**
* Represents a request to submit data.
@ -194,7 +238,9 @@ this.DataSubmissionRequest.prototype = Object.freeze({
* data collection practices.
* 5. User should have opportunity to react to this notification before
* data submission.
* 6. If data submission fails, try at most 2 additional times before giving
* 6. Display of notification without any explicit user action constitutes
* implicit consent after a certain duration of time.
* 7. If data submission fails, try at most 2 additional times before giving
* up on that day's submission.
*
* The listener passed into the instance must have the following properties
@ -243,9 +289,6 @@ this.DataReportingPolicy = function (prefs, healthReportPrefs, listener) {
this._prefs = prefs;
this._healthReportPrefs = healthReportPrefs;
this._listener = listener;
this._userNotifyPromise = null;
this._migratePrefs();
// If the policy version has changed, reset all preferences, so that
// the notification reappears.
@ -286,12 +329,30 @@ this.DataReportingPolicy = function (prefs, healthReportPrefs, listener) {
this.nextDataSubmissionDate = this._futureDate(MILLISECONDS_PER_DAY);
}
// Date at which we performed user notification of acceptance.
// This is an instance variable because implicit acceptance should only
// carry forward through a single application instance.
this._dataSubmissionPolicyNotifiedDate = null;
// Record when we last requested for submitted data to be sent. This is
// to avoid having multiple outstanding requests.
this._inProgressSubmissionRequest = null;
};
this.DataReportingPolicy.prototype = Object.freeze({
/**
* How long after first run we should notify about data submission.
*/
SUBMISSION_NOTIFY_INTERVAL_MSEC: 12 * 60 * 60 * 1000,
/**
* Time that must elapse with no user action for implicit acceptance.
*
* THERE ARE POTENTIAL LEGAL IMPLICATIONS OF CHANGING THIS VALUE. Check with
* Privacy and/or Legal before modifying.
*/
IMPLICIT_ACCEPTANCE_INTERVAL_MSEC: 8 * 60 * 60 * 1000,
/**
* How often to poll to see if we need to do something.
*
@ -332,6 +393,13 @@ this.DataReportingPolicy.prototype = Object.freeze({
60 * 60 * 1000,
],
/**
* State of user notification of data submission.
*/
STATE_NOTIFY_UNNOTIFIED: "not-notified",
STATE_NOTIFY_WAIT: "waiting",
STATE_NOTIFY_COMPLETE: "ok",
REQUIRED_LISTENERS: [
"onRequestDataUpload",
"onRequestRemoteDelete",
@ -354,6 +422,22 @@ this.DataReportingPolicy.prototype = Object.freeze({
OLDEST_ALLOWED_YEAR);
},
/**
* Short circuit policy checking and always assume acceptance.
*
* This shuld never be set by the user. Instead, it is a per-application or
* per-deployment default pref.
*/
get dataSubmissionPolicyBypassAcceptance() {
return this._prefs.get("dataSubmissionPolicyBypassAcceptance", false);
},
/**
* When the user was notified that data submission could occur.
*
* This is used for logging purposes. this._dataSubmissionPolicyNotifiedDate
* is what's used internally.
*/
get dataSubmissionPolicyNotifiedDate() {
return CommonUtils.getDatePref(this._prefs,
"dataSubmissionPolicyNotifiedTime", 0,
@ -366,12 +450,46 @@ this.DataReportingPolicy.prototype = Object.freeze({
value, OLDEST_ALLOWED_YEAR);
},
get dataSubmissionPolicyBypassNotification() {
return this._prefs.get("dataSubmissionPolicyBypassNotification", false);
/**
* When the user accepted or rejected the data submission policy.
*
* If there was implicit acceptance, this will be set to the time of that.
*/
get dataSubmissionPolicyResponseDate() {
return CommonUtils.getDatePref(this._prefs,
"dataSubmissionPolicyResponseTime",
0, this._log, OLDEST_ALLOWED_YEAR);
},
set dataSubmissionPolicyBypassNotification(value) {
return this._prefs.set("dataSubmissionPolicyBypassNotification", !!value);
set dataSubmissionPolicyResponseDate(value) {
this._log.debug("Setting user notified reaction date: " + value);
CommonUtils.setDatePref(this._prefs,
"dataSubmissionPolicyResponseTime",
value, OLDEST_ALLOWED_YEAR);
},
/**
* Records the result of user notification of data submission policy.
*
* This is used for logging and diagnostics purposes. It can answer the
* question "how was data submission agreed to on this profile?"
*
* Not all values are defined by this type and can come from other systems.
*
* The value must be a string and should be something machine readable. e.g.
* "accept-user-clicked-ok-button-in-info-bar"
*/
get dataSubmissionPolicyResponseType() {
return this._prefs.get("dataSubmissionPolicyResponseType",
"none-recorded");
},
set dataSubmissionPolicyResponseType(value) {
if (typeof(value) != "string") {
throw new Error("Value must be a string. Got " + typeof(value));
}
this._prefs.set("dataSubmissionPolicyResponseType", value);
},
/**
@ -389,10 +507,6 @@ this.DataReportingPolicy.prototype = Object.freeze({
this._prefs.set("dataSubmissionEnabled", !!value);
},
get currentPolicyVersion() {
return this._prefs.get("currentPolicyVersion", DATAREPORTING_POLICY_VERSION);
},
/**
* The minimum policy version which for dataSubmissionPolicyAccepted to
* to be valid.
@ -405,21 +519,48 @@ this.DataReportingPolicy.prototype = Object.freeze({
channelPref : this._prefs.get("minimumPolicyVersion", 1);
},
get dataSubmissionPolicyAcceptedVersion() {
return this._prefs.get("dataSubmissionPolicyAcceptedVersion", 0);
/**
* Whether the user has accepted that data submission can occur.
*
* This overrides dataSubmissionEnabled.
*/
get dataSubmissionPolicyAccepted() {
// Be conservative and default to false.
return this._prefs.get("dataSubmissionPolicyAccepted", false);
},
set dataSubmissionPolicyAcceptedVersion(value) {
this._prefs.set("dataSubmissionPolicyAcceptedVersion", value);
set dataSubmissionPolicyAccepted(value) {
this._prefs.set("dataSubmissionPolicyAccepted", !!value);
if (!!value) {
let currentPolicyVersion = this._prefs.get("currentPolicyVersion", 1);
this._prefs.set("dataSubmissionPolicyAcceptedVersion", currentPolicyVersion);
} else {
this._prefs.reset("dataSubmissionPolicyAcceptedVersion");
}
},
/**
* Checks to see if the user has been notified about data submission
* @return {bool}
* The state of user notification of the data policy.
*
* This must be DataReportingPolicy.STATE_NOTIFY_COMPLETE before data
* submission can occur.
*
* @return DataReportingPolicy.STATE_NOTIFY_* constant.
*/
get userNotifiedOfCurrentPolicy() {
return this.dataSubmissionPolicyNotifiedDate.getTime() > 0 &&
this.dataSubmissionPolicyAcceptedVersion >= this.currentPolicyVersion;
get notifyState() {
if (this.dataSubmissionPolicyResponseDate.getTime()) {
return this.STATE_NOTIFY_COMPLETE;
}
// We get the local state - not the state from prefs - because we don't want
// a value from a previous application run to interfere. This prevents
// a scenario where notification occurs just before application shutdown and
// notification is displayed for shorter than the policy requires.
if (!this._dataSubmissionPolicyNotifiedDate) {
return this.STATE_NOTIFY_UNNOTIFIED;
}
return this.STATE_NOTIFY_WAIT;
},
/**
@ -552,6 +693,43 @@ this.DataReportingPolicy.prototype = Object.freeze({
return this._healthReportPrefs.locked("uploadEnabled");
},
/**
* Record user acceptance of data submission policy.
*
* Data submission will not be allowed to occur until this is called.
*
* This is typically called through the `onUserAccept` property attached to
* the promise passed to `onUserNotify` in the policy listener. But, it can
* be called through other interfaces at any time and the call will have
* an impact on future data submissions.
*
* @param reason
* (string) How the user accepted the data submission policy.
*/
recordUserAcceptance: function recordUserAcceptance(reason="no-reason") {
this._log.info("User accepted data submission policy: " + reason);
this.dataSubmissionPolicyResponseDate = this.now();
this.dataSubmissionPolicyResponseType = "accepted-" + reason;
this.dataSubmissionPolicyAccepted = true;
},
/**
* Record user rejection of submission policy.
*
* Data submission will not be allowed to occur if this is called.
*
* This is typically called through the `onUserReject` property attached to
* the promise passed to `onUserNotify` in the policy listener. But, it can
* be called through other interfaces at any time and the call will have an
* impact on future data submissions.
*/
recordUserRejection: function recordUserRejection(reason="no-reason") {
this._log.info("User rejected data submission policy: " + reason);
this.dataSubmissionPolicyResponseDate = this.now();
this.dataSubmissionPolicyResponseType = "rejected-" + reason;
this.dataSubmissionPolicyAccepted = false;
},
/**
* Record the user's intent for whether FHR should upload data.
*
@ -704,13 +882,19 @@ this.DataReportingPolicy.prototype = Object.freeze({
return;
}
if (!this.ensureUserNotified()) {
this._log.warn("The user has not been notified about the data submission " +
"policy. Not attempting upload.");
// If the user hasn't responded to the data policy, don't do anything.
if (!this.ensureNotifyResponse(now)) {
return;
}
// Data submission is allowed to occur. Now comes the scheduling part.
// User has opted out of data submission.
if (!this.dataSubmissionPolicyAccepted && !this.dataSubmissionPolicyBypassAcceptance) {
this._log.debug("Data submission has been disabled per user request.");
return;
}
// User has responded to data policy and data submission is enabled. Now
// comes the scheduling part.
if (nowT < nextSubmissionDate.getTime()) {
this._log.debug("Next data submission is scheduled in the future: " +
@ -722,62 +906,82 @@ this.DataReportingPolicy.prototype = Object.freeze({
},
/**
* Ensure that the data policy notification has been displayed.
* Ensure user has responded to data submission policy.
*
* This must be called before data submission. If the policy has not been
* displayed, data submission must not occur.
* responded to, data submission must not occur.
*
* @return bool Whether the notification has been displayed.
* @return bool Whether user has responded to data policy.
*/
ensureUserNotified: function () {
if (this.userNotifiedOfCurrentPolicy || this.dataSubmissionPolicyBypassNotification) {
ensureNotifyResponse: function ensureNotifyResponse(now) {
if (this.dataSubmissionPolicyBypassAcceptance) {
return true;
}
// The user has not been notified yet, but is in the process of being notified.
if (this._userNotifyPromise) {
let notifyState = this.notifyState;
if (notifyState == this.STATE_NOTIFY_UNNOTIFIED) {
let notifyAt = new Date(this.firstRunDate.getTime() +
this.SUBMISSION_NOTIFY_INTERVAL_MSEC);
if (now.getTime() < notifyAt.getTime()) {
this._log.debug("Don't have to notify about data submission yet.");
return false;
}
let onComplete = function onComplete() {
this._log.info("Data submission notification presented.");
let now = this.now();
this._dataSubmissionPolicyNotifiedDate = now;
this.dataSubmissionPolicyNotifiedDate = now;
}.bind(this);
let deferred = Promise.defer();
deferred.promise.then(onComplete, (error) => {
this._log.warn("Data policy notification presentation failed: " +
CommonUtils.exceptionStr(error));
});
this._log.info("Requesting display of data policy.");
let request = new NotifyPolicyRequest(this, deferred);
try {
this._listener.onNotifyDataPolicy(request);
} catch (ex) {
this._log.warn("Exception when calling onNotifyDataPolicy: " +
CommonUtils.exceptionStr(ex));
}
return false;
}
let deferred = Promise.defer();
deferred.promise.then((function onSuccess() {
this._recordDataPolicyNotification(this.now(), this.currentPolicyVersion);
this._userNotifyPromise = null;
}).bind(this), ((error) => {
this._log.warn("Data policy notification presentation failed: " +
CommonUtils.exceptionStr(error));
this._userNotifyPromise = null;
}).bind(this));
// We're waiting for user action or implicit acceptance after display.
if (notifyState == this.STATE_NOTIFY_WAIT) {
// Check for implicit acceptance.
let implicitAcceptance =
this._dataSubmissionPolicyNotifiedDate.getTime() +
this.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC;
this._log.info("Requesting display of data policy.");
let request = new NotifyPolicyRequest(this, deferred);
try {
this._listener.onNotifyDataPolicy(request);
} catch (ex) {
this._log.warn("Exception when calling onNotifyDataPolicy: " +
CommonUtils.exceptionStr(ex));
this._log.debug("Now: " + now.getTime());
this._log.debug("Will accept: " + implicitAcceptance);
if (now.getTime() < implicitAcceptance) {
this._log.debug("Still waiting for reaction or implicit acceptance. " +
"Now: " + now.getTime() + " < " +
"Accept: " + implicitAcceptance);
return false;
}
this.recordUserAcceptance("implicit-time-elapsed");
return true;
}
this._userNotifyPromise = deferred.promise;
// If this happens, we have a coding error in this file.
if (notifyState != this.STATE_NOTIFY_COMPLETE) {
throw new Error("Unknown notification state: " + notifyState);
}
return false;
},
_recordDataPolicyNotification: function (date, version) {
this._log.debug("Recording data policy notification to version " + version +
" on date " + date);
this.dataSubmissionPolicyNotifiedDate = date;
this.dataSubmissionPolicyAcceptedVersion = version;
},
_migratePrefs: function () {
// Current prefs are mostly the same than the old ones, except for some deprecated ones.
this._prefs.reset([
"dataSubmissionPolicyAccepted",
"dataSubmissionPolicyBypassAcceptance",
"dataSubmissionPolicyResponseType",
"dataSubmissionPolicyResponseTime"
]);
return true;
},
_processInProgressSubmission: function _processInProgressSubmission() {

View File

@ -9,7 +9,6 @@ Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
Cu.import("resource://testing-common/services/datareporting/mocks.jsm");
Cu.import("resource://gre/modules/UpdateChannel.jsm");
Cu.import("resource://gre/modules/Task.jsm");
function getPolicy(name,
aCurrentPolicyVersion = 1,
@ -38,22 +37,6 @@ function getPolicy(name,
return [policy, policyPrefs, healthReportPrefs, listener];
}
/**
* Ensure that the notification has been displayed to the user therefore having
* policy.ensureUserNotified() === true, which will allow for a successful
* data upload and afterwards does a call to policy.checkStateAndTrigger()
* @param {Policy} policy
* @return {Promise}
*/
function ensureUserNotifiedAndTrigger(policy) {
return Task.spawn(function* ensureUserNotifiedAndTrigger () {
policy.ensureUserNotified();
yield policy._listener.lastNotifyRequest.deferred.promise;
do_check_true(policy.userNotifiedOfCurrentPolicy);
policy.checkStateAndTrigger();
});
}
function defineNow(policy, now) {
print("Adjusting fake system clock to " + now);
Object.defineProperty(policy, "now", {
@ -83,8 +66,7 @@ add_test(function test_constructor() {
let tomorrow = Date.now() + 24 * 60 * 60 * 1000;
do_check_true(tomorrow - policy.nextDataSubmissionDate.getTime() < 1000);
do_check_eq(policy.dataSubmissionPolicyAcceptedVersion, 0);
do_check_false(policy.userNotifiedOfCurrentPolicy);
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
run_next_test();
});
@ -99,23 +81,29 @@ add_test(function test_prefs() {
do_check_eq(policyPrefs.get("firstRunTime"), nowT);
do_check_eq(policy.firstRunDate.getTime(), nowT);
policy.dataSubmissionPolicyNotifiedDate = now;
policy.dataSubmissionPolicyNotifiedDate= now;
do_check_eq(policyPrefs.get("dataSubmissionPolicyNotifiedTime"), nowT);
do_check_neq(policy.dataSubmissionPolicyNotifiedDate, null);
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), nowT);
policy.dataSubmissionPolicyResponseDate = now;
do_check_eq(policyPrefs.get("dataSubmissionPolicyResponseTime"), nowT);
do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), nowT);
policy.dataSubmissionPolicyResponseType = "type-1";
do_check_eq(policyPrefs.get("dataSubmissionPolicyResponseType"), "type-1");
do_check_eq(policy.dataSubmissionPolicyResponseType, "type-1");
policy.dataSubmissionEnabled = false;
do_check_false(policyPrefs.get("dataSubmissionEnabled", true));
do_check_false(policy.dataSubmissionEnabled);
let new_version = DATAREPORTING_POLICY_VERSION + 1;
policy.dataSubmissionPolicyAcceptedVersion = new_version;
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"), new_version);
policy.dataSubmissionPolicyAccepted = false;
do_check_false(policyPrefs.get("dataSubmissionPolicyAccepted", true));
do_check_false(policy.dataSubmissionPolicyAccepted);
do_check_false(policy.dataSubmissionPolicyBypassNotification);
policy.dataSubmissionPolicyBypassNotification = true;
do_check_true(policy.dataSubmissionPolicyBypassNotification);
do_check_true(policyPrefs.get("dataSubmissionPolicyBypassNotification"));
do_check_false(policy.dataSubmissionPolicyBypassAcceptance);
policyPrefs.set("dataSubmissionPolicyBypassAcceptance", true);
do_check_true(policy.dataSubmissionPolicyBypassAcceptance);
policy.lastDataSubmissionRequestedDate = now;
do_check_eq(hrPrefs.get("lastDataSubmissionRequestedTime"), nowT);
@ -154,78 +142,153 @@ add_test(function test_prefs() {
run_next_test();
});
add_task(function test_migratePrefs () {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("migratePrefs");
let outdated_prefs = {
dataSubmissionPolicyAccepted: true,
dataSubmissionPolicyBypassAcceptance: true,
dataSubmissionPolicyResponseType: "something",
dataSubmissionPolicyResponseTime: Date.now() + "",
};
add_test(function test_notify_state_prefs() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notify_state_prefs");
// Test removal of old prefs.
for (let name in outdated_prefs) {
policyPrefs.set(name, outdated_prefs[name]);
}
policy._migratePrefs();
for (let name in outdated_prefs) {
do_check_false(policyPrefs.has(name));
}
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
policy._dataSubmissionPolicyNotifiedDate = new Date();
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
policy.dataSubmissionPolicyResponseDate = new Date();
policy._dataSubmissionPolicyNotifiedDate = null;
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
run_next_test();
});
add_task(function test_userNotifiedOfCurrentPolicy () {
add_task(function test_initial_submission_notification() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("initial_submission_notification");
do_check_false(policy.userNotifiedOfCurrentPolicy,
"The initial state should be unnotified.");
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION;
do_check_false(policy.userNotifiedOfCurrentPolicy,
"The default state of the date should have a time of 0 and it should therefore fail");
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0,
"Updating the accepted version should not set a notified date.");
policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
do_check_true(policy.userNotifiedOfCurrentPolicy,
"Using the proper API causes user notification to report as true.");
// It is assumed that later versions of the policy will incorporate previous
// ones, therefore this should also return true.
policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION + 1;
do_check_true(policy.userNotifiedOfCurrentPolicy, 'A future version of the policy should pass.');
policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION - 1;
do_check_false(policy.userNotifiedOfCurrentPolicy, 'A previous version of the policy should fail.');
});
add_task(function* test_notification_displayed () {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_accept_displayed");
do_check_eq(listener.requestDataUploadCount, 0);
do_check_eq(listener.notifyUserCount, 0);
// Fresh instances should not do anything initially.
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 0);
// We still shouldn't notify up to the millisecond before the barrier.
defineNow(policy, new Date(policy.firstRunDate.getTime() +
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC - 1));
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 0);
do_check_null(policy._dataSubmissionPolicyNotifiedDate);
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
// Uploads will trigger user notifications as needed.
// We have crossed the threshold. We should see notification.
defineNow(policy, new Date(policy.firstRunDate.getTime() +
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC));
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 1);
do_check_eq(listener.requestDataUploadCount, 0);
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.notifyUserCount, 1);
yield listener.lastNotifyRequest.onUserNotifyComplete();
do_check_true(policy._dataSubmissionPolicyNotifiedDate instanceof Date);
do_check_true(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0);
do_check_true(policy.userNotifiedOfCurrentPolicy);
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(),
policy._dataSubmissionPolicyNotifiedDate.getTime());
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
});
add_task(function* test_submission_kill_switch() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_kill_switch");
policy.nextDataSubmissionDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
add_test(function test_bypass_acceptance() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("bypass_acceptance");
policyPrefs.set("dataSubmissionPolicyBypassAcceptance", true);
do_check_false(policy.dataSubmissionPolicyAccepted);
do_check_true(policy.dataSubmissionPolicyBypassAcceptance);
defineNow(policy, new Date(policy.nextDataSubmissionDate.getTime()));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
run_next_test();
});
add_task(function test_notification_implicit_acceptance() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_implicit_acceptance");
let now = new Date(policy.nextDataSubmissionDate.getTime() -
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
defineNow(policy, now);
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 1);
yield listener.lastNotifyRequest.onUserNotifyComplete();
do_check_eq(policy.dataSubmissionPolicyResponseType, "none-recorded");
do_check_true(5000 < policy.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC);
defineNow(policy, new Date(now.getTime() + 5000));
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 1);
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), 0);
do_check_eq(policy.dataSubmissionPolicyResponseType, "none-recorded");
defineNow(policy, new Date(now.getTime() + policy.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC + 1));
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 1);
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), policy.now().getTime());
do_check_eq(policy.dataSubmissionPolicyResponseType, "accepted-implicit-time-elapsed");
});
add_task(function test_notification_rejected() {
// User notification failed. We should not record it as being presented.
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_failed");
let now = new Date(policy.nextDataSubmissionDate.getTime() -
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
defineNow(policy, now);
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 1);
yield listener.lastNotifyRequest.onUserNotifyFailed(new Error("testing failed."));
do_check_null(policy._dataSubmissionPolicyNotifiedDate);
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
});
add_task(function test_notification_accepted() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_accepted");
let now = new Date(policy.nextDataSubmissionDate.getTime() -
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
defineNow(policy, now);
policy.checkStateAndTrigger();
yield listener.lastNotifyRequest.onUserNotifyComplete();
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
do_check_false(policy.dataSubmissionPolicyAccepted);
listener.lastNotifyRequest.onUserNotifyComplete();
listener.lastNotifyRequest.onUserAccept("foo-bar");
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
do_check_eq(policy.dataSubmissionPolicyResponseType, "accepted-foo-bar");
do_check_true(policy.dataSubmissionPolicyAccepted);
do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), now.getTime());
});
add_task(function test_notification_rejected() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_rejected");
let now = new Date(policy.nextDataSubmissionDate.getTime() -
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
defineNow(policy, now);
policy.checkStateAndTrigger();
yield listener.lastNotifyRequest.onUserNotifyComplete();
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
do_check_false(policy.dataSubmissionPolicyAccepted);
listener.lastNotifyRequest.onUserReject();
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
do_check_eq(policy.dataSubmissionPolicyResponseType, "rejected-no-reason");
do_check_false(policy.dataSubmissionPolicyAccepted);
// No requests for submission should occur if user has rejected.
defineNow(policy, new Date(policy.nextDataSubmissionDate.getTime() + 10000));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 0);
yield ensureUserNotifiedAndTrigger(policy);
});
add_test(function test_submission_kill_switch() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_kill_switch");
policy.firstRunDate = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
policy.nextDataSubmissionDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.recordUserAcceptance("accept-old-ack");
do_check_true(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
defineNow(policy,
@ -233,32 +296,39 @@ add_task(function* test_submission_kill_switch() {
policy.dataSubmissionEnabled = false;
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
run_next_test();
});
add_task(function* test_upload_kill_switch() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("upload_kill_switch");
add_test(function test_upload_kill_switch() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("upload_kill_switch");
yield ensureUserNotifiedAndTrigger(policy);
defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
policy.recordUserAcceptance();
defineNow(policy, policy.nextDataSubmissionDate);
// So that we don't trigger deletions, which cause uploads to be delayed.
hrPrefs.ignore("uploadEnabled", policy.uploadEnabledObserver);
policy.healthReportUploadEnabled = false;
yield policy.checkStateAndTrigger();
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 0);
policy.healthReportUploadEnabled = true;
yield ensureUserNotifiedAndTrigger(policy);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
run_next_test();
});
add_task(function* test_data_submission_no_data() {
add_test(function test_data_submission_no_data() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_no_data");
policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.dataSubmissionPolicyAccepted = true;
let now = new Date(policy.nextDataSubmissionDate.getTime() + 1);
defineNow(policy, now);
do_check_eq(listener.requestDataUploadCount, 0);
yield ensureUserNotifiedAndTrigger(policy);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
listener.lastDataRequest.onNoDataAvailable();
@ -266,16 +336,20 @@ add_task(function* test_data_submission_no_data() {
defineNow(policy, new Date(now.getTime() + 155 * 60 * 1000));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 2);
});
add_task(function* test_data_submission_submit_failure_hard() {
run_next_test();
});
add_task(function test_data_submission_submit_failure_hard() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_submit_failure_hard");
policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.dataSubmissionPolicyAccepted = true;
let nextDataSubmissionDate = policy.nextDataSubmissionDate;
let now = new Date(policy.nextDataSubmissionDate.getTime() + 1);
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
yield listener.lastDataRequest.onSubmissionFailureHard();
do_check_eq(listener.lastDataRequest.state,
@ -289,27 +363,30 @@ add_task(function* test_data_submission_submit_failure_hard() {
do_check_eq(listener.requestDataUploadCount, 1);
});
add_task(function* test_data_submission_submit_try_again() {
add_task(function test_data_submission_submit_try_again() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_failure_soft");
policy.recordUserAcceptance();
let nextDataSubmissionDate = policy.nextDataSubmissionDate;
let now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
policy.checkStateAndTrigger();
yield listener.lastDataRequest.onSubmissionFailureSoft();
do_check_eq(policy.nextDataSubmissionDate.getTime(),
nextDataSubmissionDate.getTime() + 15 * 60 * 1000);
});
add_task(function* test_submission_daily_scheduling() {
add_task(function test_submission_daily_scheduling() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_daily_scheduling");
policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.dataSubmissionPolicyAccepted = true;
let nextDataSubmissionDate = policy.nextDataSubmissionDate;
// Skip ahead to next submission date. We should get a submission request.
let now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
do_check_eq(policy.lastDataSubmissionRequestedDate.getTime(), now.getTime());
@ -337,17 +414,18 @@ add_task(function* test_submission_daily_scheduling() {
new Date(nextScheduled.getTime() + 24 * 60 * 60 * 1000 + 200).getTime());
});
add_task(function* test_submission_far_future_scheduling() {
add_test(function test_submission_far_future_scheduling() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_far_future_scheduling");
let now = new Date(Date.now() - 24 * 60 * 60 * 1000);
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
policy.recordUserAcceptance();
now = new Date();
defineNow(policy, now);
let nextDate = policy._futureDate(3 * 24 * 60 * 60 * 1000 - 1);
policy.nextDataSubmissionDate = nextDate;
policy.checkStateAndTrigger();
do_check_true(policy.dataSubmissionPolicyAcceptedVersion >= DATAREPORTING_POLICY_VERSION);
do_check_eq(listener.requestDataUploadCount, 0);
do_check_eq(policy.nextDataSubmissionDate.getTime(), nextDate.getTime());
@ -356,17 +434,21 @@ add_task(function* test_submission_far_future_scheduling() {
do_check_eq(listener.requestDataUploadCount, 0);
do_check_eq(policy.nextDataSubmissionDate.getTime(),
policy._futureDate(24 * 60 * 60 * 1000).getTime());
run_next_test();
});
add_task(function* test_submission_backoff() {
add_task(function test_submission_backoff() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_backoff");
do_check_eq(policy.FAILURE_BACKOFF_INTERVALS.length, 2);
policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.dataSubmissionPolicyAccepted = true;
let now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
do_check_eq(policy.currentDaySubmissionFailureCount, 0);
@ -417,13 +499,15 @@ add_task(function* test_submission_backoff() {
});
// Ensure that only one submission request can be active at a time.
add_task(function* test_submission_expiring() {
add_test(function test_submission_expiring() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_expiring");
policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.dataSubmissionPolicyAccepted = true;
let nextDataSubmission = policy.nextDataSubmissionDate;
let now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
defineNow(policy, new Date(now.getTime() + 500));
policy.checkStateAndTrigger();
@ -434,9 +518,11 @@ add_task(function* test_submission_expiring() {
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 2);
run_next_test();
});
add_task(function* test_delete_remote_data() {
add_task(function test_delete_remote_data() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data");
do_check_false(policy.pendingDeleteRemoteData);
@ -460,13 +546,15 @@ add_task(function* test_delete_remote_data() {
});
// Ensure that deletion requests take priority over regular data submission.
add_task(function* test_delete_remote_data_priority() {
add_test(function test_delete_remote_data_priority() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_priority");
let now = new Date();
defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
policy.recordUserAcceptance();
defineNow(policy, new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000));
yield ensureUserNotifiedAndTrigger(policy);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
policy._inProgressSubmissionRequest = null;
@ -475,12 +563,16 @@ add_task(function* test_delete_remote_data_priority() {
do_check_eq(listener.requestRemoteDeleteCount, 1);
do_check_eq(listener.requestDataUploadCount, 1);
run_next_test();
});
add_test(function test_delete_remote_data_backoff() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_backoff");
let now = new Date();
defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
policy.recordUserAcceptance();
defineNow(policy, now);
policy.nextDataSubmissionDate = now;
policy.deleteRemoteData();
@ -508,12 +600,15 @@ add_test(function test_delete_remote_data_backoff() {
// If we request delete while an upload is in progress, delete should be
// scheduled immediately after upload.
add_task(function* test_delete_remote_data_in_progress_upload() {
add_task(function test_delete_remote_data_in_progress_upload() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_in_progress_upload");
let now = new Date();
defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
policy.recordUserAcceptance();
defineNow(policy, policy.nextDataSubmissionDate);
yield ensureUserNotifiedAndTrigger(policy);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
defineNow(policy, policy._futureDate(50 * 1000));
@ -559,6 +654,7 @@ add_test(function test_polling() {
if (count >= 2) {
policy.stopPolling();
do_check_eq(listener.notifyUserCount, 0);
do_check_eq(listener.requestDataUploadCount, 0);
run_next_test();
@ -576,7 +672,79 @@ add_test(function test_polling() {
policy.startPolling();
});
add_task(function* test_record_health_report_upload_enabled() {
// Ensure that implicit acceptance of policy is resolved through polling.
//
// This is probably covered by other tests. But, it's best to have explicit
// coverage from a higher-level.
add_test(function test_polling_implicit_acceptance() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("polling_implicit_acceptance");
// Redefine intervals with shorter, test-friendly values.
Object.defineProperty(policy, "POLL_INTERVAL_MSEC", {
value: 250,
});
Object.defineProperty(policy, "IMPLICIT_ACCEPTANCE_INTERVAL_MSEC", {
value: 700,
});
let count = 0;
// Track JS elapsed time, so we can decide if we've waited for enough ticks.
let start;
Object.defineProperty(policy, "checkStateAndTrigger", {
value: function CheckStateAndTriggerProxy() {
count++;
let now = Date.now();
let delta = now - start;
print("checkStateAndTrigger count: " + count + ", now " + now +
", delta " + delta);
// Account for some slack.
DataReportingPolicy.prototype.checkStateAndTrigger.call(policy);
// What should happen on different invocations:
//
// 1) We are inside the prompt interval so user gets prompted.
// 2) still ~300ms away from implicit acceptance
// 3) still ~50ms away from implicit acceptance
// 4) Implicit acceptance recorded. Data submission requested.
// 5) Request still pending. No new submission requested.
//
// Note that, due to the inaccuracy of timers, 4 might not happen until 5
// firings have occurred. Yay. So we watch times, not just counts.
do_check_eq(listener.notifyUserCount, 1);
if (count == 1) {
listener.lastNotifyRequest.onUserNotifyComplete();
}
if (delta <= (policy.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC + policy.POLL_INTERVAL_MSEC)) {
do_check_false(policy.dataSubmissionPolicyAccepted);
do_check_eq(listener.requestDataUploadCount, 0);
} else if (count > 3) {
do_check_true(policy.dataSubmissionPolicyAccepted);
do_check_eq(policy.dataSubmissionPolicyResponseType,
"accepted-implicit-time-elapsed");
do_check_eq(listener.requestDataUploadCount, 1);
}
if ((count > 4) && policy.dataSubmissionPolicyAccepted) {
do_check_eq(listener.requestDataUploadCount, 1);
policy.stopPolling();
run_next_test();
}
}
});
policy.firstRunDate = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000);
policy.nextDataSubmissionDate = new Date(Date.now());
start = Date.now();
policy.startPolling();
});
add_task(function test_record_health_report_upload_enabled() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("record_health_report_upload_enabled");
// Preconditions.
@ -623,10 +791,10 @@ add_test(function test_pref_change_initiates_deletion() {
hrPrefs.set("uploadEnabled", false);
});
add_task(function* test_policy_version() {
let policy, policyPrefs, hrPrefs, listener, now, firstRunTime;
function createPolicy(shouldBeNotified = false,
function createPolicy(shouldBeAccepted = false,
currentPolicyVersion = 1, minimumPolicyVersion = 1,
branchMinimumVersionOverride) {
[policy, policyPrefs, hrPrefs, listener] =
@ -636,7 +804,8 @@ add_task(function* test_policy_version() {
if (firstRun) {
firstRunTime = policy.firstRunDate.getTime();
do_check_true(firstRunTime > 0);
now = new Date(policy.firstRunDate.getTime());
now = new Date(policy.firstRunDate.getTime() +
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC);
}
else {
// The first-run time should not be reset even after policy-version
@ -644,18 +813,23 @@ add_task(function* test_policy_version() {
do_check_eq(policy.firstRunDate.getTime(), firstRunTime);
}
defineNow(policy, now);
do_check_eq(policy.userNotifiedOfCurrentPolicy, shouldBeNotified);
do_check_eq(policy.dataSubmissionPolicyAccepted, shouldBeAccepted);
}
function* triggerPolicyCheckAndEnsureNotified(notified = true) {
function* triggerPolicyCheckAndEnsureNotified(notified = true, accept = true) {
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, Number(notified));
if (notified) {
policy.ensureUserNotified();
yield listener.lastNotifyRequest.deferred.promise;
do_check_true(policy.userNotifiedOfCurrentPolicy);
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
policyPrefs.get("currentPolicyVersion"));
yield listener.lastNotifyRequest.onUserNotifyComplete();
if (accept) {
listener.lastNotifyRequest.onUserAccept("because,");
do_check_true(policy.dataSubmissionPolicyAccepted);
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
policyPrefs.get("currentPolicyVersion"));
}
else {
do_check_false(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
}
}
}
@ -670,16 +844,16 @@ add_task(function* test_policy_version() {
// version must be changed.
let currentPolicyVersion = policyPrefs.get("currentPolicyVersion");
let minimumPolicyVersion = policyPrefs.get("minimumPolicyVersion");
createPolicy(false, ++currentPolicyVersion, minimumPolicyVersion);
yield triggerPolicyCheckAndEnsureNotified(true);
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"), currentPolicyVersion);
createPolicy(true, ++currentPolicyVersion, minimumPolicyVersion);
yield triggerPolicyCheckAndEnsureNotified(false);
do_check_true(policy.dataSubmissionPolicyAccepted);
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
minimumPolicyVersion);
// Increase the minimum policy version and check if we're notified.
createPolicy(true, currentPolicyVersion, ++minimumPolicyVersion);
do_check_true(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
yield triggerPolicyCheckAndEnsureNotified(false);
createPolicy(false, currentPolicyVersion, ++minimumPolicyVersion);
do_check_false(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
yield triggerPolicyCheckAndEnsureNotified();
// Test increasing the minimum version just on the current channel.
createPolicy(true, currentPolicyVersion, minimumPolicyVersion);

View File

@ -1260,8 +1260,8 @@ this.HealthReporter.prototype = Object.freeze({
* Whether this instance will upload data to a server.
*/
get willUploadData() {
return this._policy.userNotifiedOfCurrentPolicy &&
this._policy.healthReportUploadEnabled;
return this._policy.dataSubmissionPolicyAccepted &&
this._policy.healthReportUploadEnabled;
},
/**
@ -1321,8 +1321,8 @@ this.HealthReporter.prototype = Object.freeze({
// Need to capture this before we call the parent else it's always
// set.
let inShutdown = this._shutdownRequested;
let result;
let result;
try {
result = AbstractHealthReporter.prototype._onInitError.call(this, error);
} catch (ex) {
@ -1335,8 +1335,8 @@ this.HealthReporter.prototype = Object.freeze({
// startup errors is important. And, they should not occur with much
// frequency in the wild. So, it shouldn't be too big of a deal.
if (!inShutdown &&
this._policy.healthReportUploadEnabled &&
this._policy.ensureUserNotified()) {
this._policy.ensureNotifyResponse(new Date()) &&
this._policy.healthReportUploadEnabled) {
// We don't care about what happens to this request. It's best
// effort.
let request = {

View File

@ -24,7 +24,6 @@ Cu.import("resource://gre/modules/services-common/utils.js");
Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
Cu.import("resource://testing-common/services/datareporting/mocks.jsm");
let APP_INFO = {
@ -192,16 +191,18 @@ this.getHealthReporter = function (name, uri=DUMMY_URI, inspected=false) {
let reporter;
let policyPrefs = new Preferences(branch + "policy.");
let listener = new MockPolicyListener();
listener.onRequestDataUpload = function (request) {
reporter.requestDataUpload(request);
MockPolicyListener.prototype.onRequestDataUpload.call(this, request);
}
listener.onRequestRemoteDelete = function (request) {
reporter.deleteRemoteData(request);
MockPolicyListener.prototype.onRequestRemoteDelete.call(this, request);
}
let policy = new DataReportingPolicy(policyPrefs, prefs, listener);
let policy = new DataReportingPolicy(policyPrefs, prefs, {
onRequestDataUpload: function (request) {
reporter.requestDataUpload(request);
},
onNotifyDataPolicy: function (request) { },
onRequestRemoteDelete: function (request) {
reporter.deleteRemoteData(request);
},
});
let type = inspected ? InspectedHealthReporter : HealthReporter;
reporter = new type(branch + "healthreport.", policy, null,
"state-" + name + ".json");

View File

@ -92,21 +92,6 @@ function getHealthReportProviderValues(reporter, day=null) {
});
}
/*
* Ensure that the notification has been displayed to the user therefore having
* reporter._policy.userNotifiedOfCurrentPolicy === true, which will allow for a
* successful data upload.
* @param {HealthReporter} reporter
* @return {Promise}
*/
function ensureUserNotified (reporter) {
return Task.spawn(function* ensureUserNotified () {
reporter._policy.ensureUserNotified();
yield reporter._policy._listener.lastNotifyRequest.deferred.promise;
do_check_true(reporter._policy.userNotifiedOfCurrentPolicy);
});
}
function run_test() {
run_next_test();
}
@ -688,8 +673,9 @@ add_task(function test_recurring_daily_pings() {
let policy = reporter._policy;
defineNow(policy, policy._futureDate(-24 * 60 * 68 * 1000));
policy.recordUserAcceptance();
defineNow(policy, policy.nextDataSubmissionDate);
yield ensureUserNotified(reporter);
let promise = policy.checkStateAndTrigger();
do_check_neq(promise, null);
yield promise;
@ -726,8 +712,8 @@ add_task(function test_request_remote_data_deletion() {
try {
let policy = reporter._policy;
defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
policy.recordUserAcceptance();
defineNow(policy, policy.nextDataSubmissionDate);
yield ensureUserNotified(reporter);
yield policy.checkStateAndTrigger();
let id = reporter.lastSubmitID;
do_check_neq(id, null);
@ -814,12 +800,16 @@ add_task(function test_policy_accept_reject() {
try {
let policy = reporter._policy;
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
do_check_true(policy.dataSubmissionPolicyAcceptedVersion < DATAREPORTING_POLICY_VERSION);
do_check_false(policy.dataSubmissionPolicyAccepted);
do_check_false(reporter.willUploadData);
yield ensureUserNotified(reporter);
policy.recordUserAcceptance();
do_check_true(policy.dataSubmissionPolicyAccepted);
do_check_true(reporter.willUploadData);
policy.recordUserRejection();
do_check_false(policy.dataSubmissionPolicyAccepted);
do_check_false(reporter.willUploadData);
} finally {
yield reporter._shutdown();
yield shutdownServer(server);
@ -950,9 +940,9 @@ add_task(function test_upload_on_init_failure() {
},
});
reporter._policy.recordUserAcceptance();
let error = false;
try {
yield ensureUserNotified(reporter);
yield reporter.init();
} catch (ex) {
error = true;

View File

@ -117,10 +117,8 @@ user_pref("dom.use_xbl_scopes_for_remote_xul", true);
// Get network events.
user_pref("network.activity.blipIntervalMilliseconds", 250);
// We do not wish to display datareporting policy notifications as it might
// cause other tests to fail. Tests that wish to test the notification functionality
// should explicitly disable this pref.
user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
// Don't allow the Data Reporting service to prompt for policy acceptance.
user_pref("datareporting.policy.dataSubmissionPolicyBypassAcceptance", true);
// Point Firefox Health Report at a local server. We don't care if it actually
// works. It just can't hit the default production endpoint.