voluptasmollitia
Version:
Monorepo for the Firebase JavaScript SDK
680 lines (632 loc) • 22 kB
text/typescript
/**
* @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();
});
});
});
});