2222
2323import { has , merge , random , get } from 'lodash' ;
2424
25- import { CloudFunction , EventContext , Resource , Change } from 'firebase-functions' ;
25+ import { CloudFunction , EventContext , Change } from 'firebase-functions' ;
2626
2727/** Fields of the event context that can be overridden/customized. */
2828export type EventContextOptions = {
@@ -34,53 +34,97 @@ export type EventContextOptions = {
3434 * If omitted, random values will be generated.
3535 */
3636 params ?: { [ option : string ] : any } ;
37- /** (Only for database functions.) Firebase auth variable representing the user that triggered
37+ /** (Only for database functions and https.onCall .) Firebase auth variable representing the user that triggered
3838 * the function. Defaults to null.
3939 */
4040 auth ?: any ;
41- /** (Only for database functions.) The authentication state of the user that triggered the function.
41+ /** (Only for database and https.onCall functions.) The authentication state of the user that triggered the function.
4242 * Default is 'UNAUTHENTICATED'.
4343 */
4444 authType ?: 'ADMIN' | 'USER' | 'UNAUTHENTICATED' ;
4545} ;
4646
47+ /** Fields of the callable context that can be overridden/customized. */
48+ export type CallableContextOptions = {
49+ /**
50+ * The result of decoding and verifying a Firebase Auth ID token.
51+ */
52+ auth ?: any ;
53+
54+ /**
55+ * An unverified token for a Firebase Instance ID.
56+ */
57+ instanceIdToken ?: string ;
58+ } ;
59+
60+ /* Fields for both Event and Callable contexts, checked at runtime */
61+ export type ContextOptions = EventContextOptions | CallableContextOptions ;
62+
4763/** A function that can be called with test data and optional override values for the event context.
4864 * It will subsequently invoke the cloud function it wraps with the provided test data and a generated event context.
4965 */
50- export type WrappedFunction = ( data : any , options ?: EventContextOptions ) => any | Promise < any > ;
66+ export type WrappedFunction = ( data : any , options ?: ContextOptions ) => any | Promise < any > ;
5167
5268/** Takes a cloud function to be tested, and returns a WrappedFunction which can be called in test code. */
5369export function wrap < T > ( cloudFunction : CloudFunction < T > ) : WrappedFunction {
5470 if ( ! has ( cloudFunction , '__trigger' ) ) {
5571 throw new Error ( 'Wrap can only be called on functions written with the firebase-functions SDK.' ) ;
5672 }
57- if ( ! has ( cloudFunction , '__trigger.eventTrigger' ) ) {
58- throw new Error ( 'Wrap function is only available for non-HTTP functions.' ) ;
73+
74+ if ( has ( cloudFunction , '__trigger.httpsTrigger' ) &&
75+ ( get ( cloudFunction , '__trigger.labels.deployment-callable' ) !== 'true' ) ) {
76+ throw new Error ( 'Wrap function is only available for `onCall` HTTP functions, not `onRequest`.' ) ;
5977 }
78+
6079 if ( ! has ( cloudFunction , 'run' ) ) {
6180 throw new Error ( 'This library can only be used with functions written with firebase-functions v1.0.0 and above' ) ;
6281 }
63- let wrapped : WrappedFunction = ( data : T , options : EventContextOptions ) => {
64- const defaultContext : EventContext = {
65- eventId : _makeEventId ( ) ,
66- resource : {
67- service : cloudFunction . __trigger . eventTrigger . service ,
68- name : _makeResourceName ( cloudFunction . __trigger . eventTrigger . resource , options ? options . params : null ) ,
69- } ,
70- eventType : cloudFunction . __trigger . eventTrigger . eventType ,
71- timestamp : ( new Date ( ) ) . toISOString ( ) ,
72- params : { } ,
73- } ;
74- if ( defaultContext . eventType . match ( / f i r e b a s e .d a t a b a s e / ) ) {
75- defaultContext . authType = 'UNAUTHENTICATED' ;
76- defaultContext . auth = null ;
82+
83+ const isCallableFunction = get ( cloudFunction , '__trigger.labels.deployment-callable' ) === 'true' ;
84+
85+ let wrapped : WrappedFunction = ( data : T , options : ContextOptions ) => {
86+ // Although in Typescript we require `options` some of our JS samples do not pass it.
87+ options = options || { } ;
88+ let context ;
89+
90+ if ( isCallableFunction ) {
91+ _checkOptionValidity ( [ 'auth' , 'instanceIdToken' ] , options ) ;
92+ let callableContextOptions = options as CallableContextOptions ;
93+ context = {
94+ ...callableContextOptions ,
95+ rawRequest : 'rawRequest is not supported in firebase-functions-test' ,
96+ } ;
97+ } else {
98+ _checkOptionValidity ( [ 'eventId' , 'timestamp' , 'params' , 'auth' , 'authType' ] , options ) ;
99+ let eventContextOptions = options as EventContextOptions ;
100+ const defaultContext : EventContext = {
101+ eventId : _makeEventId ( ) ,
102+ resource : cloudFunction . __trigger . eventTrigger && {
103+ service : cloudFunction . __trigger . eventTrigger . service ,
104+ name : _makeResourceName (
105+ cloudFunction . __trigger . eventTrigger . resource ,
106+ has ( eventContextOptions , 'params' ) && eventContextOptions . params ,
107+ ) ,
108+ } ,
109+ eventType : get ( cloudFunction , '__trigger.eventTrigger.eventType' ) ,
110+ timestamp : ( new Date ( ) ) . toISOString ( ) ,
111+ params : { } ,
112+ } ;
113+
114+ if ( has ( defaultContext , 'eventType' ) &&
115+ defaultContext . eventType . match ( / f i r e b a s e .d a t a b a s e / ) ) {
116+ defaultContext . authType = 'UNAUTHENTICATED' ;
117+ defaultContext . auth = null ;
118+ }
119+ context = merge ( { } , defaultContext , eventContextOptions ) ;
77120 }
78- let context = merge ( { } , defaultContext , options ) ;
121+
79122 return cloudFunction . run (
80123 data ,
81124 context ,
82125 ) ;
83126 } ;
127+
84128 return wrapped ;
85129}
86130
@@ -99,6 +143,14 @@ function _makeEventId(): string {
99143 return Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) + Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) ;
100144}
101145
146+ function _checkOptionValidity ( validFields : string [ ] , options : { [ s : string ] : any } ) {
147+ Object . keys ( options ) . forEach ( ( key ) => {
148+ if ( validFields . indexOf ( key ) === - 1 ) {
149+ throw new Error ( `Options object ${ JSON . stringify ( options ) } has invalid key "${ key } "` ) ;
150+ }
151+ } ) ;
152+ }
153+
102154/** Make a Change object to be used as test data for Firestore and real time database onWrite and onUpdate functions. */
103155export function makeChange < T > ( before : T , after : T ) : Change < T > {
104156 return Change . fromObjects ( before , after ) ;
0 commit comments