Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix eventarc emulator #5304

Merged
merged 2 commits into from Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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");
});
});
});