Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
- fix: support firebase-admin v14 by moving to the modular `firebase-admin/app` and `firebase-admin/firestore` entry points (#327)
- breaking: drop firebase-admin `^8` and `^9` from the peer dependency range
- breaking: type `makeDocumentSnapshot` as returning a `QueryDocumentSnapshot` when given data and a `DocumentSnapshot` when given `{}`, rather than `any` (#327)
- breaking: `WrappedV2CallableFunction` now takes the callable's request data and return types (`WrappedV2CallableFunction<T, Return>`), and wrapped v2 callables check the request against the callable's data type. Code that named `WrappedV2CallableFunction<Return>` should pass both type arguments (#257)
- fix: raise lodash to `^4.18.1` to pick up fixes for GHSA-r5fr-rjxr-66jc and GHSA-f23m-r3pf-42rh (#341)
86 changes: 83 additions & 3 deletions spec/v2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@

import { expect } from 'chai';

import { wrapV2 } from '../src/v2';
import { wrapV2, WrappedV2CallableRequest } from '../src/v2';

import {
CloudFunction,

Check warning on line 28 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (24.x)

'CloudFunction' is defined but never used

Check warning on line 28 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (24.x)

'CloudFunction' is defined but never used

Check warning on line 28 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22.x)

'CloudFunction' is defined but never used

Check warning on line 28 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22.x)

'CloudFunction' is defined but never used

Check warning on line 28 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (24.x)

'CloudFunction' is defined but never used

Check warning on line 28 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (24.x)

'CloudFunction' is defined but never used

Check warning on line 28 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22.x)

'CloudFunction' is defined but never used

Check warning on line 28 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22.x)

'CloudFunction' is defined but never used
alerts,
database,
pubsub,
Expand All @@ -36,10 +36,11 @@
https,
firestore,
} from 'firebase-functions/v2';
import { CallableRequest } from 'firebase-functions/v2/https';
import { defineString } from 'firebase-functions/params';
import { makeDataSnapshot } from '../src/providers/database';
import { makeDocumentSnapshot } from '../src/providers/firestore';
import { inspect } from 'util';

Check warning on line 43 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (24.x)

'inspect' is defined but never used

Check warning on line 43 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (24.x)

'inspect' is defined but never used

Check warning on line 43 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22.x)

'inspect' is defined but never used

Check warning on line 43 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22.x)

'inspect' is defined but never used

Check warning on line 43 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (24.x)

'inspect' is defined but never used

Check warning on line 43 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (24.x)

'inspect' is defined but never used

Check warning on line 43 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22.x)

'inspect' is defined but never used

Check warning on line 43 in spec/v2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22.x)

'inspect' is defined but never used

describe('v2', () => {
describe('#wrapV2', () => {
Expand Down Expand Up @@ -1428,8 +1429,87 @@
};

const wrappedCF = wrapV2(cloudFunction);
const result = await wrappedCF({} as any);
expect(result).equal('hello');
expect(await wrappedCF()).equal('hello');
expect(await wrappedCF({ auth: { uid: 'test-user' } })).equal('hello');
});

it('should keep the callable request and return types', async () => {
const cloudFunction = https.onCall<{ a: number }, Promise<string>>(
async (req) => `${req.data.a}`
);
cloudFunction.__endpoint = {
platform: 'gcfv2',
labels: {},
callableTrigger: {},
};

const wrappedCF = wrapV2(cloudFunction);
const result: string = await wrappedCF({ data: { a: 1 } });
expect(result).equal('1');

// @ts-expect-error wrong data type
() => wrappedCF({ data: { a: 'x' } });
});

it('should accept every CallableRequest field', () => {
type UnhandledKeys = Exclude<
keyof CallableRequest,
keyof WrappedV2CallableRequest<any>
>;
const allKeysCovered: [UnhandledKeys] extends [never] ? true : false =
true;
expect(allKeysCovered).equal(true);
});

it('should allow omitting the request when the callable takes no data', async () => {
const cloudFunction = https.onCall<void, string>(() => 'hello');
cloudFunction.__endpoint = {
platform: 'gcfv2',
labels: {},
callableTrigger: {},
};

const wrappedCF = wrapV2(cloudFunction);
expect(await wrappedCF()).equal('hello');
expect(
await wrappedCF({ rawRequest: { headers: { 'x-test': '1' } } })
).equal('hello');
});

it('should pass data as undefined when only request metadata is given', async () => {
const cloudFunction = https.onCall<void, boolean>(
(req) => 'data' in req && req.data === undefined
);
cloudFunction.__endpoint = {
platform: 'gcfv2',
labels: {},
callableTrigger: {},
};

const wrappedCF = wrapV2(cloudFunction);
expect(await wrappedCF()).equal(true);
expect(
await wrappedCF({ rawRequest: { headers: { 'x-test': '1' } } })
).equal(true);
});

it('should accept partial auth and app data', async () => {
const cloudFunction = https.onCall<{ a: number }, string>(
(req) => `${req.auth?.uid}:${req.app?.appId}`
);
cloudFunction.__endpoint = {
platform: 'gcfv2',
labels: {},
callableTrigger: {},
};

const wrappedCF = wrapV2(cloudFunction);
const result = await wrappedCF({
data: { a: 1 },
auth: { uid: 'test-user' },
app: { appId: 'test-app' },
});
expect(result).equal('test-user:test-app');
});
});

Expand Down
10 changes: 5 additions & 5 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,14 @@ export {
} from './v1';

// V2 Exports
export { WrappedV2Function } from './v2';
export { WrappedV2Function, WrappedV2CallableFunction } from './v2';

export function wrap<T>(
cloudFunction: HttpsFunction & Runnable<T>
): WrappedFunction<T, HttpsFunction & Runnable<T>>;
export function wrap<T>(
cloudFunction: CallableFunction<any, T>
): WrappedV2CallableFunction<T>;
export function wrap<T, Return>(
Comment thread
IzaakGough marked this conversation as resolved.
cloudFunction: CallableFunction<T, Return>
): WrappedV2CallableFunction<T, Return>;
export function wrap<T>(
cloudFunction: CloudFunctionV1<T>
): WrappedScheduledFunction | WrappedFunction<T>;
Expand All @@ -84,7 +84,7 @@ export function wrap<T, V extends CloudEvent<unknown>>(
| WrappedScheduledFunction
| WrappedFunction<T>
| WrappedV2Function<V>
| WrappedV2CallableFunction<T> {
| WrappedV2CallableFunction<any, any> {
// v2 callables are checked by trigger rather than arity, since
// firebase-functions@7 wraps them in rest-param functions of length 0.
if (isCallableV2Function(cloudFunction)) {
Expand Down
43 changes: 31 additions & 12 deletions src/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@
// SOFTWARE.

import { CloudFunction, CloudEvent } from 'firebase-functions/v2';
import { CallableFunction, CallableRequest } from 'firebase-functions/v2/https';
import {
CallableFunction,
CallableRequest,
Request,
} from 'firebase-functions/v2/https';

import { generateCombinedCloudEvent } from './cloudevent/generate';
import { DeepPartial } from './cloudevent/types';
import * as express from 'express';

/** A function that can be called with test data and optional override values for {@link CloudEvent}
* It will subsequently invoke the cloud function it wraps with the provided {@link CloudEvent}
Expand All @@ -34,9 +37,22 @@ export type WrappedV2Function<T extends CloudEvent<unknown>> = (
cloudEventPartial?: DeepPartial<T | object>
) => any | Promise<any>;

export type WrappedV2CallableFunction<T> = (
data: CallableRequest
) => T | Promise<T>;
export type WrappedV2CallableRequest<T> = {
data?: T;
rawRequest?: Partial<Request>;
auth?: DeepPartial<NonNullable<CallableRequest['auth']>>;
app?: DeepPartial<NonNullable<CallableRequest['app']>>;
instanceIdToken?: string;
acceptsStreaming?: boolean;
};

/**
* A function that can be called with a {@link CallableRequest} for a v2 callable function.
* Every request field is optional to allow focused unit testing of auth, headers, or payloads.
*/
export type WrappedV2CallableFunction<T = any, Return = any> = (
request?: WrappedV2CallableRequest<T>
) => Return;

export function isCallableV2Function(cf: {
__endpoint?: { platform?: string; callableTrigger?: unknown };
Expand Down Expand Up @@ -65,19 +81,22 @@ export function wrapV2<T extends CloudEvent<unknown>>(
): WrappedV2Function<T>;

/**
* Takes a v2 HTTP function to be tested, and returns a {@link WrappedV2HttpsFunction}
* Takes a v2 callable function to be tested, and returns a {@link WrappedV2CallableFunction}
* which can be called in test code.
*/
export function wrapV2(
cloudFunction: CallableFunction<any, any>
): WrappedV2CallableFunction<any>;
export function wrapV2<T, Return>(
cloudFunction: CallableFunction<T, Return>
): WrappedV2CallableFunction<T, Return>;

export function wrapV2<T extends CloudEvent<unknown>>(
cloudFunction: CloudFunction<T> | CallableFunction<any, any>
): WrappedV2Function<T> | WrappedV2CallableFunction<any> {
): WrappedV2Function<T> | WrappedV2CallableFunction<any, any> {
if (isCallableV2Function(cloudFunction)) {
return (req: CallableRequest) => {
return cloudFunction.run(req);
return (request?: WrappedV2CallableRequest<any>) => {
return cloudFunction.run({
data: undefined,
...request,
} as CallableRequest<any>);
};
}

Expand Down
Loading