feat: introduce Message, Serializer, Deserializer and Binding interfaces (#324)

* 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 and V3 events are in place. Conformance tests have been modified to use
these new interfaces vs. the HTTP Receiver class.

Signed-off-by: Lance Ball <lball@redhat.com>
This commit is contained in:
Lance Ball 2020-08-26 18:26:50 -04:00 committed by GitHub
parent 17d4bc85df
commit f3953a9a5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 668 additions and 1703 deletions

View File

@ -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));

View File

@ -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"
}
}

View File

@ -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<string, unknown> {
const event = { ...this };
event.time = this.time;

View File

@ -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,
};

View File

@ -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 };
}

234
src/message/http/index.ts Normal file
View File

@ -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<string, unknown>} body the HTTP request body
* @returns {Version} the CloudEvent specification version
*/
function getVersion(mode: Mode, headers: Headers, body: string | Record<string, string>) {
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<string, unknown> } = {};
const parserMap: Record<string, MappedParser> = 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<string, unknown>) };
const eventObj: { [key: string]: unknown } = {};
const parserMap: Record<string, MappedParser> = 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;
}

75
src/message/index.ts Normal file
View File

@ -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 };

View File

@ -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<AxiosResponse> {
options = options || {};

View File

@ -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";

View File

@ -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<string, unknown> | 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<string, unknown> } = {};
const parserMap: Record<string, MappedParser> = 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;
}
}

View File

@ -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;
}

View File

@ -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, unknown> | 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<string, unknown>) };
const eventObj: { [key: string]: unknown } = {};
const parserMap: Record<string, MappedParser> =
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;
}
}

View File

@ -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<string, unknown> | CloudEventV1 | CloudEventV03 | undefined | null,
): CloudEvent {
accept(headers: Headers, body: string | Record<string, unknown> | 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<string, unknown>} body the HTTP request body
* @returns {Version} the CloudEvent specification version
*/
function getVersion(
mode: Mode,
headers: Headers,
body: string | Record<string, unknown> | 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);
},
};

View File

@ -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<string, []>) {
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<string, string> {
const obj: Record<string, string> = {};
function extractHeaders(arr: []): Headers {
const obj: Headers = {};
// @ts-ignore
return arr.reduce(({}, keyOrValue, index, arr) => {
if (index % 2 === 0) {

View File

@ -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/";

View File

@ -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" });
});
});
});

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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",