diff --git a/docs/modules/azureservicebus.md b/docs/modules/azureservicebus.md new file mode 100644 index 000000000..c01e72c8c --- /dev/null +++ b/docs/modules/azureservicebus.md @@ -0,0 +1,35 @@ +# Azure Service Bus + +## Install + +```bash +npm install @testcontainers/azureservicebus --save-dev +``` + +## Examples + +These examples use the following libraries: + +- [@azure/service-bus](https://www.npmjs.com/package/@azure/service-bus) + + npm install @azure/service-bus + +Choose an image from the [container registry](https://mcr.microsoft.com/en-us/artifact/mar/azure-messaging/servicebus-emulator) and substitute `IMAGE`. + +### Send/receive queue messages + + +[](../../packages/modules/azureservicebus/src/azureservicebus-container.test.ts) inside_block:serviceBusConnect + + +### Customize queues/topics + + +[](../../packages/modules/azureservicebus/src/azureservicebus-container.test.ts) inside_block:serviceBusValidEmulatorConfig + + +### Customize the MS SQL container + + +[](../../packages/modules/azureservicebus/src/azureservicebus-container.test.ts) inside_block:serviceBusCustomMssqlContainer + diff --git a/mkdocs.yml b/mkdocs.yml index 45fadf802..e446b5b16 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -54,6 +54,7 @@ nav: - Advanced: features/advanced.md - Modules: - ArangoDB: modules/arangodb.md + - Azure Service Bus: modules/azureservicebus.md - Azurite: modules/azurite.md - Cassandra: modules/cassandra.md - ChromaDB: modules/chromadb.md diff --git a/package-lock.json b/package-lock.json index c3e8eb95c..915201575 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1023,21 +1023,82 @@ "node": ">=12.0.0" } }, - "node_modules/@azure/core-auth": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", - "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", + "node_modules/@azure/core-amqp": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@azure/core-amqp/-/core-amqp-4.4.1.tgz", + "integrity": "sha512-eiVwGOMpHWPS6YsX0kjW4rfH+f0Pb5L2EKNDbuXldVkuFKSEfROdl81xHLsMAl5PP5wiiTjErcMcKsJqwyaRqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "rhea": "^3.0.0", + "rhea-promise": "^3.0.0", + "tslib": "^2.6.2", + "util": "^0.12.5" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-amqp/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-util": "^1.11.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, + "node_modules/@azure/core-amqp/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", @@ -1184,18 +1245,18 @@ } }, "node_modules/@azure/core-util": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.12.0.tgz", - "integrity": "sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", "dev": true, "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@typespec/ts-http-runtime": "^0.2.2", + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { @@ -1211,6 +1272,59 @@ "node": ">=18.0.0" } }, + "node_modules/@azure/core-util/node_modules/@typespec/ts-http-runtime": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz", + "integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/core-util/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/core-util/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@azure/core-xml": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.4.5.tgz", @@ -1421,17 +1535,70 @@ } }, "node_modules/@azure/logger": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.2.0.tgz", - "integrity": "sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", "dev": true, "license": "MIT", "dependencies": { - "@typespec/ts-http-runtime": "^0.2.2", + "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger/node_modules/@typespec/ts-http-runtime": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz", + "integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/logger/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/logger/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" } }, "node_modules/@azure/msal-browser": { @@ -1472,6 +1639,61 @@ "node": ">=16" } }, + "node_modules/@azure/service-bus": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@azure/service-bus/-/service-bus-7.9.5.tgz", + "integrity": "sha512-R5Af+4jtZZII2snLomaddMyElFtTCBRZp2qERPlP8PuISLU87eFYFM7xWzxjNd0yeiyQUBkamx/ZhOC8eWhCHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-amqp": "^4.1.1", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.0.0", + "@azure/core-paging": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.1.1", + "@azure/core-xml": "^1.0.0", + "@azure/logger": "^1.0.0", + "@types/is-buffer": "^2.0.0", + "buffer": "^6.0.0", + "is-buffer": "^2.0.3", + "jssha": "^3.1.0", + "long": "^5.2.0", + "process": "^0.11.10", + "rhea-promise": "^3.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/service-bus/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/@azure/storage-blob": { "version": "12.29.1", "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.29.1.tgz", @@ -1618,6 +1840,7 @@ "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -5335,6 +5558,7 @@ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -5382,6 +5606,7 @@ "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" @@ -5626,6 +5851,7 @@ "integrity": "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -6908,6 +7134,10 @@ "resolved": "packages/modules/azurecosmosdb", "link": true }, + "node_modules/@testcontainers/azureservicebus": { + "resolved": "packages/modules/azureservicebus", + "link": true + }, "node_modules/@testcontainers/azurite": { "resolved": "packages/modules/azurite", "link": true @@ -7332,6 +7562,16 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, + "node_modules/@types/is-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/is-buffer/-/is-buffer-2.0.2.tgz", + "integrity": "sha512-G6OXy83Va+xEo8XgqAJYOuvOMxeey9xM5XKkvwJNmN8rVdcB+r15HvHsG86hl86JvU0y1aa7Z2ERkNFYWw9ySg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -7396,6 +7636,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.23.tgz", "integrity": "sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -7720,6 +7961,7 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -8178,6 +8420,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9174,6 +9417,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -9665,6 +9909,7 @@ "integrity": "sha512-aY/9NqczQHrXOIjxVbjsOhdeW+kxQjzMP+VtK4OZh7pKeYYcdo3AbalHwl3FbQHTc0HSbh30T44yql8CwcGCvQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "semver": "^7.7.1" }, @@ -10910,17 +11155,6 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -11246,6 +11480,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11306,6 +11541,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -12930,6 +13166,7 @@ "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -13569,6 +13806,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -14154,6 +14415,7 @@ "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -14405,6 +14667,16 @@ "verror": "1.10.0" } }, + "node_modules/jssha": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz", + "integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -17370,6 +17642,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "dev": true, + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -17650,6 +17923,7 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -18428,6 +18702,28 @@ "dev": true, "license": "MIT" }, + "node_modules/rhea": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/rhea/-/rhea-3.0.4.tgz", + "integrity": "sha512-n3kw8syCdrsfJ72w3rohpoHHlmv/RZZEP9VY5BVjjo0sEGIt4YSKypBgaiA+OUSgJAzLjOECYecsclG5xbYtZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.3.3" + } + }, + "node_modules/rhea-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/rhea-promise/-/rhea-promise-3.0.3.tgz", + "integrity": "sha512-a875P5YcMkePSTEWMsnmCQS7Y4v/XvIw7ZoMtJxqtQRZsqSA6PsZxuz4vktyRykPuUgdNsA6F84dS3iEXZoYnQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.0.0", + "rhea": "^3.0.0", + "tslib": "^2.6.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -20334,6 +20630,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -20764,6 +21061,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20842,6 +21140,7 @@ "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", "license": "MIT", + "peer": true, "engines": { "node": ">=20.18.1" } @@ -21050,6 +21349,7 @@ "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -21166,6 +21466,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -21179,6 +21480,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -21731,6 +22033,7 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -21981,6 +22284,17 @@ "@azure/cosmos": "^4.9.0" } }, + "packages/modules/azureservicebus": { + "name": "@testcontainers/azureservicebus", + "version": "11.10.0", + "license": "MIT", + "dependencies": { + "testcontainers": "^11.10.0" + }, + "devDependencies": { + "@azure/service-bus": "^7.9.5" + } + }, "packages/modules/azurite": { "name": "@testcontainers/azurite", "version": "11.10.0", diff --git a/packages/modules/azureservicebus/Dockerfile b/packages/modules/azureservicebus/Dockerfile new file mode 100644 index 000000000..7759fe6be --- /dev/null +++ b/packages/modules/azureservicebus/Dockerfile @@ -0,0 +1,2 @@ +FROM mcr.microsoft.com/azure-messaging/servicebus-emulator:1.1.2 +FROM mcr.microsoft.com/mssql/server:2022-latest diff --git a/packages/modules/azureservicebus/package.json b/packages/modules/azureservicebus/package.json new file mode 100644 index 000000000..42e57a22a --- /dev/null +++ b/packages/modules/azureservicebus/package.json @@ -0,0 +1,38 @@ +{ + "name": "@testcontainers/azureservicebus", + "version": "11.10.0", + "license": "MIT", + "keywords": [ + "azure", + "service-bus", + "testing", + "docker", + "testcontainers" + ], + "description": "Azure Service Bus 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" + }, + "dependencies": { + "testcontainers": "^11.10.0" + }, + "devDependencies": { + "@azure/service-bus": "^7.9.5" + } +} diff --git a/packages/modules/azureservicebus/src/azureservicebus-container.test.ts b/packages/modules/azureservicebus/src/azureservicebus-container.test.ts new file mode 100644 index 000000000..6ab830e8b --- /dev/null +++ b/packages/modules/azureservicebus/src/azureservicebus-container.test.ts @@ -0,0 +1,128 @@ +import { ServiceBusClient } from "@azure/service-bus"; +import { GenericContainer, Wait } from "testcontainers"; +import { getImage } from "../../../testcontainers/src/utils/test-helper"; +import { AzureServiceBusContainer } from "./azureservicebus-container"; + +const IMAGE = getImage(__dirname, 0); +const MSSQL_IMAGE = getImage(__dirname, 1); + +describe("AzureServiceBusContainer", { timeout: 180_000 }, () => { + it("should connect and queue a message", async () => { + // serviceBusConnect { + await using container = await new AzureServiceBusContainer(IMAGE).acceptLicense().start(); + + const client = new ServiceBusClient(container.getConnectionString()); + const sender = client.createSender("queue.1"); + const receiver = client.createReceiver("queue.1"); + + await sender.sendMessages({ body: "Hello, World!" }); + const res = await receiver.receiveMessages(1, { maxWaitTimeInMs: 5_000 }); + + expect(res).toHaveLength(1); + expect(res[0].body).toBe("Hello, World!"); + + await receiver.close(); + await sender.close(); + await client.close(); + // } + }); + + it("should connect and queue a message using custom config", async () => { + // serviceBusValidEmulatorConfig { + const queueName = "custom-queue"; + const config = JSON.stringify({ + UserConfig: { + Namespaces: [ + { + Name: "sbemulatorns", + Queues: [ + { + Name: queueName, + Properties: { + DeadLetteringOnMessageExpiration: false, + DefaultMessageTimeToLive: "PT1H", + DuplicateDetectionHistoryTimeWindow: "PT20S", + ForwardDeadLetteredMessagesTo: "", + ForwardTo: "", + LockDuration: "PT1M", + MaxDeliveryCount: 3, + RequiresDuplicateDetection: false, + RequiresSession: false, + }, + }, + ], + Topics: [], + }, + ], + Logging: { + Type: "File", + }, + }, + }); + + await using container = await new AzureServiceBusContainer(IMAGE).acceptLicense().withConfig(config).start(); + + const client = new ServiceBusClient(container.getConnectionString()); + const sender = client.createSender(queueName); + const receiver = client.createReceiver(queueName); + // } + + await sender.sendMessages({ body: "Hello, World!" }); + const res = await receiver.receiveMessages(1, { maxWaitTimeInMs: 5_000 }); + + expect(res).toHaveLength(1); + expect(res[0].body).toBe("Hello, World!"); + + await receiver.close(); + await sender.close(); + await client.close(); + }); + + it("should connect with a custom MSSQL container", async () => { + // serviceBusCustomMssqlContainer { + // You are responsible for configuring the SA password on + // BOTH containers when using a custom MSSQL container. + const customPassword = "MyC0mplexP@ssw0rd!"; + + // @testcontainers/mssqlserver can be used as well + const mssqlContainer = new GenericContainer(MSSQL_IMAGE) + .withEnvironment({ + ACCEPT_EULA: "Y", + MSSQL_SA_PASSWORD: customPassword, + }) + .withNetworkAliases("your-network-alias") + .withWaitStrategy(Wait.forLogMessage(/.*Recovery is complete.*/, 1).withStartupTimeout(120_000)); + + await using container = await new AzureServiceBusContainer(IMAGE) + .acceptLicense() + .withMssqlContainer(mssqlContainer) + .withMssqlPassword(customPassword) + .start(); + + const client = new ServiceBusClient(container.getConnectionString()); + // } + + const sender = client.createSender("queue.1"); + const receiver = client.createReceiver("queue.1"); + + await sender.sendMessages({ body: "Hello, World!" }); + const res = await receiver.receiveMessages(1, { maxWaitTimeInMs: 5_000 }); + + expect(res).toHaveLength(1); + expect(res[0].body).toBe("Hello, World!"); + + await receiver.close(); + await sender.close(); + await client.close(); + }); + + it("should connect containers to the same network", async () => { + await using serviceBusContainer = await new AzureServiceBusContainer(IMAGE).acceptLicense().start(); + + const servicebusNetworks = serviceBusContainer.getNetworkNames(); + const mssqlNetworks = serviceBusContainer.getMssqlContainer().getNetworkNames(); + + expect(servicebusNetworks).toHaveLength(1); + expect(mssqlNetworks).toEqual(expect.arrayContaining(servicebusNetworks)); + }); +}); diff --git a/packages/modules/azureservicebus/src/azureservicebus-container.ts b/packages/modules/azureservicebus/src/azureservicebus-container.ts new file mode 100644 index 000000000..4c4fc81a8 --- /dev/null +++ b/packages/modules/azureservicebus/src/azureservicebus-container.ts @@ -0,0 +1,151 @@ +import { + AbstractStartedContainer, + Content, + GenericContainer, + Network, + StartedNetwork, + StartedTestContainer, + Wait, +} from "testcontainers"; + +const DEFAULT_PORT = 5672; +const DEFAULT_HTTP_PORT = 5300; +const DEFAULT_SHARED_ACCESS_KEY_NAME = "RootManageSharedAccessKey"; +const DEFAULT_SHARED_ACCESS_KEY = "SAS_KEY_VALUE"; +const DEFAULT_MSSQL_ALIAS = "mssql"; +const DEFAULT_MSSQL_IMAGE = "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04"; +const DEFAULT_MSSQL_PASSWORD = "P@ssword1!d3adb33f#"; +const CONTAINER_CONFIG_FILE = "/ServiceBus_Emulator/ConfigFiles/Config.json"; + +export class AzureServiceBusContainer extends GenericContainer { + private acceptEula: string = "N"; + private mssqlContainer: GenericContainer | undefined; + private mssqlImage: string = DEFAULT_MSSQL_IMAGE; + private mssqlPassword: string = DEFAULT_MSSQL_PASSWORD; + private config: Content | undefined; + + constructor(image: string) { + super(image); + + this.withExposedPorts(DEFAULT_PORT, DEFAULT_HTTP_PORT) + // The emulator image is minimal and does not include a shell, so the internal port checks will fail. + // The HTTP health check should be sufficient to determine when the emulator is ready. + .withWaitStrategy(Wait.forHttp("/health", DEFAULT_HTTP_PORT).forStatusCode(200)) + .withEnvironment({ + SQL_WAIT_INTERVAL: "0", // We start the MSSQL container before the emulator + }); + } + + public acceptLicense(): this { + this.acceptEula = "Y"; + return this; + } + + public withMssqlContainer(container: GenericContainer): this { + if (this.mssqlImage !== DEFAULT_MSSQL_IMAGE) { + throw new Error("Cannot use both withMssqlImage() and withMssqlContainer()"); + } + + this.mssqlContainer = container; + return this; + } + + public withMssqlImage(image: string): this { + if (this.mssqlContainer) { + throw new Error("Cannot use both withMssqlImage() and withMssqlContainer()"); + } + + this.mssqlImage = image; + return this; + } + + public withMssqlPassword(password: string): this { + if (this.mssqlPassword !== DEFAULT_MSSQL_PASSWORD) { + throw new Error("Cannot use both withMssqlPassword() and withMssqlContainer()"); + } + + this.mssqlPassword = password; + return this; + } + + public withConfig(config: Content): this { + this.config = config; + return this; + } + + public override async start(): Promise { + const network = await new Network().start(); + this.withNetwork(network); + + if (!this.mssqlContainer) { + // This should match the behaviour of @testcontainers/mssqlserver, we + // create the container manually here to avoid module dependencies. + this.mssqlContainer = new GenericContainer(this.mssqlImage) + .withEnvironment({ + ACCEPT_EULA: "Y", + MSSQL_SA_PASSWORD: this.mssqlPassword, + }) + .withExposedPorts(1433) + .withWaitStrategy(Wait.forLogMessage(/.*Recovery is complete.*/, 1).withStartupTimeout(120_000)); + } + + const mssql = await this.mssqlContainer.withNetworkAliases(DEFAULT_MSSQL_ALIAS).withNetwork(network).start(); + + if (this.config) { + this.withCopyContentToContainer([ + { + content: this.config, + target: CONTAINER_CONFIG_FILE, + mode: 0o644, + }, + ]); + } + + this.withEnvironment({ + ACCEPT_EULA: this.acceptEula, + SQL_SERVER: mssql.getHostname(), + MSSQL_SA_PASSWORD: this.mssqlPassword, + }); + + try { + const serviceBus = await super.start(); + + return new StartedAzureServiceBusContainer(serviceBus, mssql, network); + } catch (err) { + await mssql.stop(); + await network.stop(); + + throw err; + } + } +} + +export class StartedAzureServiceBusContainer extends AbstractStartedContainer { + constructor( + startedTestContainer: StartedTestContainer, + private readonly mssql: StartedTestContainer, + private readonly network: StartedNetwork + ) { + super(startedTestContainer); + } + + public getMssqlContainer(): StartedTestContainer { + return this.mssql; + } + + public getPort(): number { + return this.getMappedPort(DEFAULT_PORT); + } + + public getConnectionString(): string { + return `Endpoint=sb://${this.getHost()}:${this.getPort()};SharedAccessKeyName=${DEFAULT_SHARED_ACCESS_KEY_NAME};SharedAccessKey=${DEFAULT_SHARED_ACCESS_KEY};UseDevelopmentEmulator=true;`; + } + + protected override async containerStopped(): Promise { + try { + await this.mssql.stop(); + } finally { + await this.network.stop(); + } + } +} diff --git a/packages/modules/azureservicebus/src/index.ts b/packages/modules/azureservicebus/src/index.ts new file mode 100644 index 000000000..e7fdad273 --- /dev/null +++ b/packages/modules/azureservicebus/src/index.ts @@ -0,0 +1 @@ +export { AzureServiceBusContainer, StartedAzureServiceBusContainer } from "./azureservicebus-container"; diff --git a/packages/modules/azureservicebus/tsconfig.build.json b/packages/modules/azureservicebus/tsconfig.build.json new file mode 100644 index 000000000..ff7390b10 --- /dev/null +++ b/packages/modules/azureservicebus/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "build", + "src/**/*.test.ts" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file diff --git a/packages/modules/azureservicebus/tsconfig.json b/packages/modules/azureservicebus/tsconfig.json new file mode 100644 index 000000000..4d74c3e41 --- /dev/null +++ b/packages/modules/azureservicebus/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "paths": { + "testcontainers": [ + "../../testcontainers/src" + ] + } + }, + "exclude": [ + "build" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file