@amqp-contract/testing 0.21.0 → 0.22.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.
@@ -13,6 +13,19 @@ import { inject, it as it$1, vi } from "vitest";
13
13
  * @packageDocumentation
14
14
  */
15
15
  const it = it$1.extend({
16
+ /**
17
+ * Test fixture that provides an isolated RabbitMQ virtual host (vhost) for the test.
18
+ *
19
+ * Creates a new vhost with a random UUID name for test isolation. The vhost is automatically
20
+ * created before the test runs using the RabbitMQ Management API.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * it('should use isolated vhost', async ({ vhost }) => {
25
+ * console.log(`Test running in vhost: ${vhost}`);
26
+ * });
27
+ * ```
28
+ */
16
29
  vhost: async ({}, use) => {
17
30
  const vhost = await createVhost();
18
31
  try {
@@ -21,25 +34,118 @@ const it = it$1.extend({
21
34
  await deleteVhost(vhost);
22
35
  }
23
36
  },
37
+ /**
38
+ * Test fixture that provides the AMQP connection URL for the test container.
39
+ *
40
+ * Constructs a connection URL using the test container's IP and port, along with
41
+ * the isolated vhost. The URL follows the format: `amqp://guest:guest@host:port/vhost`.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * it('should connect with URL', async ({ amqpConnectionUrl }) => {
46
+ * console.log(`Connecting to: ${amqpConnectionUrl}`);
47
+ * });
48
+ * ```
49
+ */
24
50
  amqpConnectionUrl: async ({ vhost }, use) => {
25
51
  await use(`amqp://guest:guest@${inject("__TESTCONTAINERS_RABBITMQ_IP__")}:${inject("__TESTCONTAINERS_RABBITMQ_PORT_5672__")}/${vhost}`);
26
52
  },
53
+ /**
54
+ * Test fixture that provides an active AMQP connection to RabbitMQ.
55
+ *
56
+ * Establishes a connection using the provided connection URL and automatically closes
57
+ * it after the test completes. This fixture is useful for tests that need direct
58
+ * access to the connection object (e.g., to create multiple channels).
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * it('should use connection', async ({ amqpConnection }) => {
63
+ * const channel = await amqpConnection.createChannel();
64
+ * // ... use channel
65
+ * });
66
+ * ```
67
+ */
27
68
  amqpConnection: async ({ amqpConnectionUrl }, use) => {
28
69
  const connection = await amqpLib.connect(amqpConnectionUrl);
29
70
  await use(connection);
30
71
  await connection.close();
31
72
  },
73
+ /**
74
+ * Test fixture that provides an AMQP channel for interacting with RabbitMQ.
75
+ *
76
+ * Creates a channel from the active connection and automatically closes it after
77
+ * the test completes. The channel is used for declaring exchanges, queues, bindings,
78
+ * and publishing/consuming messages.
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * it('should use channel', async ({ amqpChannel }) => {
83
+ * await amqpChannel.assertExchange('test-exchange', 'topic');
84
+ * await amqpChannel.assertQueue('test-queue');
85
+ * });
86
+ * ```
87
+ */
32
88
  amqpChannel: async ({ amqpConnection }, use) => {
33
89
  const channel = await amqpConnection.createChannel();
34
90
  await use(channel);
35
91
  await channel.close();
36
92
  },
93
+ /**
94
+ * Test fixture for publishing messages to an AMQP exchange.
95
+ *
96
+ * Provides a helper function to publish messages directly to an exchange during tests.
97
+ * The message content is automatically serialized to JSON and converted to a Buffer.
98
+ *
99
+ * @param exchange - The name of the exchange to publish to
100
+ * @param routingKey - The routing key for message routing
101
+ * @param content - The message payload (will be JSON serialized)
102
+ * @throws Error if the message cannot be published (e.g., write buffer is full)
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * it('should publish message', async ({ publishMessage }) => {
107
+ * publishMessage('my-exchange', 'routing.key', { data: 'test' });
108
+ * });
109
+ * ```
110
+ */
37
111
  publishMessage: async ({ amqpChannel }, use) => {
38
112
  function publishMessage(exchange, routingKey, content, options) {
39
113
  if (!amqpChannel.publish(exchange, routingKey, Buffer.from(JSON.stringify(content)), options)) throw new Error(`Failed to publish message to exchange "${exchange}" with routing key "${routingKey}"`);
40
114
  }
41
115
  await use(publishMessage);
42
116
  },
117
+ /**
118
+ * Test fixture for initializing a message consumer on an AMQP queue.
119
+ *
120
+ * Creates a temporary queue, binds it to the specified exchange with the given routing key,
121
+ * and returns a function to collect messages from that queue. The queue is automatically
122
+ * created with a random UUID name to avoid conflicts between tests.
123
+ *
124
+ * The returned function uses `vi.waitFor()` with a configurable timeout to wait for messages.
125
+ * If the expected number of messages is not received within the timeout period, the Promise
126
+ * will reject with a timeout error, preventing tests from hanging indefinitely.
127
+ *
128
+ * @param exchange - The name of the exchange to bind the queue to
129
+ * @param routingKey - The routing key pattern for message filtering
130
+ * @returns A function that accepts optional configuration ({ nbEvents?, timeout? }) and returns a Promise that resolves to an array of ConsumeMessage objects
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * it('should consume messages', async ({ initConsumer, publishMessage }) => {
135
+ * const waitForMessages = await initConsumer('my-exchange', 'routing.key');
136
+ * publishMessage('my-exchange', 'routing.key', { data: 'test' });
137
+ * // With defaults (1 message, 5000ms timeout)
138
+ * const messages = await waitForMessages();
139
+ * expect(messages).toHaveLength(1);
140
+ *
141
+ * // With custom options
142
+ * publishMessage('my-exchange', 'routing.key', { data: 'test2' });
143
+ * publishMessage('my-exchange', 'routing.key', { data: 'test3' });
144
+ * const messages2 = await waitForMessages({ nbEvents: 2, timeout: 10000 });
145
+ * expect(messages2).toHaveLength(2);
146
+ * });
147
+ * ```
148
+ */
43
149
  initConsumer: async ({ amqpChannel }, use) => {
44
150
  const consumerTags = [];
45
151
  async function initConsumer(exchange, routingKey) {
@@ -1 +1 @@
1
- {"version":3,"file":"extension.mjs","names":["vitestIt"],"sources":["../src/extension.ts"],"sourcesContent":["/**\n * Vitest extension module for AMQP testing utilities\n *\n * This module provides a Vitest test extension that adds AMQP-specific fixtures\n * to your tests. Each test gets an isolated virtual host (vhost) with pre-configured\n * connections, channels, and helper functions for publishing and consuming messages.\n *\n * @module extension\n * @packageDocumentation\n */\n\nimport amqpLib, { Options, type Channel, type ChannelModel } from \"amqplib\";\nimport { randomUUID } from \"node:crypto\";\nimport { inject, vi, it as vitestIt } from \"vitest\";\n\nexport const it = vitestIt.extend<{\n vhost: string;\n amqpConnectionUrl: string;\n amqpConnection: ChannelModel;\n amqpChannel: Channel;\n publishMessage: (\n exchange: string,\n routingKey: string,\n content: unknown,\n options?: Options.Publish,\n ) => void;\n initConsumer: (\n exchange: string,\n routingKey: string,\n ) => Promise<\n (options?: { nbEvents?: number; timeout?: number }) => Promise<amqpLib.ConsumeMessage[]>\n >;\n}>({\n /**\n * Test fixture that provides an isolated RabbitMQ virtual host (vhost) for the test.\n *\n * Creates a new vhost with a random UUID name for test isolation. The vhost is automatically\n * created before the test runs using the RabbitMQ Management API.\n *\n * @example\n * ```typescript\n * it('should use isolated vhost', async ({ vhost }) => {\n * console.log(`Test running in vhost: ${vhost}`);\n * });\n * ```\n */\n // oxlint-disable-next-line no-empty-pattern\n vhost: async ({}, use) => {\n const vhost = await createVhost();\n try {\n await use(vhost);\n } finally {\n await deleteVhost(vhost);\n }\n },\n /**\n * Test fixture that provides the AMQP connection URL for the test container.\n *\n * Constructs a connection URL using the test container's IP and port, along with\n * the isolated vhost. The URL follows the format: `amqp://guest:guest@host:port/vhost`.\n *\n * @example\n * ```typescript\n * it('should connect with URL', async ({ amqpConnectionUrl }) => {\n * console.log(`Connecting to: ${amqpConnectionUrl}`);\n * });\n * ```\n */\n amqpConnectionUrl: async ({ vhost }, use) => {\n const url = `amqp://guest:guest@${inject(\"__TESTCONTAINERS_RABBITMQ_IP__\")}:${inject(\"__TESTCONTAINERS_RABBITMQ_PORT_5672__\")}/${vhost}`;\n await use(url);\n },\n /**\n * Test fixture that provides an active AMQP connection to RabbitMQ.\n *\n * Establishes a connection using the provided connection URL and automatically closes\n * it after the test completes. This fixture is useful for tests that need direct\n * access to the connection object (e.g., to create multiple channels).\n *\n * @example\n * ```typescript\n * it('should use connection', async ({ amqpConnection }) => {\n * const channel = await amqpConnection.createChannel();\n * // ... use channel\n * });\n * ```\n */\n amqpConnection: async ({ amqpConnectionUrl }, use) => {\n const connection = await amqpLib.connect(amqpConnectionUrl);\n await use(connection);\n await connection.close();\n },\n /**\n * Test fixture that provides an AMQP channel for interacting with RabbitMQ.\n *\n * Creates a channel from the active connection and automatically closes it after\n * the test completes. The channel is used for declaring exchanges, queues, bindings,\n * and publishing/consuming messages.\n *\n * @example\n * ```typescript\n * it('should use channel', async ({ amqpChannel }) => {\n * await amqpChannel.assertExchange('test-exchange', 'topic');\n * await amqpChannel.assertQueue('test-queue');\n * });\n * ```\n */\n amqpChannel: async ({ amqpConnection }, use) => {\n const channel = await amqpConnection.createChannel();\n await use(channel);\n await channel.close();\n },\n /**\n * Test fixture for publishing messages to an AMQP exchange.\n *\n * Provides a helper function to publish messages directly to an exchange during tests.\n * The message content is automatically serialized to JSON and converted to a Buffer.\n *\n * @param exchange - The name of the exchange to publish to\n * @param routingKey - The routing key for message routing\n * @param content - The message payload (will be JSON serialized)\n * @throws Error if the message cannot be published (e.g., write buffer is full)\n *\n * @example\n * ```typescript\n * it('should publish message', async ({ publishMessage }) => {\n * publishMessage('my-exchange', 'routing.key', { data: 'test' });\n * });\n * ```\n */\n publishMessage: async ({ amqpChannel }, use) => {\n function publishMessage(\n exchange: string,\n routingKey: string,\n content: unknown,\n options?: Options.Publish,\n ): void {\n const success = amqpChannel.publish(\n exchange,\n routingKey,\n Buffer.from(JSON.stringify(content)),\n options,\n );\n if (!success) {\n throw new Error(\n `Failed to publish message to exchange \"${exchange}\" with routing key \"${routingKey}\"`,\n );\n }\n }\n await use(publishMessage);\n },\n /**\n * Test fixture for initializing a message consumer on an AMQP queue.\n *\n * Creates a temporary queue, binds it to the specified exchange with the given routing key,\n * and returns a function to collect messages from that queue. The queue is automatically\n * created with a random UUID name to avoid conflicts between tests.\n *\n * The returned function uses `vi.waitFor()` with a configurable timeout to wait for messages.\n * If the expected number of messages is not received within the timeout period, the Promise\n * will reject with a timeout error, preventing tests from hanging indefinitely.\n *\n * @param exchange - The name of the exchange to bind the queue to\n * @param routingKey - The routing key pattern for message filtering\n * @returns A function that accepts optional configuration ({ nbEvents?, timeout? }) and returns a Promise that resolves to an array of ConsumeMessage objects\n *\n * @example\n * ```typescript\n * it('should consume messages', async ({ initConsumer, publishMessage }) => {\n * const waitForMessages = await initConsumer('my-exchange', 'routing.key');\n * publishMessage('my-exchange', 'routing.key', { data: 'test' });\n * // With defaults (1 message, 5000ms timeout)\n * const messages = await waitForMessages();\n * expect(messages).toHaveLength(1);\n *\n * // With custom options\n * publishMessage('my-exchange', 'routing.key', { data: 'test2' });\n * publishMessage('my-exchange', 'routing.key', { data: 'test3' });\n * const messages2 = await waitForMessages({ nbEvents: 2, timeout: 10000 });\n * expect(messages2).toHaveLength(2);\n * });\n * ```\n */\n initConsumer: async ({ amqpChannel }, use) => {\n const consumerTags: string[] = [];\n\n async function initConsumer(\n exchange: string,\n routingKey: string,\n ): Promise<\n (options?: { nbEvents?: number; timeout?: number }) => Promise<amqpLib.ConsumeMessage[]>\n > {\n const queue = randomUUID();\n\n await amqpChannel.assertQueue(queue);\n await amqpChannel.bindQueue(queue, exchange, routingKey);\n\n const messages: amqpLib.ConsumeMessage[] = [];\n const consumer = await amqpChannel.consume(\n queue,\n (msg) => {\n if (msg) {\n messages.push(msg);\n }\n },\n { noAck: true },\n );\n\n consumerTags.push(consumer.consumerTag);\n\n return async (options = {}) => {\n const { nbEvents = 1, timeout = 5000 } = options;\n await vi.waitFor(\n () => {\n if (messages.length < nbEvents) {\n throw new Error(\n `Expected ${nbEvents} message(s) but only received ${messages.length}`,\n );\n }\n },\n { timeout },\n );\n return messages.splice(0, nbEvents);\n };\n }\n\n try {\n await use(initConsumer);\n } finally {\n // Cancel all consumers before fixture cleanup (which deletes the vhost)\n await Promise.all(\n consumerTags.map(async (consumerTag) => {\n try {\n await amqpChannel.cancel(consumerTag);\n } catch (error) {\n // Swallow cancellation errors during cleanup\n // eslint-disable-next-line no-console\n console.error(\"Failed to cancel AMQP consumer during fixture cleanup:\", error);\n }\n }),\n );\n }\n },\n});\n\nasync function createVhost() {\n const namespace = randomUUID();\n\n const username = inject(\"__TESTCONTAINERS_RABBITMQ_USERNAME__\");\n const password = inject(\"__TESTCONTAINERS_RABBITMQ_PASSWORD__\");\n\n const vhostResponse = await fetch(\n `http://${inject(\"__TESTCONTAINERS_RABBITMQ_IP__\")}:${inject(\"__TESTCONTAINERS_RABBITMQ_PORT_15672__\")}/api/vhosts/${encodeURIComponent(namespace)}`,\n {\n method: \"PUT\",\n headers: {\n Authorization: `Basic ${btoa(`${username}:${password}`)}`,\n },\n },\n );\n\n if (vhostResponse.status !== 201) {\n const responseBody = await vhostResponse.text().catch(() => \"\");\n const errorMessage = responseBody\n ? `Failed to create vhost '${namespace}': ${vhostResponse.status} - ${responseBody}`\n : `Failed to create vhost '${namespace}': ${vhostResponse.status}`;\n throw new Error(errorMessage, {\n cause: vhostResponse,\n });\n }\n\n return namespace;\n}\n\nasync function deleteVhost(vhost: string) {\n const username = inject(\"__TESTCONTAINERS_RABBITMQ_USERNAME__\");\n const password = inject(\"__TESTCONTAINERS_RABBITMQ_PASSWORD__\");\n\n const vhostResponse = await fetch(\n `http://${inject(\"__TESTCONTAINERS_RABBITMQ_IP__\")}:${inject(\"__TESTCONTAINERS_RABBITMQ_PORT_15672__\")}/api/vhosts/${encodeURIComponent(vhost)}`,\n {\n method: \"DELETE\",\n headers: {\n Authorization: `Basic ${btoa(`${username}:${password}`)}`,\n },\n },\n );\n\n // 204 = successfully deleted, 404 = already deleted or doesn't exist\n if (vhostResponse.status !== 204 && vhostResponse.status !== 404) {\n const responseBody = await vhostResponse.text().catch(() => \"\");\n const errorMessage = responseBody\n ? `Failed to delete vhost '${vhost}': ${vhostResponse.status} - ${responseBody}`\n : `Failed to delete vhost '${vhost}': ${vhostResponse.status}`;\n throw new Error(errorMessage, {\n cause: vhostResponse,\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"extension.mjs","names":["vitestIt"],"sources":["../src/extension.ts"],"sourcesContent":["/**\n * Vitest extension module for AMQP testing utilities\n *\n * This module provides a Vitest test extension that adds AMQP-specific fixtures\n * to your tests. Each test gets an isolated virtual host (vhost) with pre-configured\n * connections, channels, and helper functions for publishing and consuming messages.\n *\n * @module extension\n * @packageDocumentation\n */\n\nimport amqpLib, { Options, type Channel, type ChannelModel } from \"amqplib\";\nimport { randomUUID } from \"node:crypto\";\nimport { inject, vi, it as vitestIt } from \"vitest\";\n\nexport const it = vitestIt.extend<{\n vhost: string;\n amqpConnectionUrl: string;\n amqpConnection: ChannelModel;\n amqpChannel: Channel;\n publishMessage: (\n exchange: string,\n routingKey: string,\n content: unknown,\n options?: Options.Publish,\n ) => void;\n initConsumer: (\n exchange: string,\n routingKey: string,\n ) => Promise<\n (options?: { nbEvents?: number; timeout?: number }) => Promise<amqpLib.ConsumeMessage[]>\n >;\n}>({\n /**\n * Test fixture that provides an isolated RabbitMQ virtual host (vhost) for the test.\n *\n * Creates a new vhost with a random UUID name for test isolation. The vhost is automatically\n * created before the test runs using the RabbitMQ Management API.\n *\n * @example\n * ```typescript\n * it('should use isolated vhost', async ({ vhost }) => {\n * console.log(`Test running in vhost: ${vhost}`);\n * });\n * ```\n */\n // oxlint-disable-next-line no-empty-pattern\n vhost: async ({}, use) => {\n const vhost = await createVhost();\n try {\n await use(vhost);\n } finally {\n await deleteVhost(vhost);\n }\n },\n /**\n * Test fixture that provides the AMQP connection URL for the test container.\n *\n * Constructs a connection URL using the test container's IP and port, along with\n * the isolated vhost. The URL follows the format: `amqp://guest:guest@host:port/vhost`.\n *\n * @example\n * ```typescript\n * it('should connect with URL', async ({ amqpConnectionUrl }) => {\n * console.log(`Connecting to: ${amqpConnectionUrl}`);\n * });\n * ```\n */\n amqpConnectionUrl: async ({ vhost }, use) => {\n const url = `amqp://guest:guest@${inject(\"__TESTCONTAINERS_RABBITMQ_IP__\")}:${inject(\"__TESTCONTAINERS_RABBITMQ_PORT_5672__\")}/${vhost}`;\n await use(url);\n },\n /**\n * Test fixture that provides an active AMQP connection to RabbitMQ.\n *\n * Establishes a connection using the provided connection URL and automatically closes\n * it after the test completes. This fixture is useful for tests that need direct\n * access to the connection object (e.g., to create multiple channels).\n *\n * @example\n * ```typescript\n * it('should use connection', async ({ amqpConnection }) => {\n * const channel = await amqpConnection.createChannel();\n * // ... use channel\n * });\n * ```\n */\n amqpConnection: async ({ amqpConnectionUrl }, use) => {\n const connection = await amqpLib.connect(amqpConnectionUrl);\n await use(connection);\n await connection.close();\n },\n /**\n * Test fixture that provides an AMQP channel for interacting with RabbitMQ.\n *\n * Creates a channel from the active connection and automatically closes it after\n * the test completes. The channel is used for declaring exchanges, queues, bindings,\n * and publishing/consuming messages.\n *\n * @example\n * ```typescript\n * it('should use channel', async ({ amqpChannel }) => {\n * await amqpChannel.assertExchange('test-exchange', 'topic');\n * await amqpChannel.assertQueue('test-queue');\n * });\n * ```\n */\n amqpChannel: async ({ amqpConnection }, use) => {\n const channel = await amqpConnection.createChannel();\n await use(channel);\n await channel.close();\n },\n /**\n * Test fixture for publishing messages to an AMQP exchange.\n *\n * Provides a helper function to publish messages directly to an exchange during tests.\n * The message content is automatically serialized to JSON and converted to a Buffer.\n *\n * @param exchange - The name of the exchange to publish to\n * @param routingKey - The routing key for message routing\n * @param content - The message payload (will be JSON serialized)\n * @throws Error if the message cannot be published (e.g., write buffer is full)\n *\n * @example\n * ```typescript\n * it('should publish message', async ({ publishMessage }) => {\n * publishMessage('my-exchange', 'routing.key', { data: 'test' });\n * });\n * ```\n */\n publishMessage: async ({ amqpChannel }, use) => {\n function publishMessage(\n exchange: string,\n routingKey: string,\n content: unknown,\n options?: Options.Publish,\n ): void {\n const success = amqpChannel.publish(\n exchange,\n routingKey,\n Buffer.from(JSON.stringify(content)),\n options,\n );\n if (!success) {\n throw new Error(\n `Failed to publish message to exchange \"${exchange}\" with routing key \"${routingKey}\"`,\n );\n }\n }\n await use(publishMessage);\n },\n /**\n * Test fixture for initializing a message consumer on an AMQP queue.\n *\n * Creates a temporary queue, binds it to the specified exchange with the given routing key,\n * and returns a function to collect messages from that queue. The queue is automatically\n * created with a random UUID name to avoid conflicts between tests.\n *\n * The returned function uses `vi.waitFor()` with a configurable timeout to wait for messages.\n * If the expected number of messages is not received within the timeout period, the Promise\n * will reject with a timeout error, preventing tests from hanging indefinitely.\n *\n * @param exchange - The name of the exchange to bind the queue to\n * @param routingKey - The routing key pattern for message filtering\n * @returns A function that accepts optional configuration ({ nbEvents?, timeout? }) and returns a Promise that resolves to an array of ConsumeMessage objects\n *\n * @example\n * ```typescript\n * it('should consume messages', async ({ initConsumer, publishMessage }) => {\n * const waitForMessages = await initConsumer('my-exchange', 'routing.key');\n * publishMessage('my-exchange', 'routing.key', { data: 'test' });\n * // With defaults (1 message, 5000ms timeout)\n * const messages = await waitForMessages();\n * expect(messages).toHaveLength(1);\n *\n * // With custom options\n * publishMessage('my-exchange', 'routing.key', { data: 'test2' });\n * publishMessage('my-exchange', 'routing.key', { data: 'test3' });\n * const messages2 = await waitForMessages({ nbEvents: 2, timeout: 10000 });\n * expect(messages2).toHaveLength(2);\n * });\n * ```\n */\n initConsumer: async ({ amqpChannel }, use) => {\n const consumerTags: string[] = [];\n\n async function initConsumer(\n exchange: string,\n routingKey: string,\n ): Promise<\n (options?: { nbEvents?: number; timeout?: number }) => Promise<amqpLib.ConsumeMessage[]>\n > {\n const queue = randomUUID();\n\n await amqpChannel.assertQueue(queue);\n await amqpChannel.bindQueue(queue, exchange, routingKey);\n\n const messages: amqpLib.ConsumeMessage[] = [];\n const consumer = await amqpChannel.consume(\n queue,\n (msg) => {\n if (msg) {\n messages.push(msg);\n }\n },\n { noAck: true },\n );\n\n consumerTags.push(consumer.consumerTag);\n\n return async (options = {}) => {\n const { nbEvents = 1, timeout = 5000 } = options;\n await vi.waitFor(\n () => {\n if (messages.length < nbEvents) {\n throw new Error(\n `Expected ${nbEvents} message(s) but only received ${messages.length}`,\n );\n }\n },\n { timeout },\n );\n return messages.splice(0, nbEvents);\n };\n }\n\n try {\n await use(initConsumer);\n } finally {\n // Cancel all consumers before fixture cleanup (which deletes the vhost)\n await Promise.all(\n consumerTags.map(async (consumerTag) => {\n try {\n await amqpChannel.cancel(consumerTag);\n } catch (error) {\n // Swallow cancellation errors during cleanup\n // eslint-disable-next-line no-console\n console.error(\"Failed to cancel AMQP consumer during fixture cleanup:\", error);\n }\n }),\n );\n }\n },\n});\n\nasync function createVhost() {\n const namespace = randomUUID();\n\n const username = inject(\"__TESTCONTAINERS_RABBITMQ_USERNAME__\");\n const password = inject(\"__TESTCONTAINERS_RABBITMQ_PASSWORD__\");\n\n const vhostResponse = await fetch(\n `http://${inject(\"__TESTCONTAINERS_RABBITMQ_IP__\")}:${inject(\"__TESTCONTAINERS_RABBITMQ_PORT_15672__\")}/api/vhosts/${encodeURIComponent(namespace)}`,\n {\n method: \"PUT\",\n headers: {\n Authorization: `Basic ${btoa(`${username}:${password}`)}`,\n },\n },\n );\n\n if (vhostResponse.status !== 201) {\n const responseBody = await vhostResponse.text().catch(() => \"\");\n const errorMessage = responseBody\n ? `Failed to create vhost '${namespace}': ${vhostResponse.status} - ${responseBody}`\n : `Failed to create vhost '${namespace}': ${vhostResponse.status}`;\n throw new Error(errorMessage, {\n cause: vhostResponse,\n });\n }\n\n return namespace;\n}\n\nasync function deleteVhost(vhost: string) {\n const username = inject(\"__TESTCONTAINERS_RABBITMQ_USERNAME__\");\n const password = inject(\"__TESTCONTAINERS_RABBITMQ_PASSWORD__\");\n\n const vhostResponse = await fetch(\n `http://${inject(\"__TESTCONTAINERS_RABBITMQ_IP__\")}:${inject(\"__TESTCONTAINERS_RABBITMQ_PORT_15672__\")}/api/vhosts/${encodeURIComponent(vhost)}`,\n {\n method: \"DELETE\",\n headers: {\n Authorization: `Basic ${btoa(`${username}:${password}`)}`,\n },\n },\n );\n\n // 204 = successfully deleted, 404 = already deleted or doesn't exist\n if (vhostResponse.status !== 204 && vhostResponse.status !== 404) {\n const responseBody = await vhostResponse.text().catch(() => \"\");\n const errorMessage = responseBody\n ? `Failed to delete vhost '${vhost}': ${vhostResponse.status} - ${responseBody}`\n : `Failed to delete vhost '${vhost}': ${vhostResponse.status}`;\n throw new Error(errorMessage, {\n cause: vhostResponse,\n });\n }\n}\n"],"mappings":"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"}
package/docs/extension.md CHANGED
@@ -27,4 +27,4 @@ const it: TestAPI<{
27
27
  }>;
28
28
  ```
29
29
 
30
- Defined in: [extension.ts:16](https://github.com/btravers/amqp-contract/blob/c4431c816689353f677718608623c58fd1757388/packages/testing/src/extension.ts#L16)
30
+ Defined in: [extension.ts:16](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/testing/src/extension.ts#L16)
@@ -23,7 +23,7 @@ By default, it uses the public Docker Hub image (`rabbitmq:4.2.1-management-alpi
23
23
  function default(provide): Promise<() => Promise<void>>;
24
24
  ```
25
25
 
26
- Defined in: [global-setup.ts:58](https://github.com/btravers/amqp-contract/blob/c4431c816689353f677718608623c58fd1757388/packages/testing/src/global-setup.ts#L58)
26
+ Defined in: [global-setup.ts:58](https://github.com/btravers/amqp-contract/blob/da9b95c747db8d5af9183ca6fe2a6e0af558d1fa/packages/testing/src/global-setup.ts#L58)
27
27
 
28
28
  Setup function for Vitest globalSetup
29
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amqp-contract/testing",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Testing utilities for AMQP contracts with testcontainers",
5
5
  "keywords": [
6
6
  "amqp",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/amqplib": "0.10.8",
42
- "tsdown": "0.21.9",
42
+ "tsdown": "0.21.10",
43
43
  "typedoc": "0.28.19",
44
44
  "typedoc-plugin-markdown": "4.11.0",
45
45
  "typescript": "6.0.3",