{"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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"}