diff --git a/src/emulator/eventarcEmulator.ts b/src/emulator/eventarcEmulator.ts index e08f9c358c5..b1f487a8914 100644 --- a/src/emulator/eventarcEmulator.ts +++ b/src/emulator/eventarcEmulator.ts @@ -8,6 +8,7 @@ import { EventTrigger } from "./functionsEmulatorShared"; import { CloudEvent } from "./events/types"; import { EmulatorRegistry } from "./registry"; import { FirebaseError } from "../error"; +import { cloudEventFromProtoToJson } from "./eventarcEmulatorUtils"; interface CustomEventTrigger { projectId: string; @@ -123,7 +124,7 @@ export class EventarcEmulator implements EmulatorInstance { .request, NodeJS.ReadableStream>({ method: "POST", path: `/functions/projects/${trigger.projectId}/triggers/${trigger.triggerName}`, - body: JSON.stringify(event), + body: JSON.stringify(cloudEventFromProtoToJson(event)), responseType: "stream", resolveOnHTTPError: true, }) diff --git a/src/emulator/eventarcEmulatorUtils.ts b/src/emulator/eventarcEmulatorUtils.ts new file mode 100644 index 00000000000..3cc06f5a539 --- /dev/null +++ b/src/emulator/eventarcEmulatorUtils.ts @@ -0,0 +1,62 @@ +import { CloudEvent } from "./events/types"; +import { FirebaseError } from "../error"; + +const BUILT_IN_ATTRS: string[] = ["time", "datacontenttype", "subject"]; + +export function cloudEventFromProtoToJson(ce: any): CloudEvent { + if (ce["id"] === undefined) { + throw new FirebaseError("CloudEvent 'id' is required."); + } + if (ce["type"] === undefined) { + throw new FirebaseError("CloudEvent 'type' is required."); + } + if (ce["specVersion"] === undefined) { + throw new FirebaseError("CloudEvent 'specVersion' is required."); + } + if (ce["source"] === undefined) { + throw new FirebaseError("CloudEvent 'source' is required."); + } + const out: CloudEvent = { + id: ce["id"], + type: ce["type"], + specversion: ce["specVersion"], + source: ce["source"], + subject: getOptionalAttribute(ce, "subject", "ceString"), + time: getRequiredAttribute(ce, "time", "ceTimestamp"), + data: getData(ce), + datacontenttype: getRequiredAttribute(ce, "datacontenttype", "ceString"), + }; + for (const attr in ce["attributes"]) { + if (BUILT_IN_ATTRS.includes(attr)) { + continue; + } + out[attr] = getRequiredAttribute(ce, attr, "ceString"); + } + return out; +} + +function getOptionalAttribute(ce: any, attr: string, type: string): string | undefined { + return ce["attributes"][attr][type]; +} + +function getRequiredAttribute(ce: any, attr: string, type: string): string { + const val = ce["attributes"][attr][type]; + if (val === undefined) { + throw new FirebaseError("CloudEvent must contain " + attr + " attribute"); + } + return val; +} + +function getData(ce: any): any { + const contentType = getRequiredAttribute(ce, "datacontenttype", "ceString"); + switch (contentType) { + case "application/json": + return JSON.parse(ce["textData"]); + case "text/plain": + return ce["textData"]; + case undefined: + return undefined; + default: + throw new FirebaseError("Unsupported content type: " + contentType); + } +} diff --git a/src/test/emulators/eventarcEmulatorUtils.spec.ts b/src/test/emulators/eventarcEmulatorUtils.spec.ts new file mode 100644 index 00000000000..d7015a11829 --- /dev/null +++ b/src/test/emulators/eventarcEmulatorUtils.spec.ts @@ -0,0 +1,126 @@ +import { expect } from "chai"; + +import { cloudEventFromProtoToJson } from "../../emulator/eventarcEmulatorUtils"; + +describe("eventarcEmulatorUtils", () => { + describe("cloudEventFromProtoToJson", () => { + it("converts cloud event from proto format", () => { + expect( + cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "application/json", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + subject: { + ceString: "context", + }, + }, + id: "user-provided-id", + source: "/my/functions", + specVersion: "1.0", + textData: '{"hello":"world"}', + type: "some.custom.event", + }) + ).to.deep.eq({ + type: "some.custom.event", + specversion: "1.0", + subject: "context", + datacontenttype: "application/json", + id: "user-provided-id", + data: { + hello: "world", + }, + source: "/my/functions", + time: "2022-03-16T20:20:42.212Z", + customattr: "custom value", + }); + }); + + it("throws invalid argument when source not set", () => { + expect(() => + cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "application/json", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + subject: { + ceString: "context", + }, + }, + id: "user-provided-id", + specVersion: "1.0", + textData: '{"hello":"world"}', + type: "some.custom.event", + }) + ).throws("CloudEvent 'source' is required."); + }); + + it("populates converts object data to JSON and sets datacontenttype", () => { + const got = cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "application/json", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + subject: { + ceString: "context", + }, + }, + id: "user-provided-id", + source: "/my/functions", + specVersion: "1.0", + textData: '{"hello":"world"}', + type: "some.custom.event", + }); + expect(got.datacontenttype).to.deep.eq("application/json"); + expect(got.data).to.deep.eq({ hello: "world" }); + }); + + it("populates string data and sets datacontenttype", () => { + const got = cloudEventFromProtoToJson({ + "@type": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + attributes: { + customattr: { + ceString: "custom value", + }, + datacontenttype: { + ceString: "text/plain", + }, + time: { + ceTimestamp: "2022-03-16T20:20:42.212Z", + }, + subject: { + ceString: "context", + }, + }, + id: "user-provided-id", + source: "/my/functions", + specVersion: "1.0", + textData: "hello world", + type: "some.custom.event", + }); + expect(got.datacontenttype).to.deep.eq("text/plain"); + expect(got.data).to.eq("hello world"); + }); + }); +});