From 934d29a0f55bfb344d9db7c86eb8c698b111f5e3 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Fri, 21 Aug 2020 18:02:35 -0400 Subject: [PATCH 01/12] lib(messages): Implement a 4.0 Messages and other supporting interfaces This commit introduces the Message, Serializer and Deserializer, and Binding interfaces used to convert a CloudEvent into a Message that can be sent across a transport protocol. The first protocol implemented for this is HTTP, and some of the functionality formerly in src/transport/http has been simplified, reduced and/or moved to /src/messages/http. Test for V1 events are in place. Conformance tests have been modified to use these new interfaces vs. the HTTP Receiver class. Signed-off-by: Lance Ball --- src/index.ts | 15 +- .../versions.ts => messages/http/headers.ts} | 91 ++++ src/messages/http/index.ts | 229 +++++++++ src/messages/index.ts | 61 +++ src/transport/http/binary_emitter.ts | 3 +- src/transport/http/binary_receiver.ts | 94 ---- src/transport/http/headers.ts | 100 ---- src/transport/http/structured_receiver.ts | 91 ---- src/transport/receiver.ts | 99 +--- test/conformance/steps.ts | 21 +- test/integration/http_emitter_test.ts | 3 +- test/integration/message_test.ts | 79 +++ test/integration/receiver_binary_03_tests.ts | 443 ----------------- test/integration/receiver_binary_1_tests.ts | 448 ------------------ .../receiver_structured_0_3_test.ts | 227 --------- .../integration/receiver_structured_1_test.ts | 182 ------- test/integration/sdk_test.ts | 7 +- 17 files changed, 500 insertions(+), 1693 deletions(-) rename src/{transport/http/versions.ts => messages/http/headers.ts} (61%) create mode 100644 src/messages/http/index.ts create mode 100644 src/messages/index.ts delete mode 100644 src/transport/http/binary_receiver.ts delete mode 100644 src/transport/http/headers.ts delete mode 100644 src/transport/http/structured_receiver.ts create mode 100644 test/integration/message_test.ts delete mode 100644 test/integration/receiver_binary_03_tests.ts delete mode 100644 test/integration/receiver_binary_1_tests.ts delete mode 100644 test/integration/receiver_structured_0_3_test.ts delete mode 100644 test/integration/receiver_structured_1_test.ts diff --git a/src/index.ts b/src/index.ts index 598d61c4..69cf0562 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,9 +3,9 @@ import { ValidationError } from "./event/validation"; import { CloudEventV03, CloudEventV03Attributes, CloudEventV1, CloudEventV1Attributes } from "./event/interfaces"; import { Emitter, TransportOptions } from "./transport/emitter"; -import { Receiver, Mode } from "./transport/receiver"; +import { Receiver } from "./transport/receiver"; import { Protocol } from "./transport/protocols"; -import { Headers, headersFor } from "./transport/http/headers"; +import { Headers, Mode, Binding, HTTP, Message, Serializer, Deserializer } from "./messages"; import CONSTANTS from "./constants"; @@ -18,14 +18,19 @@ export { CloudEventV1Attributes, Version, ValidationError, + // From messages + Headers, + Mode, + Binding, + Message, + Deserializer, + Serializer, + HTTP, // From transport Emitter, Receiver, - Mode, Protocol, TransportOptions, - Headers, - headersFor, // From Constants CONSTANTS, }; diff --git a/src/transport/http/versions.ts b/src/messages/http/headers.ts similarity index 61% rename from src/transport/http/versions.ts rename to src/messages/http/headers.ts index 70ddc560..228445cb 100644 --- a/src/transport/http/versions.ts +++ b/src/messages/http/headers.ts @@ -1,6 +1,97 @@ import { PassThroughParser, DateParser, MappedParser } from "../../parsers"; +import { ValidationError, CloudEvent } from "../.."; +import { Headers } from "../"; +import { Version } from "../../event/cloudevent"; import CONSTANTS from "../../constants"; +export const allowedContentTypes = [CONSTANTS.DEFAULT_CONTENT_TYPE, CONSTANTS.MIME_JSON, CONSTANTS.MIME_OCTET_STREAM]; +export const requiredHeaders = [ + CONSTANTS.CE_HEADERS.ID, + CONSTANTS.CE_HEADERS.SOURCE, + CONSTANTS.CE_HEADERS.TYPE, + CONSTANTS.CE_HEADERS.SPEC_VERSION, +]; + +/** + * Validates cloud event headers and their values + * @param {Headers} headers event transport headers for validation + * @throws {ValidationError} if the headers are invalid + * @return {boolean} true if headers are valid + */ +export function validate(headers: Headers): Headers { + const sanitizedHeaders = sanitize(headers); + + // if content-type exists, be sure it's an allowed type + const contentTypeHeader = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]; + const noContentType = !allowedContentTypes.includes(contentTypeHeader); + if (contentTypeHeader && noContentType) { + throw new ValidationError("invalid content type", [sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]]); + } + + requiredHeaders + .filter((required: string) => !sanitizedHeaders[required]) + .forEach((required: string) => { + throw new ValidationError(`header '${required}' not found`); + }); + + if (!sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]) { + sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON; + } + + return sanitizedHeaders; +} + +/** + * Returns the HTTP headers that will be sent for this event when the HTTP transmission + * mode is "binary". Events sent over HTTP in structured mode only have a single CE header + * and that is "ce-id", corresponding to the event ID. + * @param {CloudEvent} event a CloudEvent + * @returns {Object} the headers that will be sent for the event + */ +export function headersFor(event: CloudEvent): Headers { + const headers: Headers = {}; + let headerMap: Readonly<{ [key: string]: MappedParser }>; + if (event.specversion === Version.V1) { + headerMap = v1headerMap; + } else { + headerMap = v03headerMap; + } + + // iterate over the event properties - generate a header for each + Object.getOwnPropertyNames(event).forEach((property) => { + const value = event[property]; + if (value) { + const map: MappedParser | undefined = headerMap[property] as MappedParser; + if (map) { + headers[map.name] = map.parser.parse(value as string) as string; + } else if (property !== CONSTANTS.DATA_ATTRIBUTE && property !== `${CONSTANTS.DATA_ATTRIBUTE}_base64`) { + headers[`${CONSTANTS.EXTENSIONS_PREFIX}${property}`] = value as string; + } + } + }); + // Treat time specially, since it's handled with getters and setters in CloudEvent + if (event.time) { + headers[CONSTANTS.CE_HEADERS.TIME] = event.time as string; + } + return headers; +} + +/** + * Sanitizes incoming headers by lowercasing them and potentially removing + * encoding from the content-type header. + * @param {Headers} headers HTTP headers as key/value pairs + * @returns {Headers} the sanitized headers + */ +export function sanitize(headers: Headers): Headers { + const sanitized: Headers = {}; + + Array.from(Object.keys(headers)) + .filter((header) => Object.hasOwnProperty.call(headers, header)) + .forEach((header) => (sanitized[header.toLowerCase()] = headers[header])); + + return sanitized; +} + function parser(name: string, parser = new PassThroughParser()): MappedParser { return { name: name, parser: parser }; } diff --git a/src/messages/http/index.ts b/src/messages/http/index.ts new file mode 100644 index 00000000..b850652a --- /dev/null +++ b/src/messages/http/index.ts @@ -0,0 +1,229 @@ +import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } from "../.."; +import { Message, Headers } from ".."; + +import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers, validate } from "./headers"; +import { asData, isBase64, isString, isStringOrObjectOrThrow, ValidationError } from "../../event/validation"; +import { validateCloudEvent } from "../../event/spec"; +import { Base64Parser, JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers"; + +// implements Serializer +export function binary(event: CloudEvent): Message { + const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CONTENT_TYPE }; + const headers: Headers = headersFor(event); + return { + headers: { ...contentType, ...headers }, + body: asData(event.data, event.datacontenttype as string), + }; +} + +// implements Serializer +export function structured(event: CloudEvent): Message { + return { + headers: { + [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CE_CONTENT_TYPE, + }, + body: event.toString(), + }; +} + +/** + * Converts a Message to a CloudEvent + * + * @param {Message} message the incoming message + * @return {CloudEvent} A new {CloudEvent} instance + */ +export function deserialize(message: Message): CloudEvent { + const cleanHeaders: Headers = sanitize(message.headers); + const mode: Mode = getMode(cleanHeaders); + let version = getVersion(mode, cleanHeaders, message.body); + if (version !== Version.V03 && version !== Version.V1) { + console.error(`Unknown spec version ${version}. Default to ${Version.V1}`); + version = Version.V1; + } + switch (mode) { + case Mode.BINARY: + return parseBinary(message, version); + case Mode.STRUCTURED: + return parseStructured(message, version); + default: + throw new ValidationError("Unknown Message mode"); + } +} + +/** + * Determines the HTTP transport mode (binary or structured) based + * on the incoming HTTP headers. + * @param {Headers} headers the incoming HTTP headers + * @returns {Mode} the transport mode + */ +function getMode(headers: Headers): Mode { + const contentType = headers[CONSTANTS.HEADER_CONTENT_TYPE]; + if (contentType && contentType.startsWith(CONSTANTS.MIME_CE)) { + return Mode.STRUCTURED; + } + if (headers[CONSTANTS.CE_HEADERS.ID]) { + return Mode.BINARY; + } + throw new ValidationError("no cloud event detected"); +} + +/** + * Determines the version of an incoming CloudEvent based on the + * HTTP headers or HTTP body, depending on transport mode. + * @param {Mode} mode the HTTP transport mode + * @param {Headers} headers the incoming HTTP headers + * @param {Record} body the HTTP request body + * @returns {Version} the CloudEvent specification version + */ +function getVersion(mode: Mode, headers: Headers, body: string | Record) { + if (mode === Mode.BINARY) { + // Check the headers for the version + const versionHeader = headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]; + if (versionHeader) { + return versionHeader; + } + } else { + // structured mode - the version is in the body + return typeof body === "string" ? JSON.parse(body).specversion : (body as CloudEvent).specversion; + } + return Version.V1; +} + +/** + * Parses an incoming HTTP Message, converting it to a {CloudEvent} + * instance if it conforms to the Cloud Event specification for this receiver. + * + * @param {Message} message the incoming HTTP Message + * @param {Version} version the spec version of the incoming event + * @returns {CloudEvent} an instance of CloudEvent representing the incoming request + * @throws {ValidationError} of the event does not conform to the spec + */ +function parseBinary(message: Message, version: Version): CloudEvent { + const headers = message.headers; + let body = message.body; + + if (!headers) throw new ValidationError("headers is null or undefined"); + if (body) { + isStringOrObjectOrThrow(body, new ValidationError("payload must be an object or a string")); + } + + if ( + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] && + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] !== Version.V03 && + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] !== Version.V1 + ) { + throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`); + } + + body = isString(body) && isBase64(body) ? Buffer.from(body as string, "base64").toString() : body; + + // Clone and low case all headers names + const sanitizedHeaders = validate(headers); + + const eventObj: { [key: string]: unknown | string | Record } = {}; + const parserMap: Record = version === Version.V1 ? v1binaryParsers : v1binaryParsers; + + for (const header in parserMap) { + if (sanitizedHeaders[header]) { + const mappedParser: MappedParser = parserMap[header]; + eventObj[mappedParser.name] = mappedParser.parser.parse(sanitizedHeaders[header]); + delete sanitizedHeaders[header]; + } + } + + let parsedPayload; + + if (body) { + const parser = parserByContentType[eventObj.datacontenttype as string]; + if (!parser) { + throw new ValidationError(`no parser found for content type ${eventObj.datacontenttype}`); + } + parsedPayload = parser.parse(body); + } + + // Every unprocessed header can be an extension + for (const header in sanitizedHeaders) { + if (header.startsWith(CONSTANTS.EXTENSIONS_PREFIX)) { + eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = headers[header]; + } + } + // At this point, if the datacontenttype is application/json and the datacontentencoding is base64 + // then the data has already been decoded as a string, then parsed as JSON. We don't need to have + // the datacontentencoding property set - in fact, it's incorrect to do so. + if (eventObj.datacontenttype === CONSTANTS.MIME_JSON && eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) { + delete eventObj.datacontentencoding; + } + + const cloudevent = new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03); + validateCloudEvent(cloudevent); + return cloudevent; +} + +/** + * Creates a new CloudEvent instance based on the provided payload and headers. + * + * @param {Message} message the incoming Message + * @param {Version} version the spec version of this message (v1 or v03) + * @returns {CloudEvent} a new CloudEvent instance for the provided headers and payload + * @throws {ValidationError} if the payload and header combination do not conform to the spec + */ +function parseStructured(message: Message, version: Version): CloudEvent { + let payload = message.body; + const headers = message.headers; + + if (!payload) throw new ValidationError("payload is null or undefined"); + if (!headers) throw new ValidationError("headers is null or undefined"); + isStringOrObjectOrThrow(payload, new ValidationError("payload must be an object or a string")); + + if ( + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] && + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] != Version.V03 && + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] != Version.V1 + ) { + throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`); + } + + payload = isString(payload) && isBase64(payload) ? Buffer.from(payload as string, "base64").toString() : payload; + + // Clone and low case all headers names + const sanitizedHeaders = sanitize(headers); + + const contentType = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]; + const parser: Parser = contentType ? parserByContentType[contentType] : new JSONParser(); + if (!parser) throw new ValidationError(`invalid content type ${sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]}`); + const incoming = { ...(parser.parse(payload) as Record) }; + + const eventObj: { [key: string]: unknown } = {}; + const parserMap: Record = version === Version.V1 ? v1structuredParsers : v03structuredParsers; + + for (const key in parserMap) { + const property = incoming[key]; + if (property) { + const parser: MappedParser = parserMap[key]; + eventObj[parser.name] = parser.parser.parse(property as string); + } + delete incoming[key]; + } + + // extensions are what we have left after processing all other properties + for (const key in incoming) { + eventObj[key] = incoming[key]; + } + + // ensure data content is correctly encoded + if (eventObj.data && eventObj.datacontentencoding) { + if (eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64 && !isBase64(eventObj.data)) { + throw new ValidationError("invalid payload"); + } else if (eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) { + const dataParser = new Base64Parser(); + eventObj.data = JSON.parse(dataParser.parse(eventObj.data as string)); + delete eventObj.datacontentencoding; + } + } + + const cloudevent = new CloudEvent(eventObj as CloudEventV1 | CloudEventV03); + + // Validates the event + validateCloudEvent(cloudevent); + return cloudevent; +} diff --git a/src/messages/index.ts b/src/messages/index.ts new file mode 100644 index 00000000..995e0aee --- /dev/null +++ b/src/messages/index.ts @@ -0,0 +1,61 @@ +import { CloudEvent } from ".."; +import { binary, deserialize, structured } from "./http"; + +/** + * Binding is an interface for transport protocols to implement, + * which provides functions for sending CloudEvent Messages over + * the wire. + */ +export interface Binding { + binary: Serializer; + structured: Serializer; + toEvent: Deserializer; +} + +/** + * Headers is an interface representing transport-agnostic headers as + * key/value string pairs + */ +export interface Headers { + [key: string]: string; +} + +/** + * Message is an interface representing a CloudEvent as a + * transport-agnostic message + */ +export interface Message { + headers: Headers; + body: string; +} + +/** + * An enum representing the two transport modes, binary and structured + */ +export enum Mode { + BINARY = "binary", + STRUCTURED = "structured", +} + +/** + * Serializer is an interface for functions that can convert a + * CloudEvent into a Message. + */ +export interface Serializer { + (event: CloudEvent): Message; +} + +/** + * Deserializer is a function interface that converts a + * Message to a CloudEvent + */ +export interface Deserializer { + (message: Message): CloudEvent; +} + +// HTTP Message capabilities +export const HTTP: Binding = { + binary: binary as Serializer, + structured: structured as Serializer, + toEvent: deserialize as Deserializer, +}; diff --git a/src/transport/http/binary_emitter.ts b/src/transport/http/binary_emitter.ts index 20f51662..d4acdd8a 100644 --- a/src/transport/http/binary_emitter.ts +++ b/src/transport/http/binary_emitter.ts @@ -2,7 +2,8 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; import { CloudEvent, Version } from "../../event/cloudevent"; import { TransportOptions } from "../emitter"; -import { Headers, headersFor } from "./headers"; +import { Headers } from "../../messages"; +import { headersFor } from "../../messages/http/headers"; import { asData } from "../../event/validation"; import CONSTANTS from "../../constants"; diff --git a/src/transport/http/binary_receiver.ts b/src/transport/http/binary_receiver.ts deleted file mode 100644 index aa027f2b..00000000 --- a/src/transport/http/binary_receiver.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { CloudEvent, Version } from "../.."; -import { CloudEventV1, CloudEventV03 } from "../../event/interfaces"; -import { validateCloudEvent } from "../../event/spec"; -import { Headers, validate } from "./headers"; -import { v03binaryParsers, v1binaryParsers } from "./versions"; -import { parserByContentType, MappedParser } from "../../parsers"; -import { isString, isBase64, ValidationError, isStringOrObjectOrThrow } from "../../event/validation"; -import CONSTANTS from "../../constants"; - -/** - * A class that receives binary CloudEvents over HTTP. This class can be used - * if you know that all incoming events will be using binary transport. If - * events can come as either binary or structured, use {HTTPReceiver}. - */ -export class BinaryHTTPReceiver { - /** - * The specification version of the incoming cloud event - */ - version: Version; - constructor(version: Version = Version.V1) { - this.version = version; - } - - /** - * Parses an incoming HTTP request, converting it to a {CloudEvent} - * instance if it conforms to the Cloud Event specification for this receiver. - * - * @param {Object|string} payload the HTTP request body - * @param {Object} headers the HTTP request headers - * @param {Version} version the spec version of the incoming event - * @returns {CloudEvent} an instance of CloudEvent representing the incoming request - * @throws {ValidationError} of the event does not conform to the spec - */ - parse(payload: string | Record | undefined | null, headers: Headers): CloudEvent { - if (!headers) throw new ValidationError("headers is null or undefined"); - if (payload) { - isStringOrObjectOrThrow(payload, new ValidationError("payload must be an object or a string")); - } - - if ( - headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] && - headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] !== Version.V03 && - headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] !== Version.V1 - ) { - throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`); - } - - payload = isString(payload) && isBase64(payload) ? Buffer.from(payload as string, "base64").toString() : payload; - - // Clone and low case all headers names - const sanitizedHeaders = validate(headers); - - const eventObj: { [key: string]: unknown | string | Record } = {}; - const parserMap: Record = this.version === Version.V1 ? v1binaryParsers : v03binaryParsers; - - for (const header in parserMap) { - if (sanitizedHeaders[header]) { - const mappedParser: MappedParser = parserMap[header]; - eventObj[mappedParser.name] = mappedParser.parser.parse(sanitizedHeaders[header]); - delete sanitizedHeaders[header]; - } - } - - let parsedPayload; - - if (payload) { - const parser = parserByContentType[eventObj.datacontenttype as string]; - if (!parser) { - throw new ValidationError(`no parser found for content type ${eventObj.datacontenttype}`); - } - parsedPayload = parser.parse(payload); - } - - // Every unprocessed header can be an extension - for (const header in sanitizedHeaders) { - if (header.startsWith(CONSTANTS.EXTENSIONS_PREFIX)) { - eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = headers[header]; - } - } - // At this point, if the datacontenttype is application/json and the datacontentencoding is base64 - // then the data has already been decoded as a string, then parsed as JSON. We don't need to have - // the datacontentencoding property set - in fact, it's incorrect to do so. - if ( - eventObj.datacontenttype === CONSTANTS.MIME_JSON && - eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64 - ) { - delete eventObj.datacontentencoding; - } - - const cloudevent = new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03); - validateCloudEvent(cloudevent); - return cloudevent; - } -} diff --git a/src/transport/http/headers.ts b/src/transport/http/headers.ts deleted file mode 100644 index 044aef74..00000000 --- a/src/transport/http/headers.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { ValidationError, CloudEvent } from "../.."; -import { v03headerMap, v1headerMap } from "./versions"; -import { Version } from "../../event/cloudevent"; -import { MappedParser } from "../../parsers"; -import CONSTANTS from "../../constants"; - -/** - * An interface representing HTTP headers as key/value string pairs - */ -export interface Headers { - [key: string]: string; -} - -export const allowedContentTypes = [CONSTANTS.DEFAULT_CONTENT_TYPE, CONSTANTS.MIME_JSON, CONSTANTS.MIME_OCTET_STREAM]; -export const requiredHeaders = [ - CONSTANTS.CE_HEADERS.ID, - CONSTANTS.CE_HEADERS.SOURCE, - CONSTANTS.CE_HEADERS.TYPE, - CONSTANTS.CE_HEADERS.SPEC_VERSION, -]; - -/** - * Validates cloud event headers and their values - * @param {Headers} headers event transport headers for validation - * @throws {ValidationError} if the headers are invalid - * @return {boolean} true if headers are valid - */ -export function validate(headers: Headers): Headers { - const sanitizedHeaders = sanitize(headers); - - // if content-type exists, be sure it's an allowed type - const contentTypeHeader = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]; - const noContentType = !allowedContentTypes.includes(contentTypeHeader); - if (contentTypeHeader && noContentType) { - throw new ValidationError("invalid content type", [sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]]); - } - - requiredHeaders - .filter((required: string) => !sanitizedHeaders[required]) - .forEach((required: string) => { - throw new ValidationError(`header '${required}' not found`); - }); - - if (!sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]) { - sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON; - } - - return sanitizedHeaders; -} - -/** - * Returns the HTTP headers that will be sent for this event when the HTTP transmission - * mode is "binary". Events sent over HTTP in structured mode only have a single CE header - * and that is "ce-id", corresponding to the event ID. - * @param {CloudEvent} event a CloudEvent - * @returns {Object} the headers that will be sent for the event - */ -export function headersFor(event: CloudEvent): Headers { - const headers: Headers = {}; - let headerMap: Readonly<{ [key: string]: MappedParser }>; - if (event.specversion === Version.V1) { - headerMap = v1headerMap; - } else { - headerMap = v03headerMap; - } - - // iterate over the event properties - generate a header for each - Object.getOwnPropertyNames(event).forEach((property) => { - const value = event[property]; - if (value) { - const map: MappedParser | undefined = headerMap[property] as MappedParser; - if (map) { - headers[map.name] = map.parser.parse(value as string) as string; - } else if (property !== CONSTANTS.DATA_ATTRIBUTE && property !== `${CONSTANTS.DATA_ATTRIBUTE}_base64`) { - headers[`${CONSTANTS.EXTENSIONS_PREFIX}${property}`] = value as string; - } - } - }); - // Treat time specially, since it's handled with getters and setters in CloudEvent - if (event.time) { - headers[CONSTANTS.CE_HEADERS.TIME] = event.time as string; - } - return headers; -} - -/** - * Sanitizes incoming headers by lowercasing them and potentially removing - * encoding from the content-type header. - * @param {Headers} headers HTTP headers as key/value pairs - * @returns {Headers} the sanitized headers - */ -export function sanitize(headers: Headers): Headers { - const sanitized: Headers = {}; - - Array.from(Object.keys(headers)) - .filter((header) => Object.hasOwnProperty.call(headers, header)) - .forEach((header) => (sanitized[header.toLowerCase()] = headers[header])); - - return sanitized; -} diff --git a/src/transport/http/structured_receiver.ts b/src/transport/http/structured_receiver.ts deleted file mode 100644 index ef9b3a34..00000000 --- a/src/transport/http/structured_receiver.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { CloudEvent, Version } from "../.."; -import { Headers, sanitize } from "./headers"; -import { Parser, JSONParser, MappedParser, Base64Parser } from "../../parsers"; -import { parserByContentType } from "../../parsers"; -import { v1structuredParsers, v03structuredParsers } from "./versions"; -import { isString, isBase64, ValidationError, isStringOrObjectOrThrow } from "../../event/validation"; -import { CloudEventV1, CloudEventV03 } from "../../event/interfaces"; -import { validateCloudEvent } from "../../event/spec"; -import CONSTANTS from "../../constants"; - -/** - * A utility class used to receive structured CloudEvents - * over HTTP. - * @see {StructuredReceiver} - */ -export class StructuredHTTPReceiver { - /** - * The specification version of the incoming cloud event - */ - version: Version; - constructor(version: Version = Version.V1) { - this.version = version; - } - - /** - * Creates a new CloudEvent instance based on the provided payload and headers. - * - * @param {object} payload the cloud event data payload - * @param {object} headers the HTTP headers received for this cloud event - * @returns {CloudEvent} a new CloudEvent instance for the provided headers and payload - * @throws {ValidationError} if the payload and header combination do not conform to the spec - */ - parse(payload: Record | string | undefined | null, headers: Headers): CloudEvent { - if (!payload) throw new ValidationError("payload is null or undefined"); - if (!headers) throw new ValidationError("headers is null or undefined"); - isStringOrObjectOrThrow(payload, new ValidationError("payload must be an object or a string")); - - if ( - headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] && - headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] != Version.V03 && - headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] != Version.V1 - ) { - throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`); - } - - payload = isString(payload) && isBase64(payload) ? Buffer.from(payload as string, "base64").toString() : payload; - - // Clone and low case all headers names - const sanitizedHeaders = sanitize(headers); - - const contentType = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]; - const parser: Parser = contentType ? parserByContentType[contentType] : new JSONParser(); - if (!parser) throw new ValidationError(`invalid content type ${sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]}`); - const incoming = { ...(parser.parse(payload) as Record) }; - - const eventObj: { [key: string]: unknown } = {}; - const parserMap: Record = - this.version === Version.V1 ? v1structuredParsers : v03structuredParsers; - - for (const key in parserMap) { - const property = incoming[key]; - if (property) { - const parser: MappedParser = parserMap[key]; - eventObj[parser.name] = parser.parser.parse(property as string); - } - delete incoming[key]; - } - - // extensions are what we have left after processing all other properties - for (const key in incoming) { - eventObj[key] = incoming[key]; - } - - // ensure data content is correctly encoded - if (eventObj.data && eventObj.datacontentencoding) { - if (eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64 && !isBase64(eventObj.data)) { - throw new ValidationError("invalid payload"); - } else if (eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) { - const dataParser = new Base64Parser(); - eventObj.data = JSON.parse(dataParser.parse(eventObj.data as string)); - delete eventObj.datacontentencoding; - } - } - - const cloudevent = new CloudEvent(eventObj as CloudEventV1 | CloudEventV03); - - // Validates the event - validateCloudEvent(cloudevent); - return cloudevent; - } -} diff --git a/src/transport/receiver.ts b/src/transport/receiver.ts index 5c83ec57..3d53e5ca 100644 --- a/src/transport/receiver.ts +++ b/src/transport/receiver.ts @@ -1,33 +1,11 @@ -import { Headers, sanitize } from "./http/headers"; -import { CloudEvent, Version, ValidationError } from ".."; -import { BinaryHTTPReceiver as BinaryReceiver } from "./http/binary_receiver"; -import { StructuredHTTPReceiver as StructuredReceiver } from "./http/structured_receiver"; -import { CloudEventV1, CloudEventV03 } from "../event/interfaces"; -import CONSTANTS from "../constants"; - -/** - * An enum representing the two HTTP transport modes, binary and structured - */ -export enum Mode { - BINARY = "binary", - STRUCTURED = "structured", -} - -const receivers = { - v1: { - structured: new StructuredReceiver(Version.V1), - binary: new BinaryReceiver(Version.V1), - }, - v03: { - structured: new StructuredReceiver(Version.V03), - binary: new BinaryReceiver(Version.V03), - }, -}; +import { Headers, Message, HTTP } from "../messages"; +import { sanitize } from "../messages/http/headers"; +import { CloudEvent } from ".."; /** * A class to receive a CloudEvent from an HTTP POST request. */ -export class Receiver { +export const Receiver = { /** * Acceptor for an incoming HTTP CloudEvent POST. Can process * binary and structured incoming CloudEvents. @@ -36,64 +14,13 @@ export class Receiver { * @param {Object|JSON} body The body of the HTTP request * @return {CloudEvent} A new {CloudEvent} instance */ - static accept( - headers: Headers, - body: string | Record | CloudEventV1 | CloudEventV03 | undefined | null, - ): CloudEvent { + accept(headers: Headers, body: string | Record | undefined | null): CloudEvent { const cleanHeaders: Headers = sanitize(headers); - const mode: Mode = getMode(cleanHeaders); - const version = getVersion(mode, cleanHeaders, body); - switch (version) { - case Version.V1: - return receivers.v1[mode].parse(body, headers); - case Version.V03: - return receivers.v03[mode].parse(body, headers); - default: - console.error(`Unknown spec version ${version}. Default to ${Version.V1}`); - return receivers.v1[mode].parse(body, headers); - } - } -} - -/** - * Determines the HTTP transport mode (binary or structured) based - * on the incoming HTTP headers. - * @param {Headers} headers the incoming HTTP headers - * @returns {Mode} the transport mode - */ -function getMode(headers: Headers): Mode { - const contentType = headers[CONSTANTS.HEADER_CONTENT_TYPE]; - if (contentType && contentType.startsWith(CONSTANTS.MIME_CE)) { - return Mode.STRUCTURED; - } - if (headers[CONSTANTS.CE_HEADERS.ID]) { - return Mode.BINARY; - } - throw new ValidationError("no cloud event detected"); -} - -/** - * Determines the version of an incoming CloudEvent based on the - * HTTP headers or HTTP body, depending on transport mode. - * @param {Mode} mode the HTTP transport mode - * @param {Headers} headers the incoming HTTP headers - * @param {Record} body the HTTP request body - * @returns {Version} the CloudEvent specification version - */ -function getVersion( - mode: Mode, - headers: Headers, - body: string | Record | CloudEventV03 | CloudEventV1 | undefined | null, -) { - if (mode === Mode.BINARY) { - // Check the headers for the version - const versionHeader = headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]; - if (versionHeader) { - return versionHeader; - } - } else { - // structured mode - the version is in the body - return typeof body === "string" ? JSON.parse(body).specversion : (body as CloudEvent).specversion; - } - return Version.V1; -} + const cleanBody = body ? (typeof body === "object" ? JSON.stringify(body) : body) : ""; + const message: Message = { + headers: cleanHeaders, + body: cleanBody, + }; + return HTTP.toEvent(message); + }, +}; diff --git a/test/conformance/steps.ts b/test/conformance/steps.ts index f6e57bc6..8b3377de 100644 --- a/test/conformance/steps.ts +++ b/test/conformance/steps.ts @@ -1,8 +1,7 @@ -/* eslint-disable no-console */ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { assert } from "chai"; import { Given, When, Then, World } from "cucumber"; -import { Receiver } from "../../src"; +import { Message, Headers, HTTP } from "../../src"; // eslint-disable-next-line @typescript-eslint/no-var-requires const { HTTPParser } = require("http-parser-js"); @@ -14,20 +13,24 @@ Given("HTTP Protocol Binding is supported", function (this: World) { }); Given("an HTTP request", function (request: string) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const world = this; + // Create a Message from the incoming HTTP request + const message: Message = { + headers: {}, + body: "", + }; parser.onHeadersComplete = function (record: Record) { - world.headers = arrayToObject(record.headers); + message.headers = extractHeaders(record.headers); }; parser.onBody = function (body: Buffer, offset: number) { - world.body = body.slice(offset).toString(); + message.body = body.slice(offset).toString(); }; + this.message = message; parser.execute(Buffer.from(request), 0, request.length); return true; }); When("parsed as HTTP request", function () { - this.cloudevent = Receiver.accept(this.headers, this.body); + this.cloudevent = HTTP.toEvent(this.message); return true; }); @@ -47,8 +50,8 @@ Then("the data is equal to the following JSON:", function (json: string) { return true; }); -function arrayToObject(arr: []): Record { - const obj: Record = {}; +function extractHeaders(arr: []): Headers { + const obj: Headers = {}; // @ts-ignore return arr.reduce(({}, keyOrValue, index, arr) => { if (index % 2 === 0) { diff --git a/test/integration/http_emitter_test.ts b/test/integration/http_emitter_test.ts index 3c8a2034..afd1f9a3 100644 --- a/test/integration/http_emitter_test.ts +++ b/test/integration/http_emitter_test.ts @@ -6,7 +6,8 @@ import CONSTANTS from "../../src/constants"; const DEFAULT_CE_CONTENT_TYPE = CONSTANTS.DEFAULT_CE_CONTENT_TYPE; const DEFAULT_CONTENT_TYPE = CONSTANTS.DEFAULT_CONTENT_TYPE; -import { CloudEvent, Version, Emitter, Protocol, headersFor } from "../../src"; +import { CloudEvent, Version, Emitter, Protocol } from "../../src"; +import { headersFor } from "../../src/messages/http/headers"; import { AxiosResponse } from "axios"; const receiver = "https://cloudevents.io/"; diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts new file mode 100644 index 00000000..925ec746 --- /dev/null +++ b/test/integration/message_test.ts @@ -0,0 +1,79 @@ +import { expect } from "chai"; +import { CloudEvent, CONSTANTS, Version } from "../../src"; +import { Message, HTTP } from "../../src/messages"; + +const type = "org.cncf.cloudevents.example"; +const source = "urn:event:from:myapi/resource/123"; +const time = new Date(); +const subject = "subject.ext"; +const dataschema = "http://cloudevents.io/schema.json"; +const datacontenttype = "application/json"; +const id = "b46cf653-d48a-4b90-8dfa-355c01061361"; +const data = { + foo: "bar", +}; + +const ext1Name = "extension1"; +const ext1Value = "foobar"; +const ext2Name = "extension2"; +const ext2Value = "acme"; + +const fixture: CloudEvent = new CloudEvent({ + specversion: Version.V1, + id, + type, + source, + datacontenttype, + subject, + time, + dataschema, + data, + [ext1Name]: ext1Value, + [ext2Name]: ext2Value, +}); + +describe("HTTP transport messages", () => { + it("V1 binary Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.binary(fixture); + expect(message.body).to.equal(data); + // validate all headers + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype); + expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V1); + expect(message.headers[CONSTANTS.CE_HEADERS.ID]).to.equal(id); + expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(type); + expect(message.headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(source); + expect(message.headers[CONSTANTS.CE_HEADERS.SUBJECT]).to.equal(subject); + expect(message.headers[CONSTANTS.CE_HEADERS.TIME]).to.equal(fixture.time); + expect(message.headers[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]).to.equal(dataschema); + expect(message.headers[`ce-${ext1Name}`]).to.equal(ext1Value); + expect(message.headers[`ce-${ext2Name}`]).to.equal(ext2Value); + }); + + it("V1 structured Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.structured(fixture); + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); + // Parse the message body as JSON, then validate the attributes + const body = JSON.parse(message.body); + expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V1); + expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); + expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); + expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); + expect(body[CONSTANTS.CE_ATTRIBUTES.SUBJECT]).to.equal(subject); + expect(body[CONSTANTS.CE_ATTRIBUTES.TIME]).to.equal(fixture.time); + expect(body[CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA]).to.equal(dataschema); + expect(body[ext1Name]).to.equal(ext1Value); + expect(body[ext2Name]).to.equal(ext2Value); + }); + + it("V1 CloudEvent can be converted from a binary Message", () => { + const message = HTTP.binary(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); + + it("V1 CloudEvent can be converted from a structured Message", () => { + const message = HTTP.structured(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); +}); diff --git a/test/integration/receiver_binary_03_tests.ts b/test/integration/receiver_binary_03_tests.ts deleted file mode 100644 index dfdfd736..00000000 --- a/test/integration/receiver_binary_03_tests.ts +++ /dev/null @@ -1,443 +0,0 @@ -import "mocha"; -import { expect } from "chai"; - -import { CloudEvent, ValidationError, Version } from "../../src"; -import { BinaryHTTPReceiver } from "../../src/transport/http/binary_receiver"; -import CONSTANTS from "../../src/constants"; -import { asBase64 } from "../../src/event/validation"; - -const receiver = new BinaryHTTPReceiver(Version.V03); - -describe("HTTP Transport Binding Binary Receiver for CloudEvents v0.3", () => { - describe("Check", () => { - it("Throw error when attributes arg is null or undefined", () => { - // setup - const payload = {}; - const attributes = undefined; - - expect(receiver.parse.bind(receiver, payload, (attributes as unknown) as string)).to.throw( - ValidationError, - "headers is null or undefined", - ); - }); - - it("Throw error when payload is not an object or string", () => { - // setup - const payload = 1.2; - const attributes = {}; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "payload must be an object or a string", - ); - }); - - it("Throw error when headers has no 'ce-type'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "header 'ce-type' not found", - ); - }); - - it("Throw error when headers has no 'ce-specversion'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "header 'ce-specversion' not found", - ); - }); - - it("Throw error when headers has no 'ce-source'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "header 'ce-source' not found", - ); - }); - - it("Throw error when headers has no 'ce-id'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "header 'ce-id' not found"); - }); - - it("Throw error when spec is not 0.3", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: "0.2", - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid spec version"); - }); - - it("Throw error when the content-type is invalid", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "text/html", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid content type"); - }); - - it("No error when all required headers are in place", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw(); - }); - - it("No error when content-type is unspecified", () => { - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw(); - }); - - it("Succeeds when content-type is application/json and datacontentencoding is base64", () => { - const expected = { - whose: "ours", - }; - const bindata = Uint32Array.from(JSON.stringify(expected) as string, (c) => c.codePointAt(0) as number); - const payload = asBase64(bindata); - - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "test", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/test-source", - [CONSTANTS.CE_HEADERS.ID]: "123456", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - [CONSTANTS.BINARY_HEADERS_03.CONTENT_ENCODING]: "base64", - }; - const event = receiver.parse(payload, attributes); - expect(event.data).to.deep.equal(expected); - }); - }); - - describe("Parse", () => { - it("CloudEvent contains 'type'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.type).to.equal("type"); - }); - - it("CloudEvent contains 'specversion'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.specversion).to.equal(Version.V03); - }); - - it("CloudEvent contains 'source'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.source).to.equal("/source"); - }); - - it("CloudEvent contains 'id'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.id).to.equal("id"); - }); - - it("CloudEvent contains 'time'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.time).to.equal("2019-06-16T11:42:00.000Z"); - }); - - it("CloudEvent contains 'schemaurl'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.schemaurl).to.equal("http://schema.registry/v1"); - }); - - it("CloudEvent contains 'datacontenttype' (application/json)", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.datacontenttype).to.equal("application/json"); - }); - - it("CloudEvent contains 'datacontenttype' (application/octet-stream)", () => { - // setup - const payload = "The payload is binary data"; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/octet-stream", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.datacontenttype).to.equal("application/octet-stream"); - }); - - it("CloudEvent contains 'data' (application/json)", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.data).to.deep.equal(payload); - }); - - it("CloudEvent contains 'data' (application/octet-stream)", () => { - // setup - const payload = "The payload is binary data"; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/octet-stream", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.data).to.deep.equal(payload); - }); - - it("No error when all attributes are in place", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual).to.be.an.instanceof(CloudEvent); - }); - - it("Should accept 'extension1'", () => { - // setup - const extension1 = "mycuston-ext1"; - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - [`${[CONSTANTS.EXTENSIONS_PREFIX]}extension1`]: extension1, - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.extension1).to.equal(extension1); - }); - }); -}); diff --git a/test/integration/receiver_binary_1_tests.ts b/test/integration/receiver_binary_1_tests.ts deleted file mode 100644 index 368c55ed..00000000 --- a/test/integration/receiver_binary_1_tests.ts +++ /dev/null @@ -1,448 +0,0 @@ -import "mocha"; -import { expect } from "chai"; - -import { CloudEvent, ValidationError, Version } from "../../src"; -import { asBase64 } from "../../src/event/validation"; -import { BinaryHTTPReceiver } from "../../src/transport/http/binary_receiver"; -import CONSTANTS from "../../src/constants"; - -const receiver = new BinaryHTTPReceiver(Version.V1); - -describe("HTTP Transport Binding Binary Receiver for CloudEvents v1.0", () => { - describe("Check", () => { - it("Throw error when attributes arg is null or undefined", () => { - // setup - const payload = {}; - const attributes = undefined; - - expect(receiver.parse.bind(receiver, payload, (attributes as unknown) as string)).to.throw( - ValidationError, - "headers is null or undefined", - ); - }); - - it("Throw error when payload is not an object or string", () => { - // setup - const payload = 1.2; - const attributes = {}; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "payload must be an object or a string", - ); - }); - - it("Throw error when headers has no 'ce-type'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "header 'ce-type' not found", - ); - }); - - it("Throw error when headers has no 'ce-specversion'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "header 'ce-specversion' not found", - ); - }); - - it("Throw error when headers has no 'ce-source'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "header 'ce-source' not found", - ); - }); - - it("Throw error when headers has no 'ce-id'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "header 'ce-id' not found"); - }); - - it("Throw error when spec is not 1.0", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: "0.2", - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid spec version"); - }); - - it("Throw error when the content-type is invalid", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "text/html", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid content type"); - }); - - it("No error when content-type is unspecified", () => { - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw(); - }); - - it("No error when all required headers are in place", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw(); - }); - }); - - describe("Parse", () => { - it("CloudEvent contains 'type'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.type).to.equal("type"); - }); - - it("CloudEvent contains 'specversion'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.specversion).to.equal(Version.V1); - }); - - it("CloudEvent contains 'source'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.source).to.equal("/source"); - }); - - it("CloudEvent contains 'id'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.id).to.equal("id"); - }); - - it("CloudEvent contains 'time'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.time).to.equal("2019-06-16T11:42:00.000Z"); - }); - - it("CloudEvent contains 'dataschema'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.dataschema).to.equal("http://schema.registry/v1"); - }); - - it("CloudEvent contains 'contenttype' (application/json)", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.datacontenttype).to.equal("application/json"); - }); - - it("CloudEvent contains 'contenttype' (application/octet-stream)", () => { - // setup - const payload = "The payload is binary data"; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/octet-stream", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.datacontenttype).to.equal("application/octet-stream"); - }); - - it("CloudEvent contains 'data' (application/json)", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.data).to.deep.equal(payload); - }); - - it("CloudEvent contains 'data' (application/octet-stream)", () => { - // setup - const payload = "The payload is binary data"; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/octet-stream", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.data).to.deep.equal(payload); - }); - - it("The content of 'data' is base64 for binary", () => { - // setup - const expected = { - data: "dataString", - }; - const bindata = Uint32Array.from(JSON.stringify(expected) as string, (c) => c.codePointAt(0) as number); - const payload = asBase64(bindata); - - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.data).to.deep.equal(expected); - }); - - it("No error when all attributes are in place", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual).to.be.an.instanceof(CloudEvent); - }); - - it("Should accept 'extension1'", () => { - // setup - const extension1 = "mycustom-ext1"; - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - [`${[CONSTANTS.EXTENSIONS_PREFIX]}extension1`]: extension1, - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.extension1).to.equal(extension1); - }); - }); -}); diff --git a/test/integration/receiver_structured_0_3_test.ts b/test/integration/receiver_structured_0_3_test.ts deleted file mode 100644 index d47220bf..00000000 --- a/test/integration/receiver_structured_0_3_test.ts +++ /dev/null @@ -1,227 +0,0 @@ -import "mocha"; -import { expect } from "chai"; - -import { CloudEvent, ValidationError, Version } from "../../src"; -import { StructuredHTTPReceiver } from "../../src/transport/http/structured_receiver"; -import { asBase64 } from "../../src/event/validation"; -import CONSTANTS from "../../src/constants"; - -const receiver = new StructuredHTTPReceiver(Version.V03); -const type = "com.github.pull.create"; -const source = "urn:event:from:myapi/resourse/123"; -const time = new Date(); -const schemaurl = "http://cloudevents.io/schema.json"; - -const ceContentType = "application/json"; - -const data = { - foo: "bar", -}; - -describe("HTTP Transport Binding Structured Receiver CloudEvents v0.3", () => { - describe("Check", () => { - it("Throw error when payload arg is null or undefined", () => { - // setup - const payload = null; - const attributes = {}; - - // act and assert - expect(receiver.parse.bind(receiver, (payload as unknown) as string, attributes)).to.throw( - ValidationError, - "payload is null or undefined", - ); - }); - - it("Throw error when attributes arg is null or undefined", () => { - // setup - const payload = {}; - const attributes = null; - - expect(receiver.parse.bind(receiver, payload, (attributes as unknown) as string)).to.throw( - ValidationError, - "headers is null or undefined", - ); - }); - - it("Throw error when payload is not an object or string", () => { - // setup - const payload = 1.0; - const attributes = {}; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "payload must be an object or a string", - ); - }); - - it("Throw error when the content-type is invalid", () => { - // setup - const payload = {}; - const attributes = { - "Content-Type": "text/html", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid content type"); - }); - - it("Throw error data content encoding is base64, but 'data' is not", () => { - // setup - const event = { - specversion: Version.V03, - type, - source, - time, - datacontenttype: "text/plain", - datacontentencoding: "base64", - schemaurl, - data: "No base 64 value", - }; - - const attributes = { - "Content-Type": "application/cloudevents+json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, event, attributes)).to.throw(ValidationError, "invalid payload"); - }); - - it("Succeeds when content-type is application/cloudevents+json and datacontentencoding is base64", () => { - const expected = { - whose: "ours", - }; - const bindata = Uint32Array.from(JSON.stringify(expected) as string, (c) => c.codePointAt(0) as number); - const payload = { - data: asBase64(bindata), - specversion: Version.V03, - source, - type, - datacontentencoding: CONSTANTS.ENCODING_BASE64, - }; - const attributes = { - "Content-Type": "application/cloudevents+json", - }; - - const event = receiver.parse(payload, attributes); - expect(event.data).to.deep.equal(expected); - }); - - it("No error when all required stuff are in place", () => { - // setup - const payload = { - specversion: Version.V03, - source, - type, - }; - const attributes = { - "Content-Type": "application/cloudevents+json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw(); - }); - }); - - describe("Parse", () => { - it("Throw error when the event does not follow the spec", () => { - // setup - const payload = {}; - const attributes = { - "Content-Type": "application/cloudevents+json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(); - }); - }); - - describe("Parse", () => { - it("Throw error when the event does not follow the spec", () => { - const payload = { - type, - source, - time, - schemaurl, - data, - }; - - const headers = { - "Content-Type": "application/cloudevents+xml", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, headers)).to.throw(ValidationError, "invalid content type"); - }); - - it("Should accept event that follows the spec", () => { - // setup - const id = "id-x0dk"; - const payload = { - specversion: Version.V03, - id, - type, - source, - time, - schemaurl, - datacontenttype: ceContentType, - data, - }; - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(payload, headers); - - // assert - expect(actual).to.be.an.instanceof(CloudEvent); - expect(actual.id).to.equal(id); - }); - - it("Should accept 'extension1'", () => { - // setup - const extension1 = "mycuston-ext1"; - const payload = { - specversion: Version.V03, - type, - source, - time, - schemaurl, - data, - datacontenttype: ceContentType, - extension1: extension1, - }; - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(payload, headers); - - // assert - expect(actual.extension1).to.equal(extension1); - }); - - it("Should parse 'data' stringfied json to json object", () => { - const payload = { - specversion: Version.V03, - type, - source, - time, - schemaurl, - datacontenttype: ceContentType, - data: JSON.stringify(data), - }; - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(payload, headers); - - // assert - expect(actual.data).to.deep.equal(data); - }); - }); -}); diff --git a/test/integration/receiver_structured_1_test.ts b/test/integration/receiver_structured_1_test.ts deleted file mode 100644 index 250dab9c..00000000 --- a/test/integration/receiver_structured_1_test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import "mocha"; -import { expect } from "chai"; - -import { CloudEvent, ValidationError, Version } from "../../src"; -import { asBase64 } from "../../src/event/validation"; -import { StructuredHTTPReceiver } from "../../src/transport/http/structured_receiver"; - -const receiver = new StructuredHTTPReceiver(Version.V1); -const type = "com.github.pull.create"; -const source = "urn:event:from:myapi/resourse/123"; -const time = new Date(); -const dataschema = "http://cloudevents.io/schema.json"; - -const data = { - foo: "bar", -}; - -describe("HTTP Transport Binding Structured Receiver for CloudEvents v1.0", () => { - describe("Check", () => { - it("Throw error when payload arg is null or undefined", () => { - // setup - const payload = null; - const attributes = {}; - - // act and assert - expect(receiver.parse.bind(receiver, (payload as unknown) as string, attributes)).to.throw( - ValidationError, - "payload is null or undefined", - ); - }); - - it("Throw error when attributes arg is null or undefined", () => { - // setup - const payload = {}; - const attributes = null; - - expect(receiver.parse.bind(receiver, payload, (attributes as unknown) as string)).to.throw( - ValidationError, - "headers is null or undefined", - ); - }); - - it("Throw error when payload is not an object or string", () => { - // setup - const payload = 1.0; - const attributes = {}; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "payload must be an object or a string", - ); - }); - - it("Throw error when the content-type is invalid", () => { - // setup - const payload = {}; - const attributes = { - "Content-Type": "text/html", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid content type"); - }); - - it("No error when all required stuff are in place", () => { - // setup - const payload = { - source, - type, - }; - const attributes = { - "Content-Type": "application/cloudevents+json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw(); - }); - }); - - describe("Parse", () => { - it("Throw error when the event does not follow the spec", () => { - // setup - const payload = { - type, - source, - time, - data, - }; - - const headers = { - "Content-Type": "application/cloudevents+xml", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, headers)).to.throw(ValidationError, "invalid content type"); - }); - - it("Should accept event that follows the spec", () => { - // setup - const id = "id-x0dk"; - const payload = { - id, - type, - source, - time, - data, - dataschema, - }; - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(payload, headers); - - // assert - expect(actual).to.be.an.instanceof(CloudEvent); - expect(actual.id).to.equal(id); - }); - - it("Should accept 'extension1'", () => { - // setup - const extension1 = "mycustomext1"; - const event = { - type, - source, - time, - data, - dataschema, - extension1, - }; - - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(event, headers); - expect(actual.extension1).to.equal(extension1); - }); - - it("Should parse 'data' stringified json to json object", () => { - // setup - const payload = { - type, - source, - time, - dataschema, - data: data, - }; - - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(payload, headers); - - // assert - expect(actual.data).to.deep.equal(data); - }); - - it("Should maps 'data_base64' to 'data' attribute", () => { - const bindata = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number); - const expected = asBase64(bindata); - const payload = { - type, - source, - data: bindata, - }; - - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(payload, headers); - expect(actual.data_base64).to.equal(expected); - }); - }); -}); diff --git a/test/integration/sdk_test.ts b/test/integration/sdk_test.ts index a949ed0a..d0aa3d1f 100644 --- a/test/integration/sdk_test.ts +++ b/test/integration/sdk_test.ts @@ -1,6 +1,6 @@ import "mocha"; import { expect } from "chai"; -import { CloudEvent, Receiver, Emitter, Version } from "../../src"; +import { CloudEvent, Emitter, Version } from "../../src"; const fixture = { type: "org.cloudevents.test", @@ -13,11 +13,6 @@ describe("The SDK Requirements", () => { expect(event instanceof CloudEvent).to.equal(true); }); - it("should expose a Receiver type", () => { - const receiver = new Receiver(); - expect(receiver instanceof Receiver).to.equal(true); - }); - it("should expose an Emitter type", () => { const emitter = new Emitter({ url: "http://example.com", From ea76d5f165951b6f1a9422450f0f957f80e846eb Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Sun, 23 Aug 2020 17:36:15 -0400 Subject: [PATCH 02/12] src(message): make message singular Signed-off-by: Lance Ball --- src/index.ts | 4 ++-- src/{messages => message}/http/headers.ts | 0 src/{messages => message}/http/index.ts | 0 src/{messages => message}/index.ts | 0 src/transport/http/binary_emitter.ts | 4 ++-- src/transport/receiver.ts | 4 ++-- test/integration/http_emitter_test.ts | 2 +- test/integration/message_test.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) rename src/{messages => message}/http/headers.ts (100%) rename src/{messages => message}/http/index.ts (100%) rename src/{messages => message}/index.ts (100%) diff --git a/src/index.ts b/src/index.ts index 69cf0562..8d47d13d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import { CloudEventV03, CloudEventV03Attributes, CloudEventV1, CloudEventV1Attri import { Emitter, TransportOptions } from "./transport/emitter"; import { Receiver } from "./transport/receiver"; import { Protocol } from "./transport/protocols"; -import { Headers, Mode, Binding, HTTP, Message, Serializer, Deserializer } from "./messages"; +import { Headers, Mode, Binding, HTTP, Message, Serializer, Deserializer } from "./message"; import CONSTANTS from "./constants"; @@ -18,7 +18,7 @@ export { CloudEventV1Attributes, Version, ValidationError, - // From messages + // From message Headers, Mode, Binding, diff --git a/src/messages/http/headers.ts b/src/message/http/headers.ts similarity index 100% rename from src/messages/http/headers.ts rename to src/message/http/headers.ts diff --git a/src/messages/http/index.ts b/src/message/http/index.ts similarity index 100% rename from src/messages/http/index.ts rename to src/message/http/index.ts diff --git a/src/messages/index.ts b/src/message/index.ts similarity index 100% rename from src/messages/index.ts rename to src/message/index.ts diff --git a/src/transport/http/binary_emitter.ts b/src/transport/http/binary_emitter.ts index d4acdd8a..cabf9066 100644 --- a/src/transport/http/binary_emitter.ts +++ b/src/transport/http/binary_emitter.ts @@ -2,8 +2,8 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; import { CloudEvent, Version } from "../../event/cloudevent"; import { TransportOptions } from "../emitter"; -import { Headers } from "../../messages"; -import { headersFor } from "../../messages/http/headers"; +import { Headers } from "../../message"; +import { headersFor } from "../../message/http/headers"; import { asData } from "../../event/validation"; import CONSTANTS from "../../constants"; diff --git a/src/transport/receiver.ts b/src/transport/receiver.ts index 3d53e5ca..8d8d86f0 100644 --- a/src/transport/receiver.ts +++ b/src/transport/receiver.ts @@ -1,5 +1,5 @@ -import { Headers, Message, HTTP } from "../messages"; -import { sanitize } from "../messages/http/headers"; +import { Headers, Message, HTTP } from "../message"; +import { sanitize } from "../message/http/headers"; import { CloudEvent } from ".."; /** diff --git a/test/integration/http_emitter_test.ts b/test/integration/http_emitter_test.ts index afd1f9a3..e9f184bc 100644 --- a/test/integration/http_emitter_test.ts +++ b/test/integration/http_emitter_test.ts @@ -7,7 +7,7 @@ const DEFAULT_CE_CONTENT_TYPE = CONSTANTS.DEFAULT_CE_CONTENT_TYPE; const DEFAULT_CONTENT_TYPE = CONSTANTS.DEFAULT_CONTENT_TYPE; import { CloudEvent, Version, Emitter, Protocol } from "../../src"; -import { headersFor } from "../../src/messages/http/headers"; +import { headersFor } from "../../src/message/http/headers"; import { AxiosResponse } from "axios"; const receiver = "https://cloudevents.io/"; diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index 925ec746..e43c8aa1 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { CloudEvent, CONSTANTS, Version } from "../../src"; -import { Message, HTTP } from "../../src/messages"; +import { Message, HTTP } from "../../src/message"; const type = "org.cncf.cloudevents.example"; const source = "urn:event:from:myapi/resource/123"; From 6f66d5c82d6a060771917e367060599e83c850ee Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Mon, 24 Aug 2020 09:53:08 -0400 Subject: [PATCH 03/12] fixup: fix base64 decoding of structured event data Signed-off-by: Lance Ball --- src/message/http/index.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/message/http/index.ts b/src/message/http/index.ts index b850652a..9e6519b8 100644 --- a/src/message/http/index.ts +++ b/src/message/http/index.ts @@ -168,7 +168,7 @@ function parseBinary(message: Message, version: Version): CloudEvent { * @throws {ValidationError} if the payload and header combination do not conform to the spec */ function parseStructured(message: Message, version: Version): CloudEvent { - let payload = message.body; + const payload = message.body; const headers = message.headers; if (!payload) throw new ValidationError("payload is null or undefined"); @@ -183,8 +183,6 @@ function parseStructured(message: Message, version: Version): CloudEvent { throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`); } - payload = isString(payload) && isBase64(payload) ? Buffer.from(payload as string, "base64").toString() : payload; - // Clone and low case all headers names const sanitizedHeaders = sanitize(headers); @@ -210,17 +208,12 @@ function parseStructured(message: Message, version: Version): CloudEvent { eventObj[key] = incoming[key]; } - // ensure data content is correctly encoded - if (eventObj.data && eventObj.datacontentencoding) { - if (eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64 && !isBase64(eventObj.data)) { - throw new ValidationError("invalid payload"); - } else if (eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) { - const dataParser = new Base64Parser(); - eventObj.data = JSON.parse(dataParser.parse(eventObj.data as string)); - delete eventObj.datacontentencoding; - } + // ensure data content is correctly decoded + if (eventObj.data_base64) { + const parser = new Base64Parser(); + eventObj.data = JSON.parse(parser.parse(eventObj.data_base64 as string)); + delete eventObj.data_base64; } - const cloudevent = new CloudEvent(eventObj as CloudEventV1 | CloudEventV03); // Validates the event From 1b54edd1bf038534648bbbc3aa0c915cc80b81aa Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Mon, 24 Aug 2020 10:46:17 -0400 Subject: [PATCH 04/12] fixup: export headersFor Signed-off-by: Lance Ball --- src/index.ts | 11 ++++++----- src/message/index.ts | 4 ++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8d47d13d..11149ff8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import { CloudEventV03, CloudEventV03Attributes, CloudEventV1, CloudEventV1Attri import { Emitter, TransportOptions } from "./transport/emitter"; import { Receiver } from "./transport/receiver"; import { Protocol } from "./transport/protocols"; -import { Headers, Mode, Binding, HTTP, Message, Serializer, Deserializer } from "./message"; +import { Headers, Mode, Binding, HTTP, Message, Serializer, Deserializer, headersFor } from "./message"; import CONSTANTS from "./constants"; @@ -25,12 +25,13 @@ export { Message, Deserializer, Serializer, + headersFor, // TODO: Deprecated. Remove for 4.0 HTTP, // From transport - Emitter, - Receiver, - Protocol, - TransportOptions, + Emitter, // TODO: Deprecated. Remove for 4.0 + Receiver, // TODO: Deprecated. Remove for 4.0 + Protocol, // TODO: Deprecated. Remove for 4.0 + TransportOptions, // TODO: Deprecated. Remove for 4.0 // From Constants CONSTANTS, }; diff --git a/src/message/index.ts b/src/message/index.ts index 995e0aee..062537fb 100644 --- a/src/message/index.ts +++ b/src/message/index.ts @@ -1,5 +1,6 @@ import { CloudEvent } from ".."; import { binary, deserialize, structured } from "./http"; +import { headersFor } from "./http/headers"; /** * Binding is an interface for transport protocols to implement, @@ -59,3 +60,6 @@ export const HTTP: Binding = { structured: structured as Serializer, toEvent: deserialize as Deserializer, }; + +// TODO: Deprecated. Remove this for 4.0 +export { headersFor }; From 5c82b75c73aae243c442bfd8b1b3b382a196e618 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Mon, 24 Aug 2020 11:36:39 -0400 Subject: [PATCH 05/12] test(message): add tests for v03 messages Signed-off-by: Lance Ball --- examples/express-ex/index.js | 25 +++-- test/integration/message_test.ts | 170 +++++++++++++++++++++---------- 2 files changed, 137 insertions(+), 58 deletions(-) diff --git a/examples/express-ex/index.js b/examples/express-ex/index.js index 57a71e1c..7b4ce02e 100644 --- a/examples/express-ex/index.js +++ b/examples/express-ex/index.js @@ -1,8 +1,8 @@ -/* eslint-disable no-console */ +/* eslint-disable */ const express = require("express"); -const {Receiver} = require("cloudevents"); - +const { Message, CloudEvent, HTTP } = require("cloudevents"); +const { response } = require("express"); const app = express(); app.use((req, res, next) => { @@ -23,10 +23,23 @@ app.post("/", (req, res) => { console.log("HEADERS", req.headers); console.log("BODY", req.body); + const message = { + headers: req.headers, + body: req.body + }; + try { - const event = Receiver.accept(req.headers, req.body); - console.log(`Accepted event: ${event}`); - res.status(201).json(event); + const event = HTTP.toEvent(message); + // respond as an event + const responseEventMessage = new CloudEvent({ + source: '/', + type: 'event:response', + ...event + }); + responseEventMessage.data = { + hello: 'world' + }; + res.status(201).json(responseEventMessage); } catch (err) { console.error(err); res.status(415).header("Content-Type", "application/json").send(JSON.stringify(err)); diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index e43c8aa1..a6004ece 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -13,67 +13,133 @@ const data = { foo: "bar", }; +// Attributes for v03 events +const schemaurl = "https://cloudevents.io/schema.json"; +const datacontentencoding = "base64"; + const ext1Name = "extension1"; const ext1Value = "foobar"; const ext2Name = "extension2"; const ext2Value = "acme"; -const fixture: CloudEvent = new CloudEvent({ - specversion: Version.V1, - id, - type, - source, - datacontenttype, - subject, - time, - dataschema, - data, - [ext1Name]: ext1Value, - [ext2Name]: ext2Value, -}); - describe("HTTP transport messages", () => { - it("V1 binary Messages can be created from a CloudEvent", () => { - const message: Message = HTTP.binary(fixture); - expect(message.body).to.equal(data); - // validate all headers - expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype); - expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V1); - expect(message.headers[CONSTANTS.CE_HEADERS.ID]).to.equal(id); - expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(type); - expect(message.headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(source); - expect(message.headers[CONSTANTS.CE_HEADERS.SUBJECT]).to.equal(subject); - expect(message.headers[CONSTANTS.CE_HEADERS.TIME]).to.equal(fixture.time); - expect(message.headers[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]).to.equal(dataschema); - expect(message.headers[`ce-${ext1Name}`]).to.equal(ext1Value); - expect(message.headers[`ce-${ext2Name}`]).to.equal(ext2Value); - }); + describe("Specification version V1", () => { + const fixture: CloudEvent = new CloudEvent({ + specversion: Version.V1, + id, + type, + source, + datacontenttype, + subject, + time, + dataschema, + data, + [ext1Name]: ext1Value, + [ext2Name]: ext2Value, + }); - it("V1 structured Messages can be created from a CloudEvent", () => { - const message: Message = HTTP.structured(fixture); - expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); - // Parse the message body as JSON, then validate the attributes - const body = JSON.parse(message.body); - expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V1); - expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); - expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); - expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); - expect(body[CONSTANTS.CE_ATTRIBUTES.SUBJECT]).to.equal(subject); - expect(body[CONSTANTS.CE_ATTRIBUTES.TIME]).to.equal(fixture.time); - expect(body[CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA]).to.equal(dataschema); - expect(body[ext1Name]).to.equal(ext1Value); - expect(body[ext2Name]).to.equal(ext2Value); - }); + it("Binary Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.binary(fixture); + expect(message.body).to.equal(data); + // validate all headers + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype); + expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V1); + expect(message.headers[CONSTANTS.CE_HEADERS.ID]).to.equal(id); + expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(type); + expect(message.headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(source); + expect(message.headers[CONSTANTS.CE_HEADERS.SUBJECT]).to.equal(subject); + expect(message.headers[CONSTANTS.CE_HEADERS.TIME]).to.equal(fixture.time); + expect(message.headers[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]).to.equal(dataschema); + expect(message.headers[`ce-${ext1Name}`]).to.equal(ext1Value); + expect(message.headers[`ce-${ext2Name}`]).to.equal(ext2Value); + }); + + it("Structured Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.structured(fixture); + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); + // Parse the message body as JSON, then validate the attributes + const body = JSON.parse(message.body); + expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V1); + expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); + expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); + expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); + expect(body[CONSTANTS.CE_ATTRIBUTES.SUBJECT]).to.equal(subject); + expect(body[CONSTANTS.CE_ATTRIBUTES.TIME]).to.equal(fixture.time); + expect(body[CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA]).to.equal(dataschema); + expect(body[ext1Name]).to.equal(ext1Value); + expect(body[ext2Name]).to.equal(ext2Value); + }); - it("V1 CloudEvent can be converted from a binary Message", () => { - const message = HTTP.binary(fixture); - const event = HTTP.toEvent(message); - expect(event).to.deep.equal(fixture); + it("A CloudEvent can be converted from a binary Message", () => { + const message = HTTP.binary(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); + + it("A CloudEvent can be converted from a structured Message", () => { + const message = HTTP.structured(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); }); - it("V1 CloudEvent can be converted from a structured Message", () => { - const message = HTTP.structured(fixture); - const event = HTTP.toEvent(message); - expect(event).to.deep.equal(fixture); + describe("Specification version V03", () => { + const fixture: CloudEvent = new CloudEvent({ + specversion: Version.V03, + id, + type, + source, + datacontenttype, + subject, + time, + schemaurl, + data, + [ext1Name]: ext1Value, + [ext2Name]: ext2Value, + }); + + it("Binary Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.binary(fixture); + expect(message.body).to.equal(data); + // validate all headers + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype); + expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V03); + expect(message.headers[CONSTANTS.CE_HEADERS.ID]).to.equal(id); + expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(type); + expect(message.headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(source); + expect(message.headers[CONSTANTS.CE_HEADERS.SUBJECT]).to.equal(subject); + expect(message.headers[CONSTANTS.CE_HEADERS.TIME]).to.equal(fixture.time); + expect(message.headers[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]).to.equal(schemaurl); + expect(message.headers[`ce-${ext1Name}`]).to.equal(ext1Value); + expect(message.headers[`ce-${ext2Name}`]).to.equal(ext2Value); + }); + + it("Structured Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.structured(fixture); + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); + // Parse the message body as JSON, then validate the attributes + const body = JSON.parse(message.body); + expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V03); + expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); + expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); + expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); + expect(body[CONSTANTS.CE_ATTRIBUTES.SUBJECT]).to.equal(subject); + expect(body[CONSTANTS.CE_ATTRIBUTES.TIME]).to.equal(fixture.time); + expect(body[CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL]).to.equal(schemaurl); + expect(body[ext1Name]).to.equal(ext1Value); + expect(body[ext2Name]).to.equal(ext2Value); + }); + + it("A CloudEvent can be converted from a binary Message", () => { + const message = HTTP.binary(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); + + it("V1 CloudEvent can be converted from a structured Message", () => { + const message = HTTP.structured(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); }); }); From 4a2b9f4e770fed17aa94ee76c6afcf2301d0d1cc Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Mon, 24 Aug 2020 13:59:20 -0400 Subject: [PATCH 06/12] test(message): add v1 tests for base64 encoded message data Signed-off-by: Lance Ball --- src/event/cloudevent.ts | 6 +++++ test/integration/message_test.ts | 43 ++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/event/cloudevent.ts b/src/event/cloudevent.ts index 2cb571a7..197b4bc1 100644 --- a/src/event/cloudevent.ts +++ b/src/event/cloudevent.ts @@ -156,6 +156,12 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { this.#_data = value; } + /** + * Used by JSON.stringify(). The name is confusing, but this method is called by + * JSON.stringify() when converting this object to JSON. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify + * @return {object} this event as a plain object + */ toJSON(): Record { const event = { ...this }; event.time = this.time; diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index a6004ece..36f379b4 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -1,5 +1,6 @@ import { expect } from "chai"; -import { CloudEvent, CONSTANTS, Version } from "../../src"; +import { CloudEvent, CONSTANTS, Protocol, Version } from "../../src"; +import { asBase64 } from "../../src/event/validation"; import { Message, HTTP } from "../../src/message"; const type = "org.cncf.cloudevents.example"; @@ -22,6 +23,10 @@ const ext1Value = "foobar"; const ext2Name = "extension2"; const ext2Value = "acme"; +// Binary data as base64 +const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number); +const data_base64 = asBase64(dataBinary); + describe("HTTP transport messages", () => { describe("Specification version V1", () => { const fixture: CloudEvent = new CloudEvent({ @@ -81,6 +86,22 @@ describe("HTTP transport messages", () => { const event = HTTP.toEvent(message); expect(event).to.deep.equal(fixture); }); + + it("Supports Base-64 encoded data in structured messages", () => { + const event = fixture.cloneWith({ data: dataBinary }); + expect(event.data_base64).to.equal(data_base64); + const message = HTTP.structured(event); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); + + it("Supports Base-64 encoded data in binary messages", () => { + const event = fixture.cloneWith({ data: dataBinary }); + expect(event.data_base64).to.equal(data_base64); + const message = HTTP.binary(event); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); }); describe("Specification version V03", () => { @@ -136,10 +157,28 @@ describe("HTTP transport messages", () => { expect(event).to.deep.equal(fixture); }); - it("V1 CloudEvent can be converted from a structured Message", () => { + it("A CloudEvent can be converted from a structured Message", () => { const message = HTTP.structured(fixture); const event = HTTP.toEvent(message); expect(event).to.deep.equal(fixture); }); }); }); + +// function base64Message(event: CloudEvent, protocol: Protocol): Message { +// let message: Message; +// if (protocol === Protocol.HTTPBinary) { +// message = HTTP.binary(event); +// message.body = data_base64; +// } else { +// // construct a Message where we can add data as base64 +// const message = HTTP.structured(event); +// // parse the message body where the event has been stringified +// const attributes = JSON.parse(message.body); +// // set a data_base64 attribute and delete the original data +// attributes.data_base64 = data_base64; +// delete attributes.data; +// message.body = JSON.stringify(attributes); +// } +// return message; +// } From 3d27f8bba4501e8b4dfe63ed2714e5bcfd51d75d Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Mon, 24 Aug 2020 14:00:50 -0400 Subject: [PATCH 07/12] test(message): add v03 tests for base64 encoded message data Signed-off-by: Lance Ball --- test/integration/message_test.ts | 34 +++++++++++++++----------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index 36f379b4..61cb233b 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -162,23 +162,21 @@ describe("HTTP transport messages", () => { const event = HTTP.toEvent(message); expect(event).to.deep.equal(fixture); }); + + it("Supports Base-64 encoded data in structured messages", () => { + const event = fixture.cloneWith({ data: dataBinary }); + expect(event.data_base64).to.equal(data_base64); + const message = HTTP.structured(event); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); + + it("Supports Base-64 encoded data in binary messages", () => { + const event = fixture.cloneWith({ data: dataBinary }); + expect(event.data_base64).to.equal(data_base64); + const message = HTTP.binary(event); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); }); }); - -// function base64Message(event: CloudEvent, protocol: Protocol): Message { -// let message: Message; -// if (protocol === Protocol.HTTPBinary) { -// message = HTTP.binary(event); -// message.body = data_base64; -// } else { -// // construct a Message where we can add data as base64 -// const message = HTTP.structured(event); -// // parse the message body where the event has been stringified -// const attributes = JSON.parse(message.body); -// // set a data_base64 attribute and delete the original data -// attributes.data_base64 = data_base64; -// delete attributes.data; -// message.body = JSON.stringify(attributes); -// } -// return message; -// } From 038d73ef41d82c18cdf3274871a171b140720ae1 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Mon, 24 Aug 2020 14:15:38 -0400 Subject: [PATCH 08/12] test(message): use datacontentencoding for binary v03 messages Signed-off-by: Lance Ball --- src/message/http/index.ts | 1 + test/integration/message_test.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/message/http/index.ts b/src/message/http/index.ts index 9e6519b8..946316f5 100644 --- a/src/message/http/index.ts +++ b/src/message/http/index.ts @@ -213,6 +213,7 @@ function parseStructured(message: Message, version: Version): CloudEvent { const parser = new Base64Parser(); eventObj.data = JSON.parse(parser.parse(eventObj.data_base64 as string)); delete eventObj.data_base64; + delete eventObj.datacontentencoding; } const cloudevent = new CloudEvent(eventObj as CloudEventV1 | CloudEventV03); diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index 61cb233b..8bc9da78 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { CloudEvent, CONSTANTS, Protocol, Version } from "../../src"; +import { CloudEvent, CONSTANTS, Version } from "../../src"; import { asBase64 } from "../../src/event/validation"; import { Message, HTTP } from "../../src/message"; @@ -164,7 +164,7 @@ describe("HTTP transport messages", () => { }); it("Supports Base-64 encoded data in structured messages", () => { - const event = fixture.cloneWith({ data: dataBinary }); + const event = fixture.cloneWith({ data: dataBinary, datacontentencoding }); expect(event.data_base64).to.equal(data_base64); const message = HTTP.structured(event); const eventDeserialized = HTTP.toEvent(message); @@ -172,7 +172,7 @@ describe("HTTP transport messages", () => { }); it("Supports Base-64 encoded data in binary messages", () => { - const event = fixture.cloneWith({ data: dataBinary }); + const event = fixture.cloneWith({ data: dataBinary, datacontentencoding }); expect(event.data_base64).to.equal(data_base64); const message = HTTP.binary(event); const eventDeserialized = HTTP.toEvent(message); From 02ad12cbae2ac4e0d7b6e9a0c50067b435c53b83 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Mon, 24 Aug 2020 15:05:38 -0400 Subject: [PATCH 09/12] src(message): adds Detector interface to ease detection of events Fixes: https://github.com/cloudevents/sdk-javascript/issues/295 Signed-off-by: Lance Ball --- src/message/http/index.ts | 11 +++++++++++ src/message/index.ts | 12 +++++++++++- test/integration/message_test.ts | 20 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/message/http/index.ts b/src/message/http/index.ts index 946316f5..c93917de 100644 --- a/src/message/http/index.ts +++ b/src/message/http/index.ts @@ -26,6 +26,17 @@ export function structured(event: CloudEvent): Message { }; } +// implements Detector +// TODO: this could probably be optimized +export function isEvent(message: Message): boolean { + try { + deserialize(message); + return true; + } catch (err) { + return false; + } +} + /** * Converts a Message to a CloudEvent * diff --git a/src/message/index.ts b/src/message/index.ts index 062537fb..14ed270c 100644 --- a/src/message/index.ts +++ b/src/message/index.ts @@ -1,5 +1,5 @@ import { CloudEvent } from ".."; -import { binary, deserialize, structured } from "./http"; +import { binary, deserialize, structured, isEvent } from "./http"; import { headersFor } from "./http/headers"; /** @@ -11,6 +11,7 @@ export interface Binding { binary: Serializer; structured: Serializer; toEvent: Deserializer; + isEvent: Detector; } /** @@ -54,11 +55,20 @@ export interface Deserializer { (message: Message): CloudEvent; } +/** + * Detector is a function interface that detects whether + * a message contains a valid CloudEvent + */ +export interface Detector { + (message: Message): boolean; +} + // HTTP Message capabilities export const HTTP: Binding = { binary: binary as Serializer, structured: structured as Serializer, toEvent: deserialize as Deserializer, + isEvent: isEvent as Detector, }; // TODO: Deprecated. Remove this for 4.0 diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index 8bc9da78..c6a3315f 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -28,6 +28,26 @@ const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0 const data_base64 = asBase64(dataBinary); describe("HTTP transport messages", () => { + it("can detect CloudEvent Messages", () => { + // Create a message that is not an actual event + let message: Message = { + body: "Hello world!", + headers: { + "Content-type": "text/plain", + }, + }; + expect(HTTP.isEvent(message)).to.be.false; + + // Now create a message that is an event + message = HTTP.binary( + new CloudEvent({ + source: "/message-test", + type: "example", + }), + ); + expect(HTTP.isEvent(message)).to.be.true; + }); + describe("Specification version V1", () => { const fixture: CloudEvent = new CloudEvent({ specversion: Version.V1, From c0241a5c1c73307325894bb0be7610bcf52f8f0a Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Tue, 25 Aug 2020 13:34:37 -0400 Subject: [PATCH 10/12] examples: revert updates to express example Signed-off-by: Lance Ball --- examples/express-ex/index.js | 10 ++-------- examples/express-ex/package.json | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/examples/express-ex/index.js b/examples/express-ex/index.js index 7b4ce02e..cfe68e95 100644 --- a/examples/express-ex/index.js +++ b/examples/express-ex/index.js @@ -1,8 +1,7 @@ /* eslint-disable */ const express = require("express"); -const { Message, CloudEvent, HTTP } = require("cloudevents"); -const { response } = require("express"); +const { Receiver } = require("cloudevents"); const app = express(); app.use((req, res, next) => { @@ -23,13 +22,8 @@ app.post("/", (req, res) => { console.log("HEADERS", req.headers); console.log("BODY", req.body); - const message = { - headers: req.headers, - body: req.body - }; - try { - const event = HTTP.toEvent(message); + const event = Receiver.accept(req.headers, req.body); // respond as an event const responseEventMessage = new CloudEvent({ source: '/', diff --git a/examples/express-ex/package.json b/examples/express-ex/package.json index 3d483f18..756d8f7b 100644 --- a/examples/express-ex/package.json +++ b/examples/express-ex/package.json @@ -14,7 +14,7 @@ "author": "fabiojose@gmail.com", "license": "Apache-2.0", "dependencies": { - "cloudevents": "~3.0.0", + "cloudevents": "^3.1.0", "express": "^4.17.1" } } From 594131520be95321ee92b464d3e4c30d1d687586 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Tue, 25 Aug 2020 14:02:20 -0400 Subject: [PATCH 11/12] chore(transport): add jsdoc @deprecated to Receiver.accept() Signed-off-by: Lance Ball --- src/transport/receiver.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/transport/receiver.ts b/src/transport/receiver.ts index 8d8d86f0..e07b26f9 100644 --- a/src/transport/receiver.ts +++ b/src/transport/receiver.ts @@ -13,6 +13,7 @@ export const Receiver = { * @param {Object} headers HTTP headers keyed by header name ("Content-Type") * @param {Object|JSON} body The body of the HTTP request * @return {CloudEvent} A new {CloudEvent} instance + * @deprecated Consider using the Message interface with HTTP.toEvent(message) */ accept(headers: Headers, body: string | Record | undefined | null): CloudEvent { const cleanHeaders: Headers = sanitize(headers); From a9cb90e02436aa50d4859007e8c7946624938421 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Tue, 25 Aug 2020 15:20:32 -0400 Subject: [PATCH 12/12] chore(transport): add jsdoc @deprecated to Emitter.send() Signed-off-by: Lance Ball --- src/transport/emitter.ts | 1 + src/transport/receiver.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/transport/emitter.ts b/src/transport/emitter.ts index 911339e3..ad1b8a7c 100644 --- a/src/transport/emitter.ts +++ b/src/transport/emitter.ts @@ -63,6 +63,7 @@ export class Emitter { * In that case, it will be used as the recipient endpoint. The endpoint can * be overridden by providing a URL here. * @returns {Promise} Promise with an eventual response from the receiver + * @deprecated Will be removed in 4.0.0. Consider using the Message interface with HTTP.[binary|structured](event) */ send(event: CloudEvent, options?: TransportOptions): Promise { options = options || {}; diff --git a/src/transport/receiver.ts b/src/transport/receiver.ts index e07b26f9..f15e8f4c 100644 --- a/src/transport/receiver.ts +++ b/src/transport/receiver.ts @@ -13,7 +13,7 @@ export const Receiver = { * @param {Object} headers HTTP headers keyed by header name ("Content-Type") * @param {Object|JSON} body The body of the HTTP request * @return {CloudEvent} A new {CloudEvent} instance - * @deprecated Consider using the Message interface with HTTP.toEvent(message) + * @deprecated Will be removed in 4.0.0. Consider using the Message interface with HTTP.toEvent(message) */ accept(headers: Headers, body: string | Record | undefined | null): CloudEvent { const cleanHeaders: Headers = sanitize(headers);