From cd180175911d83281bb776316eca6ed4e07fd91b Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Mon, 13 May 2019 11:39:46 -0700 Subject: [PATCH 01/11] Adds body-parser for #1277 and #1273 --- package.json | 2 + src/emulator/functionsEmulatorRuntime.ts | 22 +- .../functionsEmulatorRuntime.spec.ts | 188 +++++++++++++++++- 3 files changed, 194 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 47c955cc716..2c5a79c29c7 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "dependencies": { "JSONStream": "^1.2.1", "archiver": "^2.1.1", + "body-parser": "^1.19.0", "chokidar": "^2.1.5", "cjson": "^0.3.1", "cli-color": "^1.2.0", @@ -103,6 +104,7 @@ "winston": "^1.0.1" }, "devDependencies": { + "@types/body-parser": "^1.17.0", "@types/chai": "^4.1.6", "@types/chai-as-promised": "^7.1.0", "@types/cli-color": "^0.3.29", diff --git a/src/emulator/functionsEmulatorRuntime.ts b/src/emulator/functionsEmulatorRuntime.ts index 1117443acc6..ff359a431b8 100644 --- a/src/emulator/functionsEmulatorRuntime.ts +++ b/src/emulator/functionsEmulatorRuntime.ts @@ -9,13 +9,13 @@ import { FunctionsRuntimeFeatures, getEmulatedTriggersFromDefinitions, getTemporarySocketPath, - waitForBody, } from "./functionsEmulatorShared"; import * as express from "express"; import { extractParamsFromPath } from "./functionsEmulatorUtils"; import { spawnSync } from "child_process"; import * as path from "path"; import * as admin from "firebase-admin"; +import * as bodyParser from "body-parser"; let app: admin.app.App; let adminModuleProxy: typeof admin; @@ -536,27 +536,17 @@ async function ProcessHTTPS(frb: FunctionsRuntimeBundle, trigger: EmulatedTrigge resolveEphemeralServer(); }); - // Read data and manually set the request body - const dataStr = await waitForBody(req); - if (dataStr && dataStr.length > 0) { - if (req.is("application/json")) { - new EmulatorLog( - "DEBUG", - "runtime-status", - `Detected JSON request body: ${dataStr}` - ).log(); - req.body = JSON.parse(dataStr); - } else { - req.body = dataStr; - } - } - await Run([req, res], func); } catch (err) { rejectEphemeralServer(err); } }; + ephemeralServer.use(bodyParser.json({})); + ephemeralServer.use(bodyParser.text({})); + ephemeralServer.use(bodyParser.urlencoded({ extended: true })); + ephemeralServer.use(bodyParser.raw({ type: "*/*" })); + ephemeralServer.get("/*", handler); ephemeralServer.post("/*", handler); diff --git a/src/test/emulators/functionsEmulatorRuntime.spec.ts b/src/test/emulators/functionsEmulatorRuntime.spec.ts index 2864d187f82..d5af90f5053 100644 --- a/src/test/emulators/functionsEmulatorRuntime.spec.ts +++ b/src/test/emulators/functionsEmulatorRuntime.spec.ts @@ -428,13 +428,12 @@ describe("FunctionsEmulatorRuntime", () => { describe("Runtime", () => { describe("HTTPS", () => { - it("should handle a single invocation", async () => { + it("should handle a GET request", async () => { const serializedTriggers = (() => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions").https.onRequest( async (req: any, res: any) => { - /* tslint:disable:no-console */ res.json({ from_trigger: true }); } ), @@ -466,6 +465,191 @@ describe("FunctionsEmulatorRuntime", () => { await runtime.exit; }).timeout(TIMEOUT_MED); + + it("should handle a POST request with form data", async () => { + const serializedTriggers = (() => { + require("firebase-admin").initializeApp(); + return { + function_id: require("firebase-functions").https.onRequest( + async (req: any, res: any) => { + res.json(req.body); + } + ), + }; + }).toString(); + + const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onRequest, { + serializedTriggers, + }); + + await runtime.ready; + + await new Promise((resolve) => { + const reqData = "name=sparky"; + const req = request( + { + socketPath: runtime.metadata.socketPath, + path: "/", + method: "post", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Content-Length": reqData.length, + }, + }, + (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + expect(JSON.parse(data)).to.deep.equal({ name: "sparky" }); + resolve(); + }); + } + ); + req.write(reqData); + req.end(); + }); + + await runtime.exit; + }).timeout(TIMEOUT_MED); + + it("should handle a POST request with JSON data", async () => { + const serializedTriggers = (() => { + require("firebase-admin").initializeApp(); + return { + function_id: require("firebase-functions").https.onRequest( + async (req: any, res: any) => { + res.json(req.body); + } + ), + }; + }).toString(); + + const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onRequest, { + serializedTriggers, + }); + + await runtime.ready; + + await new Promise((resolve) => { + const reqData = '{"name": "sparky"}'; + const req = request( + { + socketPath: runtime.metadata.socketPath, + path: "/", + method: "post", + headers: { + "Content-Type": "application/json", + "Content-Length": reqData.length, + }, + }, + (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + expect(JSON.parse(data)).to.deep.equal({ name: "sparky" }); + resolve(); + }); + } + ); + req.write(reqData); + req.end(); + }); + + await runtime.exit; + }).timeout(TIMEOUT_MED); + + it("should handle a POST request with text data", async () => { + const serializedTriggers = (() => { + require("firebase-admin").initializeApp(); + return { + function_id: require("firebase-functions").https.onRequest( + async (req: any, res: any) => { + res.json(req.body); + } + ), + }; + }).toString(); + + const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onRequest, { + serializedTriggers, + }); + + await runtime.ready; + + await new Promise((resolve) => { + const reqData = "name is sparky"; + const req = request( + { + socketPath: runtime.metadata.socketPath, + path: "/", + method: "post", + headers: { + "Content-Type": "text/plain", + "Content-Length": reqData.length, + }, + }, + (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + expect(JSON.parse(data)).to.deep.equal("name is sparky"); + resolve(); + }); + } + ); + req.write(reqData); + req.end(); + }); + + await runtime.exit; + }).timeout(TIMEOUT_MED); + + it("should handle a POST request with any other type", async () => { + const serializedTriggers = (() => { + require("firebase-admin").initializeApp(); + return { + function_id: require("firebase-functions").https.onRequest( + async (req: any, res: any) => { + res.json(req.body); + } + ), + }; + }).toString(); + + const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onRequest, { + serializedTriggers, + }); + + await runtime.ready; + + await new Promise((resolve) => { + const reqData = "name is sparky"; + const req = request( + { + socketPath: runtime.metadata.socketPath, + path: "/", + method: "post", + headers: { + "Content-Type": "gibber/ish", + "Content-Length": reqData.length, + }, + }, + (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + expect(JSON.parse(data).type).to.deep.equal("Buffer"); + expect(JSON.parse(data).data.length).to.deep.equal(14); + resolve(); + }); + } + ); + req.write(reqData); + req.end(); + }); + + await runtime.exit; + }).timeout(TIMEOUT_MED); }); describe("Cloud Firestore", () => { From ae119ab82a6c33cf6e6a0f3b4402e9a2fb0fb087 Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Mon, 13 May 2019 12:41:51 -0700 Subject: [PATCH 02/11] Refactor FunctionsEmulator for easier unit testing --- src/emulator/functionsEmulator.ts | 128 ++++++++++-------- .../functionsEmulatorRuntime.spec.ts | 7 + 2 files changed, 77 insertions(+), 58 deletions(-) diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index f401fb10db6..2d0640a316f 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -61,31 +61,10 @@ export class FunctionsEmulator implements EmulatorInstance { return `http://localhost:${port}/${projectId}/${region}/${name}`; } - private readonly port: number; - private readonly projectId: string = ""; - - private server?: http.Server; - private firebaseConfig: any; - private functionsDir: string = ""; - private nodeBinary: string = ""; - private knownTriggerIDs: { [triggerId: string]: boolean } = {}; - - constructor(private options: any, private args: FunctionsEmulatorArgs) { - this.port = this.args.port || Constants.getDefaultPort(Emulators.FUNCTIONS); - this.projectId = getProjectId(this.options, false); - } - - async start(): Promise { - this.functionsDir = path.join( - this.options.config.projectDir, - this.options.config.get("functions.source") - ); - - this.nodeBinary = await askInstallNodeVersion(this.functionsDir); - - // TODO: This call requires authentication, which we should remove eventually - this.firebaseConfig = await functionsConfig.getFirebaseConfig(this.options); - + static createHubServer( + bundleTemplate: FunctionsRuntimeBundle, + nodeBinary: string + ): express.Application { const hub = express(); hub.use((req, res, next) => { @@ -105,13 +84,7 @@ export class FunctionsEmulator implements EmulatorInstance { }); hub.get("/", async (req, res) => { - res.send( - JSON.stringify( - await getTriggersFromDirectory(this.projectId, this.functionsDir, this.firebaseConfig), - null, - 2 - ) - ); + res.json({ status: "alive" }); }); // The URL for the function that the other emulators (Firestore, etc) use. @@ -132,14 +105,19 @@ export class FunctionsEmulator implements EmulatorInstance { // Define a common handler function to use for GET and POST requests. const handler: express.RequestHandler = async (req, res) => { const method = req.method; - const triggerName = req.params.trigger_name; + const triggerId = req.params.trigger_name; - logger.debug(`[functions] ${method} request to function ${triggerName} accepted.`); + logger.debug(`[functions] ${method} request to function ${triggerId} accepted.`); const reqBody = (req as RequestWithRawBody).rawBody; const proto = reqBody ? JSON.parse(reqBody) : undefined; - const runtime = this.startFunctionRuntime(triggerName, proto); + const runtime = FunctionsEmulator.startFunctionRuntime( + bundleTemplate, + triggerId, + nodeBinary, + proto + ); runtime.events.on("log", (el: EmulatorLog) => { if (el.level === "FATAL") { @@ -158,8 +136,8 @@ export class FunctionsEmulator implements EmulatorInstance { const triggerLog = await triggerLogPromise; const triggerMap: EmulatedTriggerMap = triggerLog.data.triggers; - const trigger = triggerMap[triggerName]; - const isHttpsTrigger = trigger.definition.httpsTrigger ? true : false; + const trigger = triggerMap[triggerId]; + const isHttpsTrigger = !!trigger.definition.httpsTrigger; // Log each invocation and the service type. if (isHttpsTrigger) { @@ -248,27 +226,30 @@ export class FunctionsEmulator implements EmulatorInstance { hub.get(functionRoutes, handler); hub.post(functionRoutes, handler); - this.server = hub.listen(this.port); + return hub; } - startFunctionRuntime(triggerName: string, proto?: any): FunctionsRuntimeInstance { + static startFunctionRuntime( + bundleTemplate: FunctionsRuntimeBundle, + triggerId: string, + nodeBinary: string, + proto?: any + ): FunctionsRuntimeInstance { const runtimeBundle: FunctionsRuntimeBundle = { + ...bundleTemplate, ports: { firestore: EmulatorRegistry.getPort(Emulators.FIRESTORE), }, proto, - cwd: this.functionsDir, - triggerId: triggerName, - projectId: this.projectId, - disabled_features: this.args.disabledRuntimeFeatures, + triggerId, }; - const runtime = InvokeRuntime(this.nodeBinary, runtimeBundle); - runtime.events.on("log", this.handleRuntimeLog.bind(this)); + const runtime = InvokeRuntime(nodeBinary, runtimeBundle); + runtime.events.on("log", FunctionsEmulator.handleRuntimeLog.bind(this)); return runtime; } - handleSystemLog(systemLog: EmulatorLog): void { + static handleSystemLog(systemLog: EmulatorLog): void { switch (systemLog.type) { case "runtime-status": if (systemLog.text === "killed") { @@ -351,13 +332,13 @@ You can probably fix this by running "npm install ${ } } - handleRuntimeLog(log: EmulatorLog, ignore: string[] = []): void { + static handleRuntimeLog(log: EmulatorLog, ignore: string[] = []): void { if (ignore.indexOf(log.level) >= 0) { return; } switch (log.level) { case "SYSTEM": - this.handleSystemLog(log); + FunctionsEmulator.handleSystemLog(log); break; case "USER": logger.info(`${clc.blackBright("> ")} ${log.text}`); @@ -380,6 +361,45 @@ You can probably fix this by running "npm install ${ } } + private readonly port: number; + private readonly projectId: string = ""; + + private server?: http.Server; + private firebaseConfig: any; + private functionsDir: string = ""; + private nodeBinary: string = ""; + private knownTriggerIDs: { [triggerId: string]: boolean } = {}; + private bundleTemplate: FunctionsRuntimeBundle; + + constructor(private options: any, private args: FunctionsEmulatorArgs) { + this.port = this.args.port || Constants.getDefaultPort(Emulators.FUNCTIONS); + this.projectId = getProjectId(this.options, false); + + this.functionsDir = path.join( + this.options.config.projectDir, + this.options.config.get("functions.source") + ); + + this.bundleTemplate = { + cwd: this.functionsDir, + projectId: this.projectId, + triggerId: "", + ports: {}, + disabled_features: this.args.disabledRuntimeFeatures, + }; + } + + async start(): Promise { + this.nodeBinary = await askInstallNodeVersion(this.functionsDir); + + // TODO: This call requires authentication, which we should remove eventually + this.firebaseConfig = await functionsConfig.getFirebaseConfig(this.options); + + this.server = FunctionsEmulator.createHubServer(this.bundleTemplate, this.nodeBinary).listen( + this.port + ); + } + async connect(): Promise { utils.logLabeledBullet("functions", `Watching "${this.functionsDir}" for Cloud Functions...`); @@ -391,20 +411,12 @@ You can probably fix this by running "npm install ${ persistent: true, }); - const diagnosticBundle: FunctionsRuntimeBundle = { - cwd: this.functionsDir, - projectId: this.projectId, - triggerId: "", - ports: {}, - disabled_features: this.args.disabledRuntimeFeatures, - }; - // TODO(abehaskins): Gracefully handle removal of deleted function definitions const loadTriggers = async () => { - const runtime = InvokeRuntime(this.nodeBinary, diagnosticBundle); + const runtime = InvokeRuntime(this.nodeBinary, this.bundleTemplate); runtime.events.on("log", (el: EmulatorLog) => { - this.handleRuntimeLog(el); + FunctionsEmulator.handleRuntimeLog(el); }); const triggerParseEvent = await waitForLog(runtime.events, "SYSTEM", "triggers-parsed"); diff --git a/src/test/emulators/functionsEmulatorRuntime.spec.ts b/src/test/emulators/functionsEmulatorRuntime.spec.ts index d5af90f5053..d9649a5dcc8 100644 --- a/src/test/emulators/functionsEmulatorRuntime.spec.ts +++ b/src/test/emulators/functionsEmulatorRuntime.spec.ts @@ -5,6 +5,7 @@ import { request } from "http"; import { findModuleRoot, FunctionsRuntimeBundle } from "../../emulator/functionsEmulatorShared"; import { Change } from "firebase-functions"; import { DocumentSnapshot } from "firebase-functions/lib/providers/firestore"; +import * as supertest from "supertest"; const cwd = findModuleRoot("firebase-tools", __dirname); const FunctionRuntimeBundles = { @@ -426,6 +427,12 @@ describe("FunctionsEmulatorRuntime", () => { }); }); + describe("Hub", () => { + it("should route requests to /:project_id/:trigger_id to HTTPS Function", async () => { + // supertest() + }); + }); + describe("Runtime", () => { describe("HTTPS", () => { it("should handle a GET request", async () => { From c775e99a7f1e8a35cf6383234de172da5770b569 Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Mon, 13 May 2019 12:48:58 -0700 Subject: [PATCH 03/11] Break out emulator test fixtures --- src/test/emulators/fixtures.ts | 109 ++++++++++++++++ src/test/emulators/functionsEmulator.spec.ts | 8 ++ .../functionsEmulatorRuntime.spec.ts | 118 +----------------- 3 files changed, 119 insertions(+), 116 deletions(-) create mode 100644 src/test/emulators/fixtures.ts create mode 100644 src/test/emulators/functionsEmulator.spec.ts diff --git a/src/test/emulators/fixtures.ts b/src/test/emulators/fixtures.ts new file mode 100644 index 00000000000..23505175b73 --- /dev/null +++ b/src/test/emulators/fixtures.ts @@ -0,0 +1,109 @@ +import { findModuleRoot, FunctionsRuntimeBundle } from "../../emulator/functionsEmulatorShared"; + +const cwd = findModuleRoot("firebase-tools", __dirname); +export const FunctionRuntimeBundles = { + onCreate: { + ports: { + firestore: 8080, + }, + cwd, + proto: { + data: { + value: { + name: "projects/fake-project-id/databases/(default)/documents/test/test", + fields: { + when: { + timestampValue: "2019-04-15T16:55:48.150Z", + }, + }, + createTime: "2019-04-15T16:56:13.737Z", + updateTime: "2019-04-15T16:56:13.737Z", + }, + updateMask: {}, + }, + context: { + eventId: "7ebfb089-f549-4e1f-8312-fe843efc8be7", + timestamp: "2019-04-15T16:56:13.737Z", + eventType: "providers/cloud.firestore/eventTypes/document.create", + resource: { + name: "projects/fake-project-id/databases/(default)/documents/test/test", + service: "firestore.googleapis.com", + }, + }, + }, + triggerId: "function_id", + projectId: "fake-project-id", + } as FunctionsRuntimeBundle, + onWrite: { + ports: { + firestore: 8080, + }, + cwd, + proto: { + data: { + value: { + name: "projects/fake-project-id/databases/(default)/documents/test/test", + fields: { + when: { + timestampValue: "2019-04-15T16:55:48.150Z", + }, + }, + createTime: "2019-04-15T16:56:13.737Z", + updateTime: "2019-04-15T16:56:13.737Z", + }, + updateMask: {}, + }, + context: { + eventId: "7ebfb089-f549-4e1f-8312-fe843efc8be7", + timestamp: "2019-04-15T16:56:13.737Z", + eventType: "providers/cloud.firestore/eventTypes/document.write", + resource: { + name: "projects/fake-project-id/databases/(default)/documents/test/test", + service: "firestore.googleapis.com", + }, + }, + }, + triggerId: "function_id", + projectId: "fake-project-id", + } as FunctionsRuntimeBundle, + onDelete: { + ports: { + firestore: 8080, + }, + cwd, + proto: { + data: { + oldValue: { + name: "projects/fake-project-id/databases/(default)/documents/test/test", + fields: { + when: { + timestampValue: "2019-04-15T16:55:48.150Z", + }, + }, + createTime: "2019-04-15T16:56:13.737Z", + updateTime: "2019-04-15T16:56:13.737Z", + }, + updateMask: {}, + }, + context: { + eventId: "7ebfb089-f549-4e1f-8312-fe843efc8be7", + timestamp: "2019-04-15T16:56:13.737Z", + eventType: "providers/cloud.firestore/eventTypes/document.delete", + resource: { + name: "projects/fake-project-id/databases/(default)/documents/test/test", + service: "firestore.googleapis.com", + }, + }, + }, + triggerId: "function_id", + projectId: "fake-project-id", + } as FunctionsRuntimeBundle, + onRequest: { + ports: { + firestore: 8080, + }, + cwd, + triggerId: "function_id", + projectId: "fake-project-id", + } as FunctionsRuntimeBundle, +}; \ No newline at end of file diff --git a/src/test/emulators/functionsEmulator.spec.ts b/src/test/emulators/functionsEmulator.spec.ts new file mode 100644 index 00000000000..4f59b0543a4 --- /dev/null +++ b/src/test/emulators/functionsEmulator.spec.ts @@ -0,0 +1,8 @@ +import { FunctionsEmulator } from "../../emulator/functionsEmulator"; +import * as supertest from "supertest"; + +describe.only("Hub", () => { + it("should route requests to /:project_id/:trigger_id to HTTPS Function", async () => { + supertest(FunctionsEmulator.createHubServer()) + }); +}); diff --git a/src/test/emulators/functionsEmulatorRuntime.spec.ts b/src/test/emulators/functionsEmulatorRuntime.spec.ts index d9649a5dcc8..bd126f20e19 100644 --- a/src/test/emulators/functionsEmulatorRuntime.spec.ts +++ b/src/test/emulators/functionsEmulatorRuntime.spec.ts @@ -2,118 +2,10 @@ import { expect } from "chai"; import { FunctionsRuntimeInstance, InvokeRuntime } from "../../emulator/functionsEmulator"; import { EmulatorLog } from "../../emulator/types"; import { request } from "http"; -import { findModuleRoot, FunctionsRuntimeBundle } from "../../emulator/functionsEmulatorShared"; +import { FunctionsRuntimeBundle } from "../../emulator/functionsEmulatorShared"; import { Change } from "firebase-functions"; import { DocumentSnapshot } from "firebase-functions/lib/providers/firestore"; -import * as supertest from "supertest"; -const cwd = findModuleRoot("firebase-tools", __dirname); - -const FunctionRuntimeBundles = { - onCreate: { - ports: { - firestore: 8080, - }, - cwd, - proto: { - data: { - value: { - name: "projects/fake-project-id/databases/(default)/documents/test/test", - fields: { - when: { - timestampValue: "2019-04-15T16:55:48.150Z", - }, - }, - createTime: "2019-04-15T16:56:13.737Z", - updateTime: "2019-04-15T16:56:13.737Z", - }, - updateMask: {}, - }, - context: { - eventId: "7ebfb089-f549-4e1f-8312-fe843efc8be7", - timestamp: "2019-04-15T16:56:13.737Z", - eventType: "providers/cloud.firestore/eventTypes/document.create", - resource: { - name: "projects/fake-project-id/databases/(default)/documents/test/test", - service: "firestore.googleapis.com", - }, - }, - }, - triggerId: "function_id", - projectId: "fake-project-id", - } as FunctionsRuntimeBundle, - onWrite: { - ports: { - firestore: 8080, - }, - cwd, - proto: { - data: { - value: { - name: "projects/fake-project-id/databases/(default)/documents/test/test", - fields: { - when: { - timestampValue: "2019-04-15T16:55:48.150Z", - }, - }, - createTime: "2019-04-15T16:56:13.737Z", - updateTime: "2019-04-15T16:56:13.737Z", - }, - updateMask: {}, - }, - context: { - eventId: "7ebfb089-f549-4e1f-8312-fe843efc8be7", - timestamp: "2019-04-15T16:56:13.737Z", - eventType: "providers/cloud.firestore/eventTypes/document.write", - resource: { - name: "projects/fake-project-id/databases/(default)/documents/test/test", - service: "firestore.googleapis.com", - }, - }, - }, - triggerId: "function_id", - projectId: "fake-project-id", - } as FunctionsRuntimeBundle, - onDelete: { - ports: { - firestore: 8080, - }, - cwd, - proto: { - data: { - oldValue: { - name: "projects/fake-project-id/databases/(default)/documents/test/test", - fields: { - when: { - timestampValue: "2019-04-15T16:55:48.150Z", - }, - }, - createTime: "2019-04-15T16:56:13.737Z", - updateTime: "2019-04-15T16:56:13.737Z", - }, - updateMask: {}, - }, - context: { - eventId: "7ebfb089-f549-4e1f-8312-fe843efc8be7", - timestamp: "2019-04-15T16:56:13.737Z", - eventType: "providers/cloud.firestore/eventTypes/document.delete", - resource: { - name: "projects/fake-project-id/databases/(default)/documents/test/test", - service: "firestore.googleapis.com", - }, - }, - }, - triggerId: "function_id", - projectId: "fake-project-id", - } as FunctionsRuntimeBundle, - onRequest: { - ports: { - firestore: 8080, - }, - cwd, - triggerId: "function_id", - projectId: "fake-project-id", - } as FunctionsRuntimeBundle, -}; +import { FunctionRuntimeBundles } from "./fixtures"; async function _countLogEntries( runtime: FunctionsRuntimeInstance @@ -427,12 +319,6 @@ describe("FunctionsEmulatorRuntime", () => { }); }); - describe("Hub", () => { - it("should route requests to /:project_id/:trigger_id to HTTPS Function", async () => { - // supertest() - }); - }); - describe("Runtime", () => { describe("HTTPS", () => { it("should handle a GET request", async () => { From 0c3b9df6dedb6d4a0e8a22c58d2912ca1127a79a Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Mon, 13 May 2019 13:43:14 -0700 Subject: [PATCH 04/11] Slightly cleans up runtime tests --- .../functionsEmulatorRuntime.spec.ts | 207 ++++++------------ 1 file changed, 70 insertions(+), 137 deletions(-) diff --git a/src/test/emulators/functionsEmulatorRuntime.spec.ts b/src/test/emulators/functionsEmulatorRuntime.spec.ts index bd126f20e19..4d154253204 100644 --- a/src/test/emulators/functionsEmulatorRuntime.spec.ts +++ b/src/test/emulators/functionsEmulatorRuntime.spec.ts @@ -1,5 +1,9 @@ import { expect } from "chai"; -import { FunctionsRuntimeInstance, InvokeRuntime } from "../../emulator/functionsEmulator"; +import { + FunctionsRuntimeInstance, + InvokeRuntime, + InvokeRuntimeOpts, +} from "../../emulator/functionsEmulator"; import { EmulatorLog } from "../../emulator/types"; import { request } from "http"; import { FunctionsRuntimeBundle } from "../../emulator/functionsEmulatorShared"; @@ -20,6 +24,19 @@ async function _countLogEntries( return counts; } +function InvokeRuntimeWithFunctions( + frb: FunctionsRuntimeBundle, + triggers: () => {}, + opts?: InvokeRuntimeOpts +): FunctionsRuntimeInstance { + const serializedTriggers = triggers.toString(); + + return InvokeRuntime(process.execPath, frb, { + ...opts, + serializedTriggers, + }); +} + function _is_verbose(runtime: FunctionsRuntimeInstance): void { runtime.events.on("log", (el: EmulatorLog) => { process.stdout.write(el.toPrettyString() + "\n"); @@ -29,11 +46,11 @@ function _is_verbose(runtime: FunctionsRuntimeInstance): void { const TIMEOUT_LONG = 10000; const TIMEOUT_MED = 5000; -describe("FunctionsEmulatorRuntime", () => { +describe("FunctionsEmulator-Runtime", () => { describe("Stubs, Mocks, and Helpers (aka Magic, Glee, and Awesomeness)", () => { describe("_InitializeNetworkFiltering(...)", () => { it("should log outgoing HTTPS requests", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions") @@ -51,11 +68,8 @@ describe("FunctionsEmulatorRuntime", () => { ]); }), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onCreate, { - serializedTriggers, }); + const logs = await _countLogEntries(runtime); // In Node 6 we get >=5 events here, Node 8+ gets >=4 because of changes to @@ -69,7 +83,7 @@ describe("FunctionsEmulatorRuntime", () => { describe("_InitializeFirebaseAdminStubs(...)", () => { it("should provide stubbed default app from initializeApp", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions") @@ -77,19 +91,14 @@ describe("FunctionsEmulatorRuntime", () => { // tslint:disable-next-line:no-empty .onCreate(async () => {}), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onCreate, { - serializedTriggers, }); const logs = await _countLogEntries(runtime); - expect(logs["default-admin-app-used"]).to.eq(1); }).timeout(TIMEOUT_MED); it("should provide non-stubbed non-default app from initializeApp", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { require("firebase-admin").initializeApp(); // We still need to initialize default for snapshots require("firebase-admin").initializeApp({}, "non-default"); return { @@ -98,18 +107,13 @@ describe("FunctionsEmulatorRuntime", () => { // tslint:disable-next-line:no-empty .onCreate(async () => {}), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onCreate, { - serializedTriggers, }); const logs = await _countLogEntries(runtime); - expect(logs["non-default-admin-app-used"]).to.eq(1); }).timeout(TIMEOUT_MED); it("should alert when the app is not initialized", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { return { function_id: require("firebase-functions") .firestore.document("test/test") @@ -120,18 +124,15 @@ describe("FunctionsEmulatorRuntime", () => { .get(); }), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onCreate, { - serializedTriggers, }); + const logs = await _countLogEntries(runtime); expect(logs["admin-not-initialized"]).to.eq(1); }).timeout(TIMEOUT_MED); it("should route all sub-fields accordingly", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions") @@ -143,10 +144,6 @@ describe("FunctionsEmulatorRuntime", () => { ); }), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onCreate, { - serializedTriggers, }); runtime.events.on("log", (el: EmulatorLog) => { @@ -161,7 +158,14 @@ describe("FunctionsEmulatorRuntime", () => { }).timeout(TIMEOUT_MED); it("should redirect Firestore write to emulator", async () => { - const serializedTriggers = (() => { + const onRequestCopy = JSON.parse( + JSON.stringify(FunctionRuntimeBundles.onRequest) + ) as FunctionsRuntimeBundle; + + // Set the port to something crazy to avoid conflict with live emulator + onRequestCopy.ports = { firestore: 80800 }; + + const runtime = InvokeRuntimeWithFunctions(onRequestCopy, () => { const admin = require("firebase-admin"); admin.initializeApp(); @@ -180,21 +184,9 @@ describe("FunctionsEmulatorRuntime", () => { } ), }; - }).toString(); - - const onRequestCopy = JSON.parse( - JSON.stringify(FunctionRuntimeBundles.onRequest) - ) as FunctionsRuntimeBundle; - - // Set the port to something crazy to avoid conflict with live emulator - onRequestCopy.ports = { firestore: 80800 }; - - const runtime = InvokeRuntime(process.execPath, onRequestCopy, { - serializedTriggers, }); await runtime.ready; - await new Promise((resolve) => { request( { @@ -216,7 +208,7 @@ describe("FunctionsEmulatorRuntime", () => { }).timeout(TIMEOUT_MED); it("should merge .settings() with emulator settings", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { const admin = require("firebase-admin"); admin.initializeApp(); admin.firestore().settings({ @@ -229,10 +221,6 @@ describe("FunctionsEmulatorRuntime", () => { // tslint:disable-next-line:no-empty .onCreate(async () => {}), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onCreate, { - serializedTriggers, }); runtime.events.on("log", (el: EmulatorLog) => { @@ -248,7 +236,7 @@ describe("FunctionsEmulatorRuntime", () => { return; } - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { const admin = require("firebase-admin"); admin.initializeApp({ databaseURL: "fake-app-id.firebaseio.com", @@ -266,10 +254,6 @@ describe("FunctionsEmulatorRuntime", () => { }); }), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onCreate, { - serializedTriggers, }); runtime.events.on("log", (el: EmulatorLog) => { @@ -292,26 +276,29 @@ describe("FunctionsEmulatorRuntime", () => { describe("_InitializeFunctionsConfigHelper()", () => { it("should tell the user if they've accessed a non-existent function field", async () => { - const serializedTriggers = (() => { - require("firebase-admin").initializeApp(); - return { - function_id: require("firebase-functions") - .firestore.document("test/test") - .onCreate(async () => { - /* tslint:disable:no-console */ - console.log(require("firebase-functions").config().doesnt.exist); - console.log(require("firebase-functions").config().does.exist); - console.log(require("firebase-functions").config().also_doesnt.exist); - }), - }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onCreate, { - serializedTriggers, - env: { - CLOUD_RUNTIME_CONFIG: JSON.stringify({ does: { exist: "already exists" } }), + const runtime = InvokeRuntimeWithFunctions( + FunctionRuntimeBundles.onCreate, + () => { + require("firebase-admin").initializeApp(); + return { + function_id: require("firebase-functions") + .firestore.document("test/test") + .onCreate(async () => { + /* tslint:disable:no-console */ + console.log(require("firebase-functions").config().doesnt.exist); + console.log(require("firebase-functions").config().does.exist); + console.log(require("firebase-functions").config().also_doesnt.exist); + }), + }; }, - }); + { + env: { + CLOUD_RUNTIME_CONFIG: JSON.stringify({ + does: { exist: "already exists" }, + }), + }, + } + ); const logs = await _countLogEntries(runtime); expect(logs["functions-config-missing-value"]).to.eq(2); @@ -322,7 +309,7 @@ describe("FunctionsEmulatorRuntime", () => { describe("Runtime", () => { describe("HTTPS", () => { it("should handle a GET request", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onRequest, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions").https.onRequest( @@ -331,14 +318,9 @@ describe("FunctionsEmulatorRuntime", () => { } ), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onRequest, { - serializedTriggers, }); await runtime.ready; - await new Promise((resolve) => { request( { @@ -360,7 +342,7 @@ describe("FunctionsEmulatorRuntime", () => { }).timeout(TIMEOUT_MED); it("should handle a POST request with form data", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onRequest, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions").https.onRequest( @@ -369,10 +351,6 @@ describe("FunctionsEmulatorRuntime", () => { } ), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onRequest, { - serializedTriggers, }); await runtime.ready; @@ -406,7 +384,7 @@ describe("FunctionsEmulatorRuntime", () => { }).timeout(TIMEOUT_MED); it("should handle a POST request with JSON data", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onRequest, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions").https.onRequest( @@ -415,14 +393,9 @@ describe("FunctionsEmulatorRuntime", () => { } ), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onRequest, { - serializedTriggers, }); await runtime.ready; - await new Promise((resolve) => { const reqData = '{"name": "sparky"}'; const req = request( @@ -452,7 +425,7 @@ describe("FunctionsEmulatorRuntime", () => { }).timeout(TIMEOUT_MED); it("should handle a POST request with text data", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onRequest, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions").https.onRequest( @@ -461,14 +434,9 @@ describe("FunctionsEmulatorRuntime", () => { } ), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onRequest, { - serializedTriggers, }); await runtime.ready; - await new Promise((resolve) => { const reqData = "name is sparky"; const req = request( @@ -498,7 +466,7 @@ describe("FunctionsEmulatorRuntime", () => { }).timeout(TIMEOUT_MED); it("should handle a POST request with any other type", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onRequest, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions").https.onRequest( @@ -507,14 +475,9 @@ describe("FunctionsEmulatorRuntime", () => { } ), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onRequest, { - serializedTriggers, }); await runtime.ready; - await new Promise((resolve) => { const reqData = "name is sparky"; const req = request( @@ -547,7 +510,7 @@ describe("FunctionsEmulatorRuntime", () => { describe("Cloud Firestore", () => { it("should provide Change for firestore.onWrite()", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onWrite, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions") @@ -562,10 +525,6 @@ describe("FunctionsEmulatorRuntime", () => { ); }), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onWrite, { - serializedTriggers, }); runtime.events.on("log", (el: EmulatorLog) => { @@ -580,7 +539,7 @@ describe("FunctionsEmulatorRuntime", () => { }).timeout(TIMEOUT_MED); it("should provide Change for firestore.onUpdate()", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onWrite, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions") @@ -595,10 +554,6 @@ describe("FunctionsEmulatorRuntime", () => { ); }), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onWrite, { - serializedTriggers, }); runtime.events.on("log", (el: EmulatorLog) => { @@ -613,7 +568,7 @@ describe("FunctionsEmulatorRuntime", () => { }).timeout(TIMEOUT_MED); it("should provide DocumentSnapshot for firestore.onDelete()", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onDelete, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions") @@ -627,10 +582,6 @@ describe("FunctionsEmulatorRuntime", () => { ); }), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onDelete, { - serializedTriggers, }); runtime.events.on("log", (el: EmulatorLog) => { @@ -645,7 +596,7 @@ describe("FunctionsEmulatorRuntime", () => { }).timeout(TIMEOUT_MED); it("should provide DocumentSnapshot for firestore.onCreate()", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onWrite, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions") @@ -659,10 +610,6 @@ describe("FunctionsEmulatorRuntime", () => { ); }), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onWrite, { - serializedTriggers, }); runtime.events.on("log", (el: EmulatorLog) => { @@ -679,23 +626,18 @@ describe("FunctionsEmulatorRuntime", () => { describe("Error handling", () => { it("Should handle regular functions for Express handlers", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onRequest, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { (global as any)["not a thing"](); }), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onRequest, { - serializedTriggers, }); const logs = _countLogEntries(runtime); await runtime.ready; - await new Promise((resolve) => { request( { @@ -716,7 +658,7 @@ describe("FunctionsEmulatorRuntime", () => { }).timeout(TIMEOUT_MED); it("Should handle async functions for Express handlers", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onRequest, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions").https.onRequest( @@ -725,16 +667,11 @@ describe("FunctionsEmulatorRuntime", () => { } ), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onRequest, { - serializedTriggers, }); const logs = _countLogEntries(runtime); await runtime.ready; - await new Promise((resolve) => { request( { @@ -755,7 +692,7 @@ describe("FunctionsEmulatorRuntime", () => { }).timeout(TIMEOUT_MED); it("Should handle async/runWith functions for Express handlers", async () => { - const serializedTriggers = (() => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onRequest, () => { require("firebase-admin").initializeApp(); return { function_id: require("firebase-functions") @@ -764,10 +701,6 @@ describe("FunctionsEmulatorRuntime", () => { (global as any)["not a thing"](); }), }; - }).toString(); - - const runtime = InvokeRuntime(process.execPath, FunctionRuntimeBundles.onRequest, { - serializedTriggers, }); const logs = _countLogEntries(runtime); From 5b446f5251f0826060d03ae829b9e5284b7f7324 Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Mon, 13 May 2019 13:48:24 -0700 Subject: [PATCH 05/11] Adds failing test for #1279 --- src/test/emulators/fixtures.ts | 11 ++- src/test/emulators/functionsEmulator.spec.ts | 76 ++++++++++++++++++- .../functionsEmulatorRuntime.spec.ts | 5 +- 3 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/test/emulators/fixtures.ts b/src/test/emulators/fixtures.ts index 23505175b73..6bf07ba182e 100644 --- a/src/test/emulators/fixtures.ts +++ b/src/test/emulators/fixtures.ts @@ -1,7 +1,16 @@ import { findModuleRoot, FunctionsRuntimeBundle } from "../../emulator/functionsEmulatorShared"; +export const TIMEOUT_LONG = 10000; +export const TIMEOUT_MED = 5000; + const cwd = findModuleRoot("firebase-tools", __dirname); export const FunctionRuntimeBundles = { + template: { + ports: {}, + cwd, + triggerId: "function_id", + projectId: "fake-project-id", + } as FunctionsRuntimeBundle, onCreate: { ports: { firestore: 8080, @@ -106,4 +115,4 @@ export const FunctionRuntimeBundles = { triggerId: "function_id", projectId: "fake-project-id", } as FunctionsRuntimeBundle, -}; \ No newline at end of file +}; diff --git a/src/test/emulators/functionsEmulator.spec.ts b/src/test/emulators/functionsEmulator.spec.ts index 4f59b0543a4..7db34899c48 100644 --- a/src/test/emulators/functionsEmulator.spec.ts +++ b/src/test/emulators/functionsEmulator.spec.ts @@ -1,8 +1,76 @@ -import { FunctionsEmulator } from "../../emulator/functionsEmulator"; +import { expect } from "chai"; +import { FunctionsEmulator, FunctionsRuntimeInstance } from "../../emulator/functionsEmulator"; import * as supertest from "supertest"; +import { FunctionRuntimeBundles, TIMEOUT_MED } from "./fixtures"; +import * as logger from "../../logger"; +import { FunctionsRuntimeBundle } from "../../emulator/functionsEmulatorShared"; +import * as express from "express"; -describe.only("Hub", () => { +// Uncomment this to enable --debug logging! +logger.add(require("winston").transports.Console, { + level: "debug", + showLevel: false, + colorize: true, +}); + +const startFunctionRuntime = FunctionsEmulator.startFunctionRuntime; +function UseFunctions(triggers: () => {}): void { + const serializedTriggers = triggers.toString(); + + FunctionsEmulator.startFunctionRuntime = ( + bundleTemplate: FunctionsRuntimeBundle, + triggerId: string, + nodeBinary: string, + proto?: any + ): FunctionsRuntimeInstance => { + return startFunctionRuntime(bundleTemplate, triggerId, nodeBinary, proto, { + serializedTriggers, + }); + }; +} + +describe.only("FunctionsEmulator-Hub", () => { it("should route requests to /:project_id/:trigger_id to HTTPS Function", async () => { - supertest(FunctionsEmulator.createHubServer()) - }); + UseFunctions(() => { + require("firebase-admin").initializeApp(); + return { + function_id: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ hello: "world" }); + } + ), + }; + }); + + await supertest( + FunctionsEmulator.createHubServer(FunctionRuntimeBundles.template, process.execPath) + ) + .get("/fake-project-id/us-central-1f/function_id") + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ hello: "world" }); + }); + }).timeout(TIMEOUT_MED); + + it("should rewrite req.path to hide /:project_id/:trigger_id", async () => { + UseFunctions(() => { + require("firebase-admin").initializeApp(); + return { + function_id: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + } + ), + }; + }); + + await supertest( + FunctionsEmulator.createHubServer(FunctionRuntimeBundles.template, process.execPath) + ) + .get("/fake-project-id/us-central-1f/function_id/sub/route/a") + .expect(200) + .then((res) => { + expect(res.body.path).to.eq("/sub/route/a"); + }); + }).timeout(TIMEOUT_MED); }); diff --git a/src/test/emulators/functionsEmulatorRuntime.spec.ts b/src/test/emulators/functionsEmulatorRuntime.spec.ts index 4d154253204..0988c280354 100644 --- a/src/test/emulators/functionsEmulatorRuntime.spec.ts +++ b/src/test/emulators/functionsEmulatorRuntime.spec.ts @@ -9,7 +9,7 @@ import { request } from "http"; import { FunctionsRuntimeBundle } from "../../emulator/functionsEmulatorShared"; import { Change } from "firebase-functions"; import { DocumentSnapshot } from "firebase-functions/lib/providers/firestore"; -import { FunctionRuntimeBundles } from "./fixtures"; +import { FunctionRuntimeBundles, TIMEOUT_LONG, TIMEOUT_MED } from "./fixtures"; async function _countLogEntries( runtime: FunctionsRuntimeInstance @@ -43,9 +43,6 @@ function _is_verbose(runtime: FunctionsRuntimeInstance): void { }); } -const TIMEOUT_LONG = 10000; -const TIMEOUT_MED = 5000; - describe("FunctionsEmulator-Runtime", () => { describe("Stubs, Mocks, and Helpers (aka Magic, Glee, and Awesomeness)", () => { describe("_InitializeNetworkFiltering(...)", () => { From 953e0f794c33eac85584f1183aca4d5b6038a6e5 Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Mon, 13 May 2019 13:48:49 -0700 Subject: [PATCH 06/11] Allows FunctionsEmulator.startFunctionRuntime to take InvokeRuntimeOpts --- package.json | 1 + src/emulator/functionsEmulator.ts | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 2c5a79c29c7..0d5dae69afa 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "@types/sinon-chai": "^3.2.2", "@types/supertest": "^2.0.6", "@types/tmp": "^0.1.0", + "@types/winston": "^2.4.4", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", "coveralls": "^3.0.1", diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 2d0640a316f..a27d7d1afeb 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -233,7 +233,8 @@ export class FunctionsEmulator implements EmulatorInstance { bundleTemplate: FunctionsRuntimeBundle, triggerId: string, nodeBinary: string, - proto?: any + proto?: any, + runtimeOpts?: InvokeRuntimeOpts ): FunctionsRuntimeInstance { const runtimeBundle: FunctionsRuntimeBundle = { ...bundleTemplate, @@ -244,7 +245,7 @@ export class FunctionsEmulator implements EmulatorInstance { triggerId, }; - const runtime = InvokeRuntime(nodeBinary, runtimeBundle); + const runtime = InvokeRuntime(nodeBinary, runtimeBundle, runtimeOpts || {}); runtime.events.on("log", FunctionsEmulator.handleRuntimeLog.bind(this)); return runtime; } @@ -527,10 +528,11 @@ You can probably fix this by running "npm install ${ } } +export interface InvokeRuntimeOpts { serializedTriggers?: string; env?: { [key: string]: string } } export function InvokeRuntime( nodeBinary: string, frb: FunctionsRuntimeBundle, - opts?: { serializedTriggers?: string; env?: { [key: string]: string } } + opts?: InvokeRuntimeOpts ): FunctionsRuntimeInstance { opts = opts || {}; From fa69f1ae226e42a375e16d35cbd043c2ef84036f Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Mon, 13 May 2019 14:01:13 -0700 Subject: [PATCH 07/11] Split function handler based on route --- src/emulator/functionsEmulator.ts | 76 +++++++++++++++++++------------ 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index a27d7d1afeb..ac156c7c4c3 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -89,28 +89,22 @@ export class FunctionsEmulator implements EmulatorInstance { // The URL for the function that the other emulators (Firestore, etc) use. // TODO(abehaskins): Make the other emulators use the route below and remove this. - const internalRoute = "/functions/projects/:project_id/triggers/:trigger_name"; + const backgroundFunctionRoute = "/functions/projects/:project_id/triggers/:trigger_name"; // The URL that the developer sees, this is the same URL that the legacy emulator used. - const externalRoute = `/:project_id/:region/:trigger_name`; + const httpsFunctionRoute = `/:project_id/:region/:trigger_name`; // A trigger named "foo" needs to respond at "foo" as well as "foo/*" but not "fooBar". - const functionRoutes = [ - internalRoute, - `${internalRoute}/*`, - externalRoute, - `${externalRoute}/*`, - ]; + const httpsFunctionRoutes = [httpsFunctionRoute, `${httpsFunctionRoute}/*`]; - // Define a common handler function to use for GET and POST requests. - const handler: express.RequestHandler = async (req, res) => { + const backgroundHandler = async (req: express.Request, res: express.Response) => { const method = req.method; const triggerId = req.params.trigger_name; logger.debug(`[functions] ${method} request to function ${triggerId} accepted.`); const reqBody = (req as RequestWithRawBody).rawBody; - const proto = reqBody ? JSON.parse(reqBody) : undefined; + const proto = JSON.parse(reqBody); const runtime = FunctionsEmulator.startFunctionRuntime( bundleTemplate, @@ -137,21 +131,36 @@ export class FunctionsEmulator implements EmulatorInstance { const triggerMap: EmulatedTriggerMap = triggerLog.data.triggers; const trigger = triggerMap[triggerId]; - const isHttpsTrigger = !!trigger.definition.httpsTrigger; - - // Log each invocation and the service type. - if (isHttpsTrigger) { - track(EVENT_INVOKE, "https"); - } else { - const service: string = _.get(trigger.definition, "eventTrigger.service", "unknown"); - track(EVENT_INVOKE, service); - } + const service: string = _.get(trigger.definition, "eventTrigger.service", "unknown"); + track(EVENT_INVOKE, service); - if (!isHttpsTrigger) { - // Background functions just wait and then ACK - await runtime.exit; - return res.json({ status: "acknowledged" }); - } + await runtime.exit; + return res.json({ status: "acknowledged" }); + }; + + // Define a common handler function to use for GET and POST requests. + const httpsHandler: express.RequestHandler = async ( + req: express.Request, + res: express.Response + ) => { + const method = req.method; + const triggerId = req.params.trigger_name; + + logger.debug(`[functions] ${method} request to function ${triggerId} accepted.`); + + const reqBody = (req as RequestWithRawBody).rawBody; + + const runtime = FunctionsEmulator.startFunctionRuntime(bundleTemplate, triggerId, nodeBinary); + + runtime.events.on("log", (el: EmulatorLog) => { + if (el.level === "FATAL") { + res.send(el.text); + } + }); + + await runtime.ready; + logger.debug(JSON.stringify(runtime.metadata)); + track(EVENT_INVOKE, "https"); logger.debug( `[functions] Runtime ready! Sending request! ${JSON.stringify(runtime.metadata)}` @@ -165,7 +174,12 @@ export class FunctionsEmulator implements EmulatorInstance { const runtimeReq = http.request( { method, - path: req.url, // 'url' includes the query params + path: + "/" + + req.url + .split("/") + .slice(4) + .join("/"), // 'url' includes the query params headers: req.headers, socketPath: runtime.metadata.socketPath, }, @@ -223,8 +237,9 @@ export class FunctionsEmulator implements EmulatorInstance { await runtime.exit; }; - hub.get(functionRoutes, handler); - hub.post(functionRoutes, handler); + hub.get(httpsFunctionRoutes, httpsHandler); + hub.post(httpsFunctionRoutes, httpsHandler); + hub.post(backgroundFunctionRoute, backgroundHandler); return hub; } @@ -528,7 +543,10 @@ You can probably fix this by running "npm install ${ } } -export interface InvokeRuntimeOpts { serializedTriggers?: string; env?: { [key: string]: string } } +export interface InvokeRuntimeOpts { + serializedTriggers?: string; + env?: { [key: string]: string }; +} export function InvokeRuntime( nodeBinary: string, frb: FunctionsRuntimeBundle, From 94203d0653e65be607cca57b980964ce9ce3a5b5 Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Mon, 13 May 2019 14:42:49 -0700 Subject: [PATCH 08/11] Fixes #1281, #1279, and #1277 --- src/test/emulators/functionsEmulator.spec.ts | 57 +++++++++++++++++-- .../functionsEmulatorRuntime.spec.ts | 38 +++++++++++++ 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/test/emulators/functionsEmulator.spec.ts b/src/test/emulators/functionsEmulator.spec.ts index 7db34899c48..9cddb74ca81 100644 --- a/src/test/emulators/functionsEmulator.spec.ts +++ b/src/test/emulators/functionsEmulator.spec.ts @@ -7,11 +7,11 @@ import { FunctionsRuntimeBundle } from "../../emulator/functionsEmulatorShared"; import * as express from "express"; // Uncomment this to enable --debug logging! -logger.add(require("winston").transports.Console, { - level: "debug", - showLevel: false, - colorize: true, -}); +// logger.add(require("winston").transports.Console, { +// level: "debug", +// showLevel: false, +// colorize: true, +// }); const startFunctionRuntime = FunctionsEmulator.startFunctionRuntime; function UseFunctions(triggers: () => {}): void { @@ -29,7 +29,7 @@ function UseFunctions(triggers: () => {}): void { }; } -describe.only("FunctionsEmulator-Hub", () => { +describe("FunctionsEmulator-Hub", () => { it("should route requests to /:project_id/:trigger_id to HTTPS Function", async () => { UseFunctions(() => { require("firebase-admin").initializeApp(); @@ -73,4 +73,49 @@ describe.only("FunctionsEmulator-Hub", () => { expect(res.body.path).to.eq("/sub/route/a"); }); }).timeout(TIMEOUT_MED); + + it("should route request body", async () => { + UseFunctions(() => { + require("firebase-admin").initializeApp(); + return { + function_id: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json(req.body); + } + ), + }; + }); + + await supertest( + FunctionsEmulator.createHubServer(FunctionRuntimeBundles.template, process.execPath) + ) + .post("/fake-project-id/us-central-1f/function_id/sub/route/a") + .send({ hello: "world" }) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ hello: "world" }); + }); + }).timeout(TIMEOUT_MED); + + it("should route query parameters", async () => { + UseFunctions(() => { + require("firebase-admin").initializeApp(); + return { + function_id: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json(req.query); + } + ), + }; + }); + + await supertest( + FunctionsEmulator.createHubServer(FunctionRuntimeBundles.template, process.execPath) + ) + .get("/fake-project-id/us-central-1f/function_id/sub/route/a?hello=world") + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ hello: "world" }); + }); + }).timeout(TIMEOUT_MED); }); diff --git a/src/test/emulators/functionsEmulatorRuntime.spec.ts b/src/test/emulators/functionsEmulatorRuntime.spec.ts index 0988c280354..02b7e9c6c7a 100644 --- a/src/test/emulators/functionsEmulatorRuntime.spec.ts +++ b/src/test/emulators/functionsEmulatorRuntime.spec.ts @@ -10,6 +10,7 @@ import { FunctionsRuntimeBundle } from "../../emulator/functionsEmulatorShared"; import { Change } from "firebase-functions"; import { DocumentSnapshot } from "firebase-functions/lib/providers/firestore"; import { FunctionRuntimeBundles, TIMEOUT_LONG, TIMEOUT_MED } from "./fixtures"; +import * as express from "express"; async function _countLogEntries( runtime: FunctionsRuntimeInstance @@ -503,6 +504,43 @@ describe("FunctionsEmulator-Runtime", () => { await runtime.exit; }).timeout(TIMEOUT_MED); + + it("should forward request to Express app", async () => { + const runtime = InvokeRuntimeWithFunctions(FunctionRuntimeBundles.onRequest, () => { + require("firebase-admin").initializeApp(); + const app = require("express")(); + app.get("/", (req: express.Request, res: express.Response) => { + res.json(req.query); + }); + return { + function_id: require("firebase-functions").https.onRequest(app), + }; + }); + + await runtime.ready; + await new Promise((resolve) => { + const reqData = "name is sparky"; + const req = request( + { + socketPath: runtime.metadata.socketPath, + path: "/?hello=world", + method: "get", + }, + (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + expect(JSON.parse(data)).to.deep.equal({ hello: "world" }); + resolve(); + }); + } + ); + req.write(reqData); + req.end(); + }); + + await runtime.exit; + }).timeout(TIMEOUT_MED); }); describe("Cloud Firestore", () => { From 887385dd0fe793786bb92c14766774779f4f770f Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Mon, 13 May 2019 14:46:42 -0700 Subject: [PATCH 09/11] Remove extra .write() --- src/test/emulators/functionsEmulatorRuntime.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/emulators/functionsEmulatorRuntime.spec.ts b/src/test/emulators/functionsEmulatorRuntime.spec.ts index 02b7e9c6c7a..ea2403db706 100644 --- a/src/test/emulators/functionsEmulatorRuntime.spec.ts +++ b/src/test/emulators/functionsEmulatorRuntime.spec.ts @@ -519,7 +519,6 @@ describe("FunctionsEmulator-Runtime", () => { await runtime.ready; await new Promise((resolve) => { - const reqData = "name is sparky"; const req = request( { socketPath: runtime.metadata.socketPath, @@ -535,7 +534,6 @@ describe("FunctionsEmulator-Runtime", () => { }); } ); - req.write(reqData); req.end(); }); From b28757d5790ab71b0576bdf56b3a63d2c2d5cfcf Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Mon, 13 May 2019 14:48:31 -0700 Subject: [PATCH 10/11] Longer timeouts for stability --- src/test/emulators/functionsEmulator.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/emulators/functionsEmulator.spec.ts b/src/test/emulators/functionsEmulator.spec.ts index 9cddb74ca81..20d16746a19 100644 --- a/src/test/emulators/functionsEmulator.spec.ts +++ b/src/test/emulators/functionsEmulator.spec.ts @@ -50,7 +50,7 @@ describe("FunctionsEmulator-Hub", () => { .then((res) => { expect(res.body).to.deep.equal({ hello: "world" }); }); - }).timeout(TIMEOUT_MED); + }).timeout(TIMEOUT_LONG); it("should rewrite req.path to hide /:project_id/:trigger_id", async () => { UseFunctions(() => { @@ -72,7 +72,7 @@ describe("FunctionsEmulator-Hub", () => { .then((res) => { expect(res.body.path).to.eq("/sub/route/a"); }); - }).timeout(TIMEOUT_MED); + }).timeout(TIMEOUT_LONG); it("should route request body", async () => { UseFunctions(() => { @@ -95,7 +95,7 @@ describe("FunctionsEmulator-Hub", () => { .then((res) => { expect(res.body).to.deep.equal({ hello: "world" }); }); - }).timeout(TIMEOUT_MED); + }).timeout(TIMEOUT_LONG); it("should route query parameters", async () => { UseFunctions(() => { @@ -117,5 +117,5 @@ describe("FunctionsEmulator-Hub", () => { .then((res) => { expect(res.body).to.deep.equal({ hello: "world" }); }); - }).timeout(TIMEOUT_MED); + }).timeout(TIMEOUT_LONG); }); From 1c4456329c67a085fe5323355addc1448af167bc Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Mon, 13 May 2019 14:54:06 -0700 Subject: [PATCH 11/11] Fix TIMEOUT_LONG undefined --- src/test/emulators/functionsEmulator.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/emulators/functionsEmulator.spec.ts b/src/test/emulators/functionsEmulator.spec.ts index 20d16746a19..09759240273 100644 --- a/src/test/emulators/functionsEmulator.spec.ts +++ b/src/test/emulators/functionsEmulator.spec.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { FunctionsEmulator, FunctionsRuntimeInstance } from "../../emulator/functionsEmulator"; import * as supertest from "supertest"; -import { FunctionRuntimeBundles, TIMEOUT_MED } from "./fixtures"; +import { FunctionRuntimeBundles, TIMEOUT_LONG, TIMEOUT_MED } from "./fixtures"; import * as logger from "../../logger"; import { FunctionsRuntimeBundle } from "../../emulator/functionsEmulatorShared"; import * as express from "express";