Skip to content

Commit c720e2f

Browse files
authored
Support rawRequest in callable context (#54)
1 parent f1dd34d commit c720e2f

File tree

2 files changed

+132
-66
lines changed

2 files changed

+132
-66
lines changed

spec/providers/https.spec.ts

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,75 @@ import * as functions from 'firebase-functions';
33
import fft = require('../../src/index');
44

55
const cfToUpperCaseOnRequest = functions.https.onRequest((req, res) => {
6-
res.json({msg: req.params.message.toUpperCase()});
6+
res.json({ msg: req.params.message.toUpperCase() });
77
});
88

99
const cfToUpperCaseOnCall = functions.https.onCall((data, context) => {
10-
const result = {
11-
msg: data.message.toUpperCase(),
12-
from: 'anonymous',
13-
};
10+
const result: any = {
11+
msg: data.message.toUpperCase(),
12+
from: 'anonymous',
13+
};
14+
15+
if (context.auth && context.auth.uid) {
16+
result.from = context.auth.uid;
17+
}
1418

15-
if (context.auth && context.auth.uid) {
16-
result.from = context.auth.uid;
17-
}
19+
if (context.rawRequest) {
20+
result.rawRequest = context.rawRequest;
21+
}
1822

19-
return result;
23+
return result;
2024
});
2125

2226
describe('providers/https', () => {
23-
it('should not throw when passed onRequest function', async () => {
24-
const test = fft();
25-
/*
26-
Note that we must cast the function to any here because onRequst functions
27-
do not fulfill Runnable<>, so these checks are solely for usage of this lib
28-
in JavaScript test suites.
29-
*/
30-
expect(() => test.wrap(cfToUpperCaseOnRequest as any)).to.throw();
31-
});
27+
it('should not throw when passed onRequest function', async () => {
28+
const test = fft();
29+
/*
30+
Note that we must cast the function to any here because onRequst functions
31+
do not fulfill Runnable<>, so these checks are solely for usage of this lib
32+
in JavaScript test suites.
33+
*/
34+
expect(() => test.wrap(cfToUpperCaseOnRequest as any)).to.throw();
35+
});
3236

33-
it('should run the wrapped onCall function and return result', async () => {
34-
const test = fft();
35-
const result = await test.wrap(cfToUpperCaseOnCall)({message: 'lowercase'});
36-
expect(result).to.deep.equal({msg: 'LOWERCASE', from: 'anonymous'});
37-
});
37+
it('should run the wrapped onCall function and return result', async () => {
38+
const test = fft();
39+
40+
const result = await test.wrap(cfToUpperCaseOnCall)({ message: 'lowercase' });
41+
42+
expect(result).to.deep.equal({ msg: 'LOWERCASE', from: 'anonymous' });
43+
});
44+
45+
it('should accept auth params', async () => {
46+
const test = fft();
47+
const options = {auth: {uid: 'abc'}};
48+
49+
const result = await test.wrap(cfToUpperCaseOnCall)({ message: 'lowercase' }, options);
50+
51+
expect(result).to.deep.equal({msg: 'LOWERCASE', from: 'abc'});
52+
});
53+
54+
it('should accept raw request', async () => {
55+
const mockRequest: any = (sessionData) => {
56+
return {
57+
session: {data: sessionData},
58+
};
59+
};
60+
mockRequest.rawBody = Buffer.from('foobar');
61+
const test = fft();
62+
const options = {
63+
rawRequest: mockRequest,
64+
};
65+
66+
const result = await test.wrap(cfToUpperCaseOnCall)(
67+
{ message: 'lowercase' },
68+
options,
69+
);
3870

39-
it('should accept auth params', async () => {
40-
const test = fft();
41-
const options = {auth: {uid: 'abc'}};
42-
const result = await test.wrap(cfToUpperCaseOnCall)({message: 'lowercase'}, options);
43-
expect(result).to.deep.equal({msg: 'LOWERCASE', from: 'abc'});
71+
expect(result).to.deep.equal({
72+
msg: 'LOWERCASE',
73+
from: 'anonymous',
74+
rawRequest: mockRequest,
4475
});
76+
});
4577
});

src/main.ts

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
import { has, merge, random, get } from 'lodash';
2424

25-
import { CloudFunction, EventContext, Change } from 'firebase-functions';
25+
import { CloudFunction, EventContext, Change, https } from 'firebase-functions';
2626

2727
/** Fields of the event context that can be overridden/customized. */
2828
export type EventContextOptions = {
@@ -33,7 +33,7 @@ export type EventContextOptions = {
3333
/** The values for the wildcards in the reference path that a database or Firestore function is listening to.
3434
* If omitted, random values will be generated.
3535
*/
36-
params?: { [option: string]: any };
36+
params?: {[option: string]: any};
3737
/** (Only for database functions and https.onCall.) Firebase auth variable representing the user that triggered
3838
* the function. Defaults to null.
3939
*/
@@ -55,6 +55,11 @@ export type CallableContextOptions = {
5555
* An unverified token for a Firebase Instance ID.
5656
*/
5757
instanceIdToken?: string;
58+
59+
/**
60+
* The raw HTTP request object.
61+
*/
62+
rawRequest?: https.Request;
5863
};
5964

6065
/* Fields for both Event and Callable contexts, checked at runtime */
@@ -63,73 +68,90 @@ export type ContextOptions = EventContextOptions | CallableContextOptions;
6368
/** A function that can be called with test data and optional override values for the event context.
6469
* It will subsequently invoke the cloud function it wraps with the provided test data and a generated event context.
6570
*/
66-
export type WrappedFunction = (data: any, options?: ContextOptions) => any | Promise<any>;
71+
export type WrappedFunction = (
72+
data: any,
73+
options?: ContextOptions,
74+
) => any | Promise<any>;
6775

6876
/** Takes a cloud function to be tested, and returns a WrappedFunction which can be called in test code. */
6977
export function wrap<T>(cloudFunction: CloudFunction<T>): WrappedFunction {
7078
if (!has(cloudFunction, '__trigger')) {
71-
throw new Error('Wrap can only be called on functions written with the firebase-functions SDK.');
79+
throw new Error(
80+
'Wrap can only be called on functions written with the firebase-functions SDK.',
81+
);
7282
}
7383

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`.');
84+
if (
85+
has(cloudFunction, '__trigger.httpsTrigger') &&
86+
get(cloudFunction, '__trigger.labels.deployment-callable') !== 'true'
87+
) {
88+
throw new Error(
89+
'Wrap function is only available for `onCall` HTTP functions, not `onRequest`.',
90+
);
7791
}
7892

7993
if (!has(cloudFunction, 'run')) {
80-
throw new Error('This library can only be used with functions written with firebase-functions v1.0.0 and above');
94+
throw new Error(
95+
'This library can only be used with functions written with firebase-functions v1.0.0 and above',
96+
);
8197
}
8298

83-
const isCallableFunction = get(cloudFunction, '__trigger.labels.deployment-callable') === 'true';
99+
const isCallableFunction =
100+
get(cloudFunction, '__trigger.labels.deployment-callable') === 'true';
84101

85102
let wrapped: WrappedFunction = (data: T, options: ContextOptions) => {
86103
// Although in Typescript we require `options` some of our JS samples do not pass it.
87104
options = options || {};
88105
let context;
89106

90107
if (isCallableFunction) {
91-
_checkOptionValidity(['auth', 'instanceIdToken'], options);
108+
_checkOptionValidity(['auth', 'instanceIdToken', 'rawRequest'], options);
92109
let callableContextOptions = options as CallableContextOptions;
93110
context = {
94-
...callableContextOptions,
95-
rawRequest: 'rawRequest is not supported in firebase-functions-test',
111+
...callableContextOptions,
96112
};
97113
} else {
98-
_checkOptionValidity(['eventId', 'timestamp', 'params', 'auth', 'authType'], options);
114+
_checkOptionValidity(
115+
['eventId', 'timestamp', 'params', 'auth', 'authType'],
116+
options,
117+
);
99118
let eventContextOptions = options as EventContextOptions;
100119
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: {},
120+
eventId: _makeEventId(),
121+
resource: cloudFunction.__trigger.eventTrigger && {
122+
service: cloudFunction.__trigger.eventTrigger.service,
123+
name: _makeResourceName(
124+
cloudFunction.__trigger.eventTrigger.resource,
125+
has(eventContextOptions, 'params') && eventContextOptions.params,
126+
),
127+
},
128+
eventType: get(cloudFunction, '__trigger.eventTrigger.eventType'),
129+
timestamp: new Date().toISOString(),
130+
params: {},
112131
};
113132

114-
if (has(defaultContext, 'eventType') && defaultContext.eventType !== undefined &&
115-
defaultContext.eventType.match(/firebase.database/)) {
133+
if (
134+
has(defaultContext, 'eventType') &&
135+
defaultContext.eventType !== undefined &&
136+
defaultContext.eventType.match(/firebase.database/)
137+
) {
116138
defaultContext.authType = 'UNAUTHENTICATED';
117139
defaultContext.auth = null;
118140
}
119141
context = merge({}, defaultContext, eventContextOptions);
120142
}
121143

122-
return cloudFunction.run(
123-
data,
124-
context,
125-
);
144+
return cloudFunction.run(data, context);
126145
};
127146

128147
return wrapped;
129148
}
130149

131150
/** @internal */
132-
export function _makeResourceName(triggerResource: string, params = {}): string {
151+
export function _makeResourceName(
152+
triggerResource: string,
153+
params = {},
154+
): string {
133155
const wildcardRegex = new RegExp('{[^/{}]*}', 'g');
134156
let resourceName = triggerResource.replace(wildcardRegex, (wildcard) => {
135157
let wildcardNoBraces = wildcard.slice(1, -1); // .slice removes '{' and '}' from wildcard
@@ -140,15 +162,27 @@ export function _makeResourceName(triggerResource: string, params = {}): string
140162
}
141163

142164
function _makeEventId(): string {
143-
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
165+
return (
166+
Math.random()
167+
.toString(36)
168+
.substring(2, 15) +
169+
Math.random()
170+
.toString(36)
171+
.substring(2, 15)
172+
);
144173
}
145174

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-
});
175+
function _checkOptionValidity(
176+
validFields: string[],
177+
options: {[s: string]: any},
178+
) {
179+
Object.keys(options).forEach((key) => {
180+
if (validFields.indexOf(key) === -1) {
181+
throw new Error(
182+
`Options object ${JSON.stringify(options)} has invalid key "${key}"`,
183+
);
184+
}
185+
});
152186
}
153187

154188
/** Make a Change object to be used as test data for Firestore and real time database onWrite and onUpdate functions. */

0 commit comments

Comments
 (0)