diff --git a/docs/modules/cassandra.md b/docs/modules/cassandra.md new file mode 100644 index 000000000..877071ba2 --- /dev/null +++ b/docs/modules/cassandra.md @@ -0,0 +1,29 @@ +# Cassandra Module + +[Cassandra](https://cassandra.apache.org/_/index.html) is a free and open source, distributed NoSQL database management system. It is designed to handle large amounts of data across many commodity servers, providing high availability with no single point of failure. + + + +## Install + +```bash +npm install @testcontainers/cassandra --save-dev +``` + +## Examples + + +[Connect:](../../packages/modules/cassandra/src/cassandra-container.test.ts) inside_block:connectWithDefaultCredentials + + + +[Connect with custom credentials:](../../packages/modules/cassandra/src/cassandra-container.test.ts) inside_block:connectWithCustomCredentials + + + +[With custom datacenter / rack](../../packages/modules/cassandra/src/cassandra-container.test.ts) inside_block:customDataSenterAndRack + + + +[Insert & fetch data:](../../packages/modules/cassandra/src/cassandra-container.test.ts) inside_block:createAndFetchData + diff --git a/mkdocs.yml b/mkdocs.yml index 3bf5c5077..edfb6cec6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,6 +43,7 @@ nav: - Advanced: features/advanced.md - Modules: - ArangoDB: modules/arangodb.md + - Cassandra: modules/cassandra.md - ChromaDB: modules/chromadb.md - Couchbase: modules/couchbase.md - Elasticsearch: modules/elasticsearch.md diff --git a/package-lock.json b/package-lock.json index 7281d70f2..8a4b5d287 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5257,6 +5257,10 @@ "resolved": "packages/modules/arangodb", "link": true }, + "node_modules/@testcontainers/cassandra": { + "resolved": "packages/modules/cassandra", + "link": true + }, "node_modules/@testcontainers/chromadb": { "resolved": "packages/modules/chromadb", "link": true @@ -6216,6 +6220,15 @@ "node": ">=0.4.0" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "dev": true, + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -7488,6 +7501,31 @@ } ] }, + "node_modules/cassandra-driver": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.7.2.tgz", + "integrity": "sha512-gwl1DeYvL8Wy3i1GDMzFtpUg5G473fU7EnHFZj7BUtdLB7loAfgZgB3zBhROc9fbaDSUDs6YwOPPojS5E1kbSA==", + "dev": true, + "dependencies": { + "@types/long": "~5.0.0", + "@types/node": ">=8", + "adm-zip": "~0.5.10", + "long": "~5.2.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/cassandra-driver/node_modules/@types/long": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/long/-/long-5.0.0.tgz", + "integrity": "sha512-eQs9RsucA/LNjnMoJvWG/nXa7Pot/RbBzilF/QRIU/xRl+0ApxrSUFsV5lmf01SvSlqMzJ7Zwxe440wmz2SJGA==", + "deprecated": "This is a stub types definition. long provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "long": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -19678,6 +19716,17 @@ "arangojs": "^8.8.1" } }, + "packages/modules/cassandra": { + "name": "@testcontainers/cassandra", + "version": "10.13.2", + "license": "MIT", + "dependencies": { + "testcontainers": "^10.13.2" + }, + "devDependencies": { + "cassandra-driver": "^4.7.2" + } + }, "packages/modules/chromadb": { "name": "@testcontainers/chromadb", "version": "10.13.2", diff --git a/packages/modules/cassandra/jest.config.ts b/packages/modules/cassandra/jest.config.ts new file mode 100644 index 000000000..1f677baaf --- /dev/null +++ b/packages/modules/cassandra/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/cassandra/package.json b/packages/modules/cassandra/package.json new file mode 100644 index 000000000..be025ea1f --- /dev/null +++ b/packages/modules/cassandra/package.json @@ -0,0 +1,37 @@ +{ + "name": "@testcontainers/cassandra", + "version": "10.13.2", + "license": "MIT", + "keywords": [ + "mariadb", + "testing", + "docker", + "testcontainers" + ], + "description": "Cassandra 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": { + "cassandra-driver": "^4.7.2" + } +} diff --git a/packages/modules/cassandra/src/cassandra-container.test.ts b/packages/modules/cassandra/src/cassandra-container.test.ts new file mode 100644 index 000000000..8a6eef17e --- /dev/null +++ b/packages/modules/cassandra/src/cassandra-container.test.ts @@ -0,0 +1,112 @@ +import { Client } from "cassandra-driver"; +import { CassandraContainer } from "./cassandra-container"; + +describe("Cassandra", () => { + jest.setTimeout(240_000); + + // connectWithDefaultCredentials { + it("should connect and execute a query with default credentials", async () => { + const container = await new CassandraContainer("cassandra:5.0.2").start(); + + const client = new Client({ + contactPoints: [container.getContactPoint()], + localDataCenter: container.getDatacenter(), + keyspace: "system", + }); + + await client.connect(); + + const result = await client.execute("SELECT release_version FROM system.local"); + expect(result.rows[0].release_version).toBe("5.0.2"); + + await client.shutdown(); + await container.stop(); + }); + // } + + // connectWithCustomCredentials { + it("should connect with custom username and password", async () => { + const username = "testUser"; + const password = "testPassword"; + + const container = await new CassandraContainer().withUsername(username).withPassword(password).start(); + + const client = new Client({ + contactPoints: [container.getContactPoint()], + localDataCenter: container.getDatacenter(), + credentials: { username, password }, + keyspace: "system", + }); + + await client.connect(); + + const result = await client.execute("SELECT release_version FROM system.local"); + expect(result.rows.length).toBeGreaterThan(0); + + await client.shutdown(); + await container.stop(); + }); + // } + + // customDataSenterAndRack { + it("should set datacenter and rack", async () => { + const customDataCenter = "customDC"; + const customRack = "customRack"; + const container = await new CassandraContainer().withDatacenter(customDataCenter).withRack(customRack).start(); + + const client = new Client({ + contactPoints: [container.getContactPoint()], + localDataCenter: container.getDatacenter(), + }); + + await client.connect(); + const result = await client.execute("SELECT data_center, rack FROM system.local"); + expect(result.rows[0].data_center).toBe(customDataCenter); + expect(result.rows[0].rack).toBe(customRack); + + await client.shutdown(); + await container.stop(); + }); + // } + + // createAndFetchData { + it("should create keyspace, a table, insert data, and retrieve it", async () => { + const container = await new CassandraContainer().start(); + + const client = new Client({ + contactPoints: [container.getContactPoint()], + localDataCenter: container.getDatacenter(), + }); + + await client.connect(); + + // Create the keyspace + await client.execute(` + CREATE KEYSPACE IF NOT EXISTS test_keyspace + WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} + `); + + await client.execute("USE test_keyspace"); + + // Create the table. + await client.execute(` + CREATE TABLE IF NOT EXISTS test_keyspace.users ( + id UUID PRIMARY KEY, + name text + ) + `); + + // Insert a record + const id = "d002cd08-401a-47d6-92d7-bb4204d092f8"; // Fixed UUID for testing + const username = "Testy McTesterson"; + client.execute("INSERT INTO test_keyspace.users (id, name) VALUES (?, ?)", [id, username]); + + // Fetch and verify the record + const result = await client.execute("SELECT * FROM test_keyspace.users WHERE id = ?", [id], { prepare: true }); + expect(result.rows[0].name).toBe(username); + + await client.shutdown(); + await container.stop(); + }); + // } +}); diff --git a/packages/modules/cassandra/src/cassandra-container.ts b/packages/modules/cassandra/src/cassandra-container.ts new file mode 100644 index 000000000..fa96bb532 --- /dev/null +++ b/packages/modules/cassandra/src/cassandra-container.ts @@ -0,0 +1,89 @@ +import { AbstractStartedContainer, GenericContainer, type StartedTestContainer } from "testcontainers"; + +const CASSANDRA_PORT = 9042; + +export class CassandraContainer extends GenericContainer { + private dc = "dc1"; + private rack = "rack1"; + private username = "cassandra"; + private password = "cassandra"; + + constructor(image = "cassandra:5.0.2") { + super(image); + this.withExposedPorts(CASSANDRA_PORT).withStartupTimeout(120_000); + } + + public withDatacenter(dc: string): this { + this.dc = dc; + return this; + } + + public withRack(rack: string): this { + this.rack = rack; + return this; + } + + 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 { + this.withEnvironment({ + CASSANDRA_DC: this.dc, + CASSANDRA_RACK: this.rack, + CASSANDRA_LISTEN_ADDRESS: "auto", + CASSANDRA_BROADCAST_ADDRESS: "auto", + CASSANDRA_RPC_ADDRESS: "0.0.0.0", + CASSANDRA_USERNAME: this.username, + CASSANDRA_PASSWORD: this.password, + CASSANDRA_SNITCH: "GossipingPropertyFileSnitch", + CASSANDRA_ENDPOINT_SNITCH: "GossipingPropertyFileSnitch", + }); + return new StartedCassandraContainer(await super.start(), this.dc, this.rack, this.username, this.password); + } +} + +export class StartedCassandraContainer extends AbstractStartedContainer { + private readonly port: number; + + constructor( + startedTestContainer: StartedTestContainer, + private readonly dc: string, + private readonly rack: string, + private readonly username: string, + private readonly password: string + ) { + super(startedTestContainer); + this.port = startedTestContainer.getMappedPort(CASSANDRA_PORT); + } + + public getPort(): number { + return this.port; + } + + public getDatacenter(): string { + return this.dc; + } + + public getRack(): string { + return this.rack; + } + + public getUsername(): string { + return this.username; + } + + public getPassword(): string { + return this.password; + } + + public getContactPoint(): string { + return `${this.getHost()}:${this.getPort()}`; + } +} diff --git a/packages/modules/cassandra/src/index.ts b/packages/modules/cassandra/src/index.ts new file mode 100644 index 000000000..4f2b7b344 --- /dev/null +++ b/packages/modules/cassandra/src/index.ts @@ -0,0 +1 @@ +export { CassandraContainer, StartedCassandraContainer } from "./cassandra-container"; diff --git a/packages/modules/cassandra/tsconfig.build.json b/packages/modules/cassandra/tsconfig.build.json new file mode 100644 index 000000000..0222f6ff1 --- /dev/null +++ b/packages/modules/cassandra/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/cassandra/tsconfig.json b/packages/modules/cassandra/tsconfig.json new file mode 100644 index 000000000..39b165817 --- /dev/null +++ b/packages/modules/cassandra/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