diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1a8c0f8..e5d61b4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,7 +22,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Cache npm - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} @@ -45,7 +45,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Cache npm - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} diff --git a/spec/providers/firestore.spec.ts b/spec/providers/firestore.spec.ts index 4e7107a..c28ca4f 100644 --- a/spec/providers/firestore.spec.ts +++ b/spec/providers/firestore.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import * as firebase from 'firebase-admin'; -import { FeaturesList } from '../../src/features'; +import { FeaturesList } from '../../src/types/commonTypes'; import fft = require('../../src/index'); describe('providers/firestore', () => { diff --git a/spec/v2.spec.ts b/spec/v2.spec.ts index 1101aa7..e55e697 100644 --- a/spec/v2.spec.ts +++ b/spec/v2.spec.ts @@ -39,7 +39,6 @@ import { import { defineString } from 'firebase-functions/params'; import { makeDataSnapshot } from '../src/providers/database'; import { makeDocumentSnapshot } from '../src/providers/firestore'; -import { inspect } from 'util'; describe('v2', () => { describe('#wrapV2', () => { diff --git a/src/cloudevent/generate.ts b/src/cloudevent/generate.ts index 4cb908d..fec330f 100644 --- a/src/cloudevent/generate.ts +++ b/src/cloudevent/generate.ts @@ -8,11 +8,12 @@ import { DocumentSnapshot, QueryDocumentSnapshot, } from 'firebase-admin/firestore'; -import { LIST_OF_MOCK_CLOUD_EVENT_PARTIALS } from './mocks/partials'; -import { DeepPartial } from './types'; import { Change } from 'firebase-functions/v1'; import merge from 'ts-deepmerge'; +import { LIST_OF_MOCK_CLOUD_EVENT_PARTIALS } from './mocks/partials'; +import { DeepPartial } from './types'; + /** * @return {CloudEvent} Generated Mock CloudEvent */ diff --git a/src/features.ts b/src/features.ts index 72616f8..14f4d91 100644 --- a/src/features.ts +++ b/src/features.ts @@ -5,19 +5,7 @@ import * as database from './providers/database'; import * as firestore from './providers/firestore'; import * as pubsub from './providers/pubsub'; import * as storage from './providers/storage'; -import { FirebaseFunctionsTest } from './lifecycle'; - -export interface LazyFeatures { - mockConfig: typeof mockConfig; - wrap: typeof wrap; - makeChange: typeof makeChange; - analytics: typeof analytics; - auth: typeof auth; - database: typeof database; - firestore: typeof firestore; - pubsub: typeof pubsub; - storage: typeof storage; -} +import { LazyFeatures } from './types/commonTypes'; export const features: LazyFeatures = { mockConfig, @@ -30,7 +18,3 @@ export const features: LazyFeatures = { pubsub, storage, }; - -export interface FeaturesList extends LazyFeatures { - cleanup: InstanceType['cleanup']; -} diff --git a/src/index.ts b/src/index.ts index 3d1f8e4..7495e8b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,7 @@ import { AppOptions } from 'firebase-admin'; import { merge } from 'lodash'; import { FirebaseFunctionsTest } from './lifecycle'; -import { FeaturesList } from './features'; +import { FeaturesList } from './types/commonTypes'; export = ( firebaseConfig?: AppOptions, diff --git a/src/main.ts b/src/main.ts index a2e621d..e28e9f8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,25 +25,35 @@ import { HttpsFunction, Runnable, } from 'firebase-functions/v1'; - import { CloudFunction as CloudFunctionV2, CloudEvent, } from 'firebase-functions/v2'; - import { CallableFunction, HttpsFunction as HttpsFunctionV2, } from 'firebase-functions/v2/https'; -import { wrapV1, WrappedFunction, WrappedScheduledFunction } from './v1'; - -import { wrapV2, WrappedV2Function, WrappedV2CallableFunction } from './v2'; +import { wrapV1 } from './v1'; +import { wrapV2 } from './v2'; +import { WrappedFunction, WrappedScheduledFunction } from './types/v1Types'; +import { WrappedV2Function, WrappedV2CallableFunction } from './types/v2Types'; +import { HttpsFunctionOrCloudFunctionV1 } from './types/v1Types'; -type HttpsFunctionOrCloudFunctionV1 = U extends HttpsFunction & - Runnable - ? HttpsFunction & Runnable - : CloudFunctionV1; +/** + * The key differences between V1 and V2 CloudFunctions are: + *
    + *
  • V1 CloudFunction is sometimes a binary function + *
  • V2 CloudFunction is always a unary function + *
  • V1 CloudFunction.run is always a binary function + *
  • V2 CloudFunction.run is always a unary function + * @return True iff the CloudFunction is a V2 function. + */ +function isV2CloudFunction>( + cloudFunction: any +): cloudFunction is CloudFunctionV2 { + return cloudFunction.length === 1 && cloudFunction?.run?.length === 1; +} // Re-exporting V1 (to reduce breakage) export { @@ -52,13 +62,10 @@ export { WrappedFunction, WrappedScheduledFunction, CallableContextOptions, - makeChange, - mockConfig, -} from './v1'; - +} from './types/v1Types'; +export { makeChange, mockConfig } from './v1'; // V2 Exports -export { WrappedV2Function } from './v2'; - +export { WrappedV2Function } from './types/v2Types'; export function wrap( cloudFunction: HttpsFunction & Runnable ): WrappedFunction>; @@ -85,18 +92,3 @@ export function wrap>( cloudFunction as HttpsFunctionOrCloudFunctionV1 ); } - -/** - * The key differences between V1 and V2 CloudFunctions are: - *
      - *
    • V1 CloudFunction is sometimes a binary function - *
    • V2 CloudFunction is always a unary function - *
    • V1 CloudFunction.run is always a binary function - *
    • V2 CloudFunction.run is always a unary function - * @return True iff the CloudFunction is a V2 function. - */ -function isV2CloudFunction>( - cloudFunction: any -): cloudFunction is CloudFunctionV2 { - return cloudFunction.length === 1 && cloudFunction?.run?.length === 1; -} diff --git a/src/providers/firestore.ts b/src/providers/firestore.ts index 5254e82..5b2da9c 100644 --- a/src/providers/firestore.ts +++ b/src/providers/firestore.ts @@ -23,16 +23,10 @@ import { Change } from 'firebase-functions/v1'; import { firestore, app } from 'firebase-admin'; import { has, get, isEmpty, isPlainObject, mapValues } from 'lodash'; -import { inspect } from 'util'; +import * as http from 'http'; import { testApp } from '../app'; -import * as http from 'http'; -import { - DocumentSnapshot, - QueryDocumentSnapshot, -} from 'firebase-admin/firestore'; - function dateToTimestampProto( timeString?: string ): { seconds: number; nanos: number } | undefined { diff --git a/src/types/commonTypes.ts b/src/types/commonTypes.ts new file mode 100644 index 0000000..c6c90c1 --- /dev/null +++ b/src/types/commonTypes.ts @@ -0,0 +1,24 @@ +import { makeChange, wrap, mockConfig } from '../main'; +import * as analytics from '../providers/analytics'; +import * as auth from '../providers/auth'; +import * as database from '../providers/database'; +import * as firestore from '../providers/firestore'; +import * as pubsub from '../providers/pubsub'; +import * as storage from '../providers/storage'; +import { FirebaseFunctionsTest } from '../lifecycle'; + +export interface LazyFeatures { + mockConfig: typeof mockConfig; + wrap: typeof wrap; + makeChange: typeof makeChange; + analytics: typeof analytics; + auth: typeof auth; + database: typeof database; + firestore: typeof firestore; + pubsub: typeof pubsub; + storage: typeof storage; +} + +export interface FeaturesList extends LazyFeatures { + cleanup: InstanceType['cleanup']; +} diff --git a/src/types/v1Types.ts b/src/types/v1Types.ts new file mode 100644 index 0000000..e67b9e5 --- /dev/null +++ b/src/types/v1Types.ts @@ -0,0 +1,86 @@ +import { + CloudFunction as CloudFunctionV1, + https, + HttpsFunction, + Runnable, +} from 'firebase-functions/v1'; + +/** Fields of the event context that can be overridden/customized. */ +export type EventContextOptions = { + /** ID of the event. If omitted, a random ID will be generated. */ + eventId?: string; + /** ISO time string of when the event occurred. If omitted, the current time is used. */ + timestamp?: string; + /** 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 }; + /** (Only for database functions and https.onCall.) Firebase auth variable representing the user that triggered + * the function. Defaults to null. + */ + auth?: any; + /** (Only for database and https.onCall functions.) The authentication state of the user that triggered the function. + * Default is 'UNAUTHENTICATED'. + */ + authType?: 'ADMIN' | 'USER' | 'UNAUTHENTICATED'; + + /** Resource is a standard format for defining a resource (google.rpc.context.AttributeContext.Resource). + * In Cloud Functions, it is the resource that triggered the function - such as a storage bucket. + */ + resource?: { + service: string; + name: string; + type?: string; + labels?: { + [tag: string]: string; + }; + }; +}; + +/** Fields of the callable context that can be overridden/customized. */ +export type CallableContextOptions = { + /** + * The result of decoding and verifying a Firebase AppCheck token. + */ + app?: any; + + /** + * The result of decoding and verifying a Firebase Auth ID token. + */ + auth?: any; + + /** + * 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 */ +export type ContextOptions = T extends HttpsFunction & Runnable + ? CallableContextOptions + : EventContextOptions; + +/** 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: T, + options?: ContextOptions +) => any | Promise; + +/** A scheduled function that can be called with optional override values for the event context. + * It will subsequently invoke the cloud function it wraps with a generated event context. + */ +export type WrappedScheduledFunction = ( + options?: ContextOptions +) => any | Promise; + +export type HttpsFunctionOrCloudFunctionV1 = U extends HttpsFunction & + Runnable + ? HttpsFunction & Runnable + : CloudFunctionV1; diff --git a/src/types/v2Types.ts b/src/types/v2Types.ts new file mode 100644 index 0000000..fc827db --- /dev/null +++ b/src/types/v2Types.ts @@ -0,0 +1,14 @@ +import { CallableRequest } from 'firebase-functions/v2/https'; +import { DeepPartial } from '../cloudevent/types'; +import { CloudEvent } from 'firebase-functions/v2'; + +/** 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} + */ +export type WrappedV2Function> = ( + cloudEventPartial?: DeepPartial +) => any | Promise; + +export type WrappedV2CallableFunction = ( + data: CallableRequest +) => T | Promise; diff --git a/src/v1.ts b/src/v1.ts index 8a7ccfc..b8801e6 100644 --- a/src/v1.ts +++ b/src/v1.ts @@ -21,13 +21,10 @@ // SOFTWARE. import { has, merge, random, get } from 'lodash'; - import { CloudFunction, EventContext, Change, - https, - config, database, firestore, HttpsFunction, @@ -36,86 +33,21 @@ import { resetCache, } from 'firebase-functions/v1'; import { Expression } from 'firebase-functions/params'; + import { getEventFilters, getEventType, resolveStringExpression, } from './cloudevent/mocks/helpers'; - -/** Fields of the event context that can be overridden/customized. */ -export type EventContextOptions = { - /** ID of the event. If omitted, a random ID will be generated. */ - eventId?: string; - /** ISO time string of when the event occurred. If omitted, the current time is used. */ - timestamp?: string; - /** 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 }; - /** (Only for database functions and https.onCall.) Firebase auth variable representing the user that triggered - * the function. Defaults to null. - */ - auth?: any; - /** (Only for database and https.onCall functions.) The authentication state of the user that triggered the function. - * Default is 'UNAUTHENTICATED'. - */ - authType?: 'ADMIN' | 'USER' | 'UNAUTHENTICATED'; - - /** Resource is a standard format for defining a resource (google.rpc.context.AttributeContext.Resource). - * In Cloud Functions, it is the resource that triggered the function - such as a storage bucket. - */ - resource?: { - service: string; - name: string; - type?: string; - labels?: { - [tag: string]: string; - }; - }; -}; - -/** Fields of the callable context that can be overridden/customized. */ -export type CallableContextOptions = { - /** - * The result of decoding and verifying a Firebase AppCheck token. - */ - app?: any; - - /** - * The result of decoding and verifying a Firebase Auth ID token. - */ - auth?: any; - - /** - * 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 */ -export type ContextOptions = T extends HttpsFunction & Runnable - ? CallableContextOptions - : EventContextOptions; - -/** 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: T, - options?: ContextOptions -) => any | Promise; - -/** A scheduled function that can be called with optional override values for the event context. - * It will subsequently invoke the cloud function it wraps with a generated event context. - */ -export type WrappedScheduledFunction = ( - options?: ContextOptions -) => any | Promise; +import { + ContextOptions, + CallableContextOptions, + EventContextOptions, + WrappedFunction, + WrappedScheduledFunction, +} from './types/v1Types'; + +export { EventContextOptions, ContextOptions, CallableContextOptions, WrappedFunction, WrappedScheduledFunction }; /** Takes a cloud function to be tested, and returns a WrappedFunction which can be called in test code. */ export function wrapV1( diff --git a/src/v2.ts b/src/v2.ts index e3fef18..0b908b0 100644 --- a/src/v2.ts +++ b/src/v2.ts @@ -25,18 +25,9 @@ import { CallableFunction, CallableRequest } from 'firebase-functions/v2/https'; import { generateCombinedCloudEvent } from './cloudevent/generate'; import { DeepPartial } from './cloudevent/types'; -import * as express from 'express'; +import { WrappedV2CallableFunction, WrappedV2Function } from './types/v2Types'; -/** 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} - */ -export type WrappedV2Function> = ( - cloudEventPartial?: DeepPartial -) => any | Promise; - -export type WrappedV2CallableFunction = ( - data: CallableRequest -) => T | Promise; +export { WrappedV2Function, WrappedV2CallableFunction }; function isCallableV2Function>( cf: CloudFunction | CallableFunction