BREAKING CHANGE(lib): rewrite in TypeScript (#226)

This is a major rewrite of the entire codebase into TypeScript. Nearly
all tests have been retained except where behavior is significantly
different. Some highlights of these changes:

* lowercase all CloudEvent properties and fix base64 encoded data

Previously there was a format() function that would convert a CloudEvent
object into JSON with all of the properties lowercased. With this rewrite
a CloudEvent object can be converted to JSON simply with JSON.stringify().

However, in order to be compliant with the JSON representation outlined in
the spec here https://github.com/cloudevents/spec/blob/v1.0/json-format.md
all of the event properties must be all lowercase.

* lib(transport): make transport mode an Enum
* src: allow custom headers (#1)
* lib(exports): export explicitly versioned names where appropriate
* lib(cloudevent): modify ctor to accept extensions inline
* lib(cloudevent): make extensions a part of the event object
* test: convert all tests to typescript
* examples: update all examples with latest API changes
* docs: update README with latest API changes
* src: add prettier for code style and fix a lot of linting errors
* lib: move data decoding to occur within the CloudEvent object

Signed-off-by: Lance Ball <lball@redhat.com>
This commit is contained in:
Lance Ball 2020-06-29 14:46:20 -04:00 committed by GitHub
parent 060b21ba36
commit 276b810dd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 4562 additions and 5856 deletions

View File

@ -1,5 +1,14 @@
{
"extends": "eslint:recommended",
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2015,
"sourceType": "module"
},
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"env": {
"es6": true,
"node": true,
@ -7,7 +16,6 @@
},
"rules": {
"no-var": "error",
"space-before-function-paren": ["error", "never"],
"standard/no-callback-literal": "off",
"arrow-spacing": "error",
"arrow-parens": ["error", "always"],
@ -20,7 +28,7 @@
"no-console": ["error", {
"allow": ["warn", "error"]
}],
"valid-jsdoc": "error",
"valid-jsdoc": "warn",
"semi": ["error", "always"],
"quotes": ["error", "double", { "allowTemplateLiterals": true }]
}

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ index.js
/lib
/browser
/bundles
/dist
# Runtime data
pids

7
.prettierrc.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
semi: true,
trailingComma: "all",
doubleQuote: true,
printWidth: 120,
tabWidth: 2
}

View File

@ -36,33 +36,32 @@ binary and structured events in either the 1.0 or 0.3 protocol formats.
```js
const {
CloudEvent,
HTTPReceiver
Receiver
} = require("cloudevents-sdk");
// Create a receiver to accept events over HTTP
const receiver = new HTTPReceiver();
const receiver = new Receiver();
// body and headers come from an incoming HTTP request, e.g. express.js
const receivedEvent = receiver.accept(req.headers, req.body);
console.log(receivedEvent.format());
console.log(receivedEvent);
```
#### Emitting Events
To emit events, you'll need to decide whether the event should be sent in
binary or structured format, and determine what version of the CloudEvents
specification you want to send the event as.
You can send events over HTTP in either binary or structured format.
By default, the `HTTPEmitter` will emit events over HTTP POST using the
latest supported specification version, in binary mode. You can emit version specific events by providing
the specication version in the constructor to `HTTPEmitter`. To send
structured events, add that string as a parameter to `emitter.send()`.
By default, the `Emitter` will emit events over HTTP POST using the
binary transport protocol. The `Emitter` will examine the `specversion`
of the event being sent, and use the appropriate protocol version. To send
structured events, add `Protocol.HTTPStructured` as a parameter to
`emitter.send()`.
```js
const { CloudEvent, HTTPEmitter } = require("cloudevents-sdk");
const { CloudEvent, Emitter, Protocol, Version } = require("cloudevents-sdk");
// With only an endpoint URL, this creates a v1 emitter
const v1Emitter = new HTTPEmitter({
const emitter = new Emitter({
url: "https://cloudevents.io/example"
});
const event = new CloudEvent({
@ -70,39 +69,37 @@ const event = new CloudEvent({
});
// By default, the emitter will send binary events
v1Emitter.send(event).then((response) => {
emitter.send(event).then((response) => {
// handle the response
}).catch(console.error);
// To send a structured event, just add that as an option
v1Emitter.send(event, { mode: "structured" })
emitter.send(event, { protocol: Protocol.HTTPStructured })
.then((response) => {
// handle the response
}).catch(console.error);
// To send an event to an alternate URL, add that as an option
v1Emitter.send(event, { url: "https://alternate.com/api" })
emitter.send(event, { url: "https://alternate.com/api" })
.then((response) => {
// handle the response
}).catch(console.error);
// Sending a v0.3 event works the same, just let the emitter know when
// you create it that you are working with the 0.3 spec
const v03Emitter = new HTTPEmitter({
url: "https://cloudevents.io/example",
version: "0.3"
});
// Again, the default is to send binary events
// To send a structured event or to an alternate URL, provide those
// as parameters in a options object as above
v3Emitter.send(event)
// Sending a v0.3 event works the same, If your event has a
// specversion property of Version.V03, then it will be sent
// using the 0.3 transport protocol
emitter.send(new CloudEvent({ specversion: Version.V03, source, type }))
.then((response) => {
// handle the response
}).catch(console.error);
```
### Example Applications
There are a few trivial example applications in
[the examples folder](https://github.com/cloudevents/sdk-javascript/tree/master/examples).
There you will find Express.js, TypeScript and Websocket examples.
## Supported specification features
| | [v0.3](https://github.com/cloudevents/spec/tree/v0.3) | [v1.0](https://github.com/cloudevents/spec/tree/v1.0) |

View File

@ -1,42 +1,39 @@
/* eslint-disable no-console */
const express = require("express");
const { HTTPReceiver } = require("cloudevents-sdk");
const { Receiver } = require("cloudevents-sdk");
const app = express();
const receiver = new HTTPReceiver();
const receiver = new Receiver();
app.use((req, res, next) => {
let data = "";
req.setEncoding("utf8");
req.on("data", function(chunk) {
req.on("data", function (chunk) {
data += chunk;
});
req.on("end", function() {
req.on("end", function () {
req.body = data;
next();
});
});
app.post("/", function(req, res) {
console.log(req.headers);
console.log(req.body);
app.post("/", function (req, res) {
console.log("HEADERS", req.headers);
console.log("BODY", req.body);
try {
const event = receiver.accept(req.headers, req.body);
const asJSON = event.format();
console.log(`Accepted event: ${JSON.stringify(event.format(), null, 2)}`);
res.status(201).json(asJSON);
console.log(`Accepted event: ${event}`);
res.status(201).json(event);
} catch (err) {
console.error(err);
res.status(415)
.header("Content-Type", "application/json")
.send(JSON.stringify(err));
res.status(415).header("Content-Type", "application/json").send(JSON.stringify(err));
}
});
app.listen(3000, function() {
app.listen(3000, function () {
console.log("Example app listening on port 3000!");
});

View File

@ -16,6 +16,7 @@
"check": "gts check",
"clean": "gts clean",
"compile": "tsc -p .",
"watch": "tsc -p . --watch",
"fix": "gts fix",
"prepare": "npm run compile",
"pretest": "npm run compile",

View File

@ -1,35 +1,33 @@
import { CloudEvent, HTTPReceiver } from "cloudevents-sdk";
import { CloudEventV1 } from "cloudevents-sdk/lib/v1";
import { CloudEvent, CloudEventV1, Receiver } from "cloudevents-sdk";
export function doSomeStuff() {
const receiver = new HTTPReceiver();
const receiver = new Receiver();
const myevent: CloudEventV1 = new CloudEvent({
source: "/source",
type: "type",
dataContentType: "text/plain",
dataSchema: "https://d.schema.com/my.json",
datacontenttype: "text/plain",
dataschema: "https://d.schema.com/my.json",
subject: "cha.json",
data: "my-data"
data: "my-data",
});
myevent.addExtension("extension-1", "some extension data");
myevent.extension1 = "some extension data";
console.log("My structured event:", myevent.toString());
console.log("My structured event extensions:", myevent.getExtensions());
console.log("My structured event:", myevent);
// ------ receiver structured
// The header names should be standarized to use lowercase
const headers = {
"content-type": "application/cloudevents+json"
"content-type": "application/cloudevents+json",
};
// Typically used with an incoming HTTP request where myevent.format() is the actual
// body of the HTTP
console.log("Received structured event:", receiver.accept(headers, myevent.format()).toString());
console.log("Received structured event:", receiver.accept(headers, myevent));
// ------ receiver binary
const data = {
"data": "dataString"
data: "dataString",
};
const attributes = {
"ce-type": "type",
@ -39,11 +37,11 @@ export function doSomeStuff() {
"ce-time": "2019-06-16T11:42:00Z",
"ce-dataschema": "http://schema.registry/v1",
"Content-Type": "application/json",
"ce-extension1": "extension1"
"ce-extension1": "extension1",
};
console.log("My binary event:", receiver.accept(attributes, data).toString());
console.log("My binary event extensions:", receiver.accept(attributes, data).toString());
console.log("My binary event:", receiver.accept(attributes, data));
console.log("My binary event extensions:", receiver.accept(attributes, data));
return true;
}

View File

@ -7,7 +7,7 @@ const { CloudEvent } = require("cloudevents-sdk");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
output: process.stdout,
});
rl.on("close", (_) => console.log("\n\nConnection closed! Press CTL-C to exit."));
@ -25,16 +25,16 @@ ws.on("message", function incoming(message) {
function ask() {
rl.question("Would you like to see the current weather? Provide a zip code: ", function (zip) {
console.log("Fetching weather data from server...");
ws.send(new CloudEvent({
const event = new CloudEvent({
type: "weather.query",
source: "/weather.client",
data: zip
}).toString());
data: { zip },
});
ws.send(event.toString());
});
}
function print(data) {
data = JSON.parse(data);
console.log(`
Current weather for ${data.name}: ${data.weather[0].main}
------------------------------------------

View File

@ -2,13 +2,14 @@
<html>
<head>
<title>CloudEvent Example</title>
<script src="../../bundles/cloudevents-sdk.js"></script>
<script src="../../_bundles/cloudevents-sdk.js"></script>
<script>
const CloudEvent = window['cloudevents-sdk'].CloudEvent;
const Version = window['cloudevents-sdk'].Version;
const socket = new WebSocket("ws://localhost:8080");
function print(weather) {
const data = JSON.parse(weather);
const data = weather;
const summary = `
<h2>Current weather for ${data.name}: ${data.weather[0].main}</h2>
<hr/>
@ -23,7 +24,7 @@ and the wind is blowing at ${Math.round(data.wind.speed)}mph.
function initialize() {
socket.onmessage = function(message) {
console.log(message.data)
console.log(message.data);
const event = new CloudEvent(JSON.parse(message.data));
if (event.type === "weather.error") {
console.error(`Error: ${event.data}`);
@ -37,11 +38,13 @@ and the wind is blowing at ${Math.round(data.wind.speed)}mph.
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
socket.send(new CloudEvent({
const ce = new CloudEvent({
type: "weather.query",
source: "/weather.client",
data: input.value
}).toString());
data: { zip: input.value }
});
console.log(ce);
socket.send(JSON.stringify(ce));
}
});

View File

@ -13,24 +13,29 @@ console.log("WebSocket server started. Waiting for events.");
wss.on("connection", function connection(ws) {
console.log("Connection received");
ws.on("message", function incoming(message) {
console.log(`Message received: ${message}`);
const event = new CloudEvent(JSON.parse(message));
console.log(`Message received: ${event.toString()}`);
fetch(event.data)
fetch(event.data.zip)
.then((weather) => {
ws.send(new CloudEvent({
const response = new CloudEvent({
dataContentType: "application/json",
type: "current.weather",
source: "/weather.server",
data: weather
}).toString());
data: weather,
});
ws.send(JSON.stringify(response));
})
.catch((err) => {
console.error(err);
ws.send(new CloudEvent({
type: "weather.error",
source: "/weather.server",
data: err.toString()
}).toString());
ws.send(
JSON.stringify(
new CloudEvent({
type: "weather.error",
source: "/weather.server",
data: err.toString(),
}),
),
);
});
});
});
@ -39,7 +44,7 @@ function fetch(zip) {
const query = `${api}?zip=${zip}&appid=${key}&units=imperial`;
return new Promise((resolve, reject) => {
got(query)
.then((response) => resolve(response.body))
.then((response) => resolve(JSON.parse(response.body)))
.catch((err) => reject(err.message));
});
}

1033
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,26 +2,23 @@
"name": "cloudevents-sdk",
"version": "2.0.2",
"description": "CloudEvents SDK for JavaScript",
"main": "index.js",
"main": "dist/index.js",
"scripts": {
"watch": "tsc --project tsconfig.json --watch",
"build": "tsc --project tsconfig.json && tsc --project tsconfig.browser.json && webpack",
"prelint": "npm run build",
"lint": "standardx src examples",
"fix": "standardx --fix",
"pretest": "npm run lint && npm run test:ts",
"test": "mocha test/**/*.js",
"test:ts": "mocha --require ts-node/register ./test-ts/**/*.ts",
"lint": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}'",
"lint:fix": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}' --fix",
"pretest": "npm run lint",
"test": "mocha --require ts-node/register ./test/**/*.ts",
"coverage": "nyc --reporter=lcov --reporter=text npm run test",
"coverage-publish": "wget -qO - https://coverage.codacy.com/get.sh | bash -s report -l JavaScript -r coverage/lcov.info",
"generate-docs": "typedoc --excludeNotDocumented --out docs src",
"release": "standard-version",
"prepublish": "npm run build"
"prepublishOnly": "npm run build"
},
"files": [
"index.js",
"index.d.ts",
"lib"
"dist",
"bundles"
],
"standard-version": {
"types": [
@ -108,15 +105,21 @@
"@types/chai": "^4.2.11",
"@types/mocha": "^7.0.2",
"@types/node": "^13.13.9",
"@types/uuid": "^8.0.0",
"@typescript-eslint/eslint-plugin": "^3.4.0",
"@typescript-eslint/parser": "^3.4.0",
"chai": "~4.2.0",
"eslint": "^7.3.0",
"eslint-config-prettier": "^6.11.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.1.4",
"mocha": "~7.1.1",
"nock": "~12.0.3",
"nyc": "~15.0.0",
"prettier": "^2.0.5",
"standard-version": "^7.1.0",
"standardx": "^5.0.0",
"ts-node": "^8.10.2",
"typedoc": "^0.17.7",
"typescript": "^3.8.3",
@ -126,5 +129,5 @@
"publishConfig": {
"access": "public"
},
"types": "index.d.ts"
"types": "./dist/index.d.ts"
}

View File

@ -1,74 +1,53 @@
// Commons
module.exports = Object.freeze({
HEADERS: "headers",
const CONSTANTS = Object.freeze({
CHARSET_DEFAULT: "utf-8",
BINARY: "binary",
STRUCTURED: "structured",
SPEC_V03: "0.3",
SPEC_V1: "1.0",
DEFAULT_SPEC_VERSION_HEADER: "ce-specversion",
EXTENSIONS_PREFIX: "ce-",
ENCODING_BASE64: "base64",
DATA_ATTRIBUTE: "data",
MIME_JSON: "application/json",
MIME_OCTET_STREAM: "application/octet-stream",
MIME_CE: "application/cloudevents",
MIME_CE_JSON: "application/cloudevents+json",
HEADER_CONTENT_TYPE: "content-type",
DEFAULT_CONTENT_TYPE: "application/json; charset=utf-8",
DEFAULT_CE_CONTENT_TYPE: "application/cloudevents+json; charset=utf-8",
BINARY_HEADERS_03: {
CE_HEADERS: {
TYPE: "ce-type",
SPEC_VERSION: "ce-specversion",
SOURCE: "ce-source",
ID: "ce-id",
TIME: "ce-time",
SCHEMA_URL: "ce-schemaurl",
CONTENT_ENCODING: "ce-datacontentencoding",
SUBJECT: "ce-subject",
EXTENSIONS_PREFIX: "ce-"
},
STRUCTURED_ATTRS_03: {
TYPE: "type",
SPEC_VERSION: "specversion",
SOURCE: "source",
ID: "id",
TIME: "time",
SCHEMA_URL: "schemaurl",
CONTENT_ENCODING: "datacontentencoding",
CONTENT_TYPE: "datacontenttype",
SUBJECT: "subject",
DATA: "data"
},
BINARY_HEADERS_1: {
TYPE: "ce-type",
SPEC_VERSION: "ce-specversion",
SOURCE: "ce-source",
ID: "ce-id",
TIME: "ce-time",
DATA_SCHEMA: "ce-dataschema",
SUBJECT: "ce-subject",
EXTENSIONS_PREFIX: "ce-"
},
STRUCTURED_ATTRS_1: {
TYPE: "type",
SPEC_VERSION: "specversion",
SOURCE: "source",
CE_ATTRIBUTES: {
ID: "id",
TYPE: "type",
SOURCE: "source",
SPEC_VERSION: "specversion",
TIME: "time",
DATA_SCHEMA: "dataschema",
CONTENT_TYPE: "datacontenttype",
SUBJECT: "subject",
DATA: "data",
DATA_BASE64: "data_base64"
}
},
BINARY_HEADERS_03: {
SCHEMA_URL: "ce-schemaurl",
CONTENT_ENCODING: "ce-datacontentencoding",
},
STRUCTURED_ATTRS_03: {
SCHEMA_URL: "schemaurl",
CONTENT_ENCODING: "datacontentencoding",
},
BINARY_HEADERS_1: {
DATA_SCHEMA: "ce-dataschema",
},
STRUCTURED_ATTRS_1: {
DATA_SCHEMA: "dataschema",
DATA_BASE64: "data_base64",
},
});
export default CONSTANTS;

168
src/event/cloudevent.ts Normal file
View File

@ -0,0 +1,168 @@
import { v4 as uuidv4 } from "uuid";
import { CloudEventV1, validateV1, CloudEventV1Attributes } from "./v1";
import { CloudEventV03, validateV03, CloudEventV03Attributes } from "./v03";
import { ValidationError, isBinary, asBase64 } from "./validation";
import CONSTANTS from "../constants";
import { isString } from "util";
/**
* An enum representing the CloudEvent specification version
*/
export const enum Version {
V1 = "1.0",
V03 = "0.3",
}
/**
* A CloudEvent describes event data in common formats to provide
* interoperability across services, platforms and systems.
* @see https://github.com/cloudevents/spec/blob/v1.0/spec.md
*/
export class CloudEvent implements CloudEventV1, CloudEventV03 {
id: string;
type: string;
source: string;
specversion: Version;
datacontenttype?: string;
dataschema?: string;
subject?: string;
#_time?: string | Date;
#_data?: Record<string, unknown | string | number | boolean> | string | number | boolean | null | unknown;
data_base64?: string;
// Extensions should not exist as it's own object, but instead
// exist as properties on the event as siblings of the others
[key: string]: unknown;
// V03 deprecated attributes
schemaurl?: string;
datacontentencoding?: string;
constructor(event: CloudEventV1 | CloudEventV1Attributes | CloudEventV03 | CloudEventV03Attributes) {
// copy the incoming event so that we can delete properties as we go
// everything left after we have deleted know properties becomes an extension
const properties = { ...event };
this.id = (properties.id as string) || uuidv4();
delete properties.id;
this.type = properties.type;
delete properties.type;
this.source = properties.source;
delete properties.source;
this.specversion = (properties.specversion as Version) || Version.V1;
delete properties.specversion;
this.datacontenttype = properties.datacontenttype;
delete properties.datacontenttype;
this.subject = properties.subject;
delete properties.subject;
this.#_time = properties.time;
delete properties.time;
this.datacontentencoding = properties.datacontentencoding as string;
delete properties.datacontentencoding;
this.dataschema = properties.dataschema as string;
delete properties.dataschema;
this.data_base64 = properties.data_base64 as string;
delete properties.data_base64;
this.schemaurl = properties.schemaurl as string;
delete properties.schemaurl;
this._setData(properties.data);
delete properties.data;
// Make sure time has a default value and whatever is provided is formatted
if (!this.#_time) {
this.#_time = new Date().toISOString();
} else if (this.#_time instanceof Date) {
this.#_time = this.#_time.toISOString();
}
// sanity checking
if (this.specversion === Version.V1 && this.schemaurl) {
throw new TypeError("cannot set schemaurl on version 1.0 event");
} else if (this.specversion === Version.V03 && this.dataschema) {
throw new TypeError("cannot set dataschema on version 0.3 event");
}
// finally process any remaining properties - these are extensions
for (const [key, value] of Object.entries(properties)) {
this[key] = value;
}
}
get time(): string | Date {
return this.#_time as string | Date;
}
set time(val: string | Date) {
this.#_time = new Date(val).toISOString();
}
get data(): unknown {
if (
this.datacontenttype === CONSTANTS.MIME_JSON &&
!(this.datacontentencoding === CONSTANTS.ENCODING_BASE64) &&
isString(this.#_data)
) {
return JSON.parse(this.#_data as string);
} else if (isBinary(this.#_data)) {
return asBase64(this.#_data as Uint32Array);
}
return this.#_data;
}
set data(value: unknown) {
this._setData(value);
}
private _setData(value: unknown): void {
if (isBinary(value)) {
this.#_data = value;
this.data_base64 = asBase64(value as Uint32Array);
}
this.#_data = value;
}
toJSON(): Record<string, unknown> {
const event = { ...this };
event.time = this.time;
event.data = this.data;
return event;
}
toString(): string {
return JSON.stringify(this);
}
/**
* Validates this CloudEvent against the schema
* @throws if the CloudEvent does not conform to the schema
* @return {boolean} true if this event is valid
*/
public validate(): boolean {
try {
if (this.specversion === Version.V1) {
return validateV1(this);
} else if (this.specversion === Version.V03) {
return validateV03(this);
}
throw new ValidationError("invalid payload");
} catch (e) {
if (e instanceof ValidationError) {
throw e;
} else {
throw new ValidationError("invalid payload", e);
}
}
}
}

4
src/event/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from "./cloudevent";
export * from "./validation";
export * from "./v1";
export * from "./v03";

View File

@ -1,5 +1,3 @@
import Extensions from "../extensions";
/**
* The object interface for CloudEvents 0.3.
* @see https://github.com/cloudevents/spec/blob/v0.3/spec.md
@ -71,7 +69,7 @@ export interface CloudEventV03Attributes {
* media type. When the data field's effective data type is not String, this attribute
* MUST NOT be set and MUST be ignored when set.
*/
dataContentEncoding?: string;
datacontentencoding?: string;
/**
* [OPTIONAL] Content type of `data` value. This attribute enables `data` to
@ -84,13 +82,13 @@ export interface CloudEventV03Attributes {
* example, the JSON event format defines the relationship in
* [section 3.1](./json-format.md#31-handling-of-data).
*/
dataContentType?: string;
datacontenttype?: string;
/**
* [OPTIONAL] A link to the schema that the data attribute adheres to.
* Incompatible changes to the schema SHOULD be reflected by a different URL.
* If present, MUST be a non-empty URI.
*/
schemaURL?: string;
schemaurl?: string;
/**
* [OPTIONAL] This describes the subject of the event in the context of the
* event producer (identified by `source`). In publish-subscribe scenarios, a
@ -127,13 +125,9 @@ export interface CloudEventV03Attributes {
* specified by the datacontenttype attribute (e.g. application/json), and adheres
* to the dataschema format when those respective attributes are present.
*/
// tslint:disable-next-line:no-any
data?: any | string | number | boolean | null;
data?: Record<string, unknown | string | number | boolean> | string | number | boolean | null | unknown;
/**
* [OPTIONAL] CloudEvents extension attributes.
*/
extensions?: Extensions;
// tslint:disable-next-line:no-any
[key: string]: any;
[key: string]: unknown;
}

3
src/event/v03/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from "./cloudevent";
export * from "./spec";
export * from "./schema";

74
src/event/v03/schema.ts Normal file
View File

@ -0,0 +1,74 @@
const schemaV03 = {
$ref: "#/definitions/event",
definitions: {
specversion: {
const: "0.3",
},
datacontenttype: {
type: "string",
},
data: {
type: ["object", "string"],
},
event: {
properties: {
specversion: {
$ref: "#/definitions/specversion",
},
datacontenttype: {
$ref: "#/definitions/datacontenttype",
},
data: {
$ref: "#/definitions/data",
},
id: {
$ref: "#/definitions/id",
},
time: {
$ref: "#/definitions/time",
},
schemaurl: {
$ref: "#/definitions/schemaurl",
},
subject: {
$ref: "#/definitions/subject",
},
type: {
$ref: "#/definitions/type",
},
source: {
$ref: "#/definitions/source",
},
},
required: ["specversion", "id", "type", "source"],
type: "object",
},
id: {
type: "string",
minLength: 1,
},
time: {
format: "date-time",
type: "string",
},
schemaurl: {
type: "string",
format: "uri-reference",
},
subject: {
type: "string",
minLength: 1,
},
type: {
type: "string",
minLength: 1,
},
source: {
format: "uri-reference",
type: "string",
},
},
type: "object",
};
export { schemaV03 as schema };

42
src/event/v03/spec.ts Normal file
View File

@ -0,0 +1,42 @@
import { v4 as uuidv4 } from "uuid";
import Ajv from "ajv";
import { ValidationError, isBase64 } from "../validation";
import { CloudEventV03, CloudEventV03Attributes } from "./cloudevent";
import { CloudEvent } from "../..";
import { schema } from "./schema";
import CONSTANTS from "../../constants";
const ajv = new Ajv({ extendRefs: true });
const isValidAgainstSchema = ajv.compile(schema);
export function createV03(attributes: CloudEventV03Attributes): CloudEventV03 {
const event: CloudEventV03 = {
specversion: schema.definitions.specversion.const,
id: uuidv4(),
time: new Date().toISOString(),
...attributes,
};
return new CloudEvent(event);
}
export function validateV03(event: CloudEventV03): boolean {
if (!isValidAgainstSchema(event)) {
throw new ValidationError("invalid payload", isValidAgainstSchema.errors);
}
return checkDataContentEncoding(event);
}
function checkDataContentEncoding(event: CloudEventV03): boolean {
if (event.datacontentencoding) {
// we only support base64
const encoding = event.datacontentencoding.toLocaleLowerCase();
if (encoding !== CONSTANTS.ENCODING_BASE64) {
throw new ValidationError("invalid payload", [`Unsupported content encoding: ${encoding}`]);
} else {
if (!isBase64(event.data)) {
throw new ValidationError("invalid payload", [`Invalid content encoding of data: ${event.data}`]);
}
}
}
return true;
}

View File

@ -1,4 +1,3 @@
import Extensions from "../extensions";
/**
* The object interface for CloudEvents 1.0.
* @see https://github.com/cloudevents/spec/blob/v1.0/spec.md
@ -75,7 +74,7 @@ export interface CloudEventV1Attributes {
* example, the JSON event format defines the relationship in
* [section 3.1](./json-format.md#31-handling-of-data).
*/
dataContentType?: string;
datacontenttype?: string;
/**
* [OPTIONAL] Identifies the schema that `data` adheres to. Incompatible
* changes to the schema SHOULD be reflected by a different URI. See
@ -83,7 +82,7 @@ export interface CloudEventV1Attributes {
* for more information.
* If present, MUST be a non-empty URI.
*/
dataSchema?: string;
dataschema?: string;
/**
* [OPTIONAL] This describes the subject of the event in the context of the
* event producer (identified by `source`). In publish-subscribe scenarios, a
@ -120,13 +119,17 @@ export interface CloudEventV1Attributes {
* specified by the datacontenttype attribute (e.g. application/json), and adheres
* to the dataschema format when those respective attributes are present.
*/
// tslint:disable-next-line:no-any
data?: any | string | number | boolean | null;
data?: Record<string, unknown | string | number | boolean> | string | number | boolean | null | unknown;
/**
* [OPTIONAL] The event payload encoded as base64 data. This is used when the
* data is in binary form.
* @see https://github.com/cloudevents/spec/blob/v1.0/json-format.md#31-handling-of-data
*/
data_base64?: string;
/**
* [OPTIONAL] CloudEvents extension attributes.
*/
extensions?: Extensions;
// tslint:disable-next-line:no-any
[key: string]: any;
[key: string]: unknown;
}

3
src/event/v1/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from "./cloudevent";
export * from "./spec";
export * from "./schema";

82
src/event/v1/schema.ts Normal file
View File

@ -0,0 +1,82 @@
const schemaV1 = {
$ref: "#/definitions/event",
definitions: {
specversion: {
type: "string",
minLength: 1,
const: "1.0",
},
datacontenttype: {
type: "string",
},
data: {
type: ["object", "string"],
},
data_base64: {
type: "string",
},
event: {
properties: {
specversion: {
$ref: "#/definitions/specversion",
},
datacontenttype: {
$ref: "#/definitions/datacontenttype",
},
data: {
$ref: "#/definitions/data",
},
data_base64: {
$ref: "#/definitions/data_base64",
},
id: {
$ref: "#/definitions/id",
},
time: {
$ref: "#/definitions/time",
},
dataschema: {
$ref: "#/definitions/dataschema",
},
subject: {
$ref: "#/definitions/subject",
},
type: {
$ref: "#/definitions/type",
},
source: {
$ref: "#/definitions/source",
},
},
required: ["specversion", "id", "type", "source"],
type: "object",
},
id: {
type: "string",
minLength: 1,
},
time: {
format: "date-time",
type: "string",
},
dataschema: {
type: "string",
format: "uri",
},
subject: {
type: "string",
minLength: 1,
},
type: {
type: "string",
minLength: 1,
},
source: {
format: "uri-reference",
type: "string",
},
},
type: "object",
};
export { schemaV1 };

28
src/event/v1/spec.ts Normal file
View File

@ -0,0 +1,28 @@
import Ajv from "ajv";
import { v4 as uuidv4 } from "uuid";
import { CloudEvent } from "../cloudevent";
import { CloudEventV1, CloudEventV1Attributes } from "./cloudevent";
import { ValidationError } from "../validation";
import { schemaV1 } from "./schema";
const ajv = new Ajv({ extendRefs: true });
const isValidAgainstSchema = ajv.compile(schemaV1);
export function createV1(attributes: CloudEventV1Attributes): CloudEventV1 {
const event: CloudEventV1 = {
specversion: schemaV1.definitions.specversion.const,
id: uuidv4(),
time: new Date().toISOString(),
...attributes,
};
return new CloudEvent(event);
}
export function validateV1(event: CloudEventV1): boolean {
if (!isValidAgainstSchema(event)) {
throw new ValidationError("invalid payload", isValidAgainstSchema.errors);
}
return true;
}

View File

@ -0,0 +1,2 @@
export * from "./is";
export * from "./validation_error";

View File

@ -0,0 +1,87 @@
const isString = (v: unknown): boolean => typeof v === "string";
const isObject = (v: unknown): boolean => typeof v === "object";
const isDefined = (v: unknown): boolean => v && typeof v !== "undefined";
const isBoolean = (v: unknown): boolean => typeof v === "boolean";
const isInteger = (v: unknown): boolean => Number.isInteger(v as number);
const isDate = (v: unknown): boolean => v instanceof Date;
const isBinary = (v: unknown): boolean => v instanceof Uint32Array;
const isStringOrThrow = (v: unknown, t: Error): boolean =>
isString(v)
? true
: (() => {
throw t;
})();
const isDefinedOrThrow = (v: unknown, t: Error): boolean =>
isDefined(v)
? true
: (() => {
throw t;
})();
const isStringOrObjectOrThrow = (v: unknown, t: Error): boolean =>
isString(v)
? true
: isObject(v)
? true
: (() => {
throw t;
})();
const equalsOrThrow = (v1: unknown, v2: unknown, t: Error): boolean =>
v1 === v2
? true
: (() => {
throw t;
})();
const isBase64 = (value: unknown): boolean => Buffer.from(value as string, "base64").toString("base64") === value;
const isBuffer = (value: unknown): boolean => value instanceof Buffer;
const asBuffer = (value: string | Buffer | Uint32Array): Buffer =>
isBinary(value)
? Buffer.from(value as string)
: isBuffer(value)
? (value as Buffer)
: (() => {
throw new TypeError("is not buffer or a valid binary");
})();
const asBase64 = (value: string | Buffer | Uint32Array): string => asBuffer(value).toString("base64");
const clone = (o: Record<string, unknown>): Record<string, unknown> => JSON.parse(JSON.stringify(o));
const isJsonContentType = (contentType: string) => contentType && contentType.match(/(json)/i);
const asData = (data: unknown, contentType: string): string => {
// pattern matching alike
const maybeJson =
isString(data) && !isBase64(data) && isJsonContentType(contentType) ? JSON.parse(data as string) : data;
return isBinary(maybeJson) ? asBase64(maybeJson) : maybeJson;
};
const isValidType = (v: boolean | number | string | Date | Uint32Array): boolean =>
isBoolean(v) || isInteger(v) || isString(v) || isDate(v) || isBinary(v);
export {
isString,
isStringOrThrow,
isObject,
isDefined,
isBoolean,
isInteger,
isDate,
isBinary,
isDefinedOrThrow,
isStringOrObjectOrThrow,
isValidType,
equalsOrThrow,
isBase64,
clone,
asData,
asBase64,
};

View File

@ -0,0 +1,14 @@
import { ErrorObject } from "ajv";
/**
* An Error class that will be thrown when a CloudEvent
* cannot be properly validated against a specification.
*/
export class ValidationError extends TypeError {
errors?: string[] | ErrorObject[] | null;
constructor(message: string, errors?: string[] | ErrorObject[] | null) {
super(message);
this.errors = errors ? errors : [];
}
}

View File

@ -1,11 +1,31 @@
import { CloudEvent } from "./lib/cloudevent";
import { HTTPReceiver } from "./lib/bindings/http/http_receiver";
import { HTTPEmitter } from "./lib/bindings/http/http_emitter";
const Constants = require("./lib/bindings/http/constants");
export = {
import {
CloudEvent,
HTTPReceiver,
HTTPEmitter,
Constants
CloudEventV03,
CloudEventV03Attributes,
CloudEventV1,
CloudEventV1Attributes,
ValidationError,
Version,
} from "./event";
import { Emitter, Receiver, Mode, Protocol, TransportOptions } from "./transport";
import { Headers, headersFor } from "./transport/http/headers";
export {
// From event
CloudEvent,
CloudEventV03,
CloudEventV03Attributes,
CloudEventV1,
CloudEventV1Attributes,
Version,
ValidationError,
// From transport
Emitter,
Receiver,
Mode,
Protocol,
TransportOptions,
Headers,
headersFor,
};

View File

@ -1,88 +0,0 @@
const { default: Axios } = require("axios");
const EmitterV1 = require("./v1").BinaryEmitter;
const EmitterV3 = require("./v03").BinaryEmitter;
const {
HEADERS,
BINARY_HEADERS_03,
BINARY_HEADERS_1,
HEADER_CONTENT_TYPE,
DEFAULT_CONTENT_TYPE,
DATA_ATTRIBUTE,
SPEC_V1,
SPEC_V03
} = require("./constants");
const defaults = {
[HEADERS]: {
[HEADER_CONTENT_TYPE]: DEFAULT_CONTENT_TYPE
},
method: "POST"
};
/**
* A class to emit binary CloudEvents over HTTP.
*/
class BinaryHTTPEmitter {
/**
* Create a new {BinaryHTTPEmitter} for the provided CloudEvent specification version.
* Once an instance is created for a given spec version, it may only be used to send
* events for that version.
* Default version is 1.0
* @param {string} version - the CloudEvent HTTP specification version.
* Default: 1.0
*/
constructor(version) {
if (version === SPEC_V1) {
this.headerParserMap = EmitterV1;
this.extensionPrefix = BINARY_HEADERS_1.EXTENSIONS_PREFIX;
} else if (version === SPEC_V03) {
this.headerParserMap = EmitterV3;
this.extensionPrefix = BINARY_HEADERS_03.EXTENSIONS_PREFIX;
}
}
/**
* Sends this cloud event to a receiver over HTTP.
*
* @param {Object} options The configuration options for this event. Options
* provided other than `url` will be passed along to Node.js `http.request`.
* https://nodejs.org/api/http.html#http_http_request_options_callback
* @param {URL} options.url The HTTP/S url that should receive this event
* @param {Object} cloudevent the CloudEvent to be sent
* @returns {Promise} Promise with an eventual response from the receiver
*/
async emit(options, cloudevent) {
const config = { ...options, ...defaults };
const headers = config[HEADERS];
this.headerParserMap.forEach((parser, getterName) => {
const value = cloudevent[getterName];
if (value) {
headers[parser.headerName] = parser.parse(value);
}
});
// Set the cloudevent payload
const formatted = cloudevent.format();
let data = formatted.data;
data = (formatted.data_base64 ? formatted.data_base64 : data);
// Have extensions?
const exts = cloudevent.getExtensions();
Object.keys(exts)
.filter((ext) => Object.hasOwnProperty.call(exts, ext))
.forEach((ext) => {
headers[this.extensionPrefix + ext] = exts[ext];
});
config[DATA_ATTRIBUTE] = data;
config[HEADERS] = headers;
// Return the Promise
// @ts-ignore Types of property 'url' are incompatible. Type 'URL' is not assignable to type 'string'.
return Axios.request(config);
}
}
module.exports = BinaryHTTPEmitter;

View File

@ -1,42 +0,0 @@
const { default: Axios } = require("axios");
/** @typedef {import("../../cloudevent")} CloudEvent */
const {
DATA_ATTRIBUTE,
DEFAULT_CE_CONTENT_TYPE,
HEADERS,
HEADER_CONTENT_TYPE
} = require("./constants");
const defaults = {
[HEADERS]: {
[HEADER_CONTENT_TYPE]: DEFAULT_CE_CONTENT_TYPE
},
method: "POST"
};
/**
* A class for sending {CloudEvent} instances over HTTP.
*/
class StructuredHTTPEmitter {
// TODO: Do we really need a class here? There is no state maintenance
/**
* Sends the event over HTTP
* @param {Object} options The configuration options for this event. Options
* provided will be passed along to Node.js `http.request()`.
* https://nodejs.org/api/http.html#http_http_request_options_callback
* @param {URL} options.url The HTTP/S url that should receive this event
* @param {CloudEvent} cloudevent The CloudEvent to be sent
* @returns {Promise} Promise with an eventual response from the receiver
*/
async emit(options, cloudevent) {
const config = { ...defaults, ...options };
config[DATA_ATTRIBUTE] = cloudevent.format();
// @ts-ignore Types of property 'url' are incompatible. Type 'URL' is not assignable to type 'string'.
return Axios.request(config);
}
}
module.exports = StructuredHTTPEmitter;

View File

@ -1,109 +0,0 @@
import { CloudEvent } from "../../cloudevent";
const BinaryHTTPEmitter = require("./emitter_binary.js");
const StructuredEmitter = require("./emitter_structured.js");
const EmitterV1 = require("./v1").BinaryEmitter;
const EmitterV03 = require("./v03").BinaryEmitter;
/** @typedef {import("../../cloudevent")} CloudEvent */
const {
SPEC_V03,
SPEC_V1
} = require("./constants");
/**
* A class which is capable of sending binary and structured events using
* the CloudEvents HTTP Protocol Binding specification.
*
* @see https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md
* @see https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md#13-content-modes
*/
export class HTTPEmitter {
url: URL | string;
binary: any;
structured: any;
static headers: Function;
/**
* Creates a new instance of {HTTPEmitter}. The default emitter uses the 1.0
* protocol specification in binary mode.
*
* @param {Object} [options] The configuration options for this event emitter
* @param {URL} options.url The endpoint that will receive the sent events.
* @param {string} [options.version] The HTTP binding specification version. Default: "1.0"
* @throws {TypeError} if no options.url is provided or an unknown specification version is provided.
*/
constructor({ url = "", version = SPEC_V1 }) {
if (version !== SPEC_V03 && version !== SPEC_V1) {
throw new TypeError(
`Unknown CloudEvent specification version: ${version}`);
}
if (!url) {
throw new TypeError("A default endpoint URL is required for a CloudEvent emitter");
}
this.binary = new BinaryHTTPEmitter(version);
this.structured = new StructuredEmitter();
this.url = url;
}
/**
* Sends the {CloudEvent} to an event receiver over HTTP POST
*
* @param {CloudEvent} event the CloudEvent to be sent
* @param {Object} [options] The configuration options for this event. Options
* provided will be passed along to Node.js `http.request()`.
* https://nodejs.org/api/http.html#http_http_request_options_callback
* @param {URL} [options.url] The HTTP/S url that should receive this event.
* The URL is optional if one was provided when this emitter was constructed.
* In that case, it will be used as the recipient endpoint. The endpoint can
* be overridden by providing a URL here.
* @param {string} [options.mode] the message mode for sending this event.
* Possible values are "binary" and "structured". Default: structured
* @returns {Promise} Promise with an eventual response from the receiver
*/
send(event: CloudEvent, { url = "", mode = "binary", ...httpOpts } = {}) {
// @ts-ignore Type 'URL | undefined' is not assignable to type 'undefined'. Type 'URL' is not assignable to type 'undefined'.ts(2322)
if (!url) { url = this.url; }
// @ts-ignore Property 'url' does not exist on type '{}'
httpOpts.url = url;
if (mode === "binary") {
// @ts-ignore Property 'url' is missing in type '{}' but required in type '{ url: URL; }'.
return this.binary.emit(httpOpts, event);
} else if (mode === "structured") {
return this.structured.emit(httpOpts, event);
}
throw new TypeError(`Unknown transport mode ${mode}.`);
}
}
/**
* 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
* @param {string} [version] spec version number - default 1.0
* @returns {Object} the headers that will be sent for the event
*/
function headers(event: CloudEvent, version: string = SPEC_V1) {
const headers = {};
let headerMap;
if (version === SPEC_V1) {
headerMap = EmitterV1;
} else if (version === SPEC_V03) {
headerMap = EmitterV03;
}
headerMap.forEach((parser: any, getterName: string) => {
// @ts-ignore No index signature with a parameter of type 'string' was found on type 'CloudEvent'.
const value = event[getterName];
if (value) {
// @ts-ignore Element implicitly has an 'any' type because expression of type 'any'
// can't be used to index type '{ } '.
headers[parser.headerName] = parser.parse(value);
}
});
return headers;
}
HTTPEmitter.headers = headers;

View File

@ -1,88 +0,0 @@
import BinaryReceiver = require("./receiver_binary.js");
import StructuredReceiver = require("./receiver_structured.js");
import ValidationError = require("./validation/validation_error.js");
import { CloudEvent } from "../../cloudevent.js";
const { BINARY, STRUCTURED, SPEC_V03, SPEC_V1, HEADER_CONTENT_TYPE, MIME_CE, BINARY_HEADERS_1, DEFAULT_SPEC_VERSION_HEADER } = require("./constants");
/** @typedef {import("../../cloudevent")} CloudEvent */
/**
* A class to receive a CloudEvent from an HTTP POST request.
*/
export class HTTPReceiver {
receivers: {
v1: {
structured: any,
binary: any,
[key: string]: any
},
v03: {
structured: any,
binary: any,
[key: string]: any
}
};
/**
* Create an instance of an HTTPReceiver to accept incoming CloudEvents.
*/
constructor() {
this.receivers = {
v1: {
structured: new StructuredReceiver(SPEC_V1),
binary: new BinaryReceiver(SPEC_V1)
},
v03: {
structured: new StructuredReceiver(SPEC_V03),
binary: new BinaryReceiver(SPEC_V03)
}
};
}
/**
* Acceptor for an incoming HTTP CloudEvent POST. Can process
* binary and structured incoming CloudEvents.
*
* @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
*/
accept(headers: {}, body: {}) : CloudEvent {
const mode: string = getMode(headers);
const version = getVersion(mode, headers, body);
switch (version) {
case SPEC_V1:
return this.receivers.v1[mode].parse(body, headers);
case SPEC_V03:
return this.receivers.v03[mode].parse(body, headers);
default:
console.error(`Unknown spec version ${version}. Default to ${SPEC_V1}`);
return this.receivers.v1[mode].parse(body, headers);
}
}
}
function getMode(headers: { [key: string]: string }) {
const contentType = headers[HEADER_CONTENT_TYPE];
if (contentType && contentType.startsWith(MIME_CE)) {
return STRUCTURED;
}
if (headers[BINARY_HEADERS_1.ID]) {
return BINARY;
}
throw new ValidationError("no cloud event detected");
}
function getVersion(mode: string, headers: { [key: string]: string }, body: string | { specversion?: string }) {
if (mode === BINARY) {
// Check the headers for the version
const versionHeader = headers[DEFAULT_SPEC_VERSION_HEADER];
if (versionHeader) {
return versionHeader;
}
}
else {
// structured mode - the version is in the body
return (typeof body === "string")
? JSON.parse(body).specversion : body.specversion;
}
return SPEC_V1;
}

View File

@ -1,55 +0,0 @@
const ReceiverV1 = require("./v1/receiver_binary_1.js");
const ReceiverV3 = require("./v03/receiver_binary_0_3.js");
const { SPEC_V03, SPEC_V1 } = require("./constants");
const { check, parse } = require("./validation/binary.js");
/** @typedef {import("../../cloudevent")} CloudEvent */
/**
* 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}.
*/
class BinaryHTTPReceiver {
/**
* Creates a new BinaryHTTPReceiver to accept events over HTTP.
*
* @param {string} version the Cloud Event specification version to use. Default "1.0"
*/
constructor(version = SPEC_V1) {
if (version === SPEC_V1) {
this.receiver = new ReceiverV1();
} else if (version === SPEC_V03) {
this.receiver = new ReceiverV3();
}
}
/**
* Checks an incoming HTTP request to determine if it conforms to the
* Cloud Event specification for this receiver.
*
* @param {Object} payload the HTTP request body
* @param {Object} headers the HTTP request headers
* @returns {boolean} true if the the provided payload and headers conform to the spec
* @throws {ValidationError} if the event does not conform to the spec
*/
check(payload, headers) {
return check(payload, headers, this.receiver);
}
/**
* Parses an incoming HTTP request, converting it to a {CloudEvent}
* instance if it conforms to the Cloud Event specification for this receiver.
*
* @param {Object} payload the HTTP request body
* @param {Object} headers the HTTP request headers
* @returns {CloudEvent} an instance of CloudEvent representing the incoming request
* @throws {ValidationError} of the event does not conform to the spec
*/
parse(payload, headers) {
return parse(payload, headers, this.receiver);
}
}
module.exports = BinaryHTTPReceiver;

View File

@ -1,55 +0,0 @@
const ReceiverV1 = require("./v1/receiver_structured_1.js");
const ReceiverV3 = require("./v03/receiver_structured_0_3.js");
const { SPEC_V03, SPEC_V1 } = require("./constants");
const { check, parse } = require("./validation/structured.js");
/** @typedef {import("../../cloudevent")} CloudEvent */
/**
* A utility class used to receive structured CloudEvents
* over HTTP.
* @see {StructuredReceiver}
*/
class StructuredHTTPReceiver {
/**
* Creates a new StructuredHTTPReceiver. Supports Cloud Events specification
* versions 0.3 and 1.0 (default).
*
* @param {string} version the Cloud Events specification version. Default: 1.0.
*/
constructor(version = SPEC_V1) {
if (version === SPEC_V1) {
this.receiver = new ReceiverV1();
} else if (version === SPEC_V03) {
this.receiver = new ReceiverV3();
}
}
/**
* Checks whether the provided payload and headers conform to the Cloud Events
* specification version supported by this instance.
*
* @param {object} payload the cloud event data payload
* @param {object} headers the HTTP headers received for this cloud event
* @returns {boolean} true if the payload and header combination are valid
* @throws {ValidationError} if the payload and header combination do not conform to the spec
*/
check(payload, headers) {
return check(payload, headers, this.receiver);
}
/**
* 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, headers) {
return parse(payload, headers, this.receiver);
}
}
module.exports = StructuredHTTPReceiver;

View File

@ -1,35 +0,0 @@
const {
HEADER_CONTENT_TYPE,
BINARY_HEADERS_03 : {
CONTENT_ENCODING,
SUBJECT,
TYPE,
SPEC_VERSION,
SOURCE,
ID,
TIME,
SCHEMA_URL
}
} = require("../constants");
function parser(header, parser = (v) => v) {
return { headerName: header, parse: parser };
}
const passThroughParser = parser;
/**
* A utility Map used to retrieve the header names for a CloudEvent
* using the CloudEvent getter function.
*/
const headerMap = new Map();
headerMap.set("dataContentType", passThroughParser(HEADER_CONTENT_TYPE));
headerMap.set("dataContentEncoding", passThroughParser(CONTENT_ENCODING));
headerMap.set("subject", passThroughParser(SUBJECT));
headerMap.set("type", passThroughParser(TYPE));
headerMap.set("specversion", passThroughParser(SPEC_VERSION));
headerMap.set("source", passThroughParser(SOURCE));
headerMap.set("id", passThroughParser(ID));
headerMap.set("time", passThroughParser(TIME));
headerMap.set("schemaURL", passThroughParser(SCHEMA_URL));
module.exports = headerMap;

View File

@ -1,9 +0,0 @@
const Spec = require("./spec_0_3.js");
const BinaryEmitter = require("./emitter_binary_0_3.js");
const StructuredEmitter = require("./receiver_structured_0_3.js");
module.exports = {
Spec,
BinaryEmitter,
StructuredEmitter
};

View File

@ -1,70 +0,0 @@
const {
SPEC_V03,
MIME_JSON,
MIME_OCTET_STREAM,
ENCODING_BASE64,
HEADER_CONTENT_TYPE,
BINARY_HEADERS_03
} = require("../constants");
const Spec = require("./spec_0_3.js");
const JSONParser = require("../../../formats/json/parser.js");
const Base64Parser = require("../../../formats/base64.js");
const parserByType = {
[MIME_JSON]: new JSONParser(),
[MIME_OCTET_STREAM]: {
parse(payload) { return payload; }
}
};
const parsersByEncoding = {
null: parserByType,
undefined: parserByType,
[ENCODING_BASE64]: {
[MIME_JSON]: new JSONParser(new Base64Parser()),
[MIME_OCTET_STREAM]: {
parse(payload) { return payload; }
}
}
};
const allowedContentTypes = [
MIME_JSON, MIME_OCTET_STREAM
];
const requiredHeaders = [
BINARY_HEADERS_03.TYPE,
BINARY_HEADERS_03.SPEC_VERSION,
BINARY_HEADERS_03.SOURCE,
BINARY_HEADERS_03.ID
];
const passThroughParser = (v) => v;
const setterByHeader = {
[BINARY_HEADERS_03.TYPE] : { name: "type", parser: passThroughParser },
[BINARY_HEADERS_03.SPEC_VERSION] : { name: "specversion", parser: () => "0.3" },
[BINARY_HEADERS_03.SOURCE] : { name: "source", parser: passThroughParser },
[BINARY_HEADERS_03.ID] : { name: "id", parser: passThroughParser },
[BINARY_HEADERS_03.TIME] : { name: "time", parser: (v) => new Date(Date.parse(v)) },
[BINARY_HEADERS_03.SCHEMA_URL] : { name: "schemaURL", parser: passThroughParser },
[HEADER_CONTENT_TYPE]: { name: "dataContentType", parser: passThroughParser },
[BINARY_HEADERS_03.CONTENT_ENCONDING]: { name: "dataContentEncoding", parser: passThroughParser },
[BINARY_HEADERS_03.SUBJECT] : { name: "subject", parser: passThroughParser }
};
class Receiver {
constructor() {
this.parsersByEncoding = parsersByEncoding;
this.setterByHeader = setterByHeader;
this.allowedContentTypes = allowedContentTypes;
this.requiredHeaders = requiredHeaders;
this.extensionsPrefix = BINARY_HEADERS_03.EXTENSIONS_PREFIX;
this.specversion = SPEC_V03;
this.Spec = Spec;
this.spec = new Spec();
}
}
module.exports = Receiver;

View File

@ -1,57 +0,0 @@
const {
MIME_JSON,
MIME_CE_JSON,
STRUCTURED_ATTRS_03 : {
TYPE,
SPEC_VERSION,
SOURCE,
ID,
TIME,
SCHEMA_URL,
CONTENT_ENCODING,
CONTENT_TYPE,
SUBJECT,
DATA
}
} = require("../constants");
const Spec = require("./spec_0_3.js");
const JSONParser = require("../../../formats/json/parser.js");
const jsonParserSpec = new JSONParser();
const parserByMime = {
[MIME_JSON]: jsonParserSpec,
[MIME_CE_JSON]: jsonParserSpec
};
const allowedContentTypes = [
MIME_CE_JSON
];
function parser(name, parser = (v) => v) {
return { name: name, parser: parser};
}
const passThroughParser = parser;
const parserMap = new Map();
parserMap.set(TYPE, passThroughParser("type"));
parserMap.set(SPEC_VERSION, passThroughParser("specversion"));
parserMap.set(SOURCE, passThroughParser("source"));
parserMap.set(ID, passThroughParser("id"));
parserMap.set(TIME, parser("time", (v) => new Date(Date.parse(v))));
parserMap.set(SCHEMA_URL, passThroughParser("schemaurl"));
parserMap.set(CONTENT_ENCODING, passThroughParser("dataContentEncoding"));
parserMap.set(CONTENT_TYPE, passThroughParser("dataContentType"));
parserMap.set(SUBJECT, passThroughParser("subject"));
parserMap.set(DATA, passThroughParser("data"));
class Receiver {
constructor() {
this.parserByMime = parserByMime;
this.parserMap = parserMap;
this.allowedContentTypes = allowedContentTypes;
this.Spec = Spec;
this.spec = new Spec();
}
}
module.exports = Receiver;

View File

@ -1,265 +0,0 @@
const { v4: uuidv4 } = require("uuid");
const Ajv = require("ajv");
const ValidationError = require("../validation/validation_error.js");
const {
isBase64,
clone,
asData
} = require("../validation/fun.js");
const RESERVED_ATTRIBUTES = {
type: "type",
specversion: "specversion",
source: "source",
id: "id",
time: "time",
schemaurl: "schemaurl",
datacontentencoding: "datacontentencoding",
datacontenttype: "datacontenttype",
subject: "subject",
data: "data"
};
const SUPPORTED_CONTENT_ENCODING = {};
SUPPORTED_CONTENT_ENCODING.base64 = {
check: (data) => isBase64(data)
};
const schema = {
$ref: "#/definitions/event",
definitions: {
specversion: {
const: "0.3"
},
datacontenttype: {
type: "string"
},
data: {
type: [
"object",
"string"
]
},
event: {
properties: {
specversion: {
$ref: "#/definitions/specversion"
},
datacontenttype: {
$ref: "#/definitions/datacontenttype"
},
data: {
$ref: "#/definitions/data"
},
id: {
$ref: "#/definitions/id"
},
time: {
$ref: "#/definitions/time"
},
schemaurl: {
$ref: "#/definitions/schemaurl"
},
subject: {
$ref: "#/definitions/subject"
},
type: {
$ref: "#/definitions/type"
},
extensions: {
$ref: "#/definitions/extensions"
},
source: {
$ref: "#/definitions/source"
}
},
required: [
"specversion",
"id",
"type",
"source"
],
type: "object"
},
id: {
type: "string",
minLength: 1
},
time: {
format: "date-time",
type: "string"
},
schemaurl: {
type: "string",
format: "uri-reference"
},
subject: {
type: "string",
minLength: 1
},
type: {
type: "string",
minLength: 1
},
extensions: {
type: "object"
},
source: {
format: "uri-reference",
type: "string"
}
},
type: "object"
};
const ajv = new Ajv({
extendRefs: true
});
const isValidAgainstSchema = ajv.compile(schema);
/** @typedef {import("../../../cloudevent")} CloudEvent */
/**
* Decorates a CloudEvent with the 0.3 specification getters and setters
* @ignore
*/
class Spec03 {
constructor() {
this.payload = {
specversion: schema.definitions.specversion.const,
id: uuidv4(),
time: new Date().toISOString()
};
}
check() {
const toCheck = this.payload;
if (!isValidAgainstSchema(toCheck)) {
throw new ValidationError("invalid payload", isValidAgainstSchema.errors);
}
Array.of(toCheck)
.filter((tc) => tc.datacontentencoding)
.map((tc) => tc.datacontentencoding.toLocaleLowerCase("en-US"))
.filter((dce) => !Object.keys(SUPPORTED_CONTENT_ENCODING).includes(dce))
.forEach((dce) => {
throw new ValidationError("invalid payload", [`Unsupported content encoding: ${dce}`]);
});
Array.of(toCheck)
.filter((tc) => tc.datacontentencoding)
.filter((tc) => (typeof tc.data) === "string")
.map((tc) => {
const newtc = clone(tc);
newtc.datacontentencoding =
newtc.datacontentencoding.toLocaleLowerCase("en-US");
return newtc;
})
.filter((tc) => Object.keys(SUPPORTED_CONTENT_ENCODING)
.includes(tc.datacontentencoding))
.filter((tc) => !SUPPORTED_CONTENT_ENCODING[tc.datacontentencoding]
.check(tc.data))
.forEach((tc) => {
throw new ValidationError("invalid payload", [`Invalid content encoding of data: ${tc.data}`]);
});
}
set id(id) {
this.payload.id = id;
}
get id() {
return this.payload.id;
}
set source(source) {
this.payload.source = source;
}
get source() {
return this.payload.source;
}
get specversion() {
return this.payload.specversion;
}
set type(type) {
this.payload.type = type;
}
get type() {
return this.payload.type;
}
set dataContentType(datacontenttype) {
this.payload.datacontenttype = datacontenttype;
}
get dataContentType() {
return this.payload.datacontenttype;
}
set schemaURL(schema) {
this.payload.schemaURL = schema;
}
get schemaURL() {
return this.payload.schemaURL;
}
set subject(subject) {
this.payload.subject = subject;
}
get subject() {
return this.payload.subject;
}
set time(time) {
this.payload.time = time;
}
get time() {
return this.payload.time;
}
set data(data) {
this.payload.data = data;
}
get data() {
const dct = this.payload.datacontenttype;
const dce = this.payload.datacontentencoding;
if (dct && !dce) {
this.payload.data = asData(this.payload.data, dct);
}
return this.payload.data;
}
set dataContentEncoding(encoding) {
this.payload.datacontentencoding = encoding;
}
get dataContentEncoding() {
return this.payload.datacontentencoding;
}
}
Spec03.prototype.addExtension = function(key, value) {
if (!Object.prototype.hasOwnProperty.call(RESERVED_ATTRIBUTES, key)) {
this.payload[key] = value;
} else {
throw new ValidationError(`Reserved attribute name: '${key}'`);
}
return this;
};
module.exports = Spec03;

View File

@ -1,33 +0,0 @@
const {
HEADER_CONTENT_TYPE,
BINARY_HEADERS_1 : {
SUBJECT,
TYPE,
SPEC_VERSION,
SOURCE,
ID,
TIME,
DATA_SCHEMA
}
} = require("../constants");
function parser(header, parser = (v) => v) {
return { headerName: header, parse: parser };
}
const passThroughParser = parser;
/**
* A utility Map used to retrieve the header names for a CloudEvent
* using the CloudEvent getter function.
*/
const headerMap = new Map();
headerMap.set("dataContentType", passThroughParser(HEADER_CONTENT_TYPE));
headerMap.set("subject", passThroughParser(SUBJECT));
headerMap.set("type", passThroughParser(TYPE));
headerMap.set("specversion", passThroughParser(SPEC_VERSION));
headerMap.set("source", passThroughParser(SOURCE));
headerMap.set("id", passThroughParser(ID));
headerMap.set("time", passThroughParser(TIME));
headerMap.set("dataSchema", passThroughParser(DATA_SCHEMA));
module.exports = headerMap;

View File

@ -1,9 +0,0 @@
const Spec = require("./spec_1.js");
const BinaryEmitter = require("./emitter_binary_1.js");
const StructuredEmitter = require("./receiver_structured_1.js");
module.exports = {
Spec,
BinaryEmitter,
StructuredEmitter
};

View File

@ -1,62 +0,0 @@
const {
SPEC_V1,
MIME_JSON,
MIME_OCTET_STREAM,
ENCODING_BASE64,
BINARY_HEADERS_1,
HEADER_CONTENT_TYPE
} = require("../constants");
const Spec = require("./spec_1.js");
const JSONParser = require("../../../formats/json/parser.js");
const Base64Parser = require("../../../formats/base64.js");
const parserByType = {
[MIME_JSON] : new JSONParser(),
[MIME_OCTET_STREAM] : { parse(payload) { return payload; } }
};
const parsersByEncoding = {
null: parserByType,
undefined: parserByType,
[ENCODING_BASE64]: {
[MIME_JSON]: new JSONParser(new Base64Parser()),
[MIME_OCTET_STREAM]: {
parse(payload) { return payload; }
}
}
};
const allowedContentTypes = [MIME_JSON, MIME_OCTET_STREAM];
const requiredHeaders = [BINARY_HEADERS_1.TYPE, BINARY_HEADERS_1.SPEC_VERSION,
BINARY_HEADERS_1.SOURCE, BINARY_HEADERS_1.ID];
const passThroughParser = (v) => v;
const setterByHeader = {
[BINARY_HEADERS_1.TYPE] : { name: "type", parser: passThroughParser },
[BINARY_HEADERS_1.SPEC_VERSION] : { name: "specversion", parser: () => "1.0" },
[BINARY_HEADERS_1.SOURCE] : { name: "source", parser: passThroughParser },
[BINARY_HEADERS_1.ID] : { name: "id", parser: passThroughParser },
[BINARY_HEADERS_1.TIME] : { name: "time", parser: (v) => new Date(Date.parse(v)) },
[BINARY_HEADERS_1.DATA_SCHEMA] : { name: "dataSchema", parser: passThroughParser },
[HEADER_CONTENT_TYPE] : { name: "dataContentType", parser: passThroughParser },
[BINARY_HEADERS_1.SUBJECT] : { name: "subject", parser: passThroughParser }
};
class Receiver {
constructor() {
this.parserByType = parserByType;
this.parsersByEncoding = parsersByEncoding;
this.allowedContentTypes = allowedContentTypes;
this.requiredHeaders = requiredHeaders;
this.setterByHeader = setterByHeader;
this.specversion = SPEC_V1;
this.extensionsPrefix = BINARY_HEADERS_1.EXTENSIONS_PREFIX;
this.Spec = Spec;
this.spec = new Spec();
}
}
module.exports = Receiver;

View File

@ -1,57 +0,0 @@
const {
MIME_CE_JSON,
STRUCTURED_ATTRS_1 : {
TYPE,
SPEC_VERSION,
SOURCE,
ID,
TIME,
DATA_SCHEMA,
CONTENT_TYPE,
SUBJECT,
DATA,
DATA_BASE64,
MIME_JSON
}
} = require("../constants");
const Spec = require("./spec_1.js");
const JSONParser = require("../../../formats/json/parser.js");
const jsonParser = new JSONParser();
const parserByMime = {
[MIME_JSON]: jsonParser,
[MIME_CE_JSON]: jsonParser
};
const allowedContentTypes = [ MIME_CE_JSON ];
function parser(name, parser = (v) => v) {
return { name: name, parser: parser};
}
const passThroughParser = parser;
const parserMap = new Map();
parserMap.set(TYPE, passThroughParser("type"));
parserMap.set(SPEC_VERSION, passThroughParser("specversion"));
parserMap.set(SOURCE, passThroughParser("source"));
parserMap.set(ID, passThroughParser("id"));
parserMap.set(TIME, parser("time", (v) => new Date(Date.parse(v))));
parserMap.set(DATA_SCHEMA, passThroughParser("dataSchema"));
parserMap.set(CONTENT_TYPE, passThroughParser("dataContentType"));
parserMap.set(SUBJECT, passThroughParser("subject"));
parserMap.set(DATA, passThroughParser("data"));
parserMap.set(DATA_BASE64, passThroughParser("data"));
class Receiver {
constructor() {
this.parserByMime = parserByMime;
this.parserMap = parserMap;
this.allowedContentTypes = allowedContentTypes;
this.Spec = Spec;
this.spec = new Spec();
}
}
module.exports = Receiver;

View File

@ -1,227 +0,0 @@
const { v4: uuidv4 } = require("uuid");
const Ajv = require("ajv");
const ValidationError = require("../validation/validation_error.js");
const {
asData,
isBoolean,
isInteger,
isString,
isDate,
isBinary
} = require("../validation/fun.js");
const isValidType = (v) =>
(isBoolean(v) || isInteger(v) || isString(v) || isDate(v) || isBinary(v));
const RESERVED_ATTRIBUTES = {
type: "type",
specversion: "specversion",
source: "source",
id: "id",
time: "time",
dataschema: "dataschema",
datacontenttype: "datacontenttype",
subject: "subject",
data: "data",
data_base64: "data_base64"
};
const schema = {
$ref: "#/definitions/event",
definitions: {
specversion: {
type: "string",
minLength: 1,
const: "1.0"
},
datacontenttype: {
type: "string"
},
data: {
type: ["object", "string"]
},
data_base64: {
type: "string"
},
event: {
properties: {
specversion: {
$ref: "#/definitions/specversion"
},
datacontenttype: {
$ref: "#/definitions/datacontenttype"
},
data: {
$ref: "#/definitions/data"
},
data_base64: {
$ref: "#/definitions/data_base64"
},
id: {
$ref: "#/definitions/id"
},
time: {
$ref: "#/definitions/time"
},
dataschema: {
$ref: "#/definitions/dataschema"
},
subject: {
$ref: "#/definitions/subject"
},
type: {
$ref: "#/definitions/type"
},
source: {
$ref: "#/definitions/source"
}
},
required: ["specversion", "id", "type", "source"],
type: "object"
},
id: {
type: "string",
minLength: 1
},
time: {
format: "date-time",
type: "string"
},
dataschema: {
type: "string",
format: "uri"
},
subject: {
type: "string",
minLength: 1
},
type: {
type: "string",
minLength: 1
},
source: {
format: "uri-reference",
type: "string"
}
},
type: "object"
};
const ajv = new Ajv({
extendRefs: true
});
const isValidAgainstSchema = ajv.compile(schema);
/** @typedef {import("../../../cloudevent")} CloudEvent */
/**
* Decorates a CloudEvent with the 1.0 specification getters and setters
* @ignore
*/
class Spec1 {
constructor() {
this.payload = {
specversion: schema.definitions.specversion.const,
id: uuidv4(),
time: new Date().toISOString()
};
Object.freeze(this);
}
check() {
if (!isValidAgainstSchema(this.payload)) {
throw new ValidationError("invalid payload", isValidAgainstSchema.errors);
}
}
set id(id) {
this.payload.id = id;
}
get id() {
return this.payload.id;
}
set source(source) {
this.payload.source = source;
}
get source() {
return this.payload.source;
}
get specversion() {
return this.payload.specversion;
}
set type(type) {
this.payload.type = type;
}
get type() {
return this.payload.type;
}
set dataContentType(datacontenttype) {
this.payload.datacontenttype = datacontenttype;
}
get dataContentType() {
return this.payload.datacontenttype;
}
set dataSchema(schema) {
this.payload.dataSchema = schema;
}
get dataSchema() {
return this.payload.dataSchema;
}
set subject(subject) {
this.payload.subject = subject;
}
get subject() {
return this.payload.subject;
}
set time(time) {
this.payload.time = time;
}
get time() {
return this.payload.time;
}
set data(data) {
this.payload.data = data;
}
get data() {
const dct = this.payload.datacontenttype;
if (dct) {
this.payload.data = asData(this.payload.data, dct);
}
return this.payload.data;
}
addExtension(key, value) {
if (!Object.prototype.hasOwnProperty.call(RESERVED_ATTRIBUTES, key)) {
if (isValidType(value)) {
this.payload[key] = value;
} else {
throw new ValidationError("Invalid type of extension value");
}
} else {
throw new ValidationError(`Reserved attribute name: '${key}'`);
}
return this;
}
}
module.exports = Spec1;

View File

@ -1,91 +0,0 @@
const { CloudEvent } = require("../../../cloudevent");
const {
sanityAndClone,
validateArgs
} = require("./commons.js");
const ValidationError = require("./validation_error.js");
const {
HEADER_CONTENT_TYPE,
MIME_JSON,
DEFAULT_SPEC_VERSION_HEADER
} = require("../constants");
const {
isString,
isObject,
isBase64
} = require("./fun.js");
function check(payload, headers, receiver) {
// Validation Level 0
validateArgs(payload, headers);
// The receiver determines the specification version
if (!isObject(receiver)) throw new SyntaxError("no receiver");
// Clone and low case all headers names
const sanityHeaders = sanityAndClone(headers);
// Validation Level 1 - if content-type exists, be sure it's
// an allowed type
const contentTypeHeader = sanityHeaders[HEADER_CONTENT_TYPE];
const noContentType = !receiver.allowedContentTypes.includes(contentTypeHeader);
if (contentTypeHeader && noContentType) {
throw new ValidationError("invalid content type", [sanityHeaders[HEADER_CONTENT_TYPE]]);
}
receiver.requiredHeaders
.filter((required) => !sanityHeaders[required])
.forEach((required) => {
throw new ValidationError(`header '${required}' not found`);
});
if (sanityHeaders[DEFAULT_SPEC_VERSION_HEADER] !== receiver.specversion) {
throw new ValidationError("invalid spec version", [sanityHeaders[DEFAULT_SPEC_VERSION_HEADER]]);
}
return true;
}
function parse(payload, headers, receiver) {
payload = isString(payload) && isBase64(payload)
? Buffer.from(payload, "base64").toString()
: payload;
check(payload, headers, receiver);
// Clone and low case all headers names
const sanityHeaders = sanityAndClone(headers);
if (!sanityHeaders[HEADER_CONTENT_TYPE]) {
sanityHeaders[HEADER_CONTENT_TYPE] = MIME_JSON;
}
const eventObj = {};
const setterByHeader = receiver.setterByHeader;
Array.from(Object.keys(setterByHeader))
.filter((header) => sanityHeaders[header])
.forEach((header) => {
eventObj[setterByHeader[header].name] = setterByHeader[header].parser(sanityHeaders[header]);
delete sanityHeaders[header];
});
// Parses the payload
const parser = receiver.parsersByEncoding[eventObj.dataContentEncoding][eventObj.dataContentType];
const parsedPayload = parser.parse(payload);
const cloudevent = new CloudEvent({ source: undefined, type: undefined, ...eventObj, data: parsedPayload });
// Every unprocessed header can be an extension
Array.from(Object.keys(sanityHeaders))
.filter((value) => value.startsWith(receiver.extensionsPrefix))
.map((extension) => extension.substring(receiver.extensionsPrefix.length))
.forEach((extension) => cloudevent.addExtension(extension,
sanityHeaders[receiver.extensionsPrefix + extension]));
// Validates the event
cloudevent.spec.check();
return cloudevent;
}
module.exports = {
check, parse
};

View File

@ -1,51 +0,0 @@
const ValidationError = require("./validation_error.js");
const Constants = require("../constants");
const {
isDefinedOrThrow,
isStringOrObjectOrThrow
} = require("./fun.js");
// Specific sanity for content-type header
function sanityContentType(contentType) {
if (contentType) {
return Array.of(contentType)
.map((c) => c.split(";"))
.map((c) => c.shift())
.shift();
}
return contentType;
}
function sanityAndClone(headers) {
const sanityHeaders = {};
Array.from(Object.keys(headers))
.filter((header) => Object.hasOwnProperty.call(headers, header))
.forEach((header) => {
sanityHeaders[header.toLowerCase()] = headers[header];
});
sanityHeaders[Constants.HEADER_CONTENT_TYPE] =
sanityContentType(sanityHeaders[Constants.HEADER_CONTENT_TYPE]);
return sanityHeaders;
}
function validateArgs(payload, attributes) {
Array.of(payload)
.filter((p) => isDefinedOrThrow(p, new ValidationError("payload is null or undefined")))
.filter((p) => isStringOrObjectOrThrow(p, new ValidationError("payload must be an object or a string")))
.shift();
Array.of(attributes)
.filter((a) => isDefinedOrThrow(a, new ValidationError("attributes is null or undefined")))
.shift();
}
module.exports = {
sanityAndClone,
sanityContentType,
validateArgs
};

View File

@ -1,88 +0,0 @@
// Functional approach
const isString = (v) => (typeof v) === "string";
const isObject = (v) => (typeof v) === "object";
const isDefined = (v) => v && (typeof v) !== "undefined";
const isBoolean = (v) => (typeof v) === "boolean";
const isInteger = (v) => Number.isInteger(v);
const isDate = (v) => (v instanceof Date);
const isBinary = (v) => (v instanceof Uint32Array);
const isStringOrThrow = (v, t) =>
(isString(v)
? true
: (() => { throw t; })());
const isDefinedOrThrow = (v, t) =>
(isDefined(v)
? () => true
: (() => { throw t; })());
const isStringOrObjectOrThrow = (v, t) =>
(isString(v)
? true
: isObject(v)
? true
: (() => { throw t; })());
const equalsOrThrow = (v1, v2, t) =>
(v1 === v2
? true
: (() => { throw t; })());
const isBase64 = (value) =>
Buffer.from(value, "base64").toString("base64") === value;
const isBuffer = (value) =>
value instanceof Buffer;
const asBuffer = (value) =>
isBinary(value)
? Buffer.from(value)
: isBuffer(value)
? value
: (() => { throw new TypeError("is not buffer or a valid binary"); })();
const asBase64 = (value) =>
asBuffer(value).toString("base64");
const clone = (o) =>
JSON.parse(JSON.stringify(o));
const isJsonContentType = (contentType) =>
contentType && contentType.match(/(json)/i);
const asData = (data, contentType) => {
// pattern matching alike
const maybeJson = isString(data) &&
!isBase64(data) &&
isJsonContentType(contentType)
? JSON.parse(data)
: data;
return isBinary(maybeJson)
? asBase64(maybeJson)
: maybeJson;
};
module.exports = {
isString,
isStringOrThrow,
isObject,
isDefined,
isBoolean,
isInteger,
isDate,
isBinary,
isDefinedOrThrow,
isStringOrObjectOrThrow,
equalsOrThrow,
isBase64,
clone,
asData,
asBase64
};

View File

@ -1,52 +0,0 @@
const { CloudEvent } = require("../../../cloudevent");
const ValidationError = require("./validation_error.js");
const {
sanityAndClone,
validateArgs
} = require("./commons.js");
const {
HEADER_CONTENT_TYPE
} = require("../constants");
function check(payload, headers, receiver) {
validateArgs(payload, headers);
const sanityHeaders = sanityAndClone(headers);
// Validation Level 1
if (!receiver.allowedContentTypes.includes(sanityHeaders[HEADER_CONTENT_TYPE])) {
throw new ValidationError("invalid content type", [sanityHeaders[HEADER_CONTENT_TYPE]]);
}
return true;
}
function parse(payload, headers, receiver) {
check(payload, headers, receiver);
const sanityHeaders = sanityAndClone(headers);
const contentType = sanityHeaders[HEADER_CONTENT_TYPE];
const parser = receiver.parserByMime[contentType];
const incoming = { ...parser.parse(payload) };
const event = {
type: undefined,
source: undefined
};
receiver.parserMap.forEach((value, key) => {
if (incoming[key]) {
event[value.name] = value.parser(incoming[key]);
delete incoming[key];
}
});
const cloudevent = new CloudEvent(event);
// Every unprocessed attribute should be an extension
Array.from(Object.keys(incoming)).forEach((extension) =>
cloudevent.addExtension(extension, incoming[extension]));
cloudevent.spec.check();
return cloudevent;
}
module.exports = { parse, check };

View File

@ -1,23 +0,0 @@
/**
* @ignore
* @typedef {import("ajv").ErrorObject} ErrorObject
* */
/**
* A Error class that will be thrown when a CloudEvent
* cannot be properly validated against a specification.
*/
class ValidationError extends TypeError {
/**
* Constructs a new {ValidationError} with the message
* and array of additional errors.
* @param {string} message the error message
* @param {string[]|ErrorObject[]} [errors] any additional errors related to validation
*/
constructor(message, errors) {
super(message);
this.errors = errors ? errors : [];
}
}
module.exports = ValidationError;

View File

@ -1,314 +0,0 @@
import { CloudEventV1, CloudEventV1Attributes } from "./v1";
import { CloudEventV03, CloudEventV03Attributes } from "./v03";
import Spec1 from "./bindings/http/v1/spec_1.js";
import Spec03 from "./bindings/http/v03/spec_0_3.js";
import Formatter from "./formats/json/formatter.js";
import { isBinary } from "./bindings/http/validation/fun.js";
import Extensions from "./extensions";
const { SPEC_V1, SPEC_V03 } = require("./bindings/http/constants");
export type CE = CloudEventV1 | CloudEventV1Attributes | CloudEventV03 | CloudEventV03Attributes
/**
* A CloudEvent describes event data in common formats to provide
* interoperability across services, platforms and systems.
* @see https://github.com/cloudevents/spec/blob/v1.0/spec.md
*/
export class CloudEvent {
spec: any;
formatter: any;
extensions: Extensions;
/**
* Creates a new CloudEvent instance
* @param {object} event CloudEvent properties as a simple object
* @param {string} event.source Identifies the context in which an event happened as a URI reference
* @param {string} event.type Describes the type of event related to the originating occurrence
* @param {string} [event.id] A unique ID for this event - if not supplied, will be autogenerated
* @param {string} [event.time] A timestamp for this event. May also be provided as a Date
* @param {string} [event.subject] Describes the subject of the event in the context of the event producer
* @param {string} [event.dataContentType] The mime content type for the event data
* @param {string} [event.dataSchema] The URI of the schema that the event data adheres to (v1.0 events)
* @param {string} [event.schemaURL] The URI of the schema that the event data adheres to (v0.3 events)
* @param {string} [event.dataContentEncoding] The content encoding for the event data (v0.3 events)
* @param {string} [event.specversion] The CloudEvent specification version for this event - default: 1.0
* @param {object} [event.extensions] The CloudEvent extensions for this event
* @param {*} [event.data] The event payload
*/
constructor(event: CE) {
if (!event || !event.type || !event.source) {
throw new TypeError("event type and source are required");
}
switch (event.specversion) {
case SPEC_V1:
this.spec = new Spec1();
break;
case SPEC_V03:
this.spec = new Spec03();
break;
case undefined:
this.spec = new Spec1();
break;
default:
throw new TypeError(`unknown specification version ${event.specversion}`);
}
this.source = event.source;
this.type = event.type;
this.dataContentType = event.dataContentType;
this.data = event.data;
this.subject = event.subject;
if (event.dataSchema) {
this.dataSchema = event.dataSchema;
}
// TODO: Deprecated in 1.0
if (event.dataContentEncoding) {
this.dataContentEncoding = event.dataContentEncoding;
}
// TODO: Deprecated in 1.0
if (event.schemaURL) {
this.schemaURL = event.schemaURL;
}
if (event.id) {
this.id = event.id;
}
if (event.time) {
this.time = event.time;
}
this.formatter = new Formatter();
this.extensions = {};
if (event.extensions) {
for (const key in event.extensions) {
this.addExtension(key, event.extensions[key]);
}
}
}
/**
* Gets or sets the event id. Source + id must be unique for each distinct event.
* @see https://github.com/cloudevents/spec/blob/master/spec.md#id
* @type {string}
*/
get id() {
return this.spec.id;
}
set id(id) {
this.spec.id = id;
}
/**
* Gets or sets the origination source of this event as a URI.
* @type {string}
* @see https://github.com/cloudevents/spec/blob/master/spec.md#source-1
*/
get source() {
return this.spec.source;
}
set source(source) {
this.spec.source = source;
}
/**
* Gets the CloudEvent specification version
* @type {string}
* @see https://github.com/cloudevents/spec/blob/master/spec.md#specversion
*/
get specversion() {
return this.spec.specversion;
}
/**
* Gets or sets the event type
* @type {string}
* @see https://github.com/cloudevents/spec/blob/master/spec.md#type
*/
get type() {
return this.spec.type;
}
set type(type) {
this.spec.type = type;
}
/**
* Gets or sets the content type of the data value for this event
* @type {string}
* @see https://github.com/cloudevents/spec/blob/master/spec.md#datacontenttype
*/
get dataContentType() {
return this.spec.dataContentType;
}
set dataContentType(contenttype) {
this.spec.dataContentType = contenttype;
}
/**
* Gets or sets the event's data schema
* @type {string}
* @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#dataschema
*/
get dataSchema() {
if (this.spec instanceof Spec1) {
return this.spec.dataSchema;
}
throw new TypeError("cannot get dataSchema from version 0.3 event");
}
set dataSchema(dataschema) {
if (this.spec instanceof Spec1) {
this.spec.dataSchema = dataschema;
} else {
throw new TypeError("cannot set dataSchema on version 0.3 event");
}
}
/**
* Gets or sets the event's data content encoding
* @type {string}
* @see https://github.com/cloudevents/spec/blob/v0.3/spec.md#datacontentencoding
*/
get dataContentEncoding() {
if (this.spec instanceof Spec03) {
return this.spec.dataContentEncoding;
}
throw new TypeError("cannot get dataContentEncoding from version 1.0 event");
}
set dataContentEncoding(dataContentEncoding) {
if (this.spec instanceof Spec03) {
this.spec.dataContentEncoding = dataContentEncoding;
} else {
throw new TypeError("cannot set dataContentEncoding on version 1.0 event");
}
}
/**
* Gets or sets the event subject
* @type {string}
* @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#subject
*/
get subject() {
return this.spec.subject;
}
set subject(subject) {
this.spec.subject = subject;
}
/**
* Gets or sets the timestamp for this event as an ISO formatted date string
* @type {string}
* @see https://github.com/cloudevents/spec/blob/master/spec.md#time
*/
get time() {
return this.spec.time;
}
set time(time) {
this.spec.time = new Date(time).toISOString();
}
/**
* DEPRECATED: Gets or sets the schema URL for this event. Throws {TypeError}
* if this is a version 1.0 event.
* @type {string}
* @see https://github.com/cloudevents/spec/blob/v0.3/spec.md#schemaurl
*/
get schemaURL() {
if (this.spec instanceof Spec03) {
return this.spec.schemaURL;
}
throw new TypeError("cannot get schemaURL from version 1.0 event");
}
// TODO: Deprecated in 1.0
set schemaURL(schemaurl) {
if (schemaurl && (this.spec instanceof Spec03)) {
this.spec.schemaURL = schemaurl;
} else if (schemaurl) {
throw new TypeError("cannot set schemaURL on version 1.0 event");
}
}
/**
* Gets or sets the data for this event
* @see https://github.com/cloudevents/spec/blob/master/spec.md#event-data
* @type {*}
*/
get data() {
return this.spec.data;
}
set data(data) {
this.spec.data = data;
}
/**
* Formats the CloudEvent as JSON. Validates the event according
* to the CloudEvent specification and throws an exception if
* it's invalid.
* @returns {JSON} the CloudEvent in JSON form
* @throws {ValidationError} if this event cannot be validated against the specification
*/
format() {
this.spec.check();
const payload = {
data: undefined,
data_base64: undefined,
...this.spec.payload
};
// Handle when is binary, creating the data_base64
if (isBinary(payload.data)) {
// TODO: The call to this.spec.data formats the binary data
// I think having a side effect like this is an anti-pattern.
// FIXIT
payload.data_base64 = this.spec.data;
delete payload.data;
} else {
delete payload.data_base64;
}
return this.formatter.format(payload);
}
/**
* Formats the CloudEvent as JSON. No specification validation is performed.
* @returns {string} the CloudEvent as a JSON string
*/
toString() {
return this.formatter.toString(this.spec.payload);
}
/**
* Adds an extension attribute to this CloudEvent
* @see https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes
* @param {string} key the name of the extension attribute
* @param {*} value the value of the extension attribute
* @returns {void}
*/
addExtension(key: string, value: any): void {
this.spec.addExtension(key, value);
this.extensions = { [key]: value, ...this.extensions };
}
/**
* Gets the extension attributes, if any, associated with this event
* @see https://github.com/cloudevents/spec/blob/master/spec.md#extension-context-attributes
* @returns {Object} the extensions attributes - if none exist will will be {}
*/
getExtensions(): object {
return this.extensions;
}
}

View File

@ -1,3 +0,0 @@
export default interface Extensions {
[key: string]: any
}

View File

@ -1,18 +0,0 @@
class Base64Parser {
decorator: any;
constructor(decorator: any) {
this.decorator = decorator;
}
parse(payload: any): any {
let payloadToParse = payload;
if (this.decorator) {
payloadToParse = this.decorator.parse(payload);
}
return Buffer.from(payloadToParse, "base64").toString();
}
}
module.exports = Base64Parser;

View File

@ -1,15 +0,0 @@
class JSONFormatter {
/*
* Every internal data structure is JSON by nature, so
* no transformation is required
*/
format(payload: any) {
return payload;
}
toString(payload: any) {
return JSON.stringify(payload);
}
}
export default JSONFormatter;

18
src/parsers/base64.ts Normal file
View File

@ -0,0 +1,18 @@
import { Parser } from "./parser";
export class Base64Parser implements Parser {
decorator?: Parser;
constructor(decorator?: Parser) {
this.decorator = decorator;
}
parse(payload: Record<string, unknown> | string): string {
let payloadToParse = payload;
if (this.decorator) {
payloadToParse = this.decorator.parse(payload) as string;
}
return Buffer.from(payloadToParse as string, "base64").toString();
}
}

7
src/parsers/date.ts Normal file
View File

@ -0,0 +1,7 @@
import { Parser } from "./parser";
export class DateParser extends Parser {
parse(payload: string): Date {
return new Date(Date.parse(payload));
}
}

25
src/parsers/index.ts Normal file
View File

@ -0,0 +1,25 @@
import { Parser } from "./parser";
import { JSONParser } from "./json";
import { PassThroughParser } from "./pass_through";
import { Base64Parser } from "./base64";
import CONSTANTS from "../constants";
export * from "./parser";
export * from "./base64";
export * from "./json";
export * from "./date";
export * from "./mapped";
export * from "./pass_through";
export const parserByContentType: { [key: string]: Parser } = {
[CONSTANTS.MIME_JSON]: new JSONParser(),
[CONSTANTS.MIME_CE_JSON]: new JSONParser(),
[CONSTANTS.MIME_OCTET_STREAM]: new PassThroughParser(),
};
export const parserByEncoding: { [key: string]: { [key: string]: Parser } } = {
base64: {
[CONSTANTS.MIME_CE_JSON]: new JSONParser(new Base64Parser()),
[CONSTANTS.MIME_OCTET_STREAM]: new PassThroughParser(),
},
};

View File

@ -1,18 +1,15 @@
const {
isString,
isDefinedOrThrow,
isStringOrObjectOrThrow
} = require("../../bindings/http/validation/fun");
const ValidationError = require("../../bindings/http/validation/validation_error.js");
import { Base64Parser } from "./base64";
import { isString, isDefinedOrThrow, isStringOrObjectOrThrow, ValidationError } from "../event/validation";
import { Parser } from "./parser";
const invalidPayloadTypeError = new ValidationError("invalid payload type, allowed are: string or object");
const nullOrUndefinedPayload = new ValidationError("null or undefined payload");
const asJSON = (v: object|string) => (isString(v) ? JSON.parse(v as string) : v);
const parseJSON = (v: Record<string, unknown> | string): string => (isString(v) ? JSON.parse(v as string) : v);
class JSONParser {
decorator: Base64Parser
constructor(decorator: Base64Parser) {
export class JSONParser implements Parser {
decorator?: Base64Parser;
constructor(decorator?: Base64Parser) {
this.decorator = decorator;
}
@ -21,15 +18,13 @@ class JSONParser {
* @param {object|string} payload the JSON payload
* @return {object} the parsed JSON payload.
*/
parse(payload: object|string) {
parse(payload: Record<string, unknown> | string): string {
if (this.decorator) {
payload = this.decorator.parse(payload);
}
isDefinedOrThrow(payload, nullOrUndefinedPayload);
isStringOrObjectOrThrow(payload, invalidPayloadTypeError);
return asJSON(payload);
return parseJSON(payload);
}
}
module.exports = JSONParser;

6
src/parsers/mapped.ts Normal file
View File

@ -0,0 +1,6 @@
import { Parser } from "./parser";
export interface MappedParser {
name: string;
parser: Parser;
}

3
src/parsers/parser.ts Normal file
View File

@ -0,0 +1,3 @@
export abstract class Parser {
abstract parse(payload: Record<string, unknown> | string): unknown;
}

View File

@ -0,0 +1,7 @@
import { Parser } from "./parser";
export class PassThroughParser extends Parser {
parse(payload: unknown): unknown {
return payload;
}
}

76
src/transport/emitter.ts Normal file
View File

@ -0,0 +1,76 @@
import { CloudEvent } from "../event";
import { emitBinary, emitStructured } from "./http";
import { Protocol } from ".";
import { AxiosResponse } from "axios";
import { Agent } from "http";
/**
* Options supplied to the Emitter when sending an event.
* In addition to url and protocol, TransportOptions may
* also accept custom options that will be passed to the
* Node.js http functions.
*/
export interface TransportOptions {
/**
* The endpoint that will receieve the event.
* @example http://cncf.example.com/receiver
*/
url?: string;
/**
* The network protocol over which the event will be sent.
* @example HTTPStructured
* @example HTTPBinary
*/
protocol?: Protocol;
[key: string]: string | Record<string, unknown> | Protocol | Agent | undefined;
}
interface EmitterFunction {
(event: CloudEvent, options: TransportOptions): Promise<AxiosResponse>;
}
/**
* A class to send binary and structured CloudEvents to a remote endpoint.
* Currently, supported protocols are HTTPBinary and HTTPStructured.
*
* @see https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md
* @see https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md#13-content-modes
*/
export class Emitter {
url?: string;
protocol: Protocol;
emitter: EmitterFunction;
constructor(options: TransportOptions = { protocol: Protocol.HTTPBinary }) {
this.protocol = options.protocol as Protocol;
this.url = options.url;
this.emitter = emitBinary;
if (this.protocol === Protocol.HTTPStructured) {
this.emitter = emitStructured;
}
}
/**
* Sends the {CloudEvent} to an event receiver over HTTP POST
*
* @param {CloudEvent} event the CloudEvent to be sent
* @param {Object} [options] The configuration options for this event. Options
* provided will be passed along to Node.js `http.request()`.
* https://nodejs.org/api/http.html#http_http_request_options_callback
* @param {string} [options.url] The HTTP/S url that should receive this event.
* The URL is optional if one was provided when this emitter was constructed.
* 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
*/
send(event: CloudEvent, options?: TransportOptions): Promise<AxiosResponse> {
options = options || {};
options.url = options.url || this.url;
if (options.protocol != this.protocol) {
if (this.protocol === Protocol.HTTPBinary) return emitBinary(event, options);
return emitStructured(event, options);
}
return this.emitter(event, options);
}
}

View File

@ -0,0 +1,31 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import { CloudEvent, Version } from "../../event";
import { TransportOptions } from "../emitter";
import { Headers, headersFor } from "./headers";
import { asData } from "../../event/validation";
import CONSTANTS from "../../constants";
/**
* Send a CloudEvent over HTTP POST to the `options.url` provided.
* @param {CloudEvent} event the event to send to the remote endpoint
* @param {TransportOptions} options options provided to the transport layer
* @returns {Promise<AxiosResponse>} the HTTP response from the transport layer
*/
export async function emitBinary(event: CloudEvent, options: TransportOptions): Promise<AxiosResponse> {
if (event.specversion !== Version.V1 && event.specversion !== Version.V03) {
return Promise.reject(`Unknown spec version ${event.specversion}`);
}
return emit(event, options, headersFor(event));
}
async function emit(event: CloudEvent, options: TransportOptions, headers: Headers): Promise<AxiosResponse> {
const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CE_CONTENT_TYPE };
const config = {
...options,
method: "POST",
headers: { ...contentType, ...headers, ...(options.headers as Headers) },
data: asData(event.data, event.datacontenttype as string),
};
return axios.request(config as AxiosRequestConfig);
}

View File

@ -0,0 +1,78 @@
import { CloudEvent, Version } from "../..";
import { CloudEventV1, validateV1 } from "../../event/v1";
import { CloudEventV03, validateV03 } from "../../event/v03";
import { Headers, validate } from "./headers";
import { binaryParsers as v1Parsers } from "./v1/parsers";
import { binaryParsers as v03Parsers } from "./v03/parsers";
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>, 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 = validate(headers);
const eventObj: { [key: string]: unknown | string | Record<string, unknown> } = {};
const parserMap: Record<string, MappedParser> = this.version === Version.V1 ? v1Parsers : v03Parsers;
for (const header in parserMap) {
if (sanitizedHeaders[header]) {
const mappedParser: MappedParser = parserMap[header];
eventObj[mappedParser.name] = mappedParser.parser.parse(sanitizedHeaders[header]);
delete sanitizedHeaders[header];
}
}
const parser = parserByContentType[eventObj.datacontenttype as string];
const 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)] = sanitizedHeaders[header];
}
}
const cloudevent = new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03);
this.version === Version.V1 ? validateV1(cloudevent) : validateV03(cloudevent);
return cloudevent;
}
}

View File

@ -0,0 +1,112 @@
import { ValidationError, CloudEvent } from "../..";
import { headerMap as v1Map } from "./v1";
import { headerMap as v03Map } from "./v03";
import { Version } from "../../event";
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.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 = v1Map;
} else {
headerMap = v03Map;
}
// 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]));
sanitized[CONSTANTS.HEADER_CONTENT_TYPE] = sanitizeContentType(sanitized[CONSTANTS.HEADER_CONTENT_TYPE]) as string;
return sanitized;
}
function sanitizeContentType(contentType: string): string | undefined {
if (contentType) {
return Array.of(contentType)
.map((c) => c.split(";"))
.map((c) => c.shift())
.shift();
}
return contentType;
}

View File

@ -0,0 +1,2 @@
export * from "./binary_emitter";
export * from "./structured_emitter";

View File

@ -0,0 +1,20 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import { CloudEvent } from "../../event";
import { TransportOptions } from "../emitter";
import CONSTANTS from "../../constants";
const defaults = {
headers: {
[CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CE_CONTENT_TYPE,
},
};
export function emitStructured(event: CloudEvent, options: TransportOptions): Promise<AxiosResponse> {
const config = {
...defaults,
...options,
method: "POST",
data: event,
};
return axios.request(config as AxiosRequestConfig);
}

View File

@ -0,0 +1,87 @@
import { CloudEvent, Version } from "../..";
import { Headers, sanitize } from "./headers";
import { Parser, JSONParser, MappedParser } from "../../parsers";
import { parserByContentType } from "../../parsers";
import { structuredParsers as v1Parsers } from "./v1/parsers";
import { structuredParsers as v03Parsers } from "./v03/parsers";
import { isString, isBase64, ValidationError, isStringOrObjectOrThrow } from "../../event/validation";
import { CloudEventV1, validateV1 } from "../../event/v1";
import { CloudEventV03, validateV03 } from "../../event/v03";
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, 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 ? v1Parsers : v03Parsers;
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");
}
}
const cloudevent = new CloudEvent(eventObj as CloudEventV1 | CloudEventV03);
// Validates the event
this.version === Version.V1 ? validateV1(cloudevent) : validateV03(cloudevent);
return cloudevent;
}
}

View File

@ -0,0 +1,23 @@
import { PassThroughParser, MappedParser } from "../../../parsers";
import CONSTANTS from "../../../constants";
const passThrough = new PassThroughParser();
function parser(header: string, parser = passThrough): MappedParser {
return { name: header, parser };
}
/**
* A utility Map used to retrieve the header names for a CloudEvent
* using the CloudEvent getter function.
*/
export const headerMap: Readonly<{ [key: string]: MappedParser }> = Object.freeze({
[CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE]: parser(CONSTANTS.HEADER_CONTENT_TYPE),
[CONSTANTS.CE_ATTRIBUTES.SUBJECT]: parser(CONSTANTS.CE_HEADERS.SUBJECT),
[CONSTANTS.CE_ATTRIBUTES.TYPE]: parser(CONSTANTS.CE_HEADERS.TYPE),
[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]: parser(CONSTANTS.CE_HEADERS.SPEC_VERSION),
[CONSTANTS.CE_ATTRIBUTES.SOURCE]: parser(CONSTANTS.CE_HEADERS.SOURCE),
[CONSTANTS.CE_ATTRIBUTES.ID]: parser(CONSTANTS.CE_HEADERS.ID),
[CONSTANTS.CE_ATTRIBUTES.TIME]: parser(CONSTANTS.CE_HEADERS.TIME),
[CONSTANTS.STRUCTURED_ATTRS_03.CONTENT_ENCODING]: parser(CONSTANTS.BINARY_HEADERS_03.CONTENT_ENCODING),
[CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL]: parser(CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL),
});

View File

@ -0,0 +1 @@
export * from "./headers";

View File

@ -0,0 +1,34 @@
import { MappedParser, DateParser, PassThroughParser } from "../../../parsers";
import CONSTANTS from "../../../constants";
const passThrough = new PassThroughParser();
function parser(name: string, parser = passThrough): MappedParser {
return { name: name, parser: parser };
}
const binaryParsers: Record<string, MappedParser> = Object.freeze({
[CONSTANTS.CE_HEADERS.TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.TYPE),
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: parser(CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION),
[CONSTANTS.CE_HEADERS.SOURCE]: parser(CONSTANTS.CE_ATTRIBUTES.SOURCE),
[CONSTANTS.CE_HEADERS.ID]: parser(CONSTANTS.CE_ATTRIBUTES.ID),
[CONSTANTS.CE_HEADERS.TIME]: parser(CONSTANTS.CE_ATTRIBUTES.TIME, new DateParser()),
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: parser(CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL),
[CONSTANTS.CE_HEADERS.SUBJECT]: parser(CONSTANTS.CE_ATTRIBUTES.SUBJECT),
[CONSTANTS.BINARY_HEADERS_03.CONTENT_ENCODING]: parser(CONSTANTS.STRUCTURED_ATTRS_03.CONTENT_ENCODING),
[CONSTANTS.HEADER_CONTENT_TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE),
});
const structuredParsers: Record<string, MappedParser> = Object.freeze({
[CONSTANTS.CE_ATTRIBUTES.TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.TYPE),
[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]: parser(CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION),
[CONSTANTS.CE_ATTRIBUTES.SOURCE]: parser(CONSTANTS.CE_ATTRIBUTES.SOURCE),
[CONSTANTS.CE_ATTRIBUTES.ID]: parser(CONSTANTS.CE_ATTRIBUTES.ID),
[CONSTANTS.CE_ATTRIBUTES.TIME]: parser(CONSTANTS.CE_ATTRIBUTES.TIME, new DateParser()),
[CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL]: parser(CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL),
[CONSTANTS.STRUCTURED_ATTRS_03.CONTENT_ENCODING]: parser(CONSTANTS.STRUCTURED_ATTRS_03.CONTENT_ENCODING),
[CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE),
[CONSTANTS.CE_ATTRIBUTES.SUBJECT]: parser(CONSTANTS.CE_ATTRIBUTES.SUBJECT),
[CONSTANTS.CE_ATTRIBUTES.DATA]: parser(CONSTANTS.CE_ATTRIBUTES.DATA),
});
export { binaryParsers, structuredParsers };

View File

@ -0,0 +1,22 @@
import { MappedParser, PassThroughParser } from "../../../parsers";
import CONSTANTS from "../../../constants";
const passThrough = new PassThroughParser();
function parser(header: string, parser = passThrough): MappedParser {
return { name: header, parser };
}
/**
* A utility Map used to retrieve the header names for a CloudEvent
* using the CloudEvent getter function.
*/
export const headerMap: Readonly<{ [key: string]: MappedParser }> = Object.freeze({
[CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE]: parser(CONSTANTS.HEADER_CONTENT_TYPE),
[CONSTANTS.CE_ATTRIBUTES.SUBJECT]: parser(CONSTANTS.CE_HEADERS.SUBJECT),
[CONSTANTS.CE_ATTRIBUTES.TYPE]: parser(CONSTANTS.CE_HEADERS.TYPE),
[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]: parser(CONSTANTS.CE_HEADERS.SPEC_VERSION),
[CONSTANTS.CE_ATTRIBUTES.SOURCE]: parser(CONSTANTS.CE_HEADERS.SOURCE),
[CONSTANTS.CE_ATTRIBUTES.ID]: parser(CONSTANTS.CE_HEADERS.ID),
[CONSTANTS.CE_ATTRIBUTES.TIME]: parser(CONSTANTS.CE_HEADERS.TIME),
[CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA]: parser(CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA),
});

View File

@ -0,0 +1 @@
export * from "./headers";

View File

@ -0,0 +1,35 @@
import { PassThroughParser, DateParser, MappedParser } from "../../../parsers";
import CONSTANTS from "../../../constants";
const passThrough = new PassThroughParser();
function parser(name: string, parser = passThrough): MappedParser {
return { name: name, parser: parser };
}
const binaryParsers: Record<string, MappedParser> = Object.freeze({
[CONSTANTS.CE_HEADERS.TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.TYPE),
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: parser(CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION),
[CONSTANTS.CE_HEADERS.SOURCE]: parser(CONSTANTS.CE_ATTRIBUTES.SOURCE),
[CONSTANTS.CE_HEADERS.ID]: parser(CONSTANTS.CE_ATTRIBUTES.ID),
[CONSTANTS.CE_HEADERS.TIME]: parser(CONSTANTS.CE_ATTRIBUTES.TIME, new DateParser()),
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: parser(CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA),
[CONSTANTS.CE_HEADERS.SUBJECT]: parser(CONSTANTS.CE_ATTRIBUTES.SUBJECT),
[CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE),
[CONSTANTS.HEADER_CONTENT_TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE),
});
const structuredParsers: Record<string, MappedParser> = Object.freeze({
[CONSTANTS.CE_ATTRIBUTES.TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.TYPE),
[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]: parser(CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION),
[CONSTANTS.CE_ATTRIBUTES.SOURCE]: parser(CONSTANTS.CE_ATTRIBUTES.SOURCE),
[CONSTANTS.CE_ATTRIBUTES.ID]: parser(CONSTANTS.CE_ATTRIBUTES.ID),
[CONSTANTS.CE_ATTRIBUTES.TIME]: parser(CONSTANTS.CE_ATTRIBUTES.TIME, new DateParser()),
[CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA]: parser(CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA),
[CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE),
[CONSTANTS.CE_ATTRIBUTES.SUBJECT]: parser(CONSTANTS.CE_ATTRIBUTES.SUBJECT),
[CONSTANTS.CE_ATTRIBUTES.DATA]: parser(CONSTANTS.CE_ATTRIBUTES.DATA),
[CONSTANTS.STRUCTURED_ATTRS_1.DATA_BASE64]: parser(CONSTANTS.STRUCTURED_ATTRS_1.DATA_BASE64),
});
export { structuredParsers, binaryParsers };

6
src/transport/index.ts Normal file
View File

@ -0,0 +1,6 @@
export * from "./emitter";
export * from "./receiver";
export * as v1Headers from "./http/v1";
export * as v03Headers from "./http/v03";
export * as http from "./http/headers";
export * from "./protocols";

View File

@ -0,0 +1,8 @@
/**
* An enum representing the transport protocols for an event
*/
export const enum Protocol {
HTTPBinary,
HTTPStructured,
HTTP,
}

118
src/transport/receiver.ts Normal file
View File

@ -0,0 +1,118 @@
import { Headers } 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 { CloudEventV03 } from "../event/v03";
import { CloudEventV1 } from "../event/v1";
import { Protocol } from "./protocols";
import CONSTANTS from "../constants";
/**
* An enum representing the two HTTP transport modes, binary and structured
*/
export enum Mode {
BINARY = "binary",
STRUCTURED = "structured",
}
/**
* A class to receive a CloudEvent from an HTTP POST request.
*/
export class Receiver {
protocol: Protocol;
receivers: {
v1: {
structured: StructuredReceiver;
binary: BinaryReceiver;
[key: string]: unknown;
};
v03: {
structured: StructuredReceiver;
binary: BinaryReceiver;
[key: string]: unknown;
};
};
/**
* Create an instance of an HTTPReceiver to accept incoming CloudEvents.
* @param {Protocol} protocol the transport protocol - currently only Protocol.HTTP is supported
*/
constructor(protocol: Protocol = Protocol.HTTP) {
// currently unused, but reserved for future protocol implementations
this.protocol = protocol;
this.receivers = {
v1: {
structured: new StructuredReceiver(Version.V1),
binary: new BinaryReceiver(Version.V1),
},
v03: {
structured: new StructuredReceiver(Version.V03),
binary: new BinaryReceiver(Version.V03),
},
};
}
/**
* Acceptor for an incoming HTTP CloudEvent POST. Can process
* binary and structured incoming CloudEvents.
*
* @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
*/
accept(headers: Headers, body: string | Record<string, unknown> | CloudEventV1 | CloudEventV03): CloudEvent {
const mode: Mode = getMode(headers);
const version = getVersion(mode, headers, body);
switch (version) {
case Version.V1:
return this.receivers.v1[mode].parse(body, headers);
case Version.V03:
return this.receivers.v03[mode].parse(body, headers);
default:
console.error(`Unknown spec version ${version}. Default to ${Version.V1}`);
return this.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,
) {
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.specversion;
}
return Version.V1;
}

View File

@ -1,214 +0,0 @@
import { expect } from "chai";
import { CloudEvent } from "../";
import { CloudEventV03Attributes } from "../lib/v03";
import { CloudEventV1Attributes } from "../lib/v1";
import Extensions from "../lib/extensions";
const { SPEC_V1, SPEC_V03 } = require("../lib/bindings/http/constants");
interface Message {
type: string;
subject: string;
data: any;
source: string;
dataContentType: string;
}
const type = "org.cncf.cloudevents.example";
const source = "http://unit.test";
const message: Message = {
type,
source,
subject: "greeting",
data: {
hello: "world"
},
dataContentType: "application/json"
};
const fixture: CloudEventV1Attributes|CloudEventV03Attributes = {
source,
type
};
describe("A CloudEvent", () => {
it("Can be constructed with a typed Message", () => {
const ce = new CloudEvent(message);
expect(ce.type).to.equal(type);
expect(ce.source).to.equal(source);
});
});
describe("A 1.0 CloudEvent", () => {
it("has retreivable source and type attributes", () => {
const ce = new CloudEvent(fixture);
expect(ce.source).to.equal("http://unit.test");
expect(ce.type).to.equal("org.cncf.cloudevents.example");
});
it("defaults to specversion 1.0", () => {
const ce = new CloudEvent(fixture);
expect(ce.specversion).to.equal("1.0");
});
it("generates an ID if one is not provided in the constructor", () => {
const ce = new CloudEvent(fixture);
expect(ce.id).to.not.be.empty;
})
it("can be created with the specversion SPEC_V1", () => {
const ce = new CloudEvent({ specversion: SPEC_V1, ...fixture });
expect(ce.specversion).to.equal(SPEC_V1);
});
it("can be constructed with an ID", () => {
const ce = new CloudEvent({ id: 1234, ...fixture });
expect(ce.id).to.equal(1234);
});
it("generates a timestamp by default", () => {
const ce = new CloudEvent(fixture);
expect(ce.time).to.not.be.empty;
});
it("can be constructed with a timestamp", () => {
const time = new Date();
const ce = new CloudEvent({ time, ...fixture });
expect(ce.time).to.equal(time.toISOString());
});
it("can be constructed with a dataContentType", () => {
const ce = new CloudEvent({ dataContentType: "application/json", ...fixture });
expect(ce.dataContentType).to.equal("application/json");
});
it("can be constructed with a dataSchema", () => {
const ce = new CloudEvent({ dataSchema: "http://my.schema", ...fixture });
expect(ce.dataSchema).to.equal("http://my.schema");
});
it("can be constructed with a subject", () => {
const ce = new CloudEvent({ subject: "science", ...fixture });
expect(ce.subject).to.equal("science");
});
// Handle deprecated attribute - should this really throw?
it("throws a TypeError when constructed with a schemaurl", () => {
expect(() => { new CloudEvent({ schemaURL: "http://throw.com", ...fixture }); })
.to.throw(TypeError, "cannot set schemaURL on version 1.0 event");
});
// Handle deprecated attribute - should this really throw?
it("throws a TypeError when getting a schemaURL", () => {
const ce = new CloudEvent(fixture);
expect(() => { ce.schemaURL; })
.to.throw(TypeError, "cannot get schemaURL from version 1.0 event");
});
it("can be constructed with data", () => {
const data = { lunch: "tacos" };
const ce = new CloudEvent({
data, ...fixture
});
expect(ce.data).to.equal(data);
});
it("has extensions as an empty object by default", () => {
const ce = new CloudEvent(fixture);
expect(ce.extensions).to.be.an('object')
expect(Object.keys(ce.extensions).length).to.equal(0);
});
it("can be constructed with extensions", () => {
const extensions: Extensions = {
"extension-key": "extension-value"
};
const ce = new CloudEvent({
extensions, ...fixture
});
expect(Object.keys(ce.extensions).length).to.equal(1);
expect(ce.extensions["extension-key"]).to.equal(extensions["extension-key"]);
});
it("throws ValidationError if the CloudEvent does not conform to the schema");
it("returns a JSON string even if format is invalid");
it("correctly formats a CloudEvent as JSON");
});
describe("A 0.3 CloudEvent", () => {
const v03fixture: CloudEventV03Attributes = { specversion: SPEC_V03, ...fixture };
it("has retreivable source and type attributes", () => {
const ce = new CloudEvent(v03fixture);
expect(ce.source).to.equal("http://unit.test");
expect(ce.type).to.equal("org.cncf.cloudevents.example");
});
it("generates an ID if one is not provided in the constructor", () => {
const ce = new CloudEvent(v03fixture);
expect(ce.id).to.not.be.empty;
})
it("can be constructed with an ID", () => {
const ce = new CloudEvent({ id: 1234, ...v03fixture });
expect(ce.id).to.equal(1234);
});
it("generates a timestamp by default", () => {
const ce = new CloudEvent(v03fixture);
expect(ce.time).to.not.be.empty;
});
it("can be constructed with a timestamp", () => {
const time = new Date();
const ce = new CloudEvent({ time, ...v03fixture });
expect(ce.time).to.equal(time.toISOString());
});
it("can be constructed with a dataContentType", () => {
const ce = new CloudEvent({ dataContentType: "application/json", ...v03fixture });
expect(ce.dataContentType).to.equal("application/json");
});
it("can be constructed with a dataContentEncoding", () => {
const ce = new CloudEvent({ dataContentEncoding: "Base64", ...v03fixture });
expect(ce.dataContentEncoding).to.equal("Base64");
});
it("can be constructed with a schemaURL", () => {
const ce = new CloudEvent({ schemaURL: "http://my.schema", ...v03fixture });
expect(ce.schemaURL).to.equal("http://my.schema");
});
it("can be constructed with a subject", () => {
const ce = new CloudEvent({ subject: "science", ...v03fixture });
expect(ce.subject).to.equal("science");
});
// Handle 1.0 attribute - should this really throw?
it("throws a TypeError when constructed with a dataSchema", () => {
expect(() => { new CloudEvent({ dataSchema: "http://throw.com", ...v03fixture }); })
.to.throw(TypeError, "cannot set dataSchema on version 0.3 event");
});
// Handle deprecated attribute - should this really throw?
it("throws a TypeError when getting a dataSchema", () => {
const ce = new CloudEvent(v03fixture);
expect(() => { ce.dataSchema; })
.to.throw(TypeError, "cannot get dataSchema from version 0.3 event");
});
it("can be constructed with data", () => {
const data = { lunch: "tacos" };
const ce = new CloudEvent({
data, ...v03fixture
});
expect(ce.data).to.equal(data);
});
it("throws ValidationError if the CloudEvent does not conform to the schema");
it("returns a JSON string even if format is invalid");
it("correctly formats a CloudEvent as JSON");
});

View File

@ -1,190 +0,0 @@
import "mocha";
import { expect } from "chai";
import nock from "nock";
const {
SPEC_V1,
SPEC_V03,
DEFAULT_CE_CONTENT_TYPE,
BINARY_HEADERS_03,
BINARY_HEADERS_1
} = require("../lib/bindings/http/constants");
import { CloudEvent, HTTPEmitter } from "..";
const receiver:string = "https://cloudevents.io/";
const type:string = "com.example.test";
const source:string = "urn:event:from:myapi/resource/123";
const ext1Name:string = "lunch";
const ext1Value:string = "tacos";
const ext2Name:string = "supper";
const ext2Value:string = "sushi";
const data = {
lunchBreak: "noon"
};
describe("HTTP Transport Binding Emitter for CloudEvents", () => {
beforeEach(() => {
nock(receiver)
.post("/")
.reply(function (uri, requestBody: {}) {
// return the request body and the headers so they can be
// examined in the test
if (typeof requestBody === "string") {
requestBody = JSON.parse(requestBody);
}
const returnBody = { ...requestBody, ...this.req.headers };
return [
201,
returnBody
];
});
});
describe("V1", () => {
const emitter = new HTTPEmitter({ url: receiver });
const event = new CloudEvent({
specversion: SPEC_V1,
type,
source,
time: new Date(),
data
});
event.addExtension(ext1Name, ext1Value)
event.addExtension(ext2Name, ext2Value);
it("Sends a binary 1.0 CloudEvent by default", () => {
emitter.send(event).then((response: { data: { [k: string]: string } }) => {
// A binary message will have a ce-id header
expect(response.data[BINARY_HEADERS_1.ID]).to.equal(event.id);
expect(response.data[BINARY_HEADERS_1.SPEC_VERSION]).to.equal(SPEC_V1);
// A binary message will have a request body for the data
expect(response.data.lunchBreak).to.equal(data.lunchBreak);
}).catch(expect.fail);
});
it("Provides the HTTP headers for a binary event", () => {
const headers = HTTPEmitter.headers(event);
expect(headers[BINARY_HEADERS_1.TYPE]).to.equal(event.type);
expect(headers[BINARY_HEADERS_1.SPEC_VERSION]).to.equal(event.specversion);
expect(headers[BINARY_HEADERS_1.SOURCE]).to.equal(event.source);
expect(headers[BINARY_HEADERS_1.ID]).to.equal(event.id);
expect(headers[BINARY_HEADERS_1.TIME]).to.equal(event.time);
});
it("Sends a structured 1.0 CloudEvent if specified", () => {
emitter.send(event, { mode: "structured" })
.then((response: { data: { [k: string]: string | {}, data: { lunchBreak: string } } }) => {
// A structured message will have a cloud event content type
expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE);
// Ensure other CE headers don't exist - just testing for ID
expect(response.data[BINARY_HEADERS_1.ID]).to.equal(undefined);
// The spec version would have been specified in the body
expect(response.data.specversion).to.equal(SPEC_V1);
expect(response.data.data.lunchBreak).to.equal(data.lunchBreak);
}).catch(expect.fail);
});
it("Sends to an alternate URL if specified", () => {
nock(receiver)
.post("/alternate")
.reply(function (uri, requestBody: {}) {
// return the request body and the headers so they can be
// examined in the test
if (typeof requestBody === "string") {
requestBody = JSON.parse(requestBody);
}
const returnBody = { ...requestBody, ...this.req.headers };
return [
201,
returnBody
];
});
emitter.send(event, { mode: "structured", url: `${receiver}alternate` })
.then((response: { [k: string]: string | {}, data: { [k: string]: string | {}, specversion: string, data: { lunchBreak: string }} }) => {
// A structured message will have a cloud event content type
expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE);
// Ensure other CE headers don't exist - just testing for ID
expect(response.data[BINARY_HEADERS_1.ID]).to.equal(undefined);
// The spec version would have been specified in the body
expect(response.data.specversion).to.equal(SPEC_V1);
expect(response.data.data.lunchBreak).to.equal(data.lunchBreak);
}).catch(expect.fail);
});
});
describe("V03", () => {
const emitter = new HTTPEmitter({ url: receiver, version: SPEC_V03 });
const event = new CloudEvent({
specversion: SPEC_V03,
type,
source,
time: new Date(),
data
});
event.addExtension(ext1Name, ext1Value)
event.addExtension(ext2Name, ext2Value);
it("Sends a binary 0.3 CloudEvent", () => {
emitter.send(event).then((response: { data: { lunchBreak: string, [k:string]: string }}) => {
// A binary message will have a ce-id header
expect(response.data[BINARY_HEADERS_03.ID]).to.equal(event.id);
expect(response.data[BINARY_HEADERS_03.SPEC_VERSION]).to.equal(SPEC_V03);
// A binary message will have a request body for the data
expect(response.data.lunchBreak).to.equal(data.lunchBreak);
}).catch(expect.fail);
});
it("Provides the HTTP headers for a binary event", () => {
const headers = HTTPEmitter.headers(event, SPEC_V03);
expect(headers[BINARY_HEADERS_03.TYPE]).to.equal(event.type);
expect(headers[BINARY_HEADERS_03.SPEC_VERSION]).to.equal(event.specversion);
expect(headers[BINARY_HEADERS_03.SOURCE]).to.equal(event.source);
expect(headers[BINARY_HEADERS_03.ID]).to.equal(event.id);
expect(headers[BINARY_HEADERS_03.TIME]).to.equal(event.time);
});
it("Sends a structured 0.3 CloudEvent if specified", () => {
emitter.send(event, { mode: "structured" })
.then((response: { data: { [k:string]: any, specversion: string, data: { lunchBreak: string } }}) => {
// A structured message will have a cloud event content type
expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE);
// Ensure other CE headers don't exist - just testing for ID
expect(response.data[BINARY_HEADERS_03.ID]).to.equal(undefined);
// The spec version would have been specified in the body
expect(response.data.specversion).to.equal(SPEC_V03);
expect(response.data.data.lunchBreak).to.equal(data.lunchBreak);
}).catch(expect.fail);
});
it("Sends to an alternate URL if specified", () => {
nock(receiver)
.post("/alternate")
.reply(function (uri, requestBody: {}) {
// return the request body and the headers so they can be
// examined in the test
if (typeof requestBody === "string") {
requestBody = JSON.parse(requestBody);
}
const returnBody = { ...requestBody, ...this.req.headers };
return [
201,
returnBody
];
});
emitter.send(event, { mode: "structured", url: `${receiver}alternate` })
.then((response: { data: { specversion: string, data: { lunchBreak: string }, [k:string]: any }}) => {
// A structured message will have a cloud event content type
expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE);
// Ensure other CE headers don't exist - just testing for ID
expect(response.data[BINARY_HEADERS_03.ID]).to.equal(undefined);
// The spec version would have been specified in the body
expect(response.data.specversion).to.equal(SPEC_V03);
expect(response.data.data.lunchBreak).to.equal(data.lunchBreak);
}).catch(expect.fail);
});
});
});

View File

@ -1,436 +0,0 @@
const expect = require("chai").expect;
const { CloudEvent } = require("../../../lib/cloudevent.js");
const BinaryHTTPReceiver = require("../../../lib/bindings/http/receiver_binary.js");
const ValidationError = require("../../../lib/bindings/http/validation/validation_error.js");
const {
BINARY_HEADERS_03,
SPEC_V03,
HEADER_CONTENT_TYPE
} = require("../../../lib/bindings/http/constants.js");
const receiver = new BinaryHTTPReceiver(SPEC_V03);
describe("HTTP Transport Binding Binary Receiver for 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.check.bind(receiver, payload, 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;
// act and assert
expect(receiver.check.bind(receiver, payload, attributes))
.to.throw(ValidationError, "attributes 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.check.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 = {
[BINARY_HEADERS_03.SPEC_VERSION]: "specversion",
[BINARY_HEADERS_03.SOURCE]: "source",
[BINARY_HEADERS_03.ID]: "id",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act and assert
expect(receiver.check.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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SOURCE]: "source",
[BINARY_HEADERS_03.ID]: "id",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act and assert
expect(receiver.check.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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: "specversion",
[BINARY_HEADERS_03.ID]: "id",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act and assert
expect(receiver.check.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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: "specversion",
[BINARY_HEADERS_03.SOURCE]: "source",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act and assert
expect(receiver.check.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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: "0.2",
[BINARY_HEADERS_03.SOURCE]: "source",
[BINARY_HEADERS_03.ID]: "id",
[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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: "specversion",
[BINARY_HEADERS_03.SOURCE]: "source",
[BINARY_HEADERS_03.ID]: "id",
[HEADER_CONTENT_TYPE]: "text/html"
};
// act and assert
expect(receiver.check.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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "source",
[BINARY_HEADERS_03.ID]: "id",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act and assert
expect(receiver.check.bind(receiver, payload, attributes))
.to.not.throw();
});
it("No error when content-type is unspecified", () => {
const payload = {};
const attributes = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "source",
[BINARY_HEADERS_03.ID]: "id"
};
// act and assert
expect(receiver.check.bind(receiver, payload, attributes))
.to.not.throw();
});
});
describe("Parse", () => {
it("CloudEvent contains 'type'", () => {
// setup
const payload = {
data: "dataString"
};
const attributes = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "source",
[BINARY_HEADERS_03.ID]: "id",
[BINARY_HEADERS_03.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "source",
[BINARY_HEADERS_03.ID]: "id",
[BINARY_HEADERS_03.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.specversion).to.equal(SPEC_V03);
});
it("CloudEvent contains 'source'", () => {
// setup
const payload = {
data: "dataString"
};
const attributes = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "/source",
[BINARY_HEADERS_03.ID]: "id",
[BINARY_HEADERS_03.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "/source",
[BINARY_HEADERS_03.ID]: "id",
[BINARY_HEADERS_03.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "/source",
[BINARY_HEADERS_03.ID]: "id",
[BINARY_HEADERS_03.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "/source",
[BINARY_HEADERS_03.ID]: "id",
[BINARY_HEADERS_03.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "/source",
[BINARY_HEADERS_03.ID]: "id",
[BINARY_HEADERS_03.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "/source",
[BINARY_HEADERS_03.ID]: "id",
[BINARY_HEADERS_03.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "/source",
[BINARY_HEADERS_03.ID]: "id",
[BINARY_HEADERS_03.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "/source",
[BINARY_HEADERS_03.ID]: "id",
[BINARY_HEADERS_03.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "source",
[BINARY_HEADERS_03.ID]: "id",
[BINARY_HEADERS_03.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual).to.be.an.instanceof(CloudEvent);
expect(actual).to.have.property("format");
});
it("Should accept 'extension1'", () => {
// setup
const extension1 = "mycuston-ext1";
const payload = {
data: "dataString"
};
const attributes = {
[BINARY_HEADERS_03.TYPE]: "type",
[BINARY_HEADERS_03.SPEC_VERSION]: SPEC_V03,
[BINARY_HEADERS_03.SOURCE]: "source",
[BINARY_HEADERS_03.ID]: "id",
[BINARY_HEADERS_03.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[HEADER_CONTENT_TYPE]: "application/json",
[`${[BINARY_HEADERS_03.EXTENSIONS_PREFIX]}extension1`]: extension1
};
// act
const actual = receiver.parse(payload, attributes);
const actualExtensions = actual.getExtensions();
// assert
expect(actualExtensions.extension1)
.to.equal(extension1);
});
});
});

View File

@ -1,462 +0,0 @@
const expect = require("chai").expect;
const { asBase64 } = require("../../../lib/bindings/http/validation/fun.js");
const {
BINARY_HEADERS_1,
SPEC_V1,
HEADER_CONTENT_TYPE
} = require("../../../lib/bindings/http/constants.js");
const ValidationError = require("../../../lib/bindings/http/validation/validation_error.js");
const { CloudEvent } = require("../../../lib/cloudevent.js");
const BinaryHTTPReceiver = require("../../../lib/bindings/http/receiver_binary.js");
const receiver = new BinaryHTTPReceiver(SPEC_V1);
describe("HTTP Transport Binding Binary 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.check.bind(receiver, payload, 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;
// act and assert
expect(receiver.check.bind(receiver, payload, attributes))
.to.throw(ValidationError, "attributes 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.check.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 = {
[BINARY_HEADERS_1.SPEC_VERSION]: "specversion",
[BINARY_HEADERS_1.SOURCE]: "source",
[BINARY_HEADERS_1.ID]: "id",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act and assert
expect(receiver.check.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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SOURCE]: "source",
[BINARY_HEADERS_1.ID]: "id",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act and assert
expect(receiver.check.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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: "specversion",
[BINARY_HEADERS_1.ID]: "id",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act and assert
expect(receiver.check.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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: "specversion",
[BINARY_HEADERS_1.SOURCE]: "source",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act and assert
expect(receiver.check.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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: "0.2",
[BINARY_HEADERS_1.SOURCE]: "source",
[BINARY_HEADERS_1.ID]: "id",
[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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: "specversion",
[BINARY_HEADERS_1.SOURCE]: "source",
[BINARY_HEADERS_1.ID]: "id",
[HEADER_CONTENT_TYPE]: "text/html"
};
// act and assert
expect(receiver.check.bind(receiver, payload, attributes))
.to.throw(ValidationError, "invalid content type");
});
it("No error when content-type is unspecified", () => {
const payload = {};
const attributes = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "source",
[BINARY_HEADERS_1.ID]: "id"
};
// act and assert
expect(receiver.check.bind(receiver, payload, attributes))
.to.not.throw();
});
it("No error when all required headers are in place", () => {
// setup
const payload = {};
const attributes = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "source",
[BINARY_HEADERS_1.ID]: "id",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act and assert
expect(receiver.check.bind(receiver, payload, attributes))
.to.not.throw();
});
});
describe("Parse", () => {
it("CloudEvent contains 'type'", () => {
// setup
const payload = {
data: "dataString"
};
const attributes = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.specversion).to.equal(SPEC_V1);
});
it("CloudEvent contains 'source'", () => {
// setup
const payload = {
data: "dataString"
};
const attributes = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "/source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "/source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "/source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "/source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "/source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "/source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "/source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "/source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[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), (c) => c.codePointAt(0));
const payload = asBase64(bindata);
const attributes = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "/source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[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 = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[HEADER_CONTENT_TYPE]: "application/json"
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual).to.be.an.instanceof(CloudEvent);
expect(actual).to.have.property("format");
});
it("Should accept 'extension1'", () => {
// setup
const extension1 = "mycustom-ext1";
const payload = {
data: "dataString"
};
const attributes = {
[BINARY_HEADERS_1.TYPE]: "type",
[BINARY_HEADERS_1.SPEC_VERSION]: SPEC_V1,
[BINARY_HEADERS_1.SOURCE]: "source",
[BINARY_HEADERS_1.ID]: "id",
[BINARY_HEADERS_1.TIME]: "2019-06-16T11:42:00Z",
[BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[HEADER_CONTENT_TYPE]: "application/json",
[`${[BINARY_HEADERS_1.EXTENSIONS_PREFIX]}extension1`]: extension1
};
// act
const actual = receiver.parse(payload, attributes);
const actualExtensions = actual.getExtensions();
// assert
expect(actualExtensions.extension1).to.equal(extension1);
});
});
});

View File

@ -1,213 +0,0 @@
const expect = require("chai").expect;
const ValidationError = require("../../../lib/bindings/http/validation/validation_error.js");
const HTTPStructuredReceiver = require("../../../lib/bindings/http/receiver_structured.js");
const { CloudEvent } = require("../../../index.js");
const { SPEC_V03 } = require("../../../lib/bindings/http/constants.js");
const receiver = new HTTPStructuredReceiver(SPEC_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"
};
const ext1Name = "extension1";
const ext1Value = "foobar";
const ext2Name = "extension2";
const ext2Value = "acme";
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.check.bind(receiver, payload, 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;
// act and assert
expect(receiver.check.bind(receiver, payload, attributes))
.to.throw(ValidationError, "attributes 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.check.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.check.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 = new CloudEvent({
specversion: SPEC_V03,
type,
source,
time,
dataContentType: "text/plain",
dataContentEncoding: "base64",
schemaURL,
data: "No base 64 value"
});
event.addExtension(ext1Name, ext1Value);
event.addExtension(ext2Name, ext2Value);
const payload = event.toString();
const attributes = {
"Content-Type": "application/cloudevents+json"
};
// act and assert
expect(receiver.parse.bind(receiver, payload, attributes))
.to.throw(ValidationError, "invalid payload");
});
it("No error when all required stuff are in place", () => {
// setup
const payload = {};
const attributes = {
"Content-Type": "application/cloudevents+json"
};
// act and assert
expect(receiver.check.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.check.bind(receiver, payload, attributes))
.to.not.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 = {
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).to.have.property("format");
expect(actual.id).to.equal(id);
});
it("Should accept 'extension1'", () => {
// setup
const extension1 = "mycuston-ext1";
const payload = {
type,
source,
time,
schemaURL,
data,
dataContentType: ceContentType,
"extension1": extension1
};
const headers = {
"content-type": "application/cloudevents+json"
};
// act
const actual = receiver.parse(payload, headers);
const actualExtensions = actual.getExtensions();
// assert
expect(actualExtensions.extension1).to.equal(extension1);
});
it("Should parse 'data' stringfied json to json object", () => {
const payload = new CloudEvent({
specversion: SPEC_V03,
type,
source,
time,
schemaURL,
dataContentType: ceContentType,
data: JSON.stringify(data)
}).toString();
const headers = {
"content-type": "application/cloudevents+json"
};
// act
const actual = receiver.parse(payload, headers);
// assert
expect(actual.data).to.deep.equal(data);
});
});
});

View File

@ -1,196 +0,0 @@
const expect = require("chai").expect;
const { Spec } = require("../../../lib/bindings/http/v1/index.js");
const { CloudEvent } = require("../../../index.js");
const { asBase64 } = require("../../../lib/bindings/http/validation/fun.js");
const { SPEC_V1 } = require("../../../lib/bindings/http/constants.js");
const ValidationError = require("../../../lib/bindings/http/validation/validation_error.js");
const HTTPStructuredReceiver = require("../../../lib/bindings/http/receiver_structured.js");
const receiver = new HTTPStructuredReceiver(SPEC_V1);
const type = "com.github.pull.create";
const source = "urn:event:from:myapi/resource/123";
const time = new Date();
const dataSchema = "http://cloudevents.io/schema.json";
const ceContentType = "application/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.check.bind(receiver, payload, 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;
// act and assert
expect(receiver.check.bind(receiver, payload, attributes))
.to.throw(ValidationError, "attributes 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.check.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.check.bind(receiver, payload, attributes))
.to.throw(ValidationError, "invalid content type");
});
it("No error when all required stuff are in place", () => {
// setup
const payload = {};
const attributes = {
"Content-Type": "application/cloudevents+json"
};
// act and assert
expect(receiver.check.bind(receiver, payload, attributes))
.to.not.throw();
});
});
describe("Parse", () => {
it("Throw error when the event does not follow the spec", () => {
// setup
const payload = new CloudEvent({
type,
source,
time,
data
}).toString();
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 = new CloudEvent({
id,
type,
source,
time,
data,
dataSchema,
dataContentType: ceContentType,
}).toString();
const headers = {
"content-type": "application/cloudevents+json"
};
// act
const actual = receiver.parse(payload, headers);
// assert
expect(actual).to.be.an.instanceof(CloudEvent);
expect(actual).to.have.property("format");
expect(actual.id).to.equal(id);
});
it("Should accept 'extension1'", () => {
// setup
const extension1 = "mycustom-ext1";
const event = new CloudEvent({
type,
source,
time,
data,
dataSchema,
dataContentType: ceContentType
});
event.addExtension("extension1", extension1);
const payload = event.toString();
const headers = {
"content-type": "application/cloudevents+json"
};
// act
const actual = receiver.parse(payload, headers);
const actualExtensions = actual.getExtensions();
// assert
expect(actualExtensions.extension1).to.equal(extension1);
});
it("Should parse 'data' stringified json to json object", () => {
// setup
const payload = new CloudEvent({
type,
source,
time,
dataSchema,
data: JSON.stringify(data),
dataContentType: ceContentType
}).toString();
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", () => {
// setup
const bindata = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0));
const expected = asBase64(bindata);
const payload = new CloudEvent({
type,
source,
data: bindata,
dataContentType: ceContentType
}).format();
const headers = {
"content-type": "application/cloudevents+json"
};
// act
const actual = receiver.parse(JSON.stringify(payload), headers);
// assert
expect(actual.data).to.equal(expected);
});
});
});

174
test/cloud_event_test.ts Normal file
View File

@ -0,0 +1,174 @@
import { expect } from "chai";
import { CloudEvent, Version } from "../src";
import { CloudEventV03 } from "../src/event/v03";
import { CloudEventV1 } from "../src/event/v1";
const type = "org.cncf.cloudevents.example";
const source = "http://unit.test";
const id = "b46cf653-d48a-4b90-8dfa-355c01061361";
const fixture: CloudEventV1 = {
id,
specversion: Version.V1,
source,
type,
};
describe("A CloudEvent", () => {
it("Can be constructed with a typed Message", () => {
const ce = new CloudEvent(fixture);
expect(ce.type).to.equal(type);
expect(ce.source).to.equal(source);
});
it("serializes as JSON with toString()", () => {
const ce = new CloudEvent(fixture);
expect(ce.toString()).to.deep.equal(JSON.stringify(ce));
});
});
describe("A 1.0 CloudEvent", () => {
it("has retreivable source and type attributes", () => {
const ce = new CloudEvent(fixture);
expect(ce.source).to.equal("http://unit.test");
expect(ce.type).to.equal("org.cncf.cloudevents.example");
});
it("defaults to specversion 1.0", () => {
const ce = new CloudEvent({ source, type });
expect(ce.specversion).to.equal("1.0");
});
it("generates an ID if one is not provided in the constructor", () => {
const ce = new CloudEvent({ source, type });
expect(ce.id).to.not.be.empty;
});
it("can be constructed with an ID", () => {
const ce = new CloudEvent({ id: "1234", specversion: Version.V1, source, type });
expect(ce.id).to.equal("1234");
});
it("generates a timestamp by default", () => {
const ce = new CloudEvent(fixture);
expect(ce.time).to.not.be.empty;
});
it("can be constructed with a timestamp", () => {
const time = new Date().toISOString();
const ce = new CloudEvent({ time, ...fixture });
expect(ce.time).to.equal(time);
});
it("can be constructed with a datacontenttype", () => {
const ce = new CloudEvent({ datacontenttype: "application/json", ...fixture });
expect(ce.datacontenttype).to.equal("application/json");
});
it("can be constructed with a dataschema", () => {
const ce = new CloudEvent({ dataschema: "http://my.schema", ...fixture });
expect(ce.dataschema).to.equal("http://my.schema");
});
it("can be constructed with a subject", () => {
const ce = new CloudEvent({ subject: "science", ...fixture });
expect(ce.subject).to.equal("science");
});
// Handle deprecated attribute - should this really throw?
it("throws a TypeError when constructed with a schemaurl", () => {
expect(() => {
new CloudEvent({ schemaurl: "http://throw.com", ...fixture });
}).to.throw(TypeError, "cannot set schemaurl on version 1.0 event");
});
it("can be constructed with data", () => {
const ce = new CloudEvent({
...fixture,
data: { lunch: "tacos" },
});
expect(ce.data).to.deep.equal({ lunch: "tacos" });
});
it("can be constructed with extensions", () => {
const extensions = {
"extension-key": "extension-value",
};
const ce = new CloudEvent({
...extensions,
...fixture,
});
expect(ce["extension-key"]).to.equal(extensions["extension-key"]);
});
it("throws ValidationError if the CloudEvent does not conform to the schema");
it("returns a JSON string even if format is invalid");
it("correctly formats a CloudEvent as JSON");
});
describe("A 0.3 CloudEvent", () => {
const v03fixture: CloudEventV03 = { ...fixture };
v03fixture.specversion = Version.V03;
it("has retreivable source and type attributes", () => {
const ce = new CloudEvent(v03fixture);
expect(ce.source).to.equal("http://unit.test");
expect(ce.type).to.equal("org.cncf.cloudevents.example");
});
it("generates an ID if one is not provided in the constructor", () => {
const ce = new CloudEvent({ source, type, specversion: Version.V03 });
expect(ce.id).to.not.be.empty;
expect(ce.specversion).to.equal(Version.V03);
});
it("generates a timestamp by default", () => {
const ce = new CloudEvent(v03fixture);
expect(ce.time).to.not.be.empty;
});
it("can be constructed with a timestamp", () => {
const time = new Date();
const ce = new CloudEvent({ time, ...v03fixture });
expect(ce.time).to.equal(time.toISOString());
});
it("can be constructed with a datacontenttype", () => {
const ce = new CloudEvent({ datacontenttype: "application/json", ...v03fixture });
expect(ce.datacontenttype).to.equal("application/json");
});
it("can be constructed with a datacontentencoding", () => {
const ce = new CloudEvent({ datacontentencoding: "Base64", ...v03fixture });
expect(ce.datacontentencoding).to.equal("Base64");
});
it("can be constructed with a schemaurl", () => {
const ce = new CloudEvent({ schemaurl: "http://my.schema", ...v03fixture });
expect(ce.schemaurl).to.equal("http://my.schema");
});
it("can be constructed with a subject", () => {
const ce = new CloudEvent({ subject: "science", ...v03fixture });
expect(ce.subject).to.equal("science");
});
// Handle 1.0 attribute - should this really throw?
it("throws a TypeError when constructed with a dataschema", () => {
expect(() => {
new CloudEvent({ dataschema: "http://throw.com", ...v03fixture });
}).to.throw(TypeError, "cannot set dataschema on version 0.3 event");
});
it("can be constructed with data", () => {
const ce = new CloudEvent({
...v03fixture,
data: { lunch: "tacos" },
});
expect(ce.data).to.deep.equal({ lunch: "tacos" });
});
it("throws ValidationError if the CloudEvent does not conform to the schema");
it("returns a JSON string even if format is invalid");
it("correctly formats a CloudEvent as JSON");
});

View File

@ -1,191 +0,0 @@
const expect = require("chai").expect;
const {
HEADERS,
CHARSET_DEFAULT,
BINARY,
STRUCTURED,
SPEC_V03,
SPEC_V1,
DEFAULT_SPEC_VERSION_HEADER,
ENCODING_BASE64,
DATA_ATTRIBUTE,
MIME_JSON,
MIME_OCTET_STREAM,
MIME_CE,
MIME_CE_JSON,
HEADER_CONTENT_TYPE,
DEFAULT_CONTENT_TYPE,
DEFAULT_CE_CONTENT_TYPE,
BINARY_HEADERS_03,
STRUCTURED_ATTRS_03,
BINARY_HEADERS_1,
STRUCTURED_ATTRS_1
} = require("../").Constants;
describe("Constants exposed by top level exports", () => {
it("Exports a HEADERS constant", () => {
expect(HEADERS).to.equal("headers");
});
it("Exports a CHARSET_DEFAULT constant", () => {
expect(CHARSET_DEFAULT).to.equal("utf-8");
});
it("Exports a BINARY constant", () => {
expect(BINARY).to.equal("binary");
});
it("Exports a STRUCTURED constant", () => {
expect(STRUCTURED).to.equal("structured");
});
it("Exports a SPEC_V03 constant", () => {
expect(SPEC_V03).to.equal("0.3");
});
it("Exports a SPEC_V1 constant", () => {
expect(SPEC_V1).to.equal("1.0");
});
it("Exports a DEFAULT_SPEC_VERSION_HEADER constant", () => {
expect(DEFAULT_SPEC_VERSION_HEADER).to.equal("ce-specversion");
});
it("Exports an ENCODING_BASE64 constant", () => {
expect(ENCODING_BASE64).to.equal("base64");
});
it("Exports a DATA_ATTRIBUTE constant", () => {
expect(DATA_ATTRIBUTE).to.equal("data");
});
it("Exports a MIME_JSON constant", () => {
expect(MIME_JSON).to.equal("application/json");
});
it("Exports a MIME_OCTET_STREAM constant", () => {
expect(MIME_OCTET_STREAM).to.equal("application/octet-stream");
});
it("Exports a MIME_CE constant", () => {
expect(MIME_CE).to.equal("application/cloudevents");
});
it("Exports a MIME_CE_JSON constant", () => {
expect(MIME_CE_JSON).to.equal("application/cloudevents+json");
});
it("Exports a HEADER_CONTENT_TYPE constant", () => {
expect(HEADER_CONTENT_TYPE).to.equal("content-type");
});
it("Exports a DEFAULT_CONTENT_TYPE constant", () => {
expect(DEFAULT_CONTENT_TYPE).to.equal(`${MIME_JSON}; charset=${CHARSET_DEFAULT}`);
});
it("Exports a DEFAULT_CE_CONTENT_TYPE constant", () => {
expect(DEFAULT_CE_CONTENT_TYPE).to.equal(`${MIME_CE_JSON}; charset=${CHARSET_DEFAULT}`);
});
describe("V0.3 binary headers constants", () => {
it("Provides a TYPE header", () => {
expect(BINARY_HEADERS_03.TYPE).to.equal("ce-type");
});
it("Provides a SPEC_VERSION header", () => {
expect(BINARY_HEADERS_03.SPEC_VERSION).to.equal("ce-specversion");
});
it("Provides a SOURCE header", () => {
expect(BINARY_HEADERS_03.SOURCE).to.equal("ce-source");
});
it("Provides an ID header", () => {
expect(BINARY_HEADERS_03.ID).to.equal("ce-id");
});
it("Provides a TIME header", () => {
expect(BINARY_HEADERS_03.TIME).to.equal("ce-time");
});
it("Provides a SCHEMA_URL header", () => {
expect(BINARY_HEADERS_03.SCHEMA_URL).to.equal("ce-schemaurl");
});
it("Provides a CONTENT_ENCODING header", () => {
expect(BINARY_HEADERS_03.CONTENT_ENCODING).to.equal("ce-datacontentencoding");
});
it("Provides a SUBJECT header", () => {
expect(BINARY_HEADERS_03.SUBJECT).to.equal("ce-subject");
});
it("Provides an EXTENSIONS_PREFIX constant", () => {
expect(BINARY_HEADERS_03.EXTENSIONS_PREFIX).to.equal("ce-");
});
});
describe("V0.3 structured attributes constants", () => {
it("Provides a TYPE attribute", () => {
expect(STRUCTURED_ATTRS_03.TYPE).to.equal("type");
});
it("Provides a SPEC_VERSION attribute", () => {
expect(STRUCTURED_ATTRS_03.SPEC_VERSION).to.equal("specversion");
});
it("Provides a SOURCE attribute", () => {
expect(STRUCTURED_ATTRS_03.SOURCE).to.equal("source");
});
it("Provides an ID attribute", () => {
expect(STRUCTURED_ATTRS_03.ID).to.equal("id");
});
it("Provides a TIME attribute", () => {
expect(STRUCTURED_ATTRS_03.TIME).to.equal("time");
});
it("Provides a SCHEMA_URL attribute", () => {
expect(STRUCTURED_ATTRS_03.SCHEMA_URL).to.equal("schemaurl");
});
it("Provides a CONTENT_ENCODING attribute", () => {
expect(STRUCTURED_ATTRS_03.CONTENT_ENCODING).to.equal("datacontentencoding");
});
it("Provides a SUBJECT attribute", () => {
expect(STRUCTURED_ATTRS_03.SUBJECT).to.equal("subject");
});
it("Provides a DATA attribute", () => {
expect(STRUCTURED_ATTRS_03.DATA).to.equal("data");
});
});
describe("V01 binary headers constants", () => {
it("Provides a TYPE header", () => {
expect(BINARY_HEADERS_1.TYPE).to.equal("ce-type");
});
it("Provides a SPEC_VERSION header", () => {
expect(BINARY_HEADERS_1.SPEC_VERSION).to.equal("ce-specversion");
});
it("Provides a SOURCE header", () => {
expect(BINARY_HEADERS_1.SOURCE).to.equal("ce-source");
});
it("Provides an ID header", () => {
expect(BINARY_HEADERS_1.ID).to.equal("ce-id");
});
it("Provides a TIME header", () => {
expect(BINARY_HEADERS_1.TIME).to.equal("ce-time");
});
it("Provides a DATA_SCHEMA header", () => {
expect(BINARY_HEADERS_1.DATA_SCHEMA).to.equal("ce-dataschema");
});
it("Provides a SUBJECT header", () => {
expect(BINARY_HEADERS_1.SUBJECT).to.equal("ce-subject");
});
it("Provides an EXTENSIONS_PREFIX constant", () => {
expect(BINARY_HEADERS_1.EXTENSIONS_PREFIX).to.equal("ce-");
});
});
describe("V1 structured attributes constants", () => {
it("Provides a TYPE attribute", () => {
expect(STRUCTURED_ATTRS_1.TYPE).to.equal("type");
});
it("Provides a SPEC_VERSION attribute", () => {
expect(STRUCTURED_ATTRS_1.SPEC_VERSION).to.equal("specversion");
});
it("Provides a SOURCE attribute", () => {
expect(STRUCTURED_ATTRS_1.SOURCE).to.equal("source");
});
it("Provides an ID attribute", () => {
expect(STRUCTURED_ATTRS_1.ID).to.equal("id");
});
it("Provides a TIME attribute", () => {
expect(STRUCTURED_ATTRS_1.TIME).to.equal("time");
});
it("Provides a DATA_SCHEMA attribute", () => {
expect(STRUCTURED_ATTRS_1.DATA_SCHEMA).to.equal("dataschema");
});
it("Provides a CONTENT_TYPE attribute", () => {
expect(STRUCTURED_ATTRS_1.CONTENT_TYPE).to.equal("datacontenttype");
});
it("Provides a SUBJECT attribute", () => {
expect(STRUCTURED_ATTRS_1.SUBJECT).to.equal("subject");
});
it("Provides a DATA attribute", () => {
expect(STRUCTURED_ATTRS_1.DATA).to.equal("data");
});
it("Provides a DATA_BASE64 attribute", () => {
expect(STRUCTURED_ATTRS_1.DATA_BASE64).to.equal("data_base64");
});
});
});

150
test/constants_test.ts Normal file
View File

@ -0,0 +1,150 @@
import { expect } from "chai";
import CONSTANTS from "../src/constants";
describe("Constants exposed by top level exports", () => {
it("Exports an ENCODING_BASE64 constant", () => {
expect(CONSTANTS.ENCODING_BASE64).to.equal("base64");
});
it("Exports a DATA_ATTRIBUTE constant", () => {
expect(CONSTANTS.DATA_ATTRIBUTE).to.equal("data");
});
it("Exports a MIME_JSON constant", () => {
expect(CONSTANTS.MIME_JSON).to.equal("application/json");
});
it("Exports a MIME_OCTET_STREAM constant", () => {
expect(CONSTANTS.MIME_OCTET_STREAM).to.equal("application/octet-stream");
});
it("Exports a MIME_CE constant", () => {
expect(CONSTANTS.MIME_CE).to.equal("application/cloudevents");
});
it("Exports a MIME_CE_JSON constant", () => {
expect(CONSTANTS.MIME_CE_JSON).to.equal("application/cloudevents+json");
});
it("Exports a HEADER_CONTENT_TYPE constant", () => {
expect(CONSTANTS.HEADER_CONTENT_TYPE).to.equal("content-type");
});
it("Exports a DEFAULT_CONTENT_TYPE constant", () => {
expect(CONSTANTS.DEFAULT_CONTENT_TYPE).to.equal(`${CONSTANTS.MIME_JSON}; charset=${CONSTANTS.CHARSET_DEFAULT}`);
});
it("Exports a DEFAULT_CE_CONTENT_TYPE constant", () => {
expect(CONSTANTS.DEFAULT_CE_CONTENT_TYPE).to.equal(
`${CONSTANTS.MIME_CE_JSON}; charset=${CONSTANTS.CHARSET_DEFAULT}`,
);
});
describe("V0.3 binary headers constants", () => {
it("Provides a TYPE header", () => {
expect(CONSTANTS.CE_HEADERS.TYPE).to.equal("ce-type");
});
it("Provides a SPEC_VERSION header", () => {
expect(CONSTANTS.CE_HEADERS.SPEC_VERSION).to.equal("ce-specversion");
});
it("Provides a SOURCE header", () => {
expect(CONSTANTS.CE_HEADERS.SOURCE).to.equal("ce-source");
});
it("Provides an ID header", () => {
expect(CONSTANTS.CE_HEADERS.ID).to.equal("ce-id");
});
it("Provides a TIME header", () => {
expect(CONSTANTS.CE_HEADERS.TIME).to.equal("ce-time");
});
it("Provides a SCHEMA_URL header", () => {
expect(CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL).to.equal("ce-schemaurl");
});
it("Provides a CONTENT_ENCODING header", () => {
expect(CONSTANTS.BINARY_HEADERS_03.CONTENT_ENCODING).to.equal("ce-datacontentencoding");
});
it("Provides a SUBJECT header", () => {
expect(CONSTANTS.CE_HEADERS.SUBJECT).to.equal("ce-subject");
});
it("Provides an EXTENSIONS_PREFIX constant", () => {
expect(CONSTANTS.EXTENSIONS_PREFIX).to.equal("ce-");
});
});
describe("V0.3 structured attributes constants", () => {
it("Provides a TYPE attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.TYPE).to.equal("type");
});
it("Provides a SPEC_VERSION attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION).to.equal("specversion");
});
it("Provides a SOURCE attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.SOURCE).to.equal("source");
});
it("Provides an ID attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.ID).to.equal("id");
});
it("Provides a TIME attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.TIME).to.equal("time");
});
it("Provides a SCHEMA_URL attribute", () => {
expect(CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL).to.equal("schemaurl");
});
it("Provides a CONTENT_ENCODING attribute", () => {
expect(CONSTANTS.STRUCTURED_ATTRS_03.CONTENT_ENCODING).to.equal("datacontentencoding");
});
it("Provides a SUBJECT attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.SUBJECT).to.equal("subject");
});
it("Provides a DATA attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.DATA).to.equal("data");
});
});
describe("V01 binary headers constants", () => {
it("Provides a TYPE header", () => {
expect(CONSTANTS.CE_HEADERS.TYPE).to.equal("ce-type");
});
it("Provides a SPEC_VERSION header", () => {
expect(CONSTANTS.CE_HEADERS.SPEC_VERSION).to.equal("ce-specversion");
});
it("Provides a SOURCE header", () => {
expect(CONSTANTS.CE_HEADERS.SOURCE).to.equal("ce-source");
});
it("Provides an ID header", () => {
expect(CONSTANTS.CE_HEADERS.ID).to.equal("ce-id");
});
it("Provides a TIME header", () => {
expect(CONSTANTS.CE_HEADERS.TIME).to.equal("ce-time");
});
it("Provides a DATA_SCHEMA header", () => {
expect(CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA).to.equal("ce-dataschema");
});
it("Provides a SUBJECT header", () => {
expect(CONSTANTS.CE_HEADERS.SUBJECT).to.equal("ce-subject");
});
it("Provides an EXTENSIONS_PREFIX constant", () => {
expect(CONSTANTS.EXTENSIONS_PREFIX).to.equal("ce-");
});
});
describe("V1 structured attributes constants", () => {
it("Provides a TYPE attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.TYPE).to.equal("type");
});
it("Provides a SPEC_VERSION attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION).to.equal("specversion");
});
it("Provides a SOURCE attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.SOURCE).to.equal("source");
});
it("Provides an ID attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.ID).to.equal("id");
});
it("Provides a TIME attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.TIME).to.equal("time");
});
it("Provides a DATA_SCHEMA attribute", () => {
expect(CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA).to.equal("dataschema");
});
it("Provides a CONTENT_TYPE attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE).to.equal("datacontenttype");
});
it("Provides a SUBJECT attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.SUBJECT).to.equal("subject");
});
it("Provides a DATA attribute", () => {
expect(CONSTANTS.CE_ATTRIBUTES.DATA).to.equal("data");
});
it("Provides a DATA_BASE64 attribute", () => {
expect(CONSTANTS.STRUCTURED_ATTRS_1.DATA_BASE64).to.equal("data_base64");
});
});
});

206
test/http_binding_03.ts Normal file
View File

@ -0,0 +1,206 @@
import "mocha";
import { expect } from "chai";
import nock from "nock";
import { emitBinary, emitStructured } from "../src/transport/http";
import { CloudEvent, Version } from "../src";
import { AxiosResponse } from "axios";
const type = "com.github.pull.create";
const source = "urn:event:from:myapi/resourse/123";
const contentEncoding = "base64";
const contentType = "application/cloudevents+json; charset=utf-8";
const time = new Date();
const schemaurl = "http://cloudevents.io/schema.json";
const ceContentType = "application/json";
const data = {
foo: "bar",
};
const dataBase64 = "Y2xvdWRldmVudHMK";
const ext1Name = "extension1";
const ext1Value = "foobar";
const ext2Name = "extension2";
const ext2Value = "acme";
const cloudevent = new CloudEvent({
specversion: Version.V03,
type,
source,
datacontenttype: ceContentType,
subject: "subject.ext",
time,
schemaurl,
data,
// set these so that deepEqual works
dataschema: "",
datacontentencoding: "",
data_base64: "",
});
cloudevent[ext1Name] = ext1Value;
cloudevent[ext2Name] = ext2Value;
const cebase64 = new CloudEvent({
specversion: Version.V03,
type,
source,
datacontenttype: ceContentType,
datacontentencoding: contentEncoding,
time,
schemaurl,
data: dataBase64,
});
cebase64[ext1Name] = ext1Value;
cebase64[ext2Name] = ext2Value;
const webhook = "https://cloudevents.io/webhook";
const httpcfg = {
method: "POST",
url: `${webhook}/json`,
};
describe("HTTP Transport Binding - Version 0.3", () => {
beforeEach(() => {
// Mocking the webhook
nock(webhook).post("/json").reply(201, { status: "accepted" });
});
describe("Structured", () => {
describe("JSON Format", () => {
it(`requires '${contentType}' Content-Type in the header`, () =>
emitStructured(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers["Content-Type"]).to.equal(contentType);
}));
it("the request payload should be correct", () =>
emitStructured(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.data).to.deep.equal(JSON.stringify(cloudevent));
}));
describe("'data' attribute with 'base64' encoding", () => {
it("the request payload should be correct", () =>
emitStructured(cebase64, httpcfg).then((response: AxiosResponse) => {
expect(JSON.parse(response.config.data).data).to.equal(cebase64.data);
}));
});
});
});
describe("Binary", () => {
describe("JSON Format", () => {
it(`requires ${cloudevent.datacontenttype} in the header`, () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers["Content-Type"]).to.equal(cloudevent.datacontenttype);
}));
it("the request payload should be correct", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(JSON.parse(response.config.data)).to.deep.equal(cloudevent.data);
}));
it("HTTP Header contains 'ce-type'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-type");
}));
it("HTTP Header contains 'ce-specversion'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-specversion");
}));
it("HTTP Header contains 'ce-source'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-source");
}));
it("HTTP Header contains 'ce-id'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-id");
}));
it("HTTP Header contains 'ce-time'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-time");
}));
it("HTTP Header contains 'ce-schemaurl'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-schemaurl");
}));
it(`HTTP Header contains 'ce-${ext1Name}'`, () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property(`ce-${ext1Name}`);
}));
it(`HTTP Header contains 'ce-${ext2Name}'`, () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property(`ce-${ext2Name}`);
}));
it("HTTP Header contains 'ce-subject'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-subject");
}));
it("should 'ce-type' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.type).to.equal(response.config.headers["ce-type"]);
}));
it("should 'ce-specversion' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.specversion).to.equal(response.config.headers["ce-specversion"]);
}));
it("should 'ce-source' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.source).to.equal(response.config.headers["ce-source"]);
}));
it("should 'ce-id' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.id).to.equal(response.config.headers["ce-id"]);
}));
it("should 'ce-time' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.time).to.equal(response.config.headers["ce-time"]);
}));
it("should 'ce-schemaurl' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.schemaurl).to.equal(response.config.headers["ce-schemaurl"]);
}));
it(`should 'ce-${ext1Name}' have the right value`, () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent[ext1Name]).to.equal(response.config.headers[`ce-${ext1Name}`]);
}));
it(`should 'ce-${ext2Name}' have the right value`, () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent[ext2Name]).to.equal(response.config.headers[`ce-${ext2Name}`]);
}));
it("should 'ce-subject' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.subject).to.equal(response.config.headers["ce-subject"]);
}));
describe("'data' attribute with 'base64' encoding", () => {
it("HTTP Header contains 'ce-datacontentencoding'", () =>
emitBinary(cebase64, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-datacontentencoding");
}));
it("should 'ce-datacontentencoding' have the right value", () =>
emitBinary(cebase64, httpcfg).then((response: AxiosResponse) => {
expect(cebase64.datacontentencoding).to.equal(response.config.headers["ce-datacontentencoding"]);
}));
});
});
});
});

View File

@ -1,245 +0,0 @@
const expect = require("chai").expect;
const nock = require("nock");
const BinaryHTTPEmitter = require("../lib/bindings/http/emitter_binary.js");
const StructuredHTTPEmitter = require("../lib/bindings/http/emitter_structured.js");
const { CloudEvent } = require("../");
const {
SPEC_V03
} = require("../lib/bindings/http/constants.js");
const type = "com.github.pull.create";
const source = "urn:event:from:myapi/resourse/123";
const contentEncoding = "base64";
const contentType = "application/cloudevents+json; charset=utf-8";
const time = new Date();
const schemaURL = "http://cloudevents.io/schema.json";
const ceContentType = "application/json";
const data = {
foo: "bar"
};
const dataBase64 = "Y2xvdWRldmVudHMK";
const ext1Name = "extension1";
const ext1Value = "foobar";
const ext2Name = "extension2";
const ext2Value = "acme";
const cloudevent = new CloudEvent({
specversion: SPEC_V03,
type,
source,
dataContentType: ceContentType,
subject: "subject.ext",
time,
schemaURL,
data
});
cloudevent.addExtension(ext1Name, ext1Value);
cloudevent.addExtension(ext2Name, ext2Value);
const cebase64 = new CloudEvent({
specversion: SPEC_V03,
type,
source,
dataContentType: ceContentType,
dataContentEncoding: contentEncoding,
time,
schemaURL,
data: dataBase64
});
cebase64.addExtension(ext1Name, ext1Value);
cebase64.addExtension(ext2Name, ext2Value);
const webhook = "https://cloudevents.io/webhook";
const httpcfg = {
method: "POST",
url: `${webhook}/json`
};
const binary = new BinaryHTTPEmitter(SPEC_V03);
const structured = new StructuredHTTPEmitter();
describe("HTTP Transport Binding - Version 0.3", () => {
beforeEach(() => {
// Mocking the webhook
nock(webhook)
.post("/json")
.reply(201, { status: "accepted" });
});
describe("Structured", () => {
describe("JSON Format", () => {
it(`requires '${contentType}' Content-Type in the header`,
() => structured.emit(httpcfg, cloudevent)
.then((response) => {
expect(response.config.headers["Content-Type"])
.to.equal(contentType);
}));
it("the request payload should be correct",
() => structured.emit(httpcfg, cloudevent)
.then((response) => {
expect(JSON.parse(response.config.data))
.to.deep.equal(cloudevent.format());
}));
describe("'data' attribute with 'base64' encoding", () => {
it("the request payload should be correct",
() => structured.emit(httpcfg, cebase64)
.then((response) => {
expect(JSON.parse(response.config.data).data)
.to.equal(cebase64.format().data);
}));
});
});
});
describe("Binary", () => {
describe("JSON Format", () => {
it(`requires ${cloudevent.dataContentType} in the header`,
() => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(response.config.headers["Content-Type"])
.to.equal(cloudevent.dataContentType);
}));
it("the request payload should be correct", () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(JSON.parse(response.config.data))
.to.deep.equal(cloudevent.data);
}));
it("HTTP Header contains 'ce-type'", () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(response.config.headers)
.to.have.property("ce-type");
}));
it("HTTP Header contains 'ce-specversion'", () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(response.config.headers)
.to.have.property("ce-specversion");
}));
it("HTTP Header contains 'ce-source'", () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(response.config.headers)
.to.have.property("ce-source");
}));
it("HTTP Header contains 'ce-id'", () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(response.config.headers)
.to.have.property("ce-id");
}));
it("HTTP Header contains 'ce-time'", () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(response.config.headers)
.to.have.property("ce-time");
}));
it("HTTP Header contains 'ce-schemaurl'", () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(response.config.headers)
.to.have.property("ce-schemaurl");
}));
it(`HTTP Header contains 'ce-${ext1Name}'`, () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(response.config.headers)
.to.have.property(`ce-${ext1Name}`);
}));
it(`HTTP Header contains 'ce-${ext2Name}'`, () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(response.config.headers)
.to.have.property(`ce-${ext2Name}`);
}));
it("HTTP Header contains 'ce-subject'", () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(response.config.headers)
.to.have.property("ce-subject");
}));
it("should 'ce-type' have the right value", () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(cloudevent.type)
.to.equal(response.config.headers["ce-type"]);
}));
it("should 'ce-specversion' have the right value",
() => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(cloudevent.specversion)
.to.equal(response.config.headers["ce-specversion"]);
}));
it("should 'ce-source' have the right value",
() => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(cloudevent.source)
.to.equal(response.config.headers["ce-source"]);
}));
it("should 'ce-id' have the right value", () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(cloudevent.id)
.to.equal(response.config.headers["ce-id"]);
}));
it("should 'ce-time' have the right value", () => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(cloudevent.time)
.to.equal(response.config.headers["ce-time"]);
}));
it("should 'ce-schemaurl' have the right value",
() => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(cloudevent.schemaURL)
.to.equal(response.config.headers["ce-schemaurl"]);
}));
it(`should 'ce-${ext1Name}' have the right value`,
() => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(cloudevent.getExtensions()[ext1Name])
.to.equal(response.config.headers[`ce-${ext1Name}`]);
}));
it(`should 'ce-${ext2Name}' have the right value`,
() => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(cloudevent.getExtensions()[ext2Name])
.to.equal(response.config.headers[`ce-${ext2Name}`]);
}));
it("should 'ce-subject' have the right value",
() => binary.emit(httpcfg, cloudevent)
.then((response) => {
expect(cloudevent.subject)
.to.equal(response.config.headers["ce-subject"]);
}));
describe("'data' attribute with 'base64' encoding", () => {
it("HTTP Header contains 'ce-datacontentencoding'",
() => binary.emit(httpcfg, cebase64)
.then((response) => {
expect(response.config.headers)
.to.have.property("ce-datacontentencoding");
}));
it("should 'ce-datacontentencoding' have the right value",
() => binary.emit(httpcfg, cebase64)
.then((response) => {
expect(cebase64.dataContentEncoding)
.to.equal(response.config.headers["ce-datacontentencoding"]);
}));
});
});
});
});

View File

@ -1,293 +0,0 @@
const expect = require("chai").expect;
const nock = require("nock");
const https = require("https");
const { asBase64 } = require("../lib/bindings/http/validation/fun.js");
const { SPEC_V1 } = require("../lib/bindings/http/constants.js");
const { CloudEvent } = require("../");
const BinaryHTTPEmitter = require("../lib/bindings/http/emitter_binary.js");
const StructuredHTTPEmitter = require("../lib/bindings/http/emitter_structured.js");
const type = "com.github.pull.create";
const source = "urn:event:from:myapi/resource/123";
const contentType = "application/cloudevents+json; charset=utf-8";
const time = new Date();
const subject = "subject.ext";
const dataSchema = "http://cloudevents.io/schema.json";
const dataContentType = "application/json";
const data = {
foo: "bar"
};
const ext1Name = "extension1";
const ext1Value = "foobar";
const ext2Name = "extension2";
const ext2Value = "acme";
const cloudevent = new CloudEvent({
specversion: SPEC_V1,
type,
source,
dataContentType,
subject,
time,
dataSchema,
data
});
cloudevent.addExtension(ext1Name, ext1Value);
cloudevent.addExtension(ext2Name, ext2Value);
const dataString = ")(*~^my data for ce#@#$%";
const webhook = "https://cloudevents.io/webhook/v1";
const httpcfg = {
method: "POST",
url: `${webhook}/json`
};
const binary = new BinaryHTTPEmitter(SPEC_V1);
const structured = new StructuredHTTPEmitter();
describe("HTTP Transport Binding - Version 1.0", () => {
beforeEach(() => {
// Mocking the webhook
nock(webhook)
.post("/json")
.reply(201, { status: "accepted" });
});
describe("Structured", () => {
it("works with mTLS authentication", () => {
const event = new StructuredHTTPEmitter({
method: "POST",
url: `${webhook}/json`,
httpsAgent: new https.Agent({
cert: "some value",
key: "other value"
})
});
return event.emit(httpcfg, cloudevent).then((response) => {
expect(response.config.headers["Content-Type"])
.to.equal(contentType);
});
});
describe("JSON Format", () => {
it(`requires '${contentType}' Content-Type in the header`,
() => structured.emit(httpcfg, cloudevent)
.then((response) => {
expect(response.config.headers["Content-Type"])
.to.equal(contentType);
}));
it("the request payload should be correct",
() => structured.emit(httpcfg, cloudevent)
.then((response) => {
expect(JSON.parse(response.config.data))
.to.deep.equal(cloudevent.format());
}));
describe("Binary event data", () => {
it("the request payload should be correct when data is binary", () => {
const bindata = Uint32Array.from(dataString, (c) => c.codePointAt(0));
const expected = asBase64(bindata);
const binevent = new CloudEvent({
type,
source,
dataContentType: "text/plain",
data: bindata,
});
binevent.addExtension(ext1Name, ext1Value);
binevent.addExtension(ext2Name, ext2Value);
return structured.emit(httpcfg, binevent)
.then((response) => {
expect(JSON.parse(response.config.data).data_base64)
.to.equal(expected);
});
});
it("the payload must have 'data_base64' when data is binary", () => {
const binevent = new CloudEvent({
type,
source,
dataContentType: "text/plain",
data: Uint32Array.from(dataString, (c) => c.codePointAt(0)),
});
binevent.addExtension(ext1Name, ext1Value);
binevent.addExtension(ext2Name, ext2Value);
return structured.emit(httpcfg, binevent)
.then((response) => {
expect(JSON.parse(response.config.data))
.to.have.property("data_base64");
});
});
});
});
});
// describe("Binary", () => {
// it("works with mTLS authentication", () =>
// binary.emit({
// method: "POST",
// url: `${webhook}/json`,
// httpsAgent: new https.Agent({
// cert: "some value",
// key: "other value"
// })
// }, cloudevent).then((response) => {
// expect(response.config.headers["Content-Type"])
// .to.equal(cloudevent.dataContentType);
// })
// );
// describe("JSON Format", () => {
// it(`requires '${cloudevent.dataContentType}' in the header`,
// () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(response.config.headers["Content-Type"])
// .to.equal(cloudevent.dataContentType);
// }));
// it("the request payload should be correct", () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(JSON.parse(response.config.data))
// .to.deep.equal(cloudevent.data);
// }));
// it("the request payload should be correct when event data is binary", () => {
// const bindata = Uint32Array.from(dataString, (c) => c.codePointAt(0));
// const expected = asBase64(bindata);
// const binevent = new CloudEvent({
// type,
// source,
// dataContentType: "text/plain",
// data: bindata,
// });
// binevent.addExtension(ext1Name, ext1Value);
// binevent.addExtension(ext2Name, ext2Value);
// return binary.emit(httpcfg, binevent)
// .then((response) => {
// expect(response.config.data)
// .to.equal(expected);
// });
// });
// it("HTTP Header contains 'ce-type'", () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(response.config.headers)
// .to.have.property("ce-type");
// }));
// it("HTTP Header contains 'ce-specversion'", () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(response.config.headers)
// .to.have.property("ce-specversion");
// }));
// it("HTTP Header contains 'ce-source'", () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(response.config.headers)
// .to.have.property("ce-source");
// }));
// it("HTTP Header contains 'ce-id'", () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(response.config.headers)
// .to.have.property("ce-id");
// }));
// it("HTTP Header contains 'ce-time'", () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(response.config.headers)
// .to.have.property("ce-time");
// }));
// it("HTTP Header contains 'ce-dataschema'", () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(response.config.headers)
// .to.have.property("ce-dataschema");
// }));
// it(`HTTP Header contains 'ce-${ext1Name}'`, () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(response.config.headers)
// .to.have.property(`ce-${ext1Name}`);
// }));
// it(`HTTP Header contains 'ce-${ext2Name}'`, () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(response.config.headers)
// .to.have.property(`ce-${ext2Name}`);
// }));
// it("HTTP Header contains 'ce-subject'", () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(response.config.headers)
// .to.have.property("ce-subject");
// }));
// it("should 'ce-type' have the right value", () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(cloudevent.type)
// .to.equal(response.config.headers["ce-type"]);
// }));
// it("should 'ce-specversion' have the right value",
// () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(cloudevent.specversion)
// .to.equal(response.config.headers["ce-specversion"]);
// }));
// it("should 'ce-source' have the right value",
// () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(cloudevent.source)
// .to.equal(response.config.headers["ce-source"]);
// }));
// it("should 'ce-id' have the right value", () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(cloudevent.id)
// .to.equal(response.config.headers["ce-id"]);
// }));
// it("should 'ce-time' have the right value", () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(cloudevent.time)
// .to.equal(response.config.headers["ce-time"]);
// }));
// it("should 'ce-dataschema' have the right value",
// () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(cloudevent.dataSchema)
// .to.equal(response.config.headers["ce-dataschema"]);
// }));
// it(`should 'ce-${ext1Name}' have the right value`,
// () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(cloudevent.getExtensions()[ext1Name])
// .to.equal(response.config.headers[`ce-${ext1Name}`]);
// }));
// it(`should 'ce-${ext2Name}' have the right value`,
// () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(cloudevent.getExtensions()[ext2Name])
// .to.equal(response.config.headers[`ce-${ext2Name}`]);
// }));
// it("should 'ce-subject' have the right value",
// () => binary.emit(httpcfg, cloudevent)
// .then((response) => {
// expect(cloudevent.subject)
// .to.equal(response.config.headers["ce-subject"]);
// }));
// });
// });
});

240
test/http_binding_1.ts Normal file
View File

@ -0,0 +1,240 @@
import * as https from "https";
import "mocha";
import { expect } from "chai";
import nock from "nock";
import { CloudEvent, Version } from "../src";
import { emitBinary, emitStructured } from "../src/transport/http";
import { asBase64 } from "../src/event/validation/is";
import { AxiosResponse } from "axios";
const type = "com.github.pull.create";
const source = "urn:event:from:myapi/resource/123";
const contentType = "application/cloudevents+json; charset=utf-8";
const time = new Date();
const subject = "subject.ext";
const dataschema = "http://cloudevents.io/schema.json";
const datacontenttype = "application/json";
const data = {
foo: "bar",
};
const ext1Name = "extension1";
const ext1Value = "foobar";
const ext2Name = "extension2";
const ext2Value = "acme";
const cloudevent = new CloudEvent({
specversion: Version.V1,
type,
source,
datacontenttype,
subject,
time,
dataschema,
data,
});
cloudevent[ext1Name] = ext1Value;
cloudevent[ext2Name] = ext2Value;
const dataString = ")(*~^my data for ce#@#$%";
const webhook = "https://cloudevents.io/webhook/v1";
const httpcfg = { url: `${webhook}/json` };
describe("HTTP Transport Binding - Version 1.0", () => {
beforeEach(() => {
// Mocking the webhook
nock(webhook).post("/json").reply(201, { status: "accepted" });
});
describe("Structured", () => {
it("works with mTLS authentication", () => {
const httpsAgent = new https.Agent({
cert: "some value",
key: "other value",
});
return emitStructured(cloudevent, { ...httpcfg, httpsAgent }).then((response: AxiosResponse) => {
expect(response.config.headers["Content-Type"]).to.equal(contentType);
});
});
describe("JSON Format", () => {
it(`requires '${contentType}' Content-Type in the header`, () =>
emitStructured(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers["Content-Type"]).to.equal(contentType);
}));
it("the request payload should be correct", () =>
emitStructured(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.data).to.deep.equal(JSON.stringify(cloudevent));
}));
describe("Binary event data", () => {
it("the request payload should be correct when data is binary", () => {
const bindata = Uint32Array.from(dataString as string, (c) => c.codePointAt(0) as number);
const expected = asBase64(bindata);
const binevent = new CloudEvent({
type,
source,
datacontenttype: "text/plain",
data: bindata,
});
binevent[ext1Name] = ext1Value;
binevent[ext2Name] = ext2Value;
return emitStructured(binevent, httpcfg).then((response: AxiosResponse) => {
expect(JSON.parse(response.config.data).data_base64).to.equal(expected);
});
});
it("the payload must have 'data_base64' when data is binary", () => {
const binevent = new CloudEvent({
type,
source,
datacontenttype: "text/plain",
data: Uint32Array.from(dataString as string, (c) => c.codePointAt(0) as number),
});
binevent[ext1Name] = ext1Value;
binevent[ext2Name] = ext2Value;
return emitStructured(binevent, httpcfg).then((response: AxiosResponse) => {
expect(JSON.parse(response.config.data)).to.have.property("data_base64");
});
});
});
});
});
describe("Binary", () => {
it("works with mTLS authentication", () =>
emitBinary(cloudevent, {
url: `${webhook}/json`,
httpsAgent: new https.Agent({
cert: "some value",
key: "other value",
}),
}).then((response: AxiosResponse) => {
expect(response.config.headers["Content-Type"]).to.equal(cloudevent.datacontenttype);
}));
describe("JSON Format", () => {
it(`requires '${cloudevent.datacontenttype}' in the header`, () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers["Content-Type"]).to.equal(cloudevent.datacontenttype);
}));
it("the request payload should be correct", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(JSON.parse(response.config.data)).to.deep.equal(cloudevent.data);
}));
it("the request payload should be correct when event data is binary", () => {
const bindata = Uint32Array.from(dataString as string, (c) => c.codePointAt(0) as number);
const expected = asBase64(bindata);
const binevent = new CloudEvent({
type,
source,
datacontenttype: "text/plain",
data: bindata,
});
return emitBinary(binevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.data).to.equal(expected);
});
});
it("HTTP Header contains 'ce-type'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-type");
}));
it("HTTP Header contains 'ce-specversion'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-specversion");
}));
it("HTTP Header contains 'ce-source'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-source");
}));
it("HTTP Header contains 'ce-id'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-id");
}));
it("HTTP Header contains 'ce-time'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-time");
}));
it("HTTP Header contains 'ce-dataschema'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-dataschema");
}));
it(`HTTP Header contains 'ce-${ext1Name}'`, () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property(`ce-${ext1Name}`);
}));
it(`HTTP Header contains 'ce-${ext2Name}'`, () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property(`ce-${ext2Name}`);
}));
it("HTTP Header contains 'ce-subject'", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(response.config.headers).to.have.property("ce-subject");
}));
it("should 'ce-type' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.type).to.equal(response.config.headers["ce-type"]);
}));
it("should 'ce-specversion' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.specversion).to.equal(response.config.headers["ce-specversion"]);
}));
it("should 'ce-source' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.source).to.equal(response.config.headers["ce-source"]);
}));
it("should 'ce-id' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.id).to.equal(response.config.headers["ce-id"]);
}));
it("should 'ce-time' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.time).to.equal(response.config.headers["ce-time"]);
}));
it("should 'ce-dataschema' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.dataschema).to.equal(response.config.headers["ce-dataschema"]);
}));
it(`should 'ce-${ext1Name}' have the right value`, () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent[ext1Name]).to.equal(response.config.headers[`ce-${ext1Name}`]);
}));
it(`should 'ce-${ext2Name}' have the right value`, () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent[ext2Name]).to.equal(response.config.headers[`ce-${ext2Name}`]);
}));
it("should 'ce-subject' have the right value", () =>
emitBinary(cloudevent, httpcfg).then((response: AxiosResponse) => {
expect(cloudevent.subject).to.equal(response.config.headers["ce-subject"]);
}));
});
});
});

225
test/http_emitter_test.ts Normal file
View File

@ -0,0 +1,225 @@
import "mocha";
import { expect } from "chai";
import nock from "nock";
import CONSTANTS from "../src/constants";
const DEFAULT_CE_CONTENT_TYPE = CONSTANTS.DEFAULT_CE_CONTENT_TYPE;
import { CloudEvent, Version, Emitter, Protocol, headersFor } from "../src";
import { AxiosResponse } from "axios";
const receiver = "https://cloudevents.io/";
const type = "com.example.test";
const source = "urn:event:from:myapi/resource/123";
const ext1Name = "lunch";
const ext1Value = "tacos";
const ext2Name = "supper";
const ext2Value = "sushi";
const data = {
lunchBreak: "noon",
};
describe("HTTP Transport Binding Emitter for CloudEvents", () => {
beforeEach(() => {
nock(receiver)
.post("/")
.reply(function (uri: string, body: nock.Body) {
// return the request body and the headers so they can be
// examined in the test
if (typeof body === "string") {
body = JSON.parse(body);
}
const returnBody = { ...(body as Record<string, unknown>), ...this.req.headers };
return [201, returnBody];
});
});
describe("V1", () => {
const emitter = new Emitter({ url: receiver });
const event = new CloudEvent({
type,
source,
time: new Date(),
data,
[ext1Name]: ext1Value,
[ext2Name]: ext2Value,
});
it("Sends a binary 1.0 CloudEvent by default", () => {
emitter
.send(event)
.then((response: AxiosResponse) => {
// A binary message will have a ce-id header
expect(response.data[CONSTANTS.CE_HEADERS.ID]).to.equal(event.id);
expect(response.data[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V1);
// A binary message will have a request body for the data
expect(response.data.lunchBreak).to.equal(data.lunchBreak);
// Ensure extensions are handled properly
expect(response.data[`${CONSTANTS.EXTENSIONS_PREFIX}${ext1Name}`]).to.equal(ext1Value);
expect(response.data[`${CONSTANTS.EXTENSIONS_PREFIX}${ext2Name}`]).to.equal(ext2Value);
})
.catch(expect.fail);
});
it("Provides the HTTP headers for a binary event", () => {
const headers = headersFor(event);
expect(headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(event.type);
expect(headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(event.specversion);
expect(headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(event.source);
expect(headers[CONSTANTS.CE_HEADERS.ID]).to.equal(event.id);
expect(headers[CONSTANTS.CE_HEADERS.TIME]).to.equal(event.time);
});
it("Sends a binary CloudEvent with Custom Headers", () => {
emitter
.send(event, { headers: { customheader: "value" } })
.then((response: { data: { [k: string]: string } }) => {
// A binary message will have a ce-id header
expect(response.data.customheader).to.equal("value");
expect(response.data[CONSTANTS.CE_HEADERS.ID]).to.equal(event.id);
expect(response.data[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V1);
// A binary message will have a request body for the data
expect(response.data.lunchBreak).to.equal(data.lunchBreak);
})
.catch(expect.fail);
});
it("Sends a structured 1.0 CloudEvent if specified", () => {
emitter
.send(event, { protocol: Protocol.HTTPStructured })
.then((response: { data: { [k: string]: string | Record<string, string>; data: { lunchBreak: string } } }) => {
// A structured message will have a cloud event content type
expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE);
// Ensure other CE headers don't exist - just testing for ID
expect(response.data[CONSTANTS.CE_HEADERS.ID]).to.equal(undefined);
// The spec version would have been specified in the body
expect(response.data.specversion).to.equal(Version.V1);
expect(response.data.data.lunchBreak).to.equal(data.lunchBreak);
// Ensure extensions are handled properly
expect(response.data[ext1Name]).to.equal(ext1Value);
expect(response.data[ext2Name]).to.equal(ext2Value);
})
.catch(expect.fail);
});
it("Sends to an alternate URL if specified", () => {
nock(receiver)
.post("/alternate")
.reply(function (uri, requestBody: nock.Body) {
// return the request body and the headers so they can be
// examined in the test
if (typeof requestBody === "string") {
requestBody = JSON.parse(requestBody);
}
const returnBody = { ...(requestBody as Record<string, unknown>), ...this.req.headers };
return [201, returnBody];
});
emitter
.send(event, { protocol: Protocol.HTTPStructured, url: `${receiver}alternate` })
.then((response: AxiosResponse) => {
// A structured message will have a cloud event content type
expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE);
// Ensure other CE headers don't exist - just testing for ID
expect(response.data[CONSTANTS.CE_HEADERS.ID]).to.equal(undefined);
// The spec version would have been specified in the body
expect(response.data.specversion).to.equal(Version.V1);
expect(response.data.data.lunchBreak).to.equal(data.lunchBreak);
})
.catch(expect.fail);
});
});
describe("V03", () => {
const emitter = new Emitter({ url: receiver });
const event = new CloudEvent({
specversion: Version.V03,
type,
source,
time: new Date(),
data,
[ext1Name]: ext1Value,
[ext2Name]: ext2Value,
});
it("Sends a binary 0.3 CloudEvent", () => {
emitter
.send(event)
.then((response: AxiosResponse) => {
// A binary message will have a ce-id header
expect(response.data[CONSTANTS.CE_HEADERS.ID]).to.equal(event.id);
expect(response.data[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V03);
// A binary message will have a request body for the data
expect(response.data.lunchBreak).to.equal(data.lunchBreak);
// Ensure extensions are handled properly
expect(response.data[`${CONSTANTS.EXTENSIONS_PREFIX}${ext1Name}`]).to.equal(ext1Value);
expect(response.data[`${CONSTANTS.EXTENSIONS_PREFIX}${ext2Name}`]).to.equal(ext2Value);
})
.catch(expect.fail);
});
it("Provides the HTTP headers for a binary event", () => {
const headers = headersFor(event);
expect(headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(event.type);
expect(headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(event.specversion);
expect(headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(event.source);
expect(headers[CONSTANTS.CE_HEADERS.ID]).to.equal(event.id);
expect(headers[CONSTANTS.CE_HEADERS.TIME]).to.equal(event.time);
});
it("Sends a structured 0.3 CloudEvent if specified", () => {
emitter
.send(event, { protocol: Protocol.HTTPStructured })
.then(
(response: {
data: { [k: string]: string | Record<string, string>; specversion: string; data: { lunchBreak: string } };
}) => {
// A structured message will have a cloud event content type
expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE);
// Ensure other CE headers don't exist - just testing for ID
expect(response.data[CONSTANTS.CE_HEADERS.ID]).to.equal(undefined);
// The spec version would have been specified in the body
expect(response.data.specversion).to.equal(Version.V03);
expect(response.data.data.lunchBreak).to.equal(data.lunchBreak);
// Ensure extensions are handled properly
expect(response.data[ext1Name]).to.equal(ext1Value);
expect(response.data[ext2Name]).to.equal(ext2Value);
},
)
.catch(expect.fail);
});
it("Sends to an alternate URL if specified", () => {
nock(receiver)
.post("/alternate")
.reply(function (uri, requestBody: nock.Body) {
// return the request body and the headers so they can be
// examined in the test
if (typeof requestBody === "string") {
requestBody = JSON.parse(requestBody);
}
const returnBody = { ...(requestBody as Record<string, unknown>), ...this.req.headers };
return [201, returnBody];
});
emitter
.send(event, { protocol: Protocol.HTTPStructured, url: `${receiver}alternate` })
.then(
(response: {
data: { specversion: string; data: { lunchBreak: string }; [k: string]: string | Record<string, string> };
}) => {
// A structured message will have a cloud event content type
expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE);
// Ensure other CE headers don't exist - just testing for ID
expect(response.data[CONSTANTS.CE_HEADERS.ID]).to.equal(undefined);
// The spec version would have been specified in the body
expect(response.data.specversion).to.equal(Version.V03);
expect(response.data.data.lunchBreak).to.equal(data.lunchBreak);
},
)
.catch(expect.fail);
});
});
});

View File

@ -1,24 +1,14 @@
import "mocha";
import { expect } from "chai";
import { CloudEvent, HTTPReceiver } from "..";
import { CloudEventV1 } from "../lib/v1";
const {
HEADER_CONTENT_TYPE,
DEFAULT_CONTENT_TYPE,
MIME_CE_JSON,
DEFAULT_SPEC_VERSION_HEADER,
BINARY_HEADERS_03,
BINARY_HEADERS_1
} = require("../lib/bindings/http/constants");
const ValidationError = require("../lib/bindings/http/validation/validation_error.js");
import { CloudEvent, Receiver, ValidationError } from "../src";
import { CloudEventV1 } from "../src/event/v1";
const receiver = new HTTPReceiver();
const receiver = new Receiver();
const id = "1234";
const type = "org.cncf.cloudevents.test";
const source = "urn:event:from:myapi/resourse/123";
const data = {
lunch: "sushi"
};
const structuredHeaders = { "content-type": "application/cloudevents+json" };
const data = { lunch: "sushi" };
describe("HTTP Transport Binding Receiver for CloudEvents", () => {
describe("HTTP CloudEvent format detection", () => {
@ -29,11 +19,38 @@ describe("HTTP Transport Binding Receiver for CloudEvents", () => {
type,
source,
data,
specversion
specversion,
};
expect(receiver.accept.bind(receiver, {}, payload))
.to.throw(ValidationError, "no cloud event detected");
expect(receiver.accept.bind(receiver, {}, payload)).to.throw(ValidationError, "no cloud event detected");
});
it("Converts the JSON body of a binary event to an Object", () => {
const binaryHeaders = {
"content-type": "application/json; charset=utf-8",
"ce-specversion": specversion,
"ce-id": id,
"ce-type": type,
"ce-source": source,
};
const event: CloudEvent = receiver.accept(binaryHeaders, data);
expect(typeof event.data).to.equal("object");
expect((event.data as Record<string, string>).lunch).to.equal("sushi");
});
it("Converts the JSON body of a structured event to an Object", () => {
const payload = {
id,
type,
source,
data,
specversion,
};
const event = receiver.accept(structuredHeaders, payload);
expect(typeof event.data).to.equal("object");
expect((event.data as Record<string, string>).lunch).to.equal("sushi");
});
});
@ -46,27 +63,23 @@ describe("HTTP Transport Binding Receiver for CloudEvents", () => {
type,
source,
data,
specversion
specversion,
};
const headers = {
[HEADER_CONTENT_TYPE]: MIME_CE_JSON
};
const event = receiver.accept(headers, payload);
const event = receiver.accept(structuredHeaders, payload);
validateEvent(event, specversion);
});
it("Binary data returns a CloudEvent", () => {
const headers = {
[HEADER_CONTENT_TYPE]: DEFAULT_CONTENT_TYPE,
[DEFAULT_SPEC_VERSION_HEADER]: specversion,
[BINARY_HEADERS_1.ID]: id,
[BINARY_HEADERS_1.TYPE]: type,
[BINARY_HEADERS_1.SOURCE]: source
const binaryHeaders = {
"content-type": "application/json; charset=utf-8",
"ce-specversion": specversion,
"ce-id": id,
"ce-type": type,
"ce-source": source,
};
const event = receiver.accept(headers, data);
const event = receiver.accept(binaryHeaders, data);
validateEvent(event, specversion);
});
});
@ -80,27 +93,23 @@ describe("HTTP Transport Binding Receiver for CloudEvents", () => {
type,
source,
data,
specversion
specversion,
};
const headers = {
[HEADER_CONTENT_TYPE]: MIME_CE_JSON
};
const event = receiver.accept(headers, payload);
const event = receiver.accept(structuredHeaders, payload);
validateEvent(event, specversion);
});
it("Binary data returns a CloudEvent", () => {
const headers = {
[HEADER_CONTENT_TYPE]: DEFAULT_CONTENT_TYPE,
[DEFAULT_SPEC_VERSION_HEADER]: specversion,
[BINARY_HEADERS_03.ID]: id,
[BINARY_HEADERS_03.TYPE]: type,
[BINARY_HEADERS_03.SOURCE]: source
const binaryHeaders = {
"content-type": "application/json; charset=utf-8",
"ce-specversion": specversion,
"ce-id": id,
"ce-type": type,
"ce-source": source,
};
const event = receiver.accept(headers, data);
const event = receiver.accept(binaryHeaders, data);
validateEvent(event, specversion);
});
});
@ -109,8 +118,7 @@ describe("HTTP Transport Binding Receiver for CloudEvents", () => {
const specversion = "1.0";
const id = "partition:1/offset:23";
const type = "dev.knative.kafka.event";
const source =
"/apis/v1/namespaces/kafka/kafkasources/kafka-source#knative-demo-topic";
const source = "/apis/v1/namespaces/kafka/kafkasources/kafka-source#knative-demo-topic";
it("Should be parsable", () => {
const headers = {
@ -129,7 +137,7 @@ describe("HTTP Transport Binding Receiver for CloudEvents", () => {
"x-envoy-expected-rq-timeout-ms": "600000",
"x-forwarded-for": "10.131.0.72, 10.128.2.99",
"x-forwarded-proto": "http",
"x-request-id": "d3649c1b-a968-40bf-a9da-3e853abc0c8b"
"x-request-id": "d3649c1b-a968-40bf-a9da-3e853abc0c8b",
};
const event = receiver.accept(headers, data);
expect(event instanceof CloudEvent).to.equal(true);

View File

@ -1,6 +1,8 @@
const expect = require("chai").expect;
const Parser = require("../../../lib/formats/json/parser.js");
const ValidationError = require("../../../lib/bindings/http/validation/validation_error.js");
import "mocha";
import { expect } from "chai";
import { JSONParser as Parser } from "../src/parsers/";
import { ValidationError } from "../src/";
describe("JSON Event Format Parser", () => {
it("Throw error when payload is an integer", () => {
@ -8,18 +10,20 @@ describe("JSON Event Format Parser", () => {
const payload = 83;
const parser = new Parser();
// act and assert
expect(parser.parse.bind(parser, payload))
.to.throw(ValidationError, "invalid payload type, allowed are: string or object");
expect(parser.parse.bind(parser, (payload as unknown) as string)).to.throw(
ValidationError,
"invalid payload type, allowed are: string or object",
);
});
it("Throw error when payload is null", () => {
// setup
const payload = null;
const parser = new Parser();
// act and assert
expect(parser.parse.bind(parser, payload)).to.throw(ValidationError, "null or undefined payload");
expect(parser.parse.bind(parser, (payload as unknown) as string)).to.throw(
ValidationError,
"null or undefined payload",
);
});
it("Throw error when payload is undefined", () => {
@ -35,9 +39,10 @@ describe("JSON Event Format Parser", () => {
const payload = 8.3;
const parser = new Parser();
// act and assert
expect(parser.parse.bind(parser, payload))
.to.throw(ValidationError, "invalid payload type, allowed are: string or object");
expect(parser.parse.bind(parser, (payload as unknown) as string)).to.throw(
ValidationError,
"invalid payload type, allowed are: string or object",
);
});
it("Throw error when payload is an invalid JSON", () => {
@ -51,6 +56,7 @@ describe("JSON Event Format Parser", () => {
it("Must accept when the payload is a string well formed as JSON", () => {
// setup
// eslint-disable-next-line prettier/prettier
const payload = "{\"much\" : \"wow\"}";
const parser = new Parser();

View File

@ -0,0 +1,434 @@
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";
const receiver = new BinaryHTTPReceiver(Version.V03);
describe("HTTP Transport Binding Binary Receiver for CloudEvents v0.3", () => {
describe("Check", () => {
it("Throw error when payload arg is null or undefined", () => {
// setup
const payload = undefined;
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 = 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();
});
});
describe("Parse", () => {
it("CloudEvent contains 'type'", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.type).to.equal("type");
});
it("CloudEvent contains 'specversion'", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.specversion).to.equal(Version.V03);
});
it("CloudEvent contains 'source'", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.source).to.equal("/source");
});
it("CloudEvent contains 'id'", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.id).to.equal("id");
});
it("CloudEvent contains 'time'", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.time).to.equal("2019-06-16T11:42:00.000Z");
});
it("CloudEvent contains 'schemaurl'", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.schemaurl).to.equal("http://schema.registry/v1");
});
it("CloudEvent contains 'datacontenttype' (application/json)", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.datacontenttype).to.equal("application/json");
});
it("CloudEvent contains 'datacontenttype' (application/octet-stream)", () => {
// setup
const payload = "The payload is binary data";
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/octet-stream",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.datacontenttype).to.equal("application/octet-stream");
});
it("CloudEvent contains 'data' (application/json)", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.data).to.deep.equal(payload);
});
it("CloudEvent contains 'data' (application/octet-stream)", () => {
// setup
const payload = "The payload is binary data";
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/octet-stream",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.data).to.deep.equal(payload);
});
it("No error when all attributes are in place", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual).to.be.an.instanceof(CloudEvent);
});
it("Should accept 'extension1'", () => {
// setup
const extension1 = "mycuston-ext1";
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
[`${[CONSTANTS.EXTENSIONS_PREFIX]}extension1`]: extension1,
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.extension1).to.equal(extension1);
});
});
});

View File

@ -0,0 +1,460 @@
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 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 = undefined;
expect(receiver.parse.bind(receiver, payload, (attributes as unknown) as string)).to.throw(
ValidationError,
"headers is null or undefined",
);
});
it("Throw error when payload is not an object or string", () => {
// setup
const payload = 1.2;
const attributes = {};
// act and assert
expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(
ValidationError,
"payload must be an object or a string",
);
});
it("Throw error when headers has no 'ce-type'", () => {
// setup
const payload = {};
const attributes = {
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act and assert
expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(
ValidationError,
"header 'ce-type' not found",
);
});
it("Throw error when headers has no 'ce-specversion'", () => {
// setup
const payload = {};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act and assert
expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(
ValidationError,
"header 'ce-specversion' not found",
);
});
it("Throw error when headers has no 'ce-source'", () => {
// setup
const payload = {};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act and assert
expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(
ValidationError,
"header 'ce-source' not found",
);
});
it("Throw error when headers has no 'ce-id'", () => {
// setup
const payload = {};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act and assert
expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "header 'ce-id' not found");
});
it("Throw error when spec is not 1.0", () => {
// setup
const payload = {};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: "0.2",
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act and assert
expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid spec version");
});
it("Throw error when the content-type is invalid", () => {
// setup
const payload = {};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.HEADER_CONTENT_TYPE]: "text/html",
};
// act and assert
expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid content type");
});
it("No error when content-type is unspecified", () => {
const payload = {};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
};
// act and assert
expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw();
});
it("No error when all required headers are in place", () => {
// setup
const payload = {};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act and assert
expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw();
});
});
describe("Parse", () => {
it("CloudEvent contains 'type'", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.type).to.equal("type");
});
it("CloudEvent contains 'specversion'", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.specversion).to.equal(Version.V1);
});
it("CloudEvent contains 'source'", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.source).to.equal("/source");
});
it("CloudEvent contains 'id'", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.id).to.equal("id");
});
it("CloudEvent contains 'time'", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.time).to.equal("2019-06-16T11:42:00.000Z");
});
it("CloudEvent contains 'dataschema'", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.dataschema).to.equal("http://schema.registry/v1");
});
it("CloudEvent contains 'contenttype' (application/json)", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.datacontenttype).to.equal("application/json");
});
it("CloudEvent contains 'contenttype' (application/octet-stream)", () => {
// setup
const payload = "The payload is binary data";
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/octet-stream",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.datacontenttype).to.equal("application/octet-stream");
});
it("CloudEvent contains 'data' (application/json)", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.data).to.deep.equal(payload);
});
it("CloudEvent contains 'data' (application/octet-stream)", () => {
// setup
const payload = "The payload is binary data";
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/octet-stream",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.data).to.deep.equal(payload);
});
it("The content of 'data' is base64 for binary", () => {
// setup
const expected = {
data: "dataString",
};
const bindata = Uint32Array.from(JSON.stringify(expected) as string, (c) => c.codePointAt(0) as number);
const payload = asBase64(bindata);
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "/source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.data).to.deep.equal(expected);
});
it("No error when all attributes are in place", () => {
// setup
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual).to.be.an.instanceof(CloudEvent);
});
it("Should accept 'extension1'", () => {
// setup
const extension1 = "mycustom-ext1";
const payload = {
data: "dataString",
};
const attributes = {
[CONSTANTS.CE_HEADERS.TYPE]: "type",
[CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1,
[CONSTANTS.CE_HEADERS.SOURCE]: "source",
[CONSTANTS.CE_HEADERS.ID]: "id",
[CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z",
[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1",
[CONSTANTS.HEADER_CONTENT_TYPE]: "application/json",
[`${[CONSTANTS.EXTENSIONS_PREFIX]}extension1`]: extension1,
};
// act
const actual = receiver.parse(payload, attributes);
// assert
expect(actual.extension1).to.equal(extension1);
});
});
});

View File

@ -0,0 +1,205 @@
import "mocha";
import { expect } from "chai";
import { CloudEvent, ValidationError, Version } from "../src";
import { StructuredHTTPReceiver } from "../src/transport/http/structured_receiver";
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("No error when all required stuff are in place", () => {
// setup
const payload = {
specversion: Version.V03,
source,
type,
};
const attributes = {
"Content-Type": "application/cloudevents+json",
};
// act and assert
expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw();
});
});
describe("Parse", () => {
it("Throw error when the event does not follow the spec", () => {
// setup
const payload = {};
const attributes = {
"Content-Type": "application/cloudevents+json",
};
// act and assert
expect(receiver.parse.bind(receiver, payload, attributes)).to.throw();
});
});
describe("Parse", () => {
it("Throw error when the event does not follow the spec", () => {
const payload = {
type,
source,
time,
schemaurl,
data,
};
const headers = {
"Content-Type": "application/cloudevents+xml",
};
// act and assert
expect(receiver.parse.bind(receiver, payload, headers)).to.throw(ValidationError, "invalid content type");
});
it("Should accept event that follows the spec", () => {
// setup
const id = "id-x0dk";
const payload = {
specversion: Version.V03,
id,
type,
source,
time,
schemaurl,
datacontenttype: ceContentType,
data,
};
const headers = {
"content-type": "application/cloudevents+json",
};
// act
const actual = receiver.parse(payload, headers);
// assert
expect(actual).to.be.an.instanceof(CloudEvent);
expect(actual.id).to.equal(id);
});
it("Should accept 'extension1'", () => {
// setup
const extension1 = "mycuston-ext1";
const payload = {
specversion: Version.V03,
type,
source,
time,
schemaurl,
data,
datacontenttype: ceContentType,
extension1: extension1,
};
const headers = {
"content-type": "application/cloudevents+json",
};
// act
const actual = receiver.parse(payload, headers);
// assert
expect(actual.extension1).to.equal(extension1);
});
it("Should parse 'data' stringfied json to json object", () => {
const payload = {
specversion: Version.V03,
type,
source,
time,
schemaurl,
datacontenttype: ceContentType,
data: JSON.stringify(data),
};
const headers = {
"content-type": "application/cloudevents+json",
};
// act
const actual = receiver.parse(payload, headers);
// assert
expect(actual.data).to.deep.equal(data);
});
});
});

View File

@ -0,0 +1,188 @@
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 ceContentType = "application/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,
dataContentType: ceContentType,
};
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 = "mycustom-ext1";
const event = {
type,
source,
time,
data,
dataschema,
dataContentType: ceContentType,
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,
dataContentType: ceContentType,
};
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,
dataContentType: ceContentType,
};
const headers = {
"content-type": "application/cloudevents+json",
};
// act
const actual = receiver.parse(payload, headers);
expect(actual.data_base64).to.equal(expected);
});
});
});

View File

@ -1,46 +0,0 @@
const expect = require("chai").expect;
const { CloudEvent, HTTPReceiver, HTTPEmitter } = require("../");
const {
SPEC_V03,
SPEC_V1
} = require("../lib/bindings/http/constants.js");
const fixture = {
type: "org.cloudevents.test",
source: "http://cloudevents.io"
};
describe("The SDK Requirements", () => {
it("should expose a CloudEvent type", () => {
const event = new CloudEvent(fixture);
expect(event instanceof CloudEvent).to.equal(true);
});
it("should expose an HTTPReceiver type", () => {
const receiver = new HTTPReceiver();
expect(receiver instanceof HTTPReceiver).to.equal(true);
});
it("should expose an HTTPEmitter type", () => {
const emitter = new HTTPEmitter({
url: "http://example.com"
});
expect(emitter instanceof HTTPEmitter).to.equal(true);
});
describe("v0.3", () => {
it("should create an event using the right spec version", () => {
expect(new CloudEvent({
specversion: SPEC_V03,
...fixture
}).spec.payload.specversion).to.equal(SPEC_V03);
});
});
describe("v1.0", () => {
it("should create an event using the right spec version", () => {
expect(new CloudEvent(fixture).spec.payload.specversion).to.equal(SPEC_V1);
});
});
});

44
test/sdk_test.ts Normal file
View File

@ -0,0 +1,44 @@
import "mocha";
import { expect } from "chai";
import { CloudEvent, Receiver, Emitter, Version } from "../src";
const fixture = {
type: "org.cloudevents.test",
source: "http://cloudevents.io",
};
describe("The SDK Requirements", () => {
it("should expose a CloudEvent type", () => {
const event = new CloudEvent(fixture);
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",
});
expect(emitter instanceof Emitter).to.equal(true);
});
describe("v0.3", () => {
it("should create an event using the right spec version", () => {
expect(
new CloudEvent({
...fixture,
specversion: Version.V03,
}).specversion,
).to.equal(Version.V03);
});
});
describe("v1.0", () => {
it("should create an event using the right spec version", () => {
expect(new CloudEvent(fixture).specversion).to.equal(Version.V1);
});
});
});

188
test/spec_03_tests.ts Normal file
View File

@ -0,0 +1,188 @@
import "mocha";
import { expect } from "chai";
import { CloudEvent, Version, ValidationError, Mode } from "../src";
import Constants from "../src/constants";
const id = "97699ec2-a8d9-47c1-bfa0-ff7aa526f838";
const type = "com.github.pull.create";
const source = "urn:event:from:myapi/resourse/123";
const time = new Date();
const schemaurl = "http://example.com/registry/myschema.json";
const data = {
much: "wow",
};
const subject = "subject-x0";
const cloudevent = new CloudEvent({
specversion: Version.V03,
id,
source,
type,
subject,
time,
data,
schemaurl,
datacontenttype: Constants.MIME_JSON,
});
describe("CloudEvents Spec v0.3", () => {
describe("REQUIRED Attributes", () => {
it("Should have 'id'", () => {
expect(cloudevent.id).to.equal(id);
});
it("Should have 'source'", () => {
expect(cloudevent.source).to.equal(source);
});
it("Should have 'specversion'", () => {
expect(cloudevent.specversion).to.equal(Version.V03);
});
it("Should have 'type'", () => {
expect(cloudevent.type).to.equal(type);
});
});
describe("OPTIONAL Attributes", () => {
it("Should have 'datacontentencoding'", () => {
cloudevent.datacontentencoding = Constants.ENCODING_BASE64;
expect(cloudevent.datacontentencoding).to.equal(Constants.ENCODING_BASE64);
});
it("Should have 'datacontenttype'", () => {
expect(cloudevent.datacontenttype).to.equal(Constants.MIME_JSON);
});
it("Should have 'schemaurl'", () => {
expect(cloudevent.schemaurl).to.equal(schemaurl);
});
it("Should have 'subject'", () => {
expect(cloudevent.subject).to.equal(subject);
});
it("Should have 'time'", () => {
expect(cloudevent.time).to.equal(time.toISOString());
});
it("Should have 'data'", () => {
expect(cloudevent.data).to.deep.equal(data);
});
it("Should have the 'extension1'", () => {
cloudevent.extension1 = "value1";
expect(cloudevent.extension1).to.equal("value1");
});
});
describe("The Constraints check", () => {
describe("'id'", () => {
it("should throw an error when is absent", () => {
delete cloudevent.id;
expect(cloudevent.validate.bind(cloudevent)).to.throw(ValidationError, "invalid payload");
cloudevent.id = id;
});
it("should throw an error when is empty", () => {
cloudevent.id = "";
expect(cloudevent.validate.bind(cloudevent)).to.throw(ValidationError, "invalid payload");
cloudevent.id = id;
});
});
describe("'source'", () => {
it("should throw an error when is absent", () => {
delete cloudevent.source;
expect(cloudevent.validate.bind(cloudevent)).to.throw(ValidationError, "invalid payload");
cloudevent.source = source;
});
});
describe("'specversion'", () => {
it("should throw an error when is absent", () => {
delete cloudevent.specversion;
expect(cloudevent.validate.bind(cloudevent)).to.throw(ValidationError, "invalid payload");
cloudevent.specversion = Version.V03;
});
});
describe("'type'", () => {
it("should throw an error when is absent", () => {
delete cloudevent.type;
expect(cloudevent.validate.bind(cloudevent)).to.throw(ValidationError, "invalid payload");
cloudevent.type = type;
});
it("should throw an error when is an empty string", () => {
cloudevent.type = "";
expect(cloudevent.validate.bind(cloudevent)).to.throw(ValidationError, "invalid payload");
cloudevent.type = type;
});
it("must be a non-empty string", () => {
cloudevent.type = type;
expect(cloudevent.type).to.equal(type);
});
});
describe("'datacontentencoding'", () => {
it("should throw an error when is a unsupported encoding", () => {
cloudevent.data = "Y2xvdWRldmVudHMK";
cloudevent.datacontentencoding = Mode.BINARY;
expect(cloudevent.validate.bind(cloudevent)).to.throw(ValidationError, "invalid payload");
delete cloudevent.datacontentencoding;
cloudevent.data = data;
});
it("should throw an error when 'data' does not carry base64", () => {
cloudevent.data = "no base 64 value";
cloudevent.datacontentencoding = Constants.ENCODING_BASE64;
cloudevent.datacontenttype = "text/plain";
expect(cloudevent.validate.bind(cloudevent)).to.throw(ValidationError, "invalid payload");
delete cloudevent.datacontentencoding;
cloudevent.data = data;
});
it("should accept when 'data' is a string", () => {
cloudevent.data = "Y2xvdWRldmVudHMK";
cloudevent.datacontentencoding = Constants.ENCODING_BASE64;
expect(cloudevent.validate()).to.be.true;
delete cloudevent.datacontentencoding;
cloudevent.data = data;
});
});
describe("'data'", () => {
it("should maintain the type of data when no data content type", () => {
delete cloudevent.datacontenttype;
cloudevent.data = JSON.stringify(data);
expect(typeof cloudevent.data).to.equal("string");
cloudevent.datacontenttype = Constants.MIME_JSON;
});
it("should convert data with stringified json to a json object", () => {
cloudevent.datacontenttype = Constants.MIME_JSON;
cloudevent.data = JSON.stringify(data);
expect(cloudevent.data).to.deep.equal(data);
});
});
describe("'subject'", () => {
it("should throw an error when is an empty string", () => {
cloudevent.subject = "";
expect(cloudevent.validate.bind(cloudevent)).to.throw(ValidationError, "invalid payload");
cloudevent.subject = subject;
});
});
describe("'time'", () => {
it("must adhere to the format specified in RFC 3339", () => {
expect(cloudevent.time).to.equal(time.toISOString());
});
});
});
});

View File

@ -1,218 +0,0 @@
const expect = require("chai").expect;
const { CloudEvent } = require("../index.js");
const {
MIME_JSON,
ENCODING_BASE64,
SPEC_V03,
BINARY
} = require("../lib/bindings/http/constants.js");
const ValidationError = require("../lib/bindings/http/validation/validation_error.js");
const id = "97699ec2-a8d9-47c1-bfa0-ff7aa526f838";
const type = "com.github.pull.create";
const source = "urn:event:from:myapi/resourse/123";
const time = new Date();
const schemaURL = "http://example.com/registry/myschema.json";
const data = {
much: "wow"
};
const subject = "subject-x0";
const cloudevent = new CloudEvent({
specversion: SPEC_V03,
id,
source,
type,
subject,
time,
data,
schemaURL,
dataContentType: MIME_JSON
});
describe("CloudEvents Spec v0.3", () => {
describe("REQUIRED Attributes", () => {
it("Should have 'id'", () => {
expect(cloudevent.id).to.equal(id);
});
it("Should have 'source'", () => {
expect(cloudevent.source).to.equal(source);
});
it("Should have 'specversion'", () => {
expect(cloudevent.specversion).to.equal(SPEC_V03);
});
it("Should have 'type'", () => {
expect(cloudevent.type).to.equal(type);
});
});
describe("OPTIONAL Attributes", () => {
it("Should have 'datacontentencoding'", () => {
cloudevent.dataContentEncoding = ENCODING_BASE64;
expect(cloudevent.spec.payload.datacontentencoding)
.to.equal(ENCODING_BASE64);
delete cloudevent.spec.payload.datacontentencoding;
});
it("Should have 'datacontenttype'", () => {
expect(cloudevent.dataContentType).to.equal(MIME_JSON);
});
it("Should have 'schemaurl'", () => {
expect(cloudevent.schemaURL).to.equal(schemaURL);
});
it("Should have 'subject'", () => {
expect(cloudevent.subject).to.equal(subject);
});
it("Should have 'time'", () => {
expect(cloudevent.time).to.equal(time.toISOString());
});
it("Should have 'data'", () => {
expect(cloudevent.data).to.deep.equal(data);
});
it("Should have the 'extension1'", () => {
cloudevent.addExtension("extension1", "value1");
expect(cloudevent.spec.payload.extension1)
.to.equal("value1");
});
it("should throw an error when use a reserved name as extension", () => {
expect(cloudevent.addExtension.bind(cloudevent, "id"))
.to.throw(ValidationError, "Reserved attribute name: 'id'");
});
});
describe("The Constraints check", () => {
describe("'id'", () => {
it("should throw an error when is absent", () => {
delete cloudevent.spec.payload.id;
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.spec.payload.id = id;
});
it("should throw an error when is empty", () => {
cloudevent.spec.payload.id = "";
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.spec.payload.id = id;
});
});
describe("'source'", () => {
it("should throw an error when is absent", () => {
delete cloudevent.spec.payload.source;
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.spec.payload.source = source;
});
});
describe("'specversion'", () => {
it("should throw an error when is absent", () => {
delete cloudevent.spec.payload.specversion;
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.spec.payload.specversion = SPEC_V03;
});
it("should throw an error when is empty", () => {
cloudevent.spec.payload.specversion = "";
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.spec.payload.specversion = SPEC_V03;
});
});
describe("'type'", () => {
it("should throw an error when is absent", () => {
delete cloudevent.spec.payload.type;
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.spec.payload.type = type;
});
it("should throw an error when is an empty string", () => {
cloudevent.type = "";
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.type = type;
});
it("must be a non-empty string", () => {
cloudevent.type = type;
expect(cloudevent.spec.payload.type).to.equal(type);
});
});
describe("'datacontentencoding'", () => {
it("should throw an error when is a unsupported encoding", () => {
cloudevent.data = "Y2xvdWRldmVudHMK";
cloudevent.dataContentEncoding = BINARY;
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
delete cloudevent.spec.payload.datacontentencoding;
cloudevent.data = data;
});
it("should throw an error when 'data' does not carry base64",
() => {
cloudevent.data = "no base 64 value";
cloudevent.dataContentEncoding = ENCODING_BASE64;
cloudevent.dataContentType = "text/plain";
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
delete cloudevent.spec.payload.datacontentencoding;
cloudevent.data = data;
});
it("should accept when 'data' is a string", () => {
cloudevent.data = "Y2xvdWRldmVudHMK";
cloudevent.dataContentEncoding = ENCODING_BASE64;
expect(cloudevent.format()).to.have.property("datacontentencoding");
delete cloudevent.spec.payload.datacontentencoding;
cloudevent.data = data;
});
});
describe("'data'", () => {
it("should maintain the type of data when no data content type", () => {
delete cloudevent.spec.payload.datacontenttype;
cloudevent.data = JSON.stringify(data);
expect(typeof cloudevent.data).to.equal("string");
cloudevent.dataContentType = MIME_JSON;
});
it("should convert data with stringified json to a json object", () => {
cloudevent.dataContentType = MIME_JSON;
cloudevent.data = JSON.stringify(data);
expect(cloudevent.data).to.deep.equal(data);
});
});
describe("'subject'", () => {
it("should throw an error when is an empty string", () => {
cloudevent.subject = "";
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.subject = subject;
});
});
describe("'time'", () => {
it("must adhere to the format specified in RFC 3339", () => {
expect(cloudevent.format().time).to.equal(time.toISOString());
});
});
});
});

View File

@ -1,237 +0,0 @@
const expect = require("chai").expect;
const { CloudEvent } = require("../index.js");
const { v4: uuidv4 } = require("uuid");
const { asBase64 } = require("../lib/bindings/http/validation/fun.js");
const ValidationError = require("../lib/bindings/http/validation/validation_error.js");
const {
SPEC_V1
} = require("../lib/bindings/http/constants.js");
const id = uuidv4();
const type = "com.github.pull.create";
const source = "urn:event:from:myapi/resource/123";
const time = new Date();
const dataSchema = "http://example.com/registry/myschema.json";
const dataContentType = "application/json";
const data = {
much: "wow"
};
const subject = "subject-x0";
const cloudevent = new CloudEvent({
specversion: SPEC_V1,
id,
source,
type,
dataContentType,
dataSchema,
subject,
time,
data
});
describe("CloudEvents Spec v1.0", () => {
describe("REQUIRED Attributes", () => {
it("Should have 'id'", () => {
expect(cloudevent.id).to.equal(id);
});
it("Should have 'source'", () => {
expect(cloudevent.source).to.equal(source);
});
it("Should have 'specversion'", () => {
expect(cloudevent.specversion).to.equal("1.0");
});
it("Should have 'type'", () => {
expect(cloudevent.type).to.equal(type);
});
});
describe("OPTIONAL Attributes", () => {
it("Should have 'datacontenttype'", () => {
expect(cloudevent.dataContentType).to.equal(dataContentType);
});
it("Should have 'dataschema'", () => {
expect(cloudevent.dataSchema).to.equal(dataSchema);
});
it("Should have 'subject'", () => {
expect(cloudevent.subject).to.equal(subject);
});
it("Should have 'time'", () => {
expect(cloudevent.time).to.equal(time.toISOString());
});
});
describe("Extensions Constraints", () => {
it("should be ok when type is 'boolean'", () => {
cloudevent.addExtension("ext-boolean", true);
expect(cloudevent.spec.payload["ext-boolean"])
.to.equal(true);
});
it("should be ok when type is 'integer'", () => {
cloudevent.addExtension("ext-integer", 2019);
expect(cloudevent.spec.payload["ext-integer"])
.to.equal(2019);
});
it("should be ok when type is 'string'", () => {
cloudevent.addExtension("ext-string", "an-string");
expect(cloudevent.spec.payload["ext-string"])
.to.equal("an-string");
});
it("should be ok when type is 'Uint32Array' for 'Binary'", () => {
const myBinary = new Uint32Array(2019);
cloudevent.addExtension("ext-binary", myBinary);
expect(cloudevent.spec.payload["ext-binary"])
.to.equal(myBinary);
});
// URI
it("should be ok when type is 'Date' for 'Timestamp'", () => {
const myDate = new Date();
cloudevent.addExtension("ext-date", myDate);
expect(cloudevent.spec.payload["ext-date"])
.to.equal(myDate);
});
it("Should have the 'extension1'", () => {
cloudevent.addExtension("extension1", "value1");
expect(cloudevent.spec.payload.extension1)
.to.equal("value1");
});
it("should throw an error when use a reserved name as extension", () => {
expect(cloudevent.addExtension.bind(cloudevent, "id"))
.to.throw(ValidationError, "Reserved attribute name: 'id'");
});
it("should throw an error when use an invalid type", () => {
expect(cloudevent
.addExtension.bind(cloudevent, "invalid-val", { cool: "nice" }))
.to.throw(ValidationError, "Invalid type of extension value");
});
});
describe("The Constraints check", () => {
describe("'id'", () => {
it("should throw an error when is absent", () => {
delete cloudevent.spec.payload.id;
expect(cloudevent.format.bind(cloudevent))
.to
.throw(ValidationError, "invalid payload");
cloudevent.spec.payload.id = id;
});
it("should throw an error when is empty", () => {
cloudevent.spec.payload.id = "";
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.spec.payload.id = id;
});
});
describe("'source'", () => {
it("should throw an error when is absent", () => {
delete cloudevent.spec.payload.source;
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.spec.payload.source = source;
});
});
describe("'specversion'", () => {
it("should throw an error when is absent", () => {
delete cloudevent.spec.payload.specversion;
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.spec.payload.specversion = "1.0";
});
it("should throw an error when is empty", () => {
cloudevent.spec.payload.specversion = "";
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.spec.payload.specversion = "1.0";
});
});
describe("'type'", () => {
it("should throw an error when is absent", () => {
delete cloudevent.spec.payload.type;
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.spec.payload.type = type;
});
it("should throw an error when is an empty string", () => {
cloudevent.type = "";
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.type = type;
});
it("must be a non-empty string", () => {
cloudevent.type = type;
expect(cloudevent.spec.payload.type).to.equal(type);
});
});
describe("'subject'", () => {
it("should throw an error when is an empty string", () => {
cloudevent.subject = "";
expect(cloudevent.format.bind(cloudevent))
.to.throw(ValidationError, "invalid payload");
cloudevent.subject = type;
});
});
describe("'time'", () => {
it("must adhere to the format specified in RFC 3339", () => {
cloudevent.time = time;
expect(cloudevent.format().time).to.equal(time.toISOString());
});
});
});
describe("Event data constraints", () => {
it("Should have 'data'", () => {
expect(cloudevent.data).to.deep.equal(data);
});
it("should maintain the type of data when no data content type", () => {
delete cloudevent.spec.payload.datacontenttype;
cloudevent.data = JSON.stringify(data);
expect(typeof cloudevent.data).to.equal("string");
cloudevent.dataContentType = dataContentType;
});
it("should convert data with stringified json to a json object", () => {
cloudevent.dataContentType = dataContentType;
cloudevent.data = JSON.stringify(data);
expect(cloudevent.data).to.deep.equal(data);
});
it("should be ok when type is 'Uint32Array' for 'Binary'", () => {
const dataString = ")(*~^my data for ce#@#$%";
const dataBinary = Uint32Array.from(dataString, (c) => c.codePointAt(0));
const expected = asBase64(dataBinary);
const olddct = cloudevent.dataContentType;
cloudevent.dataContentType = "text/plain";
cloudevent.data = dataBinary;
expect(cloudevent.data).to.deep.equal(expected);
cloudevent.dataContentType = olddct;
});
});
});

Some files were not shown because too many files have changed in this diff Show More