Skip to content

Commit 49bb43e

Browse files
authored
Add testing support for 2nd gen firestore triggers (#200)
* firestore onDocumentCreated trigger * on doc updated, written, deleted & unit tests * on doc written unit tests * don't overwrite user's mocked before/after data if they didn't mock the other
1 parent 8775696 commit 49bb43e

15 files changed

+1236
-261
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add testing support for 2nd gen firestore triggers (#200).

package-lock.json

+649-223
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"@types/mocha": "^5.2.7",
4848
"chai": "^4.2.0",
4949
"firebase-admin": "^10.1.0",
50-
"firebase-functions": "^4.0.0-rc.0",
50+
"firebase-functions": ">=4.3.0",
5151
"firebase-tools": "^8.9.2",
5252
"mocha": "^6.2.2",
5353
"prettier": "^1.19.1",
@@ -57,7 +57,7 @@
5757
},
5858
"peerDependencies": {
5959
"firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
60-
"firebase-functions": "^4.0.0",
60+
"firebase-functions": ">=4.3.0",
6161
"jest": ">=28.0.0"
6262
},
6363
"engines": {

spec/v2.spec.ts

+289-17
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@ import {
3434
testLab,
3535
eventarc,
3636
https,
37+
firestore,
3738
} from 'firebase-functions/v2';
3839
import { defineString } from 'firebase-functions/params';
3940
import { makeDataSnapshot } from '../src/providers/database';
41+
import { makeDocumentSnapshot } from '../src/providers/firestore';
42+
import { inspect } from 'util';
4043

4144
describe('v2', () => {
4245
describe('#wrapV2', () => {
@@ -460,6 +463,275 @@ describe('v2', () => {
460463
});
461464
});
462465

466+
describe('firestore', () => {
467+
describe('document path', () => {
468+
it('should resolve default document path', () => {
469+
const cloudFn = firestore.onDocumentCreated('foo/bar/baz', handler);
470+
const cloudFnWrap = wrapV2(cloudFn);
471+
const cloudEvent = cloudFnWrap().cloudEvent;
472+
473+
expect(cloudEvent.document).equal('foo/bar/baz');
474+
});
475+
476+
it('should resolve default document given StringParam', () => {
477+
process.env.doc_path = 'foo/StringParam/baz';
478+
const cloudFn = firestore.onDocumentCreated('', handler);
479+
cloudFn.__endpoint.eventTrigger.eventFilterPathPatterns.document = defineString(
480+
'doc_path'
481+
);
482+
const cloudFnWrap = wrapV2(cloudFn);
483+
const cloudEvent = cloudFnWrap().cloudEvent;
484+
485+
expect(cloudEvent.document).equal('foo/StringParam/baz');
486+
});
487+
488+
it('should resolve using params', () => {
489+
const cloudFn = firestore.onDocumentCreated('users/{user}', handler);
490+
const cloudFnWrap = wrapV2(cloudFn);
491+
const partial = {
492+
params: {
493+
user: '123',
494+
},
495+
};
496+
const cloudEvent = cloudFnWrap(partial).cloudEvent;
497+
498+
expect(cloudEvent.document).equal('users/123');
499+
});
500+
501+
it('should resolve with undefined string if variable is missing', () => {
502+
const cloudFn = firestore.onDocumentCreated('users/{user}', handler);
503+
const cloudFnWrap = wrapV2(cloudFn);
504+
const partial = {
505+
params: {},
506+
};
507+
const cloudEvent = cloudFnWrap(partial).cloudEvent;
508+
509+
expect(cloudEvent.document).equal('users/undefined');
510+
});
511+
});
512+
513+
describe('document options', () => {
514+
it('resolves default document options correctly', () => {
515+
const cloudFn = firestore.onDocumentCreated('foo/bar/baz', handler);
516+
const cloudFnWrap = wrapV2(cloudFn);
517+
const cloudEvent = cloudFnWrap().cloudEvent;
518+
519+
expect(cloudEvent.document).equal('foo/bar/baz');
520+
expect(cloudEvent.database).equal('(default)');
521+
expect(cloudEvent.namespace).equal('(default)');
522+
});
523+
524+
it('reads custom DocumentOptions correctly', () => {
525+
const documentOptions = {
526+
document: 'foo/bar/baz',
527+
database: 'custom-database',
528+
namespace: 'custom-namespace',
529+
};
530+
const cloudFn = firestore.onDocumentCreated(documentOptions, handler);
531+
const cloudFnWrap = wrapV2(cloudFn);
532+
const cloudEvent = cloudFnWrap().cloudEvent;
533+
534+
expect(cloudEvent.document).equal(documentOptions.document);
535+
expect(cloudEvent.database).equal(documentOptions.database);
536+
expect(cloudEvent.namespace).equal(documentOptions.namespace);
537+
});
538+
});
539+
540+
describe('firestore.onDocumentCreated', () => {
541+
it('should update CloudEvent appropriately', () => {
542+
const cloudFn = firestore.onDocumentCreated('foo/bar/baz', handler);
543+
const cloudFnWrap = wrapV2(cloudFn);
544+
const cloudEvent = cloudFnWrap().cloudEvent;
545+
546+
expect(cloudEvent).deep.equal({
547+
id: cloudEvent.id,
548+
time: cloudEvent.time,
549+
specversion: '1.0',
550+
type: 'google.cloud.firestore.document.v1.created',
551+
source: '',
552+
553+
data: cloudEvent.data,
554+
location: 'us-central1',
555+
project: 'testproject',
556+
database: '(default)',
557+
namespace: '(default)',
558+
document: 'foo/bar/baz',
559+
params: {},
560+
});
561+
});
562+
563+
it('should use overridden data', () => {
564+
const cloudFn = firestore.onDocumentCreated('foo/bar/baz', handler);
565+
const cloudFnWrap = wrapV2(cloudFn);
566+
const docData = { foo: 'bar' };
567+
const data = makeDocumentSnapshot(docData, 'foo/bar/baz');
568+
const cloudEvent = cloudFnWrap({ data }).cloudEvent;
569+
570+
expect(cloudEvent.data.data()).deep.equal(docData);
571+
});
572+
573+
it('should accept json data', () => {
574+
const cloudFn = firestore.onDocumentCreated('foo/bar/baz', handler);
575+
const cloudFnWrap = wrapV2(cloudFn);
576+
const docData = { foo: 'bar' };
577+
const cloudEvent = cloudFnWrap({ data: docData }).cloudEvent;
578+
579+
expect(cloudEvent.data.data()).deep.equal(docData);
580+
});
581+
});
582+
583+
describe('firestore.onDocumentDeleted()', () => {
584+
it('should update CloudEvent appropriately', () => {
585+
const cloudFn = firestore.onDocumentDeleted('foo/bar/baz', handler);
586+
const cloudFnWrap = wrapV2(cloudFn);
587+
const cloudEvent = cloudFnWrap().cloudEvent;
588+
589+
expect(cloudEvent).deep.equal({
590+
id: cloudEvent.id,
591+
time: cloudEvent.time,
592+
specversion: '1.0',
593+
type: 'google.cloud.firestore.document.v1.deleted',
594+
source: '',
595+
596+
data: cloudEvent.data,
597+
location: 'us-central1',
598+
project: 'testproject',
599+
database: '(default)',
600+
namespace: '(default)',
601+
document: 'foo/bar/baz',
602+
params: {},
603+
});
604+
});
605+
606+
it('should use overridden data', () => {
607+
const cloudFn = firestore.onDocumentDeleted('foo/bar/baz', handler);
608+
const cloudFnWrap = wrapV2(cloudFn);
609+
const docData = { foo: 'bar' };
610+
const data = makeDocumentSnapshot(docData, 'foo/bar/baz');
611+
const cloudEvent = cloudFnWrap({ data }).cloudEvent;
612+
613+
expect(cloudEvent.data.data()).deep.equal(docData);
614+
});
615+
616+
it('should accept json data', () => {
617+
const cloudFn = firestore.onDocumentDeleted('foo/bar/baz', handler);
618+
const cloudFnWrap = wrapV2(cloudFn);
619+
const docData = { foo: 'bar' };
620+
const cloudEvent = cloudFnWrap({ data: docData }).cloudEvent;
621+
622+
expect(cloudEvent.data.data()).deep.equal(docData);
623+
});
624+
});
625+
626+
describe('firestore.onDocumentUpdated', () => {
627+
it('should update CloudEvent appropriately', () => {
628+
const cloudFn = firestore.onDocumentUpdated('foo/bar/baz', handler);
629+
const cloudFnWrap = wrapV2(cloudFn);
630+
const cloudEvent = cloudFnWrap().cloudEvent;
631+
632+
expect(cloudEvent).deep.equal({
633+
id: cloudEvent.id,
634+
time: cloudEvent.time,
635+
specversion: '1.0',
636+
type: 'google.cloud.firestore.document.v1.updated',
637+
source: '',
638+
639+
data: cloudEvent.data,
640+
location: 'us-central1',
641+
project: 'testproject',
642+
database: '(default)',
643+
namespace: '(default)',
644+
document: 'foo/bar/baz',
645+
params: {},
646+
});
647+
});
648+
649+
it('should use overridden data', () => {
650+
const cloudFn = firestore.onDocumentUpdated('foo/bar/baz', handler);
651+
const cloudFnWrap = wrapV2(cloudFn);
652+
653+
const afterDataVal = { snapshot: 'after' };
654+
const after = makeDocumentSnapshot(afterDataVal, 'foo/bar/baz');
655+
656+
const beforeDataVal = { snapshot: 'before' };
657+
const before = makeDocumentSnapshot(beforeDataVal, 'foo/bar/baz');
658+
659+
const data = { before, after };
660+
const cloudEvent = cloudFnWrap({ data }).cloudEvent;
661+
662+
expect(cloudEvent.data.before.data()).deep.equal(beforeDataVal);
663+
expect(cloudEvent.data.after.data()).deep.equal(afterDataVal);
664+
});
665+
666+
it('should accept json data', () => {
667+
const cloudFn = firestore.onDocumentUpdated('foo/bar/baz', handler);
668+
const cloudFnWrap = wrapV2(cloudFn);
669+
const afterDataVal = { snapshot: 'after' };
670+
const beforeDataVal = { snapshot: 'before' };
671+
672+
const data = { before: beforeDataVal, after: afterDataVal };
673+
const cloudEvent = cloudFnWrap({ data }).cloudEvent;
674+
675+
expect(cloudEvent.data.before.data()).deep.equal(beforeDataVal);
676+
expect(cloudEvent.data.after.data()).deep.equal(afterDataVal);
677+
});
678+
});
679+
680+
describe('firestore.onDocumentWritten', () => {
681+
it('should update CloudEvent appropriately', () => {
682+
const cloudFn = firestore.onDocumentWritten('foo/bar/baz', handler);
683+
const cloudFnWrap = wrapV2(cloudFn);
684+
const cloudEvent = cloudFnWrap().cloudEvent;
685+
686+
expect(cloudEvent).deep.equal({
687+
id: cloudEvent.id,
688+
time: cloudEvent.time,
689+
specversion: '1.0',
690+
type: 'google.cloud.firestore.document.v1.written',
691+
source: '',
692+
693+
data: cloudEvent.data,
694+
location: 'us-central1',
695+
project: 'testproject',
696+
database: '(default)',
697+
namespace: '(default)',
698+
document: 'foo/bar/baz',
699+
params: {},
700+
});
701+
});
702+
703+
it('should use overridden data', () => {
704+
const cloudFn = firestore.onDocumentWritten('foo/bar/baz', handler);
705+
const cloudFnWrap = wrapV2(cloudFn);
706+
707+
const afterDataVal = { snapshot: 'after' };
708+
const after = makeDocumentSnapshot(afterDataVal, 'foo/bar/baz');
709+
710+
const beforeDataVal = { snapshot: 'before' };
711+
const before = makeDocumentSnapshot(beforeDataVal, 'foo/bar/baz');
712+
713+
const data = { before, after };
714+
const cloudEvent = cloudFnWrap({ data }).cloudEvent;
715+
716+
expect(cloudEvent.data.before.data()).deep.equal(beforeDataVal);
717+
expect(cloudEvent.data.after.data()).deep.equal(afterDataVal);
718+
});
719+
720+
it('should accept json data', () => {
721+
const cloudFn = firestore.onDocumentWritten('foo/bar/baz', handler);
722+
const cloudFnWrap = wrapV2(cloudFn);
723+
const afterDataVal = { snapshot: 'after' };
724+
const beforeDataVal = { snapshot: 'before' };
725+
726+
const data = { before: beforeDataVal, after: afterDataVal };
727+
const cloudEvent = cloudFnWrap({ data }).cloudEvent;
728+
729+
expect(cloudEvent.data.before.data()).deep.equal(beforeDataVal);
730+
expect(cloudEvent.data.after.data()).deep.equal(afterDataVal);
731+
});
732+
});
733+
});
734+
463735
describe('database', () => {
464736
describe('ref', () => {
465737
it('should resolve default ref', () => {
@@ -488,23 +760,23 @@ describe('v2', () => {
488760
expect(cloudEvent.ref).equal('foo/StringParam/baz');
489761
});
490762

491-
it.skip('should resolve default ref given TernaryExpression', () => {
492-
const ref1 = defineString('rtdb_ref_1');
493-
process.env.rtdb_ref_1 = 'foo/StringParam/1';
494-
const ref2 = defineString('rtdb_ref_2');
495-
process.env.rtdb_ref_2 = 'foo/StringParam/2';
496-
const referenceOptions = {
497-
ref: '',
498-
instance: 'instance-1',
499-
};
500-
const cloudFn = database.onValueCreated(referenceOptions, handler);
501-
cloudFn.__endpoint.eventTrigger.eventFilterPathPatterns.ref = ref1
502-
.equals('aa')
503-
.then('rtdb_ref_1', 'rtdb_ref_2');
504-
const cloudFnWrap = wrapV2(cloudFn);
505-
const cloudEvent = cloudFnWrap().cloudEvent;
506-
expect(cloudEvent.ref).equal('rtdb_ref_2');
507-
});
763+
// it('should resolve default ref given TernaryExpression', () => {
764+
// const ref1 = defineString('rtdb_ref_1');
765+
// process.env.rtdb_ref_1 = 'foo/StringParam/1';
766+
// const ref2 = defineString('rtdb_ref_2');
767+
// process.env.rtdb_ref_2 = 'foo/StringParam/2';
768+
// const referenceOptions = {
769+
// ref: '',
770+
// instance: 'instance-1',
771+
// };
772+
// const cloudFn = database.onValueCreated(referenceOptions, handler);
773+
// cloudFn.__endpoint.eventTrigger.eventFilterPathPatterns.ref = ref1
774+
// .equals('aa')
775+
// .then('rtdb_ref_1', 'rtdb_ref_2');
776+
// const cloudFnWrap = wrapV2(cloudFn);
777+
// const cloudEvent = cloudFnWrap().cloudEvent;
778+
// expect(cloudEvent.ref).equal('rtdb_ref_2');
779+
// });
508780

509781
it('should resolve using params', () => {
510782
const referenceOptions = {

src/cloudevent/generate.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import {
44
database,
55
pubsub,
66
} from 'firebase-functions/v2';
7+
import {
8+
DocumentSnapshot,
9+
QueryDocumentSnapshot,
10+
} from 'firebase-admin/firestore';
711
import { LIST_OF_MOCK_CLOUD_EVENT_PARTIALS } from './mocks/partials';
812
import { DeepPartial } from './types';
913
import { Change } from 'firebase-functions';
@@ -41,7 +45,13 @@ export function generateMockCloudEvent<EventType extends CloudEvent<unknown>>(
4145
return null;
4246
}
4347

44-
const IMMUTABLE_DATA_TYPES = [database.DataSnapshot, Change, pubsub.Message];
48+
const IMMUTABLE_DATA_TYPES = [
49+
database.DataSnapshot,
50+
DocumentSnapshot,
51+
QueryDocumentSnapshot,
52+
Change,
53+
pubsub.Message,
54+
];
4555

4656
function mergeCloudEvents<EventType extends CloudEvent<unknown>>(
4757
generatedCloudEvent: EventType,

0 commit comments

Comments
 (0)