From e5e772bf4a3f80235426f354ebee1199ab6e62e9 Mon Sep 17 00:00:00 2001 From: George Gunderson Date: Mon, 10 Jun 2024 21:51:41 +1000 Subject: [PATCH] Add RabbitMQ module (#770) --- docs/modules/rabbitmq.md | 23 ++++ mkdocs.yml | 1 + package-lock.json | 112 ++++++++++++++++++ packages/modules/rabbitmq/jest.config.ts | 11 ++ packages/modules/rabbitmq/package.json | 38 ++++++ packages/modules/rabbitmq/src/index.ts | 1 + .../rabbitmq/src/rabbitmq-container.test.ts | 68 +++++++++++ .../rabbitmq/src/rabbitmq-container.ts | 39 ++++++ packages/modules/rabbitmq/tsconfig.build.json | 13 ++ packages/modules/rabbitmq/tsconfig.json | 21 ++++ 10 files changed, 327 insertions(+) create mode 100644 docs/modules/rabbitmq.md create mode 100644 packages/modules/rabbitmq/jest.config.ts create mode 100644 packages/modules/rabbitmq/package.json create mode 100644 packages/modules/rabbitmq/src/index.ts create mode 100644 packages/modules/rabbitmq/src/rabbitmq-container.test.ts create mode 100644 packages/modules/rabbitmq/src/rabbitmq-container.ts create mode 100644 packages/modules/rabbitmq/tsconfig.build.json create mode 100644 packages/modules/rabbitmq/tsconfig.json diff --git a/docs/modules/rabbitmq.md b/docs/modules/rabbitmq.md new file mode 100644 index 000000000..df2b531d1 --- /dev/null +++ b/docs/modules/rabbitmq.md @@ -0,0 +1,23 @@ +# RabbitMQ Module + +[RabbitMQ](https://www.rabbitmq.com/) is a reliable and mature messaging and streaming broker, which is easy to deploy on cloud environments, on-premises, and on your local machine. It is currently used by millions worldwide. + +## Install + +```bash +npm install @testcontainers/rabbitmq --save-dev +``` + +## Examples + + +[Connect:](../../packages/modules/rabbitmq/src/rabbitmq-container.test.ts) inside_block:start + + + +[Set credentials:](../../packages/modules/rabbitmq/src/rabbitmq-container.test.ts) inside_block:credentials + + + +[Publish and subscribe:](../../packages/modules/rabbitmq/src/rabbitmq-container.test.ts) inside_block:pubsub + diff --git a/mkdocs.yml b/mkdocs.yml index d664eb740..a4adec7d9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -57,6 +57,7 @@ nav: - Neo4J: modules/neo4j.md - PostgreSQL: modules/postgresql.md - Qdrant: modules/qdrant.md + - RabbitMQ: modules/rabbitmq.md - Redis: modules/redis.md - Redpanda: modules/redpanda.md - Selenium: modules/selenium.md diff --git a/package-lock.json b/package-lock.json index 898ba88a9..915e35261 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,26 @@ "node": ">=0.10.0" } }, + "node_modules/@acuminous/bitsyntax": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz", + "integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==", + "dev": true, + "dependencies": { + "buffer-more-ints": "~1.0.0", + "debug": "^4.3.4", + "safe-buffer": "~5.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@acuminous/bitsyntax/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -4869,6 +4889,10 @@ "resolved": "packages/modules/qdrant", "link": true }, + "node_modules/@testcontainers/rabbitmq": { + "resolved": "packages/modules/rabbitmq", + "link": true + }, "node_modules/@testcontainers/redis": { "resolved": "packages/modules/redis", "link": true @@ -4970,6 +4994,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@types/amqplib": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.5.tgz", + "integrity": "sha512-/cSykxROY7BWwDoi4Y4/jLAuZTshZxd8Ey1QYa/VaXriMotBDoou7V/twJiOSHzU6t1Kp1AHAUXGCgqq+6DNeg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/archiver": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.4.tgz", @@ -5770,6 +5803,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/amqplib": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.4.tgz", + "integrity": "sha512-DMZ4eCEjAVdX1II2TfIUpJhfKAuoCeDIo/YyETbfAqehHTXxxs7WOOd+N1Xxr4cKhx12y23zk8/os98FxlZHrw==", + "dev": true, + "dependencies": { + "@acuminous/bitsyntax": "^0.1.2", + "buffer-more-ints": "~1.0.0", + "readable-stream": "1.x >=1.1.9", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/amqplib/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/amqplib/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/amqplib/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", @@ -6641,6 +6713,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "dev": true + }, "node_modules/buffer-writer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", @@ -15579,6 +15657,12 @@ } ] }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -15919,6 +16003,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -17935,6 +18025,16 @@ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "dev": true }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -18686,6 +18786,18 @@ "@qdrant/js-client-rest": "^1.8.0" } }, + "packages/modules/rabbitmq": { + "name": "@testcontainers/rabbitmq", + "version": "10.9.0", + "license": "MIT", + "dependencies": { + "testcontainers": "^10.9.0" + }, + "devDependencies": { + "@types/amqplib": "^0.10.4", + "amqplib": "^0.10.4" + } + }, "packages/modules/redis": { "name": "@testcontainers/redis", "version": "10.9.0", diff --git a/packages/modules/rabbitmq/jest.config.ts b/packages/modules/rabbitmq/jest.config.ts new file mode 100644 index 000000000..1f677baaf --- /dev/null +++ b/packages/modules/rabbitmq/jest.config.ts @@ -0,0 +1,11 @@ +import type { Config } from "jest"; +import * as path from "path"; + +const config: Config = { + preset: "ts-jest", + moduleNameMapper: { + "^testcontainers$": path.resolve(__dirname, "../../testcontainers/src"), + }, +}; + +export default config; diff --git a/packages/modules/rabbitmq/package.json b/packages/modules/rabbitmq/package.json new file mode 100644 index 000000000..16e551188 --- /dev/null +++ b/packages/modules/rabbitmq/package.json @@ -0,0 +1,38 @@ +{ + "name": "@testcontainers/rabbitmq", + "version": "10.9.0", + "license": "MIT", + "keywords": [ + "rabbitmq", + "testing", + "docker", + "testcontainers" + ], + "description": "RabbitMQ module for Testcontainers", + "homepage": "https://github.com/testcontainers/testcontainers-node#readme", + "repository": { + "type": "git", + "url": "https://github.com/testcontainers/testcontainers-node" + }, + "bugs": { + "url": "https://github.com/testcontainers/testcontainers-node/issues" + }, + "main": "build/index.js", + "files": [ + "build" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "prepack": "shx cp ../../../README.md . && shx cp ../../../LICENSE .", + "build": "tsc --project tsconfig.build.json" + }, + "dependencies": { + "testcontainers": "^10.9.0" + }, + "devDependencies": { + "@types/amqplib": "^0.10.4", + "amqplib": "^0.10.4" + } +} diff --git a/packages/modules/rabbitmq/src/index.ts b/packages/modules/rabbitmq/src/index.ts new file mode 100644 index 000000000..83b89a6cf --- /dev/null +++ b/packages/modules/rabbitmq/src/index.ts @@ -0,0 +1 @@ +export { RabbitMQContainer, StartedRabbitMQContainer } from "./rabbitmq-container"; diff --git a/packages/modules/rabbitmq/src/rabbitmq-container.test.ts b/packages/modules/rabbitmq/src/rabbitmq-container.test.ts new file mode 100644 index 000000000..0b01ea600 --- /dev/null +++ b/packages/modules/rabbitmq/src/rabbitmq-container.test.ts @@ -0,0 +1,68 @@ +import amqp from "amqplib"; +import { RabbitMQContainer } from "./rabbitmq-container"; + +describe("RabbitMQContainer", () => { + jest.setTimeout(240_000); + + // start { + it("should start, connect and close", async () => { + const rabbitMQContainer = await new RabbitMQContainer().start(); + + const connection = await amqp.connect(rabbitMQContainer.getAmqpUrl()); + await connection.close(); + + await rabbitMQContainer.stop(); + }); + // } + + // credentials { + it("different username and password", async () => { + const USER = "user"; + const PASSWORD = "password"; + + const rabbitMQContainer = await new RabbitMQContainer() + .withEnvironment({ + RABBITMQ_DEFAULT_USER: USER, + RABBITMQ_DEFAULT_PASS: PASSWORD, + }) + .start(); + + const connection = await amqp.connect({ + username: USER, + password: PASSWORD, + port: rabbitMQContainer.getMappedPort(5672), + }); + + await connection.close(); + + await rabbitMQContainer.stop(); + }); + // } + + // pubsub { + it("test publish and subscribe", async () => { + const QUEUE = "test"; + const PAYLOAD = "Hello World"; + + const rabbitMQContainer = await new RabbitMQContainer().start(); + const connection = await amqp.connect(rabbitMQContainer.getAmqpUrl()); + + const channel = await connection.createChannel(); + await channel.assertQueue(QUEUE); + + channel.sendToQueue(QUEUE, Buffer.from(PAYLOAD)); + + await new Promise((resolve) => { + channel.consume(QUEUE, (message) => { + expect(message?.content.toString()).toEqual(PAYLOAD); + resolve(true); + }); + }); + + await channel.close(); + await connection.close(); + + await rabbitMQContainer.stop(); + }, 10_000); + // } +}); diff --git a/packages/modules/rabbitmq/src/rabbitmq-container.ts b/packages/modules/rabbitmq/src/rabbitmq-container.ts new file mode 100644 index 000000000..dde5b16a9 --- /dev/null +++ b/packages/modules/rabbitmq/src/rabbitmq-container.ts @@ -0,0 +1,39 @@ +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; + +const AMQP_PORT = 5672; +const AMQPS_PORT = 5671; +const HTTPS_PORT = 15671; +const HTTP_PORT = 15672; +const RABBITMQ_DEFAULT_USER = "guest"; +const RABBITMQ_DEFAULT_PASS = "guest"; + +export class RabbitMQContainer extends GenericContainer { + constructor(image = "rabbitmq:3.12.11-management-alpine") { + super(image); + this.withExposedPorts(AMQP_PORT, AMQPS_PORT, HTTPS_PORT, HTTP_PORT) + .withEnvironment({ + RABBITMQ_DEFAULT_USER, + RABBITMQ_DEFAULT_PASS, + }) + .withWaitStrategy(Wait.forLogMessage("Server startup complete")) + .withStartupTimeout(30_000); + } + + public override async start(): Promise { + return new StartedRabbitMQContainer(await super.start()); + } +} + +export class StartedRabbitMQContainer extends AbstractStartedContainer { + constructor(startedTestContainer: StartedTestContainer) { + super(startedTestContainer); + } + + public getAmqpUrl(): string { + return `amqp://${this.getHost()}:${this.getMappedPort(AMQP_PORT)}`; + } + + public getAmqpsUrl(): string { + return `amqps://${this.getHost()}:${this.getMappedPort(AMQPS_PORT)}`; + } +} diff --git a/packages/modules/rabbitmq/tsconfig.build.json b/packages/modules/rabbitmq/tsconfig.build.json new file mode 100644 index 000000000..0222f6ff1 --- /dev/null +++ b/packages/modules/rabbitmq/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "build", + "jest.config.ts", + "src/**/*.test.ts" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file diff --git a/packages/modules/rabbitmq/tsconfig.json b/packages/modules/rabbitmq/tsconfig.json new file mode 100644 index 000000000..39b165817 --- /dev/null +++ b/packages/modules/rabbitmq/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "paths": { + "testcontainers": [ + "../../testcontainers/src" + ] + } + }, + "exclude": [ + "build", + "jest.config.ts" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file