@relaybus/testing 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  Test utilities for relaybus.
6
6
 
7
7
  ```sh
8
- pnpm add -D @relaybus/testing vitest
8
+ pnpm add -D @relaybus/testing vitest # or jest: see the codec contract below
9
9
  ```
10
10
 
11
11
  ## Store contract suite
@@ -69,7 +69,22 @@ kafkaCodecContract('orders format', {
69
69
 
70
70
  `rabbitMqCodecContract` takes the same shape with `decodeRabbitMqMessage` from `@relaybus/rabbitmq` and captured messages
71
71
  (routing key, properties, headers, body). `kafkaCodecMismatches` and `rabbitMqCodecMismatches` return the differences as
72
- text lines instead of registering tests.
72
+ text lines instead of registering tests. Compared byte for byte, the decoded payload must also keep the expected key order.
73
+
74
+ The contract does not need Vitest:
75
+
76
+ ```ts
77
+ // Jest: registers with Jest's describe and it globals
78
+ import { kafkaCodecContract } from '@relaybus/testing/jest';
79
+
80
+ // Any runner, or a plain script: no test framework imported
81
+ import {
82
+ assertKafkaCodecCase, // throws CodecContractError listing the mismatches
83
+ registerKafkaCodecContract, // registerKafkaCodecContract({ describe, it }, { name, options })
84
+ } from '@relaybus/testing/codec-contract';
85
+ ```
86
+
87
+ The RabbitMQ counterparts are `assertRabbitMqCodecCase` and `registerRabbitMqCodecContract`.
73
88
 
74
89
  ## FakeClock
75
90
 
@@ -92,7 +107,7 @@ JSON copies to subscribers with `deliver(destination, envelope, maxRedeliveries)
92
107
 
93
108
  ## When to use @relaybus/testing, and when not to
94
109
 
95
- Use it in your test suite for fast, deterministic tests, or to prove that a custom `EventStore` satisfies the contract. It is a development dependency and requires `vitest` 3 or newer.
110
+ Use it in your test suite for fast, deterministic tests, or to prove that a custom `EventStore` satisfies the contract. It is a development dependency. The store contract, the main entry and `kafkaCodecContract` / `rabbitMqCodecContract` need `vitest` 3 or newer (an optional peer); `@relaybus/testing/jest` and `@relaybus/testing/codec-contract` do not.
96
111
 
97
112
  Documentation: <https://berkcansavur.github.io/relaybus/packages/testing>
98
113
 
@@ -0,0 +1,230 @@
1
+ 'use strict';
2
+
3
+ var core = require('@relaybus/core');
4
+
5
+ // src/codec-contract/compare.ts
6
+ function toBuffer(bytes) {
7
+ return typeof bytes === "string" ? Buffer.from(bytes, "utf8") : Buffer.from(bytes);
8
+ }
9
+ function canonicalJson(value) {
10
+ return JSON.stringify(sortKeys(JSON.parse(JSON.stringify(value) ?? "null")));
11
+ }
12
+ function sortKeys(value) {
13
+ if (Array.isArray(value)) {
14
+ return value.map(sortKeys);
15
+ }
16
+ if (typeof value !== "object" || value === null) {
17
+ return value;
18
+ }
19
+ return Object.fromEntries(
20
+ Object.keys(value).sort().map((name) => [name, sortKeys(value[name])])
21
+ );
22
+ }
23
+ function compareBodies(label, {
24
+ expected,
25
+ encoded,
26
+ comparison
27
+ }) {
28
+ const text = (bytes) => bytes === null ? "null" : toBuffer(bytes).toString("utf8");
29
+ const same = expected === null || encoded === null ? expected === encoded : comparison === "json" ? canonicalJson(JSON.parse(text(expected))) === canonicalJson(JSON.parse(text(encoded))) : toBuffer(expected).equals(toBuffer(encoded));
30
+ return same ? [] : [`${label}: expected ${text(expected)}, encoded ${text(encoded)}`];
31
+ }
32
+ function compareObjects(label, {
33
+ expected,
34
+ encoded,
35
+ ignored
36
+ }) {
37
+ const kept = (object) => canonicalJson(
38
+ Object.fromEntries(Object.entries(object).filter(([name]) => !ignored.includes(name)))
39
+ );
40
+ const [left, right] = [kept(expected), kept(encoded)];
41
+ return left === right ? [] : [`${label}: expected ${left}, encoded ${right}`];
42
+ }
43
+ function compareDecoded(expected, {
44
+ decoded,
45
+ comparison
46
+ }) {
47
+ const [left, right] = [canonicalJson(expected), canonicalJson(decoded)];
48
+ if (left !== right) {
49
+ return [`decoded: expected ${left}, decoded ${right}`];
50
+ }
51
+ const [payload, decodedPayload] = [
52
+ JSON.stringify(expected.payload),
53
+ JSON.stringify(decoded.payload)
54
+ ];
55
+ return comparison === "json" || payload === decodedPayload ? [] : [
56
+ `decoded payload: expected ${payload}, decoded ${decodedPayload} (same values, other key order)`
57
+ ];
58
+ }
59
+ async function attempt(label, step) {
60
+ try {
61
+ return { result: await step(), mismatches: [] };
62
+ } catch (error) {
63
+ return { mismatches: [`${label} threw: ${core.toErrorInfo(error).message}`] };
64
+ }
65
+ }
66
+ var CodecContractError = class extends core.RelayBusError {
67
+ name = "CodecContractError";
68
+ code = "RELAYBUS_TESTING_CODEC_CONTRACT";
69
+ mismatches;
70
+ constructor(message, mismatches = []) {
71
+ super(message);
72
+ this.mismatches = mismatches;
73
+ }
74
+ };
75
+ function assertNoMismatches(caseName, mismatches) {
76
+ if (mismatches.length > 0) {
77
+ const lines = mismatches.map((line) => `- ${line}`).join("\n");
78
+ throw new CodecContractError(`Codec case '${caseName}' does not match:
79
+ ${lines}`, mismatches);
80
+ }
81
+ }
82
+ function registerCases(framework, {
83
+ title,
84
+ cases,
85
+ check
86
+ }) {
87
+ framework.describe(title, () => {
88
+ for (const testCase of cases) {
89
+ framework.it(testCase.name, () => check(testCase));
90
+ }
91
+ });
92
+ }
93
+
94
+ // src/codec-contract/kafka.ts
95
+ async function kafkaCodecMismatches(options, testCase) {
96
+ const { record } = testCase;
97
+ const encoding = testCase.only === "decode" ? { mismatches: [] } : await attempt("encode", () => options.encode(testCase.envelope));
98
+ const encoded = encoding.result;
99
+ const decoding = testCase.only === "encode" ? { mismatches: [] } : await attempt("decode", () => options.decode(delivered(record)));
100
+ return [
101
+ ...encoding.mismatches,
102
+ ...encoded === void 0 ? [] : compareRecords(options, { record, encoded }),
103
+ ...decoding.mismatches,
104
+ ...decoding.result === void 0 ? [] : compareDecoded(testCase.decoded ?? testCase.envelope, {
105
+ decoded: decoding.result,
106
+ comparison: options.compare ?? "bytes"
107
+ })
108
+ ];
109
+ }
110
+ async function assertKafkaCodecCase(options, testCase) {
111
+ assertNoMismatches(testCase.name, await kafkaCodecMismatches(options, testCase));
112
+ }
113
+ function registerKafkaCodecContract(framework, { name, options }) {
114
+ registerCases(framework, {
115
+ title: `${name} writes and reads the captured Kafka records`,
116
+ cases: options.cases,
117
+ check: (testCase) => assertKafkaCodecCase(options, testCase)
118
+ });
119
+ }
120
+ function compareRecords(options, {
121
+ record,
122
+ encoded
123
+ }) {
124
+ const text = (bytes) => bytes === null ? null : toBuffer(bytes).toString("utf8");
125
+ const timestamp = encoded.timestamp;
126
+ return [
127
+ ...text(record.key) === text(encoded.key) ? [] : [`key: expected ${text(record.key)}, encoded ${text(encoded.key)}`],
128
+ ...compareBodies("value", {
129
+ expected: record.value,
130
+ encoded: encoded.value,
131
+ comparison: options.compare ?? "bytes"
132
+ }),
133
+ ...compareObjects("headers", {
134
+ expected: textHeaders(record.headers ?? {}),
135
+ encoded: textHeaders(encoded.headers),
136
+ ignored: options.ignoreHeaders ?? []
137
+ }),
138
+ ...timestamp === void 0 || timestamp === record.timestamp ? [] : [`timestamp: expected ${record.timestamp}, encoded ${timestamp}`]
139
+ ];
140
+ }
141
+ function textHeaders(headers) {
142
+ return Object.fromEntries(
143
+ Object.entries(headers).map(([name, value]) => [
144
+ name,
145
+ toBuffer(Array.isArray(value) ? value.at(-1) : value).toString(
146
+ "utf8"
147
+ )
148
+ ])
149
+ );
150
+ }
151
+ function delivered(record) {
152
+ return {
153
+ topic: record.topic,
154
+ partition: record.partition ?? 0,
155
+ offset: record.offset ?? "0",
156
+ key: record.key === null ? null : toBuffer(record.key),
157
+ value: record.value === null ? null : toBuffer(record.value),
158
+ headers: Object.fromEntries(
159
+ Object.entries(record.headers ?? {}).map(([name, value]) => [name, toBuffer(value)])
160
+ ),
161
+ ...record.timestamp === void 0 ? {} : { timestamp: record.timestamp }
162
+ };
163
+ }
164
+
165
+ // src/codec-contract/rabbitmq.ts
166
+ async function rabbitMqCodecMismatches(options, testCase) {
167
+ const { message } = testCase;
168
+ const encoding = testCase.only === "decode" ? { mismatches: [] } : await attempt("encode", () => options.encode(testCase.envelope));
169
+ const encoded = encoding.result;
170
+ const decoding = testCase.only === "encode" ? { mismatches: [] } : await attempt("decode", () => options.decode(delivered2(message)));
171
+ return [
172
+ ...encoding.mismatches,
173
+ ...encoded === void 0 ? [] : compareMessages(options, { message, encoded }),
174
+ ...decoding.mismatches,
175
+ ...decoding.result === void 0 ? [] : compareDecoded(testCase.decoded ?? testCase.envelope, {
176
+ decoded: decoding.result,
177
+ comparison: options.compare ?? "bytes"
178
+ })
179
+ ];
180
+ }
181
+ async function assertRabbitMqCodecCase(options, testCase) {
182
+ assertNoMismatches(testCase.name, await rabbitMqCodecMismatches(options, testCase));
183
+ }
184
+ function registerRabbitMqCodecContract(framework, { name, options }) {
185
+ registerCases(framework, {
186
+ title: `${name} writes and reads the captured RabbitMQ messages`,
187
+ cases: options.cases,
188
+ check: (testCase) => assertRabbitMqCodecCase(options, testCase)
189
+ });
190
+ }
191
+ function compareMessages(options, {
192
+ message,
193
+ encoded
194
+ }) {
195
+ const { headers, ...properties } = encoded.options;
196
+ return [
197
+ ...compareObjects("properties", {
198
+ expected: message.properties ?? {},
199
+ encoded: properties,
200
+ ignored: options.ignoreProperties ?? []
201
+ }),
202
+ ...compareObjects("headers", {
203
+ expected: message.headers ?? {},
204
+ encoded: headers ?? {},
205
+ ignored: options.ignoreHeaders ?? []
206
+ }),
207
+ ...compareBodies("body", {
208
+ expected: message.body,
209
+ encoded: encoded.content,
210
+ comparison: options.compare ?? "bytes"
211
+ })
212
+ ];
213
+ }
214
+ function delivered2(message) {
215
+ return {
216
+ content: toBuffer(message.body),
217
+ fields: { routingKey: message.routingKey },
218
+ properties: { ...message.properties, headers: { ...message.headers } }
219
+ };
220
+ }
221
+
222
+ exports.CodecContractError = CodecContractError;
223
+ exports.assertKafkaCodecCase = assertKafkaCodecCase;
224
+ exports.assertRabbitMqCodecCase = assertRabbitMqCodecCase;
225
+ exports.kafkaCodecMismatches = kafkaCodecMismatches;
226
+ exports.rabbitMqCodecMismatches = rabbitMqCodecMismatches;
227
+ exports.registerKafkaCodecContract = registerKafkaCodecContract;
228
+ exports.registerRabbitMqCodecContract = registerRabbitMqCodecContract;
229
+ //# sourceMappingURL=chunk-IOWTZMTG.cjs.map
230
+ //# sourceMappingURL=chunk-IOWTZMTG.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/codec-contract/compare.ts","../src/codec-contract/framework.ts","../src/codec-contract/kafka.ts","../src/codec-contract/rabbitmq.ts"],"names":["toErrorInfo","RelayBusError","delivered"],"mappings":"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","file":"chunk-IOWTZMTG.cjs","sourcesContent":["import { toErrorInfo } from '@relaybus/core';\n\n// Comparing what a codec wrote with a captured message, for the codec contract helpers.\n\n/** How message bodies are compared: byte for byte, or as JSON regardless of key order and spacing. */\nexport type BodyComparison = 'bytes' | 'json';\n\n/** Bytes as text or bytes, as captured or encoded. */\nexport type WireBytes = string | Uint8Array;\n\nexport function toBuffer(bytes: WireBytes): Buffer {\n return typeof bytes === 'string' ? Buffer.from(bytes, 'utf8') : Buffer.from(bytes);\n}\n\n/** JSON with object keys sorted at every level, so key order does not matter. */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(sortKeys(JSON.parse(JSON.stringify(value) ?? 'null') as unknown));\n}\n\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(sortKeys);\n }\n if (typeof value !== 'object' || value === null) {\n return value;\n }\n return Object.fromEntries(\n Object.keys(value)\n .sort()\n .map((name) => [name, sortKeys((value as Record<string, unknown>)[name])]),\n );\n}\n\n/** A mismatch line when the two bodies differ under `comparison`; `null` stands for no body. */\nexport function compareBodies(\n label: string,\n {\n expected,\n encoded,\n comparison,\n }: {\n readonly expected: WireBytes | null;\n readonly encoded: WireBytes | null;\n readonly comparison: BodyComparison;\n },\n): string[] {\n const text = (bytes: WireBytes | null): string =>\n bytes === null ? 'null' : toBuffer(bytes).toString('utf8');\n const same =\n expected === null || encoded === null\n ? expected === encoded\n : comparison === 'json'\n ? canonicalJson(JSON.parse(text(expected))) === canonicalJson(JSON.parse(text(encoded)))\n : toBuffer(expected).equals(toBuffer(encoded));\n return same ? [] : [`${label}: expected ${text(expected)}, encoded ${text(encoded)}`];\n}\n\n/** A mismatch line when the two objects differ as canonical JSON, leaving out `ignored` names. */\nexport function compareObjects(\n label: string,\n {\n expected,\n encoded,\n ignored,\n }: {\n readonly expected: Readonly<Record<string, unknown>>;\n readonly encoded: Readonly<Record<string, unknown>>;\n readonly ignored: readonly string[];\n },\n): string[] {\n const kept = (object: Readonly<Record<string, unknown>>): string =>\n canonicalJson(\n Object.fromEntries(Object.entries(object).filter(([name]) => !ignored.includes(name))),\n );\n const [left, right] = [kept(expected), kept(encoded)];\n return left === right ? [] : [`${label}: expected ${left}, encoded ${right}`];\n}\n\n/**\n * A mismatch line when the decoded event differs from the expected one. Compared byte for byte,\n * the decoded payload must also serialize exactly as the expected one, key order included: a codec\n * that re-orders it would hand order-sensitive consumers different bytes.\n */\nexport function compareDecoded(\n expected: { readonly payload: unknown },\n {\n decoded,\n comparison,\n }: { readonly decoded: { readonly payload: unknown }; readonly comparison: BodyComparison },\n): string[] {\n const [left, right] = [canonicalJson(expected), canonicalJson(decoded)];\n if (left !== right) {\n return [`decoded: expected ${left}, decoded ${right}`];\n }\n const [payload, decodedPayload] = [\n JSON.stringify(expected.payload),\n JSON.stringify(decoded.payload),\n ];\n return comparison === 'json' || payload === decodedPayload\n ? []\n : [\n `decoded payload: expected ${payload}, decoded ${decodedPayload} (same values, other key order)`,\n ];\n}\n\n/** The result of a step, or why it failed. */\nexport type Attempt<TResult> = { readonly result?: TResult; readonly mismatches: string[] };\n\n/** Runs `step`, turning what it throws into a mismatch line. */\nexport async function attempt<TResult>(\n label: string,\n step: () => TResult | Promise<TResult>,\n): Promise<Attempt<TResult>> {\n try {\n return { result: await step(), mismatches: [] };\n } catch (error) {\n return { mismatches: [`${label} threw: ${toErrorInfo(error).message}`] };\n }\n}\n","import { RelayBusError } from '@relaybus/core';\n\n// Running the codec contract under any test framework: the cases as plain assertions, and the\n// registration of one test per case through the framework's own `describe` and `it`.\n\n/** The two functions of a test framework the codec contract registers its tests with. */\nexport interface TestFramework {\n readonly describe: (name: string, body: () => void) => void;\n readonly it: (name: string, body: () => Promise<void>) => void;\n}\n\n/** Thrown by a codec contract assertion: one line per mismatch between the codec and the case. */\nexport class CodecContractError extends RelayBusError {\n override name = 'CodecContractError';\n override readonly code: string = 'RELAYBUS_TESTING_CODEC_CONTRACT';\n readonly mismatches: readonly string[];\n\n constructor(message: string, mismatches: readonly string[] = []) {\n super(message);\n this.mismatches = mismatches;\n }\n}\n\n/** Throws a {@link CodecContractError} when `mismatches` is not empty. */\nexport function assertNoMismatches(caseName: string, mismatches: readonly string[]): void {\n if (mismatches.length > 0) {\n const lines = mismatches.map((line) => `- ${line}`).join('\\n');\n throw new CodecContractError(`Codec case '${caseName}' does not match:\\n${lines}`, mismatches);\n }\n}\n\n/** Registers one test per case under one `describe`, each failing with the case's mismatches. */\nexport function registerCases<TCase extends { readonly name: string }>(\n framework: TestFramework,\n {\n title,\n cases,\n check,\n }: {\n readonly title: string;\n readonly cases: readonly TCase[];\n readonly check: (testCase: TCase) => Promise<void>;\n },\n): void {\n framework.describe(title, () => {\n for (const testCase of cases) {\n framework.it(testCase.name, () => check(testCase));\n }\n });\n}\n","import type { EventEnvelope } from '@relaybus/core';\nimport {\n attempt,\n compareBodies,\n type Attempt,\n compareDecoded,\n compareObjects,\n toBuffer,\n type BodyComparison,\n type WireBytes,\n} from './compare.js';\nimport { assertNoMismatches, registerCases, type TestFramework } from './framework.js';\n\n/** A record as captured from a topic: what an existing producer wrote. */\nexport interface KafkaCapturedRecord {\n readonly topic: string;\n /** Default 0. */\n readonly partition?: number;\n /** Default `'0'`. */\n readonly offset?: string;\n readonly key: WireBytes | null;\n readonly value: WireBytes | null;\n /** Default none. */\n readonly headers?: Readonly<Record<string, WireBytes>>;\n /** Epoch milliseconds in a string; compared only when the codec sets a timestamp. */\n readonly timestamp?: string;\n}\n\n/** What a codec's `encode` returns: a `KafkaCodec` record. */\nexport interface KafkaWrittenRecord {\n readonly key: WireBytes | null;\n readonly value: WireBytes | null;\n readonly headers: Readonly<Record<string, WireBytes | readonly WireBytes[]>>;\n readonly timestamp?: string;\n}\n\n/** A record as a consumer hands it over: bytes for the key, the value and every header. */\nexport interface KafkaDeliveredRecord {\n readonly topic: string;\n readonly partition: number;\n readonly offset: string;\n readonly key: Buffer | null;\n readonly value: Buffer | null;\n readonly headers: Readonly<Record<string, Buffer>>;\n readonly timestamp?: string;\n}\n\n/** One message of the format: the event and the record that carries it. */\nexport interface KafkaCodecCase {\n readonly name: string;\n readonly envelope: EventEnvelope;\n readonly record: KafkaCapturedRecord;\n /**\n * The event decoding the record gives, when the wire does not carry all of `envelope` (for\n * example a record written without a key). Default `envelope`.\n */\n readonly decoded?: EventEnvelope;\n /**\n * Checks one direction only: `'decode'` for a message only other producers write (for\n * example one without an id, which relaybus would add), `'encode'` for one only other\n * consumers read. Default both.\n */\n readonly only?: 'encode' | 'decode';\n}\n\nexport interface KafkaCodecContractOptions {\n /** Writes an event: a codec's `encode`, or a transport's publish captured by an injected producer. */\n readonly encode: (envelope: EventEnvelope) => KafkaWrittenRecord | Promise<KafkaWrittenRecord>;\n /** Reads a record the way the transport does: `(record) => decodeKafkaRecord(codec, record)`. */\n readonly decode: (record: KafkaDeliveredRecord) => EventEnvelope | Promise<EventEnvelope>;\n readonly cases: readonly KafkaCodecCase[];\n /** How values are compared. Default `'bytes'`. */\n readonly compare?: BodyComparison;\n /** Headers left out of the comparison, such as ones relaybus adds and other consumers ignore. */\n readonly ignoreHeaders?: readonly string[];\n}\n\n/**\n * What differs between `testCase` and what the codec writes and reads, one line each: the key,\n * the value, the headers (a repeated header by its last value) and the timestamp of\n * `encode(envelope)`, and `decode(record)` against the expected event. Empty when they agree.\n */\nexport async function kafkaCodecMismatches(\n options: KafkaCodecContractOptions,\n testCase: KafkaCodecCase,\n): Promise<string[]> {\n const { record } = testCase;\n const encoding: Attempt<KafkaWrittenRecord> =\n testCase.only === 'decode'\n ? { mismatches: [] }\n : await attempt('encode', () => options.encode(testCase.envelope));\n const encoded = encoding.result;\n const decoding: Attempt<EventEnvelope> =\n testCase.only === 'encode'\n ? { mismatches: [] }\n : await attempt('decode', () => options.decode(delivered(record)));\n return [\n ...encoding.mismatches,\n ...(encoded === undefined ? [] : compareRecords(options, { record, encoded })),\n ...decoding.mismatches,\n ...(decoding.result === undefined\n ? []\n : compareDecoded(testCase.decoded ?? testCase.envelope, {\n decoded: decoding.result,\n comparison: options.compare ?? 'bytes',\n })),\n ];\n}\n\n/**\n * Throws a {@link CodecContractError} listing what {@link kafkaCodecMismatches} finds, so any test framework\n * (or a plain script) can check one case.\n */\nexport async function assertKafkaCodecCase(\n options: KafkaCodecContractOptions,\n testCase: KafkaCodecCase,\n): Promise<void> {\n assertNoMismatches(testCase.name, await kafkaCodecMismatches(options, testCase));\n}\n\n/**\n * Registers one test per case through `framework` (the `describe` and `it` of Jest, Vitest,\n * Mocha or `node:test`), each asserting {@link assertKafkaCodecCase}: a format of existing\n * producers and consumers is adopted only once relaybus writes and reads it exactly.\n */\nexport function registerKafkaCodecContract(\n framework: TestFramework,\n { name, options }: { readonly name: string; readonly options: KafkaCodecContractOptions },\n): void {\n registerCases(framework, {\n title: `${name} writes and reads the captured Kafka records`,\n cases: options.cases,\n check: (testCase) => assertKafkaCodecCase(options, testCase),\n });\n}\n\nfunction compareRecords(\n options: KafkaCodecContractOptions,\n {\n record,\n encoded,\n }: { readonly record: KafkaCapturedRecord; readonly encoded: KafkaWrittenRecord },\n): string[] {\n const text = (bytes: WireBytes | null): string | null =>\n bytes === null ? null : toBuffer(bytes).toString('utf8');\n const timestamp = encoded.timestamp;\n return [\n ...(text(record.key) === text(encoded.key)\n ? []\n : [`key: expected ${text(record.key)}, encoded ${text(encoded.key)}`]),\n ...compareBodies('value', {\n expected: record.value,\n encoded: encoded.value,\n comparison: options.compare ?? 'bytes',\n }),\n ...compareObjects('headers', {\n expected: textHeaders(record.headers ?? {}),\n encoded: textHeaders(encoded.headers),\n ignored: options.ignoreHeaders ?? [],\n }),\n ...(timestamp === undefined || timestamp === record.timestamp\n ? []\n : [`timestamp: expected ${record.timestamp}, encoded ${timestamp}`]),\n ];\n}\n\nfunction textHeaders(\n headers: Readonly<Record<string, WireBytes | readonly WireBytes[]>>,\n): Record<string, string> {\n return Object.fromEntries(\n Object.entries(headers).map(([name, value]) => [\n name,\n toBuffer(Array.isArray(value) ? (value.at(-1) as WireBytes) : (value as WireBytes)).toString(\n 'utf8',\n ),\n ]),\n );\n}\n\nfunction delivered(record: KafkaCapturedRecord): KafkaDeliveredRecord {\n return {\n topic: record.topic,\n partition: record.partition ?? 0,\n offset: record.offset ?? '0',\n key: record.key === null ? null : toBuffer(record.key),\n value: record.value === null ? null : toBuffer(record.value),\n headers: Object.fromEntries(\n Object.entries(record.headers ?? {}).map(([name, value]) => [name, toBuffer(value)]),\n ),\n ...(record.timestamp === undefined ? {} : { timestamp: record.timestamp }),\n };\n}\n","import type { EventEnvelope } from '@relaybus/core';\nimport {\n attempt,\n compareBodies,\n type Attempt,\n compareDecoded,\n compareObjects,\n toBuffer,\n type BodyComparison,\n type WireBytes,\n} from './compare.js';\nimport { assertNoMismatches, registerCases, type TestFramework } from './framework.js';\n\n/** A message as captured from a queue: what an existing publisher wrote. */\nexport interface RabbitMqCapturedMessage {\n readonly routingKey: string;\n /**\n * The message properties as publish options name them (`persistent`, `messageId`,\n * `contentType`, `timestamp`, ...), without `headers`. Default none.\n */\n readonly properties?: Readonly<Record<string, unknown>>;\n /** Default none. */\n readonly headers?: Readonly<Record<string, unknown>>;\n readonly body: WireBytes;\n}\n\n/** What a codec's `encode` returns: a `RabbitMqCodec` message. */\nexport interface RabbitMqWrittenMessage {\n readonly content: Uint8Array;\n /** The publish options: properties, and the headers under `headers`. */\n readonly options: object;\n}\n\n/** A message as a consumer receives it. */\nexport interface RabbitMqDeliveredMessage {\n readonly content: Buffer;\n readonly fields: { readonly routingKey: string };\n readonly properties: Readonly<Record<string, unknown>> & {\n readonly headers: Readonly<Record<string, unknown>>;\n };\n}\n\n/** One message of the format: the event and the message that carries it. */\nexport interface RabbitMqCodecCase {\n readonly name: string;\n readonly envelope: EventEnvelope;\n readonly message: RabbitMqCapturedMessage;\n /** The event decoding the message gives, when the wire does not carry all of `envelope`. */\n readonly decoded?: EventEnvelope;\n /**\n * Checks one direction only: `'decode'` for a message only other producers write (for\n * example one without an id, which relaybus would add), `'encode'` for one only other\n * consumers read. Default both.\n */\n readonly only?: 'encode' | 'decode';\n}\n\nexport interface RabbitMqCodecContractOptions {\n /** Writes an event: a codec's `encode`. The routing key is the route's destination, not the codec's. */\n readonly encode: (\n envelope: EventEnvelope,\n ) => RabbitMqWrittenMessage | Promise<RabbitMqWrittenMessage>;\n /** Reads a message the way the transport does: `(message) => decodeRabbitMqMessage(codec, message)`. */\n readonly decode: (message: RabbitMqDeliveredMessage) => EventEnvelope | Promise<EventEnvelope>;\n readonly cases: readonly RabbitMqCodecCase[];\n /** How bodies are compared. Default `'bytes'`. */\n readonly compare?: BodyComparison;\n /** Properties left out of the comparison, such as a `timestamp` that differs per send. */\n readonly ignoreProperties?: readonly string[];\n /** Headers left out of the comparison. */\n readonly ignoreHeaders?: readonly string[];\n}\n\n/**\n * What differs between `testCase` and what the codec writes and reads, one line each: the\n * properties, headers and body of `encode(envelope)`, and `decode(message)` against the expected\n * event. Empty when they agree.\n */\nexport async function rabbitMqCodecMismatches(\n options: RabbitMqCodecContractOptions,\n testCase: RabbitMqCodecCase,\n): Promise<string[]> {\n const { message } = testCase;\n const encoding: Attempt<RabbitMqWrittenMessage> =\n testCase.only === 'decode'\n ? { mismatches: [] }\n : await attempt('encode', () => options.encode(testCase.envelope));\n const encoded = encoding.result;\n const decoding: Attempt<EventEnvelope> =\n testCase.only === 'encode'\n ? { mismatches: [] }\n : await attempt('decode', () => options.decode(delivered(message)));\n return [\n ...encoding.mismatches,\n ...(encoded === undefined ? [] : compareMessages(options, { message, encoded })),\n ...decoding.mismatches,\n ...(decoding.result === undefined\n ? []\n : compareDecoded(testCase.decoded ?? testCase.envelope, {\n decoded: decoding.result,\n comparison: options.compare ?? 'bytes',\n })),\n ];\n}\n\n/**\n * Throws a {@link CodecContractError} listing what {@link rabbitMqCodecMismatches} finds, so any test framework\n * (or a plain script) can check one case.\n */\nexport async function assertRabbitMqCodecCase(\n options: RabbitMqCodecContractOptions,\n testCase: RabbitMqCodecCase,\n): Promise<void> {\n assertNoMismatches(testCase.name, await rabbitMqCodecMismatches(options, testCase));\n}\n\n/**\n * Registers one test per case through `framework` (the `describe` and `it` of Jest, Vitest,\n * Mocha or `node:test`), each asserting {@link assertRabbitMqCodecCase}: a format of existing\n * producers and consumers is adopted only once relaybus writes and reads it exactly.\n */\nexport function registerRabbitMqCodecContract(\n framework: TestFramework,\n { name, options }: { readonly name: string; readonly options: RabbitMqCodecContractOptions },\n): void {\n registerCases(framework, {\n title: `${name} writes and reads the captured RabbitMQ messages`,\n cases: options.cases,\n check: (testCase) => assertRabbitMqCodecCase(options, testCase),\n });\n}\n\nfunction compareMessages(\n options: RabbitMqCodecContractOptions,\n {\n message,\n encoded,\n }: { readonly message: RabbitMqCapturedMessage; readonly encoded: RabbitMqWrittenMessage },\n): string[] {\n const { headers, ...properties } = encoded.options as {\n readonly headers?: Readonly<Record<string, unknown>>;\n };\n return [\n ...compareObjects('properties', {\n expected: message.properties ?? {},\n encoded: properties,\n ignored: options.ignoreProperties ?? [],\n }),\n ...compareObjects('headers', {\n expected: message.headers ?? {},\n encoded: headers ?? {},\n ignored: options.ignoreHeaders ?? [],\n }),\n ...compareBodies('body', {\n expected: message.body,\n encoded: encoded.content,\n comparison: options.compare ?? 'bytes',\n }),\n ];\n}\n\nfunction delivered(message: RabbitMqCapturedMessage): RabbitMqDeliveredMessage {\n return {\n content: toBuffer(message.body),\n fields: { routingKey: message.routingKey },\n properties: { ...message.properties, headers: { ...message.headers } },\n };\n}\n"]}
@@ -0,0 +1,222 @@
1
+ import { RelayBusError, toErrorInfo } from '@relaybus/core';
2
+
3
+ // src/codec-contract/compare.ts
4
+ function toBuffer(bytes) {
5
+ return typeof bytes === "string" ? Buffer.from(bytes, "utf8") : Buffer.from(bytes);
6
+ }
7
+ function canonicalJson(value) {
8
+ return JSON.stringify(sortKeys(JSON.parse(JSON.stringify(value) ?? "null")));
9
+ }
10
+ function sortKeys(value) {
11
+ if (Array.isArray(value)) {
12
+ return value.map(sortKeys);
13
+ }
14
+ if (typeof value !== "object" || value === null) {
15
+ return value;
16
+ }
17
+ return Object.fromEntries(
18
+ Object.keys(value).sort().map((name) => [name, sortKeys(value[name])])
19
+ );
20
+ }
21
+ function compareBodies(label, {
22
+ expected,
23
+ encoded,
24
+ comparison
25
+ }) {
26
+ const text = (bytes) => bytes === null ? "null" : toBuffer(bytes).toString("utf8");
27
+ const same = expected === null || encoded === null ? expected === encoded : comparison === "json" ? canonicalJson(JSON.parse(text(expected))) === canonicalJson(JSON.parse(text(encoded))) : toBuffer(expected).equals(toBuffer(encoded));
28
+ return same ? [] : [`${label}: expected ${text(expected)}, encoded ${text(encoded)}`];
29
+ }
30
+ function compareObjects(label, {
31
+ expected,
32
+ encoded,
33
+ ignored
34
+ }) {
35
+ const kept = (object) => canonicalJson(
36
+ Object.fromEntries(Object.entries(object).filter(([name]) => !ignored.includes(name)))
37
+ );
38
+ const [left, right] = [kept(expected), kept(encoded)];
39
+ return left === right ? [] : [`${label}: expected ${left}, encoded ${right}`];
40
+ }
41
+ function compareDecoded(expected, {
42
+ decoded,
43
+ comparison
44
+ }) {
45
+ const [left, right] = [canonicalJson(expected), canonicalJson(decoded)];
46
+ if (left !== right) {
47
+ return [`decoded: expected ${left}, decoded ${right}`];
48
+ }
49
+ const [payload, decodedPayload] = [
50
+ JSON.stringify(expected.payload),
51
+ JSON.stringify(decoded.payload)
52
+ ];
53
+ return comparison === "json" || payload === decodedPayload ? [] : [
54
+ `decoded payload: expected ${payload}, decoded ${decodedPayload} (same values, other key order)`
55
+ ];
56
+ }
57
+ async function attempt(label, step) {
58
+ try {
59
+ return { result: await step(), mismatches: [] };
60
+ } catch (error) {
61
+ return { mismatches: [`${label} threw: ${toErrorInfo(error).message}`] };
62
+ }
63
+ }
64
+ var CodecContractError = class extends RelayBusError {
65
+ name = "CodecContractError";
66
+ code = "RELAYBUS_TESTING_CODEC_CONTRACT";
67
+ mismatches;
68
+ constructor(message, mismatches = []) {
69
+ super(message);
70
+ this.mismatches = mismatches;
71
+ }
72
+ };
73
+ function assertNoMismatches(caseName, mismatches) {
74
+ if (mismatches.length > 0) {
75
+ const lines = mismatches.map((line) => `- ${line}`).join("\n");
76
+ throw new CodecContractError(`Codec case '${caseName}' does not match:
77
+ ${lines}`, mismatches);
78
+ }
79
+ }
80
+ function registerCases(framework, {
81
+ title,
82
+ cases,
83
+ check
84
+ }) {
85
+ framework.describe(title, () => {
86
+ for (const testCase of cases) {
87
+ framework.it(testCase.name, () => check(testCase));
88
+ }
89
+ });
90
+ }
91
+
92
+ // src/codec-contract/kafka.ts
93
+ async function kafkaCodecMismatches(options, testCase) {
94
+ const { record } = testCase;
95
+ const encoding = testCase.only === "decode" ? { mismatches: [] } : await attempt("encode", () => options.encode(testCase.envelope));
96
+ const encoded = encoding.result;
97
+ const decoding = testCase.only === "encode" ? { mismatches: [] } : await attempt("decode", () => options.decode(delivered(record)));
98
+ return [
99
+ ...encoding.mismatches,
100
+ ...encoded === void 0 ? [] : compareRecords(options, { record, encoded }),
101
+ ...decoding.mismatches,
102
+ ...decoding.result === void 0 ? [] : compareDecoded(testCase.decoded ?? testCase.envelope, {
103
+ decoded: decoding.result,
104
+ comparison: options.compare ?? "bytes"
105
+ })
106
+ ];
107
+ }
108
+ async function assertKafkaCodecCase(options, testCase) {
109
+ assertNoMismatches(testCase.name, await kafkaCodecMismatches(options, testCase));
110
+ }
111
+ function registerKafkaCodecContract(framework, { name, options }) {
112
+ registerCases(framework, {
113
+ title: `${name} writes and reads the captured Kafka records`,
114
+ cases: options.cases,
115
+ check: (testCase) => assertKafkaCodecCase(options, testCase)
116
+ });
117
+ }
118
+ function compareRecords(options, {
119
+ record,
120
+ encoded
121
+ }) {
122
+ const text = (bytes) => bytes === null ? null : toBuffer(bytes).toString("utf8");
123
+ const timestamp = encoded.timestamp;
124
+ return [
125
+ ...text(record.key) === text(encoded.key) ? [] : [`key: expected ${text(record.key)}, encoded ${text(encoded.key)}`],
126
+ ...compareBodies("value", {
127
+ expected: record.value,
128
+ encoded: encoded.value,
129
+ comparison: options.compare ?? "bytes"
130
+ }),
131
+ ...compareObjects("headers", {
132
+ expected: textHeaders(record.headers ?? {}),
133
+ encoded: textHeaders(encoded.headers),
134
+ ignored: options.ignoreHeaders ?? []
135
+ }),
136
+ ...timestamp === void 0 || timestamp === record.timestamp ? [] : [`timestamp: expected ${record.timestamp}, encoded ${timestamp}`]
137
+ ];
138
+ }
139
+ function textHeaders(headers) {
140
+ return Object.fromEntries(
141
+ Object.entries(headers).map(([name, value]) => [
142
+ name,
143
+ toBuffer(Array.isArray(value) ? value.at(-1) : value).toString(
144
+ "utf8"
145
+ )
146
+ ])
147
+ );
148
+ }
149
+ function delivered(record) {
150
+ return {
151
+ topic: record.topic,
152
+ partition: record.partition ?? 0,
153
+ offset: record.offset ?? "0",
154
+ key: record.key === null ? null : toBuffer(record.key),
155
+ value: record.value === null ? null : toBuffer(record.value),
156
+ headers: Object.fromEntries(
157
+ Object.entries(record.headers ?? {}).map(([name, value]) => [name, toBuffer(value)])
158
+ ),
159
+ ...record.timestamp === void 0 ? {} : { timestamp: record.timestamp }
160
+ };
161
+ }
162
+
163
+ // src/codec-contract/rabbitmq.ts
164
+ async function rabbitMqCodecMismatches(options, testCase) {
165
+ const { message } = testCase;
166
+ const encoding = testCase.only === "decode" ? { mismatches: [] } : await attempt("encode", () => options.encode(testCase.envelope));
167
+ const encoded = encoding.result;
168
+ const decoding = testCase.only === "encode" ? { mismatches: [] } : await attempt("decode", () => options.decode(delivered2(message)));
169
+ return [
170
+ ...encoding.mismatches,
171
+ ...encoded === void 0 ? [] : compareMessages(options, { message, encoded }),
172
+ ...decoding.mismatches,
173
+ ...decoding.result === void 0 ? [] : compareDecoded(testCase.decoded ?? testCase.envelope, {
174
+ decoded: decoding.result,
175
+ comparison: options.compare ?? "bytes"
176
+ })
177
+ ];
178
+ }
179
+ async function assertRabbitMqCodecCase(options, testCase) {
180
+ assertNoMismatches(testCase.name, await rabbitMqCodecMismatches(options, testCase));
181
+ }
182
+ function registerRabbitMqCodecContract(framework, { name, options }) {
183
+ registerCases(framework, {
184
+ title: `${name} writes and reads the captured RabbitMQ messages`,
185
+ cases: options.cases,
186
+ check: (testCase) => assertRabbitMqCodecCase(options, testCase)
187
+ });
188
+ }
189
+ function compareMessages(options, {
190
+ message,
191
+ encoded
192
+ }) {
193
+ const { headers, ...properties } = encoded.options;
194
+ return [
195
+ ...compareObjects("properties", {
196
+ expected: message.properties ?? {},
197
+ encoded: properties,
198
+ ignored: options.ignoreProperties ?? []
199
+ }),
200
+ ...compareObjects("headers", {
201
+ expected: message.headers ?? {},
202
+ encoded: headers ?? {},
203
+ ignored: options.ignoreHeaders ?? []
204
+ }),
205
+ ...compareBodies("body", {
206
+ expected: message.body,
207
+ encoded: encoded.content,
208
+ comparison: options.compare ?? "bytes"
209
+ })
210
+ ];
211
+ }
212
+ function delivered2(message) {
213
+ return {
214
+ content: toBuffer(message.body),
215
+ fields: { routingKey: message.routingKey },
216
+ properties: { ...message.properties, headers: { ...message.headers } }
217
+ };
218
+ }
219
+
220
+ export { CodecContractError, assertKafkaCodecCase, assertRabbitMqCodecCase, kafkaCodecMismatches, rabbitMqCodecMismatches, registerKafkaCodecContract, registerRabbitMqCodecContract };
221
+ //# sourceMappingURL=chunk-ZZZXYZJ3.js.map
222
+ //# sourceMappingURL=chunk-ZZZXYZJ3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/codec-contract/compare.ts","../src/codec-contract/framework.ts","../src/codec-contract/kafka.ts","../src/codec-contract/rabbitmq.ts"],"names":["delivered"],"mappings":"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","file":"chunk-ZZZXYZJ3.js","sourcesContent":["import { toErrorInfo } from '@relaybus/core';\n\n// Comparing what a codec wrote with a captured message, for the codec contract helpers.\n\n/** How message bodies are compared: byte for byte, or as JSON regardless of key order and spacing. */\nexport type BodyComparison = 'bytes' | 'json';\n\n/** Bytes as text or bytes, as captured or encoded. */\nexport type WireBytes = string | Uint8Array;\n\nexport function toBuffer(bytes: WireBytes): Buffer {\n return typeof bytes === 'string' ? Buffer.from(bytes, 'utf8') : Buffer.from(bytes);\n}\n\n/** JSON with object keys sorted at every level, so key order does not matter. */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(sortKeys(JSON.parse(JSON.stringify(value) ?? 'null') as unknown));\n}\n\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(sortKeys);\n }\n if (typeof value !== 'object' || value === null) {\n return value;\n }\n return Object.fromEntries(\n Object.keys(value)\n .sort()\n .map((name) => [name, sortKeys((value as Record<string, unknown>)[name])]),\n );\n}\n\n/** A mismatch line when the two bodies differ under `comparison`; `null` stands for no body. */\nexport function compareBodies(\n label: string,\n {\n expected,\n encoded,\n comparison,\n }: {\n readonly expected: WireBytes | null;\n readonly encoded: WireBytes | null;\n readonly comparison: BodyComparison;\n },\n): string[] {\n const text = (bytes: WireBytes | null): string =>\n bytes === null ? 'null' : toBuffer(bytes).toString('utf8');\n const same =\n expected === null || encoded === null\n ? expected === encoded\n : comparison === 'json'\n ? canonicalJson(JSON.parse(text(expected))) === canonicalJson(JSON.parse(text(encoded)))\n : toBuffer(expected).equals(toBuffer(encoded));\n return same ? [] : [`${label}: expected ${text(expected)}, encoded ${text(encoded)}`];\n}\n\n/** A mismatch line when the two objects differ as canonical JSON, leaving out `ignored` names. */\nexport function compareObjects(\n label: string,\n {\n expected,\n encoded,\n ignored,\n }: {\n readonly expected: Readonly<Record<string, unknown>>;\n readonly encoded: Readonly<Record<string, unknown>>;\n readonly ignored: readonly string[];\n },\n): string[] {\n const kept = (object: Readonly<Record<string, unknown>>): string =>\n canonicalJson(\n Object.fromEntries(Object.entries(object).filter(([name]) => !ignored.includes(name))),\n );\n const [left, right] = [kept(expected), kept(encoded)];\n return left === right ? [] : [`${label}: expected ${left}, encoded ${right}`];\n}\n\n/**\n * A mismatch line when the decoded event differs from the expected one. Compared byte for byte,\n * the decoded payload must also serialize exactly as the expected one, key order included: a codec\n * that re-orders it would hand order-sensitive consumers different bytes.\n */\nexport function compareDecoded(\n expected: { readonly payload: unknown },\n {\n decoded,\n comparison,\n }: { readonly decoded: { readonly payload: unknown }; readonly comparison: BodyComparison },\n): string[] {\n const [left, right] = [canonicalJson(expected), canonicalJson(decoded)];\n if (left !== right) {\n return [`decoded: expected ${left}, decoded ${right}`];\n }\n const [payload, decodedPayload] = [\n JSON.stringify(expected.payload),\n JSON.stringify(decoded.payload),\n ];\n return comparison === 'json' || payload === decodedPayload\n ? []\n : [\n `decoded payload: expected ${payload}, decoded ${decodedPayload} (same values, other key order)`,\n ];\n}\n\n/** The result of a step, or why it failed. */\nexport type Attempt<TResult> = { readonly result?: TResult; readonly mismatches: string[] };\n\n/** Runs `step`, turning what it throws into a mismatch line. */\nexport async function attempt<TResult>(\n label: string,\n step: () => TResult | Promise<TResult>,\n): Promise<Attempt<TResult>> {\n try {\n return { result: await step(), mismatches: [] };\n } catch (error) {\n return { mismatches: [`${label} threw: ${toErrorInfo(error).message}`] };\n }\n}\n","import { RelayBusError } from '@relaybus/core';\n\n// Running the codec contract under any test framework: the cases as plain assertions, and the\n// registration of one test per case through the framework's own `describe` and `it`.\n\n/** The two functions of a test framework the codec contract registers its tests with. */\nexport interface TestFramework {\n readonly describe: (name: string, body: () => void) => void;\n readonly it: (name: string, body: () => Promise<void>) => void;\n}\n\n/** Thrown by a codec contract assertion: one line per mismatch between the codec and the case. */\nexport class CodecContractError extends RelayBusError {\n override name = 'CodecContractError';\n override readonly code: string = 'RELAYBUS_TESTING_CODEC_CONTRACT';\n readonly mismatches: readonly string[];\n\n constructor(message: string, mismatches: readonly string[] = []) {\n super(message);\n this.mismatches = mismatches;\n }\n}\n\n/** Throws a {@link CodecContractError} when `mismatches` is not empty. */\nexport function assertNoMismatches(caseName: string, mismatches: readonly string[]): void {\n if (mismatches.length > 0) {\n const lines = mismatches.map((line) => `- ${line}`).join('\\n');\n throw new CodecContractError(`Codec case '${caseName}' does not match:\\n${lines}`, mismatches);\n }\n}\n\n/** Registers one test per case under one `describe`, each failing with the case's mismatches. */\nexport function registerCases<TCase extends { readonly name: string }>(\n framework: TestFramework,\n {\n title,\n cases,\n check,\n }: {\n readonly title: string;\n readonly cases: readonly TCase[];\n readonly check: (testCase: TCase) => Promise<void>;\n },\n): void {\n framework.describe(title, () => {\n for (const testCase of cases) {\n framework.it(testCase.name, () => check(testCase));\n }\n });\n}\n","import type { EventEnvelope } from '@relaybus/core';\nimport {\n attempt,\n compareBodies,\n type Attempt,\n compareDecoded,\n compareObjects,\n toBuffer,\n type BodyComparison,\n type WireBytes,\n} from './compare.js';\nimport { assertNoMismatches, registerCases, type TestFramework } from './framework.js';\n\n/** A record as captured from a topic: what an existing producer wrote. */\nexport interface KafkaCapturedRecord {\n readonly topic: string;\n /** Default 0. */\n readonly partition?: number;\n /** Default `'0'`. */\n readonly offset?: string;\n readonly key: WireBytes | null;\n readonly value: WireBytes | null;\n /** Default none. */\n readonly headers?: Readonly<Record<string, WireBytes>>;\n /** Epoch milliseconds in a string; compared only when the codec sets a timestamp. */\n readonly timestamp?: string;\n}\n\n/** What a codec's `encode` returns: a `KafkaCodec` record. */\nexport interface KafkaWrittenRecord {\n readonly key: WireBytes | null;\n readonly value: WireBytes | null;\n readonly headers: Readonly<Record<string, WireBytes | readonly WireBytes[]>>;\n readonly timestamp?: string;\n}\n\n/** A record as a consumer hands it over: bytes for the key, the value and every header. */\nexport interface KafkaDeliveredRecord {\n readonly topic: string;\n readonly partition: number;\n readonly offset: string;\n readonly key: Buffer | null;\n readonly value: Buffer | null;\n readonly headers: Readonly<Record<string, Buffer>>;\n readonly timestamp?: string;\n}\n\n/** One message of the format: the event and the record that carries it. */\nexport interface KafkaCodecCase {\n readonly name: string;\n readonly envelope: EventEnvelope;\n readonly record: KafkaCapturedRecord;\n /**\n * The event decoding the record gives, when the wire does not carry all of `envelope` (for\n * example a record written without a key). Default `envelope`.\n */\n readonly decoded?: EventEnvelope;\n /**\n * Checks one direction only: `'decode'` for a message only other producers write (for\n * example one without an id, which relaybus would add), `'encode'` for one only other\n * consumers read. Default both.\n */\n readonly only?: 'encode' | 'decode';\n}\n\nexport interface KafkaCodecContractOptions {\n /** Writes an event: a codec's `encode`, or a transport's publish captured by an injected producer. */\n readonly encode: (envelope: EventEnvelope) => KafkaWrittenRecord | Promise<KafkaWrittenRecord>;\n /** Reads a record the way the transport does: `(record) => decodeKafkaRecord(codec, record)`. */\n readonly decode: (record: KafkaDeliveredRecord) => EventEnvelope | Promise<EventEnvelope>;\n readonly cases: readonly KafkaCodecCase[];\n /** How values are compared. Default `'bytes'`. */\n readonly compare?: BodyComparison;\n /** Headers left out of the comparison, such as ones relaybus adds and other consumers ignore. */\n readonly ignoreHeaders?: readonly string[];\n}\n\n/**\n * What differs between `testCase` and what the codec writes and reads, one line each: the key,\n * the value, the headers (a repeated header by its last value) and the timestamp of\n * `encode(envelope)`, and `decode(record)` against the expected event. Empty when they agree.\n */\nexport async function kafkaCodecMismatches(\n options: KafkaCodecContractOptions,\n testCase: KafkaCodecCase,\n): Promise<string[]> {\n const { record } = testCase;\n const encoding: Attempt<KafkaWrittenRecord> =\n testCase.only === 'decode'\n ? { mismatches: [] }\n : await attempt('encode', () => options.encode(testCase.envelope));\n const encoded = encoding.result;\n const decoding: Attempt<EventEnvelope> =\n testCase.only === 'encode'\n ? { mismatches: [] }\n : await attempt('decode', () => options.decode(delivered(record)));\n return [\n ...encoding.mismatches,\n ...(encoded === undefined ? [] : compareRecords(options, { record, encoded })),\n ...decoding.mismatches,\n ...(decoding.result === undefined\n ? []\n : compareDecoded(testCase.decoded ?? testCase.envelope, {\n decoded: decoding.result,\n comparison: options.compare ?? 'bytes',\n })),\n ];\n}\n\n/**\n * Throws a {@link CodecContractError} listing what {@link kafkaCodecMismatches} finds, so any test framework\n * (or a plain script) can check one case.\n */\nexport async function assertKafkaCodecCase(\n options: KafkaCodecContractOptions,\n testCase: KafkaCodecCase,\n): Promise<void> {\n assertNoMismatches(testCase.name, await kafkaCodecMismatches(options, testCase));\n}\n\n/**\n * Registers one test per case through `framework` (the `describe` and `it` of Jest, Vitest,\n * Mocha or `node:test`), each asserting {@link assertKafkaCodecCase}: a format of existing\n * producers and consumers is adopted only once relaybus writes and reads it exactly.\n */\nexport function registerKafkaCodecContract(\n framework: TestFramework,\n { name, options }: { readonly name: string; readonly options: KafkaCodecContractOptions },\n): void {\n registerCases(framework, {\n title: `${name} writes and reads the captured Kafka records`,\n cases: options.cases,\n check: (testCase) => assertKafkaCodecCase(options, testCase),\n });\n}\n\nfunction compareRecords(\n options: KafkaCodecContractOptions,\n {\n record,\n encoded,\n }: { readonly record: KafkaCapturedRecord; readonly encoded: KafkaWrittenRecord },\n): string[] {\n const text = (bytes: WireBytes | null): string | null =>\n bytes === null ? null : toBuffer(bytes).toString('utf8');\n const timestamp = encoded.timestamp;\n return [\n ...(text(record.key) === text(encoded.key)\n ? []\n : [`key: expected ${text(record.key)}, encoded ${text(encoded.key)}`]),\n ...compareBodies('value', {\n expected: record.value,\n encoded: encoded.value,\n comparison: options.compare ?? 'bytes',\n }),\n ...compareObjects('headers', {\n expected: textHeaders(record.headers ?? {}),\n encoded: textHeaders(encoded.headers),\n ignored: options.ignoreHeaders ?? [],\n }),\n ...(timestamp === undefined || timestamp === record.timestamp\n ? []\n : [`timestamp: expected ${record.timestamp}, encoded ${timestamp}`]),\n ];\n}\n\nfunction textHeaders(\n headers: Readonly<Record<string, WireBytes | readonly WireBytes[]>>,\n): Record<string, string> {\n return Object.fromEntries(\n Object.entries(headers).map(([name, value]) => [\n name,\n toBuffer(Array.isArray(value) ? (value.at(-1) as WireBytes) : (value as WireBytes)).toString(\n 'utf8',\n ),\n ]),\n );\n}\n\nfunction delivered(record: KafkaCapturedRecord): KafkaDeliveredRecord {\n return {\n topic: record.topic,\n partition: record.partition ?? 0,\n offset: record.offset ?? '0',\n key: record.key === null ? null : toBuffer(record.key),\n value: record.value === null ? null : toBuffer(record.value),\n headers: Object.fromEntries(\n Object.entries(record.headers ?? {}).map(([name, value]) => [name, toBuffer(value)]),\n ),\n ...(record.timestamp === undefined ? {} : { timestamp: record.timestamp }),\n };\n}\n","import type { EventEnvelope } from '@relaybus/core';\nimport {\n attempt,\n compareBodies,\n type Attempt,\n compareDecoded,\n compareObjects,\n toBuffer,\n type BodyComparison,\n type WireBytes,\n} from './compare.js';\nimport { assertNoMismatches, registerCases, type TestFramework } from './framework.js';\n\n/** A message as captured from a queue: what an existing publisher wrote. */\nexport interface RabbitMqCapturedMessage {\n readonly routingKey: string;\n /**\n * The message properties as publish options name them (`persistent`, `messageId`,\n * `contentType`, `timestamp`, ...), without `headers`. Default none.\n */\n readonly properties?: Readonly<Record<string, unknown>>;\n /** Default none. */\n readonly headers?: Readonly<Record<string, unknown>>;\n readonly body: WireBytes;\n}\n\n/** What a codec's `encode` returns: a `RabbitMqCodec` message. */\nexport interface RabbitMqWrittenMessage {\n readonly content: Uint8Array;\n /** The publish options: properties, and the headers under `headers`. */\n readonly options: object;\n}\n\n/** A message as a consumer receives it. */\nexport interface RabbitMqDeliveredMessage {\n readonly content: Buffer;\n readonly fields: { readonly routingKey: string };\n readonly properties: Readonly<Record<string, unknown>> & {\n readonly headers: Readonly<Record<string, unknown>>;\n };\n}\n\n/** One message of the format: the event and the message that carries it. */\nexport interface RabbitMqCodecCase {\n readonly name: string;\n readonly envelope: EventEnvelope;\n readonly message: RabbitMqCapturedMessage;\n /** The event decoding the message gives, when the wire does not carry all of `envelope`. */\n readonly decoded?: EventEnvelope;\n /**\n * Checks one direction only: `'decode'` for a message only other producers write (for\n * example one without an id, which relaybus would add), `'encode'` for one only other\n * consumers read. Default both.\n */\n readonly only?: 'encode' | 'decode';\n}\n\nexport interface RabbitMqCodecContractOptions {\n /** Writes an event: a codec's `encode`. The routing key is the route's destination, not the codec's. */\n readonly encode: (\n envelope: EventEnvelope,\n ) => RabbitMqWrittenMessage | Promise<RabbitMqWrittenMessage>;\n /** Reads a message the way the transport does: `(message) => decodeRabbitMqMessage(codec, message)`. */\n readonly decode: (message: RabbitMqDeliveredMessage) => EventEnvelope | Promise<EventEnvelope>;\n readonly cases: readonly RabbitMqCodecCase[];\n /** How bodies are compared. Default `'bytes'`. */\n readonly compare?: BodyComparison;\n /** Properties left out of the comparison, such as a `timestamp` that differs per send. */\n readonly ignoreProperties?: readonly string[];\n /** Headers left out of the comparison. */\n readonly ignoreHeaders?: readonly string[];\n}\n\n/**\n * What differs between `testCase` and what the codec writes and reads, one line each: the\n * properties, headers and body of `encode(envelope)`, and `decode(message)` against the expected\n * event. Empty when they agree.\n */\nexport async function rabbitMqCodecMismatches(\n options: RabbitMqCodecContractOptions,\n testCase: RabbitMqCodecCase,\n): Promise<string[]> {\n const { message } = testCase;\n const encoding: Attempt<RabbitMqWrittenMessage> =\n testCase.only === 'decode'\n ? { mismatches: [] }\n : await attempt('encode', () => options.encode(testCase.envelope));\n const encoded = encoding.result;\n const decoding: Attempt<EventEnvelope> =\n testCase.only === 'encode'\n ? { mismatches: [] }\n : await attempt('decode', () => options.decode(delivered(message)));\n return [\n ...encoding.mismatches,\n ...(encoded === undefined ? [] : compareMessages(options, { message, encoded })),\n ...decoding.mismatches,\n ...(decoding.result === undefined\n ? []\n : compareDecoded(testCase.decoded ?? testCase.envelope, {\n decoded: decoding.result,\n comparison: options.compare ?? 'bytes',\n })),\n ];\n}\n\n/**\n * Throws a {@link CodecContractError} listing what {@link rabbitMqCodecMismatches} finds, so any test framework\n * (or a plain script) can check one case.\n */\nexport async function assertRabbitMqCodecCase(\n options: RabbitMqCodecContractOptions,\n testCase: RabbitMqCodecCase,\n): Promise<void> {\n assertNoMismatches(testCase.name, await rabbitMqCodecMismatches(options, testCase));\n}\n\n/**\n * Registers one test per case through `framework` (the `describe` and `it` of Jest, Vitest,\n * Mocha or `node:test`), each asserting {@link assertRabbitMqCodecCase}: a format of existing\n * producers and consumers is adopted only once relaybus writes and reads it exactly.\n */\nexport function registerRabbitMqCodecContract(\n framework: TestFramework,\n { name, options }: { readonly name: string; readonly options: RabbitMqCodecContractOptions },\n): void {\n registerCases(framework, {\n title: `${name} writes and reads the captured RabbitMQ messages`,\n cases: options.cases,\n check: (testCase) => assertRabbitMqCodecCase(options, testCase),\n });\n}\n\nfunction compareMessages(\n options: RabbitMqCodecContractOptions,\n {\n message,\n encoded,\n }: { readonly message: RabbitMqCapturedMessage; readonly encoded: RabbitMqWrittenMessage },\n): string[] {\n const { headers, ...properties } = encoded.options as {\n readonly headers?: Readonly<Record<string, unknown>>;\n };\n return [\n ...compareObjects('properties', {\n expected: message.properties ?? {},\n encoded: properties,\n ignored: options.ignoreProperties ?? [],\n }),\n ...compareObjects('headers', {\n expected: message.headers ?? {},\n encoded: headers ?? {},\n ignored: options.ignoreHeaders ?? [],\n }),\n ...compareBodies('body', {\n expected: message.body,\n encoded: encoded.content,\n comparison: options.compare ?? 'bytes',\n }),\n ];\n}\n\nfunction delivered(message: RabbitMqCapturedMessage): RabbitMqDeliveredMessage {\n return {\n content: toBuffer(message.body),\n fields: { routingKey: message.routingKey },\n properties: { ...message.properties, headers: { ...message.headers } },\n };\n}\n"]}
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ var chunkIOWTZMTG_cjs = require('./chunk-IOWTZMTG.cjs');
4
+
5
+
6
+
7
+ Object.defineProperty(exports, "CodecContractError", {
8
+ enumerable: true,
9
+ get: function () { return chunkIOWTZMTG_cjs.CodecContractError; }
10
+ });
11
+ Object.defineProperty(exports, "assertKafkaCodecCase", {
12
+ enumerable: true,
13
+ get: function () { return chunkIOWTZMTG_cjs.assertKafkaCodecCase; }
14
+ });
15
+ Object.defineProperty(exports, "assertRabbitMqCodecCase", {
16
+ enumerable: true,
17
+ get: function () { return chunkIOWTZMTG_cjs.assertRabbitMqCodecCase; }
18
+ });
19
+ Object.defineProperty(exports, "kafkaCodecMismatches", {
20
+ enumerable: true,
21
+ get: function () { return chunkIOWTZMTG_cjs.kafkaCodecMismatches; }
22
+ });
23
+ Object.defineProperty(exports, "rabbitMqCodecMismatches", {
24
+ enumerable: true,
25
+ get: function () { return chunkIOWTZMTG_cjs.rabbitMqCodecMismatches; }
26
+ });
27
+ Object.defineProperty(exports, "registerKafkaCodecContract", {
28
+ enumerable: true,
29
+ get: function () { return chunkIOWTZMTG_cjs.registerKafkaCodecContract; }
30
+ });
31
+ Object.defineProperty(exports, "registerRabbitMqCodecContract", {
32
+ enumerable: true,
33
+ get: function () { return chunkIOWTZMTG_cjs.registerRabbitMqCodecContract; }
34
+ });
35
+ //# sourceMappingURL=codec-contract.cjs.map
36
+ //# sourceMappingURL=codec-contract.cjs.map