Bug 1275436 - Remove push subscriptions from the server after clearing permissions and site data. r=mt
authorKit Cambridge <kcambridge@mozilla.com>
Fri, 20 May 2016 09:38:26 -0700
changeset 300454 5a00b354638d0806b86a75bf9f1ceca6d4abb0f0
parent 300453 605c72e8e966af0ceab460f32d847280cb0a1e81
child 300455 71530f6566b92269c014a067a47d0d69ff79a72c
push id30313
push usercbook@mozilla.com
push dateMon, 06 Jun 2016 09:56:25 +0000
treeherdermozilla-central@0a3b6e2df656 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmt
bugs1275436
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1275436 - Remove push subscriptions from the server after clearing permissions and site data. r=mt Previously, we removed records locally, but didn't notify the server. We can be nice and avoid making the server buffer messages for subscriptions that the client will never use again. MozReview-Commit-ID: 5iohGQPHXuz
browser/base/content/sanitize.js
dom/push/PushService.jsm
dom/push/test/xpcshell/head.js
dom/push/test/xpcshell/test_clearAll_successful.js
dom/push/test/xpcshell/test_clear_forgetAboutSite.js
dom/push/test/xpcshell/test_permissions.js
dom/push/test/xpcshell/xpcshell.ini
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -571,25 +571,30 @@ Sanitizer.prototype = {
                       .getService(Ci.nsISiteSecurityService);
           sss.clearAll();
         } catch (ex) {
           seenException = ex;
         }
 
         // Clear all push notification subscriptions
         try {
-          let push = Cc["@mozilla.org/push/Service;1"]
-                       .getService(Ci.nsIPushService);
-          push.clearForDomain("*", status => {
-            if (!Components.isSuccessCode(status)) {
-              dump("Error clearing Web Push data: " + status + "\n");
-            }
+          yield new Promise((resolve, reject) => {
+            let push = Cc["@mozilla.org/push/Service;1"]
+                         .getService(Ci.nsIPushService);
+            push.clearForDomain("*", status => {
+              if (Components.isSuccessCode(status)) {
+                resolve();
+              } else {
+                reject(new Error("Error clearing push subscriptions: " +
+                                 status));
+              }
+            });
           });
-        } catch (e) {
-          dump("Web Push may not be available.\n");
+        } catch (ex) {
+          seenException = ex;
         }
 
         TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS", refObj);
         if (seenException) {
           throw seenException;
         }
       })
     },
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -96,16 +96,43 @@ const UNINIT_EVENT = 3;
  */
 function errorWithResult(message, result = Cr.NS_ERROR_FAILURE) {
   let error = new Error(message);
   error.result = result;
   return error;
 }
 
 /**
+ * Copied from ForgetAboutSite.jsm.
+ *
+ * Returns true if the string passed in is part of the root domain of the
+ * current string.  For example, if this is "www.mozilla.org", and we pass in
+ * "mozilla.org", this will return true.  It would return false the other way
+ * around.
+ */
+function hasRootDomain(str, aDomain)
+{
+  let index = str.indexOf(aDomain);
+  // If aDomain is not found, we know we do not have it as a root domain.
+  if (index == -1)
+    return false;
+
+  // If the strings are the same, we obviously have a match.
+  if (str == aDomain)
+    return true;
+
+  // Otherwise, we have aDomain as our root domain iff the index of aDomain is
+  // aDomain.length subtracted from our length and (since we do not have an
+  // exact match) the character before the index is a dot or slash.
+  let prevChar = str[index - 1];
+  return (index == (str.length - aDomain.length)) &&
+         (prevChar == "." || prevChar == "/");
+}
+
+/**
  * The implementation of the push system. It uses WebSockets
  * (PushServiceWebSocket) to communicate with the server and PushDB (IndexedDB)
  * for persistence.
  */
 this.PushService = {
   _service: null,
   _state: PUSH_SERVICE_UNINIT,
   _db: null,
@@ -325,24 +352,18 @@ this.PushService = {
   _clearOriginData: function(data) {
     console.log("clearOriginData()");
 
     if (!data) {
       return Promise.resolve();
     }
 
     let pattern = JSON.parse(data);
-    return this._db.clearIf(record => {
-      if (!record.matchesOriginAttributes(pattern)) {
-        return false;
-      }
-      this._backgroundUnregister(record,
-                                 Ci.nsIPushErrorReporter.UNSUBSCRIBE_MANUAL);
-      return true;
-    });
+    return this._dropRegistrationsIf(record =>
+      record.matchesOriginAttributes(pattern));
   },
 
   /**
    * Sends an unregister request to the server in the background. If the
    * service is not connected, this function is a no-op.
    *
    * @param {PushRecord} record The record to unregister.
    * @param {Number} reason An `nsIPushErrorReporter` unsubscribe reason,
@@ -1182,66 +1203,26 @@ this.PushService = {
           gPushNotifier.notifySubscriptionModified(record.scope,
                                                    record.principal);
           return true;
         });
       });
   },
 
   clear: function(info) {
-    if (info.domain == "*") {
-      return this._clearAll();
-    }
-    return this._clearForDomain(info.domain);
-  },
-
-  _clearAll: function _clearAll() {
     return this._checkActivated()
-      .then(_ => this._db.drop())
-      .catch(_ => Promise.resolve());
-  },
-
-  _clearForDomain: function(domain) {
-    /**
-     * Copied from ForgetAboutSite.jsm.
-     *
-     * Returns true if the string passed in is part of the root domain of the
-     * current string.  For example, if this is "www.mozilla.org", and we pass in
-     * "mozilla.org", this will return true.  It would return false the other way
-     * around.
-     */
-    function hasRootDomain(str, aDomain)
-    {
-      let index = str.indexOf(aDomain);
-      // If aDomain is not found, we know we do not have it as a root domain.
-      if (index == -1)
-        return false;
-
-      // If the strings are the same, we obviously have a match.
-      if (str == aDomain)
-        return true;
-
-      // Otherwise, we have aDomain as our root domain iff the index of aDomain is
-      // aDomain.length subtracted from our length and (since we do not have an
-      // exact match) the character before the index is a dot or slash.
-      let prevChar = str[index - 1];
-      return (index == (str.length - aDomain.length)) &&
-             (prevChar == "." || prevChar == "/");
-    }
-
-    let clear = (db, domain) => {
-      db.clearIf(record => {
-        return record.uri && hasRootDomain(record.uri.prePath, domain);
-      });
-    }
-
-    return this._checkActivated()
-      .then(_ => clear(this._db, domain))
+      .then(_ => {
+        return this._dropRegistrationsIf(record =>
+          info.domain == "*" ||
+          (record.uri && hasRootDomain(record.uri.prePath, info.domain))
+        );
+      })
       .catch(e => {
-        console.warn("clearForDomain: Error forgetting about domain", e);
+        console.warn("clear: Error dropping subscriptions for domain",
+          info.domain, e);
         return Promise.resolve();
       });
   },
 
   registration: function(aPageRecord) {
     console.debug("registration()");
 
     return this._getByPageRecord(aPageRecord)
@@ -1298,26 +1279,18 @@ this.PushService = {
   _clearPermissions() {
     console.debug("clearPermissions()");
 
     return this._db.clearIf(record => {
       if (!record.quotaApplies()) {
         // Only drop registrations that are subject to quota.
         return false;
       }
-      if (record.isExpired()) {
-        // Fire subscription modified notifications for expired
-        // records.
-        gPushNotifier.notifySubscriptionModified(record.scope,
-                                                 record.principal);
-      } else {
-        // Drop unexpired registrations in the background.
-        this._backgroundUnregister(record,
-          Ci.nsIPushErrorReporter.UNSUBSCRIBE_PERMISSION_REVOKED);
-      }
+      this._backgroundUnregister(record,
+        Ci.nsIPushErrorReporter.UNSUBSCRIBE_PERMISSION_REVOKED);
       return true;
     });
   },
 
   _updatePermission: function(permission, type) {
     console.debug("updatePermission()");
 
     let isAllow = permission.capability ==
@@ -1395,9 +1368,38 @@ this.PushService = {
       // unconditionally.
       this._notifySubscriptionChangeObservers(record);
       cursor.delete();
       return;
     }
     record.resetQuota();
     cursor.update(record);
   },
+
+  /**
+   * Drops all matching registrations from the database. Notifies the
+   * associated service workers if permission is granted, and removes
+   * unexpired registrations from the server.
+   *
+   * @param {Function} predicate A function called for each record.
+   * @returns {Promise} Resolves once the registrations have been dropped.
+   */
+  _dropRegistrationsIf(predicate) {
+    return this._db.clearIf(record => {
+      if (!predicate(record)) {
+        return false;
+      }
+      if (record.hasPermission()) {
+        // "Clear Recent History" and the Forget button remove permissions
+        // before clearing registrations, but it's possible for the worker to
+        // resubscribe if the "dom.push.testing.ignorePermission" pref is set.
+        this._notifySubscriptionChangeObservers(record);
+      }
+      if (!record.isExpired()) {
+        // Only unregister active registrations, since we already told the
+        // server about expired ones.
+        this._backgroundUnregister(record,
+                                   Ci.nsIPushErrorReporter.UNSUBSCRIBE_MANUAL);
+      }
+      return true;
+    });
+  },
 };
--- a/dom/push/test/xpcshell/head.js
+++ b/dom/push/test/xpcshell/head.js
@@ -429,8 +429,28 @@ var tearDownServiceInParent = Task.async
       { appId: 1, inIsolatedMozBrowser: true }),
   });
   ok(record.pushEndpoint.startsWith('https://example.org/push'),
     'Wrong push endpoint in app record');
 
   record = yield db.getByKeyID('3a414737-2fd0-44c0-af05-7efc172475fc');
   ok(!record, 'Unsubscribed record should not exist');
 });
+
+function putTestRecord(db, keyID, scope, quota) {
+  return db.put({
+    channelID: keyID,
+    pushEndpoint: 'https://example.org/push/' + keyID,
+    scope: scope,
+    pushCount: 0,
+    lastPush: 0,
+    version: null,
+    originAttributes: '',
+    quota: quota,
+    systemRecord: quota == Infinity,
+  });
+}
+
+function getAllKeyIDs(db) {
+  return db.getAllKeyIDs().then(records =>
+    records.map(record => record.keyID).sort(compareAscending)
+  );
+}
--- a/dom/push/test/xpcshell/test_clearAll_successful.js
+++ b/dom/push/test/xpcshell/test_clearAll_successful.js
@@ -1,47 +1,110 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 'use strict';
 
 const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
 
-const channelID = 'db0a7021-ec2d-4bd3-8802-7a6966f10ed8';
+var db;
+var unregisterDefers = {};
+var userAgentID = '4ce480ef-55b2-4f83-924c-dcd35ab978b4';
+
+function promiseUnregister(keyID, code) {
+  return new Promise(r => unregisterDefers[keyID] = r);
+}
 
 function run_test() {
   do_get_profile();
-  setPrefs();
+  setPrefs({
+    userAgentID: userAgentID,
+  });
   run_next_test();
 }
 
-add_task(function* test_unregister_success() {
-  let db = PushServiceWebSocket.newPushDB();
-  do_register_cleanup(() => {return db.drop().then(_ => db.close());});
-  yield db.put({
-    channelID,
-    pushEndpoint: 'https://example.org/update/unregister-success',
-    scope: 'https://example.com/page/unregister-success',
-    version: 1,
-    originAttributes: '',
-    quota: Infinity,
-  });
+add_task(function* setup() {
+  db = PushServiceWebSocket.newPushDB();
+  do_register_cleanup(_ => db.drop().then(_ => db.close()));
+
+  // Active subscriptions; should be expired then dropped.
+  yield putTestRecord(db, 'active-1', 'https://example.info/some-page', 8);
+  yield putTestRecord(db, 'active-2', 'https://example.com/another-page', 16);
 
+  // Expired subscription; should be dropped.
+  yield putTestRecord(db, 'expired', 'https://example.net/yet-another-page', 0);
+
+  // A privileged subscription that should not be affected by sanitizing data
+  // because its quota is set to `Infinity`.
+  yield putTestRecord(db, 'privileged', 'app://chrome/only', Infinity);
+
+  let handshakeDone;
+  let handshakePromise = new Promise(r => handshakeDone = r);
   PushService.init({
-    serverURI: "wss://push.example.org/",
-    db,
+    serverURI: 'wss://push.example.org/',
+    db: db,
     makeWebSocket(uri) {
       return new MockWebSocket(uri, {
         onHello(request) {
           this.serverSendMsg(JSON.stringify({
             messageType: 'hello',
+            uaid: userAgentID,
             status: 200,
-            uaid: 'fbe865a6-aeb8-446f-873c-aeebdb8d493c'
+            use_webpush: true,
           }));
-        }
+          handshakeDone();
+        },
+        onUnregister(request) {
+          let resolve = unregisterDefers[request.channelID];
+          equal(typeof resolve, 'function',
+            'Dropped unexpected channel ID ' + request.channelID);
+          delete unregisterDefers[request.channelID];
+          equal(request.code, 200,
+            'Expected manual unregister reason');
+          resolve();
+        },
       });
-    }
+    },
+  });
+  yield handshakePromise;
+});
+
+add_task(function* test_sanitize() {
+  let modifiedScopes = [];
+  let changeScopes = [];
+
+  let promiseCleared = Promise.all([
+    // Active subscriptions should be unregistered.
+    promiseUnregister('active-1'),
+    promiseUnregister('active-2'),
+    promiseObserverNotification(
+      PushServiceComponent.subscriptionModifiedTopic, (subject, data) => {
+        modifiedScopes.push(data);
+        return modifiedScopes.length == 3;
+    }),
+
+    // Privileged should be recreated.
+    promiseUnregister('privileged'),
+    promiseObserverNotification(
+      PushServiceComponent.subscriptionChangeTopic, (subject, data) => {
+        changeScopes.push(data);
+        return changeScopes.length == 1;
+    }),
+  ]);
+
+  yield PushService.clear({
+    domain: '*',
   });
 
-  yield PushService._clearAll();
-  let record = yield db.getByKeyID(channelID);
-  ok(!record, 'Unregister did not remove record');
+  yield promiseCleared;
+
+  deepEqual(modifiedScopes.sort(compareAscending), [
+    'app://chrome/only',
+    'https://example.com/another-page',
+    'https://example.info/some-page',
+  ], 'Should modify active subscription scopes');
+
+  deepEqual(changeScopes, ['app://chrome/only'],
+    'Should fire change notification for privileged scope');
+
+  let remainingIDs = yield getAllKeyIDs(db);
+  deepEqual(remainingIDs, [], 'Should drop all subscriptions');
 });
new file mode 100644
--- /dev/null
+++ b/dom/push/test/xpcshell/test_clear_forgetAboutSite.js
@@ -0,0 +1,123 @@
+'use strict';
+
+const {PushService, PushServiceWebSocket} = serviceExports;
+const {ForgetAboutSite} = Cu.import(
+  'resource://gre/modules/ForgetAboutSite.jsm', {});
+
+var db;
+var unregisterDefers = {};
+var userAgentID = '4fe01c2d-72ac-4c13-93d2-bb072caf461d';
+
+function promiseUnregister(keyID) {
+  return new Promise(r => unregisterDefers[keyID] = r);
+}
+
+function run_test() {
+  do_get_profile();
+  setPrefs({
+    userAgentID: userAgentID,
+  });
+  run_next_test();
+}
+
+add_task(function* setup() {
+  db = PushServiceWebSocket.newPushDB();
+  do_register_cleanup(_ => db.drop().then(_ => db.close()));
+
+  // Active and expired subscriptions for a subdomain. The active subscription
+  // should be expired, then removed; the expired subscription should be
+  // removed immediately.
+  yield putTestRecord(db, 'active-sub', 'https://sub.example.com/sub-page', 4);
+  yield putTestRecord(db, 'expired-sub', 'https://sub.example.com/yet-another-page', 0);
+
+  // Active subscriptions for another subdomain. Should be unsubscribed and
+  // dropped.
+  yield putTestRecord(db, 'active-1', 'https://sub2.example.com/some-page', 8);
+  yield putTestRecord(db, 'active-2', 'https://sub3.example.com/another-page', 16);
+
+  // A privileged subscription with a real URL that should not be affected
+  // because its quota is set to `Infinity`.
+  yield putTestRecord(db, 'privileged', 'https://sub.example.com/real-url', Infinity);
+
+  let handshakeDone;
+  let handshakePromise = new Promise(r => handshakeDone = r);
+  PushService.init({
+    serverURI: 'wss://push.example.org/',
+    db: db,
+    makeWebSocket(uri) {
+      return new MockWebSocket(uri, {
+        onHello(request) {
+          this.serverSendMsg(JSON.stringify({
+            messageType: 'hello',
+            uaid: userAgentID,
+            status: 200,
+            use_webpush: true,
+          }));
+          handshakeDone();
+        },
+        onUnregister(request) {
+          let resolve = unregisterDefers[request.channelID];
+          equal(typeof resolve, 'function',
+            'Dropped unexpected channel ID ' + request.channelID);
+          delete unregisterDefers[request.channelID];
+          equal(request.code, 200,
+            'Expected manual unregister reason');
+          resolve();
+        },
+      });
+    },
+  });
+  // For cleared subscriptions, we only send unregister requests in the
+  // background and if we're connected.
+  yield handshakePromise;
+});
+
+add_task(function* test_forgetAboutSubdomain() {
+  let modifiedScopes = [];
+  let promiseForgetSubs = Promise.all([
+    // Active subscriptions should be dropped.
+    promiseUnregister('active-sub'),
+    promiseObserverNotification(
+      PushServiceComponent.subscriptionModifiedTopic, (subject, data) => {
+        modifiedScopes.push(data);
+        return modifiedScopes.length == 1;
+      }
+    ),
+  ]);
+  yield ForgetAboutSite.removeDataFromDomain('sub.example.com');
+  yield promiseForgetSubs;
+
+  deepEqual(modifiedScopes.sort(compareAscending), [
+    'https://sub.example.com/sub-page',
+  ], 'Should fire modified notifications for active subscriptions');
+
+  let remainingIDs = yield getAllKeyIDs(db);
+  deepEqual(remainingIDs, ['active-1', 'active-2', 'privileged'],
+    'Should only forget subscriptions for subdomain');
+});
+
+add_task(function* test_forgetAboutRootDomain() {
+  let modifiedScopes = [];
+  let promiseForgetSubs = Promise.all([
+    promiseUnregister('active-1'),
+    promiseUnregister('active-2'),
+    promiseObserverNotification(
+      PushServiceComponent.subscriptionModifiedTopic, (subject, data) => {
+        modifiedScopes.push(data);
+        return modifiedScopes.length == 2;
+      }
+    ),
+  ]);
+
+  yield ForgetAboutSite.removeDataFromDomain('example.com');
+  yield promiseForgetSubs;
+
+  deepEqual(modifiedScopes.sort(compareAscending), [
+    'https://sub2.example.com/some-page',
+    'https://sub3.example.com/another-page',
+  ], 'Should fire modified notifications for entire domain');
+
+  let remainingIDs = yield getAllKeyIDs(db);
+  deepEqual(remainingIDs, ['privileged'],
+    'Should ignore privileged records with a real URL');
+});
--- a/dom/push/test/xpcshell/test_permissions.js
+++ b/dom/push/test/xpcshell/test_permissions.js
@@ -18,28 +18,18 @@ function run_test() {
   db = PushServiceWebSocket.newPushDB();
   do_register_cleanup(() => {return db.drop().then(_ => db.close());});
 
   run_next_test();
 }
 
 let unregisterDefers = {};
 
-function putRecord(channelID, scope, quota) {
-  return db.put({
-    channelID: channelID,
-    pushEndpoint: 'https://example.org/push/' + channelID,
-    scope: scope,
-    pushCount: 0,
-    lastPush: 0,
-    version: null,
-    originAttributes: '',
-    quota: quota,
-    systemRecord: quota == Infinity,
-  });
+function promiseUnregister(keyID) {
+  return new Promise(r => unregisterDefers[keyID] = r);
 }
 
 function makePushPermission(url, capability) {
   return {
     QueryInterface: XPCOMUtils.generateQI([Ci.nsIPermission]),
     capability: Ci.nsIPermissionManager[capability],
     expireTime: 0,
     expireType: Ci.nsIPermissionManager.EXPIRE_NEVER,
@@ -75,42 +65,42 @@ function allExpired(...keyIDs) {
   )).then(records =>
     records.every(record => record.isExpired())
   );
 }
 
 add_task(function* setUp() {
   // Active registration; quota should be reset to 16. Since the quota isn't
   // exposed to content, we shouldn't receive a subscription change event.
-  yield putRecord('active-allow', 'https://example.info/page/1', 8);
+  yield putTestRecord(db, 'active-allow', 'https://example.info/page/1', 8);
 
   // Expired registration; should be dropped.
-  yield putRecord('expired-allow', 'https://example.info/page/2', 0);
+  yield putTestRecord(db, 'expired-allow', 'https://example.info/page/2', 0);
 
   // Active registration; should be expired when we change the permission
   // to "deny".
-  yield putRecord('active-deny-changed', 'https://example.xyz/page/1', 16);
+  yield putTestRecord(db, 'active-deny-changed', 'https://example.xyz/page/1', 16);
 
   // Two active registrations for a visited site. These will expire when we
   // add a "deny" permission.
-  yield putRecord('active-deny-added-1', 'https://example.net/ham', 16);
-  yield putRecord('active-deny-added-2', 'https://example.net/green', 8);
+  yield putTestRecord(db, 'active-deny-added-1', 'https://example.net/ham', 16);
+  yield putTestRecord(db, 'active-deny-added-2', 'https://example.net/green', 8);
 
   // An already-expired registration for a visited site. We shouldn't send an
   // `unregister` request for this one, but still receive an observer
   // notification when we restore permissions.
-  yield putRecord('expired-deny-added', 'https://example.net/eggs', 0);
+  yield putTestRecord(db, 'expired-deny-added', 'https://example.net/eggs', 0);
 
   // A registration that should not be affected by permission list changes
   // because its quota is set to `Infinity`.
-  yield putRecord('never-expires', 'app://chrome/only', Infinity);
+  yield putTestRecord(db, 'never-expires', 'app://chrome/only', Infinity);
 
   // A registration that should be dropped when we clear the permission
   // list.
-  yield putRecord('drop-on-clear', 'https://example.edu/lonely', 16);
+  yield putTestRecord(db, 'drop-on-clear', 'https://example.edu/lonely', 16);
 
   let handshakeDone;
   let handshakePromise = new Promise(resolve => handshakeDone = resolve);
   PushService.init({
     serverURI: 'wss://push.example.org/',
     db,
     makeWebSocket(uri) {
       return new MockWebSocket(uri, {
@@ -157,18 +147,17 @@ add_task(function* test_permissions_allo
 
   record = yield db.getByKeyID('expired-allow');
   ok(!record, 'Should drop expired records after adding allow');
 });
 
 add_task(function* test_permissions_allow_deleted() {
   let subModifiedPromise = promiseSubscriptionModifications(1);
 
-  let unregisterPromise = new Promise(resolve => unregisterDefers[
-    'active-allow'] = resolve);
+  let unregisterPromise = promiseUnregister('active-allow');
 
   yield PushService._onPermissionChange(
     makePushPermission('https://example.info', 'ALLOW_ACTION'),
     'deleted'
   );
 
   yield unregisterPromise;
 
@@ -181,20 +170,18 @@ add_task(function* test_permissions_allo
   ok(record.isExpired(),
     'Should expire active record after deleting allow');
 });
 
 add_task(function* test_permissions_deny_added() {
   let subModifiedPromise = promiseSubscriptionModifications(2);
 
   let unregisterPromise = Promise.all([
-    new Promise(resolve => unregisterDefers[
-      'active-deny-added-1'] = resolve),
-    new Promise(resolve => unregisterDefers[
-      'active-deny-added-2'] = resolve),
+    promiseUnregister('active-deny-added-1'),
+    promiseUnregister('active-deny-added-2'),
   ]);
 
   yield PushService._onPermissionChange(
     makePushPermission('https://example.net', 'DENY_ACTION'),
     'added'
   );
   yield unregisterPromise;
 
@@ -247,18 +234,17 @@ add_task(function* test_permissions_allo
   ]);
   ok(!droppedRecords.some(Boolean),
     'Should drop all expired registrations after changing to allow');
 });
 
 add_task(function* test_permissions_deny_changed() {
   let subModifiedPromise = promiseSubscriptionModifications(1);
 
-  let unregisterPromise = new Promise(resolve => unregisterDefers[
-    'active-deny-changed'] = resolve);
+  let unregisterPromise = promiseUnregister('active-deny-changed');
 
   yield PushService._onPermissionChange(
     makePushPermission('https://example.xyz', 'DENY_ACTION'),
     'changed'
   );
 
   yield unregisterPromise;
 
@@ -270,35 +256,36 @@ add_task(function* test_permissions_deny
   let record = yield db.getByKeyID('active-deny-changed');
   ok(record.isExpired(),
     'Should expire active record after changing to deny');
 });
 
 add_task(function* test_permissions_clear() {
   let subModifiedPromise = promiseSubscriptionModifications(3);
 
-  let records = yield db.getAllKeyIDs();
-  deepEqual(records.map(record => record.keyID).sort(), [
+  deepEqual(yield getAllKeyIDs(db), [
     'active-allow',
     'active-deny-changed',
     'drop-on-clear',
     'never-expires',
   ], 'Wrong records in database before clearing');
 
-  let unregisterPromise = new Promise(resolve => unregisterDefers[
-      'drop-on-clear'] = resolve);
+  let unregisterPromise = Promise.all([
+    promiseUnregister('active-allow'),
+    promiseUnregister('active-deny-changed'),
+    promiseUnregister('drop-on-clear'),
+  ]);
 
   yield PushService._onPermissionChange(null, 'cleared');
 
   yield unregisterPromise;
 
   let notifiedScopes = yield subModifiedPromise;
   deepEqual(notifiedScopes, [
     'https://example.edu/lonely',
     'https://example.info/page/1',
     'https://example.xyz/page/1',
   ], 'Wrong scopes modified after clearing registrations');
 
-  records = yield db.getAllKeyIDs();
-  deepEqual(records.map(record => record.keyID).sort(), [
+  deepEqual(yield getAllKeyIDs(db), [
     'never-expires',
   ], 'Unrestricted registrations should not be dropped');
 });
--- a/dom/push/test/xpcshell/xpcshell.ini
+++ b/dom/push/test/xpcshell/xpcshell.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 head = head.js head-http2.js
 tail =
 # Push notifications and alarms are currently disabled on Android.
 skip-if = toolkit == 'android'
 
+[test_clear_forgetAboutSite.js]
 [test_clear_origin_data.js]
 [test_crypto.js]
 [test_drop_expired.js]
 [test_handler_service.js]
 support-files = PushServiceHandler.js PushServiceHandler.manifest
 [test_notification_ack.js]
 [test_notification_data.js]
 [test_notification_duplicate.js]