From 0744ecb9b84119fa991500564ccd92d6d714ff32 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 25 Jul 2022 16:17:49 -0700 Subject: [PATCH 1/6] Fix integration test. Since we aren't allowed to run public functions on internal GCP project, we pass id tokens when making calls to callable/http functions. --- integration_test/firebase.json | 5 +++ integration_test/functions/src/index.ts | 52 +++++++++++++----------- integration_test/functions/tsconfig.json | 2 +- integration_test/run_tests.sh | 4 +- package.json | 2 +- 5 files changed, 37 insertions(+), 28 deletions(-) diff --git a/integration_test/firebase.json b/integration_test/firebase.json index ce496e265..c3909bfe3 100644 --- a/integration_test/firebase.json +++ b/integration_test/firebase.json @@ -5,5 +5,10 @@ "firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json" + }, + "functions": { + "predeploy": [ + "npm --prefix \"$RESOURCE_DIR\" run build" + ] } } diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index e8c6918a5..6cff763b4 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -1,4 +1,5 @@ import { PubSub } from '@google-cloud/pubsub'; +import { GoogleAuth } from "google-auth-library"; import { Request, Response } from 'express'; import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; @@ -22,17 +23,18 @@ const firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG); admin.initializeApp(); async function callHttpsTrigger(name: string, data: any) { - const resp = await fetch( - `https://${REGION}-${firebaseConfig.projectId}.cloudfunctions.net/${name}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data }), - } - ); - if (!resp.ok) { + const url = `https://${REGION}-${firebaseConfig.projectId}.cloudfunctions.net/${name}`; + const client = await new GoogleAuth().getIdTokenClient("32555940559.apps.googleusercontent.com"); + const resp = await client.request({ + url, + method: "POST", + headers:{ + 'Content-Type': 'application/json', + }, + body: JSON.stringify({data}) + + }) + if (resp.status > 200) { throw Error(resp.statusText); } } @@ -42,7 +44,7 @@ async function callV2HttpsTrigger( data: any, accessToken: string ) { - let resp = await fetch( + const resp0 = await fetch( `https://cloudfunctions.googleapis.com/v2beta/projects/${firebaseConfig.projectId}/locations/${REGION}/functions/${name}`, { headers: { @@ -50,23 +52,27 @@ async function callV2HttpsTrigger( }, } ); - if (!resp.ok) { - throw new Error(resp.statusText); + if (!resp0.ok) { + throw new Error(resp0.statusText); } - const fn = await resp.json(); + const fn = await resp0.json(); const uri = fn.serviceConfig?.uri; if (!uri) { throw new Error(`Cannot call v2 https trigger ${name} - no uri found`); } - resp = await fetch(uri, { - method: 'POST', - headers: { + + const client = await new GoogleAuth().getIdTokenClient("32555940559.apps.googleusercontent.com"); + const resp1 = await client.request({ + url: uri, + method: "POST", + headers:{ 'Content-Type': 'application/json', }, - body: JSON.stringify({ data }), - }); - if (!resp.ok) { - throw new Error(resp.statusText); + body: JSON.stringify({data}) + + }) + if (resp1.status > 200) { + throw Error(resp1.statusText); } } @@ -233,4 +239,4 @@ export const integrationTests: any = functions } finally { testIdRef.off('child_added'); } - }); + }); \ No newline at end of file diff --git a/integration_test/functions/tsconfig.json b/integration_test/functions/tsconfig.json index a5bd57033..77fb279d5 100644 --- a/integration_test/functions/tsconfig.json +++ b/integration_test/functions/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "lib": ["es6", "dom"], "module": "commonjs", - "target": "es6", + "target": "es2020", "noImplicitAny": false, "outDir": "lib", "declaration": true, diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index 814ddc492..9098c912c 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -74,8 +74,6 @@ function delete_all_functions { } function deploy { - cd "${DIR}" - ./functions/node_modules/.bin/tsc -p functions/ # Deploy functions, and security rules for database and Firestore. If the deploy fails, retry twice if [[ "${TOKEN}" == "" ]]; then for i in 1 2 3; do firebase deploy --project="${PROJECT_ID}" --only functions,database,firestore && break; done @@ -97,7 +95,7 @@ function run_tests { TEST_URL="https://${FIREBASE_FUNCTIONS_TEST_REGION}-${PROJECT_ID}.${TEST_DOMAIN}/integrationTests" echo "${TEST_URL}" - curl --fail "${TEST_URL}" + curl --fail -H "Authorization: Bearer $(gcloud auth print-identity-token)" "${TEST_URL}" } function cleanup { diff --git a/package.json b/package.json index 000d85096..e6c277745 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ "docgen:v2:toc": "ts-node docgen/toc.ts --input docgen/v2/markdown --output docgen/v2/markdown/toc --path /docs/functions/beta/reference", "docgen:v2:gen": "api-documenter-fire markdown -i docgen/v2 -o docgen/v2/markdown && npm run docgen:v2:toc", "docgen:v2": "npm run build && npm run docgen:v2:extract && npm run docgen:v2:gen", - "build:pack": "rm -rf lib && npm install --production && tsc -p tsconfig.release.json && npm pack", + "build:pack": "rm -rf lib && npm install && tsc -p tsconfig.release.json && npm pack", "build:release": "npm ci --production && npm install --no-save typescript firebase-admin && tsc -p tsconfig.release.json", "build": "tsc -p tsconfig.release.json", "build:watch": "npm run build -- -w", From a8a29fe7ca5080b85b82f81c8a386470724b30e4 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 25 Jul 2022 16:23:22 -0700 Subject: [PATCH 2/6] Prettier. --- integration_test/firebase.json | 4 +--- integration_test/functions/src/index.ts | 30 +++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/integration_test/firebase.json b/integration_test/firebase.json index c3909bfe3..382d79c84 100644 --- a/integration_test/firebase.json +++ b/integration_test/firebase.json @@ -7,8 +7,6 @@ "indexes": "firestore.indexes.json" }, "functions": { - "predeploy": [ - "npm --prefix \"$RESOURCE_DIR\" run build" - ] + "predeploy": ["npm --prefix \"$RESOURCE_DIR\" run build"] } } diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 6cff763b4..caf716d09 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -1,5 +1,5 @@ import { PubSub } from '@google-cloud/pubsub'; -import { GoogleAuth } from "google-auth-library"; +import { GoogleAuth } from 'google-auth-library'; import { Request, Response } from 'express'; import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; @@ -24,16 +24,17 @@ admin.initializeApp(); async function callHttpsTrigger(name: string, data: any) { const url = `https://${REGION}-${firebaseConfig.projectId}.cloudfunctions.net/${name}`; - const client = await new GoogleAuth().getIdTokenClient("32555940559.apps.googleusercontent.com"); + const client = await new GoogleAuth().getIdTokenClient( + '32555940559.apps.googleusercontent.com' + ); const resp = await client.request({ url, - method: "POST", - headers:{ + method: 'POST', + headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({data}) - - }) + body: JSON.stringify({ data }), + }); if (resp.status > 200) { throw Error(resp.statusText); } @@ -61,16 +62,17 @@ async function callV2HttpsTrigger( throw new Error(`Cannot call v2 https trigger ${name} - no uri found`); } - const client = await new GoogleAuth().getIdTokenClient("32555940559.apps.googleusercontent.com"); + const client = await new GoogleAuth().getIdTokenClient( + '32555940559.apps.googleusercontent.com' + ); const resp1 = await client.request({ url: uri, - method: "POST", - headers:{ + method: 'POST', + headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({data}) - - }) + body: JSON.stringify({ data }), + }); if (resp1.status > 200) { throw Error(resp1.statusText); } @@ -239,4 +241,4 @@ export const integrationTests: any = functions } finally { testIdRef.off('child_added'); } - }); \ No newline at end of file + }); From 620b74f8aababfc780c4db1fe52d75aa0761fa08 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Tue, 26 Jul 2022 12:41:39 -0700 Subject: [PATCH 3/6] Use clearer variable names. --- integration_test/functions/src/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index caf716d09..9264616dc 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -45,7 +45,7 @@ async function callV2HttpsTrigger( data: any, accessToken: string ) { - const resp0 = await fetch( + const getFnResp = await fetch( `https://cloudfunctions.googleapis.com/v2beta/projects/${firebaseConfig.projectId}/locations/${REGION}/functions/${name}`, { headers: { @@ -53,10 +53,10 @@ async function callV2HttpsTrigger( }, } ); - if (!resp0.ok) { - throw new Error(resp0.statusText); + if (!getFnResp.ok) { + throw new Error(getFnResp.statusText); } - const fn = await resp0.json(); + const fn = await getFnResp.json(); const uri = fn.serviceConfig?.uri; if (!uri) { throw new Error(`Cannot call v2 https trigger ${name} - no uri found`); @@ -65,7 +65,7 @@ async function callV2HttpsTrigger( const client = await new GoogleAuth().getIdTokenClient( '32555940559.apps.googleusercontent.com' ); - const resp1 = await client.request({ + const invokeFnREsp = await client.request({ url: uri, method: 'POST', headers: { @@ -73,8 +73,8 @@ async function callV2HttpsTrigger( }, body: JSON.stringify({ data }), }); - if (resp1.status > 200) { - throw Error(resp1.statusText); + if (invokeFnREsp.status > 200) { + throw Error(invokeFnREsp.statusText); } } From 4e4c4b7af3db74a5d76c5c01601575f15b1d0a21 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Tue, 26 Jul 2022 14:44:30 -0700 Subject: [PATCH 4/6] Make functions private. --- integration_test/functions/src/index.ts | 1 + .../functions/src/v1/https-tests.ts | 17 ++++++++++------- .../functions/src/v2/https-tests.ts | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 9264616dc..ea2a6ece3 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -178,6 +178,7 @@ export const integrationTests: any = functions .region(REGION) .runWith({ timeoutSeconds: 540, + invoker: "private" }) .https.onRequest(async (req: Request, resp: Response) => { const testId = admin diff --git a/integration_test/functions/src/v1/https-tests.ts b/integration_test/functions/src/v1/https-tests.ts index 8acf8932f..bce05f341 100644 --- a/integration_test/functions/src/v1/https-tests.ts +++ b/integration_test/functions/src/v1/https-tests.ts @@ -2,10 +2,13 @@ import * as functions from 'firebase-functions'; import { REGION } from '../region'; import { expectEq, TestSuite } from '../testing'; -export const callableTests: any = functions.region(REGION).https.onCall((d) => { - return new TestSuite('https onCall') - .it('should have the correct data', (data: any) => - expectEq(data?.foo, 'bar') - ) - .run(d.testId, d); -}); +export const callableTests: any = functions + .runWith({ invoker: 'private' }) + .region(REGION) + .https.onCall((d) => { + return new TestSuite('https onCall') + .it('should have the correct data', (data: any) => + expectEq(data?.foo, 'bar') + ) + .run(d.testId, d); + }); diff --git a/integration_test/functions/src/v2/https-tests.ts b/integration_test/functions/src/v2/https-tests.ts index 4936c48ea..448464ef0 100644 --- a/integration_test/functions/src/v2/https-tests.ts +++ b/integration_test/functions/src/v2/https-tests.ts @@ -1,7 +1,7 @@ import { onCall } from 'firebase-functions/v2/https'; import { expectEq, TestSuite } from '../testing'; -export const callabletests = onCall((req) => { +export const callabletests = onCall({ invoker: 'private' }, (req) => { return new TestSuite('v2 https onCall') .it('should have the correct data', (data: any) => expectEq(data?.foo, 'bar') From 534359d97a80d648b24a8a1a78f23e41b2f1a6e0 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Tue, 26 Jul 2022 14:46:01 -0700 Subject: [PATCH 5/6] Update README.md. --- integration_test/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/integration_test/README.md b/integration_test/README.md index c6a0ca32d..5853cfa99 100644 --- a/integration_test/README.md +++ b/integration_test/README.md @@ -8,6 +8,14 @@ Run the integration test as follows: ./run_tests.sh [] ``` -If just one project_id is provided, the both the node6 and node8 tests will be run on that project, in series. If two project_ids are provided, the node6 tests will be run on the first project and the node8 tests will be run on the second one, in parallel. +Test runs cycles of testing, once for Node.js 14 and another for Node.js 16. -The tests run fully automatically, and will print the result on standard out. The integration test for HTTPS is that it properly kicks off other integration tests and returns a result. From there the other integration test suites will write their results back to the database, where you can check the detailed results if you'd like. +Test uses locally installed firebase to invoke commands for deploying function. The test also requires that you have +gcloud CLI installed and authenticated (`gcloud auth login`). + +Integration test is triggered by invoking HTTP function integrationTest which in turns invokes each function trigger +by issuing actions necessary to trigger it (e.g. write to storage bucket). + +### Debugging +The status and result of each test is stored in RTDB of the project used for testing. You can also inspect Cloud Logging +for more clues. From 30a47581356f2aba678b3ffff45005100313bd11 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Tue, 26 Jul 2022 14:48:50 -0700 Subject: [PATCH 6/6] Prettier. --- integration_test/README.md | 1 + integration_test/functions/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/integration_test/README.md b/integration_test/README.md index 5853cfa99..3b0f5413f 100644 --- a/integration_test/README.md +++ b/integration_test/README.md @@ -17,5 +17,6 @@ Integration test is triggered by invoking HTTP function integrationTest which in by issuing actions necessary to trigger it (e.g. write to storage bucket). ### Debugging + The status and result of each test is stored in RTDB of the project used for testing. You can also inspect Cloud Logging for more clues. diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index ea2a6ece3..3a5fd8474 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -178,7 +178,7 @@ export const integrationTests: any = functions .region(REGION) .runWith({ timeoutSeconds: 540, - invoker: "private" + invoker: 'private', }) .https.onRequest(async (req: Request, resp: Response) => { const testId = admin