Bug 1239558 - Exempt system Push subscriptions from quota and permissions checks. r?dragana draft
authorKit Cambridge <kcambridge@mozilla.com>
Wed, 16 Dec 2015 09:21:22 -0800
changeset 323333 62973ea07db8e4f0a52a28930e0baa136b504c76
parent 322694 b67316254602a63bf4e568198a5c7d3288a9db27
child 513196 99f45150392c78485925a01120f71ece510f381c
push id9703
push userkcambridge@mozilla.com
push dateWed, 20 Jan 2016 02:15:27 +0000
reviewersdragana
bugs1239558
milestone46.0a1
Bug 1239558 - Exempt system Push subscriptions from quota and permissions checks. r?dragana
dom/push/PushComponents.js
dom/push/PushRecord.jsm
dom/push/PushService.jsm
dom/push/PushServiceHttp2.jsm
dom/push/PushServiceWebSocket.jsm
dom/push/test/xpcshell/test_notification_ack.js
dom/push/test/xpcshell/test_notification_data.js
dom/push/test/xpcshell/test_notification_duplicate.js
dom/push/test/xpcshell/test_notification_error.js
dom/push/test/xpcshell/test_notification_http2.js
dom/push/test/xpcshell/test_notification_version_string.js
dom/push/test/xpcshell/test_permissions.js
dom/push/test/xpcshell/test_quota_exceeded.js
dom/push/test/xpcshell/test_quota_with_notification.js
dom/push/test/xpcshell/test_register_flush.js
dom/push/test/xpcshell/test_register_success_http2.js
dom/push/test/xpcshell/test_service_child.js
dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js
--- a/dom/push/PushComponents.js
+++ b/dom/push/PushComponents.js
@@ -140,18 +140,17 @@ Object.assign(PushServiceParent.prototyp
     }).then(result => {
       this._deliverSubscription(callback, result);
     }, error => {
       callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null);
     }).catch(Cu.reportError);
   },
 
   clearForDomain(domain, callback) {
-    let principal = Services.scriptSecurityManager.getSystemPrincipal();
-    return this._handleRequest("Push:Clear", principal, {
+    return this._handleRequest("Push:Clear", null, {
       domain: domain,
     }).then(result => {
       callback.onClear(Cr.NS_OK);
     }, error => {
       callback.onClear(Cr.NS_ERROR_FAILURE);
     }).catch(Cu.reportError);
   },
 
@@ -205,28 +204,35 @@ Object.assign(PushServiceParent.prototyp
   _handleReady() {
     this._service.init();
   },
 
   _toPageRecord(principal, data) {
     if (!data.scope) {
       throw new Error("Invalid page record: missing scope");
     }
+    if (!principal) {
+      throw new Error("Invalid page record: missing principal");
+    }
+    if (principal.isNullPrincipal || principal.isExpandedPrincipal) {
+      throw new Error("Invalid page record: unsupported principal");
+    }
+
+    // System subscriptions can only be created by chrome callers, and are
+    // exempt from the background message quota and permission checks. They
+    // also use XPCOM observer notifications instead of service worker events.
+    data.systemRecord = principal.isSystemPrincipal;
 
     data.originAttributes =
       ChromeUtils.originAttributesToSuffix(principal.originAttributes);
 
     return data;
   },
 
   _handleRequest(name, principal, data) {
-    if (!principal) {
-      return Promise.reject(new Error("Invalid request: missing principal"));
-    }
-
     if (name == "Push:Clear") {
       return this._service.clear(data);
     }
 
     let pageRecord;
     try {
       pageRecord = this._toPageRecord(principal, data);
     } catch (e) {
--- a/dom/push/PushRecord.jsm
+++ b/dom/push/PushRecord.jsm
@@ -35,31 +35,33 @@ function PushRecord(props) {
   this.pushEndpoint = props.pushEndpoint;
   this.scope = props.scope;
   this.originAttributes = props.originAttributes;
   this.pushCount = props.pushCount || 0;
   this.lastPush = props.lastPush || 0;
   this.p256dhPublicKey = props.p256dhPublicKey;
   this.p256dhPrivateKey = props.p256dhPrivateKey;
   this.authenticationSecret = props.authenticationSecret;
+  this.systemRecord = !!props.systemRecord;
   this.setQuota(props.quota);
   this.ctime = (typeof props.ctime === "number") ? props.ctime : 0;
 }
 
 PushRecord.prototype = {
   setQuota(suggestedQuota) {
-    if (!isNaN(suggestedQuota) && suggestedQuota >= 0) {
+    if (this.quotaApplies() && !isNaN(suggestedQuota) && suggestedQuota >= 0) {
       this.quota = suggestedQuota;
     } else {
       this.resetQuota();
     }
   },
 
   resetQuota() {
-    this.quota = prefs.get("maxQuotaPerSubscription");
+    this.quota = this.quotaApplies() ?
+                 prefs.get("maxQuotaPerSubscription") : Infinity;
   },
 
   updateQuota(lastVisit) {
     if (this.isExpired() || !this.quotaApplies()) {
       // Ignore updates if the registration is already expired, or isn't
       // subject to quota.
       return;
     }
@@ -83,16 +85,19 @@ PushRecord.prototype = {
 
   receivedPush(lastVisit) {
     this.updateQuota(lastVisit);
     this.pushCount++;
     this.lastPush = Date.now();
   },
 
   reduceQuota() {
+    if (!this.quotaApplies()) {
+      return;
+    }
     this.quota = Math.max(this.quota - 1, 0);
     // We check for ctime > 0 to skip older records that did not have ctime.
     if (this.isExpired() && this.ctime > 0) {
       let duration = Date.now() - this.ctime;
       Services.telemetry.getHistogramById("PUSH_API_QUOTA_EXPIRATION_TIME").add(duration / 1000);
     }
   },
 
@@ -184,41 +189,48 @@ PushRecord.prototype = {
         }
       }
     }
     return false;
   },
 
   /**
    * Indicates whether the registration can deliver push messages to its
-   * associated service worker.
+   * associated service worker. System subscriptions are exempt from the
+   * permission check.
    */
   hasPermission() {
+    if (this.systemRecord || prefs.get("testing.ignorePermission")) {
+      return true;
+    }
     let permission = Services.perms.testExactPermissionFromPrincipal(
       this.principal, "desktop-notification");
     return permission == Ci.nsIPermissionManager.ALLOW_ACTION;
   },
 
   quotaChanged() {
     if (!this.hasPermission()) {
       return Promise.resolve(false);
     }
     return this.getLastVisit()
       .then(lastVisit => lastVisit > this.lastPush);
   },
 
   quotaApplies() {
-    return Number.isFinite(this.quota);
+    return !this.systemRecord;
   },
 
   isExpired() {
     return this.quota === 0;
   },
 
   matchesOriginAttributes(pattern) {
+    if (this.systemRecord) {
+      return false;
+    }
     return ChromeUtils.originAttributesMatchPattern(
       this.principal.originAttributes, pattern);
   },
 
   toSubscription() {
     return {
       endpoint: this.pushEndpoint,
       lastPush: this.lastPush,
@@ -231,16 +243,19 @@ PushRecord.prototype = {
 };
 
 // Define lazy getters for the principal and scope URI. IndexedDB can't store
 // `nsIPrincipal` objects, so we keep them in a private weak map.
 var principals = new WeakMap();
 Object.defineProperties(PushRecord.prototype, {
   principal: {
     get() {
+      if (this.systemRecord) {
+        return Services.scriptSecurityManager.getSystemPrincipal();
+      }
       let principal = principals.get(this);
       if (!principal) {
         let url = this.scope;
         if (this.originAttributes) {
           // Allow tests to omit origin attributes.
           url += this.originAttributes;
         }
         principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(url);
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -603,16 +603,20 @@ this.PushService = {
 
     this._stateChangeProcessEnqueue(_ =>
             this._changeServerURL("", UNINIT_EVENT));
     console.debug("uninit: shutdown complete!");
   },
 
   /** |delay| should be in milliseconds. */
   setAlarm: function(delay) {
+    if (this._state <= PUSH_SERVICE_ACTIVATING) {
+      return;
+    }
+
     // Bug 909270: Since calls to AlarmService.add() are async, calls must be
     // 'queued' to ensure only one alarm is ever active.
     if (this._settingAlarm) {
         // onSuccess will handle the set. Overwriting the variable enforces the
         // last-writer-wins semantics.
         this._queuedAlarmDelay = delay;
         this._waitingForAlarmSet = true;
         return;
@@ -892,17 +896,17 @@ this.PushService = {
       // Record may have expired from an earlier quota update.
       if (record.isExpired()) {
         console.debug(
           "updateQuota: Trying to update quota for expired record", record);
         return null;
       }
       // If there are visible notifications, don't apply the quota penalty
       // for the message.
-      if (!this._visibleNotifications.has(record.uri.prePath)) {
+      if (record.uri && !this._visibleNotifications.has(record.uri.prePath)) {
         record.reduceQuota();
       }
       return record;
     }).then(record => {
       if (record && record.isExpired()) {
         this._recordDidNotNotify(kDROP_NOTIFICATION_REASON_EXPIRED);
         // Drop the registration in the background. If the user returns to the
         // site, the service worker will be notified on the next `idle-daily`
@@ -1198,17 +1202,17 @@ this.PushService = {
       // 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 hasRootDomain(record.uri.prePath, domain);
+        return record.uri && hasRootDomain(record.uri.prePath, domain);
       });
     }
 
     return this._checkActivated()
       .then(_ => clear(this._db, domain))
       .catch(e => {
         console.warn("clearForDomain: Error forgetting about domain", e);
         return Promise.resolve();
--- a/dom/push/PushServiceHttp2.jsm
+++ b/dom/push/PushServiceHttp2.jsm
@@ -316,17 +316,17 @@ SubscriptionListener.prototype = {
     }
 
     let reply = new PushRecordHttp2({
       subscriptionUri: subscriptionUri,
       pushEndpoint: linkParserResult.pushEndpoint,
       pushReceiptEndpoint: linkParserResult.pushReceiptEndpoint,
       scope: this._subInfo.record.scope,
       originAttributes: this._subInfo.record.originAttributes,
-      quota: this._subInfo.record.maxQuota,
+      systemRecord: this._subInfo.record.systemRecord,
       ctime: Date.now(),
     });
 
     Services.telemetry.getHistogramById("PUSH_API_SUBSCRIBE_HTTP2_TIME").add(Date.now() - this._ctime);
     this._resolve(reply);
   },
 };
 
--- a/dom/push/PushServiceWebSocket.jsm
+++ b/dom/push/PushServiceWebSocket.jsm
@@ -837,17 +837,17 @@ this.PushServiceWebSocket = {
       }
 
       let record = new PushRecordWebSocket({
         channelID: reply.channelID,
         pushEndpoint: reply.pushEndpoint,
         scope: tmp.record.scope,
         originAttributes: tmp.record.originAttributes,
         version: null,
-        quota: tmp.record.maxQuota,
+        systemRecord: tmp.record.systemRecord,
         ctime: Date.now(),
       });
       Services.telemetry.getHistogramById("PUSH_API_SUBSCRIBE_WS_TIME").add(Date.now() - tmp.ctime);
       tmp.resolve(record);
     } else {
       console.error("handleRegisterReply: Unexpected server response", reply);
       tmp.reject(new Error("Wrong status code for register reply: " +
         reply.status));
--- a/dom/push/test/xpcshell/test_notification_ack.js
+++ b/dom/push/test/xpcshell/test_notification_ack.js
@@ -23,30 +23,33 @@ add_task(function* test_notification_ack
   do_register_cleanup(() => {return db.drop().then(_ => db.close());});
   let records = [{
     channelID: '21668e05-6da8-42c9-b8ab-9cc3f4d5630c',
     pushEndpoint: 'https://example.com/update/1',
     scope: 'https://example.org/1',
     originAttributes: '',
     version: 1,
     quota: Infinity,
+    systemRecord: true,
   }, {
     channelID: '9a5ff87f-47c9-4215-b2b8-0bdd38b4b305',
     pushEndpoint: 'https://example.com/update/2',
     scope: 'https://example.org/2',
     originAttributes: '',
     version: 2,
     quota: Infinity,
+    systemRecord: true,
   }, {
     channelID: '5477bfda-22db-45d4-9614-fee369630260',
     pushEndpoint: 'https://example.com/update/3',
     scope: 'https://example.org/3',
     originAttributes: '',
     version: 3,
     quota: Infinity,
+    systemRecord: true,
   }];
   for (let record of records) {
     yield db.put(record);
   }
 
   let notifyPromise = Promise.all([
     promiseObserverNotification('push-notification'),
     promiseObserverNotification('push-notification'),
--- a/dom/push/test/xpcshell/test_notification_data.js
+++ b/dom/push/test/xpcshell/test_notification_data.js
@@ -21,16 +21,17 @@ function putRecord(channelID, scope, pub
   return db.put({
     channelID: channelID,
     pushEndpoint: 'https://example.org/push/' + channelID,
     scope: scope,
     pushCount: 0,
     lastPush: 0,
     originAttributes: '',
     quota: Infinity,
+    systemRecord: true,
     p256dhPublicKey: base64UrlDecode(publicKey),
     p256dhPrivateKey: privateKey,
     authenticationSecret: base64UrlDecode(authSecret),
   });
 }
 
 let ackDone;
 let server;
--- a/dom/push/test/xpcshell/test_notification_duplicate.js
+++ b/dom/push/test/xpcshell/test_notification_duplicate.js
@@ -25,23 +25,25 @@ add_task(function* test_notification_dup
   do_register_cleanup(() => {return db.drop().then(_ => db.close());});
   let records = [{
     channelID: '8d2d9400-3597-4c5a-8a38-c546b0043bcc',
     pushEndpoint: 'https://example.org/update/1',
     scope: 'https://example.com/1',
     originAttributes: "",
     version: 2,
     quota: Infinity,
+    systemRecord: true,
   }, {
     channelID: '27d1e393-03ef-4c72-a5e6-9e890dfccad0',
     pushEndpoint: 'https://example.org/update/2',
     scope: 'https://example.com/2',
     originAttributes: "",
     version: 2,
     quota: Infinity,
+    systemRecord: true,
   }];
   for (let record of records) {
     yield db.put(record);
   }
 
   let notifyPromise = promiseObserverNotification('push-notification');
 
   let acks = 0;
--- a/dom/push/test/xpcshell/test_notification_error.js
+++ b/dom/push/test/xpcshell/test_notification_error.js
@@ -27,30 +27,33 @@ add_task(function* test_notification_err
   let originAttributes = '';
   let records = [{
     channelID: 'f04f1e46-9139-4826-b2d1-9411b0821283',
     pushEndpoint: 'https://example.org/update/success-1',
     scope: 'https://example.com/a',
     originAttributes: originAttributes,
     version: 1,
     quota: Infinity,
+    systemRecord: true,
   }, {
     channelID: '3c3930ba-44de-40dc-a7ca-8a133ec1a866',
     pushEndpoint: 'https://example.org/update/error',
     scope: 'https://example.com/b',
     originAttributes: originAttributes,
     version: 2,
     quota: Infinity,
+    systemRecord: true,
   }, {
     channelID: 'b63f7bef-0a0d-4236-b41e-086a69dfd316',
     pushEndpoint: 'https://example.org/update/success-2',
     scope: 'https://example.com/c',
     originAttributes: originAttributes,
     version: 3,
     quota: Infinity,
+    systemRecord: true,
   }];
   for (let record of records) {
     yield db.put(record);
   }
 
   let notifyPromise = Promise.all([
     promiseObserverNotification(
       'push-notification',
--- a/dom/push/test/xpcshell/test_notification_http2.js
+++ b/dom/push/test/xpcshell/test_notification_http2.js
@@ -73,16 +73,17 @@ add_task(function* test_pushNotification
       key_ops: ["deriveBits"],
       kty: "EC",
       x: '8J3iA1CSPBFqHrUul0At3NkosudTlQDAPO1Dn-HRCxM',
       y: '26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA'
     },
     originAttributes: ChromeUtils.originAttributesToSuffix(
       { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
     quota: Infinity,
+    systemRecord: true,
   }, {
     subscriptionUri: serverURL + '/pushNotifications/subscription2',
     pushEndpoint: serverURL + '/pushEndpoint2',
     pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint2',
     scope: 'https://example.com/page/2',
     p256dhPublicKey: 'BPnWyUo7yMnuMlyKtERuLfWE8a09dtdjHSW2lpC9_BqR5TZ1rK8Ldih6ljyxVwnBA-nygQHGRpEmu1jV5K8437E',
     p256dhPrivateKey: {
       crv: 'P-256',
@@ -91,16 +92,17 @@ add_task(function* test_pushNotification
       key_ops: ["deriveBits"],
       kty: 'EC',
       x: '-dbJSjvIye4yXIq0RG4t9YTxrT1212MdJbaWkL38GpE',
       y: '5TZ1rK8Ldih6ljyxVwnBA-nygQHGRpEmu1jV5K8437E'
     },
     originAttributes: ChromeUtils.originAttributesToSuffix(
       { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
     quota: Infinity,
+    systemRecord: true,
   }, {
     subscriptionUri: serverURL + '/pushNotifications/subscription3',
     pushEndpoint: serverURL + '/pushEndpoint3',
     pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint3',
     scope: 'https://example.com/page/3',
     p256dhPublicKey: 'BDhUHITSeVrWYybFnb7ylVTCDDLPdQWMpf8gXhcWwvaaJa6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI',
     p256dhPrivateKey: {
       crv: 'P-256',
@@ -109,16 +111,17 @@ add_task(function* test_pushNotification
       key_ops: ["deriveBits"],
       kty: 'EC',
       x: 'OFQchNJ5WtZjJsWdvvKVVMIMMs91BYyl_yBeFxbC9po',
       y: 'Ja6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI'
     },
     originAttributes: ChromeUtils.originAttributesToSuffix(
       { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
     quota: Infinity,
+    systemRecord: true,
   }];
 
   for (let record of records) {
     yield db.put(record);
   }
 
   let notifyPromise = Promise.all([
     promiseObserverNotification('push-notification', function(subject, data) {
--- a/dom/push/test/xpcshell/test_notification_version_string.js
+++ b/dom/push/test/xpcshell/test_notification_version_string.js
@@ -23,16 +23,17 @@ add_task(function* test_notification_ver
   do_register_cleanup(() => {return db.drop().then(_ => db.close());});
   yield db.put({
     channelID: '6ff97d56-d0c0-43bc-8f5b-61b855e1d93b',
     pushEndpoint: 'https://example.org/updates/1',
     scope: 'https://example.com/page/1',
     originAttributes: '',
     version: 2,
     quota: Infinity,
+    systemRecord: true,
   });
 
   let notifyPromise = promiseObserverNotification('push-notification');
 
   let ackDone;
   let ackPromise = new Promise(resolve => ackDone = resolve);
   PushService.init({
     serverURI: "wss://push.example.org/",
--- a/dom/push/test/xpcshell/test_permissions.js
+++ b/dom/push/test/xpcshell/test_permissions.js
@@ -28,16 +28,17 @@ function putRecord(channelID, scope, quo
     channelID: channelID,
     pushEndpoint: 'https://example.org/push/' + channelID,
     scope: scope,
     pushCount: 0,
     lastPush: 0,
     version: null,
     originAttributes: '',
     quota: quota,
+    systemRecord: quota == Infinity,
   });
 }
 
 function makePushPermission(url, capability) {
   return {
     QueryInterface: XPCOMUtils.generateQI([Ci.nsIPermission]),
     capability: Ci.nsIPermissionManager[capability],
     expireTime: 0,
--- a/dom/push/test/xpcshell/test_quota_exceeded.js
+++ b/dom/push/test/xpcshell/test_quota_exceeded.js
@@ -8,16 +8,17 @@ const {PushDB, PushService, PushServiceW
 Cu.import("resource://gre/modules/Task.jsm");
 
 const userAgentID = '7eb873f9-8d47-4218-804b-fff78dc04e88';
 
 function run_test() {
   do_get_profile();
   setPrefs({
     userAgentID,
+    'testing.ignorePermission': true,
   });
   run_next_test();
 }
 
 add_task(function* test_expiration_origin_threshold() {
   let db = PushServiceWebSocket.newPushDB();
   do_register_cleanup(() => db.drop().then(_ => db.close()));
 
--- a/dom/push/test/xpcshell/test_quota_with_notification.js
+++ b/dom/push/test/xpcshell/test_quota_with_notification.js
@@ -9,25 +9,26 @@ Cu.import("resource://gre/modules/Task.j
 
 const userAgentID = 'aaabf1f8-2f68-44f1-a920-b88e9e7d7559';
 const nsIPushQuotaManager = Components.interfaces.nsIPushQuotaManager;
 
 function run_test() {
   do_get_profile();
   setPrefs({
     userAgentID,
+    'testing.ignorePermission': true,
   });
   run_next_test();
 }
 
 add_task(function* test_expiration_origin_threshold() {
   let db = PushServiceWebSocket.newPushDB();
   do_register_cleanup(() => {
-    db.drop().then(_ => db.close())
     PushService.notificationForOriginClosed("https://example.com");
+    return db.drop().then(_ => db.close());
   });
 
   // Simulate a notification being shown for the origin,
   // this should relax the quota and allow as many push messages
   // as we want.
   PushService.notificationForOriginShown("https://example.com");
 
   yield db.put({
--- a/dom/push/test/xpcshell/test_register_flush.js
+++ b/dom/push/test/xpcshell/test_register_flush.js
@@ -27,16 +27,17 @@ add_task(function* test_register_flush()
   do_register_cleanup(() => {return db.drop().then(_ => db.close());});
   let record = {
     channelID: '9bcc7efb-86c7-4457-93ea-e24e6eb59b74',
     pushEndpoint: 'https://example.org/update/1',
     scope: 'https://example.com/page/1',
     originAttributes: '',
     version: 2,
     quota: Infinity,
+    systemRecord: true,
   };
   yield db.put(record);
 
   let notifyPromise = promiseObserverNotification('push-notification');
 
   let ackDone;
   let ackPromise = new Promise(resolve => ackDone = after(2, resolve));
   PushService.init({
--- a/dom/push/test/xpcshell/test_register_success_http2.js
+++ b/dom/push/test/xpcshell/test_register_success_http2.js
@@ -6,16 +6,17 @@
 Cu.import("resource://gre/modules/Services.jsm");
 
 const {PushDB, PushService, PushServiceHttp2} = serviceExports;
 
 var prefs;
 var tlsProfile;
 var serverURL;
 var serverPort = -1;
+var db;
 
 function run_test() {
   var env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
   serverPort = env.get("MOZHTTP2_PORT");
   do_check_neq(serverPort, null);
 
   do_get_profile();
   prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
@@ -39,23 +40,27 @@ function run_test() {
   disableServiceWorkerEvents(
     'https://example.org/1',
     'https://example.org/no_receiptEndpoint'
   );
 
   run_next_test();
 }
 
-add_task(function* test_pushSubscriptionSuccess() {
+add_task(function* test_setup() {
 
-  let db = PushServiceHttp2.newPushDB();
+  db = PushServiceHttp2.newPushDB();
   do_register_cleanup(() => {
     return db.drop().then(_ => db.close());
   });
 
+});
+
+add_task(function* test_pushSubscriptionSuccess() {
+
   PushService.init({
     serverURI: serverURL + "/pushSubscriptionSuccess/subscribe",
     db
   });
 
   let newRecord = yield PushService.register({
     scope: 'https://example.org/1',
     originAttributes: ChromeUtils.originAttributesToSuffix(
@@ -76,26 +81,21 @@ add_task(function* test_pushSubscription
     'Wrong subscription ID in database record');
   equal(record.pushEndpoint, pushEndpoint,
     'Wrong push endpoint in database record');
   equal(record.pushReceiptEndpoint, pushReceiptEndpoint,
     'Wrong push endpoint receipt in database record');
   equal(record.scope, 'https://example.org/1',
     'Wrong scope in database record');
 
-  db.drop().then(PushService.uninit());
+  PushService.uninit()
 });
 
 add_task(function* test_pushSubscriptionMissingLink2() {
 
-  let db = PushServiceHttp2.newPushDB();
-  do_register_cleanup(() => {
-    return db.drop().then(_ => db.close());
-  });
-
   PushService.init({
     serverURI: serverURL + "/pushSubscriptionMissingLink2/subscribe",
     db
   });
 
   let newRecord = yield PushService.register({
     scope: 'https://example.org/no_receiptEndpoint',
     originAttributes: ChromeUtils.originAttributesToSuffix(
--- a/dom/push/test/xpcshell/test_service_child.js
+++ b/dom/push/test/xpcshell/test_service_child.js
@@ -29,16 +29,17 @@ add_test(function test_subscribe_success
   service.subscribe(
     'https://example.com/sub/ok',
     Services.scriptSecurityManager.getSystemPrincipal(),
     (result, subscription) => {
       ok(Components.isSuccessCode(result), 'Error creating subscription');
       ok(subscription.endpoint.startsWith('https://example.org/push'), 'Wrong endpoint prefix');
       equal(subscription.pushCount, 0, 'Wrong push count');
       equal(subscription.lastPush, 0, 'Wrong last push time');
+      equal(subscription.quota, -1, 'Wrong quota for system subscription');
 
       do_test_finished();
       run_next_test();
     }
   );
 });
 
 add_test(function test_subscribe_error() {
@@ -62,16 +63,17 @@ add_test(function test_getSubscription_e
     'https://example.com/get/ok',
     Services.scriptSecurityManager.getSystemPrincipal(),
     (result, subscription) => {
       ok(Components.isSuccessCode(result), 'Error getting subscription');
 
       equal(subscription.endpoint, 'https://example.org/push/get', 'Wrong endpoint');
       equal(subscription.pushCount, 10, 'Wrong push count');
       equal(subscription.lastPush, 1438360548322, 'Wrong last push');
+      equal(subscription.quota, 16, 'Wrong quota for subscription');
 
       do_test_finished();
       run_next_test();
     }
   );
 });
 
 add_test(function test_getSubscription_missing() {
@@ -144,28 +146,80 @@ add_test(function test_unsubscribe_error
       strictEqual(success, false, 'Unexpected successful unsubscribe');
 
       do_test_finished();
       run_next_test();
     }
   );
 });
 
-add_test(function test_subscribe_principal() {
+add_test(function test_subscribe_app_principal() {
   let principal = Services.scriptSecurityManager.getAppCodebasePrincipal(
     Services.io.newURI('https://example.net/app/1', null, null),
     1, /* appId */
     true /* browserOnly */
   );
 
+  do_test_pending();
   service.subscribe('https://example.net/scope/1', principal, (result, subscription) => {
     ok(Components.isSuccessCode(result), 'Error creating subscription');
     ok(subscription.endpoint.startsWith('https://example.org/push'),
       'Wrong push endpoint in app subscription');
+    equal(subscription.quota, 16, 'Wrong quota for app subscription');
+
+    do_test_finished();
+    run_next_test();
+  });
+});
+
+add_test(function test_subscribe_origin_principal() {
+  let scope = 'https://example.net/origin-principal';
+  let principal =
+    Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(scope);
+
+  do_test_pending();
+  service.subscribe(scope, principal, (result, subscription) => {
+    ok(Components.isSuccessCode(result),
+      'Expected error creating subscription with origin principal');
+    equal(subscription.quota, 16, 'Wrong quota for origin subscription');
+
+    do_test_finished();
     run_next_test();
   });
 });
 
+add_test(function test_subscribe_null_principal() {
+  do_test_pending();
+  service.subscribe(
+    'chrome://push/null-principal',
+    Services.scriptSecurityManager.createNullPrincipal({}),
+    (result, subscription) => {
+      ok(!Components.isSuccessCode(result),
+        'Expected error creating subscription with expanded principal');
+      strictEqual(subscription, null,
+        'Unexpected subscription with expanded principal');
+
+      do_test_finished();
+      run_next_test();
+    }
+  );
+});
+
+add_test(function test_subscribe_missing_principal() {
+  do_test_pending();
+  service.subscribe('chrome://push/missing-principal', null,
+    (result, subscription) => {
+      ok(!Components.isSuccessCode(result),
+        'Expected error creating subscription without principal');
+      strictEqual(subscription, null,
+        'Unexpected subscription without principal');
+
+      do_test_finished();
+      run_next_test();
+    }
+  );
+});
+
 if (isParent) {
   add_test(function tearDown() {
     tearDownServiceInParent(db).then(run_next_test, run_next_test);
   });
 }
--- a/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js
+++ b/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js
@@ -52,16 +52,17 @@ add_task(function* test1() {
 
   let record = {
     subscriptionUri: serverURL + '/subscriptionNoKey',
     pushEndpoint: serverURL + '/pushEndpoint',
     pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint',
     scope: 'https://example.com/page',
     originAttributes: '',
     quota: Infinity,
+    systemRecord: true,
   };
 
   yield db.put(record);
 
   let notifyPromise = promiseObserverNotification('push-subscription-change',
                                                   _ => true);
 
   PushService.init({