From bccad75f8c1cb252c58add5c5b981b8a5c87f652 Mon Sep 17 00:00:00 2001 From: Guillaume Chx <43764147+ChxGuillaume@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:03:27 +0200 Subject: [PATCH 01/12] Add Mosquitto module for Testcontainers, including container setup, tests, and documentation updates. --- docs/modules/mosquitto.md | 29 ++++++++ mkdocs.yml | 1 + package-lock.json | 45 +++++-------- packages/modules/mosquitto/Dockerfile | 1 + packages/modules/mosquitto/package.json | 38 +++++++++++ packages/modules/mosquitto/src/index.ts | 1 + .../mosquitto/src/mosquitto-container.test.ts | 50 ++++++++++++++ .../mosquitto/src/mosquitto-container.ts | 67 +++++++++++++++++++ .../modules/mosquitto/tsconfig.build.json | 12 ++++ packages/modules/mosquitto/tsconfig.json | 20 ++++++ 10 files changed, 234 insertions(+), 30 deletions(-) create mode 100644 docs/modules/mosquitto.md create mode 100644 packages/modules/mosquitto/Dockerfile create mode 100644 packages/modules/mosquitto/package.json create mode 100644 packages/modules/mosquitto/src/index.ts create mode 100644 packages/modules/mosquitto/src/mosquitto-container.test.ts create mode 100644 packages/modules/mosquitto/src/mosquitto-container.ts create mode 100644 packages/modules/mosquitto/tsconfig.build.json create mode 100644 packages/modules/mosquitto/tsconfig.json diff --git a/docs/modules/mosquitto.md b/docs/modules/mosquitto.md new file mode 100644 index 000000000..c61e1b83c --- /dev/null +++ b/docs/modules/mosquitto.md @@ -0,0 +1,29 @@ +# Mosquitto + +## Install + +```bash +npm install @testcontainers/mosquitto --save-dev +``` + +## Examples + +These examples use the following libraries: + +- [mqtt](https://www.npmjs.com/package/mqtt) + + npm install mqtt + +Choose an image from the [container registry](https://hub.docker.com/r/eclipse-mosquitto) and substitute `IMAGE`. + +### Produce/consume a message (anonymous) + + +[](../../packages/modules/mosquitto/src/mosquitto-container.test.ts) inside_block:mosquittoConnect + + +### Produce/consume a message (with credentials) + + +[](../../packages/modules/mosquitto/src/mosquitto-container.test.ts) inside_block:mosquittoConnectWithCredentials + diff --git a/mkdocs.yml b/mkdocs.yml index 327f3902d..2bb38d7f2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -73,6 +73,7 @@ nav: - Localstack: modules/localstack.md - MariaDB: modules/mariadb.md - MinIO: modules/minio.md + - Mosquitto: modules/mosquitto.md - Mockserver: modules/mockserver.md - MongoDB: modules/mongodb.md - MSSQLServer: modules/mssqlserver.md diff --git a/package-lock.json b/package-lock.json index 4aeaacd41..1fd927250 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4859,9 +4859,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4879,9 +4876,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4899,9 +4893,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4919,9 +4910,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4939,9 +4927,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4959,9 +4944,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5279,6 +5261,10 @@ "resolved": "packages/modules/mongodb", "link": true }, + "node_modules/@testcontainers/mosquitto": { + "resolved": "packages/modules/mosquitto", + "link": true + }, "node_modules/@testcontainers/mssqlserver": { "resolved": "packages/modules/mssqlserver", "link": true @@ -10919,9 +10905,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10943,9 +10926,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10967,9 +10947,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10991,9 +10968,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -15951,6 +15925,17 @@ "mongoose": "^9.7.1" } }, + "packages/modules/mosquitto": { + "name": "@testcontainers/mosquitto", + "version": "12.0.3", + "license": "MIT", + "dependencies": { + "testcontainers": "^12.0.3" + }, + "devDependencies": { + "mqtt": "^5.15.1" + } + }, "packages/modules/mssqlserver": { "name": "@testcontainers/mssqlserver", "version": "12.0.3", diff --git a/packages/modules/mosquitto/Dockerfile b/packages/modules/mosquitto/Dockerfile new file mode 100644 index 000000000..0ead37795 --- /dev/null +++ b/packages/modules/mosquitto/Dockerfile @@ -0,0 +1 @@ +FROM eclipse-mosquitto:2.0 diff --git a/packages/modules/mosquitto/package.json b/packages/modules/mosquitto/package.json new file mode 100644 index 000000000..9f7bbd969 --- /dev/null +++ b/packages/modules/mosquitto/package.json @@ -0,0 +1,38 @@ +{ + "name": "@testcontainers/mosquitto", + "version": "12.0.3", + "license": "MIT", + "keywords": [ + "mosquitto", + "mqtt", + "testing", + "docker", + "testcontainers" + ], + "description": "Mosquitto module for Testcontainers", + "homepage": "https://github.com/testcontainers/testcontainers-node#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/testcontainers/testcontainers-node.git" + }, + "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" + }, + "devDependencies": { + "mqtt": "^5.15.1" + }, + "dependencies": { + "testcontainers": "^12.0.3" + } +} diff --git a/packages/modules/mosquitto/src/index.ts b/packages/modules/mosquitto/src/index.ts new file mode 100644 index 000000000..eb898d044 --- /dev/null +++ b/packages/modules/mosquitto/src/index.ts @@ -0,0 +1 @@ +export { MosquittoContainer, StartedMosquittoContainer } from "./mosquitto-container"; diff --git a/packages/modules/mosquitto/src/mosquitto-container.test.ts b/packages/modules/mosquitto/src/mosquitto-container.test.ts new file mode 100644 index 000000000..270c5df44 --- /dev/null +++ b/packages/modules/mosquitto/src/mosquitto-container.test.ts @@ -0,0 +1,50 @@ +import mqtt from "mqtt"; +import { expect } from "vitest"; +import { getImage } from "../../../testcontainers/src/utils/test-helper"; +import { MosquittoContainer } from "./mosquitto-container"; + +const IMAGE = getImage(__dirname); + +describe("MosquittoContainer", { timeout: 240_000 }, () => { + it("should connect to Mosquitto via MQTT.js (anonymous)", async () => { + // mosquittoConnect { + await using container = await new MosquittoContainer(IMAGE).start(); + + const mqttClient = await mqtt.connectAsync(container.getConnectionString()); + + const firstMessagePromise = new Promise<{ topic: string; message: Buffer }>((resolve, reject) => { + mqttClient.once("message", (topic, message) => resolve({ topic, message })); + mqttClient.once("error", (err) => reject(err)); + }); + + await mqttClient.subscribeAsync("test"); + await mqttClient.publishAsync("test", "Test Message"); + + const { message } = await firstMessagePromise; + expect(message.toString()).toEqual("Test Message"); + + mqttClient.end(); + // } + }); + + it("should connect to Mosquitto via MQTT.js (with credentials)", async () => { + // mosquittoConnectWithCredentials { + await using container = await new MosquittoContainer(IMAGE).withUsername("testuser").withPassword("testpass").start(); + + const mqttClient = await mqtt.connectAsync(container.getConnectionString()); + + const firstMessagePromise = new Promise<{ topic: string; message: Buffer }>((resolve, reject) => { + mqttClient.once("message", (topic, message) => resolve({ topic, message })); + mqttClient.once("error", (err) => reject(err)); + }); + + await mqttClient.subscribeAsync("secure"); + await mqttClient.publishAsync("secure", "Secure Message"); + + const { message } = await firstMessagePromise; + expect(message.toString()).toEqual("Secure Message"); + + mqttClient.end(); + // } + }); +}); diff --git a/packages/modules/mosquitto/src/mosquitto-container.ts b/packages/modules/mosquitto/src/mosquitto-container.ts new file mode 100644 index 000000000..d0b15e6b9 --- /dev/null +++ b/packages/modules/mosquitto/src/mosquitto-container.ts @@ -0,0 +1,67 @@ +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; + +const MQTT_PORT = 1883; +const CONFIG_PATH = "/mosquitto/config/mosquitto.conf"; +const PASSWORD_FILE_PATH = "/mosquitto/config/passwords"; + +export class MosquittoContainer extends GenericContainer { + private username?: string; + private password?: string; + + constructor(image: string) { + super(image); + this.withExposedPorts(MQTT_PORT).withWaitStrategy(Wait.forLogMessage(/mosquitto version .* starting/i)).withStartupTimeout(120_000); + } + + public withUsername(username: string): this { + this.username = username; + return this; + } + + public withPassword(password: string): this { + this.password = password; + return this; + } + + public override async start(): Promise { + if (this.username !== undefined && this.password !== undefined) { + const config = `listener ${MQTT_PORT}\npassword_file ${PASSWORD_FILE_PATH}\n`; + this.withCopyContentToContainer([{ content: config, target: CONFIG_PATH }]) + .withEnvironment({ MQTT_USER: this.username, MQTT_PASS: this.password }) + .withEntrypoint(["/bin/sh"]) + .withCommand([ + "-c", + `mosquitto_passwd -b -c "${PASSWORD_FILE_PATH}" "$MQTT_USER" "$MQTT_PASS" && exec mosquitto -c "${CONFIG_PATH}"`, + ]); + } else { + const config = `listener ${MQTT_PORT}\nallow_anonymous true\n`; + this.withCopyContentToContainer([{ content: config, target: CONFIG_PATH }]); + } + + return new StartedMosquittoContainer(await super.start(), this.username, this.password); + } +} + +export class StartedMosquittoContainer extends AbstractStartedContainer { + private readonly port: number; + + constructor( + startedTestContainer: StartedTestContainer, + private readonly username?: string, + private readonly password?: string + ) { + super(startedTestContainer); + this.port = startedTestContainer.getMappedPort(MQTT_PORT); + } + + public getPort(): number { + return this.port; + } + + public getConnectionString(): string { + if (this.username && this.password) { + return `mqtt://${encodeURIComponent(this.username)}:${encodeURIComponent(this.password)}@${this.getHost()}:${this.getPort()}`; + } + return `mqtt://${this.getHost()}:${this.getPort()}`; + } +} diff --git a/packages/modules/mosquitto/tsconfig.build.json b/packages/modules/mosquitto/tsconfig.build.json new file mode 100644 index 000000000..e9236a57d --- /dev/null +++ b/packages/modules/mosquitto/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "build", + "src/**/*.test.ts" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} diff --git a/packages/modules/mosquitto/tsconfig.json b/packages/modules/mosquitto/tsconfig.json new file mode 100644 index 000000000..0e863cb02 --- /dev/null +++ b/packages/modules/mosquitto/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "paths": { + "testcontainers": [ + "../../testcontainers/src" + ] + } + }, + "exclude": [ + "build" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} From 6fc7ba09e77127c7975910d8f5ab589e484c40f3 Mon Sep 17 00:00:00 2001 From: Guillaume Chx <43764147+ChxGuillaume@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:27:07 +0200 Subject: [PATCH 02/12] Update Mosquitto module: refine connection string tests, adjust startup log matching, upgrade to Mosquitto 2 base image, and fix file permissions during setup. --- packages/modules/mosquitto/Dockerfile | 2 +- .../modules/mosquitto/src/mosquitto-container.test.ts | 11 ++++++++++- packages/modules/mosquitto/src/mosquitto-container.ts | 6 ++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/modules/mosquitto/Dockerfile b/packages/modules/mosquitto/Dockerfile index 0ead37795..d3d35c424 100644 --- a/packages/modules/mosquitto/Dockerfile +++ b/packages/modules/mosquitto/Dockerfile @@ -1 +1 @@ -FROM eclipse-mosquitto:2.0 +FROM eclipse-mosquitto:2 diff --git a/packages/modules/mosquitto/src/mosquitto-container.test.ts b/packages/modules/mosquitto/src/mosquitto-container.test.ts index 270c5df44..eca06e1d5 100644 --- a/packages/modules/mosquitto/src/mosquitto-container.test.ts +++ b/packages/modules/mosquitto/src/mosquitto-container.test.ts @@ -10,6 +10,8 @@ describe("MosquittoContainer", { timeout: 240_000 }, () => { // mosquittoConnect { await using container = await new MosquittoContainer(IMAGE).start(); + expect(container.getConnectionString()).toBe(`mqtt://${container.getHost()}:${container.getPort()}`); + const mqttClient = await mqtt.connectAsync(container.getConnectionString()); const firstMessagePromise = new Promise<{ topic: string; message: Buffer }>((resolve, reject) => { @@ -29,7 +31,14 @@ describe("MosquittoContainer", { timeout: 240_000 }, () => { it("should connect to Mosquitto via MQTT.js (with credentials)", async () => { // mosquittoConnectWithCredentials { - await using container = await new MosquittoContainer(IMAGE).withUsername("testuser").withPassword("testpass").start(); + await using container = await new MosquittoContainer(IMAGE) + .withUsername("testuser") + .withPassword("testpass") + .start(); + + expect(container.getConnectionString()).toBe( + `mqtt://testuser:testpass@${container.getHost()}:${container.getPort()}` + ); const mqttClient = await mqtt.connectAsync(container.getConnectionString()); diff --git a/packages/modules/mosquitto/src/mosquitto-container.ts b/packages/modules/mosquitto/src/mosquitto-container.ts index d0b15e6b9..1a9751f18 100644 --- a/packages/modules/mosquitto/src/mosquitto-container.ts +++ b/packages/modules/mosquitto/src/mosquitto-container.ts @@ -10,7 +10,9 @@ export class MosquittoContainer extends GenericContainer { constructor(image: string) { super(image); - this.withExposedPorts(MQTT_PORT).withWaitStrategy(Wait.forLogMessage(/mosquitto version .* starting/i)).withStartupTimeout(120_000); + this.withExposedPorts(MQTT_PORT) + .withWaitStrategy(Wait.forLogMessage(/running mosquitto as user/i)) + .withStartupTimeout(120_000); } public withUsername(username: string): this { @@ -31,7 +33,7 @@ export class MosquittoContainer extends GenericContainer { .withEntrypoint(["/bin/sh"]) .withCommand([ "-c", - `mosquitto_passwd -b -c "${PASSWORD_FILE_PATH}" "$MQTT_USER" "$MQTT_PASS" && exec mosquitto -c "${CONFIG_PATH}"`, + `mosquitto_passwd -b -c "${PASSWORD_FILE_PATH}" "$MQTT_USER" "$MQTT_PASS" && chown mosquitto:mosquitto "${PASSWORD_FILE_PATH}" && exec mosquitto -c "${CONFIG_PATH}"`, ]); } else { const config = `listener ${MQTT_PORT}\nallow_anonymous true\n`; From fcc2a86f3842600565a4e27628b3bd507b9f7ec9 Mon Sep 17 00:00:00 2001 From: Guillaume Chx <43764147+ChxGuillaume@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:39:52 +0200 Subject: [PATCH 03/12] Split Mosquitto tests: extracted credential-based connection test into a dedicated file and updated related documentation references. --- docs/modules/mosquitto.md | 2 +- .../src/mosquitto-container-auth.test.ts | 36 +++++++++++++++++++ .../mosquitto/src/mosquitto-container.test.ts | 28 --------------- 3 files changed, 37 insertions(+), 29 deletions(-) create mode 100644 packages/modules/mosquitto/src/mosquitto-container-auth.test.ts diff --git a/docs/modules/mosquitto.md b/docs/modules/mosquitto.md index c61e1b83c..94efae47e 100644 --- a/docs/modules/mosquitto.md +++ b/docs/modules/mosquitto.md @@ -25,5 +25,5 @@ Choose an image from the [container registry](https://hub.docker.com/r/eclipse-m ### Produce/consume a message (with credentials) -[](../../packages/modules/mosquitto/src/mosquitto-container.test.ts) inside_block:mosquittoConnectWithCredentials +[](../../packages/modules/mosquitto/src/mosquitto-container-auth.test.ts) inside_block:mosquittoConnectWithCredentials diff --git a/packages/modules/mosquitto/src/mosquitto-container-auth.test.ts b/packages/modules/mosquitto/src/mosquitto-container-auth.test.ts new file mode 100644 index 000000000..67d9b0830 --- /dev/null +++ b/packages/modules/mosquitto/src/mosquitto-container-auth.test.ts @@ -0,0 +1,36 @@ +import mqtt from "mqtt"; +import { expect } from "vitest"; +import { getImage } from "../../../testcontainers/src/utils/test-helper"; +import { MosquittoContainer } from "./mosquitto-container"; + +const IMAGE = getImage(__dirname); + +describe("MosquittoContainer", { timeout: 240_000 }, () => { + it("should connect to Mosquitto via MQTT.js (with credentials)", async () => { + // mosquittoConnectWithCredentials { + await using container = await new MosquittoContainer(IMAGE) + .withUsername("testuser") + .withPassword("testpass") + .start(); + + expect(container.getConnectionString()).toBe( + `mqtt://testuser:testpass@${container.getHost()}:${container.getPort()}` + ); + + const mqttClient = await mqtt.connectAsync(container.getConnectionString()); + + const firstMessagePromise = new Promise<{ topic: string; message: Buffer }>((resolve, reject) => { + mqttClient.once("message", (topic, message) => resolve({ topic, message })); + mqttClient.once("error", (err) => reject(err)); + }); + + await mqttClient.subscribeAsync("secure"); + await mqttClient.publishAsync("secure", "Secure Message"); + + const { message } = await firstMessagePromise; + expect(message.toString()).toEqual("Secure Message"); + + mqttClient.end(); + // } + }); +}); diff --git a/packages/modules/mosquitto/src/mosquitto-container.test.ts b/packages/modules/mosquitto/src/mosquitto-container.test.ts index eca06e1d5..dd9d355fb 100644 --- a/packages/modules/mosquitto/src/mosquitto-container.test.ts +++ b/packages/modules/mosquitto/src/mosquitto-container.test.ts @@ -28,32 +28,4 @@ describe("MosquittoContainer", { timeout: 240_000 }, () => { mqttClient.end(); // } }); - - it("should connect to Mosquitto via MQTT.js (with credentials)", async () => { - // mosquittoConnectWithCredentials { - await using container = await new MosquittoContainer(IMAGE) - .withUsername("testuser") - .withPassword("testpass") - .start(); - - expect(container.getConnectionString()).toBe( - `mqtt://testuser:testpass@${container.getHost()}:${container.getPort()}` - ); - - const mqttClient = await mqtt.connectAsync(container.getConnectionString()); - - const firstMessagePromise = new Promise<{ topic: string; message: Buffer }>((resolve, reject) => { - mqttClient.once("message", (topic, message) => resolve({ topic, message })); - mqttClient.once("error", (err) => reject(err)); - }); - - await mqttClient.subscribeAsync("secure"); - await mqttClient.publishAsync("secure", "Secure Message"); - - const { message } = await firstMessagePromise; - expect(message.toString()).toEqual("Secure Message"); - - mqttClient.end(); - // } - }); }); From 09e46770f764ebdce58b8735bf1c2e81a087a426 Mon Sep 17 00:00:00 2001 From: Guillaume Chx <43764147+ChxGuillaume@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:55:22 +0200 Subject: [PATCH 04/12] Pin Mosquitto base image to version 2.0.22 in Dockerfile --- packages/modules/mosquitto/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/mosquitto/Dockerfile b/packages/modules/mosquitto/Dockerfile index d3d35c424..0d1a8ff09 100644 --- a/packages/modules/mosquitto/Dockerfile +++ b/packages/modules/mosquitto/Dockerfile @@ -1 +1 @@ -FROM eclipse-mosquitto:2 +FROM eclipse-mosquitto:2.0.22 From 6574f9f44d560104970985251db7f44f05574c75 Mon Sep 17 00:00:00 2001 From: Guillaume Chx <43764147+ChxGuillaume@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:58:43 +0200 Subject: [PATCH 05/12] Reintegrate Mosquitto credential-based test into main test file and update documentation references accordingly. --- docs/modules/mosquitto.md | 4 +-- .../src/mosquitto-container-auth.test.ts | 36 ------------------- .../mosquitto/src/mosquitto-container.test.ts | 30 +++++++++++++++- 3 files changed, 31 insertions(+), 39 deletions(-) delete mode 100644 packages/modules/mosquitto/src/mosquitto-container-auth.test.ts diff --git a/docs/modules/mosquitto.md b/docs/modules/mosquitto.md index 94efae47e..212033108 100644 --- a/docs/modules/mosquitto.md +++ b/docs/modules/mosquitto.md @@ -19,11 +19,11 @@ Choose an image from the [container registry](https://hub.docker.com/r/eclipse-m ### Produce/consume a message (anonymous) -[](../../packages/modules/mosquitto/src/mosquitto-container.test.ts) inside_block:mosquittoConnect +[](../../packages/modules/mosquitto/src/mosquitto-container.test.ts) inside_block:mosquittoConnectAnonymous ### Produce/consume a message (with credentials) -[](../../packages/modules/mosquitto/src/mosquitto-container-auth.test.ts) inside_block:mosquittoConnectWithCredentials +[](../../packages/modules/mosquitto/src/mosquitto-container.test.ts) inside_block:mosquittoConnectWithCredentials diff --git a/packages/modules/mosquitto/src/mosquitto-container-auth.test.ts b/packages/modules/mosquitto/src/mosquitto-container-auth.test.ts deleted file mode 100644 index 67d9b0830..000000000 --- a/packages/modules/mosquitto/src/mosquitto-container-auth.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import mqtt from "mqtt"; -import { expect } from "vitest"; -import { getImage } from "../../../testcontainers/src/utils/test-helper"; -import { MosquittoContainer } from "./mosquitto-container"; - -const IMAGE = getImage(__dirname); - -describe("MosquittoContainer", { timeout: 240_000 }, () => { - it("should connect to Mosquitto via MQTT.js (with credentials)", async () => { - // mosquittoConnectWithCredentials { - await using container = await new MosquittoContainer(IMAGE) - .withUsername("testuser") - .withPassword("testpass") - .start(); - - expect(container.getConnectionString()).toBe( - `mqtt://testuser:testpass@${container.getHost()}:${container.getPort()}` - ); - - const mqttClient = await mqtt.connectAsync(container.getConnectionString()); - - const firstMessagePromise = new Promise<{ topic: string; message: Buffer }>((resolve, reject) => { - mqttClient.once("message", (topic, message) => resolve({ topic, message })); - mqttClient.once("error", (err) => reject(err)); - }); - - await mqttClient.subscribeAsync("secure"); - await mqttClient.publishAsync("secure", "Secure Message"); - - const { message } = await firstMessagePromise; - expect(message.toString()).toEqual("Secure Message"); - - mqttClient.end(); - // } - }); -}); diff --git a/packages/modules/mosquitto/src/mosquitto-container.test.ts b/packages/modules/mosquitto/src/mosquitto-container.test.ts index dd9d355fb..defd8bb58 100644 --- a/packages/modules/mosquitto/src/mosquitto-container.test.ts +++ b/packages/modules/mosquitto/src/mosquitto-container.test.ts @@ -7,7 +7,7 @@ const IMAGE = getImage(__dirname); describe("MosquittoContainer", { timeout: 240_000 }, () => { it("should connect to Mosquitto via MQTT.js (anonymous)", async () => { - // mosquittoConnect { + // mosquittoConnectAnonymous { await using container = await new MosquittoContainer(IMAGE).start(); expect(container.getConnectionString()).toBe(`mqtt://${container.getHost()}:${container.getPort()}`); @@ -28,4 +28,32 @@ describe("MosquittoContainer", { timeout: 240_000 }, () => { mqttClient.end(); // } }); + + it("should connect to Mosquitto via MQTT.js (with credentials)", async () => { + // mosquittoConnectWithCredentials { + await using container = await new MosquittoContainer(IMAGE) + .withUsername("testuser") + .withPassword("testpass") + .start(); + + expect(container.getConnectionString()).toBe( + `mqtt://testuser:testpass@${container.getHost()}:${container.getPort()}` + ); + + const mqttClient = await mqtt.connectAsync(container.getConnectionString()); + + const firstMessagePromise = new Promise<{ topic: string; message: Buffer }>((resolve, reject) => { + mqttClient.once("message", (topic, message) => resolve({ topic, message })); + mqttClient.once("error", (err) => reject(err)); + }); + + await mqttClient.subscribeAsync("secure"); + await mqttClient.publishAsync("secure", "Secure Message"); + + const { message } = await firstMessagePromise; + expect(message.toString()).toEqual("Secure Message"); + + mqttClient.end(); + // } + }); }); From e5b5965a4bff4346b4980852e20920b76dad93d0 Mon Sep 17 00:00:00 2001 From: Guillaume Chx <43764147+ChxGuillaume@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:01:10 +0200 Subject: [PATCH 06/12] Validate non-empty username and password in Mosquitto container configuration methods. --- packages/modules/mosquitto/src/mosquitto-container.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/modules/mosquitto/src/mosquitto-container.ts b/packages/modules/mosquitto/src/mosquitto-container.ts index 1a9751f18..aa496575a 100644 --- a/packages/modules/mosquitto/src/mosquitto-container.ts +++ b/packages/modules/mosquitto/src/mosquitto-container.ts @@ -16,11 +16,13 @@ export class MosquittoContainer extends GenericContainer { } public withUsername(username: string): this { + if (!username) throw new Error("Username should not be empty."); this.username = username; return this; } public withPassword(password: string): this { + if (!password) throw new Error("Password should not be empty."); this.password = password; return this; } From 1723a5af30b00a87ef84340c372f81def0782a2e Mon Sep 17 00:00:00 2001 From: Guillaume Chx <43764147+ChxGuillaume@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:03:06 +0200 Subject: [PATCH 07/12] Validate that username and password are set together in Mosquitto container startup logic. --- packages/modules/mosquitto/src/mosquitto-container.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/modules/mosquitto/src/mosquitto-container.ts b/packages/modules/mosquitto/src/mosquitto-container.ts index aa496575a..e9358a387 100644 --- a/packages/modules/mosquitto/src/mosquitto-container.ts +++ b/packages/modules/mosquitto/src/mosquitto-container.ts @@ -28,6 +28,10 @@ export class MosquittoContainer extends GenericContainer { } public override async start(): Promise { + if ((this.username === undefined) !== (this.password === undefined)) { + throw new Error("Both username and password must be set together."); + } + if (this.username !== undefined && this.password !== undefined) { const config = `listener ${MQTT_PORT}\npassword_file ${PASSWORD_FILE_PATH}\n`; this.withCopyContentToContainer([{ content: config, target: CONFIG_PATH }]) From ffd1018675cd5610244ada34ee0226565d09704b Mon Sep 17 00:00:00 2001 From: Guillaume Chx <43764147+ChxGuillaume@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:04:11 +0200 Subject: [PATCH 08/12] Reorder Mosquitto module entry in mkdocs navigation configuration. --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 2bb38d7f2..7ca59af31 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -73,9 +73,9 @@ nav: - Localstack: modules/localstack.md - MariaDB: modules/mariadb.md - MinIO: modules/minio.md - - Mosquitto: modules/mosquitto.md - Mockserver: modules/mockserver.md - MongoDB: modules/mongodb.md + - Mosquitto: modules/mosquitto.md - MSSQLServer: modules/mssqlserver.md - MySQL: modules/mysql.md - Nats: modules/nats.md From 2d6560723422d9bdcf4b9b3fa47f41897880492c Mon Sep 17 00:00:00 2001 From: Guillaume Chx <43764147+ChxGuillaume@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:07:54 +0200 Subject: [PATCH 09/12] Update Mosquitto container wait strategy to use listening ports instead of log message. --- packages/modules/mosquitto/src/mosquitto-container.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/modules/mosquitto/src/mosquitto-container.ts b/packages/modules/mosquitto/src/mosquitto-container.ts index e9358a387..af8bb2950 100644 --- a/packages/modules/mosquitto/src/mosquitto-container.ts +++ b/packages/modules/mosquitto/src/mosquitto-container.ts @@ -10,9 +10,7 @@ export class MosquittoContainer extends GenericContainer { constructor(image: string) { super(image); - this.withExposedPorts(MQTT_PORT) - .withWaitStrategy(Wait.forLogMessage(/running mosquitto as user/i)) - .withStartupTimeout(120_000); + this.withExposedPorts(MQTT_PORT).withWaitStrategy(Wait.forListeningPorts()).withStartupTimeout(120_000); } public withUsername(username: string): this { From 8f4c37c0fb838191188902290a8a6fd79d17d85f Mon Sep 17 00:00:00 2001 From: Guillaume Chx <43764147+ChxGuillaume@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:23:39 +0200 Subject: [PATCH 10/12] Add libc requirements for various bindings in package-lock.json --- package-lock.json | 50 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1fd927250..dcbdb2259 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4866,7 +4866,10 @@ ], "engines": { "node": "^20.19.0 || >=22.12.0" - } + }, + "libc": [ + "glibc" + ] }, "node_modules/@rolldown/binding-linux-arm64-musl": { "version": "1.0.3", @@ -4883,7 +4886,10 @@ ], "engines": { "node": "^20.19.0 || >=22.12.0" - } + }, + "libc": [ + "musl" + ] }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { "version": "1.0.3", @@ -4900,7 +4906,10 @@ ], "engines": { "node": "^20.19.0 || >=22.12.0" - } + }, + "libc": [ + "glibc" + ] }, "node_modules/@rolldown/binding-linux-s390x-gnu": { "version": "1.0.3", @@ -4917,7 +4926,10 @@ ], "engines": { "node": "^20.19.0 || >=22.12.0" - } + }, + "libc": [ + "glibc" + ] }, "node_modules/@rolldown/binding-linux-x64-gnu": { "version": "1.0.3", @@ -4934,7 +4946,10 @@ ], "engines": { "node": "^20.19.0 || >=22.12.0" - } + }, + "libc": [ + "glibc" + ] }, "node_modules/@rolldown/binding-linux-x64-musl": { "version": "1.0.3", @@ -4951,7 +4966,10 @@ ], "engines": { "node": "^20.19.0 || >=22.12.0" - } + }, + "libc": [ + "musl" + ] }, "node_modules/@rolldown/binding-openharmony-arm64": { "version": "1.0.3", @@ -10916,7 +10934,10 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" - } + }, + "libc": [ + "glibc" + ] }, "node_modules/lightningcss-linux-arm64-musl": { "version": "1.32.0", @@ -10937,7 +10958,10 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" - } + }, + "libc": [ + "musl" + ] }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.32.0", @@ -10958,7 +10982,10 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" - } + }, + "libc": [ + "glibc" + ] }, "node_modules/lightningcss-linux-x64-musl": { "version": "1.32.0", @@ -10979,7 +11006,10 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" - } + }, + "libc": [ + "musl" + ] }, "node_modules/lightningcss-win32-arm64-msvc": { "version": "1.32.0", From f25806a9ae21329af43268dcc3792bc2bb1471bb Mon Sep 17 00:00:00 2001 From: Guillaume Chx <43764147+ChxGuillaume@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:59:28 +0200 Subject: [PATCH 11/12] Simplify `StartedMosquittoContainer` port handling by removing redundant property. --- packages/modules/mosquitto/src/mosquitto-container.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/modules/mosquitto/src/mosquitto-container.ts b/packages/modules/mosquitto/src/mosquitto-container.ts index af8bb2950..93fe3130d 100644 --- a/packages/modules/mosquitto/src/mosquitto-container.ts +++ b/packages/modules/mosquitto/src/mosquitto-container.ts @@ -49,19 +49,16 @@ export class MosquittoContainer extends GenericContainer { } export class StartedMosquittoContainer extends AbstractStartedContainer { - private readonly port: number; - constructor( startedTestContainer: StartedTestContainer, private readonly username?: string, private readonly password?: string ) { super(startedTestContainer); - this.port = startedTestContainer.getMappedPort(MQTT_PORT); } - public getPort(): number { - return this.port; + public getPort(): number { + return this.getMappedPort(MQTT_PORT); } public getConnectionString(): string { From 676fffbe729d97a5fcb0df1d0fc0d72c71e2fda1 Mon Sep 17 00:00:00 2001 From: Guillaume Chx <43764147+ChxGuillaume@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:11:44 +0200 Subject: [PATCH 12/12] Fix formatting in `getPort` method and adjust libc definitions in `package-lock.json`. --- package-lock.json | 80 +++++++++---------- .../mosquitto/src/mosquitto-container.ts | 2 +- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index dcbdb2259..c58155371 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4859,6 +4859,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4866,10 +4869,7 @@ ], "engines": { "node": "^20.19.0 || >=22.12.0" - }, - "libc": [ - "glibc" - ] + } }, "node_modules/@rolldown/binding-linux-arm64-musl": { "version": "1.0.3", @@ -4879,6 +4879,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4886,10 +4889,7 @@ ], "engines": { "node": "^20.19.0 || >=22.12.0" - }, - "libc": [ - "musl" - ] + } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { "version": "1.0.3", @@ -4899,6 +4899,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4906,10 +4909,7 @@ ], "engines": { "node": "^20.19.0 || >=22.12.0" - }, - "libc": [ - "glibc" - ] + } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { "version": "1.0.3", @@ -4919,6 +4919,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4926,10 +4929,7 @@ ], "engines": { "node": "^20.19.0 || >=22.12.0" - }, - "libc": [ - "glibc" - ] + } }, "node_modules/@rolldown/binding-linux-x64-gnu": { "version": "1.0.3", @@ -4939,6 +4939,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4946,10 +4949,7 @@ ], "engines": { "node": "^20.19.0 || >=22.12.0" - }, - "libc": [ - "glibc" - ] + } }, "node_modules/@rolldown/binding-linux-x64-musl": { "version": "1.0.3", @@ -4959,6 +4959,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4966,10 +4969,7 @@ ], "engines": { "node": "^20.19.0 || >=22.12.0" - }, - "libc": [ - "musl" - ] + } }, "node_modules/@rolldown/binding-openharmony-arm64": { "version": "1.0.3", @@ -10923,6 +10923,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10934,10 +10937,7 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" - }, - "libc": [ - "glibc" - ] + } }, "node_modules/lightningcss-linux-arm64-musl": { "version": "1.32.0", @@ -10947,6 +10947,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10958,10 +10961,7 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" - }, - "libc": [ - "musl" - ] + } }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.32.0", @@ -10971,6 +10971,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10982,10 +10985,7 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" - }, - "libc": [ - "glibc" - ] + } }, "node_modules/lightningcss-linux-x64-musl": { "version": "1.32.0", @@ -10995,6 +10995,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11006,10 +11009,7 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" - }, - "libc": [ - "musl" - ] + } }, "node_modules/lightningcss-win32-arm64-msvc": { "version": "1.32.0", diff --git a/packages/modules/mosquitto/src/mosquitto-container.ts b/packages/modules/mosquitto/src/mosquitto-container.ts index 93fe3130d..f2d90fb0b 100644 --- a/packages/modules/mosquitto/src/mosquitto-container.ts +++ b/packages/modules/mosquitto/src/mosquitto-container.ts @@ -57,7 +57,7 @@ export class StartedMosquittoContainer extends AbstractStartedContainer { super(startedTestContainer); } - public getPort(): number { + public getPort(): number { return this.getMappedPort(MQTT_PORT); }