diff --git a/.gitignore b/.gitignore index d780a15..f6f6d07 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ -node_modules -lib -test/service-account-key.json -test/functions/integrify.js .nyc_output -coverage .vscode -firestore-debug.log +*-debug.log +coverage +lib +node_modules diff --git a/src/rules/replicateAttributes.ts b/src/rules/replicateAttributes.ts index e41f259..4fb7e99 100644 --- a/src/rules/replicateAttributes.ts +++ b/src/rules/replicateAttributes.ts @@ -68,6 +68,7 @@ export function integrifyReplicateAttributes( // Call "pre" hook if defined const promises = []; + // istanbul ignore else if (rule.hooks && rule.hooks.pre) { promises.push(rule.hooks.pre(change, context)); console.log(`integrify: Running pre-hook: ${rule.hooks.pre}`); diff --git a/test/functions/index.js b/test/functions/index.js index 8718136..033a6b8 100644 --- a/test/functions/index.js +++ b/test/functions/index.js @@ -1,5 +1,4 @@ const { integrify } = require('../../lib'); -const { setState } = require('./stateMachine'); const functions = require('firebase-functions'); const admin = require('firebase-admin'); @@ -34,8 +33,10 @@ module.exports.replicateMasterToDetail = integrify({ }, ], hooks: { - pre: (change, context) => { - setState({ change, context }); + pre: async (change, context) => { + await db.collection('prehooks').add({ + message: '[788a32e05504] REPLICATE_ATTRIBUTES prehook was called!', + }); }, }, }); @@ -57,8 +58,10 @@ module.exports.deleteReferencesToMaster = integrify({ }, ], hooks: { - pre: (snap, context) => { - setState({ snap, context }); + pre: async (snap, context) => { + await db.collection('prehooks').add({ + message: '[6a8f4f8f090c] DELETE_REFERENCES prehook was called!', + }); }, }, }); diff --git a/test/functions/integrify.rules.js b/test/functions/integrify.rules.js index 3d500ed..64b3bcd 100644 --- a/test/functions/integrify.rules.js +++ b/test/functions/integrify.rules.js @@ -3,11 +3,11 @@ module.exports = [ rule: 'REPLICATE_ATTRIBUTES', name: 'replicateMasterToDetailFromFile', source: { - collection: 'master', + collection: 'rules-file-master', }, targets: [ { - collection: 'detail1', + collection: 'rules-file-detail1', foreignKey: 'masterId', attributeMapping: { masterField1: 'detail1Field1', @@ -15,7 +15,7 @@ module.exports = [ }, }, { - collection: 'detail2', + collection: 'rules-file-detail2', foreignKey: 'masterId', attributeMapping: { masterField1: 'detail2Field1', @@ -29,15 +29,15 @@ module.exports = [ rule: 'DELETE_REFERENCES', name: 'deleteReferencesToMasterFromFile', source: { - collection: 'master', + collection: 'rules-file-master', }, targets: [ { - collection: 'detail1', + collection: 'rules-file-detail1', foreignKey: 'masterId', }, { - collection: 'detail2', + collection: 'rules-file-detail2', foreignKey: 'masterId', isCollectionGroup: true, }, @@ -47,11 +47,11 @@ module.exports = [ rule: 'MAINTAIN_COUNT', name: 'maintainFavoritesCountFromFile', source: { - collection: 'favorites', + collection: 'rules-file-favorites', foreignKey: 'articleId', }, target: { - collection: 'articles', + collection: 'rules-file-articles', attribute: 'favoritesCount', }, }, diff --git a/test/functions/stateMachine.js b/test/functions/stateMachine.js deleted file mode 100644 index 77bea28..0000000 --- a/test/functions/stateMachine.js +++ /dev/null @@ -1,4 +0,0 @@ -const state = {}; -const getState = () => state; -const setState = (newState) => Object.assign(state, newState); -module.exports = { getState, setState }; diff --git a/test/main.test.js b/test/main.test.js index f888631..f626d73 100644 --- a/test/main.test.js +++ b/test/main.test.js @@ -2,8 +2,12 @@ const assert = require('chai').assert; const { integrify } = require('../lib'); +const admin = require('firebase-admin'); +admin.initializeApp(); +const db = admin.firestore(); + describe('Error conditions', () => { - it('should error on bad rule', function () { + it('should error on bad rule', () => { assert.throws( () => integrify({ rule: 'BAD_RULE_ea8e3a2a2d3e' }), /Unknown rule/i @@ -11,14 +15,93 @@ describe('Error conditions', () => { assert.throws(() => require('./functions-bad-rules-file'), /Unknown rule/i); }); - it('should error on no rule or config', function () { + it('should error on no rule or config', () => { assert.throws(() => integrify(42), /Input must be rule or config/i); }); - it('should error on absent config file', function () { + it('should error on absent config file', () => { assert.throws( () => require('./functions-absent-rules-file'), /Rules file not found/i ); }); }); + +describe('REPLICATE_ATTRIBUTES', () => { + it('should replicate attributes', async () => { + // Create master document to replicate from + const masterRef = await db + .collection('master') + .add({ random: Math.random() }); + const masterId = masterRef.id; + + // Create couple of detail docs to replicate to + await db.collection('detail1').add({ masterId }); + await db.collection('detail2').add({ masterId }); + + // Update master doc + const masterField1 = randstr(); + const masterField2 = randstr(); + await masterRef.update({ masterField1, masterField2 }); + + // Ensure update is reflected in detail docs + await assertQuerySizeEventually( + db + .collection('detail1') + .where('masterId', '==', masterId) + .where('detail1Field1', '==', masterField1), + 1 + ); + await assertQuerySizeEventually( + db + .collection('detail2') + .where('masterId', '==', masterId) + .where('detail2Field1', '==', masterField1), + 1 + ); + + // Make an irrelevant update + await masterRef.set({ someOtherField: randstr() }); + + // Ensure prehook is called twice (once for each update) + await assertQuerySizeEventually( + db + .collection('prehooks') + .where( + 'message', + '==', + '[788a32e05504] REPLICATE_ATTRIBUTES prehook was called!' + ), + 2 + ); + }); +}); + +// Helper functions +function randstr() { + return Math.random().toString(36).substr(2); +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function assertQuerySizeEventually( + query, + expectedResultSize, + log = console.log +) { + log(`Asserting query result to have [${expectedResultSize}] entries ... `); + await sleep(1000); + const docs = await new Promise((res) => { + unsubscribe = query.onSnapshot((snap) => { + log(`Current result size: [${snap.size}]`); + if (snap.size === expectedResultSize) { + log('Matched!'); + unsubscribe(); + res(snap.docs); + } + }); + }); + return docs; +} diff --git a/test/run-tests.sh b/test/run-tests.sh index 8fb7550..9d68cfd 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -9,6 +9,8 @@ rm -rf ../.nyc_output ../coverage export GCLOUD_PROJECT=dummy-project export FIRESTORE_EMULATOR_HOST='localhost:8080' -npx nyc -r html -r text -r lcov firebase emulators:exec \ - 'mocha --bail --exit --jobs 1 --timeout 30s *.test.js' - +: ${MOCHA_TIMEOUT:=30000} +npx \ + nyc -r html -r text -r lcov \ + firebase emulators:exec --ui \ + "mocha --bail --exit --jobs 1 --timeout $MOCHA_TIMEOUT *.test.js"