Skip to content

Commit 0238e23

Browse files
authored
firebase-functions-test v3 (major version bump) (#180)
* Update mockConfig function for v4 firebase-functions (#173) V4 Functions SDK does not expose the config singleton directly. Use the internal-only `resetCache` function instead. * Fix Firebase Functions v4 compatibility issues. (#175) 1. Replace all use of __trigger annotation to __endpoint 2. Correct import path for Expression class/type * BREAKING: Drop support for Firebase Functions SDK v3 and below - take 2 (#178) We also pin down peer dep requirement for firebase-admin to peer dependencies required for v4 firebase-functions sdk. * Bump firebase functions sdk version to v4 release candidate. (#179)
1 parent 8a74e61 commit 0238e23

File tree

8 files changed

+89
-62
lines changed

8 files changed

+89
-62
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## BREAKING
2+
* Drop support for Firebase Functions SDK v3 and below.
3+
* Drop support for Firebase Admin SDK v7 and below.

package-lock.json

+11-13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"@types/mocha": "^5.2.7",
4848
"chai": "^4.2.0",
4949
"firebase-admin": "^10.1.0",
50-
"firebase-functions": "^3.23.0",
50+
"firebase-functions": "^4.0.0-rc.0",
5151
"firebase-tools": "^8.9.2",
5252
"mocha": "^6.2.2",
5353
"prettier": "^1.19.1",
@@ -56,8 +56,8 @@
5656
"typescript": "^4.2.5"
5757
},
5858
"peerDependencies": {
59-
"firebase-admin": ">=6.0.0",
60-
"firebase-functions": ">=3.23.0",
59+
"firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
60+
"firebase-functions": "^4.0.0",
6161
"jest": ">=28.0.0"
6262
},
6363
"engines": {

spec/main.spec.ts

+13-18
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@ describe('main', () => {
3939
set(cloudFunction, 'run', (data, context) => {
4040
return { data, context };
4141
});
42-
set(cloudFunction, '__trigger', {
42+
set(cloudFunction, '__endpoint', {
4343
eventTrigger: {
44-
resource: 'ref/{wildcard}/nested/{anotherWildcard}',
44+
eventFilters: {
45+
resource: 'ref/{wildcard}/nested/{anotherWildcard}',
46+
},
4547
eventType: eventType || 'event',
46-
service: 'service',
48+
retry: false,
4749
},
4850
});
4951
return cloudFunction as functions.CloudFunction<any>;
@@ -57,7 +59,9 @@ describe('main', () => {
5759
it('should generate the appropriate context if no fields specified', () => {
5860
const context = wrap(constructBackgroundCF())('data').context;
5961
expect(typeof context.eventId).to.equal('string');
60-
expect(context.resource.service).to.equal('service');
62+
expect(context.resource.service).to.equal(
63+
'unknown-service.googleapis.com'
64+
);
6165
expect(
6266
/ref\/wildcard[1-9]\/nested\/anotherWildcard[1-9]/.test(
6367
context.resource.name
@@ -153,7 +157,7 @@ describe('main', () => {
153157
const cf = constructBackgroundCF(
154158
'google.firebase.database.ref.create'
155159
);
156-
cf.__trigger.eventTrigger.resource =
160+
cf.__endpoint.eventTrigger.eventFilters.resource =
157161
'companies/{company}/users/{user}';
158162
const wrapped = wrap(cf);
159163
const context = wrapped(
@@ -173,7 +177,7 @@ describe('main', () => {
173177

174178
it('should extract the appropriate params for Firestore function trigger', () => {
175179
const cf = constructBackgroundCF('google.firestore.document.create');
176-
cf.__trigger.eventTrigger.resource =
180+
cf.__endpoint.eventTrigger.eventFilters.resource =
177181
'databases/(default)/documents/companies/{company}/users/{user}';
178182
const wrapped = wrap(cf);
179183
const context = wrapped(
@@ -195,7 +199,7 @@ describe('main', () => {
195199
const cf = constructBackgroundCF(
196200
'google.firebase.database.ref.create'
197201
);
198-
cf.__trigger.eventTrigger.resource =
202+
cf.__endpoint.eventTrigger.eventFilters.resource =
199203
'companies/{company}/users/{user}';
200204
const wrapped = wrap(cf);
201205
const context = wrapped(
@@ -243,11 +247,8 @@ describe('main', () => {
243247
set(cloudFunction, 'run', (data, context) => {
244248
return { data, context };
245249
});
246-
set(cloudFunction, '__trigger', {
247-
labels: {
248-
'deployment-callable': 'true',
249-
},
250-
httpsTrigger: {},
250+
set(cloudFunction, '__endpoint', {
251+
callableTrigger: {},
251252
});
252253
wrappedCF = wrap(cloudFunction as functions.CloudFunction<any>);
253254
});
@@ -340,11 +341,5 @@ describe('main', () => {
340341

341342
expect(functions.config()).to.deep.equal(config);
342343
});
343-
344-
it('should not throw an error when functions.config.singleton is missing', () => {
345-
delete functions.config.singleton;
346-
347-
expect(() => mockConfig(config)).to.not.throw(Error);
348-
});
349344
});
350345
});

spec/v2.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
eventarc,
3434
https,
3535
} from 'firebase-functions/v2';
36-
import { defineString } from 'firebase-functions/v2/params';
36+
import { defineString } from 'firebase-functions/params';
3737
import { makeDataSnapshot } from '../src/providers/database';
3838

3939
describe('v2', () => {

src/cloudevent/mocks/database/helpers.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { CloudFunction, database } from 'firebase-functions/v2';
2-
import { Expression } from 'firebase-functions/v2/params';
32
import { DeepPartial } from '../../types';
43
import {
54
exampleDataSnapshot,

src/cloudevent/mocks/helpers.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1-
import { CloudEvent, CloudFunction } from 'firebase-functions/v2';
2-
import { Expression } from 'firebase-functions/v2/params';
1+
import * as v1 from 'firebase-functions';
2+
import * as v2 from 'firebase-functions/v2';
3+
import { Expression } from 'firebase-functions/params';
34

45
export const APP_ID = '__APP_ID__';
56
export const PROJECT_ID = '42';
67
export const FILENAME = 'file_name';
78

8-
export function getEventType(cloudFunction: CloudFunction<any>): string {
9+
type CloudFunction = v1.CloudFunction<any> | v2.CloudFunction<any>;
10+
11+
export function getEventType(cloudFunction: CloudFunction): string {
912
return cloudFunction?.__endpoint?.eventTrigger?.eventType || '';
1013
}
1114

1215
export function getEventFilters(
13-
cloudFunction: CloudFunction<any>
16+
cloudFunction: CloudFunction
1417
): Record<string, string | Expression<string>> {
1518
return cloudFunction?.__endpoint?.eventTrigger?.eventFilters || {};
1619
}
1720

18-
export function getBaseCloudEvent<EventType extends CloudEvent<unknown>>(
19-
cloudFunction: CloudFunction<EventType>
21+
export function getBaseCloudEvent<EventType extends v2.CloudEvent<unknown>>(
22+
cloudFunction: v2.CloudFunction<EventType>
2023
): EventType {
2124
return {
2225
specversion: '1.0',

src/v1.ts

+49-20
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,15 @@ import {
3232
firestore,
3333
HttpsFunction,
3434
Runnable,
35+
// @ts-ignore
36+
resetCache,
3537
} from 'firebase-functions';
38+
import { Expression } from 'firebase-functions/params';
39+
import {
40+
getEventFilters,
41+
getEventType,
42+
resolveStringExpression,
43+
} from './cloudevent/mocks/helpers';
3644

3745
/** Fields of the event context that can be overridden/customized. */
3846
export type EventContextOptions = {
@@ -116,13 +124,13 @@ export function wrapV1<T>(
116124
export function wrapV1<T>(
117125
cloudFunction: CloudFunction<T>
118126
): WrappedScheduledFunction | WrappedFunction<T, CloudFunction<T>> {
119-
if (!has(cloudFunction, '__trigger')) {
127+
if (!has(cloudFunction, '__endpoint')) {
120128
throw new Error(
121129
'Wrap can only be called on functions written with the firebase-functions SDK.'
122130
);
123131
}
124132

125-
if (get(cloudFunction, '__trigger.labels.deployment-scheduled') === 'true') {
133+
if (has(cloudFunction, '__endpoint.scheduleTrigger')) {
126134
const scheduledWrapped: WrappedScheduledFunction = (
127135
options: ContextOptions
128136
) => {
@@ -139,10 +147,7 @@ export function wrapV1<T>(
139147
return scheduledWrapped;
140148
}
141149

142-
if (
143-
has(cloudFunction, '__trigger.httpsTrigger') &&
144-
get(cloudFunction, '__trigger.labels.deployment-callable') !== 'true'
145-
) {
150+
if (has(cloudFunction, '__endpoint.httpsTrigger')) {
146151
throw new Error(
147152
'Wrap function is only available for `onCall` HTTP functions, not `onRequest`.'
148153
);
@@ -154,8 +159,7 @@ export function wrapV1<T>(
154159
);
155160
}
156161

157-
const isCallableFunction =
158-
get(cloudFunction, '__trigger.labels.deployment-callable') === 'true';
162+
const isCallableFunction = has(cloudFunction, '__endpoint.callableTrigger');
159163

160164
let wrapped: WrappedFunction<T, typeof cloudFunction> = (data, options) => {
161165
// Although in Typescript we require `options` some of our JS samples do not pass it.
@@ -197,11 +201,12 @@ export function wrapV1<T>(
197201

198202
/** @internal */
199203
export function _makeResourceName(
200-
triggerResource: string,
204+
triggerResource: string | Expression<string>,
201205
params = {}
202206
): string {
207+
const resource = resolveStringExpression(triggerResource);
203208
const wildcardRegex = new RegExp('{[^/{}]*}', 'g');
204-
let resourceName = triggerResource.replace(wildcardRegex, (wildcard) => {
209+
let resourceName = resource.replace(wildcardRegex, (wildcard) => {
205210
let wildcardNoBraces = wildcard.slice(1, -1); // .slice removes '{' and '}' from wildcard
206211
let sub = get(params, wildcardNoBraces);
207212
return sub || wildcardNoBraces + random(1, 9);
@@ -244,8 +249,8 @@ function _makeDefaultContext<T>(
244249
triggerData?: T
245250
): EventContext {
246251
let eventContextOptions = options as EventContextOptions;
247-
const eventResource = cloudFunction.__trigger.eventTrigger?.resource;
248-
const eventType = cloudFunction.__trigger.eventTrigger?.eventType;
252+
const eventType = getEventType(cloudFunction);
253+
const eventResource = getEventFilters(cloudFunction).resource;
249254

250255
const optionsParams = eventContextOptions.params ?? {};
251256
let triggerParams = {};
@@ -281,7 +286,7 @@ function _makeDefaultContext<T>(
281286
const defaultContext: EventContext = {
282287
eventId: _makeEventId(),
283288
resource: eventResource && {
284-
service: cloudFunction.__trigger.eventTrigger?.service,
289+
service: serviceFromEventType(eventType),
285290
name: _makeResourceName(eventResource, params),
286291
},
287292
eventType,
@@ -292,20 +297,22 @@ function _makeDefaultContext<T>(
292297
}
293298

294299
function _extractDatabaseParams(
295-
triggerResource: string,
300+
triggerResource: string | Expression<string>,
296301
data: database.DataSnapshot
297302
): EventContext['params'] {
303+
const resource = resolveStringExpression(triggerResource);
298304
const path = data.ref.toString().replace(data.ref.root.toString(), '');
299-
return _extractParams(triggerResource, path);
305+
return _extractParams(resource, path);
300306
}
301307

302308
function _extractFirestoreDocumentParams(
303-
triggerResource: string,
309+
triggerResource: string | Expression<string>,
304310
data: firestore.DocumentSnapshot
305311
): EventContext['params'] {
312+
const resource = resolveStringExpression(triggerResource);
306313
// Resource format: databases/(default)/documents/<path>
307314
return _extractParams(
308-
triggerResource.replace(/^databases\/[^\/]+\/documents\//, ''),
315+
resource.replace(/^databases\/[^\/]+\/documents\//, ''),
309316
data.ref.path
310317
);
311318
}
@@ -338,16 +345,38 @@ export function _extractParams(
338345
return params;
339346
}
340347

348+
function serviceFromEventType(eventType?: string): string {
349+
if (eventType) {
350+
const providerToService: Array<[string, string]> = [
351+
['google.analytics', 'app-measurement.com'],
352+
['google.firebase.auth', 'firebaseauth.googleapis.com'],
353+
['google.firebase.database', 'firebaseio.com'],
354+
['google.firestore', 'firestore.googleapis.com'],
355+
['google.pubsub', 'pubsub.googleapis.com'],
356+
['google.firebase.remoteconfig', 'firebaseremoteconfig.googleapis.com'],
357+
['google.storage', 'storage.googleapis.com'],
358+
['google.testing', 'testing.googleapis.com'],
359+
];
360+
361+
const match = providerToService.find(([provider]) => {
362+
eventType.includes(provider);
363+
});
364+
if (match) {
365+
return match[1];
366+
}
367+
}
368+
return 'unknown-service.googleapis.com';
369+
}
370+
341371
/** Make a Change object to be used as test data for Firestore and real time database onWrite and onUpdate functions. */
342372
export function makeChange<T>(before: T, after: T): Change<T> {
343373
return Change.fromObjects(before, after);
344374
}
345375

346376
/** Mock values returned by `functions.config()`. */
347377
export function mockConfig(conf: { [key: string]: { [key: string]: any } }) {
348-
if (config.singleton) {
349-
delete config.singleton;
378+
if (resetCache) {
379+
resetCache();
350380
}
351-
352381
process.env.CLOUD_RUNTIME_CONFIG = JSON.stringify(conf);
353382
}

0 commit comments

Comments
 (0)