diff --git a/docs/modules/s3mock.md b/docs/modules/s3mock.md new file mode 100644 index 000000000..f97546169 --- /dev/null +++ b/docs/modules/s3mock.md @@ -0,0 +1,23 @@ +# S3Mock + +## Install + +```bash +npm install @testcontainers/s3mock --save-dev +``` + +## Examples + +These examples use the following libraries: + +- [@aws-sdk/client-s3](https://www.npmjs.com/package/@aws-sdk/client-s3) + + npm install @aws-sdk/client-s3 + +Choose an image from the [container registry](https://hub.docker.com/r/adobe/s3mock) and substitute `IMAGE`. + +### Create a S3 bucket + + +[](../../packages/modules/s3mock/src/s3mock-container.test.ts) inside_block:s3mockCreateS3Bucket + diff --git a/mkdocs.yml b/mkdocs.yml index 8482c85f4..45fadf802 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -85,6 +85,7 @@ nav: - RabbitMQ: modules/rabbitmq.md - Redis: modules/redis.md - Redpanda: modules/redpanda.md + - S3Mock: modules/s3mock.md - ScyllaDB: modules/scylladb.md - Selenium: modules/selenium.md - ToxiProxy: modules/toxiproxy.md diff --git a/package-lock.json b/package-lock.json index 9553e8060..14609fd16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1600,6 +1600,7 @@ "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -5704,6 +5705,7 @@ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -5751,6 +5753,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" @@ -5996,6 +5999,7 @@ "integrity": "sha512-MZVUE+l7LmMIYlIjubPosruJ9ltSLGFmJqsXApTqPLyHLjsJUSAbAJb/A3N34fEqean4ddiDkdWzNu4ZKPvRUg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -7355,6 +7359,10 @@ "resolved": "packages/modules/redpanda", "link": true }, + "node_modules/@testcontainers/s3mock": { + "resolved": "packages/modules/s3mock", + "link": true + }, "node_modules/@testcontainers/scylladb": { "resolved": "packages/modules/scylladb", "link": true @@ -7716,6 +7724,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" } @@ -8040,6 +8049,7 @@ "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", @@ -8491,6 +8501,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" }, @@ -9475,6 +9486,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -9963,6 +9975,7 @@ "integrity": "sha512-QDruPC+SawO2OECPK0vs+tGB+Rl+I3BO+R7AVKCC1O4SJUtEUgHpM/u+c/PrlaIgA4n9zoE/5ryZ/XKU8HPR4w==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "semver": "^7.7.1" }, @@ -11207,16 +11220,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, - "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", @@ -11539,6 +11542,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11599,6 +11603,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -12937,6 +12942,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", "dev": true, + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -14160,6 +14166,7 @@ "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -17367,6 +17374,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", @@ -17645,6 +17653,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -20311,6 +20320,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -20736,6 +20746,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20814,6 +20825,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" } @@ -21022,6 +21034,7 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -21137,6 +21150,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -21149,6 +21163,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -21701,6 +21716,7 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -22335,6 +22351,17 @@ "kafkajs": "^2.2.4" } }, + "packages/modules/s3mock": { + "name": "@testcontainers/s3mock", + "version": "11.7.2", + "license": "MIT", + "dependencies": { + "testcontainers": "^11.7.2" + }, + "devDependencies": { + "@aws-sdk/client-s3": "^3.913.0" + } + }, "packages/modules/scylladb": { "name": "@testcontainers/scylladb", "version": "11.7.2", diff --git a/packages/modules/s3mock/Dockerfile b/packages/modules/s3mock/Dockerfile new file mode 100644 index 000000000..009d78224 --- /dev/null +++ b/packages/modules/s3mock/Dockerfile @@ -0,0 +1 @@ +FROM adobe/s3mock:4.9.1 diff --git a/packages/modules/s3mock/package.json b/packages/modules/s3mock/package.json new file mode 100644 index 000000000..fd2698d58 --- /dev/null +++ b/packages/modules/s3mock/package.json @@ -0,0 +1,38 @@ +{ + "name": "@testcontainers/s3mock", + "version": "11.7.2", + "license": "MIT", + "keywords": [ + "s3mock", + "aws", + "testing", + "docker", + "testcontainers" + ], + "description": "S3Mock 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.7.2" + }, + "devDependencies": { + "@aws-sdk/client-s3": "^3.913.0" + } +} diff --git a/packages/modules/s3mock/src/index.ts b/packages/modules/s3mock/src/index.ts new file mode 100644 index 000000000..2ee00eb25 --- /dev/null +++ b/packages/modules/s3mock/src/index.ts @@ -0,0 +1 @@ +export { S3MockContainer, StartedS3MockContainer } from "./s3mock-container"; diff --git a/packages/modules/s3mock/src/s3mock-container.test.ts b/packages/modules/s3mock/src/s3mock-container.test.ts new file mode 100644 index 000000000..e19823c1a --- /dev/null +++ b/packages/modules/s3mock/src/s3mock-container.test.ts @@ -0,0 +1,29 @@ +import { CreateBucketCommand, HeadBucketCommand, S3Client } from "@aws-sdk/client-s3"; +import { getImage } from "../../../testcontainers/src/utils/test-helper"; +import { S3MockContainer } from "./s3mock-container"; + +const IMAGE = getImage(__dirname); + +describe("S3MockContainer", { timeout: 180_000 }, () => { + it("should create a S3 bucket", async () => { + // s3mockCreateS3Bucket { + await using container = await new S3MockContainer(IMAGE).start(); + + const client = new S3Client({ + endpoint: container.getHttpConnectionUrl(), + forcePathStyle: true, + region: "auto", + credentials: { + secretAccessKey: container.getSecretAccessKey(), + accessKeyId: container.getAccessKeyId(), + }, + }); + + const input = { Bucket: "testcontainers" }; + const command = new CreateBucketCommand(input); + + expect((await client.send(command)).$metadata.httpStatusCode).toEqual(200); + expect((await client.send(new HeadBucketCommand(input))).$metadata.httpStatusCode).toEqual(200); + // } + }); +}); diff --git a/packages/modules/s3mock/src/s3mock-container.ts b/packages/modules/s3mock/src/s3mock-container.ts new file mode 100644 index 000000000..846f7dca8 --- /dev/null +++ b/packages/modules/s3mock/src/s3mock-container.ts @@ -0,0 +1,83 @@ +import { AbstractStartedContainer, GenericContainer, type StartedTestContainer, Wait } from "testcontainers"; + +const S3_HTTP_PORT = 9090; +const S3_HTTPS_PORT = 3903; +// https://github.com/adobe/S3Mock/issues/1250#issuecomment-1653387576 +const ACCESS_KEY_ID = "any-access-key-id"; +const SECRET_ACCESS_KEY = "any-secret-access-key"; +const REGION = "auto"; + +export class S3MockContainer extends GenericContainer { + #accessKeyId: string = ACCESS_KEY_ID; + #secretAccessKey: string = SECRET_ACCESS_KEY; + + constructor(image: string) { + super(image); + this.withExposedPorts(S3_HTTP_PORT, S3_HTTPS_PORT); + this.withEnvironment({ + COM_ADOBE_TESTING_S3MOCK_STORE_REGION: REGION, + }); + + this.withWaitStrategy(Wait.forHttp("/favicon.ico", S3_HTTP_PORT)); + } + + withAccessKeyId(accessKeyId: string): this { + this.#accessKeyId = accessKeyId; + return this; + } + withSecretAccessKey(secretAccessKey: string): this { + this.#secretAccessKey = secretAccessKey; + return this; + } + + override async start(): Promise { + return new StartedS3MockContainer( + await super.start(), + this.#accessKeyId, + this.#secretAccessKey, + S3_HTTP_PORT, + S3_HTTPS_PORT + ); + } +} + +export class StartedS3MockContainer extends AbstractStartedContainer { + readonly #accessKeyId: string; + readonly #secretAccessKey: string; + readonly #s3HttpPort: number; + readonly #s3HttpsPort: number; + + constructor( + startedContainer: StartedTestContainer, + accessKeyId: string, + secretAccessKey: string, + s3HttpPort: number, + s3HttpsPort: number + ) { + super(startedContainer); + this.#accessKeyId = accessKeyId; + this.#secretAccessKey = secretAccessKey; + this.#s3HttpPort = s3HttpPort; + this.#s3HttpsPort = s3HttpsPort; + } + + getHttpPort() { + return this.startedTestContainer.getMappedPort(this.#s3HttpPort); + } + getHttpsPort() { + return this.startedTestContainer.getMappedPort(this.#s3HttpsPort); + } + getAccessKeyId(): string { + return this.#accessKeyId; + } + getSecretAccessKey(): string { + return this.#secretAccessKey; + } + + getHttpConnectionUrl() { + return `http://${this.getHost()}:${this.getHttpPort()}`; + } + getHttpsConnectionUrl() { + return `https://${this.getHost()}:${this.getHttpsPort()}`; + } +} diff --git a/packages/modules/s3mock/tsconfig.build.json b/packages/modules/s3mock/tsconfig.build.json new file mode 100644 index 000000000..ff7390b10 --- /dev/null +++ b/packages/modules/s3mock/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/s3mock/tsconfig.json b/packages/modules/s3mock/tsconfig.json new file mode 100644 index 000000000..4d74c3e41 --- /dev/null +++ b/packages/modules/s3mock/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