Skip to content

Commit

Permalink
Fix eventarc emulator (#5304)
Browse files Browse the repository at this point in the history
Convert events from proto to JSON format before delegating to the trigger function. The eventarc admin SDK sends events in proto format: https://github.com/firebase/firebase-admin-node/blob/9fc8e84b8f496f12141e611bf3075e94f633c117/src/eventarc/eventarc-client-internal.ts#L96
  • Loading branch information
pavelgj committed Dec 5, 2022
1 parent 1ebb9f8 commit ab11ced
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/emulator/eventarcEmulator.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -123,7 +124,7 @@ export class EventarcEmulator implements EmulatorInstance {
.request<CloudEvent<any>, 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,
})
Expand Down
62 changes: 62 additions & 0 deletions 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<any> {
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<any> = {
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);
}
}
126 changes: 126 additions & 0 deletions 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");
});
});
});

0 comments on commit ab11ced

Please sign in to comment.