From d1fcfd8b671e87fe4bf71933c602083b86d24492 Mon Sep 17 00:00:00 2001 From: Christian Brevik Date: Wed, 6 Nov 2024 14:55:43 +0100 Subject: [PATCH] Add Azurite module --- docs/modules/azurite.md | 35 +++ mkdocs.yml | 1 + package-lock.json | 182 +++++++++++++++- packages/modules/azurite/jest.config.ts | 11 + packages/modules/azurite/package.json | 39 ++++ .../azurite/src/azurite-container.test.ts | 168 ++++++++++++++ .../modules/azurite/src/azurite-container.ts | 206 ++++++++++++++++++ packages/modules/azurite/src/index.ts | 1 + packages/modules/azurite/tsconfig.build.json | 13 ++ packages/modules/azurite/tsconfig.json | 21 ++ 10 files changed, 672 insertions(+), 5 deletions(-) create mode 100644 docs/modules/azurite.md create mode 100644 packages/modules/azurite/jest.config.ts create mode 100644 packages/modules/azurite/package.json create mode 100644 packages/modules/azurite/src/azurite-container.test.ts create mode 100755 packages/modules/azurite/src/azurite-container.ts create mode 100644 packages/modules/azurite/src/index.ts create mode 100644 packages/modules/azurite/tsconfig.build.json create mode 100644 packages/modules/azurite/tsconfig.json diff --git a/docs/modules/azurite.md b/docs/modules/azurite.md new file mode 100644 index 000000000..e811456eb --- /dev/null +++ b/docs/modules/azurite.md @@ -0,0 +1,35 @@ +# Azurite Module + +[Azurite](https://github.com/Azure/Azurite) is an open source Azure Storage API compatible server (emulator). Based on Node.js, Azurite provides cross platform experiences for developers wanting to try Azure Storage easily in a local environment. Azurite simulates most of the commands supported by Azure Storage with minimal dependencies. + +## Install + +```bash +npm install @testcontainers/azurite --save-dev +``` + +## Examples + + +[Upload and download a blob:](../../packages/modules/azurite/src/azurite-container.test.ts) inside_block:uploadAndDownloadBlob + + + +[Send and receive queue messages:](../../packages/modules/azurite/src/azurite-container.test.ts) inside_block:sendAndReceiveQueue + + + +[Create and insert on table:](../../packages/modules/azurite/src/azurite-container.test.ts) inside_block:createAndInsertOnTable + + + +[Use custom credentials:](../../packages/modules/azurite/src/azurite-container.test.ts) inside_block:customCredentials + + + +[Use custom ports:](../../packages/modules/azurite/src/azurite-container.test.ts) inside_block:customPorts + + + +[Enable in-memory persistence:](../../packages/modules/azurite/src/azurite-container.test.ts) inside_block:inMemoryPersistence + diff --git a/mkdocs.yml b/mkdocs.yml index edfb6cec6..788c8eb94 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,6 +43,7 @@ nav: - Advanced: features/advanced.md - Modules: - ArangoDB: modules/arangodb.md + - Azurite: modules/azurite.md - Cassandra: modules/cassandra.md - ChromaDB: modules/chromadb.md - Couchbase: modules/couchbase.md diff --git a/package-lock.json b/package-lock.json index 8a4b5d287..6c06b298e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1368,15 +1368,15 @@ "dev": true }, "node_modules/@azure/core-tracing": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", - "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", + "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", "dev": true, "dependencies": { - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/core-tracing/node_modules/tslib": { @@ -1404,6 +1404,51 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "node_modules/@azure/core-xml": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.4.4.tgz", + "integrity": "sha512-J4FYAqakGXcbfeZjwjMzjNcpcH4E+JtEBv+xcV1yL0Ydn/6wbQfeFKTCHh9wttAi0lmajHw7yBbHPRG+YHckZQ==", + "dev": true, + "dependencies": { + "fast-xml-parser": "^4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-xml/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/@azure/data-tables": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@azure/data-tables/-/data-tables-13.2.2.tgz", + "integrity": "sha512-Dq2Aq0mMMF0BPzYQKdBY/OtO7VemP/foh6z+mJpUO1hRL+65C1rGQUJf20LJHotSyU8wHb4HJzOs+Z50GXSy1w==", + "dev": true, + "dependencies": { + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.0.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-xml": "^1.0.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/data-tables/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, "node_modules/@azure/identity": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-3.4.2.tgz", @@ -1516,6 +1561,116 @@ "node": ">=16" } }, + "node_modules/@azure/storage-blob": { + "version": "12.25.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.25.0.tgz", + "integrity": "sha512-oodouhA3nCCIh843tMMbxty3WqfNT+Vgzj3Xo5jqR9UPnzq3d7mzLjlHAYz7lW+b4km3SIgz+NAgztvhm7Z6kQ==", + "dev": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.4.0", + "@azure/core-client": "^1.6.2", + "@azure/core-http-compat": "^2.0.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.10.1", + "@azure/core-tracing": "^1.1.2", + "@azure/core-util": "^1.6.1", + "@azure/core-xml": "^1.4.3", + "@azure/logger": "^1.0.0", + "events": "^3.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/storage-blob/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, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/storage-blob/node_modules/@azure/core-http-compat": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz", + "integrity": "sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==", + "dev": true, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/storage-blob/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/@azure/storage-queue": { + "version": "12.24.0", + "resolved": "https://registry.npmjs.org/@azure/storage-queue/-/storage-queue-12.24.0.tgz", + "integrity": "sha512-U84iLcXC1YdfYeZR+aNX6BYrJLIFheQR3jedENkUg5jBH7868i/XK0KTgiNQ+J4bpgNEBdSQGaxDE+kKQv5vnA==", + "dev": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.4.0", + "@azure/core-client": "^1.6.2", + "@azure/core-http-compat": "^2.0.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.10.1", + "@azure/core-tracing": "^1.1.2", + "@azure/core-util": "^1.6.1", + "@azure/core-xml": "^1.4.3", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/storage-queue/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, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/storage-queue/node_modules/@azure/core-http-compat": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz", + "integrity": "sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==", + "dev": true, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/storage-queue/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", @@ -5257,6 +5412,10 @@ "resolved": "packages/modules/arangodb", "link": true }, + "node_modules/@testcontainers/azurite": { + "resolved": "packages/modules/azurite", + "link": true + }, "node_modules/@testcontainers/cassandra": { "resolved": "packages/modules/cassandra", "link": true @@ -19716,6 +19875,19 @@ "arangojs": "^8.8.1" } }, + "packages/modules/azurite": { + "name": "@testcontainers/azurite", + "version": "10.13.2", + "license": "MIT", + "dependencies": { + "testcontainers": "^10.13.2" + }, + "devDependencies": { + "@azure/data-tables": "^13.2.2", + "@azure/storage-blob": "^12.25.0", + "@azure/storage-queue": "^12.24.0" + } + }, "packages/modules/cassandra": { "name": "@testcontainers/cassandra", "version": "10.13.2", diff --git a/packages/modules/azurite/jest.config.ts b/packages/modules/azurite/jest.config.ts new file mode 100644 index 000000000..1f677baaf --- /dev/null +++ b/packages/modules/azurite/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/azurite/package.json b/packages/modules/azurite/package.json new file mode 100644 index 000000000..216b22942 --- /dev/null +++ b/packages/modules/azurite/package.json @@ -0,0 +1,39 @@ +{ + "name": "@testcontainers/azurite", + "version": "10.13.2", + "license": "MIT", + "keywords": [ + "azurite", + "testing", + "docker", + "testcontainers" + ], + "description": "Azurite 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.13.2" + }, + "devDependencies": { + "@azure/data-tables": "^13.2.2", + "@azure/storage-blob": "^12.25.0", + "@azure/storage-queue": "^12.24.0" + } +} diff --git a/packages/modules/azurite/src/azurite-container.test.ts b/packages/modules/azurite/src/azurite-container.test.ts new file mode 100644 index 000000000..a25bae083 --- /dev/null +++ b/packages/modules/azurite/src/azurite-container.test.ts @@ -0,0 +1,168 @@ +import { BlobServiceClient, StorageSharedKeyCredential } from "@azure/storage-blob"; +import { QueueServiceClient } from "@azure/storage-queue"; +import { TableClient, TableEntity } from "@azure/data-tables"; +import { AzuriteContainer } from "./azurite-container"; + +describe("Azurite", () => { + jest.setTimeout(240_000); + + // uploadAndDownloadBlob { + it("should upload and download blob with default credentials", async () => { + const container = await new AzuriteContainer().start(); + + const connectionString = container.getConnectionString(); + expect(connectionString).toBeTruthy(); + + const serviceClient = BlobServiceClient.fromConnectionString(connectionString); + const containerClient = serviceClient.getContainerClient("test"); + await containerClient.createIfNotExists(); + const blobName = "hello.txt"; + const content = "Hello world!"; + await containerClient.uploadBlockBlob(blobName, content, Buffer.byteLength(content)); + + const blobClient = containerClient.getBlockBlobClient(blobName); + const downloadResponse = await blobClient.download(0, undefined); + + const readable = downloadResponse.readableStreamBody as NodeJS.ReadableStream; + expect(readable).toBeTruthy(); + + readable.setEncoding("utf8"); + let data = ""; + for await (const chunk of readable) { + data += chunk; + } + + expect(data).toBe(content); + + await container.stop(); + }); + // } + + // sendAndReceiveQueue { + it("should add to queue with default credentials", async () => { + const container = await new AzuriteContainer().start(); + + const connectionString = container.getConnectionString(); + expect(connectionString).toBeTruthy(); + + const serviceClient = QueueServiceClient.fromConnectionString(connectionString); + const queueName = "test-queue"; + await serviceClient.createQueue(queueName); + + const queueClient = serviceClient.getQueueClient(queueName); + + const message = "Hello world!"; + await queueClient.sendMessage(message); + + const messages = await queueClient.receiveMessages(); + expect(messages.receivedMessageItems).toHaveLength(1); + expect(messages.receivedMessageItems[0].messageText).toBe(message); + + await container.stop(); + }); + // } + + // createAndInsertOnTable { + it("should add to table with default credentials", async () => { + const container = await new AzuriteContainer().start(); + + const connectionString = container.getConnectionString(); + expect(connectionString).toBeTruthy(); + + const tableName = "person"; + const tableClient = TableClient.fromConnectionString(connectionString, tableName, { + allowInsecureConnection: true, + }); + await tableClient.createTable(); + + const entity: TableEntity<{ name: string }> = { + partitionKey: "p1", + rowKey: "r1", + name: "John Doe", + }; + await tableClient.createEntity(entity); + + const e1 = await tableClient.listEntities().next(); + expect(e1.value).toBeTruthy(); + expect(e1.value.name).toBe(entity.name); + + await container.stop(); + }); + // } + + // customCredentials { + it("should be able to specify accountName and accountKey", async () => { + const accountName = "test-account"; + // Account key must be base64 encoded + const accountKey = Buffer.from("test-key").toString("base64"); + + const container = await new AzuriteContainer().withAccountName(accountName).withAccountKey(accountKey).start(); + + const credentials = new StorageSharedKeyCredential(accountName, accountKey); + const serviceClient = new BlobServiceClient(container.getBlobEndpoint(), credentials); + + const blobContainerName = "test"; + const containerClient = serviceClient.getContainerClient(blobContainerName); + await containerClient.createIfNotExists(); + + const blobContainer = await serviceClient.listContainers().next(); + expect(blobContainer.value).toBeTruthy(); + expect(blobContainer.value.name).toBe(blobContainerName); + + await container.stop(); + }); + // } + + // customPorts { + it("should be able to specify custom ports", async () => { + const blobPort = 13000; + const queuePort = 14000; + const tablePort = 15000; + const container = await new AzuriteContainer() + .withBlobPort(blobPort) + .withQueuePort(queuePort) + .withTablePort(tablePort) + .start(); + + expect(container.getBlobPort()).toBe(blobPort); + expect(container.getQueuePort()).toBe(queuePort); + expect(container.getTablePort()).toBe(tablePort); + + const connectionString = container.getConnectionString(); + expect(connectionString).toContain("13000"); + expect(connectionString).toContain("14000"); + expect(connectionString).toContain("15000"); + + const serviceClient = BlobServiceClient.fromConnectionString(connectionString); + const containerClient = serviceClient.getContainerClient("test"); + await containerClient.createIfNotExists(); + + await container.stop(); + }); + // } + + // inMemoryPersistence { + it("should be able to use in-memory persistence", async () => { + const container = await new AzuriteContainer().withInMemoryPersistence().start(); + + const connectionString = container.getConnectionString(); + expect(connectionString).toBeTruthy(); + + const serviceClient = BlobServiceClient.fromConnectionString(connectionString); + const containerClient = serviceClient.getContainerClient("test"); + await containerClient.createIfNotExists(); + const blobName = "hello.txt"; + const content = "Hello world!"; + await containerClient.uploadBlockBlob(blobName, content, Buffer.byteLength(content)); + + const blobClient = containerClient.getBlockBlobClient(blobName); + const blobExists = await blobClient.exists(); + expect(blobExists).toBeTruthy(); + + await container.restart(); + + const blobExistsAfterRestart = await blobClient.exists(); + expect(blobExistsAfterRestart).toBeFalsy(); + }); + // } +}); diff --git a/packages/modules/azurite/src/azurite-container.ts b/packages/modules/azurite/src/azurite-container.ts new file mode 100755 index 000000000..1af1b289c --- /dev/null +++ b/packages/modules/azurite/src/azurite-container.ts @@ -0,0 +1,206 @@ +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; + +const AZURITE_IMAGE = "mcr.microsoft.com/azure-storage/azurite:3.33.0"; +const BLOB_PORT = 10000; +const QUEUE_PORT = 10001; +const TABLE_PORT = 10002; +const DEFAULT_ACCOUNT_NAME = "devstoreaccount1"; +const DEFAULT_ACCOUNT_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; + +export class AzuriteContainer extends GenericContainer { + constructor(image = AZURITE_IMAGE) { + super(image); + this.withEntrypoint(["azurite"]) + .withWaitStrategy( + Wait.forAll([ + Wait.forLogMessage(/.*Blob service is successfully listening.*/), + Wait.forLogMessage(/.*Queue service is successfully listening.*/), + Wait.forLogMessage(/.*Table service is successfully listening.*/), + ]) + ) + .withStartupTimeout(120_000); + } + + private blobPort: number = BLOB_PORT; + private queuePort: number = QUEUE_PORT; + private tablePort: number = TABLE_PORT; + private accountName: string = DEFAULT_ACCOUNT_NAME; + private accountKey: string = DEFAULT_ACCOUNT_KEY; + + private skipApiVersionCheck = false; + private inMemoryPersistence = false; + private extentMemoryLimitInMegaBytes?: number = undefined; + + /** + * Sets a custom storage account name (default account will be disabled). + * @param accountName Storage account names must be between 3 and 24 characters in length and may contain numbers and lowercase letters only. + */ + public withAccountName(accountName: string): this { + this.accountName = accountName; + return this; + } + + /** + * Sets a custom storage account key (default account will be disabled). Note: MUST be a base64-encoded string. + * @param accountKey The account keys must be base64 encoded string. + */ + public withAccountKey(accountKey: string): this { + this.accountKey = accountKey; + return this; + } + + /** + * Sets the port to expose the Blob service on. + * @param port The port to expose the Blob service on. Default is 10000. + */ + public withBlobPort(port: number): this { + this.blobPort = port; + return this; + } + + /** + * Sets the port to expose the Queue service on. + * @param port The port to expose the Queue service on. Default is 10001. + */ + public withQueuePort(port: number): this { + this.queuePort = port; + return this; + } + + /** + * Sets the port to expose the Table service on. + * @param port The port to expose the Table service on. Default is 10002. + */ + public withTablePort(port: number): this { + this.tablePort = port; + return this; + } + + /** + * Disable persisting any data to disk and only store data in-memory. If the Azurite process is terminated, all data is lost. + */ + public withInMemoryPersistence(): this { + this.inMemoryPersistence = true; + return this; + } + + /** + * By default, the in-memory extent store (for blob and queue content) is limited to 50% of the total memory on the host machine. This is evaluated to using os.totalmem(). This limit can be overridden using the extentMemoryLimit option. This can only be used if in-memory persistence is enabled first. + * @param megabyte The extent memory limit in megabytes. + */ + public withExtentMemoryLimitInMegaBytes(megabyte: number): this { + if (!this.inMemoryPersistence) { + throw new Error("Extent memory limit can only be set when using in-memory persistence"); + } + this.extentMemoryLimitInMegaBytes = megabyte; + return this; + } + + /** + * By default Azurite will check the request API version is valid API version. This can be disabled by with this option. + */ + public withSkipApiVersionCheck(): this { + this.skipApiVersionCheck = true; + return this; + } + + public override async start(): Promise { + const command = ["--blobHost", "0.0.0.0", "--queueHost", "0.0.0.0", "--tableHost", "0.0.0.0"]; + + if (this.inMemoryPersistence) { + command.push("--inMemoryPersistence"); + + if (this.extentMemoryLimitInMegaBytes) { + command.push("--extentMemoryLimit", this.extentMemoryLimitInMegaBytes.toString()); + } + } + + if (this.skipApiVersionCheck) { + command.push("--skipApiVersionCheck"); + } + + this.withCommand(command).withExposedPorts( + { container: BLOB_PORT, host: this.blobPort }, + { container: QUEUE_PORT, host: this.queuePort }, + { container: TABLE_PORT, host: this.tablePort } + ); + + if (this.accountName !== DEFAULT_ACCOUNT_NAME || this.accountKey !== DEFAULT_ACCOUNT_KEY) { + this.withEnvironment({ + AZURITE_ACCOUNTS: `${this.accountName}:${this.accountKey}`, + }); + } + + const startedContainer = await super.start(); + + return new StartedAzuriteContainer( + startedContainer, + this.accountName, + this.accountKey, + this.blobPort, + this.queuePort, + this.tablePort + ); + } +} + +export class StartedAzuriteContainer extends AbstractStartedContainer { + constructor( + startedTestContainer: StartedTestContainer, + private readonly accountName: string, + private readonly accountKey: string, + private readonly blobPort: number, + private readonly queuePort: number, + private readonly tablePort: number + ) { + super(startedTestContainer); + } + + public getAccountName(): string { + return this.accountName; + } + + public getAccountKey(): string { + return this.accountKey; + } + + public getBlobPort(): number { + return this.blobPort; + } + + public getQueuePort(): number { + return this.queuePort; + } + + public getTablePort(): number { + return this.tablePort; + } + + public getBlobEndpoint(): string { + return this.getEndpoint(this.blobPort, this.accountName); + } + + public getQueueEndpoint(): string { + return this.getEndpoint(this.queuePort, this.accountName); + } + + public getTableEndpoint(): string { + return this.getEndpoint(this.tablePort, this.accountName); + } + + /** + * @returns A connection string in the form of `DefaultEndpointsProtocol=[protocol];AccountName=[accountName];AccountKey=[accountKey];BlobEndpoint=[blobEndpoint];QueueEndpoint=[queueEndpoint];TableEndpoint=[tableEndpoint];` + */ + public getConnectionString(): string { + return `DefaultEndpointsProtocol=http;AccountName=${this.accountName};AccountKey=${ + this.accountKey + };BlobEndpoint=${this.getBlobEndpoint()};QueueEndpoint=${this.getQueueEndpoint()};TableEndpoint=${this.getTableEndpoint()};`; + } + + private getEndpoint(port: number, containerName: string): string { + const url = new URL(`http://${this.getHost()}`); + url.port = port.toString(); + url.pathname = containerName; + return url.toString(); + } +} diff --git a/packages/modules/azurite/src/index.ts b/packages/modules/azurite/src/index.ts new file mode 100644 index 000000000..1345f0cad --- /dev/null +++ b/packages/modules/azurite/src/index.ts @@ -0,0 +1 @@ +export { AzuriteContainer, StartedAzuriteContainer } from "./azurite-container"; diff --git a/packages/modules/azurite/tsconfig.build.json b/packages/modules/azurite/tsconfig.build.json new file mode 100644 index 000000000..0222f6ff1 --- /dev/null +++ b/packages/modules/azurite/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/azurite/tsconfig.json b/packages/modules/azurite/tsconfig.json new file mode 100644 index 000000000..39b165817 --- /dev/null +++ b/packages/modules/azurite/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