diff --git a/docs/modules/oraclefree.md b/docs/modules/oraclefree.md new file mode 100644 index 000000000..4bbf814d2 --- /dev/null +++ b/docs/modules/oraclefree.md @@ -0,0 +1,23 @@ +# Oracle Free + +## Install + +```bash +npm install @testcontainers/oraclefree --save-dev +``` + +## Examples + +These examples use the following libraries: + +- [oracledb](https://www.npmjs.com/package/oracledb) + + npm install oracledb + +Recommended to use an image from [this registry](https://hub.docker.com/r/gvenzl/oracle-free) and substitute for `IMAGE` + +### Start a database and execute queries + + +[](../../packages/modules/oraclefree/src/oraclefree-container.test.ts) inside_block:customDatabase + \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index e446b5b16..327f3902d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -81,6 +81,7 @@ nav: - Neo4J: modules/neo4j.md - Ollama: modules/ollama.md - OpenSearch: modules/opensearch.md + - Oracle Free: modules/oraclefree.md - PostgreSQL: modules/postgresql.md - Qdrant: modules/qdrant.md - RabbitMQ: modules/rabbitmq.md diff --git a/package-lock.json b/package-lock.json index f197dff57..2f82c03bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7248,6 +7248,10 @@ "resolved": "packages/modules/opensearch", "link": true }, + "node_modules/@testcontainers/oraclefree": { + "resolved": "packages/modules/oraclefree", + "link": true + }, "node_modules/@testcontainers/postgresql": { "resolved": "packages/modules/postgresql", "link": true @@ -7627,6 +7631,16 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/@types/oracledb": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.10.1.tgz", + "integrity": "sha512-/P478CfP8rYvXi6897OLn/BgXOP/RnDcKYCX3JRRNSZ/J94gmKJAT1vWiLA+HDRopBmqe0pzzud6hKhKQguJcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/pg": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", @@ -15272,6 +15286,17 @@ "node": ">= 0.8.0" } }, + "node_modules/oracledb": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/oracledb/-/oracledb-6.10.0.tgz", + "integrity": "sha512-kGUumXmrEWbSpBuKJyb9Ip3rXcNgKK6grunI3/cLPzrRvboZ6ZoLi9JQ+z6M/RIG924tY8BLflihL4CKKQAYMA==", + "dev": true, + "hasInstallScript": true, + "license": "(Apache-2.0 OR UPL-1.0)", + "engines": { + "node": ">=14.17" + } + }, "node_modules/outvariant": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", @@ -19445,6 +19470,18 @@ "@types/zxcvbn": "^4.4.5" } }, + "packages/modules/oraclefree": { + "name": "@testcontainers/oraclefree", + "version": "11.12.0", + "license": "MIT", + "dependencies": { + "testcontainers": "^11.12.0" + }, + "devDependencies": { + "@types/oracledb": "^6.10.0", + "oracledb": "^6.10.0" + } + }, "packages/modules/postgresql": { "name": "@testcontainers/postgresql", "version": "11.12.0", diff --git a/packages/modules/oraclefree/Dockerfile b/packages/modules/oraclefree/Dockerfile new file mode 100644 index 000000000..c81b831ed --- /dev/null +++ b/packages/modules/oraclefree/Dockerfile @@ -0,0 +1 @@ +FROM gvenzl/oracle-free:slim-faststart \ No newline at end of file diff --git a/packages/modules/oraclefree/package.json b/packages/modules/oraclefree/package.json new file mode 100644 index 000000000..716b2bfe1 --- /dev/null +++ b/packages/modules/oraclefree/package.json @@ -0,0 +1,39 @@ +{ + "name": "@testcontainers/oraclefree", + "version": "11.12.0", + "license": "MIT", + "keywords": [ + "oracle", + "database", + "testing", + "docker", + "testcontainers" + ], + "description": "Oracle DB Free 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.12.0" + }, + "devDependencies": { + "oracledb": "^6.10.0", + "@types/oracledb": "^6.10.0" + } +} \ No newline at end of file diff --git a/packages/modules/oraclefree/src/index.ts b/packages/modules/oraclefree/src/index.ts new file mode 100644 index 000000000..85f704044 --- /dev/null +++ b/packages/modules/oraclefree/src/index.ts @@ -0,0 +1 @@ +export { OracleDbContainer, StartedOracleDbContainer } from "./oraclefree-container"; diff --git a/packages/modules/oraclefree/src/oraclefree-container.test.ts b/packages/modules/oraclefree/src/oraclefree-container.test.ts new file mode 100644 index 000000000..c9a0409c3 --- /dev/null +++ b/packages/modules/oraclefree/src/oraclefree-container.test.ts @@ -0,0 +1,107 @@ +import oracledb from "oracledb"; +import { getImage } from "../../../testcontainers/src/utils/test-helper"; +import { OracleDbContainer, StartedOracleDbContainer } from "./oraclefree-container"; + +const IMAGE = getImage(__dirname); + +describe.sequential("OracleFreeContainer", { timeout: 240_000 }, () => { + describe("default configuration", () => { + let container: StartedOracleDbContainer; + + beforeAll(async () => { + container = await new OracleDbContainer(IMAGE).start(); + }, 120_000); + + afterAll(async () => { + await container.stop(); + }); + + it("should connect and return a query result", async () => { + const connection = await oracledb.getConnection({ + user: container.getUsername(), + password: container.getPassword(), + connectString: container.getUrl(), + }); + + const result = await connection.execute("SELECT 1 FROM DUAL"); + expect(result.rows![0]).toEqual([1]); + + await connection.close(); + }); + + it("should work with connection descriptor", async () => { + const connection = await oracledb.getConnection({ + user: container.getUsername(), + password: container.getPassword(), + connectString: container.getConnectionDescriptor(), + }); + + const result = await connection.execute("SELECT 1 FROM DUAL"); + expect(result.rows![0]).toEqual([1]); + + await connection.close(); + }); + + it("should work with restarted container", async () => { + await container.restart(); + + const connection = await oracledb.getConnection({ + user: container.getUsername(), + password: container.getPassword(), + connectString: container.getUrl(), + }); + + const result = await connection.execute("SELECT 1 FROM DUAL"); + expect(result.rows![0]).toEqual([1]); + + await connection.close(); + }); + + it("should have default database name", async () => { + const connection = await oracledb.getConnection({ + user: container.getUsername(), + password: container.getPassword(), + connectString: container.getUrl(), + }); + + const result = await connection.execute("SELECT SYS_CONTEXT('USERENV', 'CON_NAME') FROM DUAL"); + expect(result.rows![0]).toEqual(["FREEPDB1"]); + + await connection.close(); + }); + }); + + it("should treat default database names as no-op and reject empty names", () => { + const container = new OracleDbContainer(IMAGE); + expect(() => container.withDatabase("FREEPDB1")).not.toThrow(); + expect(() => container.withDatabase("freepdb1")).not.toThrow(); + expect(() => container.withDatabase("")).toThrow("Database name cannot be empty."); + }); + + it("should set the custom database and user", async () => { + // customDatabase { + const customDatabase = "TESTDB"; + const customUsername = "CUSTOMUSER"; + const customPassword = "customPassword"; + await using container = await new OracleDbContainer(IMAGE) + .withDatabase(customDatabase) + .withUsername(customUsername) + .withPassword(customPassword) + .start(); + + const connection = await oracledb.getConnection({ + user: container.getUsername(), + password: container.getPassword(), + connectString: container.getUrl(), + }); + + const result = await connection.execute("SELECT SYS_CONTEXT('USERENV', 'CON_NAME') FROM DUAL"); + expect(result.rows![0]).toEqual([customDatabase]); + + const resultUser = await connection.execute("SELECT USER FROM DUAL"); + expect(resultUser.rows![0]).toEqual([customUsername]); + + await connection.close(); + // } + }); +}); diff --git a/packages/modules/oraclefree/src/oraclefree-container.ts b/packages/modules/oraclefree/src/oraclefree-container.ts new file mode 100644 index 000000000..9195ef9d1 --- /dev/null +++ b/packages/modules/oraclefree/src/oraclefree-container.ts @@ -0,0 +1,115 @@ +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; + +/** Default Oracle listener port. */ +const ORACLEDB_PORT = 1521; +/** Default pluggable database name provided by the Oracle Free image. */ +const DEFAULT_DATABASE = "FREEPDB1"; + +/** + * Testcontainers wrapper for Oracle Free. + * + * Supports configuring application user credentials and optional custom database creation. + */ +export class OracleDbContainer extends GenericContainer { + private username = "test"; + private password = "test"; + private database?: string = undefined; + + constructor(image: string) { + super(image); + this.withExposedPorts(ORACLEDB_PORT); + this.withWaitStrategy(Wait.forLogMessage("DATABASE IS READY TO USE!")); + this.withStartupTimeout(120_000); + } + + /** Sets the application username created at container startup. */ + public withUsername(username: string): this { + this.username = username; + return this; + } + + /** Sets the password for both SYS and application user startup configuration. */ + public withPassword(password: string): this { + this.password = password; + return this; + } + + /** Sets a custom application database/service name. */ + public withDatabase(database: string): this { + if (database.trim() === "") { + throw new Error("Database name cannot be empty."); + } + + if (database.toUpperCase() === DEFAULT_DATABASE) { + this.database = undefined; + return this; + } + + this.database = database; + return this; + } + + /** Starts the container and returns a typed started container instance. */ + public override async start(): Promise { + this.withEnvironment({ + ORACLE_PASSWORD: this.password, + APP_USER: this.username, + APP_USER_PASSWORD: this.password, + }); + + if (this.database) { + this.withEnvironment({ + ORACLE_DATABASE: this.database, + }); + } + + return new StartedOracleDbContainer( + await super.start(), + this.username, + this.password, + this.database ?? DEFAULT_DATABASE + ); + } +} + +/** Represents a running Oracle Free test container with Oracle-specific accessors. */ +export class StartedOracleDbContainer extends AbstractStartedContainer { + constructor( + startedTestContainer: StartedTestContainer, + private readonly username: string, + private readonly password: string, + private readonly database: string + ) { + super(startedTestContainer); + } + + /** Returns the mapped Oracle listener port. */ + public getPort(): number { + return this.getMappedPort(ORACLEDB_PORT); + } + + /** Returns the configured application username. */ + public getUsername(): string { + return this.username; + } + + /** Returns the configured password. */ + public getPassword(): string { + return this.password; + } + + /** Returns the configured service/database name. */ + public getDatabase(): string { + return this.database; + } + + /** Returns a host:port/database URL fragment. */ + public getUrl(): string { + return `${this.getHost()}:${this.getPort()}/${this.database}`; + } + + /** Returns an Oracle connection descriptor string (TNS format). */ + public getConnectionDescriptor(): string { + return `(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=${this.getHost()})(PORT=${this.getPort()}))(CONNECT_DATA=(SERVICE_NAME=${this.database})))`; + } +} diff --git a/packages/modules/oraclefree/tsconfig.build.json b/packages/modules/oraclefree/tsconfig.build.json new file mode 100644 index 000000000..ff7390b10 --- /dev/null +++ b/packages/modules/oraclefree/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/oraclefree/tsconfig.json b/packages/modules/oraclefree/tsconfig.json new file mode 100644 index 000000000..4d74c3e41 --- /dev/null +++ b/packages/modules/oraclefree/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