From 90eba3f2f6f516f617eed0d96c082e5fbe462fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Wed, 4 Jan 2023 20:07:24 -0300 Subject: [PATCH] feat(firebase-v2): added support for gen2 of firebase functions --- .../firebase/http-firebase-v2.handler.ts | 68 ++++++++++ .../firebase/http-firebase.handler.ts | 2 +- src/handlers/firebase/index.ts | 1 + .../handlers/http-firebase-v2.handler.spec.ts | 128 ++++++++++++++++++ 4 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 src/handlers/firebase/http-firebase-v2.handler.ts create mode 100644 test/handlers/http-firebase-v2.handler.spec.ts diff --git a/src/handlers/firebase/http-firebase-v2.handler.ts b/src/handlers/firebase/http-firebase-v2.handler.ts new file mode 100644 index 00000000..5c16da65 --- /dev/null +++ b/src/handlers/firebase/http-firebase-v2.handler.ts @@ -0,0 +1,68 @@ +//#region Imports + +// eslint-disable-next-line import/no-unresolved +import { https } from 'firebase-functions/v2'; +import { FrameworkContract, HandlerContract } from '../../contracts'; +import { RawRequest } from '../base'; + +//#endregion + +/** + * The class that implements a handler for Firebase Https Events + * + * @remarks Read more about Https Events {@link https://firebase.google.com/docs/functions/http-events | here} + * + * @breadcrumb Handlers / HttpFirebaseHandler + * @public + */ +export class HttpFirebaseV2Handler + extends RawRequest + implements + HandlerContract> +{ + //#region Constructor + + /** + * Construtor padrĂ£o + */ + constructor(protected readonly options?: https.HttpsOptions) { + super(); + } + + //#endregion + + //#region Public Methods + + /** + * {@inheritDoc} + */ + public getHandler( + app: TApp, + framework: FrameworkContract, + ): ReturnType['onRequestCallback']> { + if (this.options) { + return this.onRequestWithOptions( + this.options, + this.onRequestCallback(app, framework), + ); + } + + return https.onRequest(this.onRequestCallback(app, framework)); + } + + //#endregion + + //#region Protected Method + + /** + * Wrapper method around onRequest for better testability + */ + protected onRequestWithOptions( + options: https.HttpsOptions, + callback: ReturnType['onRequestCallback']>, + ): ReturnType['onRequestCallback']> { + return https.onRequest(options, callback); + } + + //#endregion +} diff --git a/src/handlers/firebase/http-firebase.handler.ts b/src/handlers/firebase/http-firebase.handler.ts index fa4f9c48..a619c4b1 100644 --- a/src/handlers/firebase/http-firebase.handler.ts +++ b/src/handlers/firebase/http-firebase.handler.ts @@ -3,7 +3,7 @@ import { IncomingMessage, ServerResponse } from 'http'; import { https } from 'firebase-functions'; import { FrameworkContract, HandlerContract } from '../../contracts'; -import { RawRequest } from '../base/index'; +import { RawRequest } from '../base'; //#endregion /** diff --git a/src/handlers/firebase/index.ts b/src/handlers/firebase/index.ts index fca7447f..3b27e17d 100644 --- a/src/handlers/firebase/index.ts +++ b/src/handlers/firebase/index.ts @@ -1 +1,2 @@ export * from './http-firebase.handler'; +export * from './http-firebase-v2.handler'; diff --git a/test/handlers/http-firebase-v2.handler.spec.ts b/test/handlers/http-firebase-v2.handler.spec.ts new file mode 100644 index 00000000..39556567 --- /dev/null +++ b/test/handlers/http-firebase-v2.handler.spec.ts @@ -0,0 +1,128 @@ +import { HttpsOptions } from 'firebase-functions/lib/v2/providers/https'; +import { + FrameworkContract, + ServerlessRequest, + ServerlessResponse, + waitForStreamComplete, +} from '../../src'; +import { HttpFirebaseV2Handler } from '../../src/handlers/firebase/http-firebase-v2.handler'; +import { FrameworkMock } from '../mocks/framework.mock'; + +jest.mock('firebase-admin', () => { + const packages = { + '12.x': 'firebase-admin-8', + latest: 'firebase-admin', + }; + const version = process.env.TEST_NODE_VERSION || 'latest'; + + // Require the original module. + const originalModule = jest.requireActual(packages[version]); + + return { + __esModule: true, + ...originalModule, + }; +}); + +describe(HttpFirebaseV2Handler.name, () => { + it('should forward correctly the request to framework', async () => { + const handlerFactory = new HttpFirebaseV2Handler(); + + const method = 'POST'; + const url = '/users/batata'; + const headers = { 'Content-Type': 'application/json' }; + const remoteAddress = '168.16.0.1'; + const body = Buffer.from('{"test": true}', 'utf-8'); + + const request = new ServerlessRequest({ + method, + url, + headers, + remoteAddress, + body, + }); + + const response = new ServerlessResponse({ + method, + }); + + const responseBody = { batata: true }; + const responseStatus = 200; + const framework = new FrameworkMock(responseStatus, responseBody); + + const handler = handlerFactory.getHandler(null, framework); + + handler(request, response); + + await waitForStreamComplete(response); + + expect(response.statusCode).toBe(responseStatus); + expect(ServerlessResponse.body(response).toString()).toStrictEqual( + JSON.stringify(responseBody), + ); + }); + + it('should handle weird body types', () => { + const handlerFactory = new HttpFirebaseV2Handler(); + + const method = 'POST'; + const url = '/users/batata'; + const headers = { 'Content-Type': 'application/json' }; + const remoteAddress = '168.16.0.1'; + const options = [{ potato: true }, [{ test: true }]]; + + for (const option of options) { + const request = new ServerlessRequest({ + method, + url, + headers, + remoteAddress, + body: option as any, + }); + + const response = new ServerlessResponse({ + method, + }); + + const framework: FrameworkContract = { + sendRequest: jest.fn( + async ( + app: null, + req: ServerlessRequest, + res: ServerlessResponse, + ) => { + expect(req.body?.toString()).toEqual(JSON.stringify(option)); + expect(req.headers['content-length']).toEqual( + Buffer.byteLength(JSON.stringify(option)).toString(), + ); + + req.pipe(res); + + await waitForStreamComplete(res); + + expect(ServerlessResponse.body(res).toString()).toEqual( + JSON.stringify(option), + ); + }, + ), + }; + + const handler = handlerFactory.getHandler(null, framework); + + handler(request, response); + } + }); + + it('should forward the properties to https.onRequest', () => { + const options: HttpsOptions = { + concurrency: 400, + }; + const factory = new HttpFirebaseV2Handler(options); + + const spyMethod = jest.spyOn(factory, 'onRequestWithOptions' as any); + + factory.getHandler(null, new FrameworkMock(200, {})); + + expect(spyMethod).toHaveBeenCalledWith(options, expect.any(Function)); + }); +});