Bug 804491 - Data submission policy and scheduling driver for Firefox Health Report; r=rnewman

This commit is contained in:
Gregory Szorc 2012-11-07 16:09:13 -08:00
parent 56416b1cff
commit f26f0a02e2
6 changed files with 1430 additions and 0 deletions

View File

@ -10,9 +10,11 @@ VPATH = @srcdir@
include $(DEPTH)/config/autoconf.mk
modules := \
policy.jsm \
$(NULL)
testing_modules := \
mocks.jsm \
$(NULL)
TEST_DIRS += tests

View 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;
},
};

View 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);

View File

@ -4,9 +4,11 @@
"use strict";
const modules = [
"policy.jsm",
];
const test_modules = [
"mocks.jsm",
];
function run_test() {

View 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();
});

View File

@ -3,3 +3,4 @@ head = head.js
tail =
[test_load_modules.js]
[test_policy.js]