Refactor IndexedDb operations (#645)

* Refactor IndexedDb operations

Simplifies a lot of IndexedDb operations and removes most of the duplicated code. Also refactors tests.

* [AUTOMATED]: Prettier Code Styling

* [AUTOMATED]: License Headers

* Fix failing tests

* Address comments

* Address comments
This commit is contained in:
Mertcan Mermerkaya
2018-04-06 19:34:38 +01:00
committed by GitHub
parent 25d32da6e6
commit 52aa813f55
13 changed files with 480 additions and 496 deletions
@@ -367,16 +367,19 @@ export class SWController extends ControllerInterface {
* This will return the default VAPID key or the uint8array version of the
* public VAPID key provided by the developer.
*/
getPublicVapidKey_(): Promise<Uint8Array> {
return this.getSWRegistration_()
.then(swReg => {
return this.getVapidDetailsModel().getVapidFromSWScope(swReg.scope);
})
.then(vapidKeyFromDatabase => {
if (vapidKeyFromDatabase === null) {
return DEFAULT_PUBLIC_VAPID_KEY;
}
return vapidKeyFromDatabase;
});
async getPublicVapidKey_(): Promise<Uint8Array> {
const swReg = await this.getSWRegistration_();
if (!swReg) {
throw this.errorFactory_.create(ERROR_CODES.SW_REGISTRATION_EXPECTED);
}
const vapidKeyFromDatabase = await this.getVapidDetailsModel().getVapidFromSWScope(
swReg.scope
);
if (vapidKeyFromDatabase == null) {
return DEFAULT_PUBLIC_VAPID_KEY;
}
return vapidKeyFromDatabase;
}
}
@@ -0,0 +1,20 @@
/**
* 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 interface VapidDetails {
swScope: string;
vapidKey: Uint8Array;
}
+105 -52
View File
@@ -18,73 +18,126 @@ import { ErrorFactory } from '@firebase/util';
import { ERROR_CODES, ERROR_MAP } from './errors';
export class DBInterface {
private readonly DB_NAME_: string;
private readonly dbVersion_: number;
private openDbPromise_: Promise<IDBDatabase> | null;
protected errorFactory_: ErrorFactory<string>;
protected TRANSACTION_READ_WRITE: IDBTransactionMode;
export abstract class DBInterface {
private dbPromise: Promise<IDBDatabase> | null = null;
constructor(dbName: string, dbVersion: number) {
this.errorFactory_ = new ErrorFactory('messaging', 'Messaging', ERROR_MAP);
this.DB_NAME_ = dbName;
this.dbVersion_ = dbVersion;
this.openDbPromise_ = null;
this.TRANSACTION_READ_WRITE = 'readwrite';
}
protected abstract readonly dbName: string;
protected abstract readonly dbVersion: number;
protected abstract readonly objectStoreName: string;
protected readonly errorFactory: ErrorFactory<string> = new ErrorFactory(
'messaging',
'Messaging',
ERROR_MAP
);
/**
* Get the indexedDB as a promise.
* Database initialization.
*
* This function should create and update object stores.
*/
// Visible for testing
// TODO: Make protected
openDatabase(): Promise<IDBDatabase> {
if (this.openDbPromise_) {
return this.openDbPromise_;
protected abstract onDbUpgrade(
db: IDBDatabase,
event: IDBVersionChangeEvent
): void;
/** Gets record(s) from the objectStore that match the given key. */
get<T>(key: IDBValidKey): Promise<T | undefined> {
return this.createTransaction(objectStore => objectStore.get(key));
}
/** Gets record(s) from the objectStore that match the given index. */
getIndex<T>(index: string, key: IDBValidKey): Promise<T | undefined> {
function runRequest(objectStore: IDBObjectStore): IDBRequest {
const idbIndex = objectStore.index(index);
return idbIndex.get(key);
}
this.openDbPromise_ = new Promise((resolve, reject) => {
const request = indexedDB.open(this.DB_NAME_, this.dbVersion_);
request.onerror = event => {
reject((event.target as IDBRequest).error);
};
request.onsuccess = event => {
resolve((event.target as IDBRequest).result);
};
request.onupgradeneeded = event => {
let db;
try {
db = (event.target as IDBRequest).result;
this.onDBUpgrade(db, event);
} catch (err) {
// close the database as it can't be used.
db.close();
reject(err);
}
};
});
return this.createTransaction(runRequest);
}
return this.openDbPromise_;
/** Assigns or overwrites the record for the given value. */
// tslint:disable-next-line:no-any IndexedDB values are of type "any"
put(value: any): Promise<void> {
return this.createTransaction(
objectStore => objectStore.put(value),
'readwrite'
);
}
/** Deletes record(s) from the objectStore that match the given key. */
delete(key: IDBValidKey | IDBKeyRange): Promise<void> {
return this.createTransaction(
objectStore => objectStore.delete(key),
'readwrite'
);
}
/**
* Close the currently open database.
*/
closeDatabase(): Promise<void> {
return Promise.resolve().then(() => {
if (this.openDbPromise_) {
return this.openDbPromise_.then(db => {
db.close();
this.openDbPromise_ = null;
});
}
});
async closeDatabase(): Promise<void> {
if (this.dbPromise) {
const db = await this.dbPromise;
db.close();
this.dbPromise = null;
}
}
/**
* @protected
* Creates an IndexedDB Transaction and passes its objectStore to the
* runRequest function, which runs the database request.
*
* @return Promise that resolves with the result of the runRequest function
*/
onDBUpgrade(db: IDBDatabase, event: IDBVersionChangeEvent): void {
throw this.errorFactory_.create(ERROR_CODES.SHOULD_BE_INHERITED);
private async createTransaction<T>(
runRequest: (objectStore: IDBObjectStore) => IDBRequest,
mode?: 'readonly' | 'readwrite'
): Promise<T> {
const db = await this.getDb();
const transaction = db.transaction(this.objectStoreName, mode);
const request = transaction.objectStore(this.objectStoreName);
const result = await promisify<T>(runRequest(request));
return new Promise<T>((resolve, reject) => {
transaction.oncomplete = () => {
resolve(result);
};
transaction.onerror = () => {
reject(transaction.error);
};
});
}
/** Gets the cached db connection or opens a new one. */
private getDb(): Promise<IDBDatabase> {
if (!this.dbPromise) {
this.dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
this.dbPromise = null;
reject(request.error);
};
request.onupgradeneeded = event =>
this.onDbUpgrade(request.result, event);
});
}
return this.dbPromise;
}
}
/** Promisifies an IDBRequest. Resolves with the IDBRequest's result. */
function promisify<T>(request: IDBRequest): Promise<T> {
return new Promise<T>((resolve, reject) => {
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
@@ -20,19 +20,15 @@ import { cleanV1 } from './clean-v1-undefined';
import { DBInterface } from './db-interface';
import { ERROR_CODES } from './errors';
const FCM_TOKEN_OBJ_STORE = 'fcm_token_object_Store';
const DB_NAME = 'fcm_token_details_db';
const DB_VERSION = 2;
export class TokenDetailsModel extends DBInterface {
constructor() {
super(DB_NAME, DB_VERSION);
}
protected readonly dbName: string = 'fcm_token_details_db';
protected readonly dbVersion: number = 2;
protected readonly objectStoreName: string = 'fcm_token_object_Store';
onDBUpgrade(db: IDBDatabase, evt: IDBVersionChangeEvent): void {
protected onDbUpgrade(db: IDBDatabase, evt: IDBVersionChangeEvent): void {
if (evt.oldVersion < 1) {
// New IDB instance
const objectStore = db.createObjectStore(FCM_TOKEN_OBJ_STORE, {
const objectStore = db.createObjectStore(this.objectStoreName, {
keyPath: 'swScope'
});
@@ -59,16 +55,16 @@ export class TokenDetailsModel extends DBInterface {
* validate the input.
* @return Promise that resolves if input is valid, rejects otherwise.
*/
private async validateInputs_(input: Partial<TokenDetails>): Promise<void> {
private validateInputs(input: Partial<TokenDetails>): void {
if (input.fcmToken) {
if (typeof input.fcmToken !== 'string' || input.fcmToken.length === 0) {
return Promise.reject(this.errorFactory_.create(ERROR_CODES.BAD_TOKEN));
throw this.errorFactory.create(ERROR_CODES.BAD_TOKEN);
}
}
if (input.swScope) {
if (typeof input.swScope !== 'string' || input.swScope.length === 0) {
return Promise.reject(this.errorFactory_.create(ERROR_CODES.BAD_SCOPE));
throw this.errorFactory.create(ERROR_CODES.BAD_SCOPE);
}
}
@@ -77,17 +73,13 @@ export class TokenDetailsModel extends DBInterface {
!(input.vapidKey instanceof Uint8Array) ||
input.vapidKey.length !== 65
) {
return Promise.reject(
this.errorFactory_.create(ERROR_CODES.BAD_VAPID_KEY)
);
throw this.errorFactory.create(ERROR_CODES.BAD_VAPID_KEY);
}
}
if (input.subscription) {
if (!(input.subscription instanceof PushSubscription)) {
return Promise.reject(
this.errorFactory_.create(ERROR_CODES.BAD_SUBSCRIPTION)
);
throw this.errorFactory.create(ERROR_CODES.BAD_SUBSCRIPTION);
}
}
@@ -96,9 +88,7 @@ export class TokenDetailsModel extends DBInterface {
typeof input.fcmSenderId !== 'string' ||
input.fcmSenderId.length === 0
) {
return Promise.reject(
this.errorFactory_.create(ERROR_CODES.BAD_SENDER_ID)
);
throw this.errorFactory.create(ERROR_CODES.BAD_SENDER_ID);
}
}
@@ -107,9 +97,7 @@ export class TokenDetailsModel extends DBInterface {
typeof input.fcmPushSet !== 'string' ||
input.fcmPushSet.length === 0
) {
return Promise.reject(
this.errorFactory_.create(ERROR_CODES.BAD_PUSH_SET)
);
throw this.errorFactory.create(ERROR_CODES.BAD_PUSH_SET);
}
}
}
@@ -119,29 +107,16 @@ export class TokenDetailsModel extends DBInterface {
* @param {string} fcmToken
* @return {Promise<TokenDetails>} The details associated with that token.
*/
getTokenDetailsFromToken(fcmToken: string): Promise<TokenDetails> {
async getTokenDetailsFromToken(
fcmToken: string
): Promise<TokenDetails | undefined> {
if (!fcmToken) {
return Promise.reject(this.errorFactory_.create(ERROR_CODES.BAD_TOKEN));
throw this.errorFactory.create(ERROR_CODES.BAD_TOKEN);
}
return this.validateInputs_({ fcmToken })
.then(() => this.openDatabase())
.then(db => {
return new Promise<TokenDetails>((resolve, reject) => {
const transaction = db.transaction([FCM_TOKEN_OBJ_STORE]);
const objectStore = transaction.objectStore(FCM_TOKEN_OBJ_STORE);
const index = objectStore.index('fcmToken');
const request = index.get(fcmToken);
request.onerror = event => {
reject((event.target as IDBRequest).error);
};
request.onsuccess = event => {
const result: TokenDetails | null = (event.target as IDBRequest)
.result;
resolve(result);
};
});
});
this.validateInputs({ fcmToken });
return this.getIndex<TokenDetails>('fcmToken', fcmToken);
}
/**
@@ -149,160 +124,97 @@ export class TokenDetailsModel extends DBInterface {
* indexedDB.
* @return The details associated with that token.
*/
getTokenDetailsFromSWScope(swScope: string): Promise<TokenDetails> {
async getTokenDetailsFromSWScope(
swScope: string
): Promise<TokenDetails | undefined> {
if (!swScope) {
return Promise.reject(this.errorFactory_.create(ERROR_CODES.BAD_SCOPE));
throw this.errorFactory.create(ERROR_CODES.BAD_SCOPE);
}
return this.validateInputs_({ swScope })
.then(() => {
return this.openDatabase();
})
.then(db => {
return new Promise<TokenDetails>((resolve, reject) => {
const transaction = db.transaction([FCM_TOKEN_OBJ_STORE]);
const objectStore = transaction.objectStore(FCM_TOKEN_OBJ_STORE);
const scopeRequest = objectStore.get(swScope);
scopeRequest.onerror = event => {
reject((event.target as IDBRequest).error);
};
this.validateInputs({ swScope });
scopeRequest.onsuccess = event => {
const result: TokenDetails | null = (event.target as IDBRequest)
.result;
resolve(result);
};
});
});
return this.get<TokenDetails>(swScope);
}
/**
* Save the details for the fcm token for re-use at a later date.
* @param input A plain js object containing args to save.
*/
saveTokenDetails({
swScope,
vapidKey,
subscription,
fcmSenderId,
fcmToken,
fcmPushSet
}: TokenDetails): Promise<void> {
if (!swScope) {
return Promise.reject(this.errorFactory_.create(ERROR_CODES.BAD_SCOPE));
async saveTokenDetails(tokenDetails: TokenDetails): Promise<void> {
if (!tokenDetails.swScope) {
throw this.errorFactory.create(ERROR_CODES.BAD_SCOPE);
}
if (!vapidKey) {
return Promise.reject(
this.errorFactory_.create(ERROR_CODES.BAD_VAPID_KEY)
);
if (!tokenDetails.vapidKey) {
throw this.errorFactory.create(ERROR_CODES.BAD_VAPID_KEY);
}
if (!subscription) {
return Promise.reject(
this.errorFactory_.create(ERROR_CODES.BAD_SUBSCRIPTION)
);
if (!tokenDetails.subscription) {
throw this.errorFactory.create(ERROR_CODES.BAD_SUBSCRIPTION);
}
if (!fcmSenderId) {
return Promise.reject(
this.errorFactory_.create(ERROR_CODES.BAD_SENDER_ID)
);
if (!tokenDetails.fcmSenderId) {
throw this.errorFactory.create(ERROR_CODES.BAD_SENDER_ID);
}
if (!fcmToken) {
return Promise.reject(this.errorFactory_.create(ERROR_CODES.BAD_TOKEN));
if (!tokenDetails.fcmToken) {
throw this.errorFactory.create(ERROR_CODES.BAD_TOKEN);
}
if (!fcmPushSet) {
return Promise.reject(
this.errorFactory_.create(ERROR_CODES.BAD_PUSH_SET)
);
if (!tokenDetails.fcmPushSet) {
throw this.errorFactory.create(ERROR_CODES.BAD_PUSH_SET);
}
return this.validateInputs_({
swScope,
vapidKey,
subscription,
fcmSenderId,
fcmToken,
fcmPushSet
})
.then(() => {
return this.openDatabase();
})
.then(db => {
const details = {
swScope: swScope,
vapidKey: arrayBufferToBase64(vapidKey as Uint8Array),
endpoint: subscription.endpoint,
auth: arrayBufferToBase64(subscription.getKey('auth')),
p256dh: arrayBufferToBase64(subscription.getKey('p256dh')),
fcmSenderId: fcmSenderId,
fcmToken: fcmToken,
fcmPushSet: fcmPushSet,
createTime: Date.now()
};
this.validateInputs(tokenDetails);
return new Promise<void>((resolve, reject) => {
const transaction = db.transaction(
[FCM_TOKEN_OBJ_STORE],
this.TRANSACTION_READ_WRITE
);
const objectStore = transaction.objectStore(FCM_TOKEN_OBJ_STORE);
const request = objectStore.put(details);
request.onerror = event => {
reject((event.target as IDBRequest).error);
};
request.onsuccess = event => {
resolve();
};
});
});
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);
}
/**
* This method deletes details of the current FCM token.
* It's returning a promise in case we need to move to an async
* method for deleting at a later date.
* @return {Promise<Object>} Resolves once the FCM token details have been
* deleted and returns the deleted details.
*
* @return Resolves once the FCM token details have been deleted and returns
* the deleted details.
*/
deleteToken(token: string): Promise<TokenDetails> {
async deleteToken(token: string): Promise<TokenDetails> {
if (typeof token !== 'string' || token.length === 0) {
return Promise.reject(
this.errorFactory_.create(ERROR_CODES.INVALID_DELETE_TOKEN)
this.errorFactory.create(ERROR_CODES.INVALID_DELETE_TOKEN)
);
}
return this.getTokenDetailsFromToken(token).then(details => {
if (!details) {
throw this.errorFactory_.create(ERROR_CODES.DELETE_TOKEN_NOT_FOUND);
}
const details = await this.getTokenDetailsFromToken(token);
if (!details) {
throw this.errorFactory.create(ERROR_CODES.DELETE_TOKEN_NOT_FOUND);
}
return this.openDatabase().then(db => {
return new Promise<TokenDetails>((resolve, reject) => {
const transaction = db.transaction(
[FCM_TOKEN_OBJ_STORE],
this.TRANSACTION_READ_WRITE
);
const objectStore = transaction.objectStore(FCM_TOKEN_OBJ_STORE);
const request = objectStore.delete(details.swScope);
request.onerror = event => {
reject((event.target as IDBRequest).error);
};
request.onsuccess = event => {
if ((event.target as IDBRequest).result === 0) {
reject(
this.errorFactory_.create(ERROR_CODES.FAILED_TO_DELETE_TOKEN)
);
return;
}
resolve(details);
};
});
});
});
await this.delete(details.swScope);
return details;
}
}
/** Promisifies an IDBRequest. Resolves with the IDBRequest's result. */
function promisify<ReturnType>(request: IDBRequest): Promise<ReturnType> {
return new Promise<ReturnType>((resolve, reject) => {
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
@@ -14,90 +14,52 @@
* limitations under the License.
*/
import { VapidDetails } from '../interfaces/vapid-details';
import { DBInterface } from './db-interface';
import { ERROR_CODES } from './errors';
const FCM_VAPID_OBJ_STORE = 'fcm_vapid_object_Store';
const DB_NAME = 'fcm_vapid_details_db';
const DB_VERSION = 1;
const UNCOMPRESSED_PUBLIC_KEY_SIZE = 65;
export class VapidDetailsModel extends DBInterface {
constructor() {
super(DB_NAME, DB_VERSION);
}
protected readonly dbName: string = 'fcm_vapid_details_db';
protected readonly dbVersion: number = 1;
protected readonly objectStoreName: string = 'fcm_vapid_object_Store';
onDBUpgrade(db: IDBDatabase): void {
db.createObjectStore(FCM_VAPID_OBJ_STORE, {
keyPath: 'swScope'
});
protected onDbUpgrade(db: IDBDatabase): void {
db.createObjectStore(this.objectStoreName, { keyPath: 'swScope' });
}
/**
* Given a service worker scope, this method will look up the vapid key
* in indexedDB.
*/
getVapidFromSWScope(swScope: string): Promise<Uint8Array> {
async getVapidFromSWScope(swScope: string): Promise<Uint8Array | undefined> {
if (typeof swScope !== 'string' || swScope.length === 0) {
return Promise.reject(this.errorFactory_.create(ERROR_CODES.BAD_SCOPE));
throw this.errorFactory.create(ERROR_CODES.BAD_SCOPE);
}
return this.openDatabase().then(db => {
return new Promise<Uint8Array>((resolve, reject) => {
const transaction = db.transaction([FCM_VAPID_OBJ_STORE]);
const objectStore = transaction.objectStore(FCM_VAPID_OBJ_STORE);
const scopeRequest = objectStore.get(swScope);
scopeRequest.onerror = () => {
reject(scopeRequest.error);
};
scopeRequest.onsuccess = () => {
const result = scopeRequest.result;
let vapidKey: any = null;
if (result) {
vapidKey = result.vapidKey;
}
resolve(vapidKey);
};
});
});
const result = await this.get<VapidDetails>(swScope);
return result ? result.vapidKey : undefined;
}
/**
* Save a vapid key against a swScope for later date.
*/
saveVapidDetails(swScope: string, vapidKey: Uint8Array): Promise<void> {
async saveVapidDetails(swScope: string, vapidKey: Uint8Array): Promise<void> {
if (typeof swScope !== 'string' || swScope.length === 0) {
return Promise.reject(this.errorFactory_.create(ERROR_CODES.BAD_SCOPE));
throw this.errorFactory.create(ERROR_CODES.BAD_SCOPE);
}
if (vapidKey === null || vapidKey.length !== UNCOMPRESSED_PUBLIC_KEY_SIZE) {
return Promise.reject(
this.errorFactory_.create(ERROR_CODES.BAD_VAPID_KEY)
);
throw this.errorFactory.create(ERROR_CODES.BAD_VAPID_KEY);
}
const details = {
swScope: swScope,
vapidKey: vapidKey
const details: VapidDetails = {
swScope,
vapidKey
};
return this.openDatabase().then(db => {
return new Promise<void>((resolve, reject) => {
const transaction = db.transaction(
[FCM_VAPID_OBJ_STORE],
this.TRANSACTION_READ_WRITE
);
const objectStore = transaction.objectStore(FCM_VAPID_OBJ_STORE);
const request = objectStore.put(details);
request.onerror = () => {
reject(request.error);
};
request.onsuccess = () => {
resolve();
};
});
});
return this.put(details);
}
/**
@@ -105,35 +67,13 @@ export class VapidDetailsModel extends DBInterface {
* Resolves once the scope/vapid details have been deleted and returns the
* deleted vapid key.
*/
deleteVapidDetails(swScope: string): Promise<Uint8Array> {
return this.getVapidFromSWScope(swScope).then(vapidKey => {
if (!vapidKey) {
throw this.errorFactory_.create(ERROR_CODES.DELETE_SCOPE_NOT_FOUND);
}
async deleteVapidDetails(swScope: string): Promise<Uint8Array> {
const vapidKey = await this.getVapidFromSWScope(swScope);
if (!vapidKey) {
throw this.errorFactory.create(ERROR_CODES.DELETE_SCOPE_NOT_FOUND);
}
return this.openDatabase().then(db => {
return new Promise<Uint8Array>((resolve, reject) => {
const transaction = db.transaction(
[FCM_VAPID_OBJ_STORE],
this.TRANSACTION_READ_WRITE
);
const objectStore = transaction.objectStore(FCM_VAPID_OBJ_STORE);
const request = objectStore.delete(swScope);
request.onerror = () => {
reject(request.error);
};
request.onsuccess = () => {
if (request.result === 0) {
reject(
this.errorFactory_.create(ERROR_CODES.FAILED_DELETE_VAPID_KEY)
);
return;
}
resolve(vapidKey);
};
});
});
});
await this.delete(swScope);
return vapidKey;
}
}
@@ -0,0 +1,124 @@
/**
* 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.
*/
import { expect } from 'chai';
import { DBInterface } from '../src/models/db-interface';
import { deleteDatabase } from './testing-utils/db-helper';
const VALUE = {
key: 'key-value',
index: 'index-value',
otherValue: 'other-value'
};
const DIFFERENT_VALUE = {
key: 'different-key-value',
index: 'different-index-value',
otherValue: 'different-other-value'
};
class MockDbInterface extends DBInterface {
readonly dbName: string = 'test-db';
readonly dbVersion: number = 1;
readonly objectStoreName: string = 'test-obj-store';
protected onDbUpgrade(db: IDBDatabase, event: IDBVersionChangeEvent): void {
const objectStore = db.createObjectStore(this.objectStoreName, {
keyPath: 'key'
});
objectStore.createIndex('indexName', 'index');
}
}
describe('DBInterface', () => {
let dbInterface: MockDbInterface;
beforeEach(() => {
dbInterface = new MockDbInterface();
});
afterEach(async () => {
dbInterface.closeDatabase();
await deleteDatabase(dbInterface.dbName);
});
describe('get', () => {
beforeEach(async () => {
await dbInterface.put(VALUE);
});
it('gets a value that exists', async () => {
const value = await dbInterface.get<string>(VALUE.key);
expect(value).to.deep.equal(VALUE);
});
it('returns undefined for a key that does not exist', async () => {
const value = await dbInterface.get<string>('randomKey');
expect(value).to.be.undefined;
});
});
describe('getIndex', () => {
beforeEach(async () => {
await dbInterface.put(VALUE);
});
it('gets a value that exists', async () => {
const value = await dbInterface.getIndex<string>(
'indexName',
VALUE.index
);
expect(value).to.deep.equal(VALUE);
});
it('returns undefined for an index that does not exist', async () => {
const value = await dbInterface.getIndex<string>(
'indexName',
'randomIndexValue'
);
expect(value).to.be.undefined;
});
});
describe('put', () => {
it('puts a value', async () => {
await dbInterface.put(VALUE);
expect(await dbInterface.get(VALUE.key)).to.deep.equal(VALUE);
});
it('puts multiple values', async () => {
await dbInterface.put(VALUE);
await dbInterface.put(DIFFERENT_VALUE);
expect(await dbInterface.get(VALUE.key)).to.deep.equal(VALUE);
expect(await dbInterface.get(DIFFERENT_VALUE.key)).to.deep.equal(
DIFFERENT_VALUE
);
});
});
describe('delete', () => {
it('deletes a value', async () => {
await dbInterface.put(VALUE);
await dbInterface.delete(VALUE.key);
expect(await dbInterface.get(VALUE.key)).to.be.undefined;
});
it('does not throw if key does not exist', async () => {
await dbInterface.delete('randomKey');
expect(await dbInterface.get('randomKey')).to.be.undefined;
});
});
});
-11
View File
@@ -31,15 +31,4 @@ describe(`Firebase Messaging > db-interface.onDBUpgrade()`, () => {
after(() => {
return cleanup();
});
it(`should throw error when not inherited`, () => {
return new DBInterface(TEST_DB_NAME, 1).openDatabase().then(
() => {
throw new Error(`An error was expected but method was successful.`);
},
err => {
expect(err.code).to.equal('messaging/should-be-overriden');
}
);
});
});
@@ -23,44 +23,33 @@ 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()', () => {
const EXAMPLE_INPUT = {
swScope: '/example-scope',
vapidKey: base64ToArrayBuffer(
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I'
),
subscription: makeFakeSubscription(),
fcmSenderId: '1234567',
fcmToken: 'qwerty',
fcmPushSet: '7654321'
};
let globalTokenModel;
const cleanUp = () => {
const promises: any = [];
if (globalTokenModel) {
promises.push(globalTokenModel.closeDatabase());
}
return Promise.all(promises)
.then(() => deleteDatabase('fcm_token_details_db'))
.then(() => (globalTokenModel = null));
};
let globalTokenModel: TokenDetailsModel;
beforeEach(() => {
return cleanUp();
globalTokenModel = new TokenDetailsModel();
});
after(() => {
return cleanUp();
afterEach(async () => {
await globalTokenModel.closeDatabase();
await deleteDatabase('fcm_token_details_db');
});
it('should handle no input', () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel.deleteToken().then(
return globalTokenModel.deleteToken(undefined as any).then(
() => {
throw new Error('Expected this to throw an error due to no token');
},
@@ -23,44 +23,34 @@ 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()', () => {
const EXAMPLE_INPUT = {
swScope: '/example-scope',
vapidKey: base64ToArrayBuffer(
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I'
),
subscription: makeFakeSubscription(),
fcmSenderId: '1234567',
fcmToken: 'qwerty',
fcmPushSet: '7654321'
};
let globalTokenModel;
const cleanUp = () => {
const promises: any[] = [];
if (globalTokenModel) {
promises.push(globalTokenModel.closeDatabase());
}
return Promise.all(promises)
.then(() => deleteDatabase('fcm_token_details_db'))
.then(() => (globalTokenModel = null));
};
let globalTokenModel: TokenDetailsModel;
beforeEach(() => {
return cleanUp();
globalTokenModel = new TokenDetailsModel();
});
after(() => {
return cleanUp();
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).then(
return globalTokenModel.getTokenDetailsFromSWScope(badInput as any).then(
() => {
throw new Error('Expected promise to reject');
},
@@ -74,7 +64,7 @@ describe('Firebase Messaging > TokenDetailsModel.getTokenDetailsFromToken()', ()
['', [], {}, true, null, 123].forEach(badInput => {
it(`should throw on bad FCM Token input: '${badInput}'`, () => {
globalTokenModel = new TokenDetailsModel();
return globalTokenModel.getTokenDetailsFromToken(badInput).then(
return globalTokenModel.getTokenDetailsFromToken(badInput as any).then(
() => {
throw new Error('Expected promise to reject');
},
@@ -92,7 +82,7 @@ describe('Firebase Messaging > TokenDetailsModel.getTokenDetailsFromToken()', ()
return globalTokenModel
.getTokenDetailsFromSWScope(EXAMPLE_INPUT.swScope)
.then(details => {
assert.equal(null, details);
assert.notExists(details);
return globalTokenModel.saveTokenDetails(EXAMPLE_INPUT);
})
.then(() => {
@@ -101,6 +91,7 @@ describe('Firebase Messaging > TokenDetailsModel.getTokenDetailsFromToken()', ()
);
})
.then(details => {
assert.exists(details);
compareDetails(EXAMPLE_INPUT, details, now);
clock.restore();
});
@@ -113,7 +104,7 @@ describe('Firebase Messaging > TokenDetailsModel.getTokenDetailsFromToken()', ()
return globalTokenModel
.getTokenDetailsFromToken(EXAMPLE_INPUT.fcmToken)
.then(details => {
assert.equal(null, details);
assert.notExists(details);
return globalTokenModel.saveTokenDetails(EXAMPLE_INPUT);
})
.then(() => {
@@ -122,6 +113,7 @@ describe('Firebase Messaging > TokenDetailsModel.getTokenDetailsFromToken()', ()
);
})
.then(details => {
assert.exists(details);
compareDetails(EXAMPLE_INPUT, details, now);
clock.restore();
});
@@ -20,38 +20,28 @@ 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()', () => {
const EXAMPLE_INPUT = {
swScope: '/example-scope',
vapidKey: base64ToArrayBuffer(
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I'
),
subscription: makeFakeSubscription(),
fcmSenderId: '1234567',
fcmToken: 'qwerty',
fcmPushSet: '7654321'
};
let globalTokenModel;
const cleanUp = () => {
const promises: any = [];
if (globalTokenModel) {
promises.push(globalTokenModel.closeDatabase());
}
return Promise.all(promises)
.then(() => deleteDatabase('fcm_token_details_db'))
.then(() => (globalTokenModel = null));
};
let globalTokenModel: TokenDetailsModel;
beforeEach(() => {
return cleanUp();
globalTokenModel = new TokenDetailsModel();
});
after(() => {
return cleanUp;
afterEach(async () => {
await globalTokenModel.closeDatabase();
await deleteDatabase('fcm_token_details_db');
});
it('should throw on bad input', () => {
@@ -19,46 +19,38 @@ 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()', () => {
const EXAMPLE_SCOPE = '/example-scope';
const EXAMPLE_VAPID_STRING =
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I';
const EXAMPLE_VAPID_KEY = base64ToArrayBuffer(EXAMPLE_VAPID_STRING);
let vapidModel;
const cleanUp = () => {
let promiseChain = Promise.resolve();
if (vapidModel) {
promiseChain = promiseChain.then(() => vapidModel.closeDatabase());
}
return promiseChain
.then(() => deleteDatabase('fcm_vapid_details_db'))
.then(() => (vapidModel = null));
};
let vapidModel: VapidDetailsModel;
beforeEach(() => {
return cleanUp();
vapidModel = new VapidDetailsModel();
});
after(() => {
return cleanUp();
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, EXAMPLE_VAPID_KEY).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SCOPE, err.code);
}
);
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);
}
);
});
});
@@ -89,7 +81,7 @@ describe('Firebase Messaging > VapidDetailsModel.deleteToken()', () => {
return vapidModel.getVapidFromSWScope(EXAMPLE_SCOPE);
})
.then(vapid => {
assert.deepEqual(vapid, null);
assert.equal(vapid, null);
});
});
});
@@ -19,39 +19,28 @@ 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()', () => {
const EXAMPLE_SCOPE = '/example-scope';
const EXAMPLE_VAPID_STRING =
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I';
const EXAMPLE_VAPID_KEY = base64ToArrayBuffer(EXAMPLE_VAPID_STRING);
let vapidModel;
const cleanUp = () => {
let promiseChain = Promise.resolve();
if (vapidModel) {
promiseChain = promiseChain.then(() => vapidModel.closeDatabase());
}
return promiseChain
.then(() => deleteDatabase('fcm_vapid_details_db'))
.then(() => (vapidModel = null));
};
let vapidModel: VapidDetailsModel;
beforeEach(() => {
return cleanUp();
vapidModel = new VapidDetailsModel();
});
after(() => {
return cleanUp();
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.getVapidFromSWScope(badInput).then(
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');
},
@@ -63,11 +52,10 @@ describe('Firebase Messaging > VapidDetailsModel.getVapidFromSWScope()', () => {
});
it('should get vapid key', () => {
vapidModel = new VapidDetailsModel();
return vapidModel
.getVapidFromSWScope(EXAMPLE_SCOPE)
.then(vapidKey => {
assert.deepEqual(null, vapidKey);
assert.equal(null, vapidKey);
return vapidModel.saveVapidDetails(EXAMPLE_SCOPE, EXAMPLE_VAPID_KEY);
})
.then(() => {
@@ -19,46 +19,38 @@ 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()', () => {
const EXAMPLE_SCOPE = '/example-scope';
const EXAMPLE_VAPID_STRING =
'BNJxw7sCGkGLOUP2cawBaBXRuWZ3lw_PmQMgreLVVvX_b' +
'4emEWVURkCF8fUTHEFe2xrEgTt5ilh5xD94v0pFe_I';
const EXAMPLE_VAPID_KEY = base64ToArrayBuffer(EXAMPLE_VAPID_STRING);
let vapidModel;
const cleanUp = () => {
let promiseChain = Promise.resolve();
if (vapidModel) {
promiseChain = promiseChain.then(() => vapidModel.closeDatabase());
}
return promiseChain
.then(() => deleteDatabase('fcm_vapid_details_db'))
.then(() => (vapidModel = null));
};
let vapidModel: VapidDetailsModel;
beforeEach(() => {
return cleanUp();
vapidModel = new VapidDetailsModel();
});
after(() => {
return cleanUp();
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, EXAMPLE_VAPID_KEY).then(
() => {
throw new Error('Expected promise to reject');
},
err => {
assert.equal('messaging/' + ERROR_CODES.BAD_SCOPE, err.code);
}
);
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);
}
);
});
});
@@ -66,7 +58,7 @@ describe('Firebase Messaging > VapidDetailsModel.saveVapidDetails()', () => {
const badInputs = ['', [], {}, true, null, 123];
badInputs.forEach(badInput => {
vapidModel = new VapidDetailsModel();
return vapidModel.saveVapidDetails(EXAMPLE_SCOPE, badInput).then(
return vapidModel.saveVapidDetails(EXAMPLE_SCOPE, badInput as any).then(
() => {
throw new Error('Expected promise to reject');
},