Skip to content

Commit

Permalink
feat(afs): auditTrail() and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
davideast committed Sep 25, 2017
1 parent 86e89a3 commit 90c8ede
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 79 deletions.
26 changes: 17 additions & 9 deletions src/firestore/collection/changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { fromCollectionRef } from '../observable/fromRef';
import { Query, DocumentChangeType, DocumentChange, DocumentSnapshot, QuerySnapshot } from 'firestore';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/map';
import 'rxjs/add/observable/filter';
import 'rxjs/add/operator/scan';

import { DocumentChangeAction, Action } from '../interfaces';
Expand All @@ -26,7 +27,8 @@ export function sortedChanges(query: Query, events: DocumentChangeType[]): Obser
return fromCollectionRef(query)
.map(changes => changes.payload.docChanges)
.scan((current, changes) => combineChanges(current, changes, events), [])
.map(changes => changes.map(c => ({ type: c.type, payload: c })));
.map(changes => changes.map(c => ({ type: c.type, payload: c })))
.filter(changes => changes.length > 0);
}

/**
Expand All @@ -37,14 +39,13 @@ export function sortedChanges(query: Query, events: DocumentChangeType[]): Obser
* @param events
*/
export function combineChanges(current: DocumentChange[], changes: DocumentChange[], events: DocumentChangeType[]) {
let combined: DocumentChange[] = [];
changes.forEach(change => {
// skip unwanted change types
if(events.indexOf(change.type) > -1) {
combined = combineChange(combined, change);
current = combineChange(current, change);
}
});
return combined;
return current;
}

/**
Expand All @@ -55,12 +56,19 @@ export function combineChanges(current: DocumentChange[], changes: DocumentChang
export function combineChange(combined: DocumentChange[], change: DocumentChange): DocumentChange[] {
switch(change.type) {
case 'added':
return [...combined, change];
case 'modified':
return combined.map(x => x.doc.id === change.doc.id ? change : x);
combined.splice(change.newIndex, 0, change);
break;
case 'modified':
// When an item changes position we first remove it
// and then add it's new position
if(change.oldIndex !== change.newIndex) {
combined.splice(change.oldIndex, 1);
}
combined.splice(change.newIndex, 0, change);
break;
case 'removed':
return combined.filter(x => x.doc.id !== change.doc.id);
combined.splice(change.oldIndex, 1);
break;
}
return combined;
}

258 changes: 193 additions & 65 deletions src/firestore/collection/collection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AngularFirestore } from '../firestore';
import { AngularFirestoreModule } from '../firestore.module';
import { AngularFirestoreDocument } from '../document/document';
import { AngularFirestoreCollection } from './collection';
import { QueryFn } from '../interfaces';

import * as firebase from 'firebase/app';
import * as firestore from 'firestore';
Expand All @@ -16,6 +17,15 @@ import { COMMON_CONFIG } from '../test-config';

import { Stock, randomName, FAKE_STOCK_DATA, createRandomStocks, delayAdd, delayDelete, delayUpdate, deleteThemAll } from '../utils.spec';

async function collectionHarness(afs: AngularFirestore, items: number, queryFn?: QueryFn) {
const randomCollectionName = randomName(afs.firestore);
const ref = afs.firestore.collection(`${randomCollectionName}`);
if(!queryFn) { queryFn = (ref) => ref; }
const stocks = new AngularFirestoreCollection<Stock>(ref, queryFn(ref));
let names = await createRandomStocks(afs.firestore, ref, items);
return { randomCollectionName, ref, stocks, names };
}

describe('AngularFirestoreCollection', () => {
let app: firebase.app.App;
let afs: AngularFirestore;
Expand All @@ -39,76 +49,168 @@ describe('AngularFirestoreCollection', () => {
done();
});

it('should get unwrapped snapshot', async (done: any) => {
const randomCollectionName = randomName(afs.firestore);
const ref = afs.firestore.collection(`${randomCollectionName}`);
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
const ITEMS = 4;

const names = await createRandomStocks(afs.firestore, ref, ITEMS)

const sub = stocks.valueChanges().subscribe(data => {
// unsub immediately as we will be deleting data at the bottom
// and that will trigger another subscribe callback and fail
// the test
sub.unsubscribe();
// We added four things. This should be four.
// This could not be four if the batch failed or
// if the collection state is altered during a test run
expect(data.length).toEqual(ITEMS);
data.forEach(stock => {
// We used the same piece of data so they should all equal
expect(stock).toEqual(FAKE_STOCK_DATA);
describe('valueChanges()', () => {

it('should get unwrapped snapshot', async (done: any) => {
const ITEMS = 4;
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);

const sub = stocks.valueChanges().subscribe(data => {
// unsub immediately as we will be deleting data at the bottom
// and that will trigger another subscribe callback and fail
// the test
sub.unsubscribe();
// We added four things. This should be four.
// This could not be four if the batch failed or
// if the collection state is altered during a test run
expect(data.length).toEqual(ITEMS);
data.forEach(stock => {
// We used the same piece of data so they should all equal
expect(stock).toEqual(FAKE_STOCK_DATA);
});
// Delete them all
const promises = names.map(name => ref.doc(name).delete());
Promise.all(promises).then(done).catch(fail);
});
// Delete them all
const promises = names.map(name => ref.doc(name).delete());
Promise.all(promises).then(done).catch(fail);

});

});

it('should get stateChanges() updates', async (done: any) => {
const randomCollectionName = randomName(afs.firestore);
const ref = afs.firestore.collection(`${randomCollectionName}`);
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
const ITEMS = 10;

const names = await createRandomStocks(afs.firestore, ref, ITEMS);

const sub = stocks.stateChanges().subscribe(data => {
// unsub immediately as we will be deleting data at the bottom
// and that will trigger another subscribe callback and fail
// the test
sub.unsubscribe();
// We added ten things. This should be ten.
// This could not be ten if the batch failed or
// if the collection state is altered during a test run
expect(data.length).toEqual(ITEMS);
data.forEach(action => {
// We used the same piece of data so they should all equal
expect(action.payload.doc.data()).toEqual(FAKE_STOCK_DATA);
describe('snapshotChanges()', () => {

it('should listen to all snapshotChanges() by default', async (done) => {
const ITEMS = 10;
let count = 0;
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
const sub = stocks.snapshotChanges().subscribe(data => {
count = count + 1;
// the first time should all be 'added'
if(count === 1) {
// make an update
stocks.doc(names[0]).update({ price: 2});
}
// on the second round, make sure the array is still the same
// length but the updated item is now modified
if(count === 2) {
expect(data.length).toEqual(ITEMS);
const change = data.filter(x => x.payload.doc.id === names[0])[0];
expect(change.type).toEqual('modified');
sub.unsubscribe();
deleteThemAll(names, ref).then(done).catch(done.fail);
}
});
deleteThemAll(names, ref).then(done).catch(done.fail);
});

});

fdescribe('snapshotChanges()', () => {
it('should update order on queries', async (done) => {
const ITEMS = 10;
let count = 0;
let firstIndex = 0;
const { randomCollectionName, ref, stocks, names } =
await collectionHarness(afs, ITEMS, ref => ref.orderBy('price', 'desc'));
const sub = stocks.snapshotChanges().subscribe(data => {
debugger;
count = count + 1;
// the first time should all be 'added'
if(count === 1) {
// make an update
firstIndex = data.filter(d => d.payload.doc.id === names[0])[0].payload.newIndex;
stocks.doc(names[0]).update({ price: 2 });
}
// on the second round, make sure the array is still the same
// length but the updated item is now modified
if(count === 2) {
expect(data.length).toEqual(ITEMS);
const change = data.filter(x => x.payload.doc.id === names[0])[0];
expect(change.type).toEqual('modified');
expect(change.payload.oldIndex).toEqual(firstIndex);
sub.unsubscribe();
deleteThemAll(names, ref).then(done).catch(done.fail);
}
});
});

it('should be able to filter snapshotChanges() types - modified', async (done) => {
const ITEMS = 10;
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);

const sub = stocks.snapshotChanges(['modified']).subscribe(data => {
sub.unsubscribe();
const change = data.filter(x => x.payload.doc.id === names[0])[0];
expect(data.length).toEqual(1);
expect(change.payload.doc.data().price).toEqual(2);
expect(change.type).toEqual('modified');
deleteThemAll(names, ref).then(done).catch(done.fail);
});

delayUpdate(stocks, names[0], { price: 2 });
});

it('should listen to all snapshotChanges() by default', async (done) => {
it('should be able to filter snapshotChanges() types - added', async (done) => {
const ITEMS = 10;
let { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
const nextId = ref.doc('a').id;

const sub = stocks.snapshotChanges(['added']).skip(1).subscribe(data => {
sub.unsubscribe();
const change = data.filter(x => x.payload.doc.id === nextId)[0];
expect(data.length).toEqual(ITEMS + 1);
expect(change.payload.doc.data().price).toEqual(2);
expect(change.type).toEqual('added');
deleteThemAll(names, ref).then(done).catch(done.fail);
done();
});


names = names.concat([nextId]);
delayAdd(stocks, nextId, { price: 2 });
});

it('should be able to filter snapshotChanges() types - removed', async (done) => {
const ITEMS = 10;
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);

const sub = stocks.snapshotChanges(['added', 'removed']).skip(1).subscribe(data => {
sub.unsubscribe();
const change = data.filter(x => x.payload.doc.id === names[0]);
expect(data.length).toEqual(ITEMS - 1);
expect(change.length).toEqual(0);
deleteThemAll(names, ref).then(done).catch(done.fail);
done();
});

delayDelete(stocks, names[0], 400);
});

});

describe('stateChanges()', () => {

it('should get stateChanges() updates', async (done: any) => {
const ITEMS = 10;
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);

const sub = stocks.stateChanges().subscribe(data => {
// unsub immediately as we will be deleting data at the bottom
// and that will trigger another subscribe callback and fail
// the test
sub.unsubscribe();
// We added ten things. This should be ten.
// This could not be ten if the batch failed or
// if the collection state is altered during a test run
expect(data.length).toEqual(ITEMS);
data.forEach(action => {
// We used the same piece of data so they should all equal
expect(action.payload.doc.data()).toEqual(FAKE_STOCK_DATA);
});
deleteThemAll(names, ref).then(done).catch(done.fail);
});

});

it('should listen to all stateChanges() by default', async (done) => {
const randomCollectionName = randomName(afs.firestore);
const ref = afs.firestore.collection(`${randomCollectionName}`);
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
const ITEMS = 10;
let count = 0;
const names = await createRandomStocks(afs.firestore, ref, ITEMS);
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
const sub = stocks.stateChanges().subscribe(data => {
count = count + 1;
if(count === 1) {
Expand All @@ -123,12 +225,9 @@ describe('AngularFirestoreCollection', () => {
});

it('should be able to filter stateChanges() types - modified', async (done) => {
const randomCollectionName = randomName(afs.firestore);
const ref = afs.firestore.collection(`${randomCollectionName}`);
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
const ITEMS = 10;
let count = 0;
const names = await createRandomStocks(afs.firestore, ref, ITEMS);
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);

const sub = stocks.stateChanges(['modified']).subscribe(data => {
sub.unsubscribe();
Expand All @@ -143,12 +242,9 @@ describe('AngularFirestoreCollection', () => {
});

it('should be able to filter stateChanges() types - added', async (done) => {
const randomCollectionName = randomName(afs.firestore);
const ref = afs.firestore.collection(`${randomCollectionName}`);
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
const ITEMS = 10;
let count = 0;
let names = await createRandomStocks(afs.firestore, ref, ITEMS);
let { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);

const sub = stocks.stateChanges(['added']).skip(1).subscribe(data => {
sub.unsubscribe();
Expand All @@ -165,11 +261,8 @@ describe('AngularFirestoreCollection', () => {
});

it('should be able to filter stateChanges() types - removed', async (done) => {
const randomCollectionName = randomName(afs.firestore);
const ref = afs.firestore.collection(`${randomCollectionName}`);
const stocks = new AngularFirestoreCollection<Stock>(ref, ref);
const ITEMS = 10;
let names = await createRandomStocks(afs.firestore, ref, ITEMS);
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);

const sub = stocks.stateChanges(['removed']).subscribe(data => {
sub.unsubscribe();
Expand All @@ -183,4 +276,39 @@ describe('AngularFirestoreCollection', () => {
});
});

describe('auditTrail()', () => {
it('should listen to all events for auditTrail() by default', async (done) => {
const ITEMS = 10;
let count = 0;
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);
const sub = stocks.auditTrail().subscribe(data => {
count = count + 1;
if(count === 1) {
stocks.doc(names[0]).update({ price: 2});
}
if(count === 2) {
sub.unsubscribe();
expect(data.length).toEqual(ITEMS + 1);
expect(data[data.length - 1].type).toEqual('modified');
deleteThemAll(names, ref).then(done).catch(done.fail);
}
});
});

it('should be able to filter auditTrail() types - removed', async (done) => {
const ITEMS = 10;
const { randomCollectionName, ref, stocks, names } = await collectionHarness(afs, ITEMS);

const sub = stocks.auditTrail(['removed']).subscribe(data => {
sub.unsubscribe();
expect(data.length).toEqual(1);
expect(data[0].type).toEqual('removed');
deleteThemAll(names, ref).then(done).catch(done.fail);
done();
});

delayDelete(stocks, names[0], 400);
});
});

});
Loading

0 comments on commit 90c8ede

Please sign in to comment.