@entifix/testing-unit 0.10.0 → 0.10.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.
@@ -7,6 +7,10 @@
7
7
  */
8
8
  export interface FakeAmqpMessage {
9
9
  content: Buffer;
10
+ /** Present only for a delivery made with headers. */
11
+ properties?: {
12
+ headers: Record<string, unknown>;
13
+ };
10
14
  }
11
15
  export interface FakeAmqpChannel {
12
16
  /** Everything published, decoded from the wire. */
@@ -79,7 +83,7 @@ export interface FakeAmqpChannel {
79
83
  * would. The routing key defaults to the envelope's own `meta.event.name`,
80
84
  * which is what the adapter publishes with.
81
85
  */
82
- deliver(body: unknown, routingKey?: string): Promise<void>;
86
+ deliver(body: unknown, routingKey?: string, headers?: Record<string, unknown>): Promise<void>;
83
87
  /** Pushes a raw payload, for malformed-message paths. */
84
88
  deliverRaw(payload: string): Promise<void>;
85
89
  /** Simulates the broker cancelling the consumer (amqplib delivers `null`). */
@@ -1 +1 @@
1
- {"version":3,"file":"fake-amqp.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-amqp.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,OAAO,CAAC;QACd,4EAA4E;QAC5E,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;QAC/B,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;IACH;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;QAC7B,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,eAAe,EAAE,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;QAC7B,OAAO,EAAE,eAAe,CAAC;QACzB,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC,CAAC;IACH,iEAAiE;IACjE,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,yDAAyD;IACzD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,8EAA8E;IAC9E,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAsBD,eAAO,MAAM,mBAAmB,QAAO,eAiLtC,CAAC"}
1
+ {"version":3,"file":"fake-amqp.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-amqp.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,UAAU,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;CACnD;AAED,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,OAAO,CAAC;QACd,4EAA4E;QAC5E,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;QAC/B,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;IACH;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;QAC7B,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,eAAe,EAAE,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;QAC7B,OAAO,EAAE,eAAe,CAAC;QACzB,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC,CAAC;IACH,iEAAiE;IACjE,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;OAIG;IACH,OAAO,CACL,IAAI,EAAE,OAAO,EACb,UAAU,CAAC,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,yDAAyD;IACzD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,8EAA8E;IAC9E,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAsBD,eAAO,MAAM,mBAAmB,QAAO,eA0LtC,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  import { matchesEventPattern } from '@entifix/core';
2
3
  /** The routing key a body would have been published with. */ const routingKeyOf = (body)=>{
3
4
  var _body_meta;
@@ -120,16 +121,20 @@ export const makeFakeAmqpChannel = ()=>{
120
121
  * `routingKey === undefined` means "the broker already routed this" — used by
121
122
  * the malformed-payload path, where the message reached the queue and it is
122
123
  * the *content* that is wrong.
123
- */ const push = async (payload, routingKey)=>{
124
+ */ const push = async (payload, routingKey, headers)=>{
124
125
  if (consumer === undefined) {
125
126
  throw new Error('fake amqp: nothing subscribed yet');
126
127
  }
127
128
  if (routingKey !== undefined && !routes(routingKey)) {
128
129
  return;
129
130
  }
130
- consumer({
131
+ consumer(_extends({
131
132
  content: Buffer.from(payload)
132
- });
133
+ }, headers === undefined ? {} : {
134
+ properties: {
135
+ headers
136
+ }
137
+ }));
133
138
  await settle();
134
139
  };
135
140
  return {
@@ -157,7 +162,7 @@ export const makeFakeAmqpChannel = ()=>{
157
162
  get cancelled () {
158
163
  return cancelled;
159
164
  },
160
- deliver: (body, routingKey)=>push(JSON.stringify(body), routingKey != null ? routingKey : routingKeyOf(body)),
165
+ deliver: (body, routingKey, headers)=>push(JSON.stringify(body), routingKey != null ? routingKey : routingKeyOf(body), headers),
161
166
  deliverRaw: (payload)=>push(payload, undefined),
162
167
  deliverCancellation: async ()=>{
163
168
  if (consumer === undefined) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/drivers/fake-amqp.ts"],"sourcesContent":["import { matchesEventPattern } from '@entifix/core';\n\n/**\n * A fake of the amqplib channel, not of the {@link EventBus} port.\n *\n * `makeAmqpEventBus` runs on top of it, so the envelope framing, the\n * `prefetch(1)` the saga tracker depends on, and the ack/nack policy are\n * all exercised for real.\n */\n\nexport interface FakeAmqpMessage {\n content: Buffer;\n}\n\nexport interface FakeAmqpChannel {\n /** Everything published, decoded from the wire. */\n readonly published: ReadonlyArray<{\n exchange: string;\n routingKey: string;\n body: unknown;\n /** The headers it was published with — a quarantine's reason rides here. */\n headers: Record<string, unknown> | undefined;\n }>;\n /**\n * Every queue binding the adapter asked for. Recorded because the exchange is\n * a topic now: the pattern a subscriber binds is the whole of its routing, so\n * a fake that swallowed it would let a wrong binding pass every test.\n */\n readonly bindings: ReadonlyArray<{\n queue: string;\n exchange: string;\n pattern: string;\n }>;\n /**\n * Every exchange the adapter declared, with its type. Recorded because the\n * dead-letter exchange is `direct` while the event exchange is `topic`, and a\n * broker will not retype an existing exchange — declaring the wrong type is a\n * failure that only appears against a real broker.\n */\n readonly exchanges: ReadonlyArray<{\n exchange: string;\n type: string;\n options: Record<string, unknown> | undefined;\n }>;\n /**\n * Every queue the adapter declared, with the arguments it declared it with.\n * The arguments *are* the delivery policy — `x-queue-type`,\n * `x-delivery-limit`, `x-dead-letter-exchange` — so a fake that swallowed\n * them would let a queue with no retry ceiling and no dead-letter path pass\n * every test, which is the exact defect a quarantine path exists to close.\n */\n readonly queues: ReadonlyArray<{\n queue: string;\n options: Record<string, unknown> | undefined;\n }>;\n /** Messages acked, so the success policy can be asserted. */\n readonly acked: FakeAmqpMessage[];\n /**\n * Messages nacked, **with their flags**. `requeue` is the whole distinction\n * between a transient failure the broker should retry and a poison message it\n * should quarantine at once, so recording only that a nack happened is how\n * \"dead-letters a failed message\" stayed a passing test for a bus that\n * discarded it.\n */\n readonly nacked: ReadonlyArray<{\n message: FakeAmqpMessage;\n allUpTo: boolean;\n requeue: boolean;\n }>;\n /** The prefetch the adapter asked for — 1, or the fold races. */\n readonly prefetchCount: number | undefined;\n /**\n * The consumer tags cancelled, in order.\n *\n * Recorded rather than merely accepted because a graceful shutdown's first\n * phase *is* the cancel: a connector that dropped the tag on the\n * floor would pass every test while leaving the broker delivering into a\n * process that is closing its connection.\n */\n readonly cancelled: readonly string[];\n /**\n * Pushes a message to the consumers whose binding matches it, as the broker\n * would. The routing key defaults to the envelope's own `meta.event.name`,\n * which is what the adapter publishes with.\n */\n deliver(body: unknown, routingKey?: string): Promise<void>;\n /** Pushes a raw payload, for malformed-message paths. */\n deliverRaw(payload: string): Promise<void>;\n /** Simulates the broker cancelling the consumer (amqplib delivers `null`). */\n deliverCancellation(): Promise<void>;\n /** Makes every subsequent channel call throw/reject with `error`. */\n failWith(error: unknown): void;\n /** The object to pass where an amqplib `Channel` is expected. */\n readonly channel: unknown;\n}\n\n/** The routing key a body would have been published with. */\nconst routingKeyOf = (body: unknown): string => {\n const event = (body as { meta?: { event?: { name?: unknown } } })?.meta\n ?.event;\n return typeof event?.name === 'string' ? event.name : '';\n};\n\n/**\n * The body as JSON, or as the text it is when it is not — a poison message\n * republished to quarantine is exactly the bytes that did not parse.\n */\nconst decode = (content: Buffer): unknown => {\n const text = content.toString();\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n};\n\nexport const makeFakeAmqpChannel = (): FakeAmqpChannel => {\n const published: Array<{\n exchange: string;\n routingKey: string;\n body: unknown;\n headers: Record<string, unknown> | undefined;\n }> = [];\n const bindings: Array<{ queue: string; exchange: string; pattern: string }> =\n [];\n const exchanges: Array<{\n exchange: string;\n type: string;\n options: Record<string, unknown> | undefined;\n }> = [];\n const queues: Array<{\n queue: string;\n options: Record<string, unknown> | undefined;\n }> = [];\n const acked: FakeAmqpMessage[] = [];\n const nacked: Array<{\n message: FakeAmqpMessage;\n allUpTo: boolean;\n requeue: boolean;\n }> = [];\n let consumer: ((message: FakeAmqpMessage | null) => void) | undefined;\n const cancelled: string[] = [];\n let consumerTags = 0;\n let boundQueue: string | undefined;\n let prefetchCount: number | undefined;\n let failure: unknown;\n\n const guard = () => {\n if (failure !== undefined) throw failure;\n };\n\n const channel = {\n publish: (\n exchange: string,\n routingKey: string,\n content: Buffer,\n options?: { headers?: Record<string, unknown> },\n ) => {\n guard();\n published.push({\n exchange,\n routingKey,\n body: decode(content),\n headers: options?.headers,\n });\n return true;\n },\n prefetch: async (count: number) => {\n guard();\n prefetchCount = count;\n },\n assertExchange: async (\n exchange: string,\n type: string,\n options?: Record<string, unknown>,\n ) => {\n guard();\n exchanges.push({ exchange, type, options });\n return { exchange };\n },\n assertQueue: async (queue: string, options?: Record<string, unknown>) => {\n guard();\n const name = queue === '' ? 'amq.gen-fake' : queue;\n queues.push({ queue: name, options });\n return { queue: name };\n },\n bindQueue: async (queue: string, exchange: string, pattern: string) => {\n guard();\n bindings.push({ queue, exchange, pattern });\n },\n consume: async (\n queue: string,\n handler: (message: FakeAmqpMessage | null) => void,\n ) => {\n guard();\n consumer = handler;\n boundQueue = queue;\n // A fresh tag per `consume`, so a reconnect's re-bind is distinguishable\n // from the binding it replaced — the connector cancels what it currently\n // holds, and a constant tag would hide it cancelling a stale one.\n consumerTags += 1;\n return { consumerTag: `fake-consumer-${consumerTags}` };\n },\n cancel: async (consumerTag: string) => {\n guard();\n cancelled.push(consumerTag);\n consumer = undefined;\n },\n ack: (message: FakeAmqpMessage) => {\n acked.push(message);\n },\n nack: (message: FakeAmqpMessage, allUpTo: boolean, requeue: boolean) => {\n nacked.push({ message, allUpTo, requeue });\n },\n close: async () => {\n guard();\n },\n };\n\n /**\n * The adapter acks/nacks from inside a floating promise chain, so a delivery\n * is only settled once the microtask queue drains.\n */\n const settle = () => new Promise<void>(resolve => setTimeout(resolve, 0));\n\n /**\n * Whether the consumer's queue is bound to a pattern matching `routingKey`.\n * The broker decides this in production, so the fake has to as well — without\n * it every subscriber sees every message and the topic exchange is untested.\n */\n const routes = (routingKey: string) =>\n bindings.some(\n binding =>\n binding.queue === boundQueue &&\n matchesEventPattern(binding.pattern, routingKey),\n );\n\n /**\n * `routingKey === undefined` means \"the broker already routed this\" — used by\n * the malformed-payload path, where the message reached the queue and it is\n * the *content* that is wrong.\n */\n const push = async (payload: string, routingKey: string | undefined) => {\n if (consumer === undefined) {\n throw new Error('fake amqp: nothing subscribed yet');\n }\n if (routingKey !== undefined && !routes(routingKey)) {\n return;\n }\n consumer({ content: Buffer.from(payload) });\n await settle();\n };\n\n return {\n get published() {\n return published;\n },\n get bindings() {\n return bindings;\n },\n get exchanges() {\n return exchanges;\n },\n get queues() {\n return queues;\n },\n get acked() {\n return acked;\n },\n get nacked() {\n return nacked;\n },\n get prefetchCount() {\n return prefetchCount;\n },\n get cancelled() {\n return cancelled;\n },\n deliver: (body, routingKey) =>\n push(JSON.stringify(body), routingKey ?? routingKeyOf(body)),\n deliverRaw: payload => push(payload, undefined),\n deliverCancellation: async () => {\n if (consumer === undefined) {\n throw new Error('fake amqp: nothing subscribed yet');\n }\n consumer(null);\n await settle();\n },\n failWith: error => {\n failure = error;\n },\n channel,\n };\n};\n"],"names":["matchesEventPattern","routingKeyOf","body","event","meta","name","decode","content","text","toString","JSON","parse","makeFakeAmqpChannel","published","bindings","exchanges","queues","acked","nacked","consumer","cancelled","consumerTags","boundQueue","prefetchCount","failure","guard","undefined","channel","publish","exchange","routingKey","options","push","headers","prefetch","count","assertExchange","type","assertQueue","queue","bindQueue","pattern","consume","handler","consumerTag","cancel","ack","message","nack","allUpTo","requeue","close","settle","Promise","resolve","setTimeout","routes","some","binding","payload","Error","Buffer","from","deliver","stringify","deliverRaw","deliverCancellation","failWith","error"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/drivers/fake-amqp.ts"],"sourcesContent":["import { matchesEventPattern } from '@entifix/core';\n\n/**\n * A fake of the amqplib channel, not of the {@link EventBus} port.\n *\n * `makeAmqpEventBus` runs on top of it, so the envelope framing, the\n * `prefetch(1)` the saga tracker depends on, and the ack/nack policy are\n * all exercised for real.\n */\n\nexport interface FakeAmqpMessage {\n content: Buffer;\n /** Present only for a delivery made with headers. */\n properties?: { headers: Record<string, unknown> };\n}\n\nexport interface FakeAmqpChannel {\n /** Everything published, decoded from the wire. */\n readonly published: ReadonlyArray<{\n exchange: string;\n routingKey: string;\n body: unknown;\n /** The headers it was published with — a quarantine's reason rides here. */\n headers: Record<string, unknown> | undefined;\n }>;\n /**\n * Every queue binding the adapter asked for. Recorded because the exchange is\n * a topic now: the pattern a subscriber binds is the whole of its routing, so\n * a fake that swallowed it would let a wrong binding pass every test.\n */\n readonly bindings: ReadonlyArray<{\n queue: string;\n exchange: string;\n pattern: string;\n }>;\n /**\n * Every exchange the adapter declared, with its type. Recorded because the\n * dead-letter exchange is `direct` while the event exchange is `topic`, and a\n * broker will not retype an existing exchange — declaring the wrong type is a\n * failure that only appears against a real broker.\n */\n readonly exchanges: ReadonlyArray<{\n exchange: string;\n type: string;\n options: Record<string, unknown> | undefined;\n }>;\n /**\n * Every queue the adapter declared, with the arguments it declared it with.\n * The arguments *are* the delivery policy — `x-queue-type`,\n * `x-delivery-limit`, `x-dead-letter-exchange` — so a fake that swallowed\n * them would let a queue with no retry ceiling and no dead-letter path pass\n * every test, which is the exact defect a quarantine path exists to close.\n */\n readonly queues: ReadonlyArray<{\n queue: string;\n options: Record<string, unknown> | undefined;\n }>;\n /** Messages acked, so the success policy can be asserted. */\n readonly acked: FakeAmqpMessage[];\n /**\n * Messages nacked, **with their flags**. `requeue` is the whole distinction\n * between a transient failure the broker should retry and a poison message it\n * should quarantine at once, so recording only that a nack happened is how\n * \"dead-letters a failed message\" stayed a passing test for a bus that\n * discarded it.\n */\n readonly nacked: ReadonlyArray<{\n message: FakeAmqpMessage;\n allUpTo: boolean;\n requeue: boolean;\n }>;\n /** The prefetch the adapter asked for — 1, or the fold races. */\n readonly prefetchCount: number | undefined;\n /**\n * The consumer tags cancelled, in order.\n *\n * Recorded rather than merely accepted because a graceful shutdown's first\n * phase *is* the cancel: a connector that dropped the tag on the\n * floor would pass every test while leaving the broker delivering into a\n * process that is closing its connection.\n */\n readonly cancelled: readonly string[];\n /**\n * Pushes a message to the consumers whose binding matches it, as the broker\n * would. The routing key defaults to the envelope's own `meta.event.name`,\n * which is what the adapter publishes with.\n */\n deliver(\n body: unknown,\n routingKey?: string,\n headers?: Record<string, unknown>,\n ): Promise<void>;\n /** Pushes a raw payload, for malformed-message paths. */\n deliverRaw(payload: string): Promise<void>;\n /** Simulates the broker cancelling the consumer (amqplib delivers `null`). */\n deliverCancellation(): Promise<void>;\n /** Makes every subsequent channel call throw/reject with `error`. */\n failWith(error: unknown): void;\n /** The object to pass where an amqplib `Channel` is expected. */\n readonly channel: unknown;\n}\n\n/** The routing key a body would have been published with. */\nconst routingKeyOf = (body: unknown): string => {\n const event = (body as { meta?: { event?: { name?: unknown } } })?.meta\n ?.event;\n return typeof event?.name === 'string' ? event.name : '';\n};\n\n/**\n * The body as JSON, or as the text it is when it is not — a poison message\n * republished to quarantine is exactly the bytes that did not parse.\n */\nconst decode = (content: Buffer): unknown => {\n const text = content.toString();\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n};\n\nexport const makeFakeAmqpChannel = (): FakeAmqpChannel => {\n const published: Array<{\n exchange: string;\n routingKey: string;\n body: unknown;\n headers: Record<string, unknown> | undefined;\n }> = [];\n const bindings: Array<{ queue: string; exchange: string; pattern: string }> =\n [];\n const exchanges: Array<{\n exchange: string;\n type: string;\n options: Record<string, unknown> | undefined;\n }> = [];\n const queues: Array<{\n queue: string;\n options: Record<string, unknown> | undefined;\n }> = [];\n const acked: FakeAmqpMessage[] = [];\n const nacked: Array<{\n message: FakeAmqpMessage;\n allUpTo: boolean;\n requeue: boolean;\n }> = [];\n let consumer: ((message: FakeAmqpMessage | null) => void) | undefined;\n const cancelled: string[] = [];\n let consumerTags = 0;\n let boundQueue: string | undefined;\n let prefetchCount: number | undefined;\n let failure: unknown;\n\n const guard = () => {\n if (failure !== undefined) throw failure;\n };\n\n const channel = {\n publish: (\n exchange: string,\n routingKey: string,\n content: Buffer,\n options?: { headers?: Record<string, unknown> },\n ) => {\n guard();\n published.push({\n exchange,\n routingKey,\n body: decode(content),\n headers: options?.headers,\n });\n return true;\n },\n prefetch: async (count: number) => {\n guard();\n prefetchCount = count;\n },\n assertExchange: async (\n exchange: string,\n type: string,\n options?: Record<string, unknown>,\n ) => {\n guard();\n exchanges.push({ exchange, type, options });\n return { exchange };\n },\n assertQueue: async (queue: string, options?: Record<string, unknown>) => {\n guard();\n const name = queue === '' ? 'amq.gen-fake' : queue;\n queues.push({ queue: name, options });\n return { queue: name };\n },\n bindQueue: async (queue: string, exchange: string, pattern: string) => {\n guard();\n bindings.push({ queue, exchange, pattern });\n },\n consume: async (\n queue: string,\n handler: (message: FakeAmqpMessage | null) => void,\n ) => {\n guard();\n consumer = handler;\n boundQueue = queue;\n // A fresh tag per `consume`, so a reconnect's re-bind is distinguishable\n // from the binding it replaced — the connector cancels what it currently\n // holds, and a constant tag would hide it cancelling a stale one.\n consumerTags += 1;\n return { consumerTag: `fake-consumer-${consumerTags}` };\n },\n cancel: async (consumerTag: string) => {\n guard();\n cancelled.push(consumerTag);\n consumer = undefined;\n },\n ack: (message: FakeAmqpMessage) => {\n acked.push(message);\n },\n nack: (message: FakeAmqpMessage, allUpTo: boolean, requeue: boolean) => {\n nacked.push({ message, allUpTo, requeue });\n },\n close: async () => {\n guard();\n },\n };\n\n /**\n * The adapter acks/nacks from inside a floating promise chain, so a delivery\n * is only settled once the microtask queue drains.\n */\n const settle = () => new Promise<void>(resolve => setTimeout(resolve, 0));\n\n /**\n * Whether the consumer's queue is bound to a pattern matching `routingKey`.\n * The broker decides this in production, so the fake has to as well — without\n * it every subscriber sees every message and the topic exchange is untested.\n */\n const routes = (routingKey: string) =>\n bindings.some(\n binding =>\n binding.queue === boundQueue &&\n matchesEventPattern(binding.pattern, routingKey),\n );\n\n /**\n * `routingKey === undefined` means \"the broker already routed this\" — used by\n * the malformed-payload path, where the message reached the queue and it is\n * the *content* that is wrong.\n */\n const push = async (\n payload: string,\n routingKey: string | undefined,\n headers?: Record<string, unknown>,\n ) => {\n if (consumer === undefined) {\n throw new Error('fake amqp: nothing subscribed yet');\n }\n if (routingKey !== undefined && !routes(routingKey)) {\n return;\n }\n consumer({\n content: Buffer.from(payload),\n // Only when asked for, so a delivery without headers stays exactly the\n // shape amqplib hands over for a message published with none.\n ...(headers === undefined ? {} : { properties: { headers } }),\n });\n await settle();\n };\n\n return {\n get published() {\n return published;\n },\n get bindings() {\n return bindings;\n },\n get exchanges() {\n return exchanges;\n },\n get queues() {\n return queues;\n },\n get acked() {\n return acked;\n },\n get nacked() {\n return nacked;\n },\n get prefetchCount() {\n return prefetchCount;\n },\n get cancelled() {\n return cancelled;\n },\n deliver: (body, routingKey, headers) =>\n push(JSON.stringify(body), routingKey ?? routingKeyOf(body), headers),\n deliverRaw: payload => push(payload, undefined),\n deliverCancellation: async () => {\n if (consumer === undefined) {\n throw new Error('fake amqp: nothing subscribed yet');\n }\n consumer(null);\n await settle();\n },\n failWith: error => {\n failure = error;\n },\n channel,\n };\n};\n"],"names":["matchesEventPattern","routingKeyOf","body","event","meta","name","decode","content","text","toString","JSON","parse","makeFakeAmqpChannel","published","bindings","exchanges","queues","acked","nacked","consumer","cancelled","consumerTags","boundQueue","prefetchCount","failure","guard","undefined","channel","publish","exchange","routingKey","options","push","headers","prefetch","count","assertExchange","type","assertQueue","queue","bindQueue","pattern","consume","handler","consumerTag","cancel","ack","message","nack","allUpTo","requeue","close","settle","Promise","resolve","setTimeout","routes","some","binding","payload","Error","Buffer","from","properties","deliver","stringify","deliverRaw","deliverCancellation","failWith","error"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entifix/testing-unit",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Test doubles for entifix: in-memory ports, driver fakes for Mongo, Redis, RabbitMQ and SQL, MSW handlers and port contract suites.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -72,9 +72,9 @@
72
72
  },
73
73
  "dependencies": {
74
74
  "@swc/helpers": "0.5.23",
75
- "@entifix/business": "0.10.0",
76
- "@entifix/core": "0.10.0",
77
- "@entifix/transactions": "0.10.0"
75
+ "@entifix/business": "0.10.1",
76
+ "@entifix/core": "0.10.1",
77
+ "@entifix/transactions": "0.10.1"
78
78
  },
79
79
  "peerDependencies": {
80
80
  "@testing-library/react": "*",
@@ -82,7 +82,7 @@
82
82
  "msw": ">=3",
83
83
  "react": "*",
84
84
  "vitest": "*",
85
- "@entifix/react-controls": "0.10.0"
85
+ "@entifix/react-controls": "0.10.1"
86
86
  },
87
87
  "peerDependenciesMeta": {
88
88
  "@entifix/react-controls": {