test: inplement the cucumber conformance tests from cloudevents/spec (#238)

This commit adds cucumber-js conformance steps and includes the cucumber
tests in 'npm test'.

Signed-off-by: Lance Ball <lball@redhat.com>
This commit is contained in:
Lance Ball 2020-07-13 09:47:02 -04:00 committed by GitHub
parent 90a998472c
commit dca2811627
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1425 additions and 52 deletions

0
.gitmodules vendored Normal file
View File

11
cucumber.js Normal file
View File

@ -0,0 +1,11 @@
// cucumber.js
let common = [
"--require-module ts-node/register", // Load TypeScript module
"--require test/conformance/steps.ts", // Load step definitions
"--format progress-bar", // Load custom formatter
"--format node_modules/cucumber-pretty", // Load custom formatter
].join(" ");
module.exports = {
default: common,
};

1289
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,9 @@
"build": "tsc --project tsconfig.json && tsc --project tsconfig.browser.json && webpack",
"lint": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}'",
"lint:fix": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}' --fix",
"pretest": "npm run lint",
"test": "mocha --require ts-node/register ./test/**/*.ts",
"pretest": "npm run lint && npm run conformance",
"test": "mocha --require ts-node/register ./test/integration/**/*.ts",
"conformance": "npx downtotemp https://raw.githubusercontent.com/cloudevents/conformance/master/features/http-protocol-binding.feature && cucumber-js /tmp/http-protocol-binding.feature -p default",
"coverage": "nyc --reporter=lcov --reporter=text npm run test",
"coverage-publish": "wget -qO - https://coverage.codacy.com/get.sh | bash -s report -l JavaScript -r coverage/lcov.info",
"generate-docs": "typedoc --excludeNotDocumented --out docs src",
@ -103,18 +104,24 @@
"@types/ajv": "^1.0.0",
"@types/axios": "^0.14.0",
"@types/chai": "^4.2.11",
"@types/cucumber": "^6.0.1",
"@types/mocha": "^7.0.2",
"@types/node": "^13.13.9",
"@types/uuid": "^8.0.0",
"@typescript-eslint/eslint-plugin": "^3.4.0",
"@typescript-eslint/parser": "^3.4.0",
"chai": "~4.2.0",
"cucumber": "^6.0.5",
"cucumber-pretty": "^6.0.0",
"cucumber-tsflow": "^3.2.0",
"downtotemp": "^0.1.2",
"eslint": "^7.3.0",
"eslint-config-prettier": "^6.11.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.1.4",
"http-parser-js": "^0.5.2",
"mocha": "~7.1.1",
"nock": "~12.0.3",
"nyc": "~15.0.0",

View File

@ -11,10 +11,15 @@ export * from "./date";
export * from "./mapped";
export * from "./pass_through";
const jsonParser = new JSONParser();
const passThroughParser = new PassThroughParser();
export const parserByContentType: { [key: string]: Parser } = {
[CONSTANTS.MIME_JSON]: new JSONParser(),
[CONSTANTS.MIME_CE_JSON]: new JSONParser(),
[CONSTANTS.MIME_OCTET_STREAM]: new PassThroughParser(),
[CONSTANTS.MIME_JSON]: jsonParser,
[CONSTANTS.MIME_CE_JSON]: jsonParser,
[CONSTANTS.DEFAULT_CONTENT_TYPE]: jsonParser,
[CONSTANTS.DEFAULT_CE_CONTENT_TYPE]: jsonParser,
[CONSTANTS.MIME_OCTET_STREAM]: passThroughParser,
};
export const parserByEncoding: { [key: string]: { [key: string]: Parser } } = {

View File

@ -62,12 +62,15 @@ export class BinaryHTTPReceiver {
}
const parser = parserByContentType[eventObj.datacontenttype as string];
if (!parser) {
throw new ValidationError(`no parser found for content type ${eventObj.datacontenttype}`);
}
const parsedPayload = parser.parse(payload);
// Every unprocessed header can be an extension
for (const header in sanitizedHeaders) {
if (header.startsWith(CONSTANTS.EXTENSIONS_PREFIX)) {
eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = sanitizedHeaders[header];
eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = headers[header];
}
}

View File

@ -12,7 +12,7 @@ export interface Headers {
[key: string]: string;
}
export const allowedContentTypes = [CONSTANTS.MIME_JSON, CONSTANTS.MIME_OCTET_STREAM];
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,
@ -97,16 +97,5 @@ export function sanitize(headers: Headers): Headers {
.filter((header) => Object.hasOwnProperty.call(headers, header))
.forEach((header) => (sanitized[header.toLowerCase()] = headers[header]));
sanitized[CONSTANTS.HEADER_CONTENT_TYPE] = sanitizeContentType(sanitized[CONSTANTS.HEADER_CONTENT_TYPE]) as string;
return sanitized;
}
function sanitizeContentType(contentType: string): string | undefined {
if (contentType) {
return Array.of(contentType)
.map((c) => c.split(";"))
.map((c) => c.shift())
.shift();
}
return contentType;
}

69
test/conformance/steps.ts Normal file
View File

@ -0,0 +1,69 @@
/* 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";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { HTTPParser } = require("http-parser-js");
const parser = new HTTPParser(HTTPParser.REQUEST);
const receiver = new Receiver();
Given("HTTP Protocol Binding is supported", function (this: World) {
return true;
});
Given("an HTTP request", function (request: string) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const world = this;
parser.onHeadersComplete = function (record: Record<string, []>) {
world.headers = arrayToObject(record.headers);
};
parser.onBody = function (body: Buffer, offset: number) {
world.body = body.slice(offset).toString();
};
parser.execute(Buffer.from(request), 0, request.length);
return true;
});
When("parsed as HTTP request", function () {
this.cloudevent = receiver.accept(this.headers, this.body);
return true;
});
Then("the attributes are:", function (attributes: { rawTable: [] }) {
const expected = tableToObject(attributes.rawTable);
assert.equal(this.cloudevent.id, expected.id);
assert.equal(this.cloudevent.type, expected.type);
assert.equal(this.cloudevent.source, expected.source);
assert.equal(this.cloudevent.time, new Date(expected.time).toISOString());
assert.equal(this.cloudevent.specversion, expected.specversion);
assert.equal(this.cloudevent.datacontenttype, expected.datacontenttype);
return true;
});
Then("the data is equal to the following JSON:", function (json: string) {
assert.deepEqual(this.cloudevent.data, JSON.parse(json));
return true;
});
function arrayToObject(arr: []): Record<string, string> {
const obj: Record<string, string> = {};
// @ts-ignore
return arr.reduce(({}, keyOrValue, index, arr) => {
if (index % 2 === 0) {
obj[keyOrValue] = arr[index + 1];
}
return obj;
});
}
function tableToObject(table: []): Record<string, string> {
const obj: Record<string, string> = {};
// @ts-ignore
return table.reduce(({}, [key, value]: Array<string, string>) => {
obj[key] = value;
return obj;
});
}

View File

@ -1,7 +1,7 @@
import { expect } from "chai";
import { CloudEvent, Version } from "../src";
import { CloudEventV03 } from "../src/event/v03";
import { CloudEventV1 } from "../src/event/v1";
import { CloudEvent, Version } from "../../src";
import { CloudEventV03 } from "../../src/event/v03";
import { CloudEventV1 } from "../../src/event/v1";
const type = "org.cncf.cloudevents.example";
const source = "http://unit.test";

View File

@ -1,5 +1,5 @@
import { expect } from "chai";
import CONSTANTS from "../src/constants";
import CONSTANTS from "../../src/constants";
describe("Constants exposed by top level exports", () => {
it("Exports an ENCODING_BASE64 constant", () => {

View File

@ -2,8 +2,8 @@ import "mocha";
import { expect } from "chai";
import nock from "nock";
import { emitBinary, emitStructured } from "../src/transport/http";
import { CloudEvent, Version } from "../src";
import { emitBinary, emitStructured } from "../../src/transport/http";
import { CloudEvent, Version } from "../../src";
import { AxiosResponse } from "axios";
const type = "com.github.pull.create";

View File

@ -3,9 +3,9 @@ import "mocha";
import { expect } from "chai";
import nock from "nock";
import { CloudEvent, Version } from "../src";
import { emitBinary, emitStructured } from "../src/transport/http";
import { asBase64 } from "../src/event/validation/is";
import { CloudEvent, Version } from "../../src";
import { emitBinary, emitStructured } from "../../src/transport/http";
import { asBase64 } from "../../src/event/validation/is";
import { AxiosResponse } from "axios";
const type = "com.github.pull.create";

View File

@ -1,11 +1,11 @@
import "mocha";
import { expect } from "chai";
import nock from "nock";
import CONSTANTS from "../src/constants";
import CONSTANTS from "../../src/constants";
const DEFAULT_CE_CONTENT_TYPE = CONSTANTS.DEFAULT_CE_CONTENT_TYPE;
import { CloudEvent, Version, Emitter, Protocol, headersFor } from "../src";
import { CloudEvent, Version, Emitter, Protocol, headersFor } from "../../src";
import { AxiosResponse } from "axios";
const receiver = "https://cloudevents.io/";

View File

@ -1,7 +1,7 @@
import "mocha";
import { expect } from "chai";
import { CloudEvent, Receiver, ValidationError } from "../src";
import { CloudEventV1 } from "../src/event/v1";
import { CloudEvent, Receiver, ValidationError } from "../../src";
import { CloudEventV1 } from "../../src/event/v1";
const receiver = new Receiver();
const id = "1234";

View File

@ -1,8 +1,8 @@
import "mocha";
import { expect } from "chai";
import { JSONParser as Parser } from "../src/parsers/";
import { ValidationError } from "../src/";
import { JSONParser as Parser } from "../../src/parsers/";
import { ValidationError } from "../../src/";
describe("JSON Event Format Parser", () => {
it("Throw error when payload is an integer", () => {

View File

@ -1,9 +1,9 @@
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 { CloudEvent, ValidationError, Version } from "../../src";
import { BinaryHTTPReceiver } from "../../src/transport/http/binary_receiver";
import CONSTANTS from "../../src/constants";
const receiver = new BinaryHTTPReceiver(Version.V03);

View File

@ -1,10 +1,10 @@
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";
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);

View File

@ -1,8 +1,8 @@
import "mocha";
import { expect } from "chai";
import { CloudEvent, ValidationError, Version } from "../src";
import { StructuredHTTPReceiver } from "../src/transport/http/structured_receiver";
import { CloudEvent, ValidationError, Version } from "../../src";
import { StructuredHTTPReceiver } from "../../src/transport/http/structured_receiver";
const receiver = new StructuredHTTPReceiver(Version.V03);
const type = "com.github.pull.create";

View File

@ -1,9 +1,9 @@
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";
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";

View File

@ -1,6 +1,6 @@
import "mocha";
import { expect } from "chai";
import { CloudEvent, Receiver, Emitter, Version } from "../src";
import { CloudEvent, Receiver, Emitter, Version } from "../../src";
const fixture = {
type: "org.cloudevents.test",

View File

@ -1,7 +1,7 @@
import "mocha";
import { expect } from "chai";
import { CloudEvent, Version, ValidationError, Mode } from "../src";
import Constants from "../src/constants";
import { CloudEvent, Version, ValidationError, Mode } from "../../src";
import Constants from "../../src/constants";
const id = "97699ec2-a8d9-47c1-bfa0-ff7aa526f838";
const type = "com.github.pull.create";

View File

@ -1,8 +1,8 @@
import "mocha";
import { expect } from "chai";
import { CloudEvent, Version, ValidationError } from "../src";
import { asBase64 } from "../src/event/validation";
import Constants from "../src/constants";
import { CloudEvent, Version, ValidationError } from "../../src";
import { asBase64 } from "../../src/event/validation";
import Constants from "../../src/constants";
const id = "97699ec2-a8d9-47c1-bfa0-ff7aa526f838";
const type = "com.github.pull.create";

View File

@ -1,6 +1,6 @@
import "mocha";
import { expect } from "chai";
import { isStringOrThrow, equalsOrThrow, isBase64, asData } from "../src/event/validation/is";
import { isStringOrThrow, equalsOrThrow, isBase64, asData } from "../../src/event/validation/is";
describe("Utilities", () => {
describe("isStringOrThrow", () => {