Fix TokenDetails in messaging/ (#649)

Makes sure that we use only one type of TokenDetails object, both in the code and in the database.

Updates DB and adds a function to fix the old values.

Fixes and adds tests.
This commit is contained in:
Mertcan Mermerkaya
2018-04-10 19:25:19 +01:00
committed by GitHub
parent 64fdd73f22
commit 14896fb83b
21 changed files with 928 additions and 947 deletions
@@ -22,7 +22,7 @@ import {
Observer,
PartialObserver
} from '@firebase/util';
import { arrayBufferToBase64 } from '../helpers/array-buffer-to-base64';
import { isArrayBufferEqual } from '../helpers/is-array-buffer-equal';
import { TokenDetails } from '../interfaces/token-details';
import { ERROR_CODES, ERROR_MAP } from '../models/errors';
import { IIDModel } from '../models/iid-model';
@@ -128,7 +128,7 @@ export abstract class ControllerInterface {
);
if (isTokenValid) {
const now = Date.now();
if (now < tokenDetails.createTime! + TOKEN_EXPIRATION_MILLIS) {
if (now < tokenDetails.createTime + TOKEN_EXPIRATION_MILLIS) {
return tokenDetails.fcmToken;
} else {
return this.updateToken(
@@ -166,10 +166,13 @@ export abstract class ControllerInterface {
const allDetails: TokenDetails = {
swScope: swReg.scope,
vapidKey: publicVapidKey,
subscription: pushSubscription,
fcmSenderId: this.messagingSenderId_,
fcmToken: updatedToken,
fcmPushSet: tokenDetails.fcmPushSet
fcmPushSet: tokenDetails.fcmPushSet,
createTime: Date.now(),
endpoint: pushSubscription.endpoint,
auth: pushSubscription.getKey('auth')!,
p256dh: pushSubscription.getKey('p256dh')!
};
await this.tokenDetailsModel_.saveTokenDetails(allDetails);
@@ -197,10 +200,13 @@ export abstract class ControllerInterface {
const allDetails: TokenDetails = {
swScope: swReg.scope,
vapidKey: publicVapidKey,
subscription: pushSubscription,
fcmSenderId: this.messagingSenderId_,
fcmToken: tokenDetails.token,
fcmPushSet: tokenDetails.pushSet
fcmPushSet: tokenDetails.pushSet,
createTime: Date.now(),
endpoint: pushSubscription.endpoint,
auth: pushSubscription.getKey('auth')!,
p256dh: pushSubscription.getKey('p256dh')!
};
await this.tokenDetailsModel_.saveTokenDetails(allDetails);
await this.vapidDetailsModel_.saveVapidDetails(swReg.scope, publicVapidKey);
@@ -328,8 +334,6 @@ export abstract class ControllerInterface {
/**
* Returns the current Notification Permission state.
* @private
* @return {string} The currenct permission state.
*/
getNotificationPermission_(): NotificationPermission {
return (Notification as any).permission;
@@ -359,17 +363,21 @@ function isTokenStillValid(
publicVapidKey: Uint8Array,
tokenDetails: TokenDetails
): boolean {
if (arrayBufferToBase64(publicVapidKey) !== tokenDetails.vapidKey) {
if (
!isArrayBufferEqual(publicVapidKey.buffer, tokenDetails.vapidKey.buffer)
) {
return false;
}
// getKey() isn't defined in the PushSubscription externs file, hence
// subscription.getKey('<key name>').
return (
pushSubscription.endpoint === tokenDetails.endpoint &&
arrayBufferToBase64(pushSubscription.getKey('auth')!) ===
tokenDetails.auth &&
arrayBufferToBase64(pushSubscription.getKey('p256dh')!) ===
tokenDetails.p256dh
const isEndpointEqual = pushSubscription.endpoint === tokenDetails.endpoint;
const isAuthEqual = isArrayBufferEqual(
pushSubscription.getKey('auth'),
tokenDetails.auth
);
const isP256dhEqual = isArrayBufferEqual(
pushSubscription.getKey('p256dh'),
tokenDetails.p256dh
);
return isEndpointEqual && isAuthEqual && isP256dhEqual;
}
@@ -0,0 +1,43 @@
/**
* Copyright 2018 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function isArrayBufferEqual(
a: ArrayBuffer | undefined | null,
b: ArrayBuffer | undefined | null
): boolean {
if (a == null || b == null) {
return false;
}
if (a === b) {
return true;
}
if (a.byteLength !== b.byteLength) {
return false;
}
const viewA = new DataView(a);
const viewB = new DataView(b);
for (let i = 0; i < a.byteLength; i++) {
if (viewA.getUint8(i) !== viewB.getUint8(i)) {
return false;
}
}
return true;
}
@@ -17,13 +17,11 @@
export interface TokenDetails {
fcmToken: string;
swScope: string;
vapidKey: string | Uint8Array;
subscription: PushSubscription;
vapidKey: Uint8Array;
fcmSenderId: string;
fcmPushSet: string;
createTime?: number;
endpoint?: string;
auth?: string;
p256dh?: string;
endpoint: string;
auth: ArrayBuffer;
p256dh: ArrayBuffer;
createTime: number;
}
@@ -37,7 +37,7 @@ export abstract class DBInterface {
* This function should create and update object stores.
*/
protected abstract onDbUpgrade(
db: IDBDatabase,
request: IDBOpenDBRequest,
event: IDBVersionChangeEvent
): void;
@@ -121,8 +121,7 @@ export abstract class DBInterface {
this.dbPromise = null;
reject(request.error);
};
request.onupgradeneeded = event =>
this.onDbUpgrade(request.result, event);
request.onupgradeneeded = event => this.onDbUpgrade(request, event);
});
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { arrayBufferToBase64 } from '../helpers/array-buffer-to-base64';
import { base64ToArrayBuffer } from '../helpers/base64-to-array-buffer';
import { TokenDetails } from '../interfaces/token-details';
import { cleanV1 } from './clean-v1-undefined';
import { DBInterface } from './db-interface';
@@ -22,31 +22,70 @@ import { ERROR_CODES } from './errors';
export class TokenDetailsModel extends DBInterface {
protected readonly dbName: string = 'fcm_token_details_db';
protected readonly dbVersion: number = 2;
protected readonly dbVersion: number = 3;
protected readonly objectStoreName: string = 'fcm_token_object_Store';
protected onDbUpgrade(db: IDBDatabase, evt: IDBVersionChangeEvent): void {
if (evt.oldVersion < 1) {
// New IDB instance
const objectStore = db.createObjectStore(this.objectStoreName, {
keyPath: 'swScope'
});
protected onDbUpgrade(
request: IDBOpenDBRequest,
event: IDBVersionChangeEvent
): void {
const db: IDBDatabase = request.result;
// Make sure the sender ID can be searched
objectStore.createIndex('fcmSenderId', 'fcmSenderId', {
unique: false
});
// Lack of 'break' statements is intentional.
switch (event.oldVersion) {
case 0: {
// New IDB instance
const objectStore = db.createObjectStore(this.objectStoreName, {
keyPath: 'swScope'
});
objectStore.createIndex('fcmToken', 'fcmToken', {
unique: true
});
}
// Make sure the sender ID can be searched
objectStore.createIndex('fcmSenderId', 'fcmSenderId', {
unique: false
});
if (evt.oldVersion < 2) {
// Prior to version 2, we were using either 'fcm_token_details_db'
// or 'undefined' as the database name due to bug in the SDK
// So remove the old tokens and databases.
cleanV1();
objectStore.createIndex('fcmToken', 'fcmToken', { unique: true });
}
case 1: {
// Prior to version 2, we were using either 'fcm_token_details_db'
// or 'undefined' as the database name due to bug in the SDK
// So remove the old tokens and databases.
cleanV1();
}
case 2: {
const objectStore = request.transaction.objectStore(
this.objectStoreName
);
const cursorRequest = objectStore.openCursor();
cursorRequest.onsuccess = () => {
const cursor: IDBCursorWithValue = cursorRequest.result;
if (cursor) {
const value = cursor.value;
const newValue: Partial<TokenDetails> = { ...value };
if (!value.createTime) {
newValue.createTime = Date.now();
}
if (typeof value.vapidKey === 'string') {
newValue.vapidKey = base64ToArrayBuffer(value.vapidKey);
}
if (typeof value.auth === 'string') {
newValue.auth = base64ToArrayBuffer(value.auth).buffer;
}
if (typeof value.auth === 'string') {
newValue.p256dh = base64ToArrayBuffer(value.p256dh).buffer;
}
cursor.update(newValue);
cursor.continue();
}
};
}
}
}
@@ -77,8 +116,20 @@ export class TokenDetailsModel extends DBInterface {
}
}
if (input.subscription) {
if (!(input.subscription instanceof PushSubscription)) {
if (input.endpoint) {
if (typeof input.endpoint !== 'string' || input.endpoint.length === 0) {
throw this.errorFactory.create(ERROR_CODES.BAD_SUBSCRIPTION);
}
}
if (input.auth) {
if (!(input.auth instanceof ArrayBuffer)) {
throw this.errorFactory.create(ERROR_CODES.BAD_SUBSCRIPTION);
}
}
if (input.p256dh) {
if (!(input.p256dh instanceof ArrayBuffer)) {
throw this.errorFactory.create(ERROR_CODES.BAD_SUBSCRIPTION);
}
}
@@ -149,7 +200,7 @@ export class TokenDetailsModel extends DBInterface {
throw this.errorFactory.create(ERROR_CODES.BAD_VAPID_KEY);
}
if (!tokenDetails.subscription) {
if (!tokenDetails.endpoint || !tokenDetails.auth || !tokenDetails.p256dh) {
throw this.errorFactory.create(ERROR_CODES.BAD_SUBSCRIPTION);
}
@@ -167,19 +218,7 @@ export class TokenDetailsModel extends DBInterface {
this.validateInputs(tokenDetails);
const details = {
swScope: tokenDetails.swScope,
vapidKey: arrayBufferToBase64(tokenDetails.vapidKey as Uint8Array),
endpoint: tokenDetails.subscription.endpoint,
auth: arrayBufferToBase64(tokenDetails.subscription.getKey('auth')!),
p256dh: arrayBufferToBase64(tokenDetails.subscription.getKey('p256dh')!),
fcmSenderId: tokenDetails.fcmSenderId,
fcmToken: tokenDetails.fcmToken,
fcmPushSet: tokenDetails.fcmPushSet,
createTime: Date.now()
};
return this.put(details);
return this.put(tokenDetails);
}
/**
@@ -25,7 +25,8 @@ export class VapidDetailsModel extends DBInterface {
protected readonly dbVersion: number = 1;
protected readonly objectStoreName: string = 'fcm_vapid_object_Store';
protected onDbUpgrade(db: IDBDatabase): void {
protected onDbUpgrade(request: IDBOpenDBRequest): void {
const db: IDBDatabase = request.result;
db.createObjectStore(this.objectStoreName, { keyPath: 'swScope' });
}
@@ -1,117 +0,0 @@
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect } from 'chai';
import { arrayBufferToBase64 } from '../src/helpers/array-buffer-to-base64';
describe('Firebase Messaging > array-buffer-to-base64', () => {
it('should return expected value for p256dh example', () => {
const buffer = new Uint8Array([
4,
181,
98,
240,
48,
62,
75,
119,
193,
227,
154,
69,
250,
216,
53,
110,
157,
120,
62,
76,
213,
249,
11,
62,
12,
19,
149,
36,
5,
82,
140,
37,
141,
134,
132,
98,
87,
152,
175,
98,
53,
83,
196,
242,
202,
155,
19,
173,
157,
216,
45,
147,
20,
12,
151,
160,
147,
159,
205,
219,
75,
133,
156,
129,
152
]).buffer;
const expectedValue =
'BLVi8DA-S3fB45pF-tg1bp14PkzV-Qs-DBOVJAVSjCWNhoRi' +
'V5ivYjVTxPLKmxOtndgtkxQMl6CTn83bS4WcgZg';
expect(arrayBufferToBase64(buffer)).to.equal(expectedValue);
});
it('should return expected value for auth example', () => {
const buffer = new Uint8Array([
255,
237,
107,
177,
171,
78,
84,
131,
221,
231,
87,
188,
22,
232,
71,
15
]).buffer;
const expectedValue = '_-1rsatOVIPd51e8FuhHDw';
expect(arrayBufferToBase64(buffer)).to.equal(expectedValue);
});
});
@@ -13,79 +13,83 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FirebaseApp } from '@firebase/app-types';
import { assert } from 'chai';
import * as sinon from 'sinon';
import { SWController } from '../src/controllers/sw-controller';
import { WindowController } from '../src/controllers/window-controller';
import { base64ToArrayBuffer } from '../src/helpers/base64-to-array-buffer';
import { ERROR_CODES } from '../src/models/errors';
import { IIDModel } from '../src/models/iid-model';
import { TokenDetailsModel } from '../src/models/token-details-model';
import { makeFakeApp } from './make-fake-app';
import { makeFakeSubscription } from './make-fake-subscription';
import { makeFakeSWReg } from './make-fake-sw-reg';
import { deleteDatabase } from './testing-utils/db-helper';
const FAKE_SUBSCRIPTION = makeFakeSubscription();
const EXAMPLE_TOKEN_SAVE = {
fcmToken: 'ExampleFCMToken1337',
fcmSenderId: '1234567890',
endpoint: 'https://example.google.com/1234',
swScope: 'firebase-messaging-sw-scope',
auth: '12345',
p256dh: '123456789098765642421'
swScope: '/example-scope',
vapidKey: base64ToArrayBuffer(
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I'
),
fcmSenderId: '1234567',
fcmToken: 'qwerty',
fcmPushSet: '7654321',
endpoint: FAKE_SUBSCRIPTION.endpoint,
auth: FAKE_SUBSCRIPTION.getKey('auth')!,
p256dh: FAKE_SUBSCRIPTION.getKey('p256dh')!,
createTime: Date.now()
};
describe('Firebase Messaging > *Controller.deleteToken()', () => {
const sandbox = sinon.sandbox.create();
let sandbox: sinon.SinonSandbox;
let app: FirebaseApp;
let globalMessagingService: WindowController | SWController;
const app = makeFakeApp({
messagingSenderId: EXAMPLE_TOKEN_SAVE.fcmSenderId
});
const configureRegistrationMocks = (ServiceClass, fakeReg) => {
function configureRegistrationMocks(
ServiceClass: typeof WindowController | typeof SWController,
fakeReg: ServiceWorkerRegistration | null
): void {
sandbox.stub(ServiceClass.prototype, 'getSWRegistration_').callsFake(() => {
return fakeReg;
});
};
}
const generateFakeReg = getSubResult => {
function generateFakeReg(
getSubscription: () => Promise<PushSubscription | null>
): ServiceWorkerRegistration {
const registration = makeFakeSWReg();
Object.defineProperty(registration, 'pushManager', {
value: {
getSubscription: () => {
if (typeof getSubResult === 'function') {
return getSubResult();
}
return getSubResult;
}
}
value: { getSubscription }
});
return Promise.resolve(registration);
};
let globalMessagingService;
const cleanUp = () => {
sandbox.restore();
const deletePromises: any[] = [];
if (globalMessagingService) {
deletePromises.push(globalMessagingService.delete());
}
return Promise.all(deletePromises)
.then(() => deleteDatabase('fcm_token_details_db'))
.then(() => (globalMessagingService = null));
};
return registration;
}
beforeEach(() => {
return cleanUp();
sandbox = sinon.sandbox.create();
app = makeFakeApp({
messagingSenderId: EXAMPLE_TOKEN_SAVE.fcmSenderId
});
});
after(() => {
return cleanUp();
afterEach(async () => {
sandbox.restore();
if (globalMessagingService) {
await globalMessagingService.delete();
}
await deleteDatabase('fcm_token_details_db');
});
it('should handle no token to delete', () => {
globalMessagingService = new WindowController(app);
return globalMessagingService.deleteToken().then(
return globalMessagingService.deleteToken(undefined as any).then(
() => {
throw new Error('Expected error to be thrown.');
},
@@ -96,7 +100,7 @@ describe('Firebase Messaging > *Controller.deleteToken()', () => {
});
it('should handle no registration', () => {
configureRegistrationMocks(WindowController, Promise.resolve(null));
configureRegistrationMocks(WindowController, null);
sandbox
.stub(TokenDetailsModel.prototype, 'deleteToken')
@@ -114,7 +118,9 @@ describe('Firebase Messaging > *Controller.deleteToken()', () => {
it('should handle get subscription error', () => {
configureRegistrationMocks(
WindowController,
generateFakeReg(() => Promise.reject(new Error('Unknown error')))
generateFakeReg(async () => {
throw new Error('Unknown error');
})
);
sandbox
@@ -141,7 +147,7 @@ describe('Firebase Messaging > *Controller.deleteToken()', () => {
it(`should handle null getSubscription() ${ServiceClass.name}`, () => {
configureRegistrationMocks(
ServiceClass,
generateFakeReg(Promise.resolve(null))
generateFakeReg(async () => null)
);
sandbox
@@ -159,14 +165,14 @@ describe('Firebase Messaging > *Controller.deleteToken()', () => {
it(`should handle error on unsubscribe ${ServiceClass.name}`, () => {
const errorMsg = 'unsubscribe-error-1234567890';
const fakeSubscription = {
const fakeSubscription: any = {
endpoint: EXAMPLE_TOKEN_SAVE.endpoint,
unsubscribe: () => Promise.reject(new Error(errorMsg))
};
configureRegistrationMocks(
ServiceClass,
generateFakeReg(Promise.resolve(fakeSubscription))
generateFakeReg(async () => fakeSubscription)
);
sandbox
@@ -192,14 +198,14 @@ describe('Firebase Messaging > *Controller.deleteToken()', () => {
});
it(`should handle error on deleteToken ${ServiceClass.name}`, () => {
const fakeSubscription = {
const fakeSubscription: any = {
endpoint: EXAMPLE_TOKEN_SAVE.endpoint,
unsubscribe: async () => {}
};
configureRegistrationMocks(
ServiceClass,
generateFakeReg(Promise.resolve(fakeSubscription))
generateFakeReg(async () => fakeSubscription)
);
sandbox
@@ -228,14 +234,14 @@ describe('Firebase Messaging > *Controller.deleteToken()', () => {
});
it(`should delete with valid unsubscribe ${ServiceClass.name}`, () => {
const fakeSubscription = {
const fakeSubscription: any = {
endpoint: EXAMPLE_TOKEN_SAVE.endpoint,
unsubscribe: async () => {}
};
configureRegistrationMocks(
ServiceClass,
generateFakeReg(Promise.resolve(fakeSubscription))
generateFakeReg(async () => fakeSubscription)
);
sandbox
@@ -21,8 +21,9 @@ import {
} from '../src/controllers/controller-interface';
import { SWController } from '../src/controllers/sw-controller';
import { WindowController } from '../src/controllers/window-controller';
import { arrayBufferToBase64 } from '../src/helpers/array-buffer-to-base64';
import { base64ToArrayBuffer } from '../src/helpers/base64-to-array-buffer';
import { isArrayBufferEqual } from '../src/helpers/is-array-buffer-equal';
import { TokenDetails } from '../src/interfaces/token-details';
import { ERROR_CODES } from '../src/models/errors';
import { DEFAULT_PUBLIC_VAPID_KEY } from '../src/models/fcm-details';
import { IIDModel } from '../src/models/iid-model';
@@ -37,44 +38,47 @@ const ONE_DAY = 24 * 60 * 60 * 1000;
describe('Firebase Messaging > *Controller.getToken()', () => {
const sandbox = sinon.sandbox.create();
const now = Date.now();
const expiredDate = new Date(now - ONE_DAY);
const expiredDate = now - ONE_DAY * 8; // 8 days ago
const FAKE_SUBSCRIPTION = makeFakeSubscription();
const EXAMPLE_FCM_TOKEN = 'ExampleFCMToken1337';
const EXAMPLE_SENDER_ID = '1234567890';
const CUSTOM_VAPID_KEY =
const CUSTOM_VAPID_KEY = base64ToArrayBuffer(
'BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXK' +
'pNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaNndIo';
const DEFAULT_VAPID_KEY = arrayBufferToBase64(DEFAULT_PUBLIC_VAPID_KEY);
'pNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaNndIo'
);
const ENDPOINT = FAKE_SUBSCRIPTION.endpoint;
const AUTH = FAKE_SUBSCRIPTION.getKey('auth')!;
const P256DH = FAKE_SUBSCRIPTION.getKey('p256dh')!;
const EXAMPLE_TOKEN_DETAILS_DEFAULT_VAPID = {
const EXAMPLE_TOKEN_DETAILS_DEFAULT_VAPID: TokenDetails = {
swScope: '/example-scope',
vapidKey: DEFAULT_VAPID_KEY,
endpoint: FAKE_SUBSCRIPTION.endpoint,
auth: arrayBufferToBase64(FAKE_SUBSCRIPTION['getKey']('auth')!),
p256dh: arrayBufferToBase64(FAKE_SUBSCRIPTION['getKey']('p256dh')!),
vapidKey: DEFAULT_PUBLIC_VAPID_KEY,
endpoint: ENDPOINT,
auth: AUTH,
p256dh: P256DH,
fcmSenderId: EXAMPLE_SENDER_ID,
fcmToken: 'qwerty1',
fcmPushSet: '87654321',
createTime: now
};
const EXAMPLE_TOKEN_DETAILS_CUSTOM_VAPID = {
const EXAMPLE_TOKEN_DETAILS_CUSTOM_VAPID: TokenDetails = {
swScope: '/example-scope',
vapidKey: CUSTOM_VAPID_KEY,
endpoint: FAKE_SUBSCRIPTION.endpoint,
auth: arrayBufferToBase64(FAKE_SUBSCRIPTION['getKey']('auth')!),
p256dh: arrayBufferToBase64(FAKE_SUBSCRIPTION['getKey']('p256dh')!),
endpoint: ENDPOINT,
auth: AUTH,
p256dh: P256DH,
fcmSenderId: EXAMPLE_SENDER_ID,
fcmToken: 'qwerty2',
fcmPushSet: '7654321',
createTime: now
};
const EXAMPLE_EXPIRED_TOKEN_DETAILS = {
const EXAMPLE_EXPIRED_TOKEN_DETAILS: TokenDetails = {
swScope: '/example-scope',
vapidKey: DEFAULT_VAPID_KEY,
endpoint: FAKE_SUBSCRIPTION.endpoint,
auth: arrayBufferToBase64(FAKE_SUBSCRIPTION['getKey']('auth')!),
p256dh: arrayBufferToBase64(FAKE_SUBSCRIPTION['getKey']('p256dh')!),
vapidKey: DEFAULT_PUBLIC_VAPID_KEY,
endpoint: ENDPOINT,
auth: AUTH,
p256dh: P256DH,
fcmSenderId: EXAMPLE_SENDER_ID,
fcmToken: 'qwerty3',
fcmPushSet: '654321',
@@ -231,7 +235,7 @@ describe('Firebase Messaging > *Controller.getToken()', () => {
let vapidKeyToUse = DEFAULT_PUBLIC_VAPID_KEY;
if (VapidSetup['name'] === 'custom') {
vapidKeyToUse = base64ToArrayBuffer(CUSTOM_VAPID_KEY);
vapidKeyToUse = CUSTOM_VAPID_KEY;
}
sandbox
.stub(ServiceClass.prototype, 'getPublicVapidKey_')
@@ -264,9 +268,7 @@ describe('Firebase Messaging > *Controller.getToken()', () => {
.callsFake(() => Promise.resolve(subscription));
sandbox
.stub(ServiceClass.prototype, 'getPublicVapidKey_')
.callsFake(() =>
Promise.resolve(base64ToArrayBuffer(CUSTOM_VAPID_KEY))
);
.callsFake(() => Promise.resolve(CUSTOM_VAPID_KEY));
sandbox
.stub(ControllerInterface.prototype, 'getNotificationPermission_')
@@ -348,7 +350,7 @@ describe('Firebase Messaging > *Controller.getToken()', () => {
let vapidKeyToUse = DEFAULT_PUBLIC_VAPID_KEY;
if (VapidSetup['name'] === 'custom') {
vapidKeyToUse = base64ToArrayBuffer(CUSTOM_VAPID_KEY);
vapidKeyToUse = CUSTOM_VAPID_KEY;
}
sandbox
.stub(ServiceClass.prototype, 'getPublicVapidKey_')
@@ -398,10 +400,16 @@ describe('Firebase Messaging > *Controller.getToken()', () => {
assert.equal(tokenModelArgs[0].swScope, registration.scope);
assert.equal(
arrayBufferToBase64(tokenModelArgs[0].vapidKey),
tokenModelArgs[0].vapidKey,
VapidSetup['details']['vapidKey']
);
assert.equal(tokenModelArgs[0].subscription, subscription);
assert.equal(tokenModelArgs[0].endpoint, ENDPOINT);
assert.equal(isArrayBufferEqual(tokenModelArgs[0].auth, AUTH), true);
assert.equal(
isArrayBufferEqual(tokenModelArgs[0].p256dh, P256DH),
true
);
assert.equal(tokenModelArgs[0].fcmSenderId, EXAMPLE_SENDER_ID);
assert.equal(tokenModelArgs[0].fcmToken, TOKEN_DETAILS['token']);
assert.equal(tokenModelArgs[0].fcmPushSet, TOKEN_DETAILS['pushSet']);
@@ -428,7 +436,7 @@ describe('Firebase Messaging > *Controller.getToken()', () => {
const existingTokenDetails = VapidSetup['details'];
let vapidKeyToUse = DEFAULT_PUBLIC_VAPID_KEY;
if (VapidSetup['name'] === 'custom') {
vapidKeyToUse = base64ToArrayBuffer(CUSTOM_VAPID_KEY);
vapidKeyToUse = CUSTOM_VAPID_KEY;
}
sandbox
.stub(ServiceClass.prototype, 'getPublicVapidKey_')
@@ -539,9 +547,7 @@ describe('Firebase Messaging > *Controller.getToken()', () => {
};
// now update the VAPID key.
getPublicVapidKeyStub.callsFake(() =>
Promise.resolve(base64ToArrayBuffer(CUSTOM_VAPID_KEY))
);
getPublicVapidKeyStub.callsFake(() => Promise.resolve(CUSTOM_VAPID_KEY));
const saveTokenDetailsStub = sandbox.stub(
TokenDetailsModel.prototype,
@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FirebaseApp } from '@firebase/app-types';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { makeFakeApp } from './make-fake-app';
import { makeFakeSWReg } from './make-fake-sw-reg';
@@ -43,29 +46,26 @@ class MockControllerInterface extends ControllerInterface {
}
describe('Firebase Messaging > *ControllerInterface', () => {
const sandbox = sinon.sandbox.create();
const app = makeFakeApp({
messagingSenderId: '12345'
});
const cleanup = () => {
sandbox.restore();
};
let sandbox: sinon.SinonSandbox;
let app: FirebaseApp;
beforeEach(() => {
return cleanup();
sandbox = sinon.sandbox.create();
app = makeFakeApp({
messagingSenderId: '12345'
});
});
after(() => {
return cleanup();
afterEach(() => {
sandbox.restore();
});
describe('INTERNAL.delete()', () => {
it('should call delete()', () => {
const controller = new MockControllerInterface(app);
sandbox.spy(controller, 'delete');
const spy = sandbox.spy(controller, 'delete');
controller.INTERNAL.delete();
expect(controller.delete['callCount']).to.equal(1);
expect(spy.callCount).to.equal(1);
});
});
@@ -84,12 +84,14 @@ describe('Firebase Messaging > *ControllerInterface', () => {
});
describe('getPushSubscription()', () => {
controllersToTest.forEach(ControllerInTest => {
for (const ControllerInTest of controllersToTest) {
it(`should return rejection error in ${ControllerInTest.name}`, () => {
const injectedError = new Error('Inject error.');
const reg = makeFakeSWReg();
sandbox.stub(reg, 'pushManager').value({
getSubscription: () => Promise.reject(injectedError)
getSubscription: async () => {
throw injectedError;
}
});
const controller = new ControllerInTest(app);
@@ -109,7 +111,7 @@ describe('Firebase Messaging > *ControllerInterface', () => {
const exampleSubscription = {};
const reg = makeFakeSWReg();
sandbox.stub(reg, 'pushManager').value({
getSubscription: () => Promise.resolve(exampleSubscription)
getSubscription: async () => exampleSubscription
});
const controller = new ControllerInTest(app);
@@ -125,13 +127,13 @@ describe('Firebase Messaging > *ControllerInterface', () => {
const reg = makeFakeSWReg();
sandbox.stub(reg, 'pushManager').value({
getSubscription: async () => {},
subscribe: options => {
subscribe: async options => {
expect(options).to.deep.equal({
userVisibleOnly: true,
applicationServerKey: DEFAULT_PUBLIC_VAPID_KEY
});
return Promise.resolve(exampleSubscription);
return exampleSubscription;
}
});
@@ -142,7 +144,7 @@ describe('Firebase Messaging > *ControllerInterface', () => {
expect(subscription).to.equal(exampleSubscription);
});
});
});
}
});
describe('useServiceWorker()', () => {
+6 -1
View File
@@ -34,7 +34,12 @@ class MockDbInterface extends DBInterface {
readonly dbVersion: number = 1;
readonly objectStoreName: string = 'test-obj-store';
protected onDbUpgrade(db: IDBDatabase, event: IDBVersionChangeEvent): void {
protected onDbUpgrade(
request: IDBOpenDBRequest,
event: IDBVersionChangeEvent
): void {
const db: IDBDatabase = request.result;
const objectStore = db.createObjectStore(this.objectStoreName, {
keyPath: 'key'
});
+106
View File
@@ -0,0 +1,106 @@
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect } from 'chai';
import { arrayBufferToBase64 } from '../src/helpers/array-buffer-to-base64';
import { base64ToArrayBuffer } from '../src/helpers/base64-to-array-buffer';
import { isArrayBufferEqual } from '../src/helpers/is-array-buffer-equal';
describe('Firebase Messaging > Helpers', () => {
describe('Array Buffer - Base64 conversion', () => {
it('returns expected value for p256dh example', () => {
// prettier-ignore
const buffer = new Uint8Array([
4, 181, 98, 240, 48, 62, 75, 119, 193, 227, 154, 69, 250, 216, 53, 110,
157, 120, 62, 76, 213, 249, 11, 62, 12, 19, 149, 36, 5, 82, 140, 37, 141,
134, 132, 98, 87, 152, 175, 98, 53, 83, 196, 242, 202, 155, 19, 173, 157,
216, 45, 147, 20, 12, 151, 160, 147, 159, 205, 219, 75, 133, 156, 129, 152
]);
const expectedValue =
'BLVi8DA-S3fB45pF-tg1bp14PkzV-Qs-DBOVJAVSjCWNhoRi' +
'V5ivYjVTxPLKmxOtndgtkxQMl6CTn83bS4WcgZg';
expect(arrayBufferToBase64(buffer)).to.equal(expectedValue);
});
it('returns expected value for auth example', () => {
// prettier-ignore
const buffer = new Uint8Array([
255, 237, 107, 177, 171, 78, 84, 131, 221, 231, 87, 188, 22, 232, 71, 15
]);
const expectedValue = '_-1rsatOVIPd51e8FuhHDw';
expect(arrayBufferToBase64(buffer)).to.equal(expectedValue);
});
it('converts back and forth', () => {
// prettier-ignore
const array = new Uint8Array([
4, 181, 98, 240, 48, 62, 75, 119, 193, 227, 154, 69, 250, 216, 53, 110,
157, 120, 62, 76, 213, 249, 11, 62, 12, 19, 149, 36, 5, 82, 140, 37, 141,
134, 132, 98, 87, 152, 175, 98, 53, 83, 196, 242, 202, 155, 19, 173, 157,
216, 45, 147, 20, 12, 151, 160, 147, 159, 205, 219, 75, 133, 156, 129, 152
]);
const string =
'BLVi8DA-S3fB45pF-tg1bp14PkzV-Qs-DBOVJAVSjCWNhoRi' +
'V5ivYjVTxPLKmxOtndgtkxQMl6CTn83bS4WcgZg';
expect(arrayBufferToBase64(base64ToArrayBuffer(string))).to.equal(string);
expect(
isArrayBufferEqual(
base64ToArrayBuffer(arrayBufferToBase64(array)).buffer,
array.buffer
)
).to.be.true;
});
});
describe('isArrayBufferEqual', () => {
let buffer: ArrayBuffer;
beforeEach(() => {
buffer = new Uint8Array([1, 2, 3, 4, 5]).buffer;
});
it('returns true if array buffers are same', () => {
expect(isArrayBufferEqual(buffer, buffer)).to.be.true;
});
it('returns true if array buffers are equal', () => {
const equalBuffer = new Uint8Array([1, 2, 3, 4, 5]).buffer;
expect(isArrayBufferEqual(buffer, equalBuffer)).to.be.true;
});
it('returns false if array buffers are not equal', () => {
const differentBuffer = new Uint8Array([1, 2, 3, 2, 1]).buffer;
expect(isArrayBufferEqual(buffer, differentBuffer)).to.be.false;
});
it('returns false if array buffers have different lengths', () => {
const longerBuffer = new Uint8Array([1, 2, 3, 4, 5, 6]).buffer;
expect(isArrayBufferEqual(buffer, longerBuffer)).to.be.false;
});
it('returns false if either array buffer is undefined or null', () => {
expect(isArrayBufferEqual(buffer, undefined)).to.be.false;
expect(isArrayBufferEqual(buffer, null)).to.be.false;
expect(isArrayBufferEqual(undefined, buffer)).to.be.false;
expect(isArrayBufferEqual(null, buffer)).to.be.false;
expect(isArrayBufferEqual(undefined, undefined)).to.be.false;
expect(isArrayBufferEqual(null, null)).to.be.false;
expect(isArrayBufferEqual(undefined, null)).to.be.false;
expect(isArrayBufferEqual(null, undefined)).to.be.false;
});
});
});
@@ -15,47 +15,40 @@
*/
import { assert } from 'chai';
import { arrayBufferToBase64 } from '../../src/helpers/array-buffer-to-base64';
/** Token details that are fed to the database */
export declare interface InputDetails {
swScope: string;
vapidKey: Uint8Array;
subscription: PushSubscription;
fcmSenderId: string;
fcmToken: string;
fcmPushSet: string;
}
import { isArrayBufferEqual } from '../../src/helpers/is-array-buffer-equal';
import { TokenDetails } from '../../src/interfaces/token-details';
/** Compares the input details and the saved ones */
export function compareDetails(
input: InputDetails,
saved: object,
now: number
): void {
const subscriptionKeys = ['endpoint', 'auth', 'p256dh'];
const subscriptionValues = {
endpoint: input.subscription.endpoint,
auth: arrayBufferToBase64(input.subscription.getKey('auth')!),
p256dh: arrayBufferToBase64(input.subscription.getKey('p256dh')!)
export function compareDetails(input: TokenDetails, saved: TokenDetails): void {
const subscription = {
endpoint: input.endpoint,
auth: input.auth,
p256dh: input.p256dh
};
subscriptionKeys.forEach(keyName => {
assert.equal(saved[keyName], subscriptionValues[keyName]);
});
for (const key of Object.keys(subscription)) {
assert.equal(
isArrayBufferEqual(saved[key], subscription[key]),
true,
`${key} does not match`
);
}
Object.keys(saved).forEach(keyName => {
if (subscriptionKeys.indexOf(keyName) !== -1) {
for (const key of Object.keys(saved)) {
if (Object.keys(subscription).indexOf(key) !== -1) {
return;
}
if (keyName === 'createTime') {
assert.equal(saved[keyName], now);
} else if (keyName === 'vapidKey') {
assert.equal(saved[keyName], arrayBufferToBase64(input[keyName]));
if (key === 'createTime') {
assert.equal(saved[key], Date.now(), `${key} does not match`);
} else if (key === 'vapidKey') {
assert.equal(
isArrayBufferEqual(saved[key].buffer, input[key].buffer),
true,
`${key} does not match`
);
} else {
assert.equal(saved[keyName], input[keyName]);
assert.equal(saved[key], input[key], `${key} does not match`);
}
return true;
});
}
}
@@ -0,0 +1,370 @@
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from 'chai';
import * as sinon from 'sinon';
import { arrayBufferToBase64 } from '../src/helpers/array-buffer-to-base64';
import { base64ToArrayBuffer } from '../src/helpers/base64-to-array-buffer';
import { TokenDetails } from '../src/interfaces/token-details';
import { ERROR_CODES } from '../src/models/errors';
import { TokenDetailsModel } from '../src/models/token-details-model';
import { makeFakeSubscription } from './make-fake-subscription';
import { deleteDatabase } from './testing-utils/db-helper';
import { compareDetails } from './testing-utils/detail-comparator';
const BAD_INPUTS: any[] = ['', [], {}, true, null, 123];
describe('Firebase Messaging > TokenDetailsModel', () => {
let clock: sinon.SinonFakeTimers;
let globalTokenModel: TokenDetailsModel;
let exampleInput: TokenDetails;
let fakeSubscription: PushSubscription;
beforeEach(() => {
clock = sinon.useFakeTimers();
globalTokenModel = new TokenDetailsModel();
fakeSubscription = makeFakeSubscription();
exampleInput = {
swScope: '/example-scope',
vapidKey: base64ToArrayBuffer(
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I'
),
fcmSenderId: '1234567',
fcmToken: 'qwerty',
fcmPushSet: '7654321',
endpoint: fakeSubscription.endpoint,
auth: fakeSubscription.getKey('auth')!,
p256dh: fakeSubscription.getKey('p256dh')!,
createTime: Date.now()
};
});
afterEach(async () => {
await globalTokenModel.closeDatabase();
await deleteDatabase('fcm_token_details_db');
clock.restore();
});
describe('Database update v2 -> v3', () => {
it('converts database contents', async () => {
class OldDBTokenDetailsModel extends TokenDetailsModel {
protected readonly dbVersion: number = 2;
}
const oldDBTokenDetailsModel = new OldDBTokenDetailsModel();
// Old (v2) version of exampleInput
// vapidKey, auth and p256dh are strings, createTime does not exist
await oldDBTokenDetailsModel.put({
swScope: '/example-scope',
vapidKey:
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I',
fcmSenderId: '1234567',
fcmToken: 'qwerty',
fcmPushSet: '7654321',
endpoint: fakeSubscription.endpoint,
auth: arrayBufferToBase64(fakeSubscription.getKey('auth')!),
p256dh: arrayBufferToBase64(fakeSubscription.getKey('p256dh')!)
});
await oldDBTokenDetailsModel.closeDatabase();
// Get the same token using DB v3
const tokenDetails = await globalTokenModel.get<TokenDetails>(
exampleInput.swScope
);
assert.exists(tokenDetails);
compareDetails(tokenDetails!, exampleInput);
});
});
describe('saveToken', () => {
it('should throw on bad input', () => {
const promises = BAD_INPUTS.map(badInput => {
globalTokenModel = new TokenDetailsModel();
exampleInput.swScope = badInput;
return globalTokenModel.saveTokenDetails(exampleInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SCOPE, err.code);
}
);
});
return Promise.all(promises);
});
it('should throw on bad vapid key input', () => {
const promises = BAD_INPUTS.map(badInput => {
globalTokenModel = new TokenDetailsModel();
exampleInput.vapidKey = badInput;
return globalTokenModel.saveTokenDetails(exampleInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_VAPID_KEY, err.code);
}
);
});
return Promise.all(promises);
});
it('should throw on bad endpoint input', () => {
const promises = BAD_INPUTS.map(badInput => {
globalTokenModel = new TokenDetailsModel();
exampleInput.endpoint = badInput;
return globalTokenModel.saveTokenDetails(exampleInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SUBSCRIPTION, err.code);
}
);
});
return Promise.all(promises);
});
it('should throw on bad auth input', () => {
const promises = BAD_INPUTS.map(badInput => {
globalTokenModel = new TokenDetailsModel();
exampleInput.auth = badInput;
return globalTokenModel.saveTokenDetails(exampleInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SUBSCRIPTION, err.code);
}
);
});
return Promise.all(promises);
});
it('should throw on bad p256dh input', () => {
const promises = BAD_INPUTS.map(badInput => {
globalTokenModel = new TokenDetailsModel();
exampleInput.p256dh = badInput;
return globalTokenModel.saveTokenDetails(exampleInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SUBSCRIPTION, err.code);
}
);
});
return Promise.all(promises);
});
it('should throw on bad send id input', () => {
const promises = BAD_INPUTS.map(badInput => {
globalTokenModel = new TokenDetailsModel();
exampleInput.fcmSenderId = badInput;
return globalTokenModel.saveTokenDetails(exampleInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SENDER_ID, err.code);
}
);
});
return Promise.all(promises);
});
it('should throw on bad token input', () => {
const promises = BAD_INPUTS.map(badInput => {
globalTokenModel = new TokenDetailsModel();
exampleInput.fcmToken = badInput;
return globalTokenModel.saveTokenDetails(exampleInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_TOKEN, err.code);
}
);
});
return Promise.all(promises);
});
it('should throw on bad pushSet input', () => {
const promises = BAD_INPUTS.map(badInput => {
globalTokenModel = new TokenDetailsModel();
exampleInput.fcmPushSet = badInput;
return globalTokenModel.saveTokenDetails(exampleInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_PUSH_SET, err.code);
}
);
});
return Promise.all(promises);
});
it('should save valid details', () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel.saveTokenDetails(exampleInput);
});
});
describe('getTokenDetailsFromToken', () => {
for (const badInput of BAD_INPUTS) {
it(`should throw on bad scope input ${JSON.stringify(badInput)}`, () => {
return globalTokenModel.getTokenDetailsFromSWScope(badInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SCOPE, err.code);
}
);
});
}
for (const badInput of BAD_INPUTS) {
it(`should throw on bad FCM Token input: '${badInput}'`, () => {
return globalTokenModel.getTokenDetailsFromToken(badInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_TOKEN, err.code);
}
);
});
}
it('should get from scope', () => {
return globalTokenModel
.getTokenDetailsFromSWScope(exampleInput.swScope)
.then(details => {
assert.notExists(details);
return globalTokenModel.saveTokenDetails(exampleInput);
})
.then(() => {
return globalTokenModel.getTokenDetailsFromSWScope(
exampleInput.swScope
);
})
.then(details => {
assert.exists(details);
compareDetails(exampleInput, details!);
});
});
it('should get from token', () => {
return globalTokenModel
.getTokenDetailsFromToken(exampleInput.fcmToken)
.then(details => {
assert.notExists(details);
return globalTokenModel.saveTokenDetails(exampleInput);
})
.then(() => {
return globalTokenModel.getTokenDetailsFromToken(
exampleInput.fcmToken
);
})
.then(details => {
assert.exists(details);
compareDetails(exampleInput, details!);
});
});
});
describe('deleteToken', () => {
it('should handle no input', () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel.deleteToken(undefined as any).then(
() => {
throw new Error('Expected this to throw an error due to no token');
},
err => {
assert.equal(
'messaging/' + ERROR_CODES.INVALID_DELETE_TOKEN,
err.code
);
}
);
});
it('should handle empty string', () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel.deleteToken('').then(
() => {
throw new Error('Expected this to throw an error due to no token');
},
err => {
assert.equal(
'messaging/' + ERROR_CODES.INVALID_DELETE_TOKEN,
err.code
);
}
);
});
it('should delete current token', () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel
.saveTokenDetails(exampleInput)
.then(() => {
return globalTokenModel.deleteToken(exampleInput.fcmToken);
})
.then(details => {
compareDetails(exampleInput, details);
return globalTokenModel.getTokenDetailsFromToken(
exampleInput.fcmToken
);
})
.then(tokenDetails => {
assert.equal(null, tokenDetails);
});
});
it('should handle deleting a non-existant token', () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel.deleteToken('bad-token').then(
() => {
throw new Error('Expected this delete to throw and error.');
},
err => {
assert.equal(
'messaging/' + ERROR_CODES.DELETE_TOKEN_NOT_FOUND,
err.code
);
}
);
});
});
});
@@ -1,109 +0,0 @@
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from 'chai';
import * as sinon from 'sinon';
import { arrayBufferToBase64 } from '../src/helpers/array-buffer-to-base64';
import { base64ToArrayBuffer } from '../src/helpers/base64-to-array-buffer';
import { ERROR_CODES } from '../src/models/errors';
import { TokenDetailsModel } from '../src/models/token-details-model';
import { makeFakeSubscription } from './make-fake-subscription';
import { deleteDatabase } from './testing-utils/db-helper';
import { compareDetails } from './testing-utils/detail-comparator';
const EXAMPLE_INPUT = {
swScope: '/example-scope',
vapidKey: base64ToArrayBuffer(
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I'
),
subscription: makeFakeSubscription(),
fcmSenderId: '1234567',
fcmToken: 'qwerty',
fcmPushSet: '7654321'
};
describe('Firebase Messaging > TokenDetailsModel.deleteToken()', () => {
let globalTokenModel: TokenDetailsModel;
beforeEach(() => {
globalTokenModel = new TokenDetailsModel();
});
afterEach(async () => {
await globalTokenModel.closeDatabase();
await deleteDatabase('fcm_token_details_db');
});
it('should handle no input', () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel.deleteToken(undefined as any).then(
() => {
throw new Error('Expected this to throw an error due to no token');
},
err => {
assert.equal('messaging/' + ERROR_CODES.INVALID_DELETE_TOKEN, err.code);
}
);
});
it('should handle empty string', () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel.deleteToken('').then(
() => {
throw new Error('Expected this to throw an error due to no token');
},
err => {
assert.equal('messaging/' + ERROR_CODES.INVALID_DELETE_TOKEN, err.code);
}
);
});
it('should delete current token', () => {
globalTokenModel = new TokenDetailsModel();
const now = Date.now();
const clock = sinon.useFakeTimers(now);
return globalTokenModel
.saveTokenDetails(EXAMPLE_INPUT)
.then(() => {
return globalTokenModel.deleteToken(EXAMPLE_INPUT.fcmToken);
})
.then(details => {
compareDetails(EXAMPLE_INPUT, details, now);
return globalTokenModel.getTokenDetailsFromToken(
EXAMPLE_INPUT.fcmToken
);
})
.then(tokenDetails => {
assert.equal(null, tokenDetails);
clock.restore();
});
});
it('should handle deleting a non-existant token', () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel.deleteToken('bad-token').then(
() => {
throw new Error('Expected this delete to throw and error.');
},
err => {
assert.equal(
'messaging/' + ERROR_CODES.DELETE_TOKEN_NOT_FOUND,
err.code
);
}
);
});
});
@@ -1,121 +0,0 @@
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from 'chai';
import * as sinon from 'sinon';
import { arrayBufferToBase64 } from '../src/helpers/array-buffer-to-base64';
import { base64ToArrayBuffer } from '../src/helpers/base64-to-array-buffer';
import { ERROR_CODES } from '../src/models/errors';
import { TokenDetailsModel } from '../src/models/token-details-model';
import { makeFakeSubscription } from './make-fake-subscription';
import { deleteDatabase } from './testing-utils/db-helper';
import { compareDetails } from './testing-utils/detail-comparator';
const EXAMPLE_INPUT = {
swScope: '/example-scope',
vapidKey: base64ToArrayBuffer(
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I'
),
subscription: makeFakeSubscription(),
fcmSenderId: '1234567',
fcmToken: 'qwerty',
fcmPushSet: '7654321'
};
describe('Firebase Messaging > TokenDetailsModel.getTokenDetailsFromToken()', () => {
let globalTokenModel: TokenDetailsModel;
beforeEach(() => {
globalTokenModel = new TokenDetailsModel();
});
afterEach(async () => {
await globalTokenModel.closeDatabase();
await deleteDatabase('fcm_token_details_db');
});
['', [], {}, true, null, 123].forEach(badInput => {
it(`should throw on bad scope input ${JSON.stringify(badInput)}`, () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel.getTokenDetailsFromSWScope(badInput as any).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SCOPE, err.code);
}
);
});
});
['', [], {}, true, null, 123].forEach(badInput => {
it(`should throw on bad FCM Token input: '${badInput}'`, () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel.getTokenDetailsFromToken(badInput as any).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_TOKEN, err.code);
}
);
});
});
it('should get from scope', () => {
globalTokenModel = new TokenDetailsModel();
const now = Date.now();
const clock = sinon.useFakeTimers(now);
return globalTokenModel
.getTokenDetailsFromSWScope(EXAMPLE_INPUT.swScope)
.then(details => {
assert.notExists(details);
return globalTokenModel.saveTokenDetails(EXAMPLE_INPUT);
})
.then(() => {
return globalTokenModel.getTokenDetailsFromSWScope(
EXAMPLE_INPUT.swScope
);
})
.then(details => {
assert.exists(details);
compareDetails(EXAMPLE_INPUT, details!, now);
clock.restore();
});
});
it('should get from token', () => {
globalTokenModel = new TokenDetailsModel();
const now = Date.now();
const clock = sinon.useFakeTimers(now);
return globalTokenModel
.getTokenDetailsFromToken(EXAMPLE_INPUT.fcmToken)
.then(details => {
assert.notExists(details);
return globalTokenModel.saveTokenDetails(EXAMPLE_INPUT);
})
.then(() => {
return globalTokenModel.getTokenDetailsFromToken(
EXAMPLE_INPUT.fcmToken
);
})
.then(details => {
assert.exists(details);
compareDetails(EXAMPLE_INPUT, details!, now);
clock.restore();
});
});
});
@@ -1,171 +0,0 @@
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from 'chai';
import { base64ToArrayBuffer } from '../src/helpers/base64-to-array-buffer';
import { ERROR_CODES } from '../src/models/errors';
import { TokenDetailsModel } from '../src/models/token-details-model';
import { makeFakeSubscription } from './make-fake-subscription';
import { deleteDatabase } from './testing-utils/db-helper';
const EXAMPLE_INPUT = {
swScope: '/example-scope',
vapidKey: base64ToArrayBuffer(
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I'
),
subscription: makeFakeSubscription(),
fcmSenderId: '1234567',
fcmToken: 'qwerty',
fcmPushSet: '7654321'
};
describe('Firebase Messaging > TokenDetailsModel.saveToken()', () => {
let globalTokenModel: TokenDetailsModel;
beforeEach(() => {
globalTokenModel = new TokenDetailsModel();
});
afterEach(async () => {
await globalTokenModel.closeDatabase();
await deleteDatabase('fcm_token_details_db');
});
it('should throw on bad input', () => {
const badInputs = ['', [], {}, true, null, 123];
const promises = badInputs.map((badInput: any) => {
globalTokenModel = new TokenDetailsModel();
const validInput = { ...EXAMPLE_INPUT };
validInput.swScope = badInput;
return globalTokenModel.saveTokenDetails(validInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SCOPE, err.code);
}
);
});
return Promise.all(promises);
});
it('should throw on bad vapid key input', () => {
const badInputs = ['', [], {}, true, null, 123];
const promises = badInputs.map((badInput: any) => {
globalTokenModel = new TokenDetailsModel();
const validInput = { ...EXAMPLE_INPUT };
validInput.vapidKey = badInput;
return globalTokenModel.saveTokenDetails(validInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_VAPID_KEY, err.code);
}
);
});
return Promise.all(promises);
});
it('should throw on bad subscription input', () => {
const badInputs = ['', [], {}, true, null, 123];
const promises = badInputs.map((badInput: any) => {
globalTokenModel = new TokenDetailsModel();
const validInput = { ...EXAMPLE_INPUT };
validInput.subscription = badInput;
return globalTokenModel.saveTokenDetails(validInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SUBSCRIPTION, err.code);
}
);
});
return Promise.all(promises);
});
it('should throw on bad send id input', () => {
const badInputs = ['', [], {}, true, null, 123];
const promises = badInputs.map((badInput: any) => {
globalTokenModel = new TokenDetailsModel();
const validInput = { ...EXAMPLE_INPUT };
validInput.fcmSenderId = badInput;
return globalTokenModel.saveTokenDetails(validInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SENDER_ID, err.code);
}
);
});
return Promise.all(promises);
});
it('should throw on bad token input', () => {
const badInputs = ['', [], {}, true, null, 123];
const promises = badInputs.map((badInput: any) => {
globalTokenModel = new TokenDetailsModel();
const validInput = { ...EXAMPLE_INPUT };
validInput.fcmToken = badInput;
return globalTokenModel.saveTokenDetails(validInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_TOKEN, err.code);
}
);
});
return Promise.all(promises);
});
it('should throw on bad pushSet input', () => {
const badInputs = ['', [], {}, true, null, 123];
const promises = badInputs.map((badInput: any) => {
globalTokenModel = new TokenDetailsModel();
const validInput = { ...EXAMPLE_INPUT };
validInput.fcmPushSet = badInput;
return globalTokenModel.saveTokenDetails(validInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_PUSH_SET, err.code);
}
);
});
return Promise.all(promises);
});
it('should save valid details', () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel.saveTokenDetails(EXAMPLE_INPUT);
});
});
@@ -1,87 +0,0 @@
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from 'chai';
import { base64ToArrayBuffer } from '../src/helpers/base64-to-array-buffer';
import { ERROR_CODES } from '../src/models/errors';
import { VapidDetailsModel } from '../src/models/vapid-details-model';
import { deleteDatabase } from './testing-utils/db-helper';
const EXAMPLE_SCOPE = '/example-scope';
const EXAMPLE_VAPID_STRING =
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I';
const EXAMPLE_VAPID_KEY = base64ToArrayBuffer(EXAMPLE_VAPID_STRING);
describe('Firebase Messaging > VapidDetailsModel.deleteToken()', () => {
let vapidModel: VapidDetailsModel;
beforeEach(() => {
vapidModel = new VapidDetailsModel();
});
afterEach(async () => {
await vapidModel.closeDatabase();
await deleteDatabase('fcm_vapid_details_db');
});
it('should throw on bad scope input', () => {
const badInputs = ['', [], {}, true, null, 123];
badInputs.forEach(badInput => {
vapidModel = new VapidDetailsModel();
return vapidModel
.saveVapidDetails(badInput as any, EXAMPLE_VAPID_KEY)
.then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SCOPE, err.code);
}
);
});
});
it('should delete non existant details', () => {
vapidModel = new VapidDetailsModel();
return vapidModel.deleteVapidDetails(EXAMPLE_SCOPE).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal(
'messaging/' + ERROR_CODES.DELETE_SCOPE_NOT_FOUND,
err.code
);
}
);
});
it('should save and delete details', () => {
vapidModel = new VapidDetailsModel();
return vapidModel
.saveVapidDetails(EXAMPLE_SCOPE, EXAMPLE_VAPID_KEY)
.then(() => {
return vapidModel.deleteVapidDetails(EXAMPLE_SCOPE);
})
.then(vapidKey => {
assert.deepEqual(vapidKey, EXAMPLE_VAPID_KEY);
return vapidModel.getVapidFromSWScope(EXAMPLE_SCOPE);
})
.then(vapid => {
assert.equal(vapid, null);
});
});
});
@@ -1,68 +0,0 @@
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from 'chai';
import { base64ToArrayBuffer } from '../src/helpers/base64-to-array-buffer';
import { ERROR_CODES } from '../src/models/errors';
import { VapidDetailsModel } from '../src/models/vapid-details-model';
import { deleteDatabase } from './testing-utils/db-helper';
const EXAMPLE_SCOPE = '/example-scope';
const EXAMPLE_VAPID_STRING =
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I';
const EXAMPLE_VAPID_KEY = base64ToArrayBuffer(EXAMPLE_VAPID_STRING);
describe('Firebase Messaging > VapidDetailsModel.getVapidFromSWScope()', () => {
let vapidModel: VapidDetailsModel;
beforeEach(() => {
vapidModel = new VapidDetailsModel();
});
afterEach(async () => {
await vapidModel.closeDatabase();
await deleteDatabase('fcm_vapid_details_db');
});
const badInputs = ['', [], {}, true, null, 123];
badInputs.forEach(badInput => {
it(`should throw on bad scope input ${badInput}`, () => {
return vapidModel.getVapidFromSWScope(badInput as any).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SCOPE, err.code);
}
);
});
});
it('should get vapid key', () => {
return vapidModel
.getVapidFromSWScope(EXAMPLE_SCOPE)
.then(vapidKey => {
assert.equal(null, vapidKey);
return vapidModel.saveVapidDetails(EXAMPLE_SCOPE, EXAMPLE_VAPID_KEY);
})
.then(() => {
return vapidModel.getVapidFromSWScope(EXAMPLE_SCOPE);
})
.then(vapidKey => {
assert.deepEqual(EXAMPLE_VAPID_KEY, vapidKey);
});
});
});
@@ -1,76 +0,0 @@
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from 'chai';
import { base64ToArrayBuffer } from '../src/helpers/base64-to-array-buffer';
import { ERROR_CODES } from '../src/models/errors';
import { VapidDetailsModel } from '../src/models/vapid-details-model';
import { deleteDatabase } from './testing-utils/db-helper';
const EXAMPLE_SCOPE = '/example-scope';
const EXAMPLE_VAPID_STRING =
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I';
const EXAMPLE_VAPID_KEY = base64ToArrayBuffer(EXAMPLE_VAPID_STRING);
describe('Firebase Messaging > VapidDetailsModel.saveVapidDetails()', () => {
let vapidModel: VapidDetailsModel;
beforeEach(() => {
vapidModel = new VapidDetailsModel();
});
afterEach(async () => {
await vapidModel.closeDatabase();
await deleteDatabase('fcm_vapid_details_db');
});
it('should throw on bad scope input', () => {
const badInputs = ['', [], {}, true, null, 123];
badInputs.forEach(badInput => {
vapidModel = new VapidDetailsModel();
return vapidModel
.saveVapidDetails(badInput as any, EXAMPLE_VAPID_KEY)
.then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SCOPE, err.code);
}
);
});
});
it('should throw on bad vapid input', () => {
const badInputs = ['', [], {}, true, null, 123];
badInputs.forEach(badInput => {
vapidModel = new VapidDetailsModel();
return vapidModel.saveVapidDetails(EXAMPLE_SCOPE, badInput as any).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_VAPID_KEY, err.code);
}
);
});
});
it('should save valid details', () => {
vapidModel = new VapidDetailsModel();
return vapidModel.saveVapidDetails(EXAMPLE_SCOPE, EXAMPLE_VAPID_KEY);
});
});
@@ -0,0 +1,154 @@
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from 'chai';
import { base64ToArrayBuffer } from '../src/helpers/base64-to-array-buffer';
import { ERROR_CODES } from '../src/models/errors';
import { VapidDetailsModel } from '../src/models/vapid-details-model';
import { deleteDatabase } from './testing-utils/db-helper';
const EXAMPLE_SCOPE = '/example-scope';
const EXAMPLE_VAPID_STRING =
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I';
const EXAMPLE_VAPID_KEY = base64ToArrayBuffer(EXAMPLE_VAPID_STRING);
const BAD_INPUTS: any[] = ['', [], {}, true, null, 123];
describe('Firebase Messaging > VapidDetailsModel.saveVapidDetails()', () => {
let vapidModel: VapidDetailsModel;
beforeEach(() => {
vapidModel = new VapidDetailsModel();
});
afterEach(async () => {
await vapidModel.closeDatabase();
await deleteDatabase('fcm_vapid_details_db');
});
describe('saveVapidDetails', () => {
it('should throw on bad scope input', () => {
for (const badInput of BAD_INPUTS) {
vapidModel = new VapidDetailsModel();
return vapidModel.saveVapidDetails(badInput, EXAMPLE_VAPID_KEY).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SCOPE, err.code);
}
);
}
});
it('should throw on bad vapid input', () => {
for (const badInput of BAD_INPUTS) {
vapidModel = new VapidDetailsModel();
return vapidModel.saveVapidDetails(EXAMPLE_SCOPE, badInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_VAPID_KEY, err.code);
}
);
}
});
it('should save valid details', () => {
vapidModel = new VapidDetailsModel();
return vapidModel.saveVapidDetails(EXAMPLE_SCOPE, EXAMPLE_VAPID_KEY);
});
});
describe('getVapidFromSWScope', () => {
for (const badInput of BAD_INPUTS) {
it(`should throw on bad scope input ${badInput}`, () => {
return vapidModel.getVapidFromSWScope(badInput).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SCOPE, err.code);
}
);
});
}
it('should get vapid key', () => {
return vapidModel
.getVapidFromSWScope(EXAMPLE_SCOPE)
.then(vapidKey => {
assert.equal(null, vapidKey);
return vapidModel.saveVapidDetails(EXAMPLE_SCOPE, EXAMPLE_VAPID_KEY);
})
.then(() => {
return vapidModel.getVapidFromSWScope(EXAMPLE_SCOPE);
})
.then(vapidKey => {
assert.deepEqual(EXAMPLE_VAPID_KEY, vapidKey);
});
});
});
describe('deleteToken', () => {
it('should throw on bad scope input', () => {
for (const badInput of BAD_INPUTS) {
vapidModel = new VapidDetailsModel();
return vapidModel.saveVapidDetails(badInput, EXAMPLE_VAPID_KEY).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SCOPE, err.code);
}
);
}
});
it('should delete non existant details', () => {
vapidModel = new VapidDetailsModel();
return vapidModel.deleteVapidDetails(EXAMPLE_SCOPE).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal(
'messaging/' + ERROR_CODES.DELETE_SCOPE_NOT_FOUND,
err.code
);
}
);
});
it('should save and delete details', () => {
vapidModel = new VapidDetailsModel();
return vapidModel
.saveVapidDetails(EXAMPLE_SCOPE, EXAMPLE_VAPID_KEY)
.then(() => {
return vapidModel.deleteVapidDetails(EXAMPLE_SCOPE);
})
.then(vapidKey => {
assert.deepEqual(vapidKey, EXAMPLE_VAPID_KEY);
return vapidModel.getVapidFromSWScope(EXAMPLE_SCOPE);
})
.then(vapid => {
assert.equal(vapid, null);
});
});
});
});