From 1d7886c50aa2c4546e116ae4f8e354ac7425ccd9 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Tue, 11 Aug 2020 10:15:12 -0400 Subject: [PATCH 01/14] docs: add ref to CoC and other things (#244) * add ref to CoC and other things Signed-off-by: Doug Davis --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b2b763f..7d56cda4 100644 --- a/README.md +++ b/README.md @@ -153,5 +153,13 @@ We love contributions from the community! Please check the [Contributor's Guide](https://github.com/cloudevents/sdk-javascript/blob/master/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 ave 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. From d78d101349c1137ee17f92245b2e250628e7cb18 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Tue, 11 Aug 2020 11:01:40 -0400 Subject: [PATCH 02/14] chore: typo (#313) Signed-off-by: Doug Davis --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d56cda4..8595a404 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ We love contributions from the community! Please check the [Contributor's Guide](https://github.com/cloudevents/sdk-javascript/blob/master/CONTRIBUTING.md) for information on how to get involved. -Each SDK may ave its own unique processes, tooling and guidelines, common +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 From 3d2f01a7e387c6519fce6e74d98c004e36f83e3d Mon Sep 17 00:00:00 2001 From: Grant Timmerman Date: Tue, 11 Aug 2020 10:08:55 -0500 Subject: [PATCH 03/14] refactor: validate cloudevent version agnostic (#311) Signed-off-by: Grant Timmerman --- src/event/cloudevent.ts | 9 ++------- src/event/spec.ts | 24 ++++++++++++----------- src/transport/http/binary_receiver.ts | 4 ++-- src/transport/http/structured_receiver.ts | 4 ++-- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/event/cloudevent.ts b/src/event/cloudevent.ts index 0cab0dad..2cb571a7 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"; @@ -174,12 +174,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; 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/transport/http/binary_receiver.ts b/src/transport/http/binary_receiver.ts index 9be2def0..aa027f2b 100644 --- a/src/transport/http/binary_receiver.ts +++ b/src/transport/http/binary_receiver.ts @@ -1,6 +1,6 @@ import { CloudEvent, Version } from "../.."; import { CloudEventV1, CloudEventV03 } from "../../event/interfaces"; -import { validateV1, validateV03 } from "../../event/spec"; +import { validateCloudEvent } from "../../event/spec"; import { Headers, validate } from "./headers"; import { v03binaryParsers, v1binaryParsers } from "./versions"; import { parserByContentType, MappedParser } from "../../parsers"; @@ -88,7 +88,7 @@ export class BinaryHTTPReceiver { } const cloudevent = new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03); - this.version === Version.V1 ? validateV1(cloudevent) : validateV03(cloudevent); + validateCloudEvent(cloudevent); return cloudevent; } } diff --git a/src/transport/http/structured_receiver.ts b/src/transport/http/structured_receiver.ts index 2450371e..ef9b3a34 100644 --- a/src/transport/http/structured_receiver.ts +++ b/src/transport/http/structured_receiver.ts @@ -5,7 +5,7 @@ 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 { validateCloudEvent } from "../../event/spec"; import CONSTANTS from "../../constants"; /** @@ -85,7 +85,7 @@ export class StructuredHTTPReceiver { const cloudevent = new CloudEvent(eventObj as CloudEventV1 | CloudEventV03); // Validates the event - this.version === Version.V1 ? validateV1(cloudevent) : validateV03(cloudevent); + validateCloudEvent(cloudevent); return cloudevent; } } From fcd869e93a11fb996f0450a0796826f136759143 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Wed, 12 Aug 2020 16:49:21 -0400 Subject: [PATCH 04/14] test: implement pending tests leftover from TS rewrite (#315) This commit implements 4 of the 6 pending tests that were not completed during the TypeScript rewrite. The two tests that were not implemented were (one for each of v1 and v03): ``` it("returns a JSON string even if format is invalid"); ``` I don't really know what that's supposed to be/mean, so I removed them. Fixes: https://github.com/cloudevents/sdk-javascript/issues/232 Signed-off-by: Lance Ball --- test/integration/cloud_event_test.ts | 46 ++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/test/integration/cloud_event_test.ts b/test/integration/cloud_event_test.ts index de733c66..70881c96 100644 --- a/test/integration/cloud_event_test.ts +++ b/test/integration/cloud_event_test.ts @@ -144,9 +144,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.equal("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 +228,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(TypeError); + expect(err.message).to.equal("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); + }); }); From 9b44dfac58dacc07bd3687f7995b4a99bed5b852 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Wed, 12 Aug 2020 19:00:02 -0400 Subject: [PATCH 05/14] chore: Update references of master to main (#316) Signed-off-by: Lucas Holmquist --- .github/pull-request-template.md | 4 +-- .github/workflows/nodejs-ci-action.yml | 4 +-- CONTRIBUTING.md | 2 +- README.md | 4 +-- RELEASE_GUIDELINES.md | 8 +++--- maintainer_guidelines.md | 6 ++--- pr_guidelines.md | 36 ++++++++++++++------------ 7 files changed, 33 insertions(+), 31 deletions(-) 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/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 8595a404..65a9511b 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ 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 @@ -150,7 +150,7 @@ 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. Each SDK may have its own unique processes, tooling and guidelines, common 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/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/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. From e433f9a898f57bf26bed9422bd598e509c63d4c0 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Tue, 18 Aug 2020 22:07:56 +0300 Subject: [PATCH 06/14] fix: upgrade uuid from 8.2.0 to 8.3.0 (#317) Snyk has created this PR to upgrade uuid from 8.2.0 to 8.3.0. See this package in npm: https://www.npmjs.com/package/uuid See this project in Snyk: https://app.snyk.io/org/lance/project/37afc620-45ad-41a3-9acc-1ac155caebc7?utm_source=github&utm_medium=upgrade-pr Signed-off-by: Lucas Holmquist --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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..d66901a0 100644 --- a/package.json +++ b/package.json @@ -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", From ffb1a137a8c47e639cce552478b015977afb8ce0 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Wed, 19 Aug 2020 05:38:33 -0400 Subject: [PATCH 07/14] chore: Remove commented version import. (#319) * The commented out version import now gets imported from the cloudevent.ts file Signed-off-by: Lucas Holmquist --- src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 266d3af9..598d61c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ 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"; From 198abb6c3f83ef17851f2c79ff3c1a91887c8ff8 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Wed, 19 Aug 2020 15:39:37 -0400 Subject: [PATCH 08/14] chore: Update README with correct links for the support specification versions (#321) * chore(readme): Remove reference of HTTPReceiver * chore(readme): fix support specification links. * This adds the real links to the v0.3 and v1.0 specification. fixes #320 Signed-off-by: Lucas Holmquist --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 65a9511b..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 @@ -115,18 +115,18 @@ 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: | From ef05f27cf1c87858a0d69a57afc40e106816fde7 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Tue, 25 Aug 2020 16:38:17 -0400 Subject: [PATCH 09/14] chore: add cucumber.js to list of files to lint and /docs to .gitignore (#327) Signed-off-by: Lance Ball --- .gitignore | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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/package.json b/package.json index d66901a0..0562f89f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "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", From e2eb4766d355c767fd3d09fcdedb258b71647135 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Wed, 26 Aug 2020 18:26:50 -0400 Subject: [PATCH 10/14] feat: introduce Message, Serializer, Deserializer and Binding interfaces (#324) * lib(messages): Implement a 4.0 Messages and other supporting interfaces This commit introduces the Message, Serializer and Deserializer, and Binding interfaces used to convert a CloudEvent into a Message that can be sent across a transport protocol. The first protocol implemented for this is HTTP, and some of the functionality formerly in src/transport/http has been simplified, reduced and/or moved to /src/messages/http. Test for V1 and V3 events are in place. Conformance tests have been modified to use these new interfaces vs. the HTTP Receiver class. Signed-off-by: Lance Ball --- examples/express-ex/index.js | 17 +- examples/express-ex/package.json | 2 +- src/event/cloudevent.ts | 6 + src/index.ts | 24 +- .../versions.ts => message/http/headers.ts} | 91 ++++ src/message/http/index.ts | 234 +++++++++ src/message/index.ts | 75 +++ src/transport/emitter.ts | 1 + src/transport/http/binary_emitter.ts | 3 +- src/transport/http/binary_receiver.ts | 94 ---- src/transport/http/headers.ts | 100 ---- src/transport/http/structured_receiver.ts | 91 ---- src/transport/receiver.ts | 100 +--- test/conformance/steps.ts | 21 +- test/integration/http_emitter_test.ts | 3 +- test/integration/message_test.ts | 202 ++++++++ test/integration/receiver_binary_03_tests.ts | 443 ----------------- test/integration/receiver_binary_1_tests.ts | 448 ------------------ .../receiver_structured_0_3_test.ts | 227 --------- .../integration/receiver_structured_1_test.ts | 182 ------- test/integration/sdk_test.ts | 7 +- 21 files changed, 668 insertions(+), 1703 deletions(-) rename src/{transport/http/versions.ts => message/http/headers.ts} (61%) create mode 100644 src/message/http/index.ts create mode 100644 src/message/index.ts delete mode 100644 src/transport/http/binary_receiver.ts delete mode 100644 src/transport/http/headers.ts delete mode 100644 src/transport/http/structured_receiver.ts create mode 100644 test/integration/message_test.ts delete mode 100644 test/integration/receiver_binary_03_tests.ts delete mode 100644 test/integration/receiver_binary_1_tests.ts delete mode 100644 test/integration/receiver_structured_0_3_test.ts delete mode 100644 test/integration/receiver_structured_1_test.ts diff --git a/examples/express-ex/index.js b/examples/express-ex/index.js index 57a71e1c..cfe68e95 100644 --- a/examples/express-ex/index.js +++ b/examples/express-ex/index.js @@ -1,8 +1,7 @@ -/* eslint-disable no-console */ +/* eslint-disable */ const express = require("express"); -const {Receiver} = require("cloudevents"); - +const { Receiver } = require("cloudevents"); const app = express(); app.use((req, res, next) => { @@ -25,8 +24,16 @@ app.post("/", (req, res) => { try { const event = Receiver.accept(req.headers, req.body); - console.log(`Accepted event: ${event}`); - res.status(201).json(event); + // respond as an event + const responseEventMessage = new CloudEvent({ + source: '/', + type: 'event:response', + ...event + }); + responseEventMessage.data = { + hello: 'world' + }; + res.status(201).json(responseEventMessage); } catch (err) { console.error(err); res.status(415).header("Content-Type", "application/json").send(JSON.stringify(err)); diff --git a/examples/express-ex/package.json b/examples/express-ex/package.json index 3d483f18..756d8f7b 100644 --- a/examples/express-ex/package.json +++ b/examples/express-ex/package.json @@ -14,7 +14,7 @@ "author": "fabiojose@gmail.com", "license": "Apache-2.0", "dependencies": { - "cloudevents": "~3.0.0", + "cloudevents": "^3.1.0", "express": "^4.17.1" } } diff --git a/src/event/cloudevent.ts b/src/event/cloudevent.ts index 2cb571a7..197b4bc1 100644 --- a/src/event/cloudevent.ts +++ b/src/event/cloudevent.ts @@ -156,6 +156,12 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { this.#_data = value; } + /** + * Used by JSON.stringify(). The name is confusing, but this method is called by + * JSON.stringify() when converting this object to JSON. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify + * @return {object} this event as a plain object + */ toJSON(): Record { const event = { ...this }; event.time = this.time; diff --git a/src/index.ts b/src/index.ts index 598d61c4..11149ff8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,9 +3,9 @@ import { ValidationError } from "./event/validation"; import { CloudEventV03, CloudEventV03Attributes, CloudEventV1, CloudEventV1Attributes } from "./event/interfaces"; import { Emitter, TransportOptions } from "./transport/emitter"; -import { Receiver, Mode } from "./transport/receiver"; +import { Receiver } from "./transport/receiver"; import { Protocol } from "./transport/protocols"; -import { Headers, headersFor } from "./transport/http/headers"; +import { Headers, Mode, Binding, HTTP, Message, Serializer, Deserializer, headersFor } from "./message"; import CONSTANTS from "./constants"; @@ -18,14 +18,20 @@ export { CloudEventV1Attributes, Version, ValidationError, - // From transport - Emitter, - Receiver, - Mode, - Protocol, - TransportOptions, + // From message Headers, - headersFor, + Mode, + Binding, + Message, + Deserializer, + Serializer, + headersFor, // TODO: Deprecated. Remove for 4.0 + HTTP, + // From transport + Emitter, // TODO: Deprecated. Remove for 4.0 + Receiver, // TODO: Deprecated. Remove for 4.0 + Protocol, // TODO: Deprecated. Remove for 4.0 + TransportOptions, // TODO: Deprecated. Remove for 4.0 // From Constants CONSTANTS, }; diff --git a/src/transport/http/versions.ts b/src/message/http/headers.ts similarity index 61% rename from src/transport/http/versions.ts rename to src/message/http/headers.ts index 70ddc560..228445cb 100644 --- a/src/transport/http/versions.ts +++ b/src/message/http/headers.ts @@ -1,6 +1,97 @@ import { PassThroughParser, DateParser, MappedParser } from "../../parsers"; +import { ValidationError, CloudEvent } from "../.."; +import { Headers } from "../"; +import { Version } from "../../event/cloudevent"; import CONSTANTS from "../../constants"; +export const allowedContentTypes = [CONSTANTS.DEFAULT_CONTENT_TYPE, CONSTANTS.MIME_JSON, CONSTANTS.MIME_OCTET_STREAM]; +export const requiredHeaders = [ + CONSTANTS.CE_HEADERS.ID, + CONSTANTS.CE_HEADERS.SOURCE, + CONSTANTS.CE_HEADERS.TYPE, + CONSTANTS.CE_HEADERS.SPEC_VERSION, +]; + +/** + * Validates cloud event headers and their values + * @param {Headers} headers event transport headers for validation + * @throws {ValidationError} if the headers are invalid + * @return {boolean} true if headers are valid + */ +export function validate(headers: Headers): Headers { + const sanitizedHeaders = sanitize(headers); + + // if content-type exists, be sure it's an allowed type + const contentTypeHeader = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]; + const noContentType = !allowedContentTypes.includes(contentTypeHeader); + if (contentTypeHeader && noContentType) { + throw new ValidationError("invalid content type", [sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]]); + } + + requiredHeaders + .filter((required: string) => !sanitizedHeaders[required]) + .forEach((required: string) => { + throw new ValidationError(`header '${required}' not found`); + }); + + if (!sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]) { + sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON; + } + + return sanitizedHeaders; +} + +/** + * Returns the HTTP headers that will be sent for this event when the HTTP transmission + * mode is "binary". Events sent over HTTP in structured mode only have a single CE header + * and that is "ce-id", corresponding to the event ID. + * @param {CloudEvent} event a CloudEvent + * @returns {Object} the headers that will be sent for the event + */ +export function headersFor(event: CloudEvent): Headers { + const headers: Headers = {}; + let headerMap: Readonly<{ [key: string]: MappedParser }>; + if (event.specversion === Version.V1) { + headerMap = v1headerMap; + } else { + headerMap = v03headerMap; + } + + // iterate over the event properties - generate a header for each + Object.getOwnPropertyNames(event).forEach((property) => { + const value = event[property]; + if (value) { + const map: MappedParser | undefined = headerMap[property] as MappedParser; + if (map) { + headers[map.name] = map.parser.parse(value as string) as string; + } else if (property !== CONSTANTS.DATA_ATTRIBUTE && property !== `${CONSTANTS.DATA_ATTRIBUTE}_base64`) { + headers[`${CONSTANTS.EXTENSIONS_PREFIX}${property}`] = value as string; + } + } + }); + // Treat time specially, since it's handled with getters and setters in CloudEvent + if (event.time) { + headers[CONSTANTS.CE_HEADERS.TIME] = event.time as string; + } + return headers; +} + +/** + * Sanitizes incoming headers by lowercasing them and potentially removing + * encoding from the content-type header. + * @param {Headers} headers HTTP headers as key/value pairs + * @returns {Headers} the sanitized headers + */ +export function sanitize(headers: Headers): Headers { + const sanitized: Headers = {}; + + Array.from(Object.keys(headers)) + .filter((header) => Object.hasOwnProperty.call(headers, header)) + .forEach((header) => (sanitized[header.toLowerCase()] = headers[header])); + + return sanitized; +} + function parser(name: string, parser = new PassThroughParser()): MappedParser { return { name: name, parser: parser }; } diff --git a/src/message/http/index.ts b/src/message/http/index.ts new file mode 100644 index 00000000..c93917de --- /dev/null +++ b/src/message/http/index.ts @@ -0,0 +1,234 @@ +import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } from "../.."; +import { Message, Headers } from ".."; + +import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers, validate } from "./headers"; +import { asData, isBase64, isString, isStringOrObjectOrThrow, ValidationError } from "../../event/validation"; +import { validateCloudEvent } from "../../event/spec"; +import { Base64Parser, JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers"; + +// implements Serializer +export function binary(event: CloudEvent): Message { + const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CONTENT_TYPE }; + const headers: Headers = headersFor(event); + return { + headers: { ...contentType, ...headers }, + body: asData(event.data, event.datacontenttype as string), + }; +} + +// implements Serializer +export function structured(event: CloudEvent): Message { + return { + headers: { + [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CE_CONTENT_TYPE, + }, + body: event.toString(), + }; +} + +// implements Detector +// TODO: this could probably be optimized +export function isEvent(message: Message): boolean { + try { + deserialize(message); + return true; + } catch (err) { + return false; + } +} + +/** + * Converts a Message to a CloudEvent + * + * @param {Message} message the incoming message + * @return {CloudEvent} A new {CloudEvent} instance + */ +export function deserialize(message: Message): CloudEvent { + const cleanHeaders: Headers = sanitize(message.headers); + const mode: Mode = getMode(cleanHeaders); + let version = getVersion(mode, cleanHeaders, message.body); + if (version !== Version.V03 && version !== Version.V1) { + console.error(`Unknown spec version ${version}. Default to ${Version.V1}`); + version = Version.V1; + } + switch (mode) { + case Mode.BINARY: + return parseBinary(message, version); + case Mode.STRUCTURED: + return parseStructured(message, version); + default: + throw new ValidationError("Unknown Message mode"); + } +} + +/** + * Determines the HTTP transport mode (binary or structured) based + * on the incoming HTTP headers. + * @param {Headers} headers the incoming HTTP headers + * @returns {Mode} the transport mode + */ +function getMode(headers: Headers): Mode { + const contentType = headers[CONSTANTS.HEADER_CONTENT_TYPE]; + if (contentType && contentType.startsWith(CONSTANTS.MIME_CE)) { + return Mode.STRUCTURED; + } + if (headers[CONSTANTS.CE_HEADERS.ID]) { + return Mode.BINARY; + } + throw new ValidationError("no cloud event detected"); +} + +/** + * Determines the version of an incoming CloudEvent based on the + * HTTP headers or HTTP body, depending on transport mode. + * @param {Mode} mode the HTTP transport mode + * @param {Headers} headers the incoming HTTP headers + * @param {Record} 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 = validate(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; + } + + const cloudevent = new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03); + validateCloudEvent(cloudevent); + return cloudevent; +} + +/** + * Creates a new CloudEvent instance based on the provided payload and headers. + * + * @param {Message} message the incoming Message + * @param {Version} version the spec version of this message (v1 or v03) + * @returns {CloudEvent} a new CloudEvent instance for the provided headers and payload + * @throws {ValidationError} if the payload and header combination do not conform to the spec + */ +function parseStructured(message: Message, version: Version): CloudEvent { + const payload = message.body; + const headers = message.headers; + + if (!payload) throw new ValidationError("payload is null or undefined"); + if (!headers) throw new ValidationError("headers is null or undefined"); + isStringOrObjectOrThrow(payload, new ValidationError("payload must be an object or a string")); + + if ( + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] && + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] != Version.V03 && + headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] != Version.V1 + ) { + throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`); + } + + // Clone and low case all headers names + const sanitizedHeaders = sanitize(headers); + + const contentType = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]; + const parser: Parser = contentType ? parserByContentType[contentType] : new JSONParser(); + if (!parser) throw new ValidationError(`invalid content type ${sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]}`); + const incoming = { ...(parser.parse(payload) as Record) }; + + 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; + } + const cloudevent = new CloudEvent(eventObj as CloudEventV1 | CloudEventV03); + + // Validates the event + validateCloudEvent(cloudevent); + return cloudevent; +} 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 aa027f2b..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 { validateCloudEvent } from "../../event/spec"; -import { Headers, validate } from "./headers"; -import { v03binaryParsers, v1binaryParsers } from "./versions"; -import { parserByContentType, MappedParser } from "../../parsers"; -import { isString, isBase64, ValidationError, isStringOrObjectOrThrow } from "../../event/validation"; -import CONSTANTS from "../../constants"; - -/** - * A class that receives binary CloudEvents over HTTP. This class can be used - * if you know that all incoming events will be using binary transport. If - * events can come as either binary or structured, use {HTTPReceiver}. - */ -export class BinaryHTTPReceiver { - /** - * The specification version of the incoming cloud event - */ - version: Version; - constructor(version: Version = Version.V1) { - this.version = version; - } - - /** - * Parses an incoming HTTP request, converting it to a {CloudEvent} - * instance if it conforms to the Cloud Event specification for this receiver. - * - * @param {Object|string} payload the HTTP request body - * @param {Object} headers the HTTP request headers - * @param {Version} version the spec version of the incoming event - * @returns {CloudEvent} an instance of CloudEvent representing the incoming request - * @throws {ValidationError} of the event does not conform to the spec - */ - parse(payload: string | Record | 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); - validateCloudEvent(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 ef9b3a34..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 { validateCloudEvent } from "../../event/spec"; -import CONSTANTS from "../../constants"; - -/** - * A utility class used to receive structured CloudEvents - * over HTTP. - * @see {StructuredReceiver} - */ -export class StructuredHTTPReceiver { - /** - * The specification version of the incoming cloud event - */ - version: Version; - constructor(version: Version = Version.V1) { - this.version = version; - } - - /** - * Creates a new CloudEvent instance based on the provided payload and headers. - * - * @param {object} payload the cloud event data payload - * @param {object} headers the HTTP headers received for this cloud event - * @returns {CloudEvent} a new CloudEvent instance for the provided headers and payload - * @throws {ValidationError} if the payload and header combination do not conform to the spec - */ - parse(payload: Record | string | 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 - validateCloudEvent(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/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..c6a3315f --- /dev/null +++ b/test/integration/message_test.ts @@ -0,0 +1,202 @@ +import { expect } from "chai"; +import { CloudEvent, CONSTANTS, Version } from "../../src"; +import { asBase64 } from "../../src/event/validation"; +import { Message, HTTP } from "../../src/message"; + +const type = "org.cncf.cloudevents.example"; +const source = "urn:event:from:myapi/resource/123"; +const time = new Date(); +const subject = "subject.ext"; +const dataschema = "http://cloudevents.io/schema.json"; +const datacontenttype = "application/json"; +const id = "b46cf653-d48a-4b90-8dfa-355c01061361"; +const data = { + foo: "bar", +}; + +// Attributes for v03 events +const schemaurl = "https://cloudevents.io/schema.json"; +const datacontentencoding = "base64"; + +const ext1Name = "extension1"; +const ext1Value = "foobar"; +const ext2Name = "extension2"; +const ext2Value = "acme"; + +// Binary data as base64 +const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number); +const data_base64 = asBase64(dataBinary); + +describe("HTTP transport messages", () => { + it("can detect CloudEvent Messages", () => { + // Create a message that is not an actual event + let message: Message = { + body: "Hello world!", + headers: { + "Content-type": "text/plain", + }, + }; + expect(HTTP.isEvent(message)).to.be.false; + + // Now create a message that is an event + message = HTTP.binary( + new CloudEvent({ + source: "/message-test", + type: "example", + }), + ); + expect(HTTP.isEvent(message)).to.be.true; + }); + + describe("Specification version V1", () => { + const fixture: CloudEvent = new CloudEvent({ + specversion: Version.V1, + id, + type, + source, + datacontenttype, + subject, + time, + dataschema, + data, + [ext1Name]: ext1Value, + [ext2Name]: ext2Value, + }); + + it("Binary Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.binary(fixture); + expect(message.body).to.equal(data); + // validate all headers + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype); + expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V1); + expect(message.headers[CONSTANTS.CE_HEADERS.ID]).to.equal(id); + expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(type); + expect(message.headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(source); + expect(message.headers[CONSTANTS.CE_HEADERS.SUBJECT]).to.equal(subject); + expect(message.headers[CONSTANTS.CE_HEADERS.TIME]).to.equal(fixture.time); + expect(message.headers[CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]).to.equal(dataschema); + expect(message.headers[`ce-${ext1Name}`]).to.equal(ext1Value); + expect(message.headers[`ce-${ext2Name}`]).to.equal(ext2Value); + }); + + it("Structured Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.structured(fixture); + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); + // Parse the message body as JSON, then validate the attributes + const body = JSON.parse(message.body); + expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V1); + expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); + expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); + expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); + expect(body[CONSTANTS.CE_ATTRIBUTES.SUBJECT]).to.equal(subject); + expect(body[CONSTANTS.CE_ATTRIBUTES.TIME]).to.equal(fixture.time); + expect(body[CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA]).to.equal(dataschema); + expect(body[ext1Name]).to.equal(ext1Value); + expect(body[ext2Name]).to.equal(ext2Value); + }); + + it("A CloudEvent can be converted from a binary Message", () => { + const message = HTTP.binary(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); + + it("A CloudEvent can be converted from a structured Message", () => { + const message = HTTP.structured(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); + + it("Supports Base-64 encoded data in structured messages", () => { + const event = fixture.cloneWith({ data: dataBinary }); + expect(event.data_base64).to.equal(data_base64); + const message = HTTP.structured(event); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); + + it("Supports Base-64 encoded data in binary messages", () => { + const event = fixture.cloneWith({ data: dataBinary }); + expect(event.data_base64).to.equal(data_base64); + const message = HTTP.binary(event); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); + }); + + describe("Specification version V03", () => { + const fixture: CloudEvent = new CloudEvent({ + specversion: Version.V03, + id, + type, + source, + datacontenttype, + subject, + time, + schemaurl, + data, + [ext1Name]: ext1Value, + [ext2Name]: ext2Value, + }); + + it("Binary Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.binary(fixture); + expect(message.body).to.equal(data); + // validate all headers + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype); + expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V03); + expect(message.headers[CONSTANTS.CE_HEADERS.ID]).to.equal(id); + expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(type); + expect(message.headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(source); + expect(message.headers[CONSTANTS.CE_HEADERS.SUBJECT]).to.equal(subject); + expect(message.headers[CONSTANTS.CE_HEADERS.TIME]).to.equal(fixture.time); + expect(message.headers[CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]).to.equal(schemaurl); + expect(message.headers[`ce-${ext1Name}`]).to.equal(ext1Value); + expect(message.headers[`ce-${ext2Name}`]).to.equal(ext2Value); + }); + + it("Structured Messages can be created from a CloudEvent", () => { + const message: Message = HTTP.structured(fixture); + expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); + // Parse the message body as JSON, then validate the attributes + const body = JSON.parse(message.body); + expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V03); + expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); + expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); + expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); + expect(body[CONSTANTS.CE_ATTRIBUTES.SUBJECT]).to.equal(subject); + expect(body[CONSTANTS.CE_ATTRIBUTES.TIME]).to.equal(fixture.time); + expect(body[CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL]).to.equal(schemaurl); + expect(body[ext1Name]).to.equal(ext1Value); + expect(body[ext2Name]).to.equal(ext2Value); + }); + + it("A CloudEvent can be converted from a binary Message", () => { + const message = HTTP.binary(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); + + it("A CloudEvent can be converted from a structured Message", () => { + const message = HTTP.structured(fixture); + const event = HTTP.toEvent(message); + expect(event).to.deep.equal(fixture); + }); + + it("Supports Base-64 encoded data in structured messages", () => { + const event = fixture.cloneWith({ data: dataBinary, datacontentencoding }); + expect(event.data_base64).to.equal(data_base64); + const message = HTTP.structured(event); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); + + it("Supports Base-64 encoded data in binary messages", () => { + const event = fixture.cloneWith({ data: dataBinary, datacontentencoding }); + expect(event.data_base64).to.equal(data_base64); + const message = HTTP.binary(event); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + }); + }); +}); 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", From a0e72814d381b366ff8efe8ca0e7523d458d7159 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Tue, 8 Sep 2020 15:32:16 -0400 Subject: [PATCH 11/14] feat: add a constructor parameter for loose validation (#328) * feat: add a constructor parameter for loose validation This commit adds a second, optional boolean parameter to the `CloudEvent` constructor. When `false` is provided, the event constructor will not perform validation of the event properties, values and extension names. This commit also modifies the ValidationError class so that the error message string includes the JSON.stringified version of any schema validation errors. It also makes the HTTP.toEvent() function create CloudEvent objects with loose/no validation. Incorporates comments from https://github.com/cloudevents/sdk-javascript/pull/328 Fixes: https://github.com/cloudevents/sdk-javascript/issues/325 Signed-off-by: Lance Ball --- src/event/cloudevent.ts | 20 +++++++++---- src/event/validation.ts | 13 +++++++- src/message/http/headers.ts | 36 ++++------------------ src/message/http/index.ts | 15 +++------- test/integration/cloud_event_test.ts | 24 ++++++++++++--- test/integration/message_test.ts | 45 +++++++++++++++++++++++++--- 6 files changed, 98 insertions(+), 55 deletions(-) diff --git a/src/event/cloudevent.ts b/src/event/cloudevent.ts index 197b4bc1..df47b297 100644 --- a/src/event/cloudevent.ts +++ b/src/event/cloudevent.ts @@ -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); } @@ -193,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 */ @@ -204,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/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/message/http/headers.ts b/src/message/http/headers.ts index 228445cb..dcef105c 100644 --- a/src/message/http/headers.ts +++ b/src/message/http/headers.ts @@ -1,5 +1,5 @@ import { PassThroughParser, DateParser, MappedParser } from "../../parsers"; -import { ValidationError, CloudEvent } from "../.."; +import { CloudEvent } from "../.."; import { Headers } from "../"; import { Version } from "../../event/cloudevent"; import CONSTANTS from "../../constants"; @@ -12,35 +12,6 @@ export const requiredHeaders = [ 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 @@ -89,6 +60,11 @@ export function sanitize(headers: Headers): 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; } diff --git a/src/message/http/index.ts b/src/message/http/index.ts index c93917de..492f4916 100644 --- a/src/message/http/index.ts +++ b/src/message/http/index.ts @@ -1,9 +1,8 @@ import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } from "../.."; import { Message, Headers } from ".."; -import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers, validate } from "./headers"; +import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers } from "./headers"; import { asData, isBase64, isString, isStringOrObjectOrThrow, ValidationError } from "../../event/validation"; -import { validateCloudEvent } from "../../event/spec"; import { Base64Parser, JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers"; // implements Serializer @@ -129,7 +128,7 @@ function parseBinary(message: Message, version: Version): CloudEvent { body = isString(body) && isBase64(body) ? Buffer.from(body as string, "base64").toString() : body; // Clone and low case all headers names - const sanitizedHeaders = validate(headers); + const sanitizedHeaders = sanitize(headers); const eventObj: { [key: string]: unknown | string | Record } = {}; const parserMap: Record = version === Version.V1 ? v1binaryParsers : v1binaryParsers; @@ -165,9 +164,7 @@ function parseBinary(message: Message, version: Version): CloudEvent { delete eventObj.datacontentencoding; } - const cloudevent = new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03); - validateCloudEvent(cloudevent); - return cloudevent; + return new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03, false); } /** @@ -226,9 +223,5 @@ function parseStructured(message: Message, version: Version): CloudEvent { delete eventObj.data_base64; delete eventObj.datacontentencoding; } - const cloudevent = new CloudEvent(eventObj as CloudEventV1 | CloudEventV03); - - // Validates the event - validateCloudEvent(cloudevent); - return cloudevent; + return new CloudEvent(eventObj as CloudEventV1 | CloudEventV03, false); } diff --git a/test/integration/cloud_event_test.ts b/test/integration/cloud_event_test.ts index 70881c96..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)); @@ -152,7 +168,7 @@ describe("A 1.0 CloudEvent", () => { }); } catch (err) { expect(err).to.be.instanceOf(TypeError); - expect(err.message).to.equal("invalid payload"); + expect(err.message).to.include("invalid payload"); } }); @@ -235,8 +251,8 @@ describe("A 0.3 CloudEvent", () => { source: (null as unknown) as string, }); } catch (err) { - expect(err).to.be.instanceOf(TypeError); - expect(err.message).to.equal("invalid payload"); + expect(err).to.be.instanceOf(ValidationError); + expect(err.message).to.include("invalid payload"); } }); diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index c6a3315f..55f0c04c 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -27,19 +27,21 @@ const ext2Value = "acme"; const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number); const data_base64 = asBase64(dataBinary); -describe("HTTP transport messages", () => { - it("can detect CloudEvent Messages", () => { +describe("HTTP transport", () => { + it("Can detect invalid CloudEvent Messages", () => { // Create a message that is not an actual event - let message: Message = { + 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 - message = HTTP.binary( + const message = HTTP.binary( new CloudEvent({ source: "/message-test", type: "example", @@ -48,6 +50,41 @@ describe("HTTP transport messages", () => { 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, From 673219483a153e7a65fa17b3db85b9c6aa5d9dd7 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Thu, 3 Sep 2020 14:52:50 -0400 Subject: [PATCH 12/14] fix: upgrade cloudevents from 3.0.1 to 3.1.0 (#335) Snyk has created this PR to upgrade cloudevents from 3.0.1 to 3.1.0. See this package in npm: https://www.npmjs.com/package/cloudevents See this project in Snyk: https://app.snyk.io/org/lance/project/cb2960b0-db0c-4e77-9ab2-e78efded812e?utm_source=github&utm_medium=upgrade-pr Co-authored-by: snyk-bot Signed-off-by: Lucas Holmquist --- examples/typescript-ex/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" } } From e6204af8d146f213d687e011e44e1120edaffa44 Mon Sep 17 00:00:00 2001 From: Philip Hayes Date: Wed, 2 Sep 2020 17:09:53 -0400 Subject: [PATCH 13/14] chore(example): Replaced body parser with express JSON parser (#334) Signed-off-by: Philip Hayes Co-authored-by: Philip Hayes --- examples/express-ex/index.js | 16 ++-------------- examples/express-ex/package.json | 1 + 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/examples/express-ex/index.js b/examples/express-ex/index.js index cfe68e95..cc8e2349 100644 --- a/examples/express-ex/index.js +++ b/examples/express-ex/index.js @@ -3,20 +3,8 @@ const express = require("express"); 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); diff --git a/examples/express-ex/package.json b/examples/express-ex/package.json index 756d8f7b..74fa5232 100644 --- a/examples/express-ex/package.json +++ b/examples/express-ex/package.json @@ -14,6 +14,7 @@ "author": "fabiojose@gmail.com", "license": "Apache-2.0", "dependencies": { + "body-parser": "^1.19.0", "cloudevents": "^3.1.0", "express": "^4.17.1" } From b9ed2619c0362390c221e9bf8bf61deea7bd83fd Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Fri, 11 Sep 2020 15:33:17 -0400 Subject: [PATCH 14/14] "chore(release): 3.2.0" Signed-off-by: Lance Ball --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) 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/package.json b/package.json index 0562f89f..bb75d532 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cloudevents", - "version": "3.1.0", + "version": "3.2.0", "description": "CloudEvents SDK for JavaScript", "main": "dist/index.js", "scripts": {