diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md index e7043837..5fe5aa7c 100644 --- a/.github/pull-request-template.md +++ b/.github/pull-request-template.md @@ -1,12 +1,12 @@ ## Proposed Changes -- +- - - diff --git a/.github/workflows/nodejs-ci-action.yml b/.github/workflows/nodejs-ci-action.yml index c1de8925..732a93ce 100644 --- a/.github/workflows/nodejs-ci-action.yml +++ b/.github/workflows/nodejs-ci-action.yml @@ -5,9 +5,9 @@ name: Node.js CI on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: build: diff --git a/.gitignore b/.gitignore index 8fa1cf23..1a0ce74e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ index.js /browser /bundles /dist +/docs # Runtime data pids diff --git a/CHANGELOG.md b/CHANGELOG.md index 43f4e243..3af118c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [3.2.0](https://github.com/cloudevents/sdk-javascript/compare/v3.1.0...v3.2.0) (2020-09-11) + + +### Features + +* add a constructor parameter for loose validation ([#328](https://github.com/cloudevents/sdk-javascript/issues/328)) ([a0e7281](https://github.com/cloudevents/sdk-javascript/commit/a0e72814d381b366ff8efe8ca0e7523d458d7159)) +* introduce Message, Serializer, Deserializer and Binding interfaces ([#324](https://github.com/cloudevents/sdk-javascript/issues/324)) ([e2eb476](https://github.com/cloudevents/sdk-javascript/commit/e2eb4766d355c767fd3d09fcdedb258b71647135)) + + +### Bug Fixes + +* upgrade cloudevents from 3.0.1 to 3.1.0 ([#335](https://github.com/cloudevents/sdk-javascript/issues/335)) ([6732194](https://github.com/cloudevents/sdk-javascript/commit/673219483a153e7a65fa17b3db85b9c6aa5d9dd7)) +* upgrade uuid from 8.2.0 to 8.3.0 ([#317](https://github.com/cloudevents/sdk-javascript/issues/317)) ([e433f9a](https://github.com/cloudevents/sdk-javascript/commit/e433f9a898f57bf26bed9422bd598e509c63d4c0)) + + +### Documentation + +* add ref to CoC and other things ([#244](https://github.com/cloudevents/sdk-javascript/issues/244)) ([1d7886c](https://github.com/cloudevents/sdk-javascript/commit/1d7886c50aa2c4546e116ae4f8e354ac7425ccd9)) + + +### Tests + +* implement pending tests leftover from TS rewrite ([#315](https://github.com/cloudevents/sdk-javascript/issues/315)) ([fcd869e](https://github.com/cloudevents/sdk-javascript/commit/fcd869e93a11fb996f0450a0796826f136759143)) + + +### Miscellaneous + +* **example:** Replaced body parser with express JSON parser ([#334](https://github.com/cloudevents/sdk-javascript/issues/334)) ([e6204af](https://github.com/cloudevents/sdk-javascript/commit/e6204af8d146f213d687e011e44e1120edaffa44)) +* add cucumber.js to list of files to lint and /docs to .gitignore ([#327](https://github.com/cloudevents/sdk-javascript/issues/327)) ([ef05f27](https://github.com/cloudevents/sdk-javascript/commit/ef05f27cf1c87858a0d69a57afc40e106816fde7)) +* Remove commented version import. ([#319](https://github.com/cloudevents/sdk-javascript/issues/319)) ([ffb1a13](https://github.com/cloudevents/sdk-javascript/commit/ffb1a137a8c47e639cce552478b015977afb8ce0)) +* typo ([#313](https://github.com/cloudevents/sdk-javascript/issues/313)) ([d78d101](https://github.com/cloudevents/sdk-javascript/commit/d78d101349c1137ee17f92245b2e250628e7cb18)) +* Update README with correct links for the support specification versions ([#321](https://github.com/cloudevents/sdk-javascript/issues/321)) ([198abb6](https://github.com/cloudevents/sdk-javascript/commit/198abb6c3f83ef17851f2c79ff3c1a91887c8ff8)), closes [#320](https://github.com/cloudevents/sdk-javascript/issues/320) +* Update references of master to main ([#316](https://github.com/cloudevents/sdk-javascript/issues/316)) ([9b44dfa](https://github.com/cloudevents/sdk-javascript/commit/9b44dfac58dacc07bd3687f7995b4a99bed5b852)) +* validate cloudevent version agnostic ([#311](https://github.com/cloudevents/sdk-javascript/issues/311)) ([3d2f01a](https://github.com/cloudevents/sdk-javascript/commit/3d2f01a7e387c6519fce6e74d98c004e36f83e3d)) + ## [3.1.0](https://github.com/cloudevents/sdk-javascript/compare/v3.0.1...v3.1.0) (2020-08-11) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e2a4d45..74e2fc70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,4 +38,4 @@ check your code style for linting errors without running tests, you can just run `npm run lint`. If there are errors, you can usually fix them automatically by running `npm run fix`. -Linting rules are declared in [.eslintrc](https://github.com/cloudevents/sdk-javascript/blob/master/.eslintrc). +Linting rules are declared in [.eslintrc](https://github.com/cloudevents/sdk-javascript/blob/main/.eslintrc). diff --git a/README.md b/README.md index 5b2b763f..beaf46a7 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ npm install cloudevents #### Receiving Events -You can choose almost any popular web framework for port binding. Use an -`HTTPReceiver` to process the incoming HTTP request. The receiver accepts +You can choose almost any popular web framework for port binding. Use a +`Receiver` to process the incoming HTTP request. The receiver accepts binary and structured events in either the 1.0 or 0.3 protocol formats. ```js @@ -110,23 +110,23 @@ const ce2 = ce.cloneWith({extension: "Value"}); ### Example Applications There are a few trivial example applications in -[the examples folder](https://github.com/cloudevents/sdk-javascript/tree/master/examples). +[the examples folder](https://github.com/cloudevents/sdk-javascript/tree/main/examples). There you will find Express.js, TypeScript and Websocket examples. ## Supported specification features -| Core Specification | [v0.3](v03spec) | [v1.0](v1spec) | +| Core Specification | [v0.3](https://github.com/cloudevents/spec/blob/v0.3/spec.md) | [v1.0](https://github.com/cloudevents/spec/blob/v1.0/spec.md) | | ----------------------------- | --- | --- | | CloudEvents Core | :heavy_check_mark: | :heavy_check_mark: | --- -| Event Formats | [v0.3](v03spec) | [v1.0](v1spec) | +| Event Formats | [v0.3](https://github.com/cloudevents/spec/tree/v0.3) | [v1.0](https://github.com/cloudevents/spec/tree/v1.0) | | ----------------------------- | --- | --- | | AVRO Event Format | :x: | :x: | | JSON Event Format | :heavy_check_mark: | :heavy_check_mark: | --- -| Transport Protocols | [v0.3](v03spec) | [v1.0](v1spec) | +| Transport Protocols | [v0.3](https://github.com/cloudevents/spec/tree/v0.3) | [v1.0](https://github.com/cloudevents/spec/tree/v1.0) | | ----------------------------- | --- | --- | | AMQP Protocol Binding | :x: | :x: | | HTTP Protocol Binding | :heavy_check_mark: | :heavy_check_mark: | @@ -150,8 +150,16 @@ There you will find Express.js, TypeScript and Websocket examples. ## Contributing We love contributions from the community! Please check the -[Contributor's Guide](https://github.com/cloudevents/sdk-javascript/blob/master/CONTRIBUTING.md) +[Contributor's Guide](https://github.com/cloudevents/sdk-javascript/blob/main/CONTRIBUTING.md) for information on how to get involved. -[v1spec]: https://github.com/cloudevents/spec/tree/v1.0 -[v103pec]: https://github.com/cloudevents/spec/tree/v0.3 +Each SDK may have its own unique processes, tooling and guidelines, common +governance related material can be found in the +[CloudEvents `community`](https://github.com/cloudevents/spec/tree/master/community) +directory. In particular, in there you will find information concerning +how SDK projects are +[managed](https://github.com/cloudevents/spec/blob/master/community/SDK-GOVERNANCE.md), +[guidelines](https://github.com/cloudevents/spec/blob/master/community/SDK-maintainer-guidelines.md) +for how PR reviews and approval, and our +[Code of Conduct](https://github.com/cloudevents/spec/blob/master/community/GOVERNANCE.md#additional-information) +information. diff --git a/RELEASE_GUIDELINES.md b/RELEASE_GUIDELINES.md index ae29e11b..1c920c1a 100644 --- a/RELEASE_GUIDELINES.md +++ b/RELEASE_GUIDELINES.md @@ -6,15 +6,15 @@ To prepare for a new release, create a [new issue](https://github.com/cloudevent For example: "Proposal for 3.2.0 release", or something similar. If you are not sure which version is the next version to be released, you can run `npm run release -- --dry-run` to find out what the next version will be. -The body of the issue should be the commits that will be part of the release. This can be easily accomplished by running a git log command with a defined **range**. This range should start at the most recent version tag and end at the latest commit in the master branch. +The body of the issue should be the commits that will be part of the release. This can be easily accomplished by running a git log command with a defined **range**. This range should start at the most recent version tag and end at the latest commit in the main branch. For example: ``` -git log v3.0.1..upstream/master --oneline +git log v3.0.1..upstream/main --oneline ``` -This will output all the commits from the 3.0.1 tag to the latest commits in the remote upstream/master branch. +This will output all the commits from the 3.0.1 tag to the latest commits in the remote upstream/main branch. This output should be pasted into the issue as normal text. This will allow Github to magically turn all commit hashes and PR/Issues numbers to links. @@ -37,7 +37,7 @@ npm run release This will update the CHANGELOG.md and create a new tag based on the version. This can then be pushed upstream by doing: ``` -git push upstream master --follow-tags +git push upstream main --follow-tags ``` ### Create the release on GitHub diff --git a/examples/express-ex/index.js b/examples/express-ex/index.js index 57a71e1c..cc8e2349 100644 --- a/examples/express-ex/index.js +++ b/examples/express-ex/index.js @@ -1,23 +1,10 @@ -/* eslint-disable no-console */ +/* eslint-disable */ const express = require("express"); -const {Receiver} = require("cloudevents"); - +const { Receiver } = require("cloudevents"); const app = express(); - -app.use((req, res, next) => { - let data = ""; - - req.setEncoding("utf8"); - req.on("data", function (chunk) { - data += chunk; - }); - - req.on("end", function () { - req.body = data; - next(); - }); -}); +const bodyParser = require('body-parser') +app.use(bodyParser.json()) app.post("/", (req, res) => { console.log("HEADERS", req.headers); @@ -25,8 +12,16 @@ app.post("/", (req, res) => { try { const event = Receiver.accept(req.headers, req.body); - console.log(`Accepted event: ${event}`); - res.status(201).json(event); + // respond as an event + const responseEventMessage = new CloudEvent({ + source: '/', + type: 'event:response', + ...event + }); + responseEventMessage.data = { + hello: 'world' + }; + res.status(201).json(responseEventMessage); } catch (err) { console.error(err); res.status(415).header("Content-Type", "application/json").send(JSON.stringify(err)); diff --git a/examples/express-ex/package.json b/examples/express-ex/package.json index 3d483f18..74fa5232 100644 --- a/examples/express-ex/package.json +++ b/examples/express-ex/package.json @@ -14,7 +14,8 @@ "author": "fabiojose@gmail.com", "license": "Apache-2.0", "dependencies": { - "cloudevents": "~3.0.0", + "body-parser": "^1.19.0", + "cloudevents": "^3.1.0", "express": "^4.17.1" } } diff --git a/examples/typescript-ex/package.json b/examples/typescript-ex/package.json index 111195e7..7a2c9c48 100644 --- a/examples/typescript-ex/package.json +++ b/examples/typescript-ex/package.json @@ -28,6 +28,6 @@ "typescript": "~3.9.5" }, "dependencies": { - "cloudevents": "~3.0.1" + "cloudevents": "~3.1.0" } } diff --git a/maintainer_guidelines.md b/maintainer_guidelines.md index 49a438ce..b55677ad 100644 --- a/maintainer_guidelines.md +++ b/maintainer_guidelines.md @@ -26,15 +26,15 @@ When landing pull requests, be sure to check the first line uses an appropriate ## Branch Management -The `master` branch is the bleeding edge. New major versions of the module +The `main` branch is the bleeding edge. New major versions of the module are cut from this branch and tagged. If you intend to submit a pull request -you should use `master HEAD` as your starting point. +you should use `main HEAD` as your starting point. Each major release will result in a new branch and tag. For example, the release of version 1.0.0 of the module will result in a `v1.0.0` tag on the release commit, and a new branch `v1.x.y` for subsequent minor and patch level releases of that major version. However, development will continue -apace on `master` for the next major version - e.g. 2.0.0. Version branches +apace on `main` for the next major version - e.g. 2.0.0. Version branches are only created for each major version. Minor and patch level releases are simply tagged. diff --git a/package-lock.json b/package-lock.json index 52c4183d..a88dab0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cloudevents", - "version": "3.0.1", + "version": "3.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -9484,9 +9484,9 @@ "dev": true }, "uuid": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz", - "integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==" + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" }, "v8-compile-cache": { "version": "2.1.1", diff --git a/package.json b/package.json index a945f22f..bb75d532 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "cloudevents", - "version": "3.1.0", + "version": "3.2.0", "description": "CloudEvents SDK for JavaScript", "main": "dist/index.js", "scripts": { "watch": "tsc --project tsconfig.json --watch", "build": "tsc --project tsconfig.json && tsc --project tsconfig.browser.json && webpack", - "lint": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}'", + "lint": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}' cucumber.js", "lint:fix": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}' --fix", "pretest": "npm run lint && npm run conformance", "test": "mocha --require ts-node/register ./test/integration/**/*.ts", @@ -100,7 +100,7 @@ "dependencies": { "ajv": "~6.12.3", "axios": "~0.19.2", - "uuid": "~8.2.0" + "uuid": "~8.3.0" }, "devDependencies": { "@types/ajv": "^1.0.0", diff --git a/pr_guidelines.md b/pr_guidelines.md index 440674c8..7a465451 100644 --- a/pr_guidelines.md +++ b/pr_guidelines.md @@ -5,13 +5,15 @@ a pull request in this repository. We hope it will help you have an easy time managing your work and a positive, satisfying experience when contributing your code. Thanks for getting involved! :rocket: -* [Getting Started](#getting-started) -* [Branches](#branches) -* [Commit Messages](#commit-messages) -* [Staying current with master](#staying-current-with-master) -* [Style Guide](#style-guide) -* [Submitting and Updating a Pull Request](#submitting-and-updating-a-pull-request) -* [Congratulations!](#congratulations) +- [Pull Request Guidelines](#pull-request-guidelines) + - [Getting Started](#getting-started) + - [Branches](#branches) + - [Commit Messages](#commit-messages) + - [Signing your commits](#signing-your-commits) + - [Staying Current with `main`](#staying-current-with-main) + - [Style Guide](#style-guide) + - [Submitting and Updating Your Pull Request](#submitting-and-updating-your-pull-request) + - [Congratulations!](#congratulations) ## Getting Started @@ -35,7 +37,7 @@ you might create a branch named `48-fix-http-agent-error`. ```console git fetch upstream -git reset --hard upstream/master +git reset --hard upstream/main git checkout FETCH_HEAD git checkout -b 48-fix-http-agent-error ``` @@ -102,19 +104,19 @@ Date: Thu Feb 2 11:41:15 2018 -0800 Notice the `Author` and `Signed-off-by` lines match. If they don't your PR will be rejected by the automated DCO check. -## Staying Current with `master` +## Staying Current with `main` -As you are working on your branch, changes may happen on `master`. Before +As you are working on your branch, changes may happen on `main`. Before submitting your pull request, be sure that your branch has been updated with the latest commits. ```console git fetch upstream -git rebase upstream/master +git rebase upstream/main ``` This may cause conflicts if the files you are changing on your branch are -also changed on master. Error messages from `git` will indicate if conflicts +also changed on main. Error messages from `git` will indicate if conflicts exist and what files need attention. Resolve the conflicts in each file, then continue with the rebase with `git rebase --continue`. @@ -134,22 +136,22 @@ check your code style for linting errors without running tests, you can just run `npm run lint`. If there are errors, you can usually fix them automatically by running `npm run fix`. -Linting rules are declared in [.eslintrc](https://github.com/cloudevents/sdk-javascript/blob/master/.eslintrc). +Linting rules are declared in [.eslintrc](https://github.com/cloudevents/sdk-javascript/blob/main/.eslintrc). ## Submitting and Updating Your Pull Request Before submitting a pull request, you should make sure that all of the tests successfully pass by running `npm test`. -Once you have sent your pull request, `master` may continue to evolve -before your pull request has landed. If there are any commits on `master` +Once you have sent your pull request, `main` may continue to evolve +before your pull request has landed. If there are any commits on `main` that conflict with your changes, you may need to update your branch with these changes before the pull request can land. Resolve conflicts the same way as before. ```console git fetch upstream -git rebase upstream/master +git rebase upstream/main # fix any potential conflicts git push -f origin 48-fix-http-agent-error ``` @@ -166,7 +168,7 @@ for details. ```console git commit -m "fixup: fix typo" -git rebase -i upstream/master # follow git instructions +git rebase -i upstream/main # follow git instructions ``` Once you have rebased your commits, you can force push to your fork as before. diff --git a/src/event/cloudevent.ts b/src/event/cloudevent.ts index 0cab0dad..df47b297 100644 --- a/src/event/cloudevent.ts +++ b/src/event/cloudevent.ts @@ -8,7 +8,7 @@ import { CloudEventV1Attributes, CloudEventV1OptionalAttributes, } from "./interfaces"; -import { validateV1, validateV03 } from "./spec"; +import { validateCloudEvent } from "./spec"; import { ValidationError, isBinary, asBase64, isValidType } from "./validation"; import CONSTANTS from "../constants"; import { isString } from "util"; @@ -46,7 +46,15 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { schemaurl?: string; datacontentencoding?: string; - constructor(event: CloudEventV1 | CloudEventV1Attributes | CloudEventV03 | CloudEventV03Attributes) { + /** + * Creates a new CloudEvent object with the provided properties. If there is a chance that the event + * properties will not conform to the CloudEvent specification, you may pass a boolean `false` as a + * second parameter to bypass event validation. + * + * @param {object} event the event properties + * @param {boolean?} strict whether to perform event validation when creating the object - default: true + */ + constructor(event: CloudEventV1 | CloudEventV1Attributes | CloudEventV03 | CloudEventV03Attributes, strict = true) { // 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 }; @@ -105,20 +113,20 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { for (const [key, value] of Object.entries(properties)) { // Extension names should only allow lowercase a-z and 0-9 in the name // names should not exceed 20 characters in length - if (!key.match(/^[a-z0-9]{1,20}$/)) { + if (!key.match(/^[a-z0-9]{1,20}$/) && strict) { throw new ValidationError("invalid extension name"); } // Value should be spec compliant // https://github.com/cloudevents/spec/blob/master/spec.md#type-system - if (!isValidType(value)) { + if (!isValidType(value) && strict) { throw new ValidationError("invalid extension value"); } this[key] = value; } - this.validate(); + strict ? this.validate() : undefined; Object.freeze(this); } @@ -156,6 +164,12 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { this.#_data = value; } + /** + * Used by JSON.stringify(). The name is confusing, but this method is called by + * JSON.stringify() when converting this object to JSON. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify + * @return {object} this event as a plain object + */ toJSON(): Record { const event = { ...this }; event.time = this.time; @@ -174,12 +188,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { */ 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"); + return validateCloudEvent(this); } catch (e) { if (e instanceof ValidationError) { throw e; @@ -192,6 +201,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { /** * Clone a CloudEvent with new/update attributes * @param {object} options attributes to augment the CloudEvent with + * @param {boolean} strict whether or not to use strict validation when cloning (default: true) * @throws if the CloudEvent does not conform to the schema * @return {CloudEvent} returns a new CloudEvent */ @@ -203,7 +213,8 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { | CloudEventV03 | CloudEventV03Attributes | CloudEventV03OptionalAttributes, + strict = true, ): CloudEvent { - return new CloudEvent(Object.assign({}, this.toJSON(), options) as CloudEvent); + return new CloudEvent(Object.assign({}, this.toJSON(), options) as CloudEvent, strict); } } diff --git a/src/event/spec.ts b/src/event/spec.ts index afa75681..537b6432 100644 --- a/src/event/spec.ts +++ b/src/event/spec.ts @@ -3,24 +3,26 @@ import { ValidationError, isBase64 } from "./validation"; import { CloudEventV1, CloudEventV03 } from "./interfaces"; import { schemaV03, schemaV1 } from "./schemas"; +import { Version } from "./cloudevent"; import CONSTANTS from "../constants"; const ajv = new Ajv({ extendRefs: true }); const isValidAgainstSchemaV1 = ajv.compile(schemaV1); const isValidAgainstSchemaV03 = ajv.compile(schemaV03); -export function validateV1(event: CloudEventV1): boolean { - if (!isValidAgainstSchemaV1(event)) { - throw new ValidationError("invalid payload", isValidAgainstSchemaV1.errors); - } - return true; -} - -export function validateV03(event: CloudEventV03): boolean { - if (!isValidAgainstSchemaV03(event)) { - throw new ValidationError("invalid payload", isValidAgainstSchemaV03.errors); +export function validateCloudEvent(event: CloudEventV03 | CloudEventV1): boolean { + if (event.specversion === Version.V1) { + if (!isValidAgainstSchemaV1(event)) { + throw new ValidationError("invalid payload", isValidAgainstSchemaV1.errors); + } + return true; + } else if (event.specversion === Version.V03) { + if (!isValidAgainstSchemaV03(event)) { + throw new ValidationError("invalid payload", isValidAgainstSchemaV03.errors); + } + return checkDataContentEncoding(event); } - return checkDataContentEncoding(event); + return false; } function checkDataContentEncoding(event: CloudEventV03): boolean { diff --git a/src/event/validation.ts b/src/event/validation.ts index 6ca0258d..645df308 100644 --- a/src/event/validation.ts +++ b/src/event/validation.ts @@ -8,7 +8,18 @@ export class ValidationError extends TypeError { errors?: string[] | ErrorObject[] | null; constructor(message: string, errors?: string[] | ErrorObject[] | null) { - super(message); + const messageString = + errors instanceof Array + ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + errors?.reduce( + (accum: string, err: Record) => + (accum as string).concat(` + ${err instanceof Object ? JSON.stringify(err) : err}`), + message, + ) + : message; + super(messageString); this.errors = errors ? errors : []; } } diff --git a/src/index.ts b/src/index.ts index 266d3af9..11149ff8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,11 @@ import { CloudEvent, Version } from "./event/cloudevent"; import { ValidationError } from "./event/validation"; -// import {Version} from './event/' import { CloudEventV03, CloudEventV03Attributes, CloudEventV1, CloudEventV1Attributes } from "./event/interfaces"; import { Emitter, TransportOptions } from "./transport/emitter"; -import { Receiver, Mode } from "./transport/receiver"; +import { Receiver } from "./transport/receiver"; import { Protocol } from "./transport/protocols"; -import { Headers, headersFor } from "./transport/http/headers"; +import { Headers, Mode, Binding, HTTP, Message, Serializer, Deserializer, headersFor } from "./message"; import CONSTANTS from "./constants"; @@ -19,14 +18,20 @@ export { CloudEventV1Attributes, Version, ValidationError, - // From transport - Emitter, - Receiver, - Mode, - Protocol, - TransportOptions, + // From message Headers, - headersFor, + Mode, + Binding, + Message, + Deserializer, + Serializer, + headersFor, // TODO: Deprecated. Remove for 4.0 + HTTP, + // From transport + Emitter, // TODO: Deprecated. Remove for 4.0 + Receiver, // TODO: Deprecated. Remove for 4.0 + Protocol, // TODO: Deprecated. Remove for 4.0 + TransportOptions, // TODO: Deprecated. Remove for 4.0 // From Constants CONSTANTS, }; diff --git a/src/transport/http/versions.ts b/src/message/http/headers.ts similarity index 68% rename from src/transport/http/versions.ts rename to src/message/http/headers.ts index 70ddc560..dcef105c 100644 --- a/src/transport/http/versions.ts +++ b/src/message/http/headers.ts @@ -1,6 +1,73 @@ import { PassThroughParser, DateParser, MappedParser } from "../../parsers"; +import { CloudEvent } from "../.."; +import { Headers } from "../"; +import { Version } from "../../event/cloudevent"; import CONSTANTS from "../../constants"; +export const allowedContentTypes = [CONSTANTS.DEFAULT_CONTENT_TYPE, CONSTANTS.MIME_JSON, CONSTANTS.MIME_OCTET_STREAM]; +export const requiredHeaders = [ + CONSTANTS.CE_HEADERS.ID, + CONSTANTS.CE_HEADERS.SOURCE, + CONSTANTS.CE_HEADERS.TYPE, + CONSTANTS.CE_HEADERS.SPEC_VERSION, +]; + +/** + * Returns the HTTP headers that will be sent for this event when the HTTP transmission + * mode is "binary". Events sent over HTTP in structured mode only have a single CE header + * and that is "ce-id", corresponding to the event ID. + * @param {CloudEvent} event a CloudEvent + * @returns {Object} the headers that will be sent for the event + */ +export function headersFor(event: CloudEvent): Headers { + const headers: Headers = {}; + let headerMap: Readonly<{ [key: string]: MappedParser }>; + if (event.specversion === Version.V1) { + headerMap = v1headerMap; + } else { + headerMap = v03headerMap; + } + + // iterate over the event properties - generate a header for each + Object.getOwnPropertyNames(event).forEach((property) => { + const value = event[property]; + if (value) { + const map: MappedParser | undefined = headerMap[property] as MappedParser; + if (map) { + headers[map.name] = map.parser.parse(value as string) as string; + } else if (property !== CONSTANTS.DATA_ATTRIBUTE && property !== `${CONSTANTS.DATA_ATTRIBUTE}_base64`) { + headers[`${CONSTANTS.EXTENSIONS_PREFIX}${property}`] = value as string; + } + } + }); + // Treat time specially, since it's handled with getters and setters in CloudEvent + if (event.time) { + headers[CONSTANTS.CE_HEADERS.TIME] = event.time as string; + } + return headers; +} + +/** + * Sanitizes incoming headers by lowercasing them and potentially removing + * encoding from the content-type header. + * @param {Headers} headers HTTP headers as key/value pairs + * @returns {Headers} the sanitized headers + */ +export function sanitize(headers: Headers): Headers { + const sanitized: Headers = {}; + + Array.from(Object.keys(headers)) + .filter((header) => Object.hasOwnProperty.call(headers, header)) + .forEach((header) => (sanitized[header.toLowerCase()] = headers[header])); + + // If no content-type header is sent, assume application/json + if (!sanitized[CONSTANTS.HEADER_CONTENT_TYPE]) { + sanitized[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON; + } + + return sanitized; +} + function parser(name: string, parser = new PassThroughParser()): MappedParser { return { name: name, parser: parser }; } diff --git a/src/message/http/index.ts b/src/message/http/index.ts new file mode 100644 index 00000000..492f4916 --- /dev/null +++ b/src/message/http/index.ts @@ -0,0 +1,227 @@ +import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } from "../.."; +import { Message, Headers } from ".."; + +import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers } from "./headers"; +import { asData, isBase64, isString, isStringOrObjectOrThrow, ValidationError } from "../../event/validation"; +import { Base64Parser, JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers"; + +// implements Serializer +export function binary(event: CloudEvent): Message { + const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CONTENT_TYPE }; + const headers: Headers = headersFor(event); + return { + headers: { ...contentType, ...headers }, + body: asData(event.data, event.datacontenttype as string), + }; +} + +// implements Serializer +export function structured(event: CloudEvent): Message { + return { + headers: { + [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CE_CONTENT_TYPE, + }, + body: event.toString(), + }; +} + +// implements Detector +// TODO: this could probably be optimized +export function isEvent(message: Message): boolean { + try { + deserialize(message); + return true; + } catch (err) { + return false; + } +} + +/** + * Converts a Message to a CloudEvent + * + * @param {Message} message the incoming message + * @return {CloudEvent} A new {CloudEvent} instance + */ +export function deserialize(message: Message): CloudEvent { + const cleanHeaders: Headers = sanitize(message.headers); + const mode: Mode = getMode(cleanHeaders); + let version = getVersion(mode, cleanHeaders, message.body); + if (version !== Version.V03 && version !== Version.V1) { + console.error(`Unknown spec version ${version}. Default to ${Version.V1}`); + version = Version.V1; + } + switch (mode) { + case Mode.BINARY: + return parseBinary(message, version); + case Mode.STRUCTURED: + return parseStructured(message, version); + default: + throw new ValidationError("Unknown Message mode"); + } +} + +/** + * Determines the HTTP transport mode (binary or structured) based + * on the incoming HTTP headers. + * @param {Headers} headers the incoming HTTP headers + * @returns {Mode} the transport mode + */ +function getMode(headers: Headers): Mode { + const contentType = headers[CONSTANTS.HEADER_CONTENT_TYPE]; + if (contentType && contentType.startsWith(CONSTANTS.MIME_CE)) { + return Mode.STRUCTURED; + } + if (headers[CONSTANTS.CE_HEADERS.ID]) { + return Mode.BINARY; + } + throw new ValidationError("no cloud event detected"); +} + +/** + * Determines the version of an incoming CloudEvent based on the + * HTTP headers or HTTP body, depending on transport mode. + * @param {Mode} mode the HTTP transport mode + * @param {Headers} headers the incoming HTTP headers + * @param {Record} body the HTTP request body + * @returns {Version} the CloudEvent specification version + */ +function getVersion(mode: Mode, headers: Headers, body: string | Record) { + if (mode === Mode.BINARY) { + // Check the headers for the version + const versionHeader = headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]; + if (versionHeader) { + return versionHeader; + } + } else { + // structured mode - the version is in the body + return typeof body === "string" ? JSON.parse(body).specversion : (body as CloudEvent).specversion; + } + return Version.V1; +} + +/** + * Parses an incoming HTTP Message, converting it to a {CloudEvent} + * instance if it conforms to the Cloud Event specification for this receiver. + * + * @param {Message} message the incoming HTTP Message + * @param {Version} version the spec version of the incoming event + * @returns {CloudEvent} an instance of CloudEvent representing the incoming request + * @throws {ValidationError} of the event does not conform to the spec + */ +function parseBinary(message: Message, version: Version): CloudEvent { + const headers = message.headers; + let body = message.body; + + if (!headers) throw new ValidationError("headers is null or undefined"); + if (body) { + isStringOrObjectOrThrow(body, new ValidationError("payload must be an object or a string")); + } + + if ( + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] && + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] !== Version.V03 && + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] !== Version.V1 + ) { + throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`); + } + + body = isString(body) && isBase64(body) ? Buffer.from(body as string, "base64").toString() : body; + + // Clone and low case all headers names + const sanitizedHeaders = sanitize(headers); + + const eventObj: { [key: string]: unknown | string | Record } = {}; + const parserMap: Record = version === Version.V1 ? v1binaryParsers : v1binaryParsers; + + for (const header in parserMap) { + if (sanitizedHeaders[header]) { + const mappedParser: MappedParser = parserMap[header]; + eventObj[mappedParser.name] = mappedParser.parser.parse(sanitizedHeaders[header]); + delete sanitizedHeaders[header]; + } + } + + let parsedPayload; + + if (body) { + const parser = parserByContentType[eventObj.datacontenttype as string]; + if (!parser) { + throw new ValidationError(`no parser found for content type ${eventObj.datacontenttype}`); + } + parsedPayload = parser.parse(body); + } + + // Every unprocessed header can be an extension + for (const header in sanitizedHeaders) { + if (header.startsWith(CONSTANTS.EXTENSIONS_PREFIX)) { + eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = headers[header]; + } + } + // At this point, if the datacontenttype is application/json and the datacontentencoding is base64 + // then the data has already been decoded as a string, then parsed as JSON. We don't need to have + // the datacontentencoding property set - in fact, it's incorrect to do so. + if (eventObj.datacontenttype === CONSTANTS.MIME_JSON && eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) { + delete eventObj.datacontentencoding; + } + + return new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03, false); +} + +/** + * Creates a new CloudEvent instance based on the provided payload and headers. + * + * @param {Message} message the incoming Message + * @param {Version} version the spec version of this message (v1 or v03) + * @returns {CloudEvent} a new CloudEvent instance for the provided headers and payload + * @throws {ValidationError} if the payload and header combination do not conform to the spec + */ +function parseStructured(message: Message, version: Version): CloudEvent { + const payload = message.body; + const headers = message.headers; + + if (!payload) throw new ValidationError("payload is null or undefined"); + if (!headers) throw new ValidationError("headers is null or undefined"); + isStringOrObjectOrThrow(payload, new ValidationError("payload must be an object or a string")); + + if ( + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] && + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] != Version.V03 && + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] != Version.V1 + ) { + throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`); + } + + // Clone and low case all headers names + const sanitizedHeaders = sanitize(headers); + + const contentType = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]; + const parser: Parser = contentType ? parserByContentType[contentType] : new JSONParser(); + if (!parser) throw new ValidationError(`invalid content type ${sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]}`); + const incoming = { ...(parser.parse(payload) as Record) }; + + const eventObj: { [key: string]: unknown } = {}; + const parserMap: Record = version === Version.V1 ? v1structuredParsers : v03structuredParsers; + + for (const key in parserMap) { + const property = incoming[key]; + if (property) { + const parser: MappedParser = parserMap[key]; + eventObj[parser.name] = parser.parser.parse(property as string); + } + delete incoming[key]; + } + + // extensions are what we have left after processing all other properties + for (const key in incoming) { + eventObj[key] = incoming[key]; + } + + // ensure data content is correctly decoded + if (eventObj.data_base64) { + const parser = new Base64Parser(); + eventObj.data = JSON.parse(parser.parse(eventObj.data_base64 as string)); + delete eventObj.data_base64; + delete eventObj.datacontentencoding; + } + return new CloudEvent(eventObj as CloudEventV1 | CloudEventV03, false); +} diff --git a/src/message/index.ts b/src/message/index.ts new file mode 100644 index 00000000..14ed270c --- /dev/null +++ b/src/message/index.ts @@ -0,0 +1,75 @@ +import { CloudEvent } from ".."; +import { binary, deserialize, structured, isEvent } from "./http"; +import { headersFor } from "./http/headers"; + +/** + * Binding is an interface for transport protocols to implement, + * which provides functions for sending CloudEvent Messages over + * the wire. + */ +export interface Binding { + binary: Serializer; + structured: Serializer; + toEvent: Deserializer; + isEvent: Detector; +} + +/** + * Headers is an interface representing transport-agnostic headers as + * key/value string pairs + */ +export interface Headers { + [key: string]: string; +} + +/** + * Message is an interface representing a CloudEvent as a + * transport-agnostic message + */ +export interface Message { + headers: Headers; + body: string; +} + +/** + * An enum representing the two transport modes, binary and structured + */ +export enum Mode { + BINARY = "binary", + STRUCTURED = "structured", +} + +/** + * Serializer is an interface for functions that can convert a + * CloudEvent into a Message. + */ +export interface Serializer { + (event: CloudEvent): Message; +} + +/** + * Deserializer is a function interface that converts a + * Message to a CloudEvent + */ +export interface Deserializer { + (message: Message): CloudEvent; +} + +/** + * Detector is a function interface that detects whether + * a message contains a valid CloudEvent + */ +export interface Detector { + (message: Message): boolean; +} + +// HTTP Message capabilities +export const HTTP: Binding = { + binary: binary as Serializer, + structured: structured as Serializer, + toEvent: deserialize as Deserializer, + isEvent: isEvent as Detector, +}; + +// TODO: Deprecated. Remove this for 4.0 +export { headersFor }; diff --git a/src/transport/emitter.ts b/src/transport/emitter.ts index 911339e3..ad1b8a7c 100644 --- a/src/transport/emitter.ts +++ b/src/transport/emitter.ts @@ -63,6 +63,7 @@ export class Emitter { * In that case, it will be used as the recipient endpoint. The endpoint can * be overridden by providing a URL here. * @returns {Promise} Promise with an eventual response from the receiver + * @deprecated Will be removed in 4.0.0. Consider using the Message interface with HTTP.[binary|structured](event) */ send(event: CloudEvent, options?: TransportOptions): Promise { options = options || {}; diff --git a/src/transport/http/binary_emitter.ts b/src/transport/http/binary_emitter.ts index 20f51662..cabf9066 100644 --- a/src/transport/http/binary_emitter.ts +++ b/src/transport/http/binary_emitter.ts @@ -2,7 +2,8 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; import { CloudEvent, Version } from "../../event/cloudevent"; import { TransportOptions } from "../emitter"; -import { Headers, headersFor } from "./headers"; +import { Headers } from "../../message"; +import { headersFor } from "../../message/http/headers"; import { asData } from "../../event/validation"; import CONSTANTS from "../../constants"; diff --git a/src/transport/http/binary_receiver.ts b/src/transport/http/binary_receiver.ts deleted file mode 100644 index 9be2def0..00000000 --- a/src/transport/http/binary_receiver.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { CloudEvent, Version } from "../.."; -import { CloudEventV1, CloudEventV03 } from "../../event/interfaces"; -import { validateV1, validateV03 } from "../../event/spec"; -import { Headers, validate } from "./headers"; -import { v03binaryParsers, v1binaryParsers } from "./versions"; -import { parserByContentType, MappedParser } from "../../parsers"; -import { isString, isBase64, ValidationError, isStringOrObjectOrThrow } from "../../event/validation"; -import CONSTANTS from "../../constants"; - -/** - * A class that receives binary CloudEvents over HTTP. This class can be used - * if you know that all incoming events will be using binary transport. If - * events can come as either binary or structured, use {HTTPReceiver}. - */ -export class BinaryHTTPReceiver { - /** - * The specification version of the incoming cloud event - */ - version: Version; - constructor(version: Version = Version.V1) { - this.version = version; - } - - /** - * Parses an incoming HTTP request, converting it to a {CloudEvent} - * instance if it conforms to the Cloud Event specification for this receiver. - * - * @param {Object|string} payload the HTTP request body - * @param {Object} headers the HTTP request headers - * @param {Version} version the spec version of the incoming event - * @returns {CloudEvent} an instance of CloudEvent representing the incoming request - * @throws {ValidationError} of the event does not conform to the spec - */ - parse(payload: string | Record | undefined | null, headers: Headers): CloudEvent { - if (!headers) throw new ValidationError("headers is null or undefined"); - if (payload) { - isStringOrObjectOrThrow(payload, new ValidationError("payload must be an object or a string")); - } - - if ( - headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] && - headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] !== Version.V03 && - headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] !== Version.V1 - ) { - throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`); - } - - payload = isString(payload) && isBase64(payload) ? Buffer.from(payload as string, "base64").toString() : payload; - - // Clone and low case all headers names - const sanitizedHeaders = validate(headers); - - const eventObj: { [key: string]: unknown | string | Record } = {}; - const parserMap: Record = this.version === Version.V1 ? v1binaryParsers : v03binaryParsers; - - for (const header in parserMap) { - if (sanitizedHeaders[header]) { - const mappedParser: MappedParser = parserMap[header]; - eventObj[mappedParser.name] = mappedParser.parser.parse(sanitizedHeaders[header]); - delete sanitizedHeaders[header]; - } - } - - let parsedPayload; - - if (payload) { - const parser = parserByContentType[eventObj.datacontenttype as string]; - if (!parser) { - throw new ValidationError(`no parser found for content type ${eventObj.datacontenttype}`); - } - parsedPayload = parser.parse(payload); - } - - // Every unprocessed header can be an extension - for (const header in sanitizedHeaders) { - if (header.startsWith(CONSTANTS.EXTENSIONS_PREFIX)) { - eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = headers[header]; - } - } - // At this point, if the datacontenttype is application/json and the datacontentencoding is base64 - // then the data has already been decoded as a string, then parsed as JSON. We don't need to have - // the datacontentencoding property set - in fact, it's incorrect to do so. - if ( - eventObj.datacontenttype === CONSTANTS.MIME_JSON && - eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64 - ) { - delete eventObj.datacontentencoding; - } - - const cloudevent = new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03); - this.version === Version.V1 ? validateV1(cloudevent) : validateV03(cloudevent); - return cloudevent; - } -} diff --git a/src/transport/http/headers.ts b/src/transport/http/headers.ts deleted file mode 100644 index 044aef74..00000000 --- a/src/transport/http/headers.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { ValidationError, CloudEvent } from "../.."; -import { v03headerMap, v1headerMap } from "./versions"; -import { Version } from "../../event/cloudevent"; -import { MappedParser } from "../../parsers"; -import CONSTANTS from "../../constants"; - -/** - * An interface representing HTTP headers as key/value string pairs - */ -export interface Headers { - [key: string]: string; -} - -export const allowedContentTypes = [CONSTANTS.DEFAULT_CONTENT_TYPE, CONSTANTS.MIME_JSON, CONSTANTS.MIME_OCTET_STREAM]; -export const requiredHeaders = [ - CONSTANTS.CE_HEADERS.ID, - CONSTANTS.CE_HEADERS.SOURCE, - CONSTANTS.CE_HEADERS.TYPE, - CONSTANTS.CE_HEADERS.SPEC_VERSION, -]; - -/** - * Validates cloud event headers and their values - * @param {Headers} headers event transport headers for validation - * @throws {ValidationError} if the headers are invalid - * @return {boolean} true if headers are valid - */ -export function validate(headers: Headers): Headers { - const sanitizedHeaders = sanitize(headers); - - // if content-type exists, be sure it's an allowed type - const contentTypeHeader = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]; - const noContentType = !allowedContentTypes.includes(contentTypeHeader); - if (contentTypeHeader && noContentType) { - throw new ValidationError("invalid content type", [sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]]); - } - - requiredHeaders - .filter((required: string) => !sanitizedHeaders[required]) - .forEach((required: string) => { - throw new ValidationError(`header '${required}' not found`); - }); - - if (!sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]) { - sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON; - } - - return sanitizedHeaders; -} - -/** - * Returns the HTTP headers that will be sent for this event when the HTTP transmission - * mode is "binary". Events sent over HTTP in structured mode only have a single CE header - * and that is "ce-id", corresponding to the event ID. - * @param {CloudEvent} event a CloudEvent - * @returns {Object} the headers that will be sent for the event - */ -export function headersFor(event: CloudEvent): Headers { - const headers: Headers = {}; - let headerMap: Readonly<{ [key: string]: MappedParser }>; - if (event.specversion === Version.V1) { - headerMap = v1headerMap; - } else { - headerMap = v03headerMap; - } - - // iterate over the event properties - generate a header for each - Object.getOwnPropertyNames(event).forEach((property) => { - const value = event[property]; - if (value) { - const map: MappedParser | undefined = headerMap[property] as MappedParser; - if (map) { - headers[map.name] = map.parser.parse(value as string) as string; - } else if (property !== CONSTANTS.DATA_ATTRIBUTE && property !== `${CONSTANTS.DATA_ATTRIBUTE}_base64`) { - headers[`${CONSTANTS.EXTENSIONS_PREFIX}${property}`] = value as string; - } - } - }); - // Treat time specially, since it's handled with getters and setters in CloudEvent - if (event.time) { - headers[CONSTANTS.CE_HEADERS.TIME] = event.time as string; - } - return headers; -} - -/** - * Sanitizes incoming headers by lowercasing them and potentially removing - * encoding from the content-type header. - * @param {Headers} headers HTTP headers as key/value pairs - * @returns {Headers} the sanitized headers - */ -export function sanitize(headers: Headers): Headers { - const sanitized: Headers = {}; - - Array.from(Object.keys(headers)) - .filter((header) => Object.hasOwnProperty.call(headers, header)) - .forEach((header) => (sanitized[header.toLowerCase()] = headers[header])); - - return sanitized; -} diff --git a/src/transport/http/structured_receiver.ts b/src/transport/http/structured_receiver.ts deleted file mode 100644 index 2450371e..00000000 --- a/src/transport/http/structured_receiver.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { CloudEvent, Version } from "../.."; -import { Headers, sanitize } from "./headers"; -import { Parser, JSONParser, MappedParser, Base64Parser } from "../../parsers"; -import { parserByContentType } from "../../parsers"; -import { v1structuredParsers, v03structuredParsers } from "./versions"; -import { isString, isBase64, ValidationError, isStringOrObjectOrThrow } from "../../event/validation"; -import { CloudEventV1, CloudEventV03 } from "../../event/interfaces"; -import { validateV1, validateV03 } from "../../event/spec"; -import CONSTANTS from "../../constants"; - -/** - * A utility class used to receive structured CloudEvents - * over HTTP. - * @see {StructuredReceiver} - */ -export class StructuredHTTPReceiver { - /** - * The specification version of the incoming cloud event - */ - version: Version; - constructor(version: Version = Version.V1) { - this.version = version; - } - - /** - * Creates a new CloudEvent instance based on the provided payload and headers. - * - * @param {object} payload the cloud event data payload - * @param {object} headers the HTTP headers received for this cloud event - * @returns {CloudEvent} a new CloudEvent instance for the provided headers and payload - * @throws {ValidationError} if the payload and header combination do not conform to the spec - */ - parse(payload: Record | string | undefined | null, headers: Headers): CloudEvent { - if (!payload) throw new ValidationError("payload is null or undefined"); - if (!headers) throw new ValidationError("headers is null or undefined"); - isStringOrObjectOrThrow(payload, new ValidationError("payload must be an object or a string")); - - if ( - headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] && - headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] != Version.V03 && - headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] != Version.V1 - ) { - throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`); - } - - payload = isString(payload) && isBase64(payload) ? Buffer.from(payload as string, "base64").toString() : payload; - - // Clone and low case all headers names - const sanitizedHeaders = sanitize(headers); - - const contentType = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]; - const parser: Parser = contentType ? parserByContentType[contentType] : new JSONParser(); - if (!parser) throw new ValidationError(`invalid content type ${sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]}`); - const incoming = { ...(parser.parse(payload) as Record) }; - - const eventObj: { [key: string]: unknown } = {}; - const parserMap: Record = - this.version === Version.V1 ? v1structuredParsers : v03structuredParsers; - - for (const key in parserMap) { - const property = incoming[key]; - if (property) { - const parser: MappedParser = parserMap[key]; - eventObj[parser.name] = parser.parser.parse(property as string); - } - delete incoming[key]; - } - - // extensions are what we have left after processing all other properties - for (const key in incoming) { - eventObj[key] = incoming[key]; - } - - // ensure data content is correctly encoded - if (eventObj.data && eventObj.datacontentencoding) { - if (eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64 && !isBase64(eventObj.data)) { - throw new ValidationError("invalid payload"); - } else if (eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) { - const dataParser = new Base64Parser(); - eventObj.data = JSON.parse(dataParser.parse(eventObj.data as string)); - delete eventObj.datacontentencoding; - } - } - - const cloudevent = new CloudEvent(eventObj as CloudEventV1 | CloudEventV03); - - // Validates the event - this.version === Version.V1 ? validateV1(cloudevent) : validateV03(cloudevent); - return cloudevent; - } -} diff --git a/src/transport/receiver.ts b/src/transport/receiver.ts index 5c83ec57..f15e8f4c 100644 --- a/src/transport/receiver.ts +++ b/src/transport/receiver.ts @@ -1,33 +1,11 @@ -import { Headers, sanitize } from "./http/headers"; -import { CloudEvent, Version, ValidationError } from ".."; -import { BinaryHTTPReceiver as BinaryReceiver } from "./http/binary_receiver"; -import { StructuredHTTPReceiver as StructuredReceiver } from "./http/structured_receiver"; -import { CloudEventV1, CloudEventV03 } from "../event/interfaces"; -import CONSTANTS from "../constants"; - -/** - * An enum representing the two HTTP transport modes, binary and structured - */ -export enum Mode { - BINARY = "binary", - STRUCTURED = "structured", -} - -const receivers = { - v1: { - structured: new StructuredReceiver(Version.V1), - binary: new BinaryReceiver(Version.V1), - }, - v03: { - structured: new StructuredReceiver(Version.V03), - binary: new BinaryReceiver(Version.V03), - }, -}; +import { Headers, Message, HTTP } from "../message"; +import { sanitize } from "../message/http/headers"; +import { CloudEvent } from ".."; /** * A class to receive a CloudEvent from an HTTP POST request. */ -export class Receiver { +export const Receiver = { /** * Acceptor for an incoming HTTP CloudEvent POST. Can process * binary and structured incoming CloudEvents. @@ -35,65 +13,15 @@ export class Receiver { * @param {Object} headers HTTP headers keyed by header name ("Content-Type") * @param {Object|JSON} body The body of the HTTP request * @return {CloudEvent} A new {CloudEvent} instance + * @deprecated Will be removed in 4.0.0. Consider using the Message interface with HTTP.toEvent(message) */ - static accept( - headers: Headers, - body: string | Record | CloudEventV1 | CloudEventV03 | undefined | null, - ): CloudEvent { + accept(headers: Headers, body: string | Record | undefined | null): CloudEvent { const cleanHeaders: Headers = sanitize(headers); - const mode: Mode = getMode(cleanHeaders); - const version = getVersion(mode, cleanHeaders, body); - switch (version) { - case Version.V1: - return receivers.v1[mode].parse(body, headers); - case Version.V03: - return receivers.v03[mode].parse(body, headers); - default: - console.error(`Unknown spec version ${version}. Default to ${Version.V1}`); - return receivers.v1[mode].parse(body, headers); - } - } -} - -/** - * Determines the HTTP transport mode (binary or structured) based - * on the incoming HTTP headers. - * @param {Headers} headers the incoming HTTP headers - * @returns {Mode} the transport mode - */ -function getMode(headers: Headers): Mode { - const contentType = headers[CONSTANTS.HEADER_CONTENT_TYPE]; - if (contentType && contentType.startsWith(CONSTANTS.MIME_CE)) { - return Mode.STRUCTURED; - } - if (headers[CONSTANTS.CE_HEADERS.ID]) { - return Mode.BINARY; - } - throw new ValidationError("no cloud event detected"); -} - -/** - * Determines the version of an incoming CloudEvent based on the - * HTTP headers or HTTP body, depending on transport mode. - * @param {Mode} mode the HTTP transport mode - * @param {Headers} headers the incoming HTTP headers - * @param {Record} body the HTTP request body - * @returns {Version} the CloudEvent specification version - */ -function getVersion( - mode: Mode, - headers: Headers, - body: string | Record | CloudEventV03 | CloudEventV1 | undefined | null, -) { - if (mode === Mode.BINARY) { - // Check the headers for the version - const versionHeader = headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]; - if (versionHeader) { - return versionHeader; - } - } else { - // structured mode - the version is in the body - return typeof body === "string" ? JSON.parse(body).specversion : (body as CloudEvent).specversion; - } - return Version.V1; -} + const cleanBody = body ? (typeof body === "object" ? JSON.stringify(body) : body) : ""; + const message: Message = { + headers: cleanHeaders, + body: cleanBody, + }; + return HTTP.toEvent(message); + }, +}; diff --git a/test/conformance/steps.ts b/test/conformance/steps.ts index f6e57bc6..8b3377de 100644 --- a/test/conformance/steps.ts +++ b/test/conformance/steps.ts @@ -1,8 +1,7 @@ -/* eslint-disable no-console */ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { assert } from "chai"; import { Given, When, Then, World } from "cucumber"; -import { Receiver } from "../../src"; +import { Message, Headers, HTTP } from "../../src"; // eslint-disable-next-line @typescript-eslint/no-var-requires const { HTTPParser } = require("http-parser-js"); @@ -14,20 +13,24 @@ Given("HTTP Protocol Binding is supported", function (this: World) { }); Given("an HTTP request", function (request: string) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const world = this; + // Create a Message from the incoming HTTP request + const message: Message = { + headers: {}, + body: "", + }; parser.onHeadersComplete = function (record: Record) { - world.headers = arrayToObject(record.headers); + message.headers = extractHeaders(record.headers); }; parser.onBody = function (body: Buffer, offset: number) { - world.body = body.slice(offset).toString(); + message.body = body.slice(offset).toString(); }; + this.message = message; parser.execute(Buffer.from(request), 0, request.length); return true; }); When("parsed as HTTP request", function () { - this.cloudevent = Receiver.accept(this.headers, this.body); + this.cloudevent = HTTP.toEvent(this.message); return true; }); @@ -47,8 +50,8 @@ Then("the data is equal to the following JSON:", function (json: string) { return true; }); -function arrayToObject(arr: []): Record { - const obj: Record = {}; +function extractHeaders(arr: []): Headers { + const obj: Headers = {}; // @ts-ignore return arr.reduce(({}, keyOrValue, index, arr) => { if (index % 2 === 0) { diff --git a/test/integration/cloud_event_test.ts b/test/integration/cloud_event_test.ts index de733c66..5b4ba134 100644 --- a/test/integration/cloud_event_test.ts +++ b/test/integration/cloud_event_test.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { CloudEvent, Version } from "../../src"; +import { CloudEvent, ValidationError, Version } from "../../src"; import { CloudEventV03, CloudEventV1 } from "../../src/event/interfaces"; const type = "org.cncf.cloudevents.example"; @@ -11,6 +11,7 @@ const fixture: CloudEventV1 = { specversion: Version.V1, source, type, + data: `"some data"`, }; describe("A CloudEvent", () => { @@ -20,6 +21,21 @@ describe("A CloudEvent", () => { expect(ce.source).to.equal(source); }); + it("Can be constructed with loose validation", () => { + const ce = new CloudEvent({} as CloudEventV1, false); + expect(ce).to.be.instanceOf(CloudEvent); + }); + + it("Loosely validated events can be cloned", () => { + const ce = new CloudEvent({} as CloudEventV1, false); + expect(ce.cloneWith({}, false)).to.be.instanceOf(CloudEvent); + }); + + it("Loosely validated events throw when validated", () => { + const ce = new CloudEvent({} as CloudEventV1, false); + expect(ce.validate).to.throw(ValidationError, "invalid payload"); + }); + it("serializes as JSON with toString()", () => { const ce = new CloudEvent(fixture); expect(ce.toString()).to.deep.equal(JSON.stringify(ce)); @@ -144,9 +160,26 @@ describe("A 1.0 CloudEvent", () => { expect(ce["extensionkey"]).to.equal(extensions["extensionkey"]); }); - 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"); + it("throws TypeError if the CloudEvent does not conform to the schema", () => { + try { + new CloudEvent({ + ...fixture, + source: (null as unknown) as string, + }); + } catch (err) { + expect(err).to.be.instanceOf(TypeError); + expect(err.message).to.include("invalid payload"); + } + }); + + it("correctly formats a CloudEvent as JSON", () => { + const ce = new CloudEvent({ ...fixture }); + const json = ce.toString(); + const obj = JSON.parse((json as unknown) as string); + expect(obj.type).to.equal(type); + expect(obj.source).to.equal(source); + expect(obj.specversion).to.equal(Version.V1); + }); }); describe("A 0.3 CloudEvent", () => { @@ -211,7 +244,24 @@ describe("A 0.3 CloudEvent", () => { 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"); + it("throws TypeError if the CloudEvent does not conform to the schema", () => { + try { + new CloudEvent({ + ...v03fixture, + source: (null as unknown) as string, + }); + } catch (err) { + expect(err).to.be.instanceOf(ValidationError); + expect(err.message).to.include("invalid payload"); + } + }); + + it("correctly formats a CloudEvent as JSON", () => { + const ce = new CloudEvent({ ...v03fixture }); + const json = ce.toString(); + const obj = JSON.parse((json as unknown) as string); + expect(obj.type).to.equal(type); + expect(obj.source).to.equal(source); + expect(obj.specversion).to.equal(Version.V03); + }); }); diff --git a/test/integration/http_emitter_test.ts b/test/integration/http_emitter_test.ts index 3c8a2034..e9f184bc 100644 --- a/test/integration/http_emitter_test.ts +++ b/test/integration/http_emitter_test.ts @@ -6,7 +6,8 @@ import CONSTANTS from "../../src/constants"; const DEFAULT_CE_CONTENT_TYPE = CONSTANTS.DEFAULT_CE_CONTENT_TYPE; const DEFAULT_CONTENT_TYPE = CONSTANTS.DEFAULT_CONTENT_TYPE; -import { CloudEvent, Version, Emitter, Protocol, headersFor } from "../../src"; +import { CloudEvent, Version, Emitter, Protocol } from "../../src"; +import { headersFor } from "../../src/message/http/headers"; import { AxiosResponse } from "axios"; const receiver = "https://cloudevents.io/"; diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts new file mode 100644 index 00000000..55f0c04c --- /dev/null +++ b/test/integration/message_test.ts @@ -0,0 +1,239 @@ +import { expect } from "chai"; +import { CloudEvent, CONSTANTS, Version } from "../../src"; +import { asBase64 } from "../../src/event/validation"; +import { Message, HTTP } from "../../src/message"; + +const type = "org.cncf.cloudevents.example"; +const source = "urn:event:from:myapi/resource/123"; +const time = new Date(); +const subject = "subject.ext"; +const dataschema = "http://cloudevents.io/schema.json"; +const datacontenttype = "application/json"; +const id = "b46cf653-d48a-4b90-8dfa-355c01061361"; +const data = { + foo: "bar", +}; + +// Attributes for v03 events +const schemaurl = "https://cloudevents.io/schema.json"; +const datacontentencoding = "base64"; + +const ext1Name = "extension1"; +const ext1Value = "foobar"; +const ext2Name = "extension2"; +const ext2Value = "acme"; + +// Binary data as base64 +const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number); +const data_base64 = asBase64(dataBinary); + +describe("HTTP transport", () => { + it("Can detect invalid CloudEvent Messages", () => { + // Create a message that is not an actual event + const message: Message = { + body: "Hello world!", + headers: { + "Content-type": "text/plain", + }, + }; + expect(HTTP.isEvent(message)).to.be.false; + }); + + it("Can detect valid CloudEvent Messages", () => { + // Now create a message that is an event + const message = HTTP.binary( + new CloudEvent({ + source: "/message-test", + type: "example", + }), + ); + expect(HTTP.isEvent(message)).to.be.true; + }); + + // Allow for external systems to send bad events - do what we can + // to accept them + it("Does not throw an exception when converting an invalid Message to a CloudEvent", () => { + const message: Message = { + body: `"hello world"`, + headers: { + "content-type": "application/json", + "ce-id": "1234", + "ce-type": "example.bad.event", + "ce-specversion": "1.0", + // no required ce-source header, thus an invalid event + }, + }; + const event = HTTP.toEvent(message); + expect(event).to.be.instanceOf(CloudEvent); + // ensure that we actually now have an invalid event + expect(event.validate).to.throw; + }); + + it("Does not allow an invalid CloudEvent to be converted to a Message", () => { + const badEvent = new CloudEvent( + { + source: "/example.source", + type: "", // type is required, empty string will throw with strict validation + }, + false, // turn off strict validation + ); + expect(() => { + HTTP.binary(badEvent); + }).to.throw; + expect(() => { + HTTP.structured(badEvent); + }).to.throw; + }); + + describe("Specification version V1", () => { + const fixture: CloudEvent = new CloudEvent({ + specversion: Version.V1, + id, + type, + source, + datacontenttype, + subject, + time, + dataschema, + data, + [ext1Name]: ext1Value, + [ext2Name]: ext2Value, + }); + + it("Binary Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.binary(fixture); + expect(message.body).to.equal(data); + // validate all headers + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype); + expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V1); + expect(message.headers[CONSTANTS.CE_HEADERS.ID]).to.equal(id); + expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(type); + expect(message.headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(source); + expect(message.headers[CONSTANTS.CE_HEADERS.SUBJECT]).to.equal(subject); + expect(message.headers[CONSTANTS.CE_HEADERS.TIME]).to.equal(fixture.time); + expect(message.headers[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]).to.equal(dataschema); + expect(message.headers[`ce-${ext1Name}`]).to.equal(ext1Value); + expect(message.headers[`ce-${ext2Name}`]).to.equal(ext2Value); + }); + + it("Structured Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.structured(fixture); + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); + // Parse the message body as JSON, then validate the attributes + const body = JSON.parse(message.body); + expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V1); + expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); + expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); + expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); + expect(body[CONSTANTS.CE_ATTRIBUTES.SUBJECT]).to.equal(subject); + expect(body[CONSTANTS.CE_ATTRIBUTES.TIME]).to.equal(fixture.time); + expect(body[CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA]).to.equal(dataschema); + expect(body[ext1Name]).to.equal(ext1Value); + expect(body[ext2Name]).to.equal(ext2Value); + }); + + it("A CloudEvent can be converted from a binary Message", () => { + const message = HTTP.binary(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); + + it("A CloudEvent can be converted from a structured Message", () => { + const message = HTTP.structured(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); + + it("Supports Base-64 encoded data in structured messages", () => { + const event = fixture.cloneWith({ data: dataBinary }); + expect(event.data_base64).to.equal(data_base64); + const message = HTTP.structured(event); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); + + it("Supports Base-64 encoded data in binary messages", () => { + const event = fixture.cloneWith({ data: dataBinary }); + expect(event.data_base64).to.equal(data_base64); + const message = HTTP.binary(event); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); + }); + + describe("Specification version V03", () => { + const fixture: CloudEvent = new CloudEvent({ + specversion: Version.V03, + id, + type, + source, + datacontenttype, + subject, + time, + schemaurl, + data, + [ext1Name]: ext1Value, + [ext2Name]: ext2Value, + }); + + it("Binary Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.binary(fixture); + expect(message.body).to.equal(data); + // validate all headers + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype); + expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V03); + expect(message.headers[CONSTANTS.CE_HEADERS.ID]).to.equal(id); + expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(type); + expect(message.headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(source); + expect(message.headers[CONSTANTS.CE_HEADERS.SUBJECT]).to.equal(subject); + expect(message.headers[CONSTANTS.CE_HEADERS.TIME]).to.equal(fixture.time); + expect(message.headers[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]).to.equal(schemaurl); + expect(message.headers[`ce-${ext1Name}`]).to.equal(ext1Value); + expect(message.headers[`ce-${ext2Name}`]).to.equal(ext2Value); + }); + + it("Structured Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.structured(fixture); + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); + // Parse the message body as JSON, then validate the attributes + const body = JSON.parse(message.body); + expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V03); + expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); + expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); + expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); + expect(body[CONSTANTS.CE_ATTRIBUTES.SUBJECT]).to.equal(subject); + expect(body[CONSTANTS.CE_ATTRIBUTES.TIME]).to.equal(fixture.time); + expect(body[CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL]).to.equal(schemaurl); + expect(body[ext1Name]).to.equal(ext1Value); + expect(body[ext2Name]).to.equal(ext2Value); + }); + + it("A CloudEvent can be converted from a binary Message", () => { + const message = HTTP.binary(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); + + it("A CloudEvent can be converted from a structured Message", () => { + const message = HTTP.structured(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); + + it("Supports Base-64 encoded data in structured messages", () => { + const event = fixture.cloneWith({ data: dataBinary, datacontentencoding }); + expect(event.data_base64).to.equal(data_base64); + const message = HTTP.structured(event); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); + + it("Supports Base-64 encoded data in binary messages", () => { + const event = fixture.cloneWith({ data: dataBinary, datacontentencoding }); + expect(event.data_base64).to.equal(data_base64); + const message = HTTP.binary(event); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); + }); +}); diff --git a/test/integration/receiver_binary_03_tests.ts b/test/integration/receiver_binary_03_tests.ts deleted file mode 100644 index dfdfd736..00000000 --- a/test/integration/receiver_binary_03_tests.ts +++ /dev/null @@ -1,443 +0,0 @@ -import "mocha"; -import { expect } from "chai"; - -import { CloudEvent, ValidationError, Version } from "../../src"; -import { BinaryHTTPReceiver } from "../../src/transport/http/binary_receiver"; -import CONSTANTS from "../../src/constants"; -import { asBase64 } from "../../src/event/validation"; - -const receiver = new BinaryHTTPReceiver(Version.V03); - -describe("HTTP Transport Binding Binary Receiver for CloudEvents v0.3", () => { - describe("Check", () => { - it("Throw error when attributes arg is null or undefined", () => { - // setup - const payload = {}; - const attributes = undefined; - - expect(receiver.parse.bind(receiver, payload, (attributes as unknown) as string)).to.throw( - ValidationError, - "headers is null or undefined", - ); - }); - - it("Throw error when payload is not an object or string", () => { - // setup - const payload = 1.2; - const attributes = {}; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "payload must be an object or a string", - ); - }); - - it("Throw error when headers has no 'ce-type'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "header 'ce-type' not found", - ); - }); - - it("Throw error when headers has no 'ce-specversion'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "header 'ce-specversion' not found", - ); - }); - - it("Throw error when headers has no 'ce-source'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "header 'ce-source' not found", - ); - }); - - it("Throw error when headers has no 'ce-id'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "header 'ce-id' not found"); - }); - - it("Throw error when spec is not 0.3", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: "0.2", - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid spec version"); - }); - - it("Throw error when the content-type is invalid", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "text/html", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid content type"); - }); - - it("No error when all required headers are in place", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw(); - }); - - it("No error when content-type is unspecified", () => { - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw(); - }); - - it("Succeeds when content-type is application/json and datacontentencoding is base64", () => { - const expected = { - whose: "ours", - }; - const bindata = Uint32Array.from(JSON.stringify(expected) as string, (c) => c.codePointAt(0) as number); - const payload = asBase64(bindata); - - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "test", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/test-source", - [CONSTANTS.CE_HEADERS.ID]: "123456", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - [CONSTANTS.BINARY_HEADERS_03.CONTENT_ENCODING]: "base64", - }; - const event = receiver.parse(payload, attributes); - expect(event.data).to.deep.equal(expected); - }); - }); - - describe("Parse", () => { - it("CloudEvent contains 'type'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.type).to.equal("type"); - }); - - it("CloudEvent contains 'specversion'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.specversion).to.equal(Version.V03); - }); - - it("CloudEvent contains 'source'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.source).to.equal("/source"); - }); - - it("CloudEvent contains 'id'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.id).to.equal("id"); - }); - - it("CloudEvent contains 'time'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.time).to.equal("2019-06-16T11:42:00.000Z"); - }); - - it("CloudEvent contains 'schemaurl'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.schemaurl).to.equal("http://schema.registry/v1"); - }); - - it("CloudEvent contains 'datacontenttype' (application/json)", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.datacontenttype).to.equal("application/json"); - }); - - it("CloudEvent contains 'datacontenttype' (application/octet-stream)", () => { - // setup - const payload = "The payload is binary data"; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/octet-stream", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.datacontenttype).to.equal("application/octet-stream"); - }); - - it("CloudEvent contains 'data' (application/json)", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.data).to.deep.equal(payload); - }); - - it("CloudEvent contains 'data' (application/octet-stream)", () => { - // setup - const payload = "The payload is binary data"; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/octet-stream", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.data).to.deep.equal(payload); - }); - - it("No error when all attributes are in place", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual).to.be.an.instanceof(CloudEvent); - }); - - it("Should accept 'extension1'", () => { - // setup - const extension1 = "mycuston-ext1"; - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V03, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - [`${[CONSTANTS.EXTENSIONS_PREFIX]}extension1`]: extension1, - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.extension1).to.equal(extension1); - }); - }); -}); diff --git a/test/integration/receiver_binary_1_tests.ts b/test/integration/receiver_binary_1_tests.ts deleted file mode 100644 index 368c55ed..00000000 --- a/test/integration/receiver_binary_1_tests.ts +++ /dev/null @@ -1,448 +0,0 @@ -import "mocha"; -import { expect } from "chai"; - -import { CloudEvent, ValidationError, Version } from "../../src"; -import { asBase64 } from "../../src/event/validation"; -import { BinaryHTTPReceiver } from "../../src/transport/http/binary_receiver"; -import CONSTANTS from "../../src/constants"; - -const receiver = new BinaryHTTPReceiver(Version.V1); - -describe("HTTP Transport Binding Binary Receiver for CloudEvents v1.0", () => { - describe("Check", () => { - it("Throw error when attributes arg is null or undefined", () => { - // setup - const payload = {}; - const attributes = undefined; - - expect(receiver.parse.bind(receiver, payload, (attributes as unknown) as string)).to.throw( - ValidationError, - "headers is null or undefined", - ); - }); - - it("Throw error when payload is not an object or string", () => { - // setup - const payload = 1.2; - const attributes = {}; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "payload must be an object or a string", - ); - }); - - it("Throw error when headers has no 'ce-type'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "header 'ce-type' not found", - ); - }); - - it("Throw error when headers has no 'ce-specversion'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "header 'ce-specversion' not found", - ); - }); - - it("Throw error when headers has no 'ce-source'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "header 'ce-source' not found", - ); - }); - - it("Throw error when headers has no 'ce-id'", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "header 'ce-id' not found"); - }); - - it("Throw error when spec is not 1.0", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: "0.2", - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid spec version"); - }); - - it("Throw error when the content-type is invalid", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "text/html", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid content type"); - }); - - it("No error when content-type is unspecified", () => { - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw(); - }); - - it("No error when all required headers are in place", () => { - // setup - const payload = {}; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw(); - }); - }); - - describe("Parse", () => { - it("CloudEvent contains 'type'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.type).to.equal("type"); - }); - - it("CloudEvent contains 'specversion'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.specversion).to.equal(Version.V1); - }); - - it("CloudEvent contains 'source'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.source).to.equal("/source"); - }); - - it("CloudEvent contains 'id'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.id).to.equal("id"); - }); - - it("CloudEvent contains 'time'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.time).to.equal("2019-06-16T11:42:00.000Z"); - }); - - it("CloudEvent contains 'dataschema'", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.dataschema).to.equal("http://schema.registry/v1"); - }); - - it("CloudEvent contains 'contenttype' (application/json)", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.datacontenttype).to.equal("application/json"); - }); - - it("CloudEvent contains 'contenttype' (application/octet-stream)", () => { - // setup - const payload = "The payload is binary data"; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/octet-stream", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.datacontenttype).to.equal("application/octet-stream"); - }); - - it("CloudEvent contains 'data' (application/json)", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.data).to.deep.equal(payload); - }); - - it("CloudEvent contains 'data' (application/octet-stream)", () => { - // setup - const payload = "The payload is binary data"; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/octet-stream", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.data).to.deep.equal(payload); - }); - - it("The content of 'data' is base64 for binary", () => { - // setup - const expected = { - data: "dataString", - }; - const bindata = Uint32Array.from(JSON.stringify(expected) as string, (c) => c.codePointAt(0) as number); - const payload = asBase64(bindata); - - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "/source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.data).to.deep.equal(expected); - }); - - it("No error when all attributes are in place", () => { - // setup - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual).to.be.an.instanceof(CloudEvent); - }); - - it("Should accept 'extension1'", () => { - // setup - const extension1 = "mycustom-ext1"; - const payload = { - data: "dataString", - }; - const attributes = { - [CONSTANTS.CE_HEADERS.TYPE]: "type", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, - [CONSTANTS.CE_HEADERS.SOURCE]: "source", - [CONSTANTS.CE_HEADERS.ID]: "id", - [CONSTANTS.CE_HEADERS.TIME]: "2019-06-16T11:42:00Z", - [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: "http://schema.registry/v1", - [CONSTANTS.HEADER_CONTENT_TYPE]: "application/json", - [`${[CONSTANTS.EXTENSIONS_PREFIX]}extension1`]: extension1, - }; - - // act - const actual = receiver.parse(payload, attributes); - - // assert - expect(actual.extension1).to.equal(extension1); - }); - }); -}); diff --git a/test/integration/receiver_structured_0_3_test.ts b/test/integration/receiver_structured_0_3_test.ts deleted file mode 100644 index d47220bf..00000000 --- a/test/integration/receiver_structured_0_3_test.ts +++ /dev/null @@ -1,227 +0,0 @@ -import "mocha"; -import { expect } from "chai"; - -import { CloudEvent, ValidationError, Version } from "../../src"; -import { StructuredHTTPReceiver } from "../../src/transport/http/structured_receiver"; -import { asBase64 } from "../../src/event/validation"; -import CONSTANTS from "../../src/constants"; - -const receiver = new StructuredHTTPReceiver(Version.V03); -const type = "com.github.pull.create"; -const source = "urn:event:from:myapi/resourse/123"; -const time = new Date(); -const schemaurl = "http://cloudevents.io/schema.json"; - -const ceContentType = "application/json"; - -const data = { - foo: "bar", -}; - -describe("HTTP Transport Binding Structured Receiver CloudEvents v0.3", () => { - describe("Check", () => { - it("Throw error when payload arg is null or undefined", () => { - // setup - const payload = null; - const attributes = {}; - - // act and assert - expect(receiver.parse.bind(receiver, (payload as unknown) as string, attributes)).to.throw( - ValidationError, - "payload is null or undefined", - ); - }); - - it("Throw error when attributes arg is null or undefined", () => { - // setup - const payload = {}; - const attributes = null; - - expect(receiver.parse.bind(receiver, payload, (attributes as unknown) as string)).to.throw( - ValidationError, - "headers is null or undefined", - ); - }); - - it("Throw error when payload is not an object or string", () => { - // setup - const payload = 1.0; - const attributes = {}; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "payload must be an object or a string", - ); - }); - - it("Throw error when the content-type is invalid", () => { - // setup - const payload = {}; - const attributes = { - "Content-Type": "text/html", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid content type"); - }); - - it("Throw error data content encoding is base64, but 'data' is not", () => { - // setup - const event = { - specversion: Version.V03, - type, - source, - time, - datacontenttype: "text/plain", - datacontentencoding: "base64", - schemaurl, - data: "No base 64 value", - }; - - const attributes = { - "Content-Type": "application/cloudevents+json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, event, attributes)).to.throw(ValidationError, "invalid payload"); - }); - - it("Succeeds when content-type is application/cloudevents+json and datacontentencoding is base64", () => { - const expected = { - whose: "ours", - }; - const bindata = Uint32Array.from(JSON.stringify(expected) as string, (c) => c.codePointAt(0) as number); - const payload = { - data: asBase64(bindata), - specversion: Version.V03, - source, - type, - datacontentencoding: CONSTANTS.ENCODING_BASE64, - }; - const attributes = { - "Content-Type": "application/cloudevents+json", - }; - - const event = receiver.parse(payload, attributes); - expect(event.data).to.deep.equal(expected); - }); - - it("No error when all required stuff are in place", () => { - // setup - const payload = { - specversion: Version.V03, - source, - type, - }; - const attributes = { - "Content-Type": "application/cloudevents+json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw(); - }); - }); - - describe("Parse", () => { - it("Throw error when the event does not follow the spec", () => { - // setup - const payload = {}; - const attributes = { - "Content-Type": "application/cloudevents+json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(); - }); - }); - - describe("Parse", () => { - it("Throw error when the event does not follow the spec", () => { - const payload = { - type, - source, - time, - schemaurl, - data, - }; - - const headers = { - "Content-Type": "application/cloudevents+xml", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, headers)).to.throw(ValidationError, "invalid content type"); - }); - - it("Should accept event that follows the spec", () => { - // setup - const id = "id-x0dk"; - const payload = { - specversion: Version.V03, - id, - type, - source, - time, - schemaurl, - datacontenttype: ceContentType, - data, - }; - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(payload, headers); - - // assert - expect(actual).to.be.an.instanceof(CloudEvent); - expect(actual.id).to.equal(id); - }); - - it("Should accept 'extension1'", () => { - // setup - const extension1 = "mycuston-ext1"; - const payload = { - specversion: Version.V03, - type, - source, - time, - schemaurl, - data, - datacontenttype: ceContentType, - extension1: extension1, - }; - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(payload, headers); - - // assert - expect(actual.extension1).to.equal(extension1); - }); - - it("Should parse 'data' stringfied json to json object", () => { - const payload = { - specversion: Version.V03, - type, - source, - time, - schemaurl, - datacontenttype: ceContentType, - data: JSON.stringify(data), - }; - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(payload, headers); - - // assert - expect(actual.data).to.deep.equal(data); - }); - }); -}); diff --git a/test/integration/receiver_structured_1_test.ts b/test/integration/receiver_structured_1_test.ts deleted file mode 100644 index 250dab9c..00000000 --- a/test/integration/receiver_structured_1_test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import "mocha"; -import { expect } from "chai"; - -import { CloudEvent, ValidationError, Version } from "../../src"; -import { asBase64 } from "../../src/event/validation"; -import { StructuredHTTPReceiver } from "../../src/transport/http/structured_receiver"; - -const receiver = new StructuredHTTPReceiver(Version.V1); -const type = "com.github.pull.create"; -const source = "urn:event:from:myapi/resourse/123"; -const time = new Date(); -const dataschema = "http://cloudevents.io/schema.json"; - -const data = { - foo: "bar", -}; - -describe("HTTP Transport Binding Structured Receiver for CloudEvents v1.0", () => { - describe("Check", () => { - it("Throw error when payload arg is null or undefined", () => { - // setup - const payload = null; - const attributes = {}; - - // act and assert - expect(receiver.parse.bind(receiver, (payload as unknown) as string, attributes)).to.throw( - ValidationError, - "payload is null or undefined", - ); - }); - - it("Throw error when attributes arg is null or undefined", () => { - // setup - const payload = {}; - const attributes = null; - - expect(receiver.parse.bind(receiver, payload, (attributes as unknown) as string)).to.throw( - ValidationError, - "headers is null or undefined", - ); - }); - - it("Throw error when payload is not an object or string", () => { - // setup - const payload = 1.0; - const attributes = {}; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw( - ValidationError, - "payload must be an object or a string", - ); - }); - - it("Throw error when the content-type is invalid", () => { - // setup - const payload = {}; - const attributes = { - "Content-Type": "text/html", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.throw(ValidationError, "invalid content type"); - }); - - it("No error when all required stuff are in place", () => { - // setup - const payload = { - source, - type, - }; - const attributes = { - "Content-Type": "application/cloudevents+json", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, attributes)).to.not.throw(); - }); - }); - - describe("Parse", () => { - it("Throw error when the event does not follow the spec", () => { - // setup - const payload = { - type, - source, - time, - data, - }; - - const headers = { - "Content-Type": "application/cloudevents+xml", - }; - - // act and assert - expect(receiver.parse.bind(receiver, payload, headers)).to.throw(ValidationError, "invalid content type"); - }); - - it("Should accept event that follows the spec", () => { - // setup - const id = "id-x0dk"; - const payload = { - id, - type, - source, - time, - data, - dataschema, - }; - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(payload, headers); - - // assert - expect(actual).to.be.an.instanceof(CloudEvent); - expect(actual.id).to.equal(id); - }); - - it("Should accept 'extension1'", () => { - // setup - const extension1 = "mycustomext1"; - const event = { - type, - source, - time, - data, - dataschema, - extension1, - }; - - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(event, headers); - expect(actual.extension1).to.equal(extension1); - }); - - it("Should parse 'data' stringified json to json object", () => { - // setup - const payload = { - type, - source, - time, - dataschema, - data: data, - }; - - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(payload, headers); - - // assert - expect(actual.data).to.deep.equal(data); - }); - - it("Should maps 'data_base64' to 'data' attribute", () => { - const bindata = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number); - const expected = asBase64(bindata); - const payload = { - type, - source, - data: bindata, - }; - - const headers = { - "content-type": "application/cloudevents+json", - }; - - // act - const actual = receiver.parse(payload, headers); - expect(actual.data_base64).to.equal(expected); - }); - }); -}); diff --git a/test/integration/sdk_test.ts b/test/integration/sdk_test.ts index a949ed0a..d0aa3d1f 100644 --- a/test/integration/sdk_test.ts +++ b/test/integration/sdk_test.ts @@ -1,6 +1,6 @@ import "mocha"; import { expect } from "chai"; -import { CloudEvent, Receiver, Emitter, Version } from "../../src"; +import { CloudEvent, Emitter, Version } from "../../src"; const fixture = { type: "org.cloudevents.test", @@ -13,11 +13,6 @@ describe("The SDK Requirements", () => { expect(event instanceof CloudEvent).to.equal(true); }); - it("should expose a Receiver type", () => { - const receiver = new Receiver(); - expect(receiver instanceof Receiver).to.equal(true); - }); - it("should expose an Emitter type", () => { const emitter = new Emitter({ url: "http://example.com",