UNPKG

voluptasmollitia

Version:
680 lines (632 loc) 22 kB
/** * @license * Copyright 2018 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. */ /* eslint-disable @typescript-eslint/no-floating-promises */ import { expect } from 'chai'; // app/database is used as namespaces to access types // eslint-disable-next-line @typescript-eslint/no-unused-vars import firebase from 'firebase'; import { fromRef } from '../database/fromRef'; import { list, ListenEvent, objectVal, listVal, QueryChange } from '../database'; import { take, skip, switchMap } from 'rxjs/operators'; import { BehaviorSubject, Observable } from 'rxjs'; import { auditTrail } from '../database/list/audit-trail'; // eslint-disable-next-line @typescript-eslint/no-require-imports export const TEST_PROJECT = require('../../../config/project.json'); const rando = (): string => Math.random().toString(36).substring(5); const batch = ( items: Array<{ name: string; key: string }> ): Readonly<{ [key: string]: unknown }> => { const batch: { [key: string]: unknown } = {}; items.forEach(item => { batch[item.key] = item; }); // make batch immutable to preserve integrity return Object.freeze(batch); }; describe('RxFire Database', () => { let app: firebase.app.App; let database: firebase.database.Database; const ref = (path: string): firebase.database.Reference => { app!.database().goOffline(); return app!.database().ref(path); }; function prepareList( opts: { events?: ListenEvent[]; skipnumber: number } = { skipnumber: 0 } ): { snapChanges: Observable<QueryChange[]>; ref: firebase.database.Reference; } { const { events, skipnumber } = opts; const aref = ref(rando()); const snapChanges = list(aref, events); return { snapChanges: snapChanges.pipe(skip(skipnumber)), ref: aref }; } /** * Each test runs inside it's own app instance and the app * is deleted after the test runs. * * Database tests run "offline" to reduce "flakeyness". * * Each test is responsible for seeding and removing data. Helper * functions are useful if the process becomes brittle or tedious. * Note that removing is less necessary since the tests are run * offline. * * Note: Database tests do not run exactly the same offline as * they do online. Querying can act differently, tests must * account for this. */ beforeEach(() => { app = firebase.initializeApp({ apiKey: TEST_PROJECT.apiKey, projectId: TEST_PROJECT.projectId, databaseURL: TEST_PROJECT.databaseURL }); database = app.database(); database.goOffline(); }); afterEach((done: MochaDone) => { app.delete().then(() => done()); }); describe('fromRef', () => { const items = [ { name: 'one' }, { name: 'two' }, { name: 'three' } ].map(item => ({ key: rando(), ...item })); const itemsObj = batch(items); /** * This test checks that "non-existent" or null value references are * handled. */ it('it should should handle non-existence', done => { const itemRef = ref(rando()); itemRef.set({}); const obs = fromRef(itemRef, ListenEvent.value); obs .pipe(take(1)) .subscribe(change => { expect(change.snapshot.exists()).to.equal(false); expect(change.snapshot.val()).to.equal(null); }) .add(done); }); /** * This test checks that the Observable unsubscribe mechanism works. * * Calling unsubscribe should trigger the ref.off() method. */ it('it should listen and then unsubscribe', done => { const itemRef = ref(rando()); itemRef.set(itemsObj); const obs = fromRef(itemRef, ListenEvent.value); let count = 0; const sub = obs.subscribe(_ => { count = count + 1; // hard coding count to one will fail if the unsub // doesn't actually unsub expect(count).to.equal(1); done(); sub.unsubscribe(); itemRef.push({ name: 'anotha one' }); }); }); describe('events', () => { /** * This test provides the `child_added` event and tests that only * `child_added` events are received. */ it('should stream back a child_added event', (done: any) => { const itemRef = ref(rando()); const data = itemsObj; itemRef.set(data); const obs = fromRef(itemRef, ListenEvent.added); let count = 0; const sub = obs.subscribe(change => { count = count + 1; const { event, snapshot } = change; expect(event).to.equal(ListenEvent.added); expect(snapshot.val()).to.eql(data[snapshot.key!]); if (count === items.length) { done(); sub.unsubscribe(); expect(sub.closed).to.equal(true); } }); }); /** * This test provides the `child_changed` event and tests that only * `child_changed` events are received. */ it('should stream back a child_changed event', (done: any) => { const itemRef = ref(rando()); itemRef.set(itemsObj); const obs = fromRef(itemRef, ListenEvent.changed); const name = 'look at what you made me do'; const key = items[0].key; const sub = obs.subscribe(change => { const { event, snapshot } = change; expect(event).to.equal(ListenEvent.changed); expect(snapshot.key).to.equal(key); expect(snapshot.val()).to.eql({ key, name }); sub.unsubscribe(); done(); }); itemRef.child(key).update({ name }); }); /** * This test provides the `child_removed` event and tests that only * `child_removed` events are received. */ it('should stream back a child_removed event', (done: any) => { const itemRef = ref(rando()); itemRef.set(itemsObj); const obs = fromRef(itemRef, ListenEvent.removed); const key = items[0].key; const name = items[0].name; const sub = obs.subscribe(change => { const { event, snapshot } = change; expect(event).to.equal(ListenEvent.removed); expect(snapshot.key).to.equal(key); expect(snapshot.val()).to.eql({ key, name }); sub.unsubscribe(); done(); }); itemRef.child(key).remove(); }); /** * This test provides the `child_moved` event and tests that only * `child_moved` events are received. */ it('should stream back a child_moved event', (done: any) => { const itemRef = ref(rando()); itemRef.set(itemsObj); const obs = fromRef(itemRef, ListenEvent.moved); const key = items[2].key; const name = items[2].name; const sub = obs.subscribe(change => { const { event, snapshot } = change; expect(event).to.equal(ListenEvent.moved); expect(snapshot.key).to.equal(key); expect(snapshot.val()).to.eql({ key, name }); sub.unsubscribe(); done(); }); itemRef.child(key).setPriority(-100, () => {}); }); /** * This test provides the `value` event and tests that only * `value` events are received. */ it('should stream back a value event', (done: any) => { const itemRef = ref(rando()); const data = itemsObj; itemRef.set(data); const obs = fromRef(itemRef, ListenEvent.value); const sub = obs.subscribe(change => { const { event, snapshot } = change; expect(event).to.equal(ListenEvent.value); expect(snapshot.val()).to.eql(data); done(); sub.unsubscribe(); expect(sub.closed).to.equal(true); }); }); /** * This test provides queries a reference and checks that the queried * values are streamed back. */ it('should stream back query results', (done: any) => { const itemRef = ref(rando()); itemRef.set(itemsObj); const query = itemRef.orderByChild('name').equalTo(items[0].name); const obs = fromRef(query, ListenEvent.value); obs.subscribe(change => { let child; change.snapshot.forEach(snap => { child = snap.val(); return true; }); expect(child).to.eql(items[0]); done(); }); }); }); }); describe('list', () => { const items = [ { name: 'zero' }, { name: 'one' }, { name: 'two' } ].map((item, i) => ({ key: `${i}`, ...item })); const itemsObj = batch(items); describe('events', () => { /** * `value` events are provided first when subscribing to a list. We need * to know what the "intial" data list is, so a value event is used. */ it('should stream value at first', done => { const someRef = ref(rando()); const obs = list(someRef, [ListenEvent.added]); obs .pipe(take(1)) .subscribe(changes => { const data = changes.map(change => change.snapshot.val()); expect(data).to.eql(items); }) .add(done); someRef.set(itemsObj); }); /** * This test checks that `child_added` events are only triggered when * specified in the events array. * * The first result is skipped because it is always `value`. A `take(1)` * is used to close the stream after the `child_added` event occurs. */ it('should process a new child_added event', done => { const aref = ref(rando()); const obs = list(aref, [ListenEvent.added]); obs .pipe(skip(1), take(1)) .subscribe(changes => { const data = changes.map(change => change.snapshot.val()); expect(data[3]).to.eql({ name: 'anotha one' }); }) .add(done); aref.set(itemsObj); aref.push({ name: 'anotha one' }); }); /** * This test checks that events are emitted in proper order. The reference * is queried and the test ensures that the array is in proper order. */ it('should stream in order events', done => { const aref = ref(rando()); const obs = list(aref.orderByChild('name'), [ListenEvent.added]); obs .pipe(take(1)) .subscribe(changes => { const names = changes.map(change => change.snapshot.val().name); expect(names[0]).to.eql('one'); expect(names[1]).to.eql('two'); expect(names[2]).to.eql('zero'); }) .add(done); aref.set(itemsObj); }); /** * This test checks that the array is in order with child_added specified. * A new record is added that appears on top of the query and the test * skips the first value event and checks that the newly added item is * on top. */ it('should stream in order events w/child_added', done => { const aref = ref(rando()); const obs = list(aref.orderByChild('name'), [ListenEvent.added]); obs .pipe(skip(1), take(1)) .subscribe(changes => { const names = changes.map(change => change.snapshot.val().name); expect(names[0]).to.eql('anotha one'); expect(names[1]).to.eql('one'); expect(names[2]).to.eql('two'); expect(names[3]).to.eql('zero'); }) .add(done); aref.set(itemsObj); aref.push({ name: 'anotha one' }); }); /** * This test checks that a filtered reference still emits the proper events. */ it('should stream events filtering', done => { const aref = ref(rando()); const obs = list(aref.orderByChild('name').equalTo('zero'), [ ListenEvent.added ]); obs .pipe(skip(1), take(1)) .subscribe(changes => { const names = changes.map(change => change.snapshot.val().name); expect(names[0]).to.eql('zero'); expect(names[1]).to.eql('zero'); }) .add(done); aref.set(itemsObj); aref.push({ name: 'zero' }); }); /** * This test checks that the a `child_removed` event is processed in the * array by testing that the new length is shorter than the original * length. */ it('should process a new child_removed event', done => { const aref = ref(rando()); const obs = list(aref, [ListenEvent.added, ListenEvent.removed]); const _sub = obs .pipe(skip(1), take(1)) .subscribe(changes => { const data = changes.map(change => change.snapshot.val()); expect(data.length).to.eql(items.length - 1); }) .add(done); app.database().goOnline(); aref.set(itemsObj).then(() => { aref.child(items[0].key).remove(); }); }); /** * This test checks that the `child_changed` event is processed by * checking the new value of the object in the array. */ it('should process a new child_changed event', done => { const aref = ref(rando()); const obs = list(aref, [ListenEvent.added, ListenEvent.changed]); const _sub = obs .pipe(skip(1), take(1)) .subscribe(changes => { const data = changes.map(change => change.snapshot.val()); expect(data[1].name).to.eql('lol'); }) .add(done); app.database().goOnline(); aref.set(itemsObj).then(() => { aref.child(items[1].key).update({ name: 'lol' }); }); }); /** * This test checks the `child_moved` event is processed by checking that * the new position is properly updated. */ it('should process a new child_moved event', done => { const aref = ref(rando()); const obs = list(aref, [ListenEvent.added, ListenEvent.moved]); const _sub = obs .pipe(skip(1), take(1)) .subscribe(changes => { const data = changes.map(change => change.snapshot.val()); // We moved the first item to the last item, so we check that // the new result is now the last result expect(data[data.length - 1]).to.eql(items[0]); }) .add(done); app.database().goOnline(); aref.set(itemsObj).then(() => { aref.child(items[0].key).setPriority('a', () => {}); }); }); /** * If no events array is provided in `list()` all events are listened to. * * This test checks that all events are processed without providing the * array. */ it('should listen to all events by default', done => { const { snapChanges, ref } = prepareList(); snapChanges .pipe(take(1)) .subscribe(actions => { const data = actions.map(a => a.snapshot.val()); expect(data).to.eql(items); }) .add(done); ref.set(itemsObj); }); /** * This test checks that multiple subscriptions work properly. */ it('should handle multiple subscriptions (hot)', done => { const { snapChanges, ref } = prepareList(); const sub = snapChanges.subscribe(() => {}).add(done); snapChanges .pipe(take(1)) .subscribe(actions => { const data = actions.map(a => a.snapshot.val()); expect(data).to.eql(items); }) .add(sub); ref.set(itemsObj); }); /** * This test checks that multiple subscriptions work properly. */ it('should handle multiple subscriptions (warm)', done => { const { snapChanges, ref } = prepareList(); snapChanges .pipe(take(1)) .subscribe(() => {}) .add(() => { snapChanges .pipe(take(1)) .subscribe(actions => { const data = actions.map(a => a.snapshot.val()); expect(data).to.eql(items); }) .add(done); }); ref.set(itemsObj); }); /** * This test checks that only `child_added` events are processed. */ it('should listen to only child_added events', done => { const { snapChanges, ref } = prepareList({ events: [ListenEvent.added], skipnumber: 0 }); snapChanges .pipe(take(1)) .subscribe(actions => { const data = actions.map(a => a.snapshot.val()); expect(data).to.eql(items); }) .add(done); ref.set(itemsObj); }); /** * This test checks that only `child_added` and `child_changed` events are * processed. */ it('should listen to only child_added, child_changed events', done => { const { snapChanges, ref } = prepareList({ events: [ListenEvent.added, ListenEvent.changed], skipnumber: 1 }); const name = 'ligatures'; snapChanges .pipe(take(1)) .subscribe(actions => { const data = actions.map(a => a.snapshot.val()); const copy = [...items]; copy[0].name = name; expect(data).to.eql(copy); }) .add(done); app.database().goOnline(); ref.set(itemsObj).then(() => { ref.child(items[0].key).update({ name }); }); }); /** * This test checks that empty sets are processed. */ it('should handle empty sets', done => { const aref = ref(rando()); aref.set({}); list(aref) .pipe(take(1)) .subscribe(data => { expect(data.length).to.eql(0); }) .add(done); }); /** * This test checks that dynamic querying works even with results that * are empty. */ it('should handle dynamic queries that return empty sets', done => { let count = 0; const namefilter$ = new BehaviorSubject<number | null>(null); const aref = ref(rando()); aref.set(itemsObj); namefilter$ .pipe( switchMap(name => { const filteredRef = name ? aref.child('name').equalTo(name) : aref; return list(filteredRef); }), take(2) ) .subscribe(data => { count = count + 1; // the first time should all be 'added' if (count === 1) { expect(Object.keys(data).length).to.eql(3); namefilter$.next(-1); } // on the second round, we should have filtered out everything if (count === 2) { expect(Object.keys(data).length).to.eql(0); } }) .add(done); }); }); }); describe('auditTrail', () => { const items = [ { name: 'zero' }, { name: 'one' }, { name: 'two' } ].map((item, i) => ({ key: `${i}`, ...item })); const itemsObj = batch(items); function prepareAuditTrail( opts: { events?: ListenEvent[]; skipnumber: number } = { skipnumber: 0 } ): { changes: Observable<QueryChange[]>; ref: firebase.database.Reference; } { const { events, skipnumber } = opts; const aref = ref(rando()); aref.set(itemsObj); const changes = auditTrail(aref, events); return { changes: changes.pipe(skip(skipnumber)), ref: aref }; } /** * This test checks that auditTrail retuns all events by default. */ it('should listen to all events by default', done => { const { changes } = prepareAuditTrail(); changes.subscribe(actions => { const data = actions.map(a => a.snapshot.val()); expect(data).to.eql(items); done(); }); }); }); describe('Data Mapping Functions', () => { const items = [ { name: 'one' }, { name: 'two' }, { name: 'three' } ].map(item => ({ key: rando(), ...item })); const itemsObj = batch(items); /** * The `listVal` function should map a query to an array of objects */ it('listVal should map a query to an array of objects', (done: MochaDone) => { const itemRef = ref(rando()); const data = { testKey: { hello: 'world' } }; itemRef.set(data); const obs = listVal<any>(itemRef, 'KEY').pipe(take(1)); obs.subscribe(val => { expect(val).to.be.instanceOf(Array); expect(val[0].KEY).to.equal('testKey'); expect(val[0].hello).to.equal('world'); done(); }); }); /** * The `objectVal` function should map a query to its object val */ it('objectVal should map a reference or query to its value', (done: MochaDone) => { const itemRef = ref(rando()); itemRef.set(itemsObj); const obs = objectVal(itemRef).pipe(take(1)); obs.subscribe(val => { expect(val).to.be.instanceOf(Object); expect(val).to.deep.equal(itemsObj); done(); }); }); }); });