22
22
23
23
import { has , merge , random , get } from 'lodash' ;
24
24
25
- import { CloudFunction , EventContext , Resource , Change } from 'firebase-functions' ;
25
+ import { CloudFunction , EventContext , Change } from 'firebase-functions' ;
26
26
27
27
/** Fields of the event context that can be overridden/customized. */
28
28
export type EventContextOptions = {
@@ -34,53 +34,97 @@ export type EventContextOptions = {
34
34
* If omitted, random values will be generated.
35
35
*/
36
36
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
38
38
* the function. Defaults to null.
39
39
*/
40
40
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.
42
42
* Default is 'UNAUTHENTICATED'.
43
43
*/
44
44
authType ?: 'ADMIN' | 'USER' | 'UNAUTHENTICATED' ;
45
45
} ;
46
46
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
+
47
63
/** A function that can be called with test data and optional override values for the event context.
48
64
* It will subsequently invoke the cloud function it wraps with the provided test data and a generated event context.
49
65
*/
50
- export type WrappedFunction = ( data : any , options ?: EventContextOptions ) => any | Promise < any > ;
66
+ export type WrappedFunction = ( data : any , options ?: ContextOptions ) => any | Promise < any > ;
51
67
52
68
/** Takes a cloud function to be tested, and returns a WrappedFunction which can be called in test code. */
53
69
export function wrap < T > ( cloudFunction : CloudFunction < T > ) : WrappedFunction {
54
70
if ( ! has ( cloudFunction , '__trigger' ) ) {
55
71
throw new Error ( 'Wrap can only be called on functions written with the firebase-functions SDK.' ) ;
56
72
}
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`.' ) ;
59
77
}
78
+
60
79
if ( ! has ( cloudFunction , 'run' ) ) {
61
80
throw new Error ( 'This library can only be used with functions written with firebase-functions v1.0.0 and above' ) ;
62
81
}
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 ) ;
77
120
}
78
- let context = merge ( { } , defaultContext , options ) ;
121
+
79
122
return cloudFunction . run (
80
123
data ,
81
124
context ,
82
125
) ;
83
126
} ;
127
+
84
128
return wrapped ;
85
129
}
86
130
@@ -99,6 +143,14 @@ function _makeEventId(): string {
99
143
return Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) + Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) ;
100
144
}
101
145
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
+
102
154
/** Make a Change object to be used as test data for Firestore and real time database onWrite and onUpdate functions. */
103
155
export function makeChange < T > ( before : T , after : T ) : Change < T > {
104
156
return Change . fromObjects ( before , after ) ;
0 commit comments