UNPKG

quodolores

Version:

Monorepo for the Firebase JavaScript SDK

369 lines (311 loc) 11.1 kB
/** * @license * Copyright 2019 Google LLC * * 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, use } from 'chai'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import { FakeServiceWorker } from '../../../test/helpers/fake_service_worker'; import { testAuth, testUser } from '../../../test/helpers/mock_auth'; import { PersistenceInternal, PersistenceType } from '../../core/persistence'; import { SingletonInstantiator, _getInstance } from '../../core/util/instantiator'; import { _EventType, KeyChangedRequest, _TimeoutDuration } from '../messagechannel/index'; import { Receiver } from '../messagechannel/receiver'; import { Sender } from '../messagechannel/sender'; import * as workerUtil from '../util/worker'; import { _deleteObject, indexedDBLocalPersistence, _clearDatabase, _openDatabase, _POLLING_INTERVAL_MS, _putObject } from './indexed_db'; use(sinonChai); interface TestPersistence extends PersistenceInternal { _workerInitializationPromise: Promise<void>; } describe('platform_browser/persistence/indexed_db', () => { const persistence: PersistenceInternal = _getInstance( indexedDBLocalPersistence ); afterEach(sinon.restore); async function waitUntilPoll(clock: sinon.SinonFakeTimers): Promise<void> { clock.tick(_POLLING_INTERVAL_MS + 1); clock.restore(); // Wait a little for the poll operation to complete await new Promise(resolve => setTimeout(resolve, 100)); } it('should work with persistence type', async () => { const key = 'my-super-special-persistence-type'; const value = PersistenceType.LOCAL; expect(await persistence._get(key)).to.be.null; await persistence._set(key, value); expect(await persistence._get(key)).to.be.eq(value); expect(await persistence._get('other-key')).to.be.null; await persistence._remove(key); expect(await persistence._get(key)).to.be.null; }); it('should return blobified user value', async () => { const key = 'my-super-special-user'; const auth = await testAuth(); const value = testUser(auth, 'some-uid'); expect(await persistence._get(key)).to.be.null; await persistence._set(key, value.toJSON()); const out = await persistence._get(key); expect(out).to.eql(value.toJSON()); await persistence._remove(key); expect(await persistence._get(key)).to.be.null; }); describe('#isAvaliable', () => { it('should return true if db is available', async () => { expect(await persistence._isAvailable()).to.be.true; }); it('should return false if db creation errors', async () => { sinon.stub(indexedDB, 'open').returns({ addEventListener(evt: string, cb: () => void) { if (evt === 'error') { cb(); } }, error: new DOMException('yes there was an error') } as any); expect(await persistence._isAvailable()).to.be.false; }); }); describe('#addEventListener', () => { let clock: sinon.SinonFakeTimers; const key = 'my-key'; const newValue = 'new-value'; let callback: sinon.SinonSpy; let db: IDBDatabase; before(async () => { db = await _openDatabase(); }); beforeEach(async () => { clock = sinon.useFakeTimers(); callback = sinon.spy(); persistence._addListener(key, callback); }); afterEach(async () => { persistence._removeListener(key, callback); await _clearDatabase(db); clock.restore(); }); it('should not trigger a listener when there are no changes', async () => { await waitUntilPoll(clock); expect(callback).not.to.have.been.called; }); it('should trigger a listener when the key changes', async () => { await _putObject(db, key, newValue); await waitUntilPoll(clock); expect(callback).to.have.been.calledWith(newValue); }); it('should trigger the listener when the key is removed', async () => { await _putObject(db, key, newValue); await waitUntilPoll(clock); callback.resetHistory(); await _deleteObject(db, key); await waitUntilPoll(clock); expect(callback).to.have.been.calledOnceWith(null); }); it('should not trigger the listener when a different key changes', async () => { await _putObject(db, 'other-key', newValue); await waitUntilPoll(clock); expect(callback).not.to.have.been.called; }); it('should not trigger if a write is pending', async () => { await _putObject(db, key, newValue); (persistence as any)['pendingWrites'] = 1; await waitUntilPoll(clock); expect(callback).not.to.have.been.called; (persistence as any)['pendingWrites'] = 0; }); context('with multiple listeners', () => { let otherCallback: sinon.SinonSpy; beforeEach(() => { otherCallback = sinon.spy(); persistence._addListener(key, otherCallback); }); afterEach(() => { persistence._removeListener(key, otherCallback); }); it('should trigger both listeners if multiple listeners are registered', async () => { await _putObject(db, key, newValue); await waitUntilPoll(clock); expect(callback).to.have.been.calledWith(newValue); expect(otherCallback).to.have.been.calledWith(newValue); }); }); }); context('service worker integration', () => { let serviceWorker: ServiceWorker; let persistence: TestPersistence; beforeEach(() => { serviceWorker = (new FakeServiceWorker() as unknown) as ServiceWorker; }); afterEach(() => { sinon.restore(); }); context('as a service worker', () => { let sender: Sender; let db: IDBDatabase; beforeEach(async () => { sender = new Sender(serviceWorker); sinon.stub(workerUtil, '_isWorker').returns(true); sinon.stub(workerUtil, '_getWorkerGlobalScope').returns(serviceWorker); persistence = new ((indexedDBLocalPersistence as unknown) as SingletonInstantiator<TestPersistence>)(); db = await _openDatabase(); }); it('should respond to pings', async () => { await persistence._workerInitializationPromise; const response = await sender._send( _EventType.PING, {}, _TimeoutDuration.ACK ); expect(response).to.have.deep.members([ { fulfilled: true, value: [_EventType.KEY_CHANGED] } ]); }); it('should let us know if the key didnt actually change on a key changed event', async () => { await persistence._workerInitializationPromise; const response = await sender._send( _EventType.KEY_CHANGED, { key: 'foo' }, _TimeoutDuration.LONG_ACK ); expect(response).to.have.deep.members([ { fulfilled: true, value: { keyProcessed: false } } ]); }); it('should refresh on key changed events when a key has changed', async () => { await persistence._workerInitializationPromise; await _putObject(db, 'foo', 'bar'); const response = await sender._send( _EventType.KEY_CHANGED, { key: 'foo' }, _TimeoutDuration.LONG_ACK ); expect(response).to.have.deep.members([ { fulfilled: true, value: { keyProcessed: true } } ]); }); }); context('as a service worker controller', () => { let receiver: Receiver; beforeEach(() => { receiver = Receiver._getInstance(serviceWorker); sinon.stub(workerUtil, '_isWorker').returns(false); sinon .stub(workerUtil, '_getActiveServiceWorker') .returns(Promise.resolve(serviceWorker)); sinon .stub(workerUtil, '_getServiceWorkerController') .returns(serviceWorker); persistence = new ((indexedDBLocalPersistence as unknown) as SingletonInstantiator<TestPersistence>)(); }); it('should send a ping on init', async () => { return new Promise(resolve => { receiver._subscribe(_EventType.PING, () => { resolve(); return [_EventType.KEY_CHANGED]; }); return persistence._workerInitializationPromise; }); }); it('should send a key changed event when a key is set', async () => { return new Promise(async resolve => { await persistence._workerInitializationPromise; receiver._subscribe( _EventType.KEY_CHANGED, (_origin: string, data: KeyChangedRequest) => { expect(data.key).to.eq('foo'); resolve(); return { keyProcessed: true }; } ); return persistence._set('foo', 'bar'); }); }); it('should send a key changed event when a key is removed', async () => { return new Promise(async resolve => { receiver._subscribe( _EventType.KEY_CHANGED, async (_origin: string, data: KeyChangedRequest) => { expect(data.key).to.eq('foo'); const persistedValue = await persistence._get('foo'); if (!persistedValue) { resolve(); } return { keyProcessed: true }; } ); await persistence._workerInitializationPromise; await persistence._set('foo', 'bar'); return persistence._remove('foo'); }); }); }); }); describe('closed IndexedDB connection', () => { it('should retry by reopening the connection', async () => { const closeDb = async (): Promise<void> => { const db = await ((persistence as unknown) as { _openDb(): Promise<IDBDatabase>; })._openDb(); db.close(); }; const key = 'my-super-special-persistence-type'; const value = PersistenceType.LOCAL; expect(await persistence._get(key)).to.be.null; await closeDb(); await persistence._set(key, value); await closeDb(); expect(await persistence._get(key)).to.be.eq(value); await closeDb(); await persistence._remove(key); expect(await persistence._get(key)).to.be.null; }); }); });