diff --git a/src/event/validation.ts b/src/event/validation.ts index 663e034d..8665304a 100644 --- a/src/event/validation.ts +++ b/src/event/validation.ts @@ -5,7 +5,7 @@ import { ErrorObject } from "ajv"; -export type TypeArray = Int8Array | Uint8Array | Int16Array | Uint16Array | +export type TypeArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array; const globalThisPolyfill = (function() { @@ -103,7 +103,7 @@ export const base64AsBinary = (base64String: string): Uint8Array => { return Uint8Array.from(toBinaryString(base64String), (c) => c.charCodeAt(0)); }; -export const asBase64 = +export const asBase64 = (value: string | Buffer | TypeArray): string => asBuffer(value).toString("base64"); export const clone = (o: Record): Record => JSON.parse(JSON.stringify(o)); diff --git a/src/parsers.ts b/src/parsers.ts index 7a050672..90b26e52 100644 --- a/src/parsers.ts +++ b/src/parsers.ts @@ -89,11 +89,13 @@ export interface MappedParser { export class DateParser extends Parser { parse(payload: string): string { - let date = new Date(Date.parse(payload)); - if (date.toString() === "Invalid Date") { - date = new Date(); + const timestamp = Date.parse(payload); + if (isNaN(timestamp)) { + // Deprecated: substituting the current time for an unparseable time hides an + // invalid message. The spec requires RFC 3339, so v11 will throw a ValidationError. + return new Date().toISOString(); } - return date.toISOString(); + return new Date(timestamp).toISOString(); } } diff --git a/test/conformance/steps.ts b/test/conformance/steps.ts index 7d53a566..a72203ed 100644 --- a/test/conformance/steps.ts +++ b/test/conformance/steps.ts @@ -71,7 +71,11 @@ Then("the attributes are:", function (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()); + if (this.cloudevent.time && expected.time) { + assert.equal(new Date(this.cloudevent.time).toISOString(), new Date(expected.time).toISOString()); + } else { + assert.equal(this.cloudevent.time, expected.time); + } assert.equal(this.cloudevent.specversion, expected.specversion); assert.equal(this.cloudevent.datacontenttype, expected.datacontenttype); return true; diff --git a/test/integration/parser_test.ts b/test/integration/parser_test.ts index 5be19b3e..39e72932 100644 --- a/test/integration/parser_test.ts +++ b/test/integration/parser_test.ts @@ -6,8 +6,9 @@ import "mocha"; import { expect } from "chai"; -import { JSONParser as Parser } from "../../src/parsers"; -import { ValidationError } from "../../src/"; +import { DateParser, JSONParser as Parser } from "../../src/parsers"; +import { CloudEvent, ValidationError } from "../../src/"; +import { HTTP } from "../../src/message"; describe("JSON Event Format Parser", () => { it("Throw error when payload is an integer", () => { @@ -79,3 +80,39 @@ describe("JSON Event Format Parser", () => { expect(actual).to.be.an("object"); }); }); + +describe("Date Parser", () => { + const parser = new DateParser(); + + it("Returns an RFC 3339 UTC timestamp unchanged", () => { + expect(parser.parse("2018-04-05T17:31:00.000Z")).to.equal("2018-04-05T17:31:00.000Z"); + }); + + it("Normalizes an RFC 3339 timestamp with an offset to UTC", () => { + expect(parser.parse("2018-04-05T17:31:00+02:00")).to.equal("2018-04-05T15:31:00.000Z"); + }); + + it("Falls back to the current time when the payload is not a date", () => { + const before = Date.now(); + const parsed = Date.parse(parser.parse("not a date")); + const after = Date.now(); + expect(parsed).to.be.at.least(before).and.at.most(after); + }); + + it("Does not throw when a binary message carries an unparseable ce-time header", () => { + const message = { + headers: { + "ce-id": "1234", + "ce-source": "test", + "ce-type": "test.event", + "ce-specversion": "1.0", + "ce-time": "garbage", + "content-type": "application/json", + }, + body: "{}", + }; + const event = HTTP.toEvent(message) as CloudEvent; + expect(Number.isNaN(Date.parse(event.time as string))).to.equal(false); + expect(() => event.validate()).not.to.throw(); + }); +});