@relaybus/testing 0.4.0 → 0.5.0
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 +18 -3
- package/dist/chunk-IOWTZMTG.cjs +230 -0
- package/dist/chunk-IOWTZMTG.cjs.map +1 -0
- package/dist/chunk-ZZZXYZJ3.js +222 -0
- package/dist/chunk-ZZZXYZJ3.js.map +1 -0
- package/dist/codec-contract.cjs +36 -0
- package/dist/codec-contract.cjs.map +1 -0
- package/dist/codec-contract.d.cts +178 -0
- package/dist/codec-contract.d.ts +178 -0
- package/dist/codec-contract.js +3 -0
- package/dist/codec-contract.js.map +1 -0
- package/dist/index.cjs +140 -170
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -139
- package/dist/index.d.ts +9 -139
- package/dist/index.js +115 -170
- package/dist/index.js.map +1 -1
- package/dist/jest.cjs +56 -0
- package/dist/jest.cjs.map +1 -0
- package/dist/jest.d.cts +10 -0
- package/dist/jest.d.ts +10 -0
- package/dist/jest.js +26 -0
- package/dist/jest.js.map +1 -0
- package/package.json +40 -4
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { NewDelivery, EventEnvelope, EventStore, Clock, Transport, TransportMessageHandler } from '@relaybus/core';
|
|
2
|
+
import { KafkaCodecContractOptions, RabbitMqCodecContractOptions } from './codec-contract.cjs';
|
|
3
|
+
export { BodyComparison, CodecContractError, KafkaCapturedRecord, KafkaCodecCase, KafkaDeliveredRecord, KafkaWrittenRecord, RabbitMqCapturedMessage, RabbitMqCodecCase, RabbitMqDeliveredMessage, RabbitMqWrittenMessage, TestFramework, WireBytes, assertKafkaCodecCase, assertRabbitMqCodecCase, kafkaCodecMismatches, rabbitMqCodecMismatches, registerKafkaCodecContract, registerRabbitMqCodecContract } from './codec-contract.cjs';
|
|
2
4
|
|
|
3
5
|
declare function at(offsetMs: number): Date;
|
|
4
6
|
declare function createEnvelope(overrides?: Partial<EventEnvelope>): EventEnvelope;
|
|
@@ -66,10 +68,13 @@ declare class InMemoryTransport implements Transport {
|
|
|
66
68
|
#private;
|
|
67
69
|
readonly name: string;
|
|
68
70
|
readonly published: PublishedMessage[];
|
|
71
|
+
/** Wire names (event type to name) that routes and catalogs declared; the transport sends events as they are. */
|
|
72
|
+
readonly declaredWireTypes: Record<string, string>;
|
|
69
73
|
connected: boolean;
|
|
70
74
|
closed: boolean;
|
|
71
75
|
constructor(name?: string);
|
|
72
76
|
connect(): Promise<void>;
|
|
77
|
+
declareWireTypes(wireTypes: Readonly<Record<string, string>>): () => void;
|
|
73
78
|
/** Makes the next `count` publishes reject, simulating a broker outage. */
|
|
74
79
|
failNextPublishes(count: number): void;
|
|
75
80
|
publish(envelope: EventEnvelope, destination: string): Promise<void>;
|
|
@@ -83,145 +88,10 @@ declare class InMemoryTransport implements Transport {
|
|
|
83
88
|
close(): Promise<void>;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
/**
|
|
87
|
-
type BodyComparison = 'bytes' | 'json';
|
|
88
|
-
/** Bytes as text or bytes, as captured or encoded. */
|
|
89
|
-
type WireBytes = string | Uint8Array;
|
|
90
|
-
|
|
91
|
-
/** A record as captured from a topic: what an existing producer wrote. */
|
|
92
|
-
interface KafkaCapturedRecord {
|
|
93
|
-
readonly topic: string;
|
|
94
|
-
/** Default 0. */
|
|
95
|
-
readonly partition?: number;
|
|
96
|
-
/** Default `'0'`. */
|
|
97
|
-
readonly offset?: string;
|
|
98
|
-
readonly key: WireBytes | null;
|
|
99
|
-
readonly value: WireBytes | null;
|
|
100
|
-
/** Default none. */
|
|
101
|
-
readonly headers?: Readonly<Record<string, WireBytes>>;
|
|
102
|
-
/** Epoch milliseconds in a string; compared only when the codec sets a timestamp. */
|
|
103
|
-
readonly timestamp?: string;
|
|
104
|
-
}
|
|
105
|
-
/** What a codec's `encode` returns: a `KafkaCodec` record. */
|
|
106
|
-
interface KafkaWrittenRecord {
|
|
107
|
-
readonly key: WireBytes | null;
|
|
108
|
-
readonly value: WireBytes | null;
|
|
109
|
-
readonly headers: Readonly<Record<string, WireBytes | readonly WireBytes[]>>;
|
|
110
|
-
readonly timestamp?: string;
|
|
111
|
-
}
|
|
112
|
-
/** A record as a consumer hands it over: bytes for the key, the value and every header. */
|
|
113
|
-
interface KafkaDeliveredRecord {
|
|
114
|
-
readonly topic: string;
|
|
115
|
-
readonly partition: number;
|
|
116
|
-
readonly offset: string;
|
|
117
|
-
readonly key: Buffer | null;
|
|
118
|
-
readonly value: Buffer | null;
|
|
119
|
-
readonly headers: Readonly<Record<string, Buffer>>;
|
|
120
|
-
readonly timestamp?: string;
|
|
121
|
-
}
|
|
122
|
-
/** One message of the format: the event and the record that carries it. */
|
|
123
|
-
interface KafkaCodecCase {
|
|
124
|
-
readonly name: string;
|
|
125
|
-
readonly envelope: EventEnvelope;
|
|
126
|
-
readonly record: KafkaCapturedRecord;
|
|
127
|
-
/**
|
|
128
|
-
* The event decoding the record gives, when the wire does not carry all of `envelope` (for
|
|
129
|
-
* example a record written without a key). Default `envelope`.
|
|
130
|
-
*/
|
|
131
|
-
readonly decoded?: EventEnvelope;
|
|
132
|
-
/**
|
|
133
|
-
* Checks one direction only: `'decode'` for a message only other producers write (for
|
|
134
|
-
* example one without an id, which relaybus would add), `'encode'` for one only other
|
|
135
|
-
* consumers read. Default both.
|
|
136
|
-
*/
|
|
137
|
-
readonly only?: 'encode' | 'decode';
|
|
138
|
-
}
|
|
139
|
-
interface KafkaCodecContractOptions {
|
|
140
|
-
/** Writes an event: a codec's `encode`, or a transport's publish captured by an injected producer. */
|
|
141
|
-
readonly encode: (envelope: EventEnvelope) => KafkaWrittenRecord | Promise<KafkaWrittenRecord>;
|
|
142
|
-
/** Reads a record the way the transport does: `(record) => decodeKafkaRecord(codec, record)`. */
|
|
143
|
-
readonly decode: (record: KafkaDeliveredRecord) => EventEnvelope | Promise<EventEnvelope>;
|
|
144
|
-
readonly cases: readonly KafkaCodecCase[];
|
|
145
|
-
/** How values are compared. Default `'bytes'`. */
|
|
146
|
-
readonly compare?: BodyComparison;
|
|
147
|
-
/** Headers left out of the comparison, such as ones relaybus adds and other consumers ignore. */
|
|
148
|
-
readonly ignoreHeaders?: readonly string[];
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* What differs between `testCase` and what the codec writes and reads, one line each: the key,
|
|
152
|
-
* the value, the headers (a repeated header by its last value) and the timestamp of
|
|
153
|
-
* `encode(envelope)`, and `decode(record)` against the expected event. Empty when they agree.
|
|
154
|
-
*/
|
|
155
|
-
declare function kafkaCodecMismatches(options: KafkaCodecContractOptions, testCase: KafkaCodecCase): Promise<string[]>;
|
|
156
|
-
/**
|
|
157
|
-
* Registers one test per case asserting {@link kafkaCodecMismatches} finds nothing: a format of
|
|
158
|
-
* existing producers and consumers is adopted only once relaybus writes and reads it exactly.
|
|
159
|
-
*/
|
|
91
|
+
/** Registers one Vitest test per captured Kafka record (see `registerKafkaCodecContract`). */
|
|
160
92
|
declare function kafkaCodecContract(name: string, options: KafkaCodecContractOptions): void;
|
|
161
|
-
|
|
162
|
-
/** A message as captured from a queue: what an existing publisher wrote. */
|
|
163
|
-
interface RabbitMqCapturedMessage {
|
|
164
|
-
readonly routingKey: string;
|
|
165
|
-
/**
|
|
166
|
-
* The message properties as publish options name them (`persistent`, `messageId`,
|
|
167
|
-
* `contentType`, `timestamp`, ...), without `headers`. Default none.
|
|
168
|
-
*/
|
|
169
|
-
readonly properties?: Readonly<Record<string, unknown>>;
|
|
170
|
-
/** Default none. */
|
|
171
|
-
readonly headers?: Readonly<Record<string, unknown>>;
|
|
172
|
-
readonly body: WireBytes;
|
|
173
|
-
}
|
|
174
|
-
/** What a codec's `encode` returns: a `RabbitMqCodec` message. */
|
|
175
|
-
interface RabbitMqWrittenMessage {
|
|
176
|
-
readonly content: Uint8Array;
|
|
177
|
-
/** The publish options: properties, and the headers under `headers`. */
|
|
178
|
-
readonly options: object;
|
|
179
|
-
}
|
|
180
|
-
/** A message as a consumer receives it. */
|
|
181
|
-
interface RabbitMqDeliveredMessage {
|
|
182
|
-
readonly content: Buffer;
|
|
183
|
-
readonly fields: {
|
|
184
|
-
readonly routingKey: string;
|
|
185
|
-
};
|
|
186
|
-
readonly properties: Readonly<Record<string, unknown>> & {
|
|
187
|
-
readonly headers: Readonly<Record<string, unknown>>;
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
/** One message of the format: the event and the message that carries it. */
|
|
191
|
-
interface RabbitMqCodecCase {
|
|
192
|
-
readonly name: string;
|
|
193
|
-
readonly envelope: EventEnvelope;
|
|
194
|
-
readonly message: RabbitMqCapturedMessage;
|
|
195
|
-
/** The event decoding the message gives, when the wire does not carry all of `envelope`. */
|
|
196
|
-
readonly decoded?: EventEnvelope;
|
|
197
|
-
/**
|
|
198
|
-
* Checks one direction only: `'decode'` for a message only other producers write (for
|
|
199
|
-
* example one without an id, which relaybus would add), `'encode'` for one only other
|
|
200
|
-
* consumers read. Default both.
|
|
201
|
-
*/
|
|
202
|
-
readonly only?: 'encode' | 'decode';
|
|
203
|
-
}
|
|
204
|
-
interface RabbitMqCodecContractOptions {
|
|
205
|
-
/** Writes an event: a codec's `encode`. The routing key is the route's destination, not the codec's. */
|
|
206
|
-
readonly encode: (envelope: EventEnvelope) => RabbitMqWrittenMessage | Promise<RabbitMqWrittenMessage>;
|
|
207
|
-
/** Reads a message the way the transport does: `(message) => decodeRabbitMqMessage(codec, message)`. */
|
|
208
|
-
readonly decode: (message: RabbitMqDeliveredMessage) => EventEnvelope | Promise<EventEnvelope>;
|
|
209
|
-
readonly cases: readonly RabbitMqCodecCase[];
|
|
210
|
-
/** How bodies are compared. Default `'bytes'`. */
|
|
211
|
-
readonly compare?: BodyComparison;
|
|
212
|
-
/** Properties left out of the comparison, such as a `timestamp` that differs per send. */
|
|
213
|
-
readonly ignoreProperties?: readonly string[];
|
|
214
|
-
/** Headers left out of the comparison. */
|
|
215
|
-
readonly ignoreHeaders?: readonly string[];
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* What differs between `testCase` and what the codec writes and reads, one line each: the
|
|
219
|
-
* properties, headers and body of `encode(envelope)`, and `decode(message)` against the expected
|
|
220
|
-
* event. Empty when they agree.
|
|
221
|
-
*/
|
|
222
|
-
declare function rabbitMqCodecMismatches(options: RabbitMqCodecContractOptions, testCase: RabbitMqCodecCase): Promise<string[]>;
|
|
223
|
-
/** Registers one test per case asserting {@link rabbitMqCodecMismatches} finds nothing. */
|
|
93
|
+
/** Registers one Vitest test per captured RabbitMQ message (see `registerRabbitMqCodecContract`). */
|
|
224
94
|
declare function rabbitMqCodecContract(name: string, options: RabbitMqCodecContractOptions): void;
|
|
225
95
|
|
|
226
|
-
export { FakeClock, InMemoryTransport, at, createDelivery, createEnvelope, eventStoreContract, kafkaCodecContract,
|
|
227
|
-
export type {
|
|
96
|
+
export { FakeClock, InMemoryTransport, KafkaCodecContractOptions, RabbitMqCodecContractOptions, at, createDelivery, createEnvelope, eventStoreContract, kafkaCodecContract, rabbitMqCodecContract };
|
|
97
|
+
export type { EventStoreContractOptions, PublishedMessage };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { NewDelivery, EventEnvelope, EventStore, Clock, Transport, TransportMessageHandler } from '@relaybus/core';
|
|
2
|
+
import { KafkaCodecContractOptions, RabbitMqCodecContractOptions } from './codec-contract.js';
|
|
3
|
+
export { BodyComparison, CodecContractError, KafkaCapturedRecord, KafkaCodecCase, KafkaDeliveredRecord, KafkaWrittenRecord, RabbitMqCapturedMessage, RabbitMqCodecCase, RabbitMqDeliveredMessage, RabbitMqWrittenMessage, TestFramework, WireBytes, assertKafkaCodecCase, assertRabbitMqCodecCase, kafkaCodecMismatches, rabbitMqCodecMismatches, registerKafkaCodecContract, registerRabbitMqCodecContract } from './codec-contract.js';
|
|
2
4
|
|
|
3
5
|
declare function at(offsetMs: number): Date;
|
|
4
6
|
declare function createEnvelope(overrides?: Partial<EventEnvelope>): EventEnvelope;
|
|
@@ -66,10 +68,13 @@ declare class InMemoryTransport implements Transport {
|
|
|
66
68
|
#private;
|
|
67
69
|
readonly name: string;
|
|
68
70
|
readonly published: PublishedMessage[];
|
|
71
|
+
/** Wire names (event type to name) that routes and catalogs declared; the transport sends events as they are. */
|
|
72
|
+
readonly declaredWireTypes: Record<string, string>;
|
|
69
73
|
connected: boolean;
|
|
70
74
|
closed: boolean;
|
|
71
75
|
constructor(name?: string);
|
|
72
76
|
connect(): Promise<void>;
|
|
77
|
+
declareWireTypes(wireTypes: Readonly<Record<string, string>>): () => void;
|
|
73
78
|
/** Makes the next `count` publishes reject, simulating a broker outage. */
|
|
74
79
|
failNextPublishes(count: number): void;
|
|
75
80
|
publish(envelope: EventEnvelope, destination: string): Promise<void>;
|
|
@@ -83,145 +88,10 @@ declare class InMemoryTransport implements Transport {
|
|
|
83
88
|
close(): Promise<void>;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
/**
|
|
87
|
-
type BodyComparison = 'bytes' | 'json';
|
|
88
|
-
/** Bytes as text or bytes, as captured or encoded. */
|
|
89
|
-
type WireBytes = string | Uint8Array;
|
|
90
|
-
|
|
91
|
-
/** A record as captured from a topic: what an existing producer wrote. */
|
|
92
|
-
interface KafkaCapturedRecord {
|
|
93
|
-
readonly topic: string;
|
|
94
|
-
/** Default 0. */
|
|
95
|
-
readonly partition?: number;
|
|
96
|
-
/** Default `'0'`. */
|
|
97
|
-
readonly offset?: string;
|
|
98
|
-
readonly key: WireBytes | null;
|
|
99
|
-
readonly value: WireBytes | null;
|
|
100
|
-
/** Default none. */
|
|
101
|
-
readonly headers?: Readonly<Record<string, WireBytes>>;
|
|
102
|
-
/** Epoch milliseconds in a string; compared only when the codec sets a timestamp. */
|
|
103
|
-
readonly timestamp?: string;
|
|
104
|
-
}
|
|
105
|
-
/** What a codec's `encode` returns: a `KafkaCodec` record. */
|
|
106
|
-
interface KafkaWrittenRecord {
|
|
107
|
-
readonly key: WireBytes | null;
|
|
108
|
-
readonly value: WireBytes | null;
|
|
109
|
-
readonly headers: Readonly<Record<string, WireBytes | readonly WireBytes[]>>;
|
|
110
|
-
readonly timestamp?: string;
|
|
111
|
-
}
|
|
112
|
-
/** A record as a consumer hands it over: bytes for the key, the value and every header. */
|
|
113
|
-
interface KafkaDeliveredRecord {
|
|
114
|
-
readonly topic: string;
|
|
115
|
-
readonly partition: number;
|
|
116
|
-
readonly offset: string;
|
|
117
|
-
readonly key: Buffer | null;
|
|
118
|
-
readonly value: Buffer | null;
|
|
119
|
-
readonly headers: Readonly<Record<string, Buffer>>;
|
|
120
|
-
readonly timestamp?: string;
|
|
121
|
-
}
|
|
122
|
-
/** One message of the format: the event and the record that carries it. */
|
|
123
|
-
interface KafkaCodecCase {
|
|
124
|
-
readonly name: string;
|
|
125
|
-
readonly envelope: EventEnvelope;
|
|
126
|
-
readonly record: KafkaCapturedRecord;
|
|
127
|
-
/**
|
|
128
|
-
* The event decoding the record gives, when the wire does not carry all of `envelope` (for
|
|
129
|
-
* example a record written without a key). Default `envelope`.
|
|
130
|
-
*/
|
|
131
|
-
readonly decoded?: EventEnvelope;
|
|
132
|
-
/**
|
|
133
|
-
* Checks one direction only: `'decode'` for a message only other producers write (for
|
|
134
|
-
* example one without an id, which relaybus would add), `'encode'` for one only other
|
|
135
|
-
* consumers read. Default both.
|
|
136
|
-
*/
|
|
137
|
-
readonly only?: 'encode' | 'decode';
|
|
138
|
-
}
|
|
139
|
-
interface KafkaCodecContractOptions {
|
|
140
|
-
/** Writes an event: a codec's `encode`, or a transport's publish captured by an injected producer. */
|
|
141
|
-
readonly encode: (envelope: EventEnvelope) => KafkaWrittenRecord | Promise<KafkaWrittenRecord>;
|
|
142
|
-
/** Reads a record the way the transport does: `(record) => decodeKafkaRecord(codec, record)`. */
|
|
143
|
-
readonly decode: (record: KafkaDeliveredRecord) => EventEnvelope | Promise<EventEnvelope>;
|
|
144
|
-
readonly cases: readonly KafkaCodecCase[];
|
|
145
|
-
/** How values are compared. Default `'bytes'`. */
|
|
146
|
-
readonly compare?: BodyComparison;
|
|
147
|
-
/** Headers left out of the comparison, such as ones relaybus adds and other consumers ignore. */
|
|
148
|
-
readonly ignoreHeaders?: readonly string[];
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* What differs between `testCase` and what the codec writes and reads, one line each: the key,
|
|
152
|
-
* the value, the headers (a repeated header by its last value) and the timestamp of
|
|
153
|
-
* `encode(envelope)`, and `decode(record)` against the expected event. Empty when they agree.
|
|
154
|
-
*/
|
|
155
|
-
declare function kafkaCodecMismatches(options: KafkaCodecContractOptions, testCase: KafkaCodecCase): Promise<string[]>;
|
|
156
|
-
/**
|
|
157
|
-
* Registers one test per case asserting {@link kafkaCodecMismatches} finds nothing: a format of
|
|
158
|
-
* existing producers and consumers is adopted only once relaybus writes and reads it exactly.
|
|
159
|
-
*/
|
|
91
|
+
/** Registers one Vitest test per captured Kafka record (see `registerKafkaCodecContract`). */
|
|
160
92
|
declare function kafkaCodecContract(name: string, options: KafkaCodecContractOptions): void;
|
|
161
|
-
|
|
162
|
-
/** A message as captured from a queue: what an existing publisher wrote. */
|
|
163
|
-
interface RabbitMqCapturedMessage {
|
|
164
|
-
readonly routingKey: string;
|
|
165
|
-
/**
|
|
166
|
-
* The message properties as publish options name them (`persistent`, `messageId`,
|
|
167
|
-
* `contentType`, `timestamp`, ...), without `headers`. Default none.
|
|
168
|
-
*/
|
|
169
|
-
readonly properties?: Readonly<Record<string, unknown>>;
|
|
170
|
-
/** Default none. */
|
|
171
|
-
readonly headers?: Readonly<Record<string, unknown>>;
|
|
172
|
-
readonly body: WireBytes;
|
|
173
|
-
}
|
|
174
|
-
/** What a codec's `encode` returns: a `RabbitMqCodec` message. */
|
|
175
|
-
interface RabbitMqWrittenMessage {
|
|
176
|
-
readonly content: Uint8Array;
|
|
177
|
-
/** The publish options: properties, and the headers under `headers`. */
|
|
178
|
-
readonly options: object;
|
|
179
|
-
}
|
|
180
|
-
/** A message as a consumer receives it. */
|
|
181
|
-
interface RabbitMqDeliveredMessage {
|
|
182
|
-
readonly content: Buffer;
|
|
183
|
-
readonly fields: {
|
|
184
|
-
readonly routingKey: string;
|
|
185
|
-
};
|
|
186
|
-
readonly properties: Readonly<Record<string, unknown>> & {
|
|
187
|
-
readonly headers: Readonly<Record<string, unknown>>;
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
/** One message of the format: the event and the message that carries it. */
|
|
191
|
-
interface RabbitMqCodecCase {
|
|
192
|
-
readonly name: string;
|
|
193
|
-
readonly envelope: EventEnvelope;
|
|
194
|
-
readonly message: RabbitMqCapturedMessage;
|
|
195
|
-
/** The event decoding the message gives, when the wire does not carry all of `envelope`. */
|
|
196
|
-
readonly decoded?: EventEnvelope;
|
|
197
|
-
/**
|
|
198
|
-
* Checks one direction only: `'decode'` for a message only other producers write (for
|
|
199
|
-
* example one without an id, which relaybus would add), `'encode'` for one only other
|
|
200
|
-
* consumers read. Default both.
|
|
201
|
-
*/
|
|
202
|
-
readonly only?: 'encode' | 'decode';
|
|
203
|
-
}
|
|
204
|
-
interface RabbitMqCodecContractOptions {
|
|
205
|
-
/** Writes an event: a codec's `encode`. The routing key is the route's destination, not the codec's. */
|
|
206
|
-
readonly encode: (envelope: EventEnvelope) => RabbitMqWrittenMessage | Promise<RabbitMqWrittenMessage>;
|
|
207
|
-
/** Reads a message the way the transport does: `(message) => decodeRabbitMqMessage(codec, message)`. */
|
|
208
|
-
readonly decode: (message: RabbitMqDeliveredMessage) => EventEnvelope | Promise<EventEnvelope>;
|
|
209
|
-
readonly cases: readonly RabbitMqCodecCase[];
|
|
210
|
-
/** How bodies are compared. Default `'bytes'`. */
|
|
211
|
-
readonly compare?: BodyComparison;
|
|
212
|
-
/** Properties left out of the comparison, such as a `timestamp` that differs per send. */
|
|
213
|
-
readonly ignoreProperties?: readonly string[];
|
|
214
|
-
/** Headers left out of the comparison. */
|
|
215
|
-
readonly ignoreHeaders?: readonly string[];
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* What differs between `testCase` and what the codec writes and reads, one line each: the
|
|
219
|
-
* properties, headers and body of `encode(envelope)`, and `decode(message)` against the expected
|
|
220
|
-
* event. Empty when they agree.
|
|
221
|
-
*/
|
|
222
|
-
declare function rabbitMqCodecMismatches(options: RabbitMqCodecContractOptions, testCase: RabbitMqCodecCase): Promise<string[]>;
|
|
223
|
-
/** Registers one test per case asserting {@link rabbitMqCodecMismatches} finds nothing. */
|
|
93
|
+
/** Registers one Vitest test per captured RabbitMQ message (see `registerRabbitMqCodecContract`). */
|
|
224
94
|
declare function rabbitMqCodecContract(name: string, options: RabbitMqCodecContractOptions): void;
|
|
225
95
|
|
|
226
|
-
export { FakeClock, InMemoryTransport, at, createDelivery, createEnvelope, eventStoreContract, kafkaCodecContract,
|
|
227
|
-
export type {
|
|
96
|
+
export { FakeClock, InMemoryTransport, KafkaCodecContractOptions, RabbitMqCodecContractOptions, at, createDelivery, createEnvelope, eventStoreContract, kafkaCodecContract, rabbitMqCodecContract };
|
|
97
|
+
export type { EventStoreContractOptions, PublishedMessage };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { registerKafkaCodecContract, registerRabbitMqCodecContract } from './chunk-ZZZXYZJ3.js';
|
|
2
|
+
export { CodecContractError, assertKafkaCodecCase, assertRabbitMqCodecCase, kafkaCodecMismatches, rabbitMqCodecMismatches, registerKafkaCodecContract, registerRabbitMqCodecContract } from './chunk-ZZZXYZJ3.js';
|
|
1
3
|
import { describe, beforeEach, afterEach, it, expect } from 'vitest';
|
|
2
|
-
import { createUuidV7
|
|
4
|
+
import { createUuidV7 } from '@relaybus/core';
|
|
3
5
|
|
|
4
|
-
// src/event-store-contract.ts
|
|
5
6
|
function createDeferred() {
|
|
6
7
|
let resolve;
|
|
7
8
|
const promise = new Promise((onResolve) => {
|
|
@@ -1242,6 +1243,103 @@ function lostLeasesContract(suite) {
|
|
|
1242
1243
|
});
|
|
1243
1244
|
});
|
|
1244
1245
|
}
|
|
1246
|
+
function unsortedPayload() {
|
|
1247
|
+
return {
|
|
1248
|
+
zulu: "last letter first",
|
|
1249
|
+
b: { yankee: [{ zz: 1, a: 2, mm: { q: null, c: true } }, [], {}], alpha: {} },
|
|
1250
|
+
longestKeyOfTheObject: 1,
|
|
1251
|
+
a: '\xE7\u011F\u0131\xF6\u015F\xFC \u{1F680} \u2028 "quoted" \\ tab ',
|
|
1252
|
+
exponent: 1e21,
|
|
1253
|
+
tiny: 5e-324,
|
|
1254
|
+
fraction: 0.1 + 0.2,
|
|
1255
|
+
negative: -42.5,
|
|
1256
|
+
maxSafe: Number.MAX_SAFE_INTEGER,
|
|
1257
|
+
minSafe: Number.MIN_SAFE_INTEGER,
|
|
1258
|
+
emptyObject: {},
|
|
1259
|
+
emptyArray: [],
|
|
1260
|
+
"7": "integer-like key",
|
|
1261
|
+
mixed: ["z", { d: 4, c: 3 }, 0, false, null]
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function payloadFidelityContract(suite) {
|
|
1265
|
+
const { append, claim } = suite;
|
|
1266
|
+
describe("payload fidelity", () => {
|
|
1267
|
+
it("hands out the payload and headers with their key order, as published", async () => {
|
|
1268
|
+
const event = createEnvelope({
|
|
1269
|
+
payload: unsortedPayload(),
|
|
1270
|
+
headers: { zz: "last", traceparent: "00-abc-def-01", a: "first", tenant: "acme" }
|
|
1271
|
+
});
|
|
1272
|
+
await append([{ event, deliveries: [createDelivery("handler-a")] }]);
|
|
1273
|
+
const [claimed] = await claim(["handler-a"]);
|
|
1274
|
+
expect(JSON.stringify(claimed.event.payload)).toBe(JSON.stringify(event.payload));
|
|
1275
|
+
expect(JSON.stringify(claimed.event.headers)).toBe(JSON.stringify(event.headers));
|
|
1276
|
+
});
|
|
1277
|
+
it("keeps the order of a payload appended in a batch and of a top-level array", async () => {
|
|
1278
|
+
const object = createEnvelope({ payload: unsortedPayload() });
|
|
1279
|
+
const list = createEnvelope({ payload: [{ zz: 1, a: { y: 2, b: 1 } }, "x", 1e21] });
|
|
1280
|
+
const scalar = createEnvelope({ payload: "plain text" });
|
|
1281
|
+
await append([
|
|
1282
|
+
{ event: object, deliveries: [createDelivery("handler-a", at(1))] },
|
|
1283
|
+
{ event: list, deliveries: [createDelivery("handler-a", at(2))] },
|
|
1284
|
+
{ event: scalar, deliveries: [createDelivery("handler-a", at(3))] }
|
|
1285
|
+
]);
|
|
1286
|
+
const claimed = await claim(["handler-a"], at(10));
|
|
1287
|
+
expect(claimed.map((delivery) => JSON.stringify(delivery.event.payload))).toEqual(
|
|
1288
|
+
[object, list, scalar].map((event) => JSON.stringify(event.payload))
|
|
1289
|
+
);
|
|
1290
|
+
});
|
|
1291
|
+
it("keeps the order for a handler added to an event that already exists", async () => {
|
|
1292
|
+
const event = createEnvelope({ payload: unsortedPayload() });
|
|
1293
|
+
await append([{ event, deliveries: [createDelivery("handler-a")] }]);
|
|
1294
|
+
await append([{ event, deliveries: [createDelivery("handler-b")] }]);
|
|
1295
|
+
const [claimed] = await claim(["handler-b"]);
|
|
1296
|
+
expect(JSON.stringify(claimed.event.payload)).toBe(JSON.stringify(event.payload));
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
function transactionIsolationContract(suite) {
|
|
1301
|
+
let store;
|
|
1302
|
+
beforeEach(() => {
|
|
1303
|
+
store = suite.store();
|
|
1304
|
+
});
|
|
1305
|
+
describe("transaction isolation levels", () => {
|
|
1306
|
+
it("recognizes no unrelated error as a serialization failure", () => {
|
|
1307
|
+
for (const error of [new Error("boom"), void 0, null, "text", { code: "23505" }]) {
|
|
1308
|
+
expect(store.isSerializationFailure?.(error) ?? false).toBe(false);
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
it("commits and rolls back at every level it lists", async () => {
|
|
1312
|
+
for (const isolationLevel of store.isolationLevels ?? []) {
|
|
1313
|
+
const committed = createEnvelope();
|
|
1314
|
+
await store.transaction(
|
|
1315
|
+
(transaction) => store.append(
|
|
1316
|
+
transaction,
|
|
1317
|
+
[{ event: committed, deliveries: [createDelivery("h")] }],
|
|
1318
|
+
/* @__PURE__ */ new Date(0)
|
|
1319
|
+
),
|
|
1320
|
+
{ timeoutMs: 5e3, isolationLevel }
|
|
1321
|
+
);
|
|
1322
|
+
const abandoned = createEnvelope();
|
|
1323
|
+
await expect(
|
|
1324
|
+
store.transaction(
|
|
1325
|
+
async (transaction) => {
|
|
1326
|
+
await store.append(
|
|
1327
|
+
transaction,
|
|
1328
|
+
[{ event: abandoned, deliveries: [createDelivery("h")] }],
|
|
1329
|
+
/* @__PURE__ */ new Date(0)
|
|
1330
|
+
);
|
|
1331
|
+
throw new Error(`rolled back at ${isolationLevel}`);
|
|
1332
|
+
},
|
|
1333
|
+
{ timeoutMs: 5e3, isolationLevel }
|
|
1334
|
+
)
|
|
1335
|
+
).rejects.toThrow(`rolled back at ${isolationLevel}`);
|
|
1336
|
+
const stored = (await store.listDeliveries({})).map((delivery) => delivery.event.id);
|
|
1337
|
+
expect(stored).toContain(committed.id);
|
|
1338
|
+
expect(stored).not.toContain(abandoned.id);
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1245
1343
|
|
|
1246
1344
|
// src/event-store-contract.ts
|
|
1247
1345
|
function eventStoreContract(name, options) {
|
|
@@ -1290,6 +1388,8 @@ function eventStoreContract(name, options) {
|
|
|
1290
1388
|
orderingAndLeasesContract(suite);
|
|
1291
1389
|
completionContract(suite);
|
|
1292
1390
|
lostLeasesContract(suite);
|
|
1391
|
+
payloadFidelityContract(suite);
|
|
1392
|
+
transactionIsolationContract(suite);
|
|
1293
1393
|
});
|
|
1294
1394
|
}
|
|
1295
1395
|
|
|
@@ -1358,6 +1458,8 @@ var FakeClock = class {
|
|
|
1358
1458
|
var InMemoryTransport = class {
|
|
1359
1459
|
name;
|
|
1360
1460
|
published = [];
|
|
1461
|
+
/** Wire names (event type to name) that routes and catalogs declared; the transport sends events as they are. */
|
|
1462
|
+
declaredWireTypes = {};
|
|
1361
1463
|
#subscribers = /* @__PURE__ */ new Map();
|
|
1362
1464
|
#failNextPublishes = 0;
|
|
1363
1465
|
connected = false;
|
|
@@ -1368,6 +1470,11 @@ var InMemoryTransport = class {
|
|
|
1368
1470
|
async connect() {
|
|
1369
1471
|
this.connected = true;
|
|
1370
1472
|
}
|
|
1473
|
+
declareWireTypes(wireTypes) {
|
|
1474
|
+
return () => {
|
|
1475
|
+
Object.assign(this.declaredWireTypes, wireTypes);
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1371
1478
|
/** Makes the next `count` publishes reject, simulating a broker outage. */
|
|
1372
1479
|
failNextPublishes(count) {
|
|
1373
1480
|
this.#failNextPublishes = count;
|
|
@@ -1404,186 +1511,24 @@ var InMemoryTransport = class {
|
|
|
1404
1511
|
}
|
|
1405
1512
|
};
|
|
1406
1513
|
async function deliverWithRedeliveries(handler, delivery) {
|
|
1407
|
-
for (let
|
|
1514
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1408
1515
|
try {
|
|
1409
1516
|
await handler(delivery.envelope, { destination: delivery.destination });
|
|
1410
|
-
return
|
|
1517
|
+
return attempt + 1;
|
|
1411
1518
|
} catch (error) {
|
|
1412
|
-
if (
|
|
1519
|
+
if (attempt >= delivery.maxRedeliveries) {
|
|
1413
1520
|
throw error;
|
|
1414
1521
|
}
|
|
1415
1522
|
}
|
|
1416
1523
|
}
|
|
1417
1524
|
}
|
|
1418
|
-
function toBuffer(bytes) {
|
|
1419
|
-
return typeof bytes === "string" ? Buffer.from(bytes, "utf8") : Buffer.from(bytes);
|
|
1420
|
-
}
|
|
1421
|
-
function canonicalJson(value) {
|
|
1422
|
-
return JSON.stringify(sortKeys(JSON.parse(JSON.stringify(value) ?? "null")));
|
|
1423
|
-
}
|
|
1424
|
-
function sortKeys(value) {
|
|
1425
|
-
if (Array.isArray(value)) {
|
|
1426
|
-
return value.map(sortKeys);
|
|
1427
|
-
}
|
|
1428
|
-
if (typeof value !== "object" || value === null) {
|
|
1429
|
-
return value;
|
|
1430
|
-
}
|
|
1431
|
-
return Object.fromEntries(
|
|
1432
|
-
Object.keys(value).sort().map((name) => [name, sortKeys(value[name])])
|
|
1433
|
-
);
|
|
1434
|
-
}
|
|
1435
|
-
function compareBodies(label, {
|
|
1436
|
-
expected,
|
|
1437
|
-
encoded,
|
|
1438
|
-
comparison
|
|
1439
|
-
}) {
|
|
1440
|
-
const text = (bytes) => bytes === null ? "null" : toBuffer(bytes).toString("utf8");
|
|
1441
|
-
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));
|
|
1442
|
-
return same ? [] : [`${label}: expected ${text(expected)}, encoded ${text(encoded)}`];
|
|
1443
|
-
}
|
|
1444
|
-
function compareObjects(label, {
|
|
1445
|
-
expected,
|
|
1446
|
-
encoded,
|
|
1447
|
-
ignored
|
|
1448
|
-
}) {
|
|
1449
|
-
const kept = (object) => canonicalJson(
|
|
1450
|
-
Object.fromEntries(Object.entries(object).filter(([name]) => !ignored.includes(name)))
|
|
1451
|
-
);
|
|
1452
|
-
const [left, right] = [kept(expected), kept(encoded)];
|
|
1453
|
-
return left === right ? [] : [`${label}: expected ${left}, encoded ${right}`];
|
|
1454
|
-
}
|
|
1455
|
-
function compareDecoded(expected, decoded) {
|
|
1456
|
-
const [left, right] = [canonicalJson(expected), canonicalJson(decoded)];
|
|
1457
|
-
return left === right ? [] : [`decoded: expected ${left}, decoded ${right}`];
|
|
1458
|
-
}
|
|
1459
|
-
async function attempt(label, step) {
|
|
1460
|
-
try {
|
|
1461
|
-
return { result: await step(), mismatches: [] };
|
|
1462
|
-
} catch (error) {
|
|
1463
|
-
return { mismatches: [`${label} threw: ${toErrorInfo(error).message}`] };
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
// src/codec-contract/kafka.ts
|
|
1468
|
-
async function kafkaCodecMismatches(options, testCase) {
|
|
1469
|
-
const { record } = testCase;
|
|
1470
|
-
const encoding = testCase.only === "decode" ? { mismatches: [] } : await attempt("encode", () => options.encode(testCase.envelope));
|
|
1471
|
-
const encoded = encoding.result;
|
|
1472
|
-
const decoding = testCase.only === "encode" ? { mismatches: [] } : await attempt("decode", () => options.decode(delivered(record)));
|
|
1473
|
-
return [
|
|
1474
|
-
...encoding.mismatches,
|
|
1475
|
-
...encoded === void 0 ? [] : compareRecords(options, { record, encoded }),
|
|
1476
|
-
...decoding.mismatches,
|
|
1477
|
-
...decoding.result === void 0 ? [] : compareDecoded(testCase.decoded ?? testCase.envelope, decoding.result)
|
|
1478
|
-
];
|
|
1479
|
-
}
|
|
1480
1525
|
function kafkaCodecContract(name, options) {
|
|
1481
|
-
|
|
1482
|
-
it.each(options.cases.map((testCase) => [testCase.name, testCase]))(
|
|
1483
|
-
"%s",
|
|
1484
|
-
async (_name, testCase) => {
|
|
1485
|
-
expect(await kafkaCodecMismatches(options, testCase)).toEqual([]);
|
|
1486
|
-
}
|
|
1487
|
-
);
|
|
1488
|
-
});
|
|
1489
|
-
}
|
|
1490
|
-
function compareRecords(options, {
|
|
1491
|
-
record,
|
|
1492
|
-
encoded
|
|
1493
|
-
}) {
|
|
1494
|
-
const text = (bytes) => bytes === null ? null : toBuffer(bytes).toString("utf8");
|
|
1495
|
-
const timestamp = encoded.timestamp;
|
|
1496
|
-
return [
|
|
1497
|
-
...text(record.key) === text(encoded.key) ? [] : [`key: expected ${text(record.key)}, encoded ${text(encoded.key)}`],
|
|
1498
|
-
...compareBodies("value", {
|
|
1499
|
-
expected: record.value,
|
|
1500
|
-
encoded: encoded.value,
|
|
1501
|
-
comparison: options.compare ?? "bytes"
|
|
1502
|
-
}),
|
|
1503
|
-
...compareObjects("headers", {
|
|
1504
|
-
expected: textHeaders(record.headers ?? {}),
|
|
1505
|
-
encoded: textHeaders(encoded.headers),
|
|
1506
|
-
ignored: options.ignoreHeaders ?? []
|
|
1507
|
-
}),
|
|
1508
|
-
...timestamp === void 0 || timestamp === record.timestamp ? [] : [`timestamp: expected ${record.timestamp}, encoded ${timestamp}`]
|
|
1509
|
-
];
|
|
1510
|
-
}
|
|
1511
|
-
function textHeaders(headers) {
|
|
1512
|
-
return Object.fromEntries(
|
|
1513
|
-
Object.entries(headers).map(([name, value]) => [
|
|
1514
|
-
name,
|
|
1515
|
-
toBuffer(Array.isArray(value) ? value.at(-1) : value).toString(
|
|
1516
|
-
"utf8"
|
|
1517
|
-
)
|
|
1518
|
-
])
|
|
1519
|
-
);
|
|
1520
|
-
}
|
|
1521
|
-
function delivered(record) {
|
|
1522
|
-
return {
|
|
1523
|
-
topic: record.topic,
|
|
1524
|
-
partition: record.partition ?? 0,
|
|
1525
|
-
offset: record.offset ?? "0",
|
|
1526
|
-
key: record.key === null ? null : toBuffer(record.key),
|
|
1527
|
-
value: record.value === null ? null : toBuffer(record.value),
|
|
1528
|
-
headers: Object.fromEntries(
|
|
1529
|
-
Object.entries(record.headers ?? {}).map(([name, value]) => [name, toBuffer(value)])
|
|
1530
|
-
),
|
|
1531
|
-
...record.timestamp === void 0 ? {} : { timestamp: record.timestamp }
|
|
1532
|
-
};
|
|
1533
|
-
}
|
|
1534
|
-
async function rabbitMqCodecMismatches(options, testCase) {
|
|
1535
|
-
const { message } = testCase;
|
|
1536
|
-
const encoding = testCase.only === "decode" ? { mismatches: [] } : await attempt("encode", () => options.encode(testCase.envelope));
|
|
1537
|
-
const encoded = encoding.result;
|
|
1538
|
-
const decoding = testCase.only === "encode" ? { mismatches: [] } : await attempt("decode", () => options.decode(delivered2(message)));
|
|
1539
|
-
return [
|
|
1540
|
-
...encoding.mismatches,
|
|
1541
|
-
...encoded === void 0 ? [] : compareMessages(options, { message, encoded }),
|
|
1542
|
-
...decoding.mismatches,
|
|
1543
|
-
...decoding.result === void 0 ? [] : compareDecoded(testCase.decoded ?? testCase.envelope, decoding.result)
|
|
1544
|
-
];
|
|
1526
|
+
registerKafkaCodecContract({ describe: describe, it: it }, { name, options });
|
|
1545
1527
|
}
|
|
1546
1528
|
function rabbitMqCodecContract(name, options) {
|
|
1547
|
-
|
|
1548
|
-
it.each(options.cases.map((testCase) => [testCase.name, testCase]))(
|
|
1549
|
-
"%s",
|
|
1550
|
-
async (_name, testCase) => {
|
|
1551
|
-
expect(await rabbitMqCodecMismatches(options, testCase)).toEqual([]);
|
|
1552
|
-
}
|
|
1553
|
-
);
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
function compareMessages(options, {
|
|
1557
|
-
message,
|
|
1558
|
-
encoded
|
|
1559
|
-
}) {
|
|
1560
|
-
const { headers, ...properties } = encoded.options;
|
|
1561
|
-
return [
|
|
1562
|
-
...compareObjects("properties", {
|
|
1563
|
-
expected: message.properties ?? {},
|
|
1564
|
-
encoded: properties,
|
|
1565
|
-
ignored: options.ignoreProperties ?? []
|
|
1566
|
-
}),
|
|
1567
|
-
...compareObjects("headers", {
|
|
1568
|
-
expected: message.headers ?? {},
|
|
1569
|
-
encoded: headers ?? {},
|
|
1570
|
-
ignored: options.ignoreHeaders ?? []
|
|
1571
|
-
}),
|
|
1572
|
-
...compareBodies("body", {
|
|
1573
|
-
expected: message.body,
|
|
1574
|
-
encoded: encoded.content,
|
|
1575
|
-
comparison: options.compare ?? "bytes"
|
|
1576
|
-
})
|
|
1577
|
-
];
|
|
1578
|
-
}
|
|
1579
|
-
function delivered2(message) {
|
|
1580
|
-
return {
|
|
1581
|
-
content: toBuffer(message.body),
|
|
1582
|
-
fields: { routingKey: message.routingKey },
|
|
1583
|
-
properties: { ...message.properties, headers: { ...message.headers } }
|
|
1584
|
-
};
|
|
1529
|
+
registerRabbitMqCodecContract({ describe: describe, it: it }, { name, options });
|
|
1585
1530
|
}
|
|
1586
1531
|
|
|
1587
|
-
export { FakeClock, InMemoryTransport, at, createDelivery, createEnvelope, eventStoreContract, kafkaCodecContract,
|
|
1532
|
+
export { FakeClock, InMemoryTransport, at, createDelivery, createEnvelope, eventStoreContract, kafkaCodecContract, rabbitMqCodecContract };
|
|
1588
1533
|
//# sourceMappingURL=index.js.map
|
|
1589
1534
|
//# sourceMappingURL=index.js.map
|