Skip to content

Commit d26d559

Browse files
authored
Add support for scheduled functions. (#51)
1 parent 49e265e commit d26d559

File tree

3 files changed

+102
-16
lines changed

3 files changed

+102
-16
lines changed

spec/index.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import './app.spec';
7272
import './providers/https.spec';
7373
import './providers/firestore.spec';
7474
import './providers/database.spec';
75+
import './providers/scheduled.spec';
7576
// import './providers/analytics.spec';
7677
// import './providers/auth.spec';
7778
// import './providers/https.spec';

spec/providers/scheduled.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as sinon from 'sinon';
2+
import * as functions from 'firebase-functions';
3+
import fft = require('../../src/index');
4+
import { WrappedScheduledFunction } from '../../src/main';
5+
6+
describe('providers/scheduled', () => {
7+
const fakeFn = sinon.fake.resolves();
8+
const scheduledFunc = functions.pubsub
9+
.schedule('every 2 hours')
10+
.onRun(fakeFn);
11+
12+
const emptyObjectMatcher = sinon.match(
13+
(v) => sinon.match.object.test(v) && Object.keys(v).length === 0
14+
);
15+
16+
afterEach(() => {
17+
fakeFn.resetHistory();
18+
});
19+
20+
it('should run the wrapped function with generated context', async () => {
21+
const test = fft();
22+
const fn: WrappedScheduledFunction = test.wrap(scheduledFunc);
23+
await fn();
24+
// Function should only be called with 1 argument
25+
sinon.assert.calledOnce(fakeFn);
26+
sinon.assert.calledWithExactly(
27+
fakeFn,
28+
sinon.match({
29+
eventType: sinon.match.string,
30+
timestamp: sinon.match.string,
31+
params: emptyObjectMatcher,
32+
})
33+
);
34+
});
35+
36+
it('should run the wrapped function with provided context', async () => {
37+
const timestamp = new Date().toISOString();
38+
const test = fft();
39+
const fn: WrappedScheduledFunction = test.wrap(scheduledFunc);
40+
await fn({ timestamp });
41+
sinon.assert.calledOnce(fakeFn);
42+
sinon.assert.calledWithExactly(
43+
fakeFn,
44+
sinon.match({
45+
eventType: sinon.match.string,
46+
timestamp,
47+
params: emptyObjectMatcher,
48+
})
49+
);
50+
});
51+
});

src/main.ts

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,40 @@ export type WrappedFunction = (
7373
options?: ContextOptions
7474
) => any | Promise<any>;
7575

76+
/** A scheduled function that can be called with optional override values for the event context.
77+
* It will subsequently invoke the cloud function it wraps with a generated event context.
78+
*/
79+
export type WrappedScheduledFunction = (
80+
options?: ContextOptions
81+
) => any | Promise<any>;
82+
7683
/** Takes a cloud function to be tested, and returns a WrappedFunction which can be called in test code. */
77-
export function wrap<T>(cloudFunction: CloudFunction<T>): WrappedFunction {
84+
export function wrap<T>(
85+
cloudFunction: CloudFunction<T>
86+
): WrappedScheduledFunction | WrappedFunction {
7887
if (!has(cloudFunction, '__trigger')) {
7988
throw new Error(
8089
'Wrap can only be called on functions written with the firebase-functions SDK.'
8190
);
8291
}
8392

93+
if (get(cloudFunction, '__trigger.labels.deployment-scheduled') === 'true') {
94+
const scheduledWrapped: WrappedScheduledFunction = (
95+
options: ContextOptions
96+
) => {
97+
// Although in Typescript we require `options` some of our JS samples do not pass it.
98+
options = options || {};
99+
100+
_checkOptionValidity(['eventId', 'timestamp'], options);
101+
const defaultContext = _makeDefaultContext(cloudFunction, options);
102+
const context = merge({}, defaultContext, options);
103+
104+
// @ts-ignore
105+
return cloudFunction.run(context);
106+
};
107+
return scheduledWrapped;
108+
}
109+
84110
if (
85111
has(cloudFunction, '__trigger.httpsTrigger') &&
86112
get(cloudFunction, '__trigger.labels.deployment-callable') !== 'true'
@@ -115,20 +141,7 @@ export function wrap<T>(cloudFunction: CloudFunction<T>): WrappedFunction {
115141
['eventId', 'timestamp', 'params', 'auth', 'authType'],
116142
options
117143
);
118-
let eventContextOptions = options as EventContextOptions;
119-
const defaultContext: EventContext = {
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: {},
131-
};
144+
const defaultContext = _makeDefaultContext(cloudFunction, options);
132145

133146
if (
134147
has(defaultContext, 'eventType') &&
@@ -138,7 +151,7 @@ export function wrap<T>(cloudFunction: CloudFunction<T>): WrappedFunction {
138151
defaultContext.authType = 'UNAUTHENTICATED';
139152
defaultContext.auth = null;
140153
}
141-
context = merge({}, defaultContext, eventContextOptions);
154+
context = merge({}, defaultContext, options);
142155
}
143156

144157
return cloudFunction.run(data, context);
@@ -185,6 +198,27 @@ function _checkOptionValidity(
185198
});
186199
}
187200

201+
function _makeDefaultContext<T>(
202+
cloudFunction: CloudFunction<T>,
203+
options: ContextOptions
204+
): EventContext {
205+
let eventContextOptions = options as EventContextOptions;
206+
const defaultContext: EventContext = {
207+
eventId: _makeEventId(),
208+
resource: cloudFunction.__trigger.eventTrigger && {
209+
service: cloudFunction.__trigger.eventTrigger.service,
210+
name: _makeResourceName(
211+
cloudFunction.__trigger.eventTrigger.resource,
212+
has(eventContextOptions, 'params') && eventContextOptions.params
213+
),
214+
},
215+
eventType: get(cloudFunction, '__trigger.eventTrigger.eventType'),
216+
timestamp: new Date().toISOString(),
217+
params: {},
218+
};
219+
return defaultContext;
220+
}
221+
188222
/** Make a Change object to be used as test data for Firestore and real time database onWrite and onUpdate functions. */
189223
export function makeChange<T>(before: T, after: T): Change<T> {
190224
return Change.fromObjects(before, after);

0 commit comments

Comments
 (0)