Skip to content

Support rawRequest in callable context #54

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 60 additions & 28 deletions spec/providers/https.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,75 @@ import * as functions from 'firebase-functions';
import fft = require('../../src/index');

const cfToUpperCaseOnRequest = functions.https.onRequest((req, res) => {
res.json({msg: req.params.message.toUpperCase()});
res.json({ msg: req.params.message.toUpperCase() });
});

const cfToUpperCaseOnCall = functions.https.onCall((data, context) => {
const result = {
msg: data.message.toUpperCase(),
from: 'anonymous',
};
const result: any = {
msg: data.message.toUpperCase(),
from: 'anonymous',
};

if (context.auth && context.auth.uid) {
result.from = context.auth.uid;
}

if (context.auth && context.auth.uid) {
result.from = context.auth.uid;
}
if (context.rawRequest) {
result.rawRequest = context.rawRequest;
}

return result;
return result;
});

describe('providers/https', () => {
it('should not throw when passed onRequest function', async () => {
const test = fft();
/*
Note that we must cast the function to any here because onRequst functions
do not fulfill Runnable<>, so these checks are solely for usage of this lib
in JavaScript test suites.
*/
expect(() => test.wrap(cfToUpperCaseOnRequest as any)).to.throw();
});
it('should not throw when passed onRequest function', async () => {
const test = fft();
/*
Note that we must cast the function to any here because onRequst functions
do not fulfill Runnable<>, so these checks are solely for usage of this lib
in JavaScript test suites.
*/
expect(() => test.wrap(cfToUpperCaseOnRequest as any)).to.throw();
});

it('should run the wrapped onCall function and return result', async () => {
const test = fft();
const result = await test.wrap(cfToUpperCaseOnCall)({message: 'lowercase'});
expect(result).to.deep.equal({msg: 'LOWERCASE', from: 'anonymous'});
});
it('should run the wrapped onCall function and return result', async () => {
const test = fft();

const result = await test.wrap(cfToUpperCaseOnCall)({ message: 'lowercase' });

expect(result).to.deep.equal({ msg: 'LOWERCASE', from: 'anonymous' });
});

it('should accept auth params', async () => {
const test = fft();
const options = {auth: {uid: 'abc'}};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit - add a newline after this line and after you call test.wrap, to split up the arrange ,act, and assert steps of this test


const result = await test.wrap(cfToUpperCaseOnCall)({ message: 'lowercase' }, options);

expect(result).to.deep.equal({msg: 'LOWERCASE', from: 'abc'});
});

it('should accept raw request', async () => {
const mockRequest: any = (sessionData) => {
return {
session: {data: sessionData},
};
};
mockRequest.rawBody = Buffer.from('foobar');
const test = fft();
const options = {
rawRequest: mockRequest,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit - add a newline after this line and after you call test.wrap, to split up the arrange ,act, and assert steps of this test


const result = await test.wrap(cfToUpperCaseOnCall)(
{ message: 'lowercase' },
options,
);

it('should accept auth params', async () => {
const test = fft();
const options = {auth: {uid: 'abc'}};
const result = await test.wrap(cfToUpperCaseOnCall)({message: 'lowercase'}, options);
expect(result).to.deep.equal({msg: 'LOWERCASE', from: 'abc'});
expect(result).to.deep.equal({
msg: 'LOWERCASE',
from: 'anonymous',
rawRequest: mockRequest,
});
});
});
110 changes: 72 additions & 38 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

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

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

/** Fields of the event context that can be overridden/customized. */
export type EventContextOptions = {
Expand All @@ -33,7 +33,7 @@ export type EventContextOptions = {
/** The values for the wildcards in the reference path that a database or Firestore function is listening to.
* If omitted, random values will be generated.
*/
params?: { [option: string]: any };
params?: {[option: string]: any};
/** (Only for database functions and https.onCall.) Firebase auth variable representing the user that triggered
* the function. Defaults to null.
*/
Expand All @@ -55,6 +55,11 @@ export type CallableContextOptions = {
* An unverified token for a Firebase Instance ID.
*/
instanceIdToken?: string;

/**
* The raw HTTP request object.
*/
rawRequest?: https.Request;
};

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

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

if (has(cloudFunction, '__trigger.httpsTrigger') &&
(get(cloudFunction, '__trigger.labels.deployment-callable') !== 'true')) {
throw new Error('Wrap function is only available for `onCall` HTTP functions, not `onRequest`.');
if (
has(cloudFunction, '__trigger.httpsTrigger') &&
get(cloudFunction, '__trigger.labels.deployment-callable') !== 'true'
) {
throw new Error(
'Wrap function is only available for `onCall` HTTP functions, not `onRequest`.',
);
}

if (!has(cloudFunction, 'run')) {
throw new Error('This library can only be used with functions written with firebase-functions v1.0.0 and above');
throw new Error(
'This library can only be used with functions written with firebase-functions v1.0.0 and above',
);
}

const isCallableFunction = get(cloudFunction, '__trigger.labels.deployment-callable') === 'true';
const isCallableFunction =
get(cloudFunction, '__trigger.labels.deployment-callable') === 'true';

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

if (isCallableFunction) {
_checkOptionValidity(['auth', 'instanceIdToken'], options);
_checkOptionValidity(['auth', 'instanceIdToken', 'rawRequest'], options);
let callableContextOptions = options as CallableContextOptions;
context = {
...callableContextOptions,
rawRequest: 'rawRequest is not supported in firebase-functions-test',
...callableContextOptions,
};
} else {
_checkOptionValidity(['eventId', 'timestamp', 'params', 'auth', 'authType'], options);
_checkOptionValidity(
['eventId', 'timestamp', 'params', 'auth', 'authType'],
options,
);
let eventContextOptions = options as EventContextOptions;
const defaultContext: EventContext = {
eventId: _makeEventId(),
resource: cloudFunction.__trigger.eventTrigger && {
service: cloudFunction.__trigger.eventTrigger.service,
name: _makeResourceName(
cloudFunction.__trigger.eventTrigger.resource,
has(eventContextOptions, 'params') && eventContextOptions.params,
),
},
eventType: get(cloudFunction, '__trigger.eventTrigger.eventType'),
timestamp: (new Date()).toISOString(),
params: {},
eventId: _makeEventId(),
resource: cloudFunction.__trigger.eventTrigger && {
service: cloudFunction.__trigger.eventTrigger.service,
name: _makeResourceName(
cloudFunction.__trigger.eventTrigger.resource,
has(eventContextOptions, 'params') && eventContextOptions.params,
),
},
eventType: get(cloudFunction, '__trigger.eventTrigger.eventType'),
timestamp: new Date().toISOString(),
params: {},
};

if (has(defaultContext, 'eventType') && defaultContext.eventType !== undefined &&
defaultContext.eventType.match(/firebase.database/)) {
if (
has(defaultContext, 'eventType') &&
defaultContext.eventType !== undefined &&
defaultContext.eventType.match(/firebase.database/)
) {
defaultContext.authType = 'UNAUTHENTICATED';
defaultContext.auth = null;
}
context = merge({}, defaultContext, eventContextOptions);
}

return cloudFunction.run(
data,
context,
);
return cloudFunction.run(data, context);
};

return wrapped;
}

/** @internal */
export function _makeResourceName(triggerResource: string, params = {}): string {
export function _makeResourceName(
triggerResource: string,
params = {},
): string {
const wildcardRegex = new RegExp('{[^/{}]*}', 'g');
let resourceName = triggerResource.replace(wildcardRegex, (wildcard) => {
let wildcardNoBraces = wildcard.slice(1, -1); // .slice removes '{' and '}' from wildcard
Expand All @@ -140,15 +162,27 @@ export function _makeResourceName(triggerResource: string, params = {}): string
}

function _makeEventId(): string {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
return (
Math.random()
.toString(36)
.substring(2, 15) +
Math.random()
.toString(36)
.substring(2, 15)
);
}

function _checkOptionValidity(validFields: string[], options: {[s: string]: any}) {
Object.keys(options).forEach((key) => {
if (validFields.indexOf(key) === -1) {
throw new Error(`Options object ${JSON.stringify(options)} has invalid key "${key}"`);
}
});
function _checkOptionValidity(
validFields: string[],
options: {[s: string]: any},
) {
Object.keys(options).forEach((key) => {
if (validFields.indexOf(key) === -1) {
throw new Error(
`Options object ${JSON.stringify(options)} has invalid key "${key}"`,
);
}
});
}

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