diff --git a/examples/express-ex/index.js b/examples/express-ex/index.js index 57a71e1..cfe68e9 100644 --- a/examples/express-ex/index.js +++ b/examples/express-ex/index.js @@ -1,8 +1,7 @@ -/* eslint-disable no-console */ +/* eslint-disable */ const express = require("express"); -const {Receiver} = require("cloudevents"); - +const { Receiver } = require("cloudevents"); const app = express(); app.use((req, res, next) => { @@ -25,8 +24,16 @@ app.post("/", (req, res) => { try { const event = Receiver.accept(req.headers, req.body); - console.log(`Accepted event: ${event}`); - res.status(201).json(event); + // 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/examples/express-ex/package.json b/examples/express-ex/package.json index 3d483f1..756d8f7 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" } } diff --git a/src/event/cloudevent.ts b/src/event/cloudevent.ts index 2cb571a..197b4bc 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/src/index.ts b/src/index.ts index 598d61c..11149ff 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, headersFor } from "./message"; import CONSTANTS from "./constants"; @@ -18,14 +18,20 @@ export { CloudEventV1Attributes, Version, ValidationError, - // From transport - Emitter, - Receiver, - Mode, - Protocol, - TransportOptions, + // From message Headers, - headersFor, + Mode, + Binding, + Message, + Deserializer, + Serializer, + headersFor, // TODO: Deprecated. Remove for 4.0 + HTTP, + // From transport + 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/transport/http/versions.ts b/src/message/http/headers.ts similarity index 61% rename from src/transport/http/versions.ts rename to src/message/http/headers.ts index 70ddc56..228445c 100644 --- a/src/transport/http/versions.ts +++ b/src/message/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/message/http/index.ts b/src/message/http/index.ts new file mode 100644 index 0000000..c93917d --- /dev/null +++ b/src/message/http/index.ts @@ -0,0 +1,234 @@ +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(), + }; +} + +// 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 + * + * @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 { + const 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]}`); + } + + // 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 decoded + if (eventObj.data_base64) { + 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); + + // Validates the event + validateCloudEvent(cloudevent); + return cloudevent; +} diff --git a/src/message/index.ts b/src/message/index.ts new file mode 100644 index 0000000..14ed270 --- /dev/null +++ b/src/message/index.ts @@ -0,0 +1,75 @@ +import { CloudEvent } from ".."; +import { binary, deserialize, structured, isEvent } from "./http"; +import { headersFor } from "./http/headers"; + +/** + * 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; + isEvent: Detector; +} + +/** + * 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; +} + +/** + * 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 +export { headersFor }; diff --git a/src/transport/emitter.ts b/src/transport/emitter.ts index 911339e..ad1b8a7 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/http/binary_emitter.ts b/src/transport/http/binary_emitter.ts index 20f5166..cabf906 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 "../../message"; +import { headersFor } from "../../message/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 aa027f2..0000000 --- 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 044aef7..0000000 --- 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 ef9b3a3..0000000 --- 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 5c83ec5..f15e8f4 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 "../message"; +import { sanitize } from "../message/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. @@ -35,65 +13,15 @@ export class 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 Will be removed in 4.0.0. Consider using the Message interface with HTTP.toEvent(message) */ - 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 f6e57bc..8b3377d 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 3c8a203..e9f184b 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/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 new file mode 100644 index 0000000..c6a3315 --- /dev/null +++ b/test/integration/message_test.ts @@ -0,0 +1,202 @@ +import { expect } from "chai"; +import { CloudEvent, CONSTANTS, Version } from "../../src"; +import { asBase64 } from "../../src/event/validation"; +import { Message, HTTP } from "../../src/message"; + +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", +}; + +// 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"; + +// 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", () => { + 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, + id, + type, + source, + datacontenttype, + subject, + time, + dataschema, + 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.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("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("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", () => { + 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("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("Supports Base-64 encoded data in structured messages", () => { + 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); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); + + it("Supports Base-64 encoded data in binary messages", () => { + 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); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); + }); +}); diff --git a/test/integration/receiver_binary_03_tests.ts b/test/integration/receiver_binary_03_tests.ts deleted file mode 100644 index dfdfd73..0000000 --- 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 368c55e..0000000 --- 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 d47220b..0000000 --- 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 250dab9..0000000 --- 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 a949ed0..d0aa3d1 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",