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:
parent
17d4bc85df
commit
08e98c7fe9
|
@ -1,8 +1,7 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable */
|
||||||
|
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const { Receiver } = require("cloudevents");
|
const { Receiver } = require("cloudevents");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
|
@ -25,8 +24,16 @@ app.post("/", (req, res) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const event = Receiver.accept(req.headers, req.body);
|
const event = Receiver.accept(req.headers, req.body);
|
||||||
console.log(`Accepted event: ${event}`);
|
// respond as an event
|
||||||
res.status(201).json(event);
|
const responseEventMessage = new CloudEvent({
|
||||||
|
source: '/',
|
||||||
|
type: 'event:response',
|
||||||
|
...event
|
||||||
|
});
|
||||||
|
responseEventMessage.data = {
|
||||||
|
hello: 'world'
|
||||||
|
};
|
||||||
|
res.status(201).json(responseEventMessage);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(415).header("Content-Type", "application/json").send(JSON.stringify(err));
|
res.status(415).header("Content-Type", "application/json").send(JSON.stringify(err));
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
"author": "fabiojose@gmail.com",
|
"author": "fabiojose@gmail.com",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cloudevents": "~3.0.0",
|
"cloudevents": "^3.1.0",
|
||||||
"express": "^4.17.1"
|
"express": "^4.17.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,6 +156,12 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
|
||||||
this.#_data = value;
|
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> {
|
toJSON(): Record<string, unknown> {
|
||||||
const event = { ...this };
|
const event = { ...this };
|
||||||
event.time = this.time;
|
event.time = this.time;
|
||||||
|
|
24
src/index.ts
24
src/index.ts
|
@ -3,9 +3,9 @@ import { ValidationError } from "./event/validation";
|
||||||
import { CloudEventV03, CloudEventV03Attributes, CloudEventV1, CloudEventV1Attributes } from "./event/interfaces";
|
import { CloudEventV03, CloudEventV03Attributes, CloudEventV1, CloudEventV1Attributes } from "./event/interfaces";
|
||||||
|
|
||||||
import { Emitter, TransportOptions } from "./transport/emitter";
|
import { Emitter, TransportOptions } from "./transport/emitter";
|
||||||
import { Receiver, Mode } from "./transport/receiver";
|
import { Receiver } from "./transport/receiver";
|
||||||
import { Protocol } from "./transport/protocols";
|
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";
|
import CONSTANTS from "./constants";
|
||||||
|
|
||||||
|
@ -18,14 +18,20 @@ export {
|
||||||
CloudEventV1Attributes,
|
CloudEventV1Attributes,
|
||||||
Version,
|
Version,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
// From transport
|
// From message
|
||||||
Emitter,
|
|
||||||
Receiver,
|
|
||||||
Mode,
|
|
||||||
Protocol,
|
|
||||||
TransportOptions,
|
|
||||||
Headers,
|
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
|
// From Constants
|
||||||
CONSTANTS,
|
CONSTANTS,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,97 @@
|
||||||
import { PassThroughParser, DateParser, MappedParser } from "../../parsers";
|
import { PassThroughParser, DateParser, MappedParser } from "../../parsers";
|
||||||
|
import { ValidationError, CloudEvent } from "../..";
|
||||||
|
import { Headers } from "../";
|
||||||
|
import { Version } from "../../event/cloudevent";
|
||||||
import CONSTANTS from "../../constants";
|
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 {
|
function parser(name: string, parser = new PassThroughParser()): MappedParser {
|
||||||
return { name: name, parser: parser };
|
return { name: name, parser: parser };
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 };
|
|
@ -63,6 +63,7 @@ export class Emitter {
|
||||||
* In that case, it will be used as the recipient endpoint. The endpoint can
|
* In that case, it will be used as the recipient endpoint. The endpoint can
|
||||||
* be overridden by providing a URL here.
|
* be overridden by providing a URL here.
|
||||||
* @returns {Promise} Promise with an eventual response from the receiver
|
* @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> {
|
send(event: CloudEvent, options?: TransportOptions): Promise<AxiosResponse> {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
|
@ -2,7 +2,8 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
|
|
||||||
import { CloudEvent, Version } from "../../event/cloudevent";
|
import { CloudEvent, Version } from "../../event/cloudevent";
|
||||||
import { TransportOptions } from "../emitter";
|
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 { asData } from "../../event/validation";
|
||||||
import CONSTANTS from "../../constants";
|
import CONSTANTS from "../../constants";
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +1,11 @@
|
||||||
import { Headers, sanitize } from "./http/headers";
|
import { Headers, Message, HTTP } from "../message";
|
||||||
import { CloudEvent, Version, ValidationError } from "..";
|
import { sanitize } from "../message/http/headers";
|
||||||
import { BinaryHTTPReceiver as BinaryReceiver } from "./http/binary_receiver";
|
import { CloudEvent } from "..";
|
||||||
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),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to receive a CloudEvent from an HTTP POST request.
|
* 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
|
* Acceptor for an incoming HTTP CloudEvent POST. Can process
|
||||||
* binary and structured incoming CloudEvents.
|
* 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} headers HTTP headers keyed by header name ("Content-Type")
|
||||||
* @param {Object|JSON} body The body of the HTTP request
|
* @param {Object|JSON} body The body of the HTTP request
|
||||||
* @return {CloudEvent} A new {CloudEvent} instance
|
* @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(
|
accept(headers: Headers, body: string | Record<string, unknown> | undefined | null): CloudEvent {
|
||||||
headers: Headers,
|
|
||||||
body: string | Record<string, unknown> | CloudEventV1 | CloudEventV03 | undefined | null,
|
|
||||||
): CloudEvent {
|
|
||||||
const cleanHeaders: Headers = sanitize(headers);
|
const cleanHeaders: Headers = sanitize(headers);
|
||||||
const mode: Mode = getMode(cleanHeaders);
|
const cleanBody = body ? (typeof body === "object" ? JSON.stringify(body) : body) : "";
|
||||||
const version = getVersion(mode, cleanHeaders, body);
|
const message: Message = {
|
||||||
switch (version) {
|
headers: cleanHeaders,
|
||||||
case Version.V1:
|
body: cleanBody,
|
||||||
return receivers.v1[mode].parse(body, headers);
|
};
|
||||||
case Version.V03:
|
return HTTP.toEvent(message);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
/* eslint-disable no-console */
|
|
||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
import { assert } from "chai";
|
import { assert } from "chai";
|
||||||
import { Given, When, Then, World } from "cucumber";
|
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
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const { HTTPParser } = require("http-parser-js");
|
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) {
|
Given("an HTTP request", function (request: string) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// Create a Message from the incoming HTTP request
|
||||||
const world = this;
|
const message: Message = {
|
||||||
|
headers: {},
|
||||||
|
body: "",
|
||||||
|
};
|
||||||
parser.onHeadersComplete = function (record: Record<string, []>) {
|
parser.onHeadersComplete = function (record: Record<string, []>) {
|
||||||
world.headers = arrayToObject(record.headers);
|
message.headers = extractHeaders(record.headers);
|
||||||
};
|
};
|
||||||
parser.onBody = function (body: Buffer, offset: number) {
|
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);
|
parser.execute(Buffer.from(request), 0, request.length);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
When("parsed as HTTP request", function () {
|
When("parsed as HTTP request", function () {
|
||||||
this.cloudevent = Receiver.accept(this.headers, this.body);
|
this.cloudevent = HTTP.toEvent(this.message);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -47,8 +50,8 @@ Then("the data is equal to the following JSON:", function (json: string) {
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
function arrayToObject(arr: []): Record<string, string> {
|
function extractHeaders(arr: []): Headers {
|
||||||
const obj: Record<string, string> = {};
|
const obj: Headers = {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return arr.reduce(({}, keyOrValue, index, arr) => {
|
return arr.reduce(({}, keyOrValue, index, arr) => {
|
||||||
if (index % 2 === 0) {
|
if (index % 2 === 0) {
|
||||||
|
|
|
@ -6,7 +6,8 @@ import CONSTANTS from "../../src/constants";
|
||||||
const DEFAULT_CE_CONTENT_TYPE = CONSTANTS.DEFAULT_CE_CONTENT_TYPE;
|
const DEFAULT_CE_CONTENT_TYPE = CONSTANTS.DEFAULT_CE_CONTENT_TYPE;
|
||||||
const DEFAULT_CONTENT_TYPE = CONSTANTS.DEFAULT_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";
|
import { AxiosResponse } from "axios";
|
||||||
|
|
||||||
const receiver = "https://cloudevents.io/";
|
const receiver = "https://cloudevents.io/";
|
||||||
|
|
|
@ -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" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,6 +1,6 @@
|
||||||
import "mocha";
|
import "mocha";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import { CloudEvent, Receiver, Emitter, Version } from "../../src";
|
import { CloudEvent, Emitter, Version } from "../../src";
|
||||||
|
|
||||||
const fixture = {
|
const fixture = {
|
||||||
type: "org.cloudevents.test",
|
type: "org.cloudevents.test",
|
||||||
|
@ -13,11 +13,6 @@ describe("The SDK Requirements", () => {
|
||||||
expect(event instanceof CloudEvent).to.equal(true);
|
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", () => {
|
it("should expose an Emitter type", () => {
|
||||||
const emitter = new Emitter({
|
const emitter = new Emitter({
|
||||||
url: "http://example.com",
|
url: "http://example.com",
|
||||||
|
|
Loading…
Reference in New Issue