From bc6886eadab75c7f18e3e9c2bcc886ec3b7f714c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Sun, 1 Dec 2024 17:34:09 -0300 Subject: [PATCH] feat(firebase): ensure support to v5 and v6 sdk --- package-lock.json | 122 +++++++++++++----- package.json | 4 +- .../http-firebase-v2.sdk-v5.handler.spec.ts | 119 +++++++++++++++++ .../http-firebase-v2.sdk-v6.handler.spec.ts | 119 +++++++++++++++++ 4 files changed, 334 insertions(+), 30 deletions(-) create mode 100644 test/handlers/http-firebase-v2.sdk-v5.handler.spec.ts create mode 100644 test/handlers/http-firebase-v2.sdk-v6.handler.spec.ts diff --git a/package-lock.json b/package-lock.json index 78e43ebe..593b743e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@deepkit/framework": "1.0.1-alpha.99", "@deepkit/http": "1.0.1-alpha.98", "@deepkit/workflow": "1.0.1-alpha.97", - "@google-cloud/functions-framework": "3.3.0", + "@google-cloud/functions-framework": "3.4.2", "@hapi/hapi": "21.3.2", "@microsoft/api-documenter": "7.23.15", "@microsoft/api-extractor": "7.39.0", @@ -53,6 +53,8 @@ "fastify-v5": "npm:fastify@5", "firebase-admin": "11.11.1", "firebase-functions": "4.5.0", + "firebase-functions-v5": "npm:firebase-functions@5", + "firebase-functions-v6": "npm:firebase-functions@6", "glob": "10.3.10", "husky": "8.0.3", "koa": "2.15.0", @@ -1923,14 +1925,15 @@ } }, "node_modules/@google-cloud/functions-framework": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.3.0.tgz", - "integrity": "sha512-+4O1dX5VNRK1W1NyAia7zy5jLf88ytuz39/1kVUUaNiOf76YbMZKV0YjZwfk7uEwRrC6l2wynK1G+q8Gb5DeVw==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-3.4.2.tgz", + "integrity": "sha512-yJcxfVgjLoKFO3p6Wy6Fc+Gi6l3PFSwJg4m0mjebx/UHdLeXLYYxgKMP8RCODaApXEWXbSITIjXO0m5kSv2Ilw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@types/express": "4.17.17", + "@types/express": "4.17.21", "body-parser": "^1.18.3", - "cloudevents": "^7.0.0", + "cloudevents": "^8.0.0", "express": "^4.16.4", "minimist": "^1.2.7", "on-finished": "^2.3.0", @@ -1945,18 +1948,6 @@ "node": ">=10.0.0" } }, - "node_modules/@google-cloud/functions-framework/node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, "node_modules/@google-cloud/paginator": { "version": "3.0.7", "dev": true, @@ -5564,10 +5555,11 @@ } }, "node_modules/cloudevents": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-7.0.2.tgz", - "integrity": "sha512-WiOqWsNkMZmMMZ6xa3kzx/MA+8+V+c5eGkStZIcik+Px2xCobmzcacw1EOGyfhODaQKkIv8TxXOOLzV69oXFqA==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-8.0.2.tgz", + "integrity": "sha512-93KKRR61D2NNE+2lg2HmLbl17beVTKpf1UYd/8BcXpuiDxbU2fb8gAfriSmVGmj1xX/Oh2t5Fh/xGOWFdu6F4A==", "dev": true, + "license": "Apache-2.0", "dependencies": { "ajv": "^8.11.0", "ajv-formats": "^2.1.1", @@ -5577,30 +5569,39 @@ "uuid": "^8.3.2" }, "engines": { - "node": ">=16 <=20" + "node": ">=16 <=22" } }, "node_modules/cloudevents/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/cloudevents/node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/cloudevents/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/co": { "version": "4.6.0", @@ -8575,6 +8576,66 @@ "firebase-admin": "^10.0.0 || ^11.0.0" } }, + "node_modules/firebase-functions-v5": { + "name": "firebase-functions", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.1.tgz", + "integrity": "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^11.10.0 || ^12.0.0" + } + }, + "node_modules/firebase-functions-v5/node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "node_modules/firebase-functions-v6": { + "name": "firebase-functions", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.1.1.tgz", + "integrity": "sha512-q+4zsQhX04YJUz6hqaiH/j5kixljPj0PMxkm8KN3juYp3I4NC6CZ4qfy5JRfwvV8VfXM2KkJrZuyJtLyZr97aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "^4.17.21", + "cors": "^2.8.5", + "express": "^4.21.0", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^11.10.0 || ^12.0.0 || ^13.0.0" + } + }, "node_modules/firebase-functions/node_modules/@types/express": { "version": "4.17.3", "dev": true, @@ -9599,6 +9660,7 @@ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -11706,6 +11768,7 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" } @@ -13875,6 +13938,7 @@ "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", diff --git a/package.json b/package.json index dcb84af6..0d1a4dd5 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@deepkit/framework": "1.0.1-alpha.99", "@deepkit/http": "1.0.1-alpha.98", "@deepkit/workflow": "1.0.1-alpha.97", - "@google-cloud/functions-framework": "3.3.0", + "@google-cloud/functions-framework": "3.4.2", "@hapi/hapi": "21.3.2", "@microsoft/api-documenter": "7.23.15", "@microsoft/api-extractor": "7.39.0", @@ -122,6 +122,8 @@ "fastify-v5": "npm:fastify@5", "firebase-admin": "11.11.1", "firebase-functions": "4.5.0", + "firebase-functions-v5": "npm:firebase-functions@5", + "firebase-functions-v6": "npm:firebase-functions@6", "glob": "10.3.10", "husky": "8.0.3", "koa": "2.15.0", diff --git a/test/handlers/http-firebase-v2.sdk-v5.handler.spec.ts b/test/handlers/http-firebase-v2.sdk-v5.handler.spec.ts new file mode 100644 index 00000000..792893d5 --- /dev/null +++ b/test/handlers/http-firebase-v2.sdk-v5.handler.spec.ts @@ -0,0 +1,119 @@ +import type { HttpsOptions } from 'firebase-functions/v2/https'; +import { describe, expect, it, vitest } from 'vitest'; +import { + type FrameworkContract, + ServerlessRequest, + ServerlessResponse, + waitForStreamComplete, +} from '../../src'; +import { HttpFirebaseV2Handler } from '../../src/handlers/firebase'; +import { FrameworkMock } from '../mocks/framework.mock'; + +vitest.mock('firebase-functions/v2', async () => { + // eslint-disable-next-line import/no-unresolved + return await import('firebase-functions-v5/v2'); +}); + +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 = { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + sendRequest: vitest.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 = vitest.spyOn(factory, 'onRequestWithOptions' as any); + + factory.getHandler(null, new FrameworkMock(200, {})); + + expect(spyMethod).toHaveBeenCalledWith(options, expect.any(Function)); + }); +}); diff --git a/test/handlers/http-firebase-v2.sdk-v6.handler.spec.ts b/test/handlers/http-firebase-v2.sdk-v6.handler.spec.ts new file mode 100644 index 00000000..7a1e26de --- /dev/null +++ b/test/handlers/http-firebase-v2.sdk-v6.handler.spec.ts @@ -0,0 +1,119 @@ +import type { HttpsOptions } from 'firebase-functions/v2/https'; +import { describe, expect, it, vitest } from 'vitest'; +import { + type FrameworkContract, + ServerlessRequest, + ServerlessResponse, + waitForStreamComplete, +} from '../../src'; +import { HttpFirebaseV2Handler } from '../../src/handlers/firebase'; +import { FrameworkMock } from '../mocks/framework.mock'; + +vitest.mock('firebase-functions/v2', async () => { + // eslint-disable-next-line import/no-unresolved + return await import('firebase-functions-v6/v2'); +}); + +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 = { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + sendRequest: vitest.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 = vitest.spyOn(factory, 'onRequestWithOptions' as any); + + factory.getHandler(null, new FrameworkMock(200, {})); + + expect(spyMethod).toHaveBeenCalledWith(options, expect.any(Function)); + }); +});