mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-18 06:45:33 +00:00
Bug 804491 - Data submission policy and scheduling driver for Firefox Health Report; r=rnewman
This commit is contained in:
parent
56416b1cff
commit
f26f0a02e2
@ -10,9 +10,11 @@ VPATH = @srcdir@
|
||||
include $(DEPTH)/config/autoconf.mk
|
||||
|
||||
modules := \
|
||||
policy.jsm \
|
||||
$(NULL)
|
||||
|
||||
testing_modules := \
|
||||
mocks.jsm \
|
||||
$(NULL)
|
||||
|
||||
TEST_DIRS += tests
|
||||
|
37
services/healthreport/modules-testing/mocks.jsm
Normal file
37
services/healthreport/modules-testing/mocks.jsm
Normal file
@ -0,0 +1,37 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MockPolicyListener"];
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
|
||||
|
||||
this.MockPolicyListener = function MockPolicyListener() {
|
||||
this._log = Log4Moz.repository.getLogger("HealthReport.Testing.MockPolicyListener");
|
||||
this._log.level = Log4Moz.Level["Debug"];
|
||||
|
||||
this.requestDataSubmissionCount = 0;
|
||||
this.lastDataRequest = null;
|
||||
|
||||
this.notifyUserCount = 0;
|
||||
this.lastNotifyRequest = null;
|
||||
}
|
||||
|
||||
MockPolicyListener.prototype = {
|
||||
onRequestDataSubmission: function onRequestDataSubmission(request) {
|
||||
this._log.info("onRequestDataSubmission invoked.");
|
||||
this.requestDataSubmissionCount++;
|
||||
this.lastDataRequest = request;
|
||||
},
|
||||
|
||||
onNotifyDataPolicy: function onNotifyDataPolicy(request) {
|
||||
this._log.info("onNotifyUser invoked.");
|
||||
this.notifyUserCount++;
|
||||
this.lastNotifyRequest = request;
|
||||
},
|
||||
};
|
850
services/healthreport/policy.jsm
Normal file
850
services/healthreport/policy.jsm
Normal file
@ -0,0 +1,850 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"HealthReportPolicy",
|
||||
];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/commonjs/promise/core.js");
|
||||
Cu.import("resource://gre/modules/services-common/log4moz.js");
|
||||
Cu.import("resource://gre/modules/services-common/utils.js");
|
||||
|
||||
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
// Used as a sanity lower bound for dates stored in prefs. This module was
|
||||
// implemented in 2012, so any earlier dates indicate an incorrect clock.
|
||||
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`.
|
||||
*
|
||||
* 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
|
||||
* HealthReportPolicy.{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
|
||||
* (HealthReportPolicy) The policy instance this request came from.
|
||||
* @param promise
|
||||
* (deferred) The promise that will be fulfilled when display occurs.
|
||||
*/
|
||||
function NotifyPolicyRequest(policy, promise) {
|
||||
this.policy = policy;
|
||||
this.promise = promise;
|
||||
}
|
||||
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 onUserNotified() {
|
||||
this.promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when there was an error notifying the user about the policy.
|
||||
*
|
||||
* @param error
|
||||
* (Error) Explains what went wrong.
|
||||
*/
|
||||
onUserNotifyFailed: function onUserNotifyFailed(error) {
|
||||
this.promise.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.
|
||||
*
|
||||
* Instances of this are created when the policy requests data submission.
|
||||
* Receivers are expected to call one of the provided on* functions to signal
|
||||
* completion of the request.
|
||||
*
|
||||
* Instances of this type should not be instantiated outside of this file.
|
||||
* Receivers of instances of this type should not attempt to do anything with
|
||||
* the instance except call one of the on* methods.
|
||||
*/
|
||||
function DataSubmissionRequest(promise, expiresDate) {
|
||||
this.promise = promise;
|
||||
this.expiresDate = expiresDate;
|
||||
|
||||
this.state = null;
|
||||
this.reason = null;
|
||||
}
|
||||
|
||||
DataSubmissionRequest.prototype = {
|
||||
NO_DATA_AVAILABLE: "no-data-available",
|
||||
SUBMISSION_SUCCESS: "success",
|
||||
SUBMISSION_FAILURE_SOFT: "failure-soft",
|
||||
SUBMISSION_FAILURE_HARD: "failure-hard",
|
||||
|
||||
/**
|
||||
* No submission was attempted because no data was available.
|
||||
*/
|
||||
onNoDataAvailable: function onNoDataAvailable() {
|
||||
this.state = this.NO_DATA_AVAILABLE;
|
||||
this.promise.resolve(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Data submission has completed successfully.
|
||||
*
|
||||
* @param date
|
||||
* (Date) When data submission occurred.
|
||||
*/
|
||||
onSubmissionSuccess: function onSubmissionSuccess(date) {
|
||||
this.state = this.SUBMISSION_SUCCESS;
|
||||
this.submissionDate = date;
|
||||
this.promise.resolve(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* There was a recoverable failure when submitting data.
|
||||
*
|
||||
* Perhaps the server was down. Perhaps the network wasn't available. The
|
||||
* policy may request submission again after a short delay.
|
||||
*
|
||||
* @param reason
|
||||
* (string) Why the failure occurred. For logging purposes only.
|
||||
*/
|
||||
onSubmissionFailureSoft: function onSubmissionFailureSoft(reason=null) {
|
||||
this.state = this.SUBMISSION_FAILURE_SOFT;
|
||||
this.reason = reason;
|
||||
this.promise.resolve(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* There was an unrecoverable failure when submitting data.
|
||||
*
|
||||
* Perhaps the client is misconfigured. Perhaps the server rejected the data.
|
||||
* Attempts at performing submission again will yield the same result. So,
|
||||
* the policy should not try again (until the next day).
|
||||
*
|
||||
* @param reason
|
||||
* (string) Why the failure occurred. For logging purposes only.
|
||||
*/
|
||||
onSubmissionFailureHard: function onSubmissionFailureHard(reason=null) {
|
||||
this.state = this.SUBMISSION_FAILURE_HARD;
|
||||
this.reason = reason;
|
||||
this.promise.resolve(this);
|
||||
},
|
||||
};
|
||||
|
||||
Object.freeze(DataSubmissionRequest.prototype);
|
||||
|
||||
/**
|
||||
* Manages scheduling of Firefox Health Report data submission.
|
||||
*
|
||||
* The rules of data submission are as follows:
|
||||
*
|
||||
* 1. Do not submit data more than once every 24 hours.
|
||||
* 2. Try to submit as close to 24 hours apart as possible.
|
||||
* 3. Do not submit too soon after application startup so as to not negatively
|
||||
* impact performance at startup.
|
||||
* 4. Before first ever data submission, the user should be notified about
|
||||
* data collection practices.
|
||||
* 5. User should have opportunity to react to this notification before
|
||||
* data submission.
|
||||
* 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
|
||||
* (which are callbacks that will be invoked at certain key events):
|
||||
*
|
||||
* * onRequestDataSubmission(request) - Called when the policy is requesting
|
||||
* data to be submitted. The function is passed a `DataSubmissionRequest`.
|
||||
* The listener should call one of the special resolving functions on that
|
||||
* instance (see the documentation for that type).
|
||||
*
|
||||
* * onNotifyDataPolicy(request) - Called when the policy is requesting the
|
||||
* user to be notified that data submission will occur. The function
|
||||
* receives a `NotifyPolicyRequest` instance. The callee should call one or
|
||||
* more of the functions on that instance when specific events occur. See
|
||||
* the documentation for that type for more.
|
||||
*
|
||||
* Note that the notification method is abstracted. Different applications
|
||||
* can have different mechanisms by which they notify the user of data
|
||||
* submission practices.
|
||||
*
|
||||
* @param prefs
|
||||
* (Preferences) Handle on preferences branch on which state will be
|
||||
* queried and stored.
|
||||
* @param listener
|
||||
* (object) Object with callbacks that will be invoked at certain key
|
||||
* events.
|
||||
*/
|
||||
this.HealthReportPolicy = function HealthReportPolicy(prefs, listener) {
|
||||
this._log = Log4Moz.repository.getLogger("HealthReport.Policy");
|
||||
this._log.level = Log4Moz.Level["Debug"];
|
||||
|
||||
for (let handler of this.REQUIRED_LISTENERS) {
|
||||
if (!listener[handler]) {
|
||||
throw new Error("Passed listener does not contain required handler: " +
|
||||
handler);
|
||||
}
|
||||
}
|
||||
|
||||
this._prefs = prefs;
|
||||
this._listener = listener;
|
||||
|
||||
// If we've never run before, record the current time.
|
||||
if (!this.firstRunDate.getTime()) {
|
||||
this.firstRunDate = this.now();
|
||||
}
|
||||
|
||||
// Ensure we are scheduled to submit.
|
||||
if (!this.nextDataSubmissionDate.getTime()) {
|
||||
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;
|
||||
}
|
||||
|
||||
HealthReportPolicy.prototype = {
|
||||
/**
|
||||
* 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: 5 * 60 * 1000,
|
||||
|
||||
/**
|
||||
* How often to poll to see if we need to do something.
|
||||
*
|
||||
* The interval needs to be short enough such that short-lived applications
|
||||
* have an opportunity to submit data. But, it also needs to be long enough
|
||||
* to not negatively impact performance.
|
||||
*
|
||||
* The random bit is to ensure that other systems scheduling around the same
|
||||
* interval don't all get scheduled together.
|
||||
*/
|
||||
POLL_INTERVAL_MSEC: (60 * 1000) + Math.floor(2.5 * 1000 * Math.random()),
|
||||
|
||||
/**
|
||||
* How long individual data submission requests live before expiring.
|
||||
*
|
||||
* Data submission requests have this long to complete before we give up on
|
||||
* them and try again.
|
||||
*
|
||||
* We want this to be short enough that we retry frequently enough but long
|
||||
* enough to give slow networks and systems time to handle it.
|
||||
*/
|
||||
SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC: 10 * 60 * 1000,
|
||||
|
||||
/**
|
||||
* Our backoff schedule in case of submission failure.
|
||||
*
|
||||
* This dictates both the number of times we retry a daily submission and
|
||||
* when to retry after each failure.
|
||||
*
|
||||
* Each element represents how long to wait after each recoverable failure.
|
||||
* After the first failure, we wait the time in element 0 before trying
|
||||
* again. After the second failure, we wait the time in element 1. Once
|
||||
* we run out of values in this array, we give up on that day's submission
|
||||
* and schedule for a day out.
|
||||
*/
|
||||
FAILURE_BACKOFF_INTERVALS: [
|
||||
15 * 60 * 1000,
|
||||
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: ["onRequestDataSubmission", "onNotifyDataPolicy"],
|
||||
|
||||
/**
|
||||
* The first time the health report policy came into existence.
|
||||
*
|
||||
* This is used for scheduling of the initial submission.
|
||||
*/
|
||||
get firstRunDate() {
|
||||
return CommonUtils.getDatePref(this._prefs, "firstRunTime", 0, this._log,
|
||||
OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
set firstRunDate(value) {
|
||||
this._log.debug("Setting first-run date: " + value);
|
||||
CommonUtils.setDatePref(this._prefs, "firstRunTime", value,
|
||||
OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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,
|
||||
this._log, OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
set dataSubmissionPolicyNotifiedDate(value) {
|
||||
this._log.debug("Setting user notified date: " + value);
|
||||
CommonUtils.setDatePref(this._prefs, "dataSubmissionPolicyNotifiedTime",
|
||||
value, OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether submission of data is allowed.
|
||||
*
|
||||
* This is the master switch for data submission. If it is off, we will
|
||||
* never submit data, even if the user has agreed to it.
|
||||
*/
|
||||
get dataSubmissionEnabled() {
|
||||
// Default is true because we are opt-out.
|
||||
return this._prefs.get("dataSubmissionEnabled", true);
|
||||
},
|
||||
|
||||
set dataSubmissionEnabled(value) {
|
||||
this._prefs.set("dataSubmissionEnabled", !!value);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 dataSubmissionPolicyAccepted(value) {
|
||||
this._prefs.set("dataSubmissionPolicyAccepted", !!value);
|
||||
},
|
||||
|
||||
/**
|
||||
* The state of user notification of the data policy.
|
||||
*
|
||||
* This must be HealthReportPolicy.STATE_NOTIFY_COMPLETE before data
|
||||
* submission can occur.
|
||||
*
|
||||
* @return HealthReportPolicy.STATE_NOTIFY_* constant.
|
||||
*/
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
* When this policy last requested data submission.
|
||||
*
|
||||
* This is used mainly for forensics purposes and should have no bearing
|
||||
* on scheduling or run-time behavior.
|
||||
*/
|
||||
get lastDataSubmissionRequestedDate() {
|
||||
return CommonUtils.getDatePref(this._prefs,
|
||||
"lastDataSubmissionRequestedTime", 0,
|
||||
this._log, OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
set lastDataSubmissionRequestedDate(value) {
|
||||
CommonUtils.setDatePref(this._prefs, "lastDataSubmissionRequestedTime",
|
||||
value, OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
/**
|
||||
* When the last data submission actually occurred.
|
||||
*
|
||||
* This is used mainly for forensics purposes and should have no bearing on
|
||||
* actual scheduling.
|
||||
*/
|
||||
get lastDataSubmissionSuccessfulDate() {
|
||||
return CommonUtils.getDatePref(this._prefs,
|
||||
"lastDataSubmissionSuccessfulTime", 0,
|
||||
this._log, OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
set lastDataSubmissionSuccessfulDate(value) {
|
||||
CommonUtils.setDatePref(this._prefs, "lastDataSubmissionSuccessfulTime",
|
||||
value, OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
/**
|
||||
* When we last encountered a submission failure.
|
||||
*
|
||||
* This is used for forensics purposes and should have no bearing on
|
||||
* scheduling.
|
||||
*/
|
||||
get lastDataSubmissionFailureDate() {
|
||||
return CommonUtils.getDatePref(this._prefs, "lastDataSubmissionFailureTime",
|
||||
0, this._log, OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
set lastDataSubmissionFailureDate(value) {
|
||||
CommonUtils.setDatePref(this._prefs, "lastDataSubmissionFailureTime", value,
|
||||
OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
/**
|
||||
* When the next data submission is scheduled to occur.
|
||||
*
|
||||
* This is maintained internally by this type. External users should not
|
||||
* mutate this value.
|
||||
*/
|
||||
get nextDataSubmissionDate() {
|
||||
return CommonUtils.getDatePref(this._prefs, "nextDataSubmissionTime", 0,
|
||||
this._log, OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
set nextDataSubmissionDate(value) {
|
||||
CommonUtils.setDatePref(this._prefs, "nextDataSubmissionTime", value,
|
||||
OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
/**
|
||||
* The number of submission failures for this day's upload.
|
||||
*
|
||||
* This is used to drive backoff and scheduling.
|
||||
*/
|
||||
get currentDaySubmissionFailureCount() {
|
||||
let v = this._prefs.get("currentDaySubmissionFailureCount", 0);
|
||||
|
||||
if (!Number.isInteger(v)) {
|
||||
v = 0;
|
||||
}
|
||||
|
||||
return v;
|
||||
},
|
||||
|
||||
set currentDaySubmissionFailureCount(value) {
|
||||
if (!Number.isInteger(value)) {
|
||||
throw new Error("Value must be integer: " + value);
|
||||
}
|
||||
|
||||
this._prefs.set("currentDaySubmissionFailureCount", value);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
|
||||
/**
|
||||
* Start background polling for activity.
|
||||
*
|
||||
* This will set up a recurring timer that will periodically check if
|
||||
* activity is warranted.
|
||||
*
|
||||
* You typically call this function for each constructed instance.
|
||||
*/
|
||||
startPolling: function startPolling() {
|
||||
this.stopPolling();
|
||||
|
||||
this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
this._timer.initWithCallback({
|
||||
notify: function notify() {
|
||||
this.checkStateAndTrigger();
|
||||
}.bind(this)
|
||||
}, this.POLL_INTERVAL_MSEC, this._timer.TYPE_REPEATING_SLACK);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop background polling for activity.
|
||||
*
|
||||
* This should be called when the instance is no longer needed.
|
||||
*/
|
||||
stopPolling: function stopPolling() {
|
||||
if (this._timer) {
|
||||
this._timer.cancel();
|
||||
this._timer = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Abstraction for obtaining current time.
|
||||
*
|
||||
* The purpose of this is to facilitate testing. Testing code can monkeypatch
|
||||
* this on instances instead of modifying the singleton Date object.
|
||||
*/
|
||||
now: function now() {
|
||||
return new Date();
|
||||
},
|
||||
|
||||
/**
|
||||
* Check state and trigger actions, if necessary.
|
||||
*
|
||||
* This is what enforces the submission and notification policy detailed
|
||||
* above. You can think of this as the driver for health report data
|
||||
* submission.
|
||||
*
|
||||
* Typically this function is called automatically by the background polling.
|
||||
* But, it can safely be called manually as needed.
|
||||
*/
|
||||
checkStateAndTrigger: function checkStateAndTrigger() {
|
||||
// If the master data submission kill switch is toggled, we have nothing
|
||||
// to do. We don't notify about data policies because this would have
|
||||
// no effect.
|
||||
if (!this.dataSubmissionEnabled) {
|
||||
this._log.debug("Data submission is disabled. Doing nothing.");
|
||||
return;
|
||||
}
|
||||
|
||||
let now = this.now();
|
||||
let nowT = now.getTime();
|
||||
|
||||
// If the user hasn't responded to the data policy, don't do anything.
|
||||
if (!this.ensureNotifyResponse(now)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// User has opted out of data submission.
|
||||
if (!this.dataSubmissionPolicyAccepted) {
|
||||
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.
|
||||
|
||||
let nextSubmissionDate = this.nextDataSubmissionDate;
|
||||
|
||||
if (nowT < nextSubmissionDate.getTime()) {
|
||||
this._log.debug("Next data submission is scheduled in the future: " +
|
||||
nextSubmissionDate);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._inProgressSubmissionRequest) {
|
||||
if (this._inProgressSubmissionRequest.expiresDate.getTime() > nowT) {
|
||||
this._log.info("Waiting on in-progress submission request to finish.");
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.warn("Old submission request has expired from no activity.");
|
||||
this._inProgressSubmissionRequest.promise.reject(new Error("Request has expired."));
|
||||
this._inProgressSubmissionRequest = null;
|
||||
if (!this._handleSubmissionFailure()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We're past our scheduled next data submission date, so let's do it!
|
||||
this.lastDataSubmissionRequestedDate = now;
|
||||
let deferred = Promise.defer();
|
||||
let requestExpiresDate =
|
||||
this._futureDate(this.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC);
|
||||
this._inProgressSubmissionRequest = new DataSubmissionRequest(deferred,
|
||||
requestExpiresDate);
|
||||
|
||||
let onSuccess = function onSuccess(result) {
|
||||
this._inProgressSubmissionRequest = null;
|
||||
this._handleSubmissionResult(result);
|
||||
}.bind(this);
|
||||
|
||||
let onError = function onError(error) {
|
||||
this._log.error("Error when handling data submission result: " +
|
||||
CommonUtils.exceptionStr(result));
|
||||
this._inProgressSubmissionRequest = null;
|
||||
this._handleSubmissionFailure();
|
||||
}.bind(this);
|
||||
|
||||
deferred.promise.then(onSuccess, onError);
|
||||
|
||||
this._log.info("Requesting data submission. Will expire at " +
|
||||
requestExpiresDate);
|
||||
try {
|
||||
this._listener.onRequestDataSubmission(this._inProgressSubmissionRequest);
|
||||
} catch (ex) {
|
||||
this._log.warn("Exception when calling onRequestDataSubmission: " +
|
||||
CommonUtils.exceptionStr(ex));
|
||||
this._inProgressSubmissionRequest = null;
|
||||
this._handleSubmissionFailure();
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure user has responded to data submission policy.
|
||||
*
|
||||
* This must be called before data submission. If the policy has not been
|
||||
* responded to, data submission must not occur.
|
||||
*
|
||||
* @return bool Whether user has responded to data policy.
|
||||
*/
|
||||
ensureNotifyResponse: function ensureNotifyResponse(now) {
|
||||
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, function onError(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;
|
||||
}
|
||||
|
||||
// We're waiting for user action or implicit acceptance after display.
|
||||
if (notifyState == this.STATE_NOTIFY_WAIT) {
|
||||
// Check for implicit acceptance.
|
||||
let implicitAcceptanceDate =
|
||||
new Date(this._dataSubmissionPolicyNotifiedDate.getTime() +
|
||||
this.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC);
|
||||
|
||||
if (now.getTime() < implicitAcceptanceDate.getTime()) {
|
||||
this._log.debug("Still waiting for reaction or implicit acceptance.");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.recordUserAcceptance("implicit-time-elapsed");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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 true;
|
||||
},
|
||||
|
||||
_handleSubmissionResult: function _handleSubmissionResult(request) {
|
||||
let state = request.state;
|
||||
let reason = request.reason || "no reason";
|
||||
this._log.info("Got submission request result: " + state);
|
||||
|
||||
if (state == request.SUBMISSION_SUCCESS) {
|
||||
this._log.info("Successful data submission reported.");
|
||||
this.lastDataSubmissionSuccessfulDate = request.submissionDate;
|
||||
this.nextDataSubmissionDate =
|
||||
new Date(request.submissionDate.getTime() + MILLISECONDS_PER_DAY);
|
||||
this.currentDaySubmissionFailureCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == request.NO_DATA_AVAILABLE) {
|
||||
this._log.info("No data was available to submit. May try later.");
|
||||
this._handleSubmissionFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == request.SUBMISSION_FAILURE_SOFT) {
|
||||
this._log.warn("Soft error submitting data: " + reason);
|
||||
this.lastDataSubmissionFailureDate = this.now();
|
||||
this._handleSubmissionFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == request.SUBMISSION_FAILURE_HARD) {
|
||||
this._log.warn("Hard error submitting data: " + reason);
|
||||
this.lastDataSubmissionFailureDate = this.now();
|
||||
this._moveScheduleForward24h();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Unknown state on DataSubmissionRequest: " + request.state);
|
||||
},
|
||||
|
||||
_handleSubmissionFailure: function _handleSubmissionFailure() {
|
||||
if (this.currentDaySubmissionFailureCount >= this.FAILURE_BACKOFF_INTERVALS.length) {
|
||||
this._log.warn("Reached the limit of daily submission attempts. " +
|
||||
"Rescheduling for tomorrow.");
|
||||
this._moveScheduleForward24h();
|
||||
return false;
|
||||
}
|
||||
|
||||
let offset = this.FAILURE_BACKOFF_INTERVALS[this.currentDaySubmissionFailureCount];
|
||||
this.nextDataSubmissionDate = this._futureDate(offset);
|
||||
this.currentDaySubmissionFailureCount++;
|
||||
return true;
|
||||
},
|
||||
|
||||
_moveScheduleForward24h: function _moveScheduleForward24h() {
|
||||
let d = this._futureDate(MILLISECONDS_PER_DAY);
|
||||
this._log.info("Setting next scheduled data submission for " + d);
|
||||
|
||||
this.nextDataSubmissionDate = d;
|
||||
this.currentDaySubmissionFailureCount = 0;
|
||||
},
|
||||
|
||||
_futureDate: function _futureDate(offset) {
|
||||
return new Date(this.now().getTime() + offset);
|
||||
},
|
||||
};
|
||||
|
||||
Object.freeze(HealthReportPolicy.prototype);
|
@ -4,9 +4,11 @@
|
||||
"use strict";
|
||||
|
||||
const modules = [
|
||||
"policy.jsm",
|
||||
];
|
||||
|
||||
const test_modules = [
|
||||
"mocks.jsm",
|
||||
];
|
||||
|
||||
function run_test() {
|
||||
|
538
services/healthreport/tests/xpcshell/test_policy.js
Normal file
538
services/healthreport/tests/xpcshell/test_policy.js
Normal file
@ -0,0 +1,538 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
|
||||
Cu.import("resource://testing-common/services/healthreport/mocks.jsm");
|
||||
|
||||
|
||||
function getPolicy(name) {
|
||||
let prefs = new Preferences(name);
|
||||
let listener = new MockPolicyListener();
|
||||
|
||||
return [new HealthReportPolicy(prefs, listener), prefs, listener];
|
||||
}
|
||||
|
||||
function defineNow(policy, now) {
|
||||
print("Adjusting fake system clock to " + now);
|
||||
Object.defineProperty(policy, "now", {
|
||||
value: function customNow() {
|
||||
return now;
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_test(function test_constructor() {
|
||||
let prefs = new Preferences("foo.bar");
|
||||
let listener = {
|
||||
onRequestDataSubmission: function() {},
|
||||
onNotifyDataPolicy: function() {},
|
||||
};
|
||||
|
||||
let policy = new HealthReportPolicy(prefs, listener);
|
||||
do_check_true(Date.now() - policy.firstRunDate.getTime() < 1000);
|
||||
|
||||
let tomorrow = Date.now() + 24 * 60 * 60 * 1000;
|
||||
do_check_true(tomorrow - policy.nextDataSubmissionDate.getTime() < 1000);
|
||||
|
||||
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_prefs() {
|
||||
let [policy, prefs, listener] = getPolicy("prefs");
|
||||
|
||||
let now = new Date();
|
||||
let nowT = now.getTime();
|
||||
|
||||
policy.firstRunDate = now;
|
||||
do_check_eq(prefs.get("firstRunTime"), nowT);
|
||||
do_check_eq(policy.firstRunDate.getTime(), nowT);
|
||||
|
||||
policy.dataSubmissionPolicyNotifiedDate= now;
|
||||
do_check_eq(prefs.get("dataSubmissionPolicyNotifiedTime"), nowT);
|
||||
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), nowT);
|
||||
|
||||
policy.dataSubmissionPolicyResponseDate = now;
|
||||
do_check_eq(prefs.get("dataSubmissionPolicyResponseTime"), nowT);
|
||||
do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), nowT);
|
||||
|
||||
policy.dataSubmissionPolicyResponseType = "type-1";
|
||||
do_check_eq(prefs.get("dataSubmissionPolicyResponseType"), "type-1");
|
||||
do_check_eq(policy.dataSubmissionPolicyResponseType, "type-1");
|
||||
|
||||
policy.dataSubmissionEnabled = false;
|
||||
do_check_false(prefs.get("dataSubmissionEnabled", true));
|
||||
do_check_false(policy.dataSubmissionEnabled);
|
||||
|
||||
policy.dataSubmissionPolicyAccepted = false;
|
||||
do_check_false(prefs.get("dataSubmissionPolicyAccepted", true));
|
||||
do_check_false(policy.dataSubmissionPolicyAccepted);
|
||||
|
||||
policy.lastDataSubmissionRequestedDate = now;
|
||||
do_check_eq(prefs.get("lastDataSubmissionRequestedTime"), nowT);
|
||||
do_check_eq(policy.lastDataSubmissionRequestedDate.getTime(), nowT);
|
||||
|
||||
policy.lastDataSubmissionSuccessfulDate = now;
|
||||
do_check_eq(prefs.get("lastDataSubmissionSuccessfulTime"), nowT);
|
||||
do_check_eq(policy.lastDataSubmissionSuccessfulDate.getTime(), nowT);
|
||||
|
||||
policy.lastDataSubmissionFailureDate = now;
|
||||
do_check_eq(prefs.get("lastDataSubmissionFailureTime"), nowT);
|
||||
do_check_eq(policy.lastDataSubmissionFailureDate.getTime(), nowT);
|
||||
|
||||
policy.nextDataSubmissionDate = now;
|
||||
do_check_eq(prefs.get("nextDataSubmissionTime"), nowT);
|
||||
do_check_eq(policy.nextDataSubmissionDate.getTime(), nowT);
|
||||
|
||||
policy.currentDaySubmissionFailureCount = 2;
|
||||
do_check_eq(prefs.get("currentDaySubmissionFailureCount", 0), 2);
|
||||
do_check_eq(policy.currentDaySubmissionFailureCount, 2);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_notify_state_prefs() {
|
||||
let [policy, prefs, listener] = getPolicy("notify_state_prefs");
|
||||
|
||||
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_test(function test_initial_submission_notification() {
|
||||
let [policy, prefs, listener] = getPolicy("initial_submission_notification");
|
||||
|
||||
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);
|
||||
|
||||
// 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);
|
||||
listener.lastNotifyRequest.onUserNotifyComplete();
|
||||
do_check_true(policy._dataSubmissionPolicyNotifiedDate instanceof Date);
|
||||
do_check_true(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0);
|
||||
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(),
|
||||
policy._dataSubmissionPolicyNotifiedDate.getTime());
|
||||
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_notification_implicit_acceptance() {
|
||||
let [policy, prefs, 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);
|
||||
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");
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_notification_rejected() {
|
||||
// User notification failed. We should not record it as being presented.
|
||||
let [policy, prefs, 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);
|
||||
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);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_notification_accepted() {
|
||||
let [policy, prefs, listener] = getPolicy("notification_accepted");
|
||||
|
||||
let now = new Date(policy.nextDataSubmissionDate.getTime() -
|
||||
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
|
||||
defineNow(policy, now);
|
||||
policy.checkStateAndTrigger();
|
||||
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());
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_notification_rejected() {
|
||||
let [policy, prefs, listener] = getPolicy("notification_rejected");
|
||||
|
||||
let now = new Date(policy.nextDataSubmissionDate.getTime() -
|
||||
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
|
||||
defineNow(policy, now);
|
||||
policy.checkStateAndTrigger();
|
||||
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.requestDataSubmissionCount, 0);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_submission_kill_switch() {
|
||||
let [policy, prefs, 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");
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
|
||||
defineNow(policy,
|
||||
new Date(Date.now() + policy.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC + 100));
|
||||
policy.dataSubmissionEnabled = false;
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_data_submission_no_data() {
|
||||
let [policy, prefs, 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.requestDataSubmissionCount, 0);
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
listener.lastDataRequest.onNoDataAvailable();
|
||||
|
||||
// The next trigger should try again.
|
||||
defineNow(policy, new Date(now.getTime() + 155 * 60 * 1000));
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 2);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_data_submission_submit_failure_hard() {
|
||||
let [policy, prefs, 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);
|
||||
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
listener.lastDataRequest.onSubmissionFailureHard();
|
||||
do_check_eq(listener.lastDataRequest.state,
|
||||
listener.lastDataRequest.SUBMISSION_FAILURE_HARD);
|
||||
|
||||
let expected = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
do_check_eq(policy.nextDataSubmissionDate.getTime(), expected.getTime());
|
||||
|
||||
defineNow(policy, new Date(now.getTime() + 10));
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_data_submission_submit_try_again() {
|
||||
let [policy, prefs, listener] = getPolicy("data_submission_failure_soft");
|
||||
|
||||
policy.recordUserAcceptance();
|
||||
let nextDataSubmissionDate = policy.nextDataSubmissionDate;
|
||||
let now = new Date(policy.nextDataSubmissionDate.getTime());
|
||||
defineNow(policy, now);
|
||||
policy.checkStateAndTrigger();
|
||||
listener.lastDataRequest.onSubmissionFailureSoft();
|
||||
do_check_eq(policy.nextDataSubmissionDate.getTime(),
|
||||
nextDataSubmissionDate.getTime() + 15 * 60 * 1000);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_submission_daily_scheduling() {
|
||||
let [policy, prefs, 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);
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
do_check_eq(policy.lastDataSubmissionRequestedDate.getTime(), now.getTime());
|
||||
|
||||
let finishedDate = new Date(now.getTime() + 250);
|
||||
defineNow(policy, new Date(finishedDate.getTime() + 50));
|
||||
listener.lastDataRequest.onSubmissionSuccess(finishedDate);
|
||||
do_check_eq(policy.lastDataSubmissionSuccessfulDate.getTime(), finishedDate.getTime());
|
||||
|
||||
// Next scheduled submission should be exactly 1 day after the reported
|
||||
// submission success.
|
||||
|
||||
let nextScheduled = new Date(finishedDate.getTime() + 24 * 60 * 60 * 1000);
|
||||
do_check_eq(policy.nextDataSubmissionDate.getTime(), nextScheduled.getTime());
|
||||
|
||||
// Fast forward some arbitrary time. We shouldn't do any work yet.
|
||||
defineNow(policy, new Date(now.getTime() + 40000));
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
|
||||
defineNow(policy, nextScheduled);
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 2);
|
||||
listener.lastDataRequest.onSubmissionSuccess(new Date(nextScheduled.getTime() + 200));
|
||||
do_check_eq(policy.nextDataSubmissionDate.getTime(),
|
||||
new Date(nextScheduled.getTime() + 24 * 60 * 60 * 1000 + 200).getTime());
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_submission_backoff() {
|
||||
let [policy, prefs, 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);
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
do_check_eq(policy.currentDaySubmissionFailureCount, 0);
|
||||
|
||||
now = new Date(now.getTime() + 5000);
|
||||
defineNow(policy, now);
|
||||
|
||||
// On first soft failure we should back off by scheduled interval.
|
||||
listener.lastDataRequest.onSubmissionFailureSoft();
|
||||
do_check_eq(policy.currentDaySubmissionFailureCount, 1);
|
||||
do_check_eq(policy.nextDataSubmissionDate.getTime(),
|
||||
new Date(now.getTime() + policy.FAILURE_BACKOFF_INTERVALS[0]).getTime());
|
||||
do_check_eq(policy.lastDataSubmissionFailureDate.getTime(), now.getTime());
|
||||
|
||||
// Should not request submission until scheduled.
|
||||
now = new Date(policy.nextDataSubmissionDate.getTime() - 1);
|
||||
defineNow(policy, now);
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
|
||||
// 2nd request for submission.
|
||||
now = new Date(policy.nextDataSubmissionDate.getTime());
|
||||
defineNow(policy, now);
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 2);
|
||||
|
||||
now = new Date(now.getTime() + 5000);
|
||||
defineNow(policy, now);
|
||||
|
||||
// On second failure we should back off by more.
|
||||
listener.lastDataRequest.onSubmissionFailureSoft();
|
||||
do_check_eq(policy.currentDaySubmissionFailureCount, 2);
|
||||
do_check_eq(policy.nextDataSubmissionDate.getTime(),
|
||||
new Date(now.getTime() + policy.FAILURE_BACKOFF_INTERVALS[1]).getTime());
|
||||
|
||||
now = new Date(policy.nextDataSubmissionDate.getTime());
|
||||
defineNow(policy, now);
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 3);
|
||||
|
||||
now = new Date(now.getTime() + 5000);
|
||||
defineNow(policy, now);
|
||||
|
||||
// On 3rd failure we should back off by a whole day.
|
||||
listener.lastDataRequest.onSubmissionFailureSoft();
|
||||
do_check_eq(policy.currentDaySubmissionFailureCount, 0);
|
||||
do_check_eq(policy.nextDataSubmissionDate.getTime(),
|
||||
new Date(now.getTime() + 24 * 60 * 60 * 1000).getTime());
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
// Ensure that only one submission request can be active at a time.
|
||||
add_test(function test_submission_expiring() {
|
||||
let [policy, prefs, 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);
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
defineNow(policy, new Date(now.getTime() + 500));
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
|
||||
defineNow(policy, new Date(policy.now().getTime() +
|
||||
policy.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC));
|
||||
|
||||
policy.checkStateAndTrigger();
|
||||
do_check_eq(listener.requestDataSubmissionCount, 2);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_polling() {
|
||||
let [policy, prefs, listener] = getPolicy("polling");
|
||||
|
||||
// Ensure checkStateAndTrigger is called at a regular interval.
|
||||
let now = new Date();
|
||||
Object.defineProperty(policy, "POLL_INTERVAL_MSEC", {
|
||||
value: 500,
|
||||
});
|
||||
let count = 0;
|
||||
|
||||
Object.defineProperty(policy, "checkStateAndTrigger", {
|
||||
value: function fakeCheckStateAndTrigger() {
|
||||
let now2 = new Date();
|
||||
count++;
|
||||
|
||||
do_check_true(now2.getTime() - now.getTime() >= 500);
|
||||
now = now2;
|
||||
HealthReportPolicy.prototype.checkStateAndTrigger.call(policy);
|
||||
|
||||
if (count >= 2) {
|
||||
policy.stopPolling();
|
||||
|
||||
do_check_eq(listener.notifyUserCount, 0);
|
||||
do_check_eq(listener.requestDataSubmissionCount, 0);
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
}
|
||||
});
|
||||
policy.startPolling();
|
||||
});
|
||||
|
||||
// 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, prefs, 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: 750,
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
Object.defineProperty(policy, "checkStateAndTrigger", {
|
||||
value: function CheckStateAndTriggerProxy() {
|
||||
count++;
|
||||
print("checkStateAndTrigger count: " + count);
|
||||
|
||||
// Account for some slack.
|
||||
HealthReportPolicy.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.
|
||||
|
||||
do_check_eq(listener.notifyUserCount, 1);
|
||||
|
||||
if (count == 1) {
|
||||
listener.lastNotifyRequest.onUserNotifyComplete();
|
||||
}
|
||||
|
||||
if (count < 4) {
|
||||
do_check_false(policy.dataSubmissionPolicyAccepted);
|
||||
do_check_eq(listener.requestDataSubmissionCount, 0);
|
||||
} else {
|
||||
do_check_true(policy.dataSubmissionPolicyAccepted);
|
||||
do_check_eq(policy.dataSubmissionPolicyResponseType,
|
||||
"accepted-implicit-time-elapsed");
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
}
|
||||
|
||||
if (count > 4) {
|
||||
do_check_eq(listener.requestDataSubmissionCount, 1);
|
||||
policy.stopPolling();
|
||||
run_next_test();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
policy.firstRunDate = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000);
|
||||
policy.nextDataSubmissionDate = new Date(Date.now());
|
||||
policy.startPolling();
|
||||
});
|
||||
|
@ -3,3 +3,4 @@ head = head.js
|
||||
tail =
|
||||
|
||||
[test_load_modules.js]
|
||||
[test_policy.js]
|
||||
|
Loading…
x
Reference in New Issue
Block a user