From 5883096418159da8156c6d38bd0159c23ce9ec5f Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Mon, 11 May 2020 19:16:01 -0400 Subject: [PATCH 1/6] feat!: expose a version agnostic event emitter This is a breaking change. This commit exposes an HTTP based event emitter that simplifes the API. To use it, simply import the SDK and start emitting. The default spec version is 1.0, but you can use 0.3 by supplying that to the constructor. By default, CloudEvents are emitted in binary mode, but this can be changed by providing the "structured" parameter to the `emit()` function. This commit also eliminates the version specific emitters and receivers from the `v1` and `v03` exports, and eliminates the explicit usage of versioned emitters from `lib/bindings/http`. Signed-off-by: Lance Ball --- index.js | 4 +- lib/bindings/http/emitter_binary.js | 122 ++++++++++++------ lib/bindings/http/emitter_binary_0_3.js | 39 ++---- lib/bindings/http/emitter_binary_1.js | 37 ++---- lib/bindings/http/emitter_structured.js | 50 ++++--- lib/bindings/http/http_emitter.js | 56 ++++++++ .../http/receiver_structured_0_3_test.js | 26 ++-- .../http/receiver_structured_1_test.js | 10 +- test/http_binding_0_3.js | 63 ++++----- test/http_binding_1.js | 79 ++++++------ test/sdk_test.js | 61 +-------- v03/index.js | 25 +--- v1/index.js | 24 +--- 13 files changed, 294 insertions(+), 302 deletions(-) create mode 100644 lib/bindings/http/http_emitter.js diff --git a/index.js b/index.js index 22e87aab..11036a11 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,9 @@ const CloudEvent = require("./lib/cloudevent.js"); const HTTPReceiver = require("./lib/bindings/http/http_receiver.js"); +const HTTPEmitter = require("./lib/bindings/http/http_emitter.js"); module.exports = { CloudEvent, - HTTPReceiver + HTTPReceiver, + HTTPEmitter }; diff --git a/lib/bindings/http/emitter_binary.js b/lib/bindings/http/emitter_binary.js index 8c60e05a..9c9ac276 100644 --- a/lib/bindings/http/emitter_binary.js +++ b/lib/bindings/http/emitter_binary.js @@ -1,49 +1,85 @@ const axios = require("axios"); -const Constants = require("./constants.js"); -const defaults = {}; -defaults[Constants.HEADERS] = {}; -defaults[Constants.HEADERS][Constants.HEADER_CONTENT_TYPE] = - Constants.DEFAULT_CONTENT_TYPE; - -function BinaryHTTPEmitter(config, headerByGetter, extensionPrefix) { - this.config = Object.assign({}, defaults, config); - this.headerByGetter = headerByGetter; - this.extensionPrefix = extensionPrefix; -} +const { + HEADERS, + BINARY_HEADERS_03, + BINARY_HEADERS_1, + HEADER_CONTENT_TYPE, + DEFAULT_CONTENT_TYPE, + DATA_ATTRIBUTE, + SPEC_V1, + SPEC_V03 +} = require("./constants.js"); -BinaryHTTPEmitter.prototype.emit = function(cloudevent) { - const config = Object.assign({}, this.config); - const headers = Object.assign({}, this.config[Constants.HEADERS]); - - Object.keys(this.headerByGetter) - .filter((getter) => cloudevent[getter]()) - .forEach((getter) => { - const header = this.headerByGetter[getter]; - headers[header.name] = - header.parser( - cloudevent[getter]() - ); - }); - - // Set the cloudevent payload - const formatted = cloudevent.format(); - let data = formatted.data; - data = (formatted.data_base64 ? formatted.data_base64 : data); - - // Have extensions? - const exts = cloudevent.getExtensions(); - Object.keys(exts) - .filter((ext) => Object.hasOwnProperty.call(exts, ext)) - .forEach((ext) => { - headers[this.extensionPrefix + ext] = exts[ext]; - }); - - config[Constants.DATA_ATTRIBUTE] = data; - config.headers = headers; - - // Return the Promise - return axios.request(config); +const defaults = { + [HEADERS]: { + [HEADER_CONTENT_TYPE]: DEFAULT_CONTENT_TYPE + }, + method: "POST" }; +/** + * A class to emit binary CloudEvents over HTTP. + */ +class BinaryHTTPEmitter { + /** + * + * @param {string} version - the CloudEvent HTTP specification version. + * Default: 1.0 + */ + constructor(version) { + if (version === SPEC_V1) { + this.headerByGetter = require("./emitter_binary_1"); + this.extensionPrefix = BINARY_HEADERS_1.EXTENSIONS_PREFIX; + } else if (version === SPEC_V03) { + this.headerByGetter = require("./emitter_binary_0_3.js"); + this.extensionPrefix = BINARY_HEADERS_03.EXTENSIONS_PREFIX; + } + } + + /** + * Sends this cloud event to a receiver over HTTP. + * + * @param {Object} options The configuration options for this event. Options + * provided other than `url` will be passed along to Node.js `http.request`. + * https://nodejs.org/api/http.html#http_http_request_options_callback + * @param {URL} options.url The HTTP/S url that should receive this event + * @param {Object} cloudevent the CloudEvent to be sent + * @returns {Promise} Promise with an eventual response from the receiver + */ + async emit(options, cloudevent) { + const config = { ...options, ...defaults }; + const headers = config[HEADERS]; + + Object.keys(this.headerByGetter) + .filter((getter) => cloudevent[getter]()) + .forEach((getter) => { + const header = this.headerByGetter[getter]; + headers[header.name] = + header.parser( + cloudevent[getter]() + ); + }); + + // Set the cloudevent payload + const formatted = cloudevent.format(); + let data = formatted.data; + data = (formatted.data_base64 ? formatted.data_base64 : data); + + // Have extensions? + const exts = cloudevent.getExtensions(); + Object.keys(exts) + .filter((ext) => Object.hasOwnProperty.call(exts, ext)) + .forEach((ext) => { + headers[this.extensionPrefix + ext] = exts[ext]; + }); + + config[DATA_ATTRIBUTE] = data; + config.headers = headers; + + // Return the Promise + return axios.request(config); + } +} + module.exports = BinaryHTTPEmitter; diff --git a/lib/bindings/http/emitter_binary_0_3.js b/lib/bindings/http/emitter_binary_0_3.js index 5aa6c022..46c9f9dd 100644 --- a/lib/bindings/http/emitter_binary_0_3.js +++ b/lib/bindings/http/emitter_binary_0_3.js @@ -1,64 +1,53 @@ -const BinaryHTTPEmitter = require("./emitter_binary.js"); - -const Constants = require("./constants.js"); +const { + HEADER_CONTENT_TYPE, + BINARY_HEADERS_03 +} = require("./constants.js"); const headerByGetter = {}; headerByGetter.getDataContentType = { - name: Constants.HEADER_CONTENT_TYPE, + name: HEADER_CONTENT_TYPE, parser: (v) => v }; headerByGetter.getDataContentEncoding = { - name: Constants.BINARY_HEADERS_03.CONTENT_ENCONDING, + name: BINARY_HEADERS_03.CONTENT_ENCONDING, parser: (v) => v }; headerByGetter.getSubject = { - name: Constants.BINARY_HEADERS_03.SUBJECT, + name: BINARY_HEADERS_03.SUBJECT, parser: (v) => v }; headerByGetter.getType = { - name: Constants.BINARY_HEADERS_03.TYPE, + name: BINARY_HEADERS_03.TYPE, parser: (v) => v }; headerByGetter.getSpecversion = { - name: Constants.BINARY_HEADERS_03.SPEC_VERSION, + name: BINARY_HEADERS_03.SPEC_VERSION, parser: (v) => v }; headerByGetter.getSource = { - name: Constants.BINARY_HEADERS_03.SOURCE, + name: BINARY_HEADERS_03.SOURCE, parser: (v) => v }; headerByGetter.getId = { - name: Constants.BINARY_HEADERS_03.ID, + name: BINARY_HEADERS_03.ID, parser: (v) => v }; headerByGetter.getTime = { - name: Constants.BINARY_HEADERS_03.TIME, + name: BINARY_HEADERS_03.TIME, parser: (v) => v }; headerByGetter.getSchemaurl = { - name: Constants.BINARY_HEADERS_03.SCHEMA_URL, + name: BINARY_HEADERS_03.SCHEMA_URL, parser: (v) => v }; -function HTTPBinary(configuration) { - this.emitter = new BinaryHTTPEmitter( - configuration, - headerByGetter, - Constants.BINARY_HEADERS_03.EXTENSIONS_PREFIX - ); -} - -HTTPBinary.prototype.emit = function(cloudevent) { - return this.emitter.emit(cloudevent); -}; - -module.exports = HTTPBinary; +module.exports = headerByGetter; diff --git a/lib/bindings/http/emitter_binary_1.js b/lib/bindings/http/emitter_binary_1.js index 402b85ea..3c08e952 100644 --- a/lib/bindings/http/emitter_binary_1.js +++ b/lib/bindings/http/emitter_binary_1.js @@ -1,59 +1,48 @@ -const BinaryHTTPEmitter = require("./emitter_binary.js"); - -const Constants = require("./constants.js"); +const { + HEADER_CONTENT_TYPE, + BINARY_HEADERS_1 +} = require("./constants.js"); const headerByGetter = {}; headerByGetter.getDataContentType = { - name: Constants.HEADER_CONTENT_TYPE, + name: HEADER_CONTENT_TYPE, parser: (v) => v }; headerByGetter.getSubject = { - name: Constants.BINARY_HEADERS_1.SUBJECT, + name: BINARY_HEADERS_1.SUBJECT, parser: (v) => v }; headerByGetter.getType = { - name: Constants.BINARY_HEADERS_1.TYPE, + name: BINARY_HEADERS_1.TYPE, parser: (v) => v }; headerByGetter.getSpecversion = { - name: Constants.BINARY_HEADERS_1.SPEC_VERSION, + name: BINARY_HEADERS_1.SPEC_VERSION, parser: (v) => v }; headerByGetter.getSource = { - name: Constants.BINARY_HEADERS_1.SOURCE, + name: BINARY_HEADERS_1.SOURCE, parser: (v) => v }; headerByGetter.getId = { - name: Constants.BINARY_HEADERS_1.ID, + name: BINARY_HEADERS_1.ID, parser: (v) => v }; headerByGetter.getTime = { - name: Constants.BINARY_HEADERS_1.TIME, + name: BINARY_HEADERS_1.TIME, parser: (v) => v }; headerByGetter.getDataschema = { - name: Constants.BINARY_HEADERS_1.DATA_SCHEMA, + name: BINARY_HEADERS_1.DATA_SCHEMA, parser: (v) => v }; -function HTTPBinary(configuration) { - this.emitter = new BinaryHTTPEmitter( - configuration, - headerByGetter, - Constants.BINARY_HEADERS_1.EXTENSIONS_PREFIX - ); -} - -HTTPBinary.prototype.emit = function(cloudevent) { - return this.emitter.emit(cloudevent); -}; - -module.exports = HTTPBinary; +module.exports = headerByGetter; diff --git a/lib/bindings/http/emitter_structured.js b/lib/bindings/http/emitter_structured.js index 21b83479..6c768ec5 100644 --- a/lib/bindings/http/emitter_structured.js +++ b/lib/bindings/http/emitter_structured.js @@ -1,24 +1,38 @@ const axios = require("axios"); +const { + DATA_ATTRIBUTE, + DEFAULT_CE_CONTENT_TYPE, + HEADERS, + HEADER_CONTENT_TYPE +} = require("./constants.js"); -const Constants = require("./constants.js"); -const defaults = {}; -defaults[Constants.HEADERS] = {}; -defaults[Constants.HEADERS][Constants.HEADER_CONTENT_TYPE] = - Constants.DEFAULT_CE_CONTENT_TYPE; - -function StructuredHTTPEmitter(configuration) { - this.config = Object.assign({}, defaults, configuration); -} +const defaults = { + [HEADERS]: { + [HEADER_CONTENT_TYPE]: DEFAULT_CE_CONTENT_TYPE + }, + method: "POST" +}; -StructuredHTTPEmitter.prototype.emit = function(cloudevent) { - // Set the cloudevent payload - this.config[Constants.DATA_ATTRIBUTE] = cloudevent.format(); +/** + * A class for sending {CloudEvent} instances over HTTP. + */ +class StructuredHTTPEmitter { + // TODO: Do we really need a class here? There is no state maintenance - // Return the Promise - return axios.request(this.config).then((response) => { - delete this.config[Constants.DATA_ATTRIBUTE]; - return response; - }); -}; + /** + * Sends the event over HTTP + * @param {Object} options The configuration options for this event. Options + * provided will be passed along to Node.js `http.request()`. + * https://nodejs.org/api/http.html#http_http_request_options_callback + * @param {URL} options.url The HTTP/S url that should receive this event + * @param {CloudEvent} cloudevent The CloudEvent to be sent + * @returns {Promise} Promise with an eventual response from the receiver + */ + async emit(options, cloudevent) { + const config = { ...defaults, ...options }; + config[DATA_ATTRIBUTE] = cloudevent.format(); + return axios.request(config); + } +} module.exports = StructuredHTTPEmitter; diff --git a/lib/bindings/http/http_emitter.js b/lib/bindings/http/http_emitter.js new file mode 100644 index 00000000..2b82fec0 --- /dev/null +++ b/lib/bindings/http/http_emitter.js @@ -0,0 +1,56 @@ +const BinaryHTTPEmitter = require("./emitter_binary.js"); +const StructuredEmitter = require("./emitter_structured.js"); + +const { + SPEC_V03, + SPEC_V1 +} = require("./constants"); + +/** + * A class which is capable of sending binary and structured events using + * the CloudEvents HTTP Protocol Binding specification. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md + * @see https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md#13-content-modes + */ +class HTTPEmitter { + /** + * Creates a new instance of {HTTPEmitter}. The default emitter uses the 1.0 + * protocol specification in binary mode. + * + * @param {string} [version] The HTTP binding specification version. Default: "1.0" + * @throws {TypeError} if no options.url is provided or an unknown specification version is provided. + */ + constructor(version = SPEC_V1) { + if (version !== SPEC_V03 && version !== SPEC_V1) { + throw new TypeError( + `Unknown CloudEvent specification version: ${version}`); + } + this.binary = new BinaryHTTPEmitter(version); + this.structured = new StructuredEmitter(); + } + + /** + * Sends the {CloudEvent} to an event receiver over HTTP POST + * + * @param {Object} options The configuration options for this event. Options + * provided will be passed along to Node.js `http.request()`. + * https://nodejs.org/api/http.html#http_http_request_options_callback + * @param {URL} options.url The HTTP/S url that should receive this event + * @param {CloudEvent} event the CloudEvent to be sent + * @param {string} [mode] the message mode for sending this event. + * Possible values are "binary" and "structured". Default: structured + * @returns {Promise} Promise with an eventual response from the receiver + */ + send(options, event, mode = "binary") { + if (mode === "binary") { + this.binary.emit(options, event); + } else if (mode === "structured") { + this.structured.emit(options, event); + } else { + throw new TypeError(`Unknown transport mode ${mode}.`); + } + } +} + +module.exports = HTTPEmitter; diff --git a/test/bindings/http/receiver_structured_0_3_test.js b/test/bindings/http/receiver_structured_0_3_test.js index 59fa54b2..7a0157ef 100644 --- a/test/bindings/http/receiver_structured_0_3_test.js +++ b/test/bindings/http/receiver_structured_0_3_test.js @@ -1,7 +1,8 @@ const expect = require("chai").expect; -const v03 = require("../../../v03/index.js"); const ValidationError = require("../../../lib/validation_error.js"); const HTTPStructuredReceiver = require("../../../lib/bindings/http/receiver_structured_0_3.js"); +const CloudEvent = require("../../../lib/cloudevent.js"); +const { Spec } = require("../../../v03/index.js"); const receiver = new HTTPStructuredReceiver(); @@ -68,7 +69,7 @@ describe("HTTP Transport Binding Structured Receiver CloudEvents v0.3", () => { it("Throw error data content encoding is base64, but 'data' is not", () => { // setup - const payload = v03.event() + const payload = new CloudEvent(Spec) .type(type) .source(source) .dataContentType("text/plain") @@ -119,14 +120,13 @@ describe("HTTP Transport Binding Structured Receiver CloudEvents v0.3", () => { describe("Parse", () => { it("Throw error when the event does not follow the spec", () => { // setup - const payload = - v03.event() - .type(type) - .source(source) - .time(now) - .schemaurl(schemaurl) - .data(data) - .toString(); + const payload = new CloudEvent(Spec) + .type(type) + .source(source) + .time(now) + .schemaurl(schemaurl) + .data(data) + .toString(); const headers = { "Content-Type": "application/cloudevents+xml" @@ -140,7 +140,7 @@ describe("HTTP Transport Binding Structured Receiver CloudEvents v0.3", () => { it("Should accept event that follows the spec", () => { // setup const id = "id-x0dk"; - const payload = v03.event() + const payload = new CloudEvent(Spec) .type(type) .source(source) .id(id) @@ -170,7 +170,7 @@ describe("HTTP Transport Binding Structured Receiver CloudEvents v0.3", () => { it("Should accept 'extension1'", () => { // setup const extension1 = "mycuston-ext1"; - const payload = v03.event() + const payload = new CloudEvent(Spec) .type(type) .source(source) .dataContentType(ceContentType) @@ -195,7 +195,7 @@ describe("HTTP Transport Binding Structured Receiver CloudEvents v0.3", () => { it("Should parse 'data' stringfied json to json object", () => { // setup - const payload = v03.event() + const payload = new CloudEvent(Spec) .type(type) .source(source) .dataContentType(ceContentType) diff --git a/test/bindings/http/receiver_structured_1_test.js b/test/bindings/http/receiver_structured_1_test.js index 6436c250..1a8601b8 100644 --- a/test/bindings/http/receiver_structured_1_test.js +++ b/test/bindings/http/receiver_structured_1_test.js @@ -1,5 +1,5 @@ const expect = require("chai").expect; -const v1 = require("../../../v1/index.js"); +const { Spec } = require("../../../v1/index.js"); const { CloudEvent } = require("../../../index.js"); const { asBase64 } = require("../../../lib/utils/fun.js"); const ValidationError = require("../../../lib/validation_error.js"); @@ -99,7 +99,7 @@ describe("HTTP Transport Binding Structured Receiver for CloudEvents v1.0", it("Should accept event that follows the spec", () => { // setup const id = "id-x0dk"; - const payload = v1.event() + const payload = new CloudEvent(Spec) .type(type) .source(source) .id(id) @@ -129,7 +129,7 @@ describe("HTTP Transport Binding Structured Receiver for CloudEvents v1.0", it("Should accept 'extension1'", () => { // setup const extension1 = "mycustom-ext1"; - const payload = v1.event() + const payload = new CloudEvent(Spec) .type(type) .source(source) .dataContentType(ceContentType) @@ -154,7 +154,7 @@ describe("HTTP Transport Binding Structured Receiver for CloudEvents v1.0", it("Should parse 'data' stringified json to json object", () => { // setup - const payload = v1.event() + const payload = new CloudEvent(Spec) .type(type) .source(source) .dataContentType(ceContentType) @@ -179,7 +179,7 @@ describe("HTTP Transport Binding Structured Receiver for CloudEvents v1.0", const bindata = Uint32Array .from(JSON.stringify(data), (c) => c.codePointAt(0)); const expected = asBase64(bindata); - const payload = v1.event() + const payload = new CloudEvent(Spec) .type(type) .source(source) .dataContentType(ceContentType) diff --git a/test/http_binding_0_3.js b/test/http_binding_0_3.js index d1e47cee..97d125e3 100644 --- a/test/http_binding_0_3.js +++ b/test/http_binding_0_3.js @@ -1,9 +1,12 @@ const expect = require("chai").expect; const nock = require("nock"); -const BinaryHTTPEmitter = - require("../lib/bindings/http/emitter_binary_0_3.js"); +const BinaryHTTPEmitter = require("../lib/bindings/http/emitter_binary.js"); +const StructuredHTTPEmitter = require("../lib/bindings/http/emitter_structured.js"); const CloudEvent = require("../lib/cloudevent.js"); const v03 = require("../v03/index.js"); +const { + SPEC_V03 +} = require("../lib/bindings/http/constants.js"); const type = "com.github.pull.create"; const source = "urn:event:from:myapi/resourse/123"; @@ -54,8 +57,8 @@ const httpcfg = { url: `${webhook}/json` }; -const binary = new BinaryHTTPEmitter(httpcfg); -const structured = new v03.StructuredHTTPEmitter(httpcfg); +const binary = new BinaryHTTPEmitter(SPEC_V03); +const structured = new StructuredHTTPEmitter(); describe("HTTP Transport Binding - Version 0.3", () => { beforeEach(() => { @@ -68,14 +71,14 @@ describe("HTTP Transport Binding - Version 0.3", () => { describe("Structured", () => { describe("JSON Format", () => { it(`requires '${contentType}' Content-Type in the header`, - () => structured.emit(cloudevent) + () => structured.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers["Content-Type"]) .to.equal(contentType); })); it("the request payload should be correct", - () => structured.emit(cloudevent) + () => structured.emit(httpcfg, cloudevent) .then((response) => { expect(JSON.parse(response.config.data)) .to.deep.equal(cloudevent.format()); @@ -83,7 +86,7 @@ describe("HTTP Transport Binding - Version 0.3", () => { describe("'data' attribute with 'base64' encoding", () => { it("the request payload should be correct", - () => structured.emit(cebase64) + () => structured.emit(httpcfg, cebase64) .then((response) => { expect(JSON.parse(response.config.data).data) .to.equal(cebase64.format().data); @@ -95,127 +98,127 @@ describe("HTTP Transport Binding - Version 0.3", () => { describe("Binary", () => { describe("JSON Format", () => { it(`requires ${cloudevent.getDataContentType()} in the header`, - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers["Content-Type"]) .to.equal(cloudevent.getDataContentType()); })); - it("the request payload should be correct", () => binary.emit(cloudevent) + it("the request payload should be correct", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(JSON.parse(response.config.data)) .to.deep.equal(cloudevent.getData()); })); - it("HTTP Header contains 'ce-type'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-type'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-type"); })); - it("HTTP Header contains 'ce-specversion'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-specversion'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-specversion"); })); - it("HTTP Header contains 'ce-source'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-source'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-source"); })); - it("HTTP Header contains 'ce-id'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-id'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-id"); })); - it("HTTP Header contains 'ce-time'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-time'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-time"); })); - it("HTTP Header contains 'ce-schemaurl'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-schemaurl'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-schemaurl"); })); - it(`HTTP Header contains 'ce-${ext1Name}'`, () => binary.emit(cloudevent) + it(`HTTP Header contains 'ce-${ext1Name}'`, () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property(`ce-${ext1Name}`); })); - it(`HTTP Header contains 'ce-${ext2Name}'`, () => binary.emit(cloudevent) + it(`HTTP Header contains 'ce-${ext2Name}'`, () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property(`ce-${ext2Name}`); })); - it("HTTP Header contains 'ce-subject'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-subject'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-subject"); })); - it("should 'ce-type' have the right value", () => binary.emit(cloudevent) + it("should 'ce-type' have the right value", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getType()) .to.equal(response.config.headers["ce-type"]); })); it("should 'ce-specversion' have the right value", - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getSpecversion()) .to.equal(response.config.headers["ce-specversion"]); })); it("should 'ce-source' have the right value", - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getSource()) .to.equal(response.config.headers["ce-source"]); })); - it("should 'ce-id' have the right value", () => binary.emit(cloudevent) + it("should 'ce-id' have the right value", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getId()) .to.equal(response.config.headers["ce-id"]); })); - it("should 'ce-time' have the right value", () => binary.emit(cloudevent) + it("should 'ce-time' have the right value", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getTime()) .to.equal(response.config.headers["ce-time"]); })); it("should 'ce-schemaurl' have the right value", - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getSchemaurl()) .to.equal(response.config.headers["ce-schemaurl"]); })); it(`should 'ce-${ext1Name}' have the right value`, - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getExtensions()[ext1Name]) .to.equal(response.config.headers[`ce-${ext1Name}`]); })); it(`should 'ce-${ext2Name}' have the right value`, - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getExtensions()[ext2Name]) .to.equal(response.config.headers[`ce-${ext2Name}`]); })); it("should 'ce-subject' have the right value", - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getSubject()) .to.equal(response.config.headers["ce-subject"]); @@ -223,16 +226,16 @@ describe("HTTP Transport Binding - Version 0.3", () => { describe("'data' attribute with 'base64' encoding", () => { it("HTTP Header contains 'ce-datacontentencoding'", - () => binary.emit(cebase64) + () => binary.emit(httpcfg, cebase64) .then((response) => { expect(response.config.headers) .to.have.property("ce-datacontentencoding"); })); it("should 'ce-datacontentencoding' have the right value", - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cebase64) .then((response) => { - expect(cloudevent.getDataContentEncoding()) + expect(cebase64.getDataContentEncoding()) .to.equal(response.config.headers["ce-datacontentencoding"]); })); }); diff --git a/test/http_binding_1.js b/test/http_binding_1.js index 300c908c..505e533f 100644 --- a/test/http_binding_1.js +++ b/test/http_binding_1.js @@ -2,14 +2,14 @@ const expect = require("chai").expect; const nock = require("nock"); const https = require("https"); const { asBase64 } = require("../lib/utils/fun.js"); - const { - Spec, - BinaryHTTPEmitter, - StructuredHTTPEmitter, - CloudEvent -} = require("../v1/index.js"); + SPEC_V1 +} = require("../lib/bindings/http/constants.js"); +const { Spec } = require("../v1/index.js"); +const CloudEvent = require("../lib/cloudevent.js"); +const BinaryHTTPEmitter = require("../lib/bindings/http/emitter_binary.js"); +const StructuredHTTPEmitter = require("../lib/bindings/http/emitter_structured.js"); const type = "com.github.pull.create"; const source = "urn:event:from:myapi/resource/123"; const contentType = "application/cloudevents+json; charset=utf-8"; @@ -47,8 +47,8 @@ const httpcfg = { url: `${webhook}/json` }; -const binary = new BinaryHTTPEmitter(httpcfg); -const structured = new StructuredHTTPEmitter(httpcfg); +const binary = new BinaryHTTPEmitter(SPEC_V1); +const structured = new StructuredHTTPEmitter(); describe("HTTP Transport Binding - Version 1.0", () => { beforeEach(() => { @@ -68,7 +68,7 @@ describe("HTTP Transport Binding - Version 1.0", () => { key: "other value" }) }); - return event.emit(cloudevent).then((response) => { + return event.emit(httpcfg, cloudevent).then((response) => { expect(response.config.headers["Content-Type"]) .to.equal(contentType); }); @@ -76,14 +76,14 @@ describe("HTTP Transport Binding - Version 1.0", () => { describe("JSON Format", () => { it(`requires '${contentType}' Content-Type in the header`, - () => structured.emit(cloudevent) + () => structured.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers["Content-Type"]) .to.equal(contentType); })); it("the request payload should be correct", - () => structured.emit(cloudevent) + () => structured.emit(httpcfg, cloudevent) .then((response) => { expect(JSON.parse(response.config.data)) .to.deep.equal(cloudevent.format()); @@ -102,7 +102,7 @@ describe("HTTP Transport Binding - Version 1.0", () => { .addExtension(ext1Name, ext1Value) .addExtension(ext2Name, ext2Value); - return structured.emit(binevent) + return structured.emit(httpcfg, binevent) .then((response) => { expect(JSON.parse(response.config.data).data_base64) .to.equal(expected); @@ -119,7 +119,7 @@ describe("HTTP Transport Binding - Version 1.0", () => { .addExtension(ext1Name, ext1Value) .addExtension(ext2Name, ext2Value); - return structured.emit(binevent) + return structured.emit(httpcfg, binevent) .then((response) => { expect(JSON.parse(response.config.data)) .to.have.property("data_base64"); @@ -130,30 +130,29 @@ describe("HTTP Transport Binding - Version 1.0", () => { }); describe("Binary", () => { - it("works with mTLS authentication", () => { - const event = new BinaryHTTPEmitter({ + it("works with mTLS authentication", () => + binary.emit({ method: "POST", url: `${webhook}/json`, httpsAgent: new https.Agent({ cert: "some value", key: "other value" }) - }); - return event.emit(cloudevent).then((response) => { + }, cloudevent).then((response) => { expect(response.config.headers["Content-Type"]) .to.equal(cloudevent.getDataContentType()); - }); - }); + }) + ); describe("JSON Format", () => { it(`requires '${cloudevent.getDataContentType()}' in the header`, - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers["Content-Type"]) .to.equal(cloudevent.getDataContentType()); })); - it("the request payload should be correct", () => binary.emit(cloudevent) + it("the request payload should be correct", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(JSON.parse(response.config.data)) .to.deep.equal(cloudevent.getData()); @@ -172,122 +171,122 @@ describe("HTTP Transport Binding - Version 1.0", () => { .addExtension(ext1Name, ext1Value) .addExtension(ext2Name, ext2Value); - return binary.emit(binevent) + return binary.emit(httpcfg, binevent) .then((response) => { expect(response.config.data) .to.equal(expected); }); }); - it("HTTP Header contains 'ce-type'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-type'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-type"); })); - it("HTTP Header contains 'ce-specversion'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-specversion'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-specversion"); })); - it("HTTP Header contains 'ce-source'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-source'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-source"); })); - it("HTTP Header contains 'ce-id'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-id'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-id"); })); - it("HTTP Header contains 'ce-time'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-time'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-time"); })); - it("HTTP Header contains 'ce-dataschema'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-dataschema'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-dataschema"); })); - it(`HTTP Header contains 'ce-${ext1Name}'`, () => binary.emit(cloudevent) + it(`HTTP Header contains 'ce-${ext1Name}'`, () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property(`ce-${ext1Name}`); })); - it(`HTTP Header contains 'ce-${ext2Name}'`, () => binary.emit(cloudevent) + it(`HTTP Header contains 'ce-${ext2Name}'`, () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property(`ce-${ext2Name}`); })); - it("HTTP Header contains 'ce-subject'", () => binary.emit(cloudevent) + it("HTTP Header contains 'ce-subject'", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(response.config.headers) .to.have.property("ce-subject"); })); - it("should 'ce-type' have the right value", () => binary.emit(cloudevent) + it("should 'ce-type' have the right value", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getType()) .to.equal(response.config.headers["ce-type"]); })); it("should 'ce-specversion' have the right value", - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getSpecversion()) .to.equal(response.config.headers["ce-specversion"]); })); it("should 'ce-source' have the right value", - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getSource()) .to.equal(response.config.headers["ce-source"]); })); - it("should 'ce-id' have the right value", () => binary.emit(cloudevent) + it("should 'ce-id' have the right value", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getId()) .to.equal(response.config.headers["ce-id"]); })); - it("should 'ce-time' have the right value", () => binary.emit(cloudevent) + it("should 'ce-time' have the right value", () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getTime()) .to.equal(response.config.headers["ce-time"]); })); it("should 'ce-dataschema' have the right value", - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getDataschema()) .to.equal(response.config.headers["ce-dataschema"]); })); it(`should 'ce-${ext1Name}' have the right value`, - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getExtensions()[ext1Name]) .to.equal(response.config.headers[`ce-${ext1Name}`]); })); it(`should 'ce-${ext2Name}' have the right value`, - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getExtensions()[ext2Name]) .to.equal(response.config.headers[`ce-${ext2Name}`]); })); it("should 'ce-subject' have the right value", - () => binary.emit(cloudevent) + () => binary.emit(httpcfg, cloudevent) .then((response) => { expect(cloudevent.getSubject()) .to.equal(response.config.headers["ce-subject"]); diff --git a/test/sdk_test.js b/test/sdk_test.js index 90e03870..4d8de50b 100644 --- a/test/sdk_test.js +++ b/test/sdk_test.js @@ -1,69 +1,18 @@ const expect = require("chai").expect; -const v03 = require("../v03/index.js"); -const v1 = require("../v1/index.js"); +const { CloudEvent } = require("../"); +const SpecV03 = require("../v03").Spec; +const SpecV1 = require("../v1").Spec; describe("The SDK Requirements", () => { describe("v0.3", () => { it("should create an event using the right spec version", () => { - expect(v03.event().spec.payload.specversion).to.equal("0.3"); - }); - - it("should exports 'Spec'", () => { - expect(v03).to.have.property("Spec"); - }); - - it("should exports 'StructuredHTTPEmitter'", () => { - expect(v03).to.have.property("StructuredHTTPEmitter"); - }); - - it("should exports 'StructuredHTTPReceiver'", () => { - expect(v03).to.have.property("StructuredHTTPReceiver"); - }); - - it("should exports 'BinaryHTTPEmitter'", () => { - expect(v03).to.have.property("BinaryHTTPEmitter"); - }); - - it("should exports 'BinaryHTTPReceiver'", () => { - expect(v03).to.have.property("BinaryHTTPReceiver"); - }); - - it("should exports 'HTTPUnmarshaller'", () => { - expect(v03).to.have.property("HTTPUnmarshaller"); - }); - - it("should exports 'event'", () => { - expect(v03).to.have.property("event"); + expect(new CloudEvent(SpecV03).spec.payload.specversion).to.equal("0.3"); }); }); describe("v1.0", () => { it("should create an event using the right spec version", () => { - expect(v1.event().spec.payload.specversion).to.equal("1.0"); - }); - - it("should exports 'Spec'", () => { - expect(v1).to.have.property("Spec"); - }); - - it("should exports 'StructuredHTTPEmitter'", () => { - expect(v1).to.have.property("StructuredHTTPEmitter"); - }); - - it("should exports 'StructuredHTTPReceiver'", () => { - expect(v1).to.have.property("StructuredHTTPReceiver"); - }); - - it("should exports 'BinaryHTTPEmitter'", () => { - expect(v1).to.have.property("BinaryHTTPEmitter"); - }); - - it("should exports 'BinaryHTTPReceiver'", () => { - expect(v1).to.have.property("BinaryHTTPReceiver"); - }); - - it("should exports 'event'", () => { - expect(v1).to.have.property("event"); + expect(new CloudEvent(SpecV1).spec.payload.specversion).to.equal("1.0"); }); }); }); diff --git a/v03/index.js b/v03/index.js index 0567070e..40ec1cbc 100644 --- a/v03/index.js +++ b/v03/index.js @@ -1,28 +1,5 @@ -const CloudEvent = require("../lib/cloudevent.js"); const Spec = require("../lib/specs/spec_0_3.js"); -const StructuredHTTPEmitter = - require("../lib/bindings/http/emitter_structured.js"); -const BinaryHTTPEmitter = require("../lib/bindings/http/emitter_binary_0_3.js"); - -const StructuredHTTPReceiver = - require("../lib/bindings/http/receiver_structured_0_3.js"); - -const BinaryHTTPReceiver = - require("../lib/bindings/http/receiver_binary_0_3.js"); - -const HTTPUnmarshaller = require("../lib/bindings/http/unmarshaller_0_3.js"); - -function newEvent() { - return new CloudEvent(Spec); -} module.exports = { - Spec, - StructuredHTTPEmitter, - StructuredHTTPReceiver, - BinaryHTTPEmitter, - BinaryHTTPReceiver, - HTTPUnmarshaller, - CloudEvent: newEvent, - event: newEvent + Spec }; diff --git a/v1/index.js b/v1/index.js index 3197dab5..e8330bcb 100644 --- a/v1/index.js +++ b/v1/index.js @@ -1,27 +1,5 @@ -const CloudEvent = require("../lib/cloudevent.js"); const Spec = require("../lib/specs/spec_1.js"); -const StructuredHTTPEmitter = - require("../lib/bindings/http/emitter_structured.js"); - -const BinaryHTTPEmitter = require("../lib/bindings/http/emitter_binary_1.js"); - -const StructuredHTTPReceiver = - require("../lib/bindings/http/receiver_structured_1.js"); - -const BinaryHTTPReceiver = - require("../lib/bindings/http/receiver_binary_1.js"); - -function newEvent() { - return new CloudEvent(Spec); -} - module.exports = { - Spec, - StructuredHTTPEmitter, - BinaryHTTPEmitter, - StructuredHTTPReceiver, - BinaryHTTPReceiver, - CloudEvent: newEvent, - event: newEvent + Spec }; From d7d1df5abdf2d5e210142fb5f066fe79d0d04340 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Tue, 12 May 2020 17:11:57 -0400 Subject: [PATCH 2/6] fixup: incorporate comments and add test Signed-off-by: Lance Ball --- lib/bindings/http/emitter_binary.js | 12 +-- lib/bindings/http/http_emitter.js | 7 +- test/bindings/http/http_emitter_test.js | 116 ++++++++++++++++++++++++ test/http_binding_1.js | 21 ++--- test/sdk_test.js | 25 ++++- 5 files changed, 157 insertions(+), 24 deletions(-) create mode 100644 test/bindings/http/http_emitter_test.js diff --git a/lib/bindings/http/emitter_binary.js b/lib/bindings/http/emitter_binary.js index 9c9ac276..5bed37d9 100644 --- a/lib/bindings/http/emitter_binary.js +++ b/lib/bindings/http/emitter_binary.js @@ -23,13 +23,16 @@ const defaults = { */ class BinaryHTTPEmitter { /** - * + * Create a new {BinaryHTTPEmitter} for the provided CloudEvent specification version. + * Once an instance is created for a given spec version, it may only be used to send + * events for that version. + * Default version is 1.0 * @param {string} version - the CloudEvent HTTP specification version. * Default: 1.0 */ constructor(version) { if (version === SPEC_V1) { - this.headerByGetter = require("./emitter_binary_1"); + this.headerByGetter = require("./emitter_binary_1.js"); this.extensionPrefix = BINARY_HEADERS_1.EXTENSIONS_PREFIX; } else if (version === SPEC_V03) { this.headerByGetter = require("./emitter_binary_0_3.js"); @@ -55,10 +58,7 @@ class BinaryHTTPEmitter { .filter((getter) => cloudevent[getter]()) .forEach((getter) => { const header = this.headerByGetter[getter]; - headers[header.name] = - header.parser( - cloudevent[getter]() - ); + headers[header.name] = header.parser(cloudevent[getter]()); }); // Set the cloudevent payload diff --git a/lib/bindings/http/http_emitter.js b/lib/bindings/http/http_emitter.js index 2b82fec0..8445c707 100644 --- a/lib/bindings/http/http_emitter.js +++ b/lib/bindings/http/http_emitter.js @@ -44,12 +44,11 @@ class HTTPEmitter { */ send(options, event, mode = "binary") { if (mode === "binary") { - this.binary.emit(options, event); + return this.binary.emit(options, event); } else if (mode === "structured") { - this.structured.emit(options, event); - } else { - throw new TypeError(`Unknown transport mode ${mode}.`); + return this.structured.emit(options, event); } + throw new TypeError(`Unknown transport mode ${mode}.`); } } diff --git a/test/bindings/http/http_emitter_test.js b/test/bindings/http/http_emitter_test.js new file mode 100644 index 00000000..21580b34 --- /dev/null +++ b/test/bindings/http/http_emitter_test.js @@ -0,0 +1,116 @@ +const { expect } = require("chai"); +const nock = require("nock"); + +const { + SPEC_V1, + SPEC_V03, + DEFAULT_CE_CONTENT_TYPE, + BINARY_HEADERS_03, + BINARY_HEADERS_1 +} = require("../../../lib/bindings/http/constants.js"); + +const { CloudEvent, HTTPEmitter } = require("../../../"); + +const V1Spec = require("../../../v1").Spec; +const V03Spec = require("../../../v03").Spec; + +const receiver = "https://cloudevents.io/"; +const type = "com.example.test"; +const source = "urn:event:from:myapi/resource/123"; +const ext1Name = "lunch"; +const ext1Value = "tacos"; +const ext2Name = "supper"; +const ext2Value = "sushi"; + +const data = { + lunchBreak: "noon" +}; + +describe("HTTP Transport Binding Emitter for CloudEvents", () => { + beforeEach(() => { + nock(receiver) + .post("/") + .reply(function(uri, requestBody) { + // return the request body and the headers so they can be + // examined in the test + if (typeof requestBody === "string") { + requestBody = JSON.parse(requestBody); + } + const returnBody = { ...requestBody, ...this.req.headers }; + return [ + 201, + returnBody + ]; + }); + }); + + describe("V1", () => { + const emitter = new HTTPEmitter(); + const event = new CloudEvent(V1Spec) + .type(type) + .source(source) + .time(new Date()) + .data(data) + .addExtension(ext1Name, ext1Value) + .addExtension(ext2Name, ext2Value); + + it("Sends a binary 1.0 CloudEvent by default", () => { + emitter.send({ url: receiver }, event) + .then((response) => { + // A binary message will have a ce-id header + expect(response.data[BINARY_HEADERS_1.ID]).to.equal(event.getId()); + expect(response.data[BINARY_HEADERS_1.SPEC_VERSION]).to.equal(SPEC_V1); + // A binary message will have a request body for the data + expect(response.data.lunchBreak).to.equal(data.lunchBreak); + }).catch(expect.fail); + }); + + it("Sends a structured 1.0 CloudEvent if created that way", () => { + emitter.send({ url: receiver }, event, "structured") + .then((response) => { + // A structured message will have a cloud event content type + expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE); + // Ensure other CE headers don't exist - just testing for ID + expect(response.data[BINARY_HEADERS_1.ID]).to.equal(undefined); + // The spec version would have been specified in the body + expect(response.data.specversion).to.equal(SPEC_V1); + expect(response.data.data.lunchBreak).to.equal(data.lunchBreak); + }).catch(expect.fail); + }); + }); + + describe("V03", () => { + const emitter = new HTTPEmitter(SPEC_V03); + const event = new CloudEvent(V03Spec) + .type(type) + .source(source) + .time(new Date()) + .data(data) + .addExtension(ext1Name, ext1Value) + .addExtension(ext2Name, ext2Value); + + it("Sends a binary 0.3 CloudEvent", () => { + emitter.send({ url: receiver }, event) + .then((response) => { + // A binary message will have a ce-id header + expect(response.data[BINARY_HEADERS_03.ID]).to.equal(event.getId()); + expect(response.data[BINARY_HEADERS_03.SPEC_VERSION]).to.equal(SPEC_V03); + // A binary message will have a request body for the data + expect(response.data.lunchBreak).to.equal(data.lunchBreak); + }).catch(expect.fail); + }); + + it("Sends a structured 0.3 CloudEvent", () => { + emitter.send({ url: receiver }, event, "structured") + .then((response) => { + // A structured message will have a cloud event content type + expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE); + // Ensure other CE headers don't exist - just testing for ID + expect(response.data[BINARY_HEADERS_03.ID]).to.equal(undefined); + // The spec version would have been specified in the body + expect(response.data.specversion).to.equal(SPEC_V03); + expect(response.data.data.lunchBreak).to.equal(data.lunchBreak); + }).catch(expect.fail); + }); + }); +}); diff --git a/test/http_binding_1.js b/test/http_binding_1.js index 505e533f..da640f6c 100644 --- a/test/http_binding_1.js +++ b/test/http_binding_1.js @@ -27,17 +27,16 @@ const ext1Value = "foobar"; const ext2Name = "extension2"; const ext2Value = "acme"; -const cloudevent = - new CloudEvent(Spec) - .type(type) - .source(source) - .dataContentType(ceContentType) - .subject("subject.ext") - .time(now) - .dataschema(dataschema) - .data(data) - .addExtension(ext1Name, ext1Value) - .addExtension(ext2Name, ext2Value); +const cloudevent = new CloudEvent(Spec) + .type(type) + .source(source) + .dataContentType(ceContentType) + .subject("subject.ext") + .time(now) + .dataschema(dataschema) + .data(data) + .addExtension(ext1Name, ext1Value) + .addExtension(ext2Name, ext2Value); const dataString = ")(*~^my data for ce#@#$%"; diff --git a/test/sdk_test.js b/test/sdk_test.js index 4d8de50b..4e88ebf2 100644 --- a/test/sdk_test.js +++ b/test/sdk_test.js @@ -1,18 +1,37 @@ const expect = require("chai").expect; -const { CloudEvent } = require("../"); +const { CloudEvent, HTTPReceiver, HTTPEmitter } = require("../"); const SpecV03 = require("../v03").Spec; const SpecV1 = require("../v1").Spec; +const { + SPEC_V03, + SPEC_V1 +} = require("../lib/bindings/http/constants.js"); describe("The SDK Requirements", () => { + it("should expose a CloudEvent type", () => { + const event = new CloudEvent(); + expect(event instanceof CloudEvent).to.equal(true); + }); + + it("should expose an HTTPReceiver type", () => { + const receiver = new HTTPReceiver(); + expect(receiver instanceof HTTPReceiver).to.equal(true); + }); + + it("should expose an HTTPEmitter type", () => { + const emitter = new HTTPEmitter(); + expect(emitter instanceof HTTPEmitter).to.equal(true); + }); + describe("v0.3", () => { it("should create an event using the right spec version", () => { - expect(new CloudEvent(SpecV03).spec.payload.specversion).to.equal("0.3"); + expect(new CloudEvent(SpecV03).spec.payload.specversion).to.equal(SPEC_V03); }); }); describe("v1.0", () => { it("should create an event using the right spec version", () => { - expect(new CloudEvent(SpecV1).spec.payload.specversion).to.equal("1.0"); + expect(new CloudEvent(SpecV1).spec.payload.specversion).to.equal(SPEC_V1); }); }); }); From 1c2856e4e429a1628125061c19104feb702070ca Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Tue, 12 May 2020 17:24:23 -0400 Subject: [PATCH 3/6] docs: add HTTPEmitter examples to README.md Signed-off-by: Lance Ball --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f856086f..7ec07852 100644 --- a/README.md +++ b/README.md @@ -62,25 +62,52 @@ console.log(receivedEvent.format()); #### Emitting Events -Currently, to emit events, you'll need to decide whether the event is in +To emit events, you'll need to decide whether the event should be sent in binary or structured format, and determine what version of the CloudEvents specification you want to send the event as. -```js -const { CloudEvent } = require("cloudevents-sdk"); -const { StructuredHTTPEmitter } = require("cloudevents-sdk/v1"); - -const myevent = new CloudEvent() - .type("com.github.pull.create") - .source("urn:event:from:myapi/resource/123"); +By default, the `HTTPEmitter` will emit events over HTTP POST using the +1.0 specification, in binary mode. You can emit 0.3 events by providing +the specication version in the constructor to `HTTPEmitter`. To send +structured events, add that string as a parameter to `emitter.sent()`. -const emitter = new StructuredHTTPEmitter({ - method: "POST", - url : "https://myserver.com" -}); +```js +const { CloudEvent, HTTPEmitter } = require("cloudevents-sdk"); + +// Without any parameters, this creates a v1 emitter +const v1Emitter = new HTTPEmitter(); +const event = new CloudEvent() + .type(type) + .source(source) + .time(new Date()) + .data(data) + +// By default, the emitter will send binary events +v1Emitter.send({ url: "https://cloudevents.io/example" }, event) + .then((response) => { + // handle the response + }) + .catch(console.error); + +// To send a structured event, just add that as a parameter +v1Emitter.send({ url: "https://cloudevents.io/example" }, event, "structured") + .then((response) => { + // handle the response + }) + .catch(console.error); + +// Sending a v0.3 event works the same, just let the emitter know when +// you create it that you are working with the 0.3 spec +const v03Emitter = new HTTPEmitter("0.3"); + +// Again, the default is to send binary events +// To send a structured event, add "structured" as a final parameter +v3Emitter.send({ url: "https://cloudevents.io/example" }, event) + .then((response) => { + // handle the response + }) + .catch(console.error); -// Emit the event -emitter.emit(myevent) ``` ## Supported specification features From 85f610ddb0185370902c3e6009c24f3ed6450c23 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Wed, 13 May 2020 15:09:13 -0400 Subject: [PATCH 4/6] fixup: make send accept an alternate URL Signed-off-by: Lance Ball --- lib/bindings/http/http_emitter.js | 29 ++++++---- test/bindings/http/http_emitter_test.js | 71 ++++++++++++++++++++++--- test/sdk_test.js | 4 +- 3 files changed, 86 insertions(+), 18 deletions(-) diff --git a/lib/bindings/http/http_emitter.js b/lib/bindings/http/http_emitter.js index 8445c707..fb8af5c1 100644 --- a/lib/bindings/http/http_emitter.js +++ b/lib/bindings/http/http_emitter.js @@ -18,35 +18,46 @@ class HTTPEmitter { * Creates a new instance of {HTTPEmitter}. The default emitter uses the 1.0 * protocol specification in binary mode. * - * @param {string} [version] The HTTP binding specification version. Default: "1.0" + * @param {Object} [options] The configuration options for this event emitter + * @param {URL} options.url The endpoint that will receive the sent events. + * @param {string} [options.version] The HTTP binding specification version. Default: "1.0" * @throws {TypeError} if no options.url is provided or an unknown specification version is provided. */ - constructor(version = SPEC_V1) { + constructor({ url, version = SPEC_V1 } = {}) { if (version !== SPEC_V03 && version !== SPEC_V1) { throw new TypeError( `Unknown CloudEvent specification version: ${version}`); } + if (!url) { + throw new TypeError("A default endpoint URL is required for a CloudEvent emitter"); + } this.binary = new BinaryHTTPEmitter(version); this.structured = new StructuredEmitter(); + this.url = url; } /** * Sends the {CloudEvent} to an event receiver over HTTP POST * - * @param {Object} options The configuration options for this event. Options + * @param {CloudEvent} event the CloudEvent to be sent + * @param {Object} [options] The configuration options for this event. Options * provided will be passed along to Node.js `http.request()`. * https://nodejs.org/api/http.html#http_http_request_options_callback - * @param {URL} options.url The HTTP/S url that should receive this event - * @param {CloudEvent} event the CloudEvent to be sent - * @param {string} [mode] the message mode for sending this event. + * @param {URL} [options.url] The HTTP/S url that should receive this event. + * The URL is optional if one was provided when this emitter was constructed. + * In that case, it will be used as the recipient endpoint. The endpoint can + * be overridden by providing a URL here. + * @param {string} [options.mode] the message mode for sending this event. * Possible values are "binary" and "structured". Default: structured * @returns {Promise} Promise with an eventual response from the receiver */ - send(options, event, mode = "binary") { + send(event, { url, mode = "binary", ...httpOpts } = {}) { + if (!url) { url = this.url; } + httpOpts.url = url; if (mode === "binary") { - return this.binary.emit(options, event); + return this.binary.emit(httpOpts, event); } else if (mode === "structured") { - return this.structured.emit(options, event); + return this.structured.emit(httpOpts, event); } throw new TypeError(`Unknown transport mode ${mode}.`); } diff --git a/test/bindings/http/http_emitter_test.js b/test/bindings/http/http_emitter_test.js index 21580b34..361f1f09 100644 --- a/test/bindings/http/http_emitter_test.js +++ b/test/bindings/http/http_emitter_test.js @@ -45,7 +45,7 @@ describe("HTTP Transport Binding Emitter for CloudEvents", () => { }); describe("V1", () => { - const emitter = new HTTPEmitter(); + const emitter = new HTTPEmitter({ url: receiver }); const event = new CloudEvent(V1Spec) .type(type) .source(source) @@ -55,7 +55,7 @@ describe("HTTP Transport Binding Emitter for CloudEvents", () => { .addExtension(ext2Name, ext2Value); it("Sends a binary 1.0 CloudEvent by default", () => { - emitter.send({ url: receiver }, event) + emitter.send(event) .then((response) => { // A binary message will have a ce-id header expect(response.data[BINARY_HEADERS_1.ID]).to.equal(event.getId()); @@ -65,8 +65,36 @@ describe("HTTP Transport Binding Emitter for CloudEvents", () => { }).catch(expect.fail); }); - it("Sends a structured 1.0 CloudEvent if created that way", () => { - emitter.send({ url: receiver }, event, "structured") + it("Sends a structured 1.0 CloudEvent if specified", () => { + emitter.send(event, { mode: "structured" }) + .then((response) => { + // A structured message will have a cloud event content type + expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE); + // Ensure other CE headers don't exist - just testing for ID + expect(response.data[BINARY_HEADERS_1.ID]).to.equal(undefined); + // The spec version would have been specified in the body + expect(response.data.specversion).to.equal(SPEC_V1); + expect(response.data.data.lunchBreak).to.equal(data.lunchBreak); + }).catch(expect.fail); + }); + + it("Sends to an alternate URL if specified", () => { + nock(receiver) + .post("/alternate") + .reply(function(uri, requestBody) { + // return the request body and the headers so they can be + // examined in the test + if (typeof requestBody === "string") { + requestBody = JSON.parse(requestBody); + } + const returnBody = { ...requestBody, ...this.req.headers }; + return [ + 201, + returnBody + ]; + }); + + emitter.send(event, { mode: "structured", url: `${receiver}alternate` }) .then((response) => { // A structured message will have a cloud event content type expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE); @@ -80,7 +108,7 @@ describe("HTTP Transport Binding Emitter for CloudEvents", () => { }); describe("V03", () => { - const emitter = new HTTPEmitter(SPEC_V03); + const emitter = new HTTPEmitter({ url: receiver, version: SPEC_V03 }); const event = new CloudEvent(V03Spec) .type(type) .source(source) @@ -90,7 +118,7 @@ describe("HTTP Transport Binding Emitter for CloudEvents", () => { .addExtension(ext2Name, ext2Value); it("Sends a binary 0.3 CloudEvent", () => { - emitter.send({ url: receiver }, event) + emitter.send(event) .then((response) => { // A binary message will have a ce-id header expect(response.data[BINARY_HEADERS_03.ID]).to.equal(event.getId()); @@ -100,8 +128,35 @@ describe("HTTP Transport Binding Emitter for CloudEvents", () => { }).catch(expect.fail); }); - it("Sends a structured 0.3 CloudEvent", () => { - emitter.send({ url: receiver }, event, "structured") + it("Sends a structured 0.3 CloudEvent if specified", () => { + emitter.send(event, { mode: "structured", foo: "bar" }) + .then((response) => { + // A structured message will have a cloud event content type + expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE); + // Ensure other CE headers don't exist - just testing for ID + expect(response.data[BINARY_HEADERS_03.ID]).to.equal(undefined); + // The spec version would have been specified in the body + expect(response.data.specversion).to.equal(SPEC_V03); + expect(response.data.data.lunchBreak).to.equal(data.lunchBreak); + }).catch(expect.fail); + }); + it("Sends to an alternate URL if specified", () => { + nock(receiver) + .post("/alternate") + .reply(function(uri, requestBody) { + // return the request body and the headers so they can be + // examined in the test + if (typeof requestBody === "string") { + requestBody = JSON.parse(requestBody); + } + const returnBody = { ...requestBody, ...this.req.headers }; + return [ + 201, + returnBody + ]; + }); + + emitter.send(event, { mode: "structured", url: `${receiver}alternate` }) .then((response) => { // A structured message will have a cloud event content type expect(response.data["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE); diff --git a/test/sdk_test.js b/test/sdk_test.js index 4e88ebf2..7ef660c8 100644 --- a/test/sdk_test.js +++ b/test/sdk_test.js @@ -19,7 +19,9 @@ describe("The SDK Requirements", () => { }); it("should expose an HTTPEmitter type", () => { - const emitter = new HTTPEmitter(); + const emitter = new HTTPEmitter({ + url: "http://example.com" + }); expect(emitter instanceof HTTPEmitter).to.equal(true); }); From ffed52d0599247f8ed678194dc1b7ee12efbb617 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Wed, 13 May 2020 15:21:02 -0400 Subject: [PATCH 5/6] docs: Update HTTPEmitter examples in the README Signed-off-by: Lance Ball --- README.md | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7ec07852..9cd673b7 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,10 @@ structured events, add that string as a parameter to `emitter.sent()`. ```js const { CloudEvent, HTTPEmitter } = require("cloudevents-sdk"); -// Without any parameters, this creates a v1 emitter -const v1Emitter = new HTTPEmitter(); +// With only an endpoint URL, this creates a v1 emitter +const v1Emitter = new HTTPEmitter({ + url: "https://cloudevents.io/example" +}); const event = new CloudEvent() .type(type) .source(source) @@ -83,30 +85,36 @@ const event = new CloudEvent() .data(data) // By default, the emitter will send binary events -v1Emitter.send({ url: "https://cloudevents.io/example" }, event) +v1Emitter.send(event).then((response) => { + // handle the response + }).catch(console.error); + +// To send a structured event, just add that as an option +v1Emitter.send(event, { mode: "structured" }) .then((response) => { // handle the response - }) - .catch(console.error); + }).catch(console.error); -// To send a structured event, just add that as a parameter -v1Emitter.send({ url: "https://cloudevents.io/example" }, event, "structured") +// To send an event to an alternate URL, add that as an option +v1Emitter.send(event, { url: "https://alternate.com/api" }) .then((response) => { // handle the response - }) - .catch(console.error); + }).catch(console.error); // Sending a v0.3 event works the same, just let the emitter know when // you create it that you are working with the 0.3 spec -const v03Emitter = new HTTPEmitter("0.3"); +const v03Emitter = new HTTPEmitter({ + url: "https://cloudevents.io/example", + version: "0.3" +}); // Again, the default is to send binary events -// To send a structured event, add "structured" as a final parameter -v3Emitter.send({ url: "https://cloudevents.io/example" }, event) +// To send a structured event or to an alternate URL, provide those +// as parameters in a options object as above +v3Emitter.send(event) .then((response) => { // handle the response - }) - .catch(console.error); + }).catch(console.error); ``` From 0b27c616fd22f53dc54823b4f431291eedd13794 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Wed, 13 May 2020 18:05:17 -0400 Subject: [PATCH 6/6] squash: add headers() to the HTTPEmitter function Fixes: https://github.com/cloudevents/sdk-javascript/issues/149 Signed-off-by: Lance Ball --- lib/bindings/http/http_emitter.js | 20 ++++++++++++++++++++ test/bindings/http/http_emitter_test.js | 18 ++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/lib/bindings/http/http_emitter.js b/lib/bindings/http/http_emitter.js index fb8af5c1..c3d441ba 100644 --- a/lib/bindings/http/http_emitter.js +++ b/lib/bindings/http/http_emitter.js @@ -61,6 +61,26 @@ class HTTPEmitter { } throw new TypeError(`Unknown transport mode ${mode}.`); } + + /** + * Returns the HTTP headers that will be sent for this event when the HTTP transmission + * mode is "binary". Events sent over HTTP in structured mode only have a single CE header + * and that is "ce-id", corresponding to the event ID. + * @param {CloudEvent} event a CloudEvent + * @returns {Object} the headers that will be sent for the event + */ + headers(event) { + const headers = {}; + + Object.keys(this.binary.headerByGetter) + .filter((getter) => event[getter]()) + .forEach((getter) => { + const header = this.binary.headerByGetter[getter]; + headers[header.name] = header.parser(event[getter]()); + }); + + return headers; + } } module.exports = HTTPEmitter; diff --git a/test/bindings/http/http_emitter_test.js b/test/bindings/http/http_emitter_test.js index 361f1f09..ffae9e37 100644 --- a/test/bindings/http/http_emitter_test.js +++ b/test/bindings/http/http_emitter_test.js @@ -65,6 +65,15 @@ describe("HTTP Transport Binding Emitter for CloudEvents", () => { }).catch(expect.fail); }); + it("Provides the HTTP headers for a binary event", () => { + const headers = emitter.headers(event); + expect(headers[BINARY_HEADERS_1.TYPE]).to.equal(event.getType()); + expect(headers[BINARY_HEADERS_1.SPEC_VERSION]).to.equal(event.getSpecversion()); + expect(headers[BINARY_HEADERS_1.SOURCE]).to.equal(event.getSource()); + expect(headers[BINARY_HEADERS_1.ID]).to.equal(event.getId()); + expect(headers[BINARY_HEADERS_1.TIME]).to.equal(event.getTime()); + }); + it("Sends a structured 1.0 CloudEvent if specified", () => { emitter.send(event, { mode: "structured" }) .then((response) => { @@ -128,6 +137,15 @@ describe("HTTP Transport Binding Emitter for CloudEvents", () => { }).catch(expect.fail); }); + it("Provides the HTTP headers for a binary event", () => { + const headers = emitter.headers(event); + expect(headers[BINARY_HEADERS_03.TYPE]).to.equal(event.getType()); + expect(headers[BINARY_HEADERS_03.SPEC_VERSION]).to.equal(event.getSpecversion()); + expect(headers[BINARY_HEADERS_03.SOURCE]).to.equal(event.getSource()); + expect(headers[BINARY_HEADERS_03.ID]).to.equal(event.getId()); + expect(headers[BINARY_HEADERS_03.TIME]).to.equal(event.getTime()); + }); + it("Sends a structured 0.3 CloudEvent if specified", () => { emitter.send(event, { mode: "structured", foo: "bar" }) .then((response) => {