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
f3953a9a5a
|
@ -1,8 +1,7 @@
|
|||
/* eslint-disable no-console */
|
||||
/* eslint-disable */
|
||||
|
||||
const express = require("express");
|
||||
const {Receiver} = require("cloudevents");
|
||||
|
||||
const { Receiver } = require("cloudevents");
|
||||
const app = express();
|
||||
|
||||
app.use((req, res, next) => {
|
||||
|
@ -25,8 +24,16 @@ app.post("/", (req, res) => {
|
|||
|
||||
try {
|
||||
const event = Receiver.accept(req.headers, req.body);
|
||||
console.log(`Accepted event: ${event}`);
|
||||
res.status(201).json(event);
|
||||
// respond as an event
|
||||
const responseEventMessage = new CloudEvent({
|
||||
source: '/',
|
||||
type: 'event:response',
|
||||
...event
|
||||
});
|
||||
responseEventMessage.data = {
|
||||
hello: 'world'
|
||||
};
|
||||
res.status(201).json(responseEventMessage);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(415).header("Content-Type", "application/json").send(JSON.stringify(err));
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
"author": "fabiojose@gmail.com",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"cloudevents": "~3.0.0",
|
||||
"cloudevents": "^3.1.0",
|
||||
"express": "^4.17.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -156,6 +156,12 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
|
|||
this.#_data = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by JSON.stringify(). The name is confusing, but this method is called by
|
||||
* JSON.stringify() when converting this object to JSON.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
|
||||
* @return {object} this event as a plain object
|
||||
*/
|
||||
toJSON(): Record<string, unknown> {
|
||||
const event = { ...this };
|
||||
event.time = this.time;
|
||||
|
|
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 { Emitter, TransportOptions } from "./transport/emitter";
|
||||
import { Receiver, Mode } from "./transport/receiver";
|
||||
import { Receiver } from "./transport/receiver";
|
||||
import { Protocol } from "./transport/protocols";
|
||||
import { Headers, headersFor } from "./transport/http/headers";
|
||||
import { Headers, Mode, Binding, HTTP, Message, Serializer, Deserializer, headersFor } from "./message";
|
||||
|
||||
import CONSTANTS from "./constants";
|
||||
|
||||
|
@ -18,14 +18,20 @@ export {
|
|||
CloudEventV1Attributes,
|
||||
Version,
|
||||
ValidationError,
|
||||
// From transport
|
||||
Emitter,
|
||||
Receiver,
|
||||
Mode,
|
||||
Protocol,
|
||||
TransportOptions,
|
||||
// From message
|
||||
Headers,
|
||||
headersFor,
|
||||
Mode,
|
||||
Binding,
|
||||
Message,
|
||||
Deserializer,
|
||||
Serializer,
|
||||
headersFor, // TODO: Deprecated. Remove for 4.0
|
||||
HTTP,
|
||||
// From transport
|
||||
Emitter, // TODO: Deprecated. Remove for 4.0
|
||||
Receiver, // TODO: Deprecated. Remove for 4.0
|
||||
Protocol, // TODO: Deprecated. Remove for 4.0
|
||||
TransportOptions, // TODO: Deprecated. Remove for 4.0
|
||||
// From Constants
|
||||
CONSTANTS,
|
||||
};
|
||||
|
|
|
@ -1,6 +1,97 @@
|
|||
import { PassThroughParser, DateParser, MappedParser } from "../../parsers";
|
||||
import { ValidationError, CloudEvent } from "../..";
|
||||
import { Headers } from "../";
|
||||
import { Version } from "../../event/cloudevent";
|
||||
import CONSTANTS from "../../constants";
|
||||
|
||||
export const allowedContentTypes = [CONSTANTS.DEFAULT_CONTENT_TYPE, CONSTANTS.MIME_JSON, CONSTANTS.MIME_OCTET_STREAM];
|
||||
export const requiredHeaders = [
|
||||
CONSTANTS.CE_HEADERS.ID,
|
||||
CONSTANTS.CE_HEADERS.SOURCE,
|
||||
CONSTANTS.CE_HEADERS.TYPE,
|
||||
CONSTANTS.CE_HEADERS.SPEC_VERSION,
|
||||
];
|
||||
|
||||
/**
|
||||
* Validates cloud event headers and their values
|
||||
* @param {Headers} headers event transport headers for validation
|
||||
* @throws {ValidationError} if the headers are invalid
|
||||
* @return {boolean} true if headers are valid
|
||||
*/
|
||||
export function validate(headers: Headers): Headers {
|
||||
const sanitizedHeaders = sanitize(headers);
|
||||
|
||||
// if content-type exists, be sure it's an allowed type
|
||||
const contentTypeHeader = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE];
|
||||
const noContentType = !allowedContentTypes.includes(contentTypeHeader);
|
||||
if (contentTypeHeader && noContentType) {
|
||||
throw new ValidationError("invalid content type", [sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]]);
|
||||
}
|
||||
|
||||
requiredHeaders
|
||||
.filter((required: string) => !sanitizedHeaders[required])
|
||||
.forEach((required: string) => {
|
||||
throw new ValidationError(`header '${required}' not found`);
|
||||
});
|
||||
|
||||
if (!sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]) {
|
||||
sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON;
|
||||
}
|
||||
|
||||
return sanitizedHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTTP headers that will be sent for this event when the HTTP transmission
|
||||
* mode is "binary". Events sent over HTTP in structured mode only have a single CE header
|
||||
* and that is "ce-id", corresponding to the event ID.
|
||||
* @param {CloudEvent} event a CloudEvent
|
||||
* @returns {Object} the headers that will be sent for the event
|
||||
*/
|
||||
export function headersFor(event: CloudEvent): Headers {
|
||||
const headers: Headers = {};
|
||||
let headerMap: Readonly<{ [key: string]: MappedParser }>;
|
||||
if (event.specversion === Version.V1) {
|
||||
headerMap = v1headerMap;
|
||||
} else {
|
||||
headerMap = v03headerMap;
|
||||
}
|
||||
|
||||
// iterate over the event properties - generate a header for each
|
||||
Object.getOwnPropertyNames(event).forEach((property) => {
|
||||
const value = event[property];
|
||||
if (value) {
|
||||
const map: MappedParser | undefined = headerMap[property] as MappedParser;
|
||||
if (map) {
|
||||
headers[map.name] = map.parser.parse(value as string) as string;
|
||||
} else if (property !== CONSTANTS.DATA_ATTRIBUTE && property !== `${CONSTANTS.DATA_ATTRIBUTE}_base64`) {
|
||||
headers[`${CONSTANTS.EXTENSIONS_PREFIX}${property}`] = value as string;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Treat time specially, since it's handled with getters and setters in CloudEvent
|
||||
if (event.time) {
|
||||
headers[CONSTANTS.CE_HEADERS.TIME] = event.time as string;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes incoming headers by lowercasing them and potentially removing
|
||||
* encoding from the content-type header.
|
||||
* @param {Headers} headers HTTP headers as key/value pairs
|
||||
* @returns {Headers} the sanitized headers
|
||||
*/
|
||||
export function sanitize(headers: Headers): Headers {
|
||||
const sanitized: Headers = {};
|
||||
|
||||
Array.from(Object.keys(headers))
|
||||
.filter((header) => Object.hasOwnProperty.call(headers, header))
|
||||
.forEach((header) => (sanitized[header.toLowerCase()] = headers[header]));
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
function parser(name: string, parser = new PassThroughParser()): MappedParser {
|
||||
return { name: name, parser: parser };
|
||||
}
|
|
@ -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
|
||||
* be overridden by providing a URL here.
|
||||
* @returns {Promise} Promise with an eventual response from the receiver
|
||||
* @deprecated Will be removed in 4.0.0. Consider using the Message interface with HTTP.[binary|structured](event)
|
||||
*/
|
||||
send(event: CloudEvent, options?: TransportOptions): Promise<AxiosResponse> {
|
||||
options = options || {};
|
||||
|
|
|
@ -2,7 +2,8 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
|||
|
||||
import { CloudEvent, Version } from "../../event/cloudevent";
|
||||
import { TransportOptions } from "../emitter";
|
||||
import { Headers, headersFor } from "./headers";
|
||||
import { Headers } from "../../message";
|
||||
import { headersFor } from "../../message/http/headers";
|
||||
import { asData } from "../../event/validation";
|
||||
import CONSTANTS from "../../constants";
|
||||
|
||||
|
|
|
@ -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 { CloudEvent, Version, ValidationError } from "..";
|
||||
import { BinaryHTTPReceiver as BinaryReceiver } from "./http/binary_receiver";
|
||||
import { StructuredHTTPReceiver as StructuredReceiver } from "./http/structured_receiver";
|
||||
import { CloudEventV1, CloudEventV03 } from "../event/interfaces";
|
||||
import CONSTANTS from "../constants";
|
||||
|
||||
/**
|
||||
* An enum representing the two HTTP transport modes, binary and structured
|
||||
*/
|
||||
export enum Mode {
|
||||
BINARY = "binary",
|
||||
STRUCTURED = "structured",
|
||||
}
|
||||
|
||||
const receivers = {
|
||||
v1: {
|
||||
structured: new StructuredReceiver(Version.V1),
|
||||
binary: new BinaryReceiver(Version.V1),
|
||||
},
|
||||
v03: {
|
||||
structured: new StructuredReceiver(Version.V03),
|
||||
binary: new BinaryReceiver(Version.V03),
|
||||
},
|
||||
};
|
||||
import { Headers, Message, HTTP } from "../message";
|
||||
import { sanitize } from "../message/http/headers";
|
||||
import { CloudEvent } from "..";
|
||||
|
||||
/**
|
||||
* A class to receive a CloudEvent from an HTTP POST request.
|
||||
*/
|
||||
export class Receiver {
|
||||
export const Receiver = {
|
||||
/**
|
||||
* Acceptor for an incoming HTTP CloudEvent POST. Can process
|
||||
* binary and structured incoming CloudEvents.
|
||||
|
@ -35,65 +13,15 @@ export class Receiver {
|
|||
* @param {Object} headers HTTP headers keyed by header name ("Content-Type")
|
||||
* @param {Object|JSON} body The body of the HTTP request
|
||||
* @return {CloudEvent} A new {CloudEvent} instance
|
||||
* @deprecated Will be removed in 4.0.0. Consider using the Message interface with HTTP.toEvent(message)
|
||||
*/
|
||||
static accept(
|
||||
headers: Headers,
|
||||
body: string | Record<string, unknown> | CloudEventV1 | CloudEventV03 | undefined | null,
|
||||
): CloudEvent {
|
||||
accept(headers: Headers, body: string | Record<string, unknown> | undefined | null): CloudEvent {
|
||||
const cleanHeaders: Headers = sanitize(headers);
|
||||
const mode: Mode = getMode(cleanHeaders);
|
||||
const version = getVersion(mode, cleanHeaders, body);
|
||||
switch (version) {
|
||||
case Version.V1:
|
||||
return receivers.v1[mode].parse(body, headers);
|
||||
case Version.V03:
|
||||
return receivers.v03[mode].parse(body, headers);
|
||||
default:
|
||||
console.error(`Unknown spec version ${version}. Default to ${Version.V1}`);
|
||||
return receivers.v1[mode].parse(body, headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the HTTP transport mode (binary or structured) based
|
||||
* on the incoming HTTP headers.
|
||||
* @param {Headers} headers the incoming HTTP headers
|
||||
* @returns {Mode} the transport mode
|
||||
*/
|
||||
function getMode(headers: Headers): Mode {
|
||||
const contentType = headers[CONSTANTS.HEADER_CONTENT_TYPE];
|
||||
if (contentType && contentType.startsWith(CONSTANTS.MIME_CE)) {
|
||||
return Mode.STRUCTURED;
|
||||
}
|
||||
if (headers[CONSTANTS.CE_HEADERS.ID]) {
|
||||
return Mode.BINARY;
|
||||
}
|
||||
throw new ValidationError("no cloud event detected");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the version of an incoming CloudEvent based on the
|
||||
* HTTP headers or HTTP body, depending on transport mode.
|
||||
* @param {Mode} mode the HTTP transport mode
|
||||
* @param {Headers} headers the incoming HTTP headers
|
||||
* @param {Record<string, unknown>} body the HTTP request body
|
||||
* @returns {Version} the CloudEvent specification version
|
||||
*/
|
||||
function getVersion(
|
||||
mode: Mode,
|
||||
headers: Headers,
|
||||
body: string | Record<string, unknown> | CloudEventV03 | CloudEventV1 | undefined | null,
|
||||
) {
|
||||
if (mode === Mode.BINARY) {
|
||||
// Check the headers for the version
|
||||
const versionHeader = headers[CONSTANTS.CE_HEADERS.SPEC_VERSION];
|
||||
if (versionHeader) {
|
||||
return versionHeader;
|
||||
}
|
||||
} else {
|
||||
// structured mode - the version is in the body
|
||||
return typeof body === "string" ? JSON.parse(body).specversion : (body as CloudEvent).specversion;
|
||||
}
|
||||
return Version.V1;
|
||||
}
|
||||
const cleanBody = body ? (typeof body === "object" ? JSON.stringify(body) : body) : "";
|
||||
const message: Message = {
|
||||
headers: cleanHeaders,
|
||||
body: cleanBody,
|
||||
};
|
||||
return HTTP.toEvent(message);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
/* eslint-disable no-console */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { assert } from "chai";
|
||||
import { Given, When, Then, World } from "cucumber";
|
||||
import { Receiver } from "../../src";
|
||||
import { Message, Headers, HTTP } from "../../src";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { HTTPParser } = require("http-parser-js");
|
||||
|
@ -14,20 +13,24 @@ Given("HTTP Protocol Binding is supported", function (this: World) {
|
|||
});
|
||||
|
||||
Given("an HTTP request", function (request: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const world = this;
|
||||
// Create a Message from the incoming HTTP request
|
||||
const message: Message = {
|
||||
headers: {},
|
||||
body: "",
|
||||
};
|
||||
parser.onHeadersComplete = function (record: Record<string, []>) {
|
||||
world.headers = arrayToObject(record.headers);
|
||||
message.headers = extractHeaders(record.headers);
|
||||
};
|
||||
parser.onBody = function (body: Buffer, offset: number) {
|
||||
world.body = body.slice(offset).toString();
|
||||
message.body = body.slice(offset).toString();
|
||||
};
|
||||
this.message = message;
|
||||
parser.execute(Buffer.from(request), 0, request.length);
|
||||
return true;
|
||||
});
|
||||
|
||||
When("parsed as HTTP request", function () {
|
||||
this.cloudevent = Receiver.accept(this.headers, this.body);
|
||||
this.cloudevent = HTTP.toEvent(this.message);
|
||||
return true;
|
||||
});
|
||||
|
||||
|
@ -47,8 +50,8 @@ Then("the data is equal to the following JSON:", function (json: string) {
|
|||
return true;
|
||||
});
|
||||
|
||||
function arrayToObject(arr: []): Record<string, string> {
|
||||
const obj: Record<string, string> = {};
|
||||
function extractHeaders(arr: []): Headers {
|
||||
const obj: Headers = {};
|
||||
// @ts-ignore
|
||||
return arr.reduce(({}, keyOrValue, index, arr) => {
|
||||
if (index % 2 === 0) {
|
||||
|
|
|
@ -6,7 +6,8 @@ import CONSTANTS from "../../src/constants";
|
|||
const DEFAULT_CE_CONTENT_TYPE = CONSTANTS.DEFAULT_CE_CONTENT_TYPE;
|
||||
const DEFAULT_CONTENT_TYPE = CONSTANTS.DEFAULT_CONTENT_TYPE;
|
||||
|
||||
import { CloudEvent, Version, Emitter, Protocol, headersFor } from "../../src";
|
||||
import { CloudEvent, Version, Emitter, Protocol } from "../../src";
|
||||
import { headersFor } from "../../src/message/http/headers";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
||||
const receiver = "https://cloudevents.io/";
|
||||
|
|
|
@ -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 { expect } from "chai";
|
||||
import { CloudEvent, Receiver, Emitter, Version } from "../../src";
|
||||
import { CloudEvent, Emitter, Version } from "../../src";
|
||||
|
||||
const fixture = {
|
||||
type: "org.cloudevents.test",
|
||||
|
@ -13,11 +13,6 @@ describe("The SDK Requirements", () => {
|
|||
expect(event instanceof CloudEvent).to.equal(true);
|
||||
});
|
||||
|
||||
it("should expose a Receiver type", () => {
|
||||
const receiver = new Receiver();
|
||||
expect(receiver instanceof Receiver).to.equal(true);
|
||||
});
|
||||
|
||||
it("should expose an Emitter type", () => {
|
||||
const emitter = new Emitter({
|
||||
url: "http://example.com",
|
||||
|
|
Loading…
Reference in New Issue