From 34bf6817469431a502bd6261ee10691741fc4107 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Mon, 12 Oct 2026 20:03:49 +0100 Subject: [PATCH 1/9] Test Elasticsearch, OpenSearch and MongoDB against their supported versions --- .../src/elasticsearch-container.test.ts | 15 ++++++------ .../mongodb/src/mongodb-container.test.ts | 24 ++++++++++++++++--- .../src/opensearch-container.test.ts | 16 ++++++------- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/packages/modules/elasticsearch/src/elasticsearch-container.test.ts b/packages/modules/elasticsearch/src/elasticsearch-container.test.ts index 886365a0e..1ae909acc 100644 --- a/packages/modules/elasticsearch/src/elasticsearch-container.test.ts +++ b/packages/modules/elasticsearch/src/elasticsearch-container.test.ts @@ -3,10 +3,10 @@ import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { ElasticsearchContainer } from "./elasticsearch-container"; const IMAGE = getImage(__dirname); -const images = ["elasticsearch:7.17.28", "elasticsearch:8.18.1", IMAGE]; +const OLDEST_IMAGE = "elasticsearch:8.19.23"; -describe("ElasticsearchContainer", { timeout: 180_000 }, () => { - it.each(images)("should create an index with %s", async (image) => { +describe.each([IMAGE, OLDEST_IMAGE])("ElasticsearchContainer (%s)", { timeout: 180_000 }, (image) => { + it("should create an index", async () => { // createIndex { await using container = await new ElasticsearchContainer(image).start(); @@ -21,9 +21,10 @@ describe("ElasticsearchContainer", { timeout: 180_000 }, () => { // } }); - it("should index a document", async () => { + // The 9.x client sends a compatible-with=9 media type with request bodies, which Elasticsearch 8 rejects. + it.skipIf(image === OLDEST_IMAGE)("should index a document", async () => { // indexDocument { - await using container = await new ElasticsearchContainer(IMAGE).start(); + await using container = await new ElasticsearchContainer(image).start(); const client = new Client({ node: container.getHttpUrl(), @@ -46,7 +47,7 @@ describe("ElasticsearchContainer", { timeout: 180_000 }, () => { it("should set custom password", async () => { // withPassword { - await using container = await new ElasticsearchContainer(IMAGE).withPassword("testPassword").start(); + await using container = await new ElasticsearchContainer(image).withPassword("testPassword").start(); const client = new Client({ node: container.getHttpUrl(), @@ -60,7 +61,7 @@ describe("ElasticsearchContainer", { timeout: 180_000 }, () => { }); it("should work with restarted container", async () => { - await using container = await new ElasticsearchContainer(IMAGE).start(); + await using container = await new ElasticsearchContainer(image).start(); await container.restart(); const client = new Client({ diff --git a/packages/modules/mongodb/src/mongodb-container.test.ts b/packages/modules/mongodb/src/mongodb-container.test.ts index d44864e9e..477d112f6 100644 --- a/packages/modules/mongodb/src/mongodb-container.test.ts +++ b/packages/modules/mongodb/src/mongodb-container.test.ts @@ -3,9 +3,10 @@ import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { MongoDBContainer } from "./mongodb-container"; const IMAGE = getImage(__dirname); +const OLDEST_IMAGE = "mongo:7.0.43"; -describe("MongoDBContainer", { timeout: 240_000 }, () => { - it.each([IMAGE, "mongo:6.0.25", "mongo:4.4.29"])("should work with %s", async (image) => { +describe.each([IMAGE, OLDEST_IMAGE])("MongoDBContainer (%s)", { timeout: 240_000 }, (image) => { + it("should connect", async () => { // connectMongo { await using container = await new MongoDBContainer(image).start(); @@ -24,7 +25,7 @@ describe("MongoDBContainer", { timeout: 240_000 }, () => { it("should connect with credentials", async () => { // connectWithCredentials { - await using container = await new MongoDBContainer(IMAGE) + await using container = await new MongoDBContainer(image) .withUsername("customUsername") .withPassword("customPassword") .start(); @@ -42,3 +43,20 @@ describe("MongoDBContainer", { timeout: 240_000 }, () => { await db.close(); }); }); + +describe("MongoDBContainer", { timeout: 240_000 }, () => { + it.each(["mongo:8.0.32", "mongo:8.3.11"])("should connect with %s", async (image) => { + await using container = await new MongoDBContainer(image).start(); + + const db = await mongoose.createConnection(container.getConnectionString(), { directConnection: true }).asPromise(); + + const obj = { value: 1 }; + const collection = db.collection("test"); + await collection.insertOne(obj); + + const result = await collection.findOne({ value: 1 }); + expect(result).toEqual(obj); + + await db.close(); + }); +}); diff --git a/packages/modules/opensearch/src/opensearch-container.test.ts b/packages/modules/opensearch/src/opensearch-container.test.ts index 2e7d57eaf..84802940e 100644 --- a/packages/modules/opensearch/src/opensearch-container.test.ts +++ b/packages/modules/opensearch/src/opensearch-container.test.ts @@ -3,10 +3,10 @@ import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { OpenSearchContainer } from "./opensearch-container"; const IMAGE = getImage(__dirname); -const images = ["opensearchproject/opensearch:2.19.2", IMAGE]; +const OLDEST_IMAGE = "opensearchproject/opensearch:2.19.6"; -describe("OpenSearchContainer", { timeout: 180_000 }, () => { - it.each(images)("should create an index with %s", async (image) => { +describe.each([IMAGE, OLDEST_IMAGE])("OpenSearchContainer (%s)", { timeout: 180_000 }, (image) => { + it("should create an index", async () => { // opensearchCreateIndex { await using container = await new OpenSearchContainer(image).start(); @@ -30,7 +30,7 @@ describe("OpenSearchContainer", { timeout: 180_000 }, () => { it("should index a document", async () => { // opensearchIndexDocument { - await using container = await new OpenSearchContainer(IMAGE).start(); + await using container = await new OpenSearchContainer(image).start(); const client = new Client({ node: container.getHttpUrl(), @@ -57,7 +57,7 @@ describe("OpenSearchContainer", { timeout: 180_000 }, () => { }); it("should work with restarted container", async () => { - await using container = await new OpenSearchContainer(IMAGE).start(); + await using container = await new OpenSearchContainer(image).start(); await container.restart(); const client = new Client({ @@ -78,12 +78,12 @@ describe("OpenSearchContainer", { timeout: 180_000 }, () => { }); it("should throw when given an invalid password", () => { - expect(() => new OpenSearchContainer(IMAGE).withPassword("weakpwd")).toThrowError(/Password "weakpwd" is too weak/); + expect(() => new OpenSearchContainer(image).withPassword("weakpwd")).toThrowError(/Password "weakpwd" is too weak/); }); it("should set custom password", async () => { // opensearchCustomPassword { - await using container = await new OpenSearchContainer(IMAGE).withPassword("Str0ng!Passw0rd2025").start(); + await using container = await new OpenSearchContainer(image).withPassword("Str0ng!Passw0rd2025").start(); // } const client = new Client({ @@ -105,7 +105,7 @@ describe("OpenSearchContainer", { timeout: 180_000 }, () => { it("should be reachable with security disabled", async () => { // opensearchDisableSecurity { - await using container = await new OpenSearchContainer(IMAGE).withSecurityEnabled(false).start(); + await using container = await new OpenSearchContainer(image).withSecurityEnabled(false).start(); const client = new Client({ node: container.getHttpUrl(), From 267736295219acd0b0c0d65193daaf2804a73b8a Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 13 Oct 2026 10:41:24 +0100 Subject: [PATCH 2/9] Loop the suites over a list of every supported image --- .../src/elasticsearch-container.test.ts | 7 +++--- .../mongodb/src/mongodb-container.test.ts | 22 ++----------------- .../src/opensearch-container.test.ts | 5 ++--- 3 files changed, 7 insertions(+), 27 deletions(-) diff --git a/packages/modules/elasticsearch/src/elasticsearch-container.test.ts b/packages/modules/elasticsearch/src/elasticsearch-container.test.ts index 1ae909acc..03979d232 100644 --- a/packages/modules/elasticsearch/src/elasticsearch-container.test.ts +++ b/packages/modules/elasticsearch/src/elasticsearch-container.test.ts @@ -2,10 +2,9 @@ import { Client } from "@elastic/elasticsearch"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { ElasticsearchContainer } from "./elasticsearch-container"; -const IMAGE = getImage(__dirname); -const OLDEST_IMAGE = "elasticsearch:8.19.23"; +const IMAGES = ["elasticsearch:8.19.23", getImage(__dirname)]; -describe.each([IMAGE, OLDEST_IMAGE])("ElasticsearchContainer (%s)", { timeout: 180_000 }, (image) => { +describe.each(IMAGES)("ElasticsearchContainer (%s)", { timeout: 180_000 }, (image) => { it("should create an index", async () => { // createIndex { await using container = await new ElasticsearchContainer(image).start(); @@ -22,7 +21,7 @@ describe.each([IMAGE, OLDEST_IMAGE])("ElasticsearchContainer (%s)", { timeout: 1 }); // The 9.x client sends a compatible-with=9 media type with request bodies, which Elasticsearch 8 rejects. - it.skipIf(image === OLDEST_IMAGE)("should index a document", async () => { + it.skipIf(image.startsWith("elasticsearch:8."))("should index a document", async () => { // indexDocument { await using container = await new ElasticsearchContainer(image).start(); diff --git a/packages/modules/mongodb/src/mongodb-container.test.ts b/packages/modules/mongodb/src/mongodb-container.test.ts index 477d112f6..893733a43 100644 --- a/packages/modules/mongodb/src/mongodb-container.test.ts +++ b/packages/modules/mongodb/src/mongodb-container.test.ts @@ -2,10 +2,9 @@ import mongoose from "mongoose"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { MongoDBContainer } from "./mongodb-container"; -const IMAGE = getImage(__dirname); -const OLDEST_IMAGE = "mongo:7.0.43"; +const IMAGES = ["mongo:7.0.43", "mongo:8.0.32", "mongo:8.3.11", getImage(__dirname)]; -describe.each([IMAGE, OLDEST_IMAGE])("MongoDBContainer (%s)", { timeout: 240_000 }, (image) => { +describe.each(IMAGES)("MongoDBContainer (%s)", { timeout: 240_000 }, (image) => { it("should connect", async () => { // connectMongo { await using container = await new MongoDBContainer(image).start(); @@ -43,20 +42,3 @@ describe.each([IMAGE, OLDEST_IMAGE])("MongoDBContainer (%s)", { timeout: 240_000 await db.close(); }); }); - -describe("MongoDBContainer", { timeout: 240_000 }, () => { - it.each(["mongo:8.0.32", "mongo:8.3.11"])("should connect with %s", async (image) => { - await using container = await new MongoDBContainer(image).start(); - - const db = await mongoose.createConnection(container.getConnectionString(), { directConnection: true }).asPromise(); - - const obj = { value: 1 }; - const collection = db.collection("test"); - await collection.insertOne(obj); - - const result = await collection.findOne({ value: 1 }); - expect(result).toEqual(obj); - - await db.close(); - }); -}); diff --git a/packages/modules/opensearch/src/opensearch-container.test.ts b/packages/modules/opensearch/src/opensearch-container.test.ts index 84802940e..ff4d4dfda 100644 --- a/packages/modules/opensearch/src/opensearch-container.test.ts +++ b/packages/modules/opensearch/src/opensearch-container.test.ts @@ -2,10 +2,9 @@ import { Client } from "@opensearch-project/opensearch"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { OpenSearchContainer } from "./opensearch-container"; -const IMAGE = getImage(__dirname); -const OLDEST_IMAGE = "opensearchproject/opensearch:2.19.6"; +const IMAGES = ["opensearchproject/opensearch:2.19.6", getImage(__dirname)]; -describe.each([IMAGE, OLDEST_IMAGE])("OpenSearchContainer (%s)", { timeout: 180_000 }, (image) => { +describe.each(IMAGES)("OpenSearchContainer (%s)", { timeout: 180_000 }, (image) => { it("should create an index", async () => { // opensearchCreateIndex { await using container = await new OpenSearchContainer(image).start(); From cf3237694ad77c5352259f7259e54f6ea08ddc9a Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 13 Oct 2026 11:12:14 +0100 Subject: [PATCH 3/9] List one image per supported major version --- packages/modules/mongodb/src/mongodb-container.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/mongodb/src/mongodb-container.test.ts b/packages/modules/mongodb/src/mongodb-container.test.ts index 893733a43..e66773c2b 100644 --- a/packages/modules/mongodb/src/mongodb-container.test.ts +++ b/packages/modules/mongodb/src/mongodb-container.test.ts @@ -2,7 +2,7 @@ import mongoose from "mongoose"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { MongoDBContainer } from "./mongodb-container"; -const IMAGES = ["mongo:7.0.43", "mongo:8.0.32", "mongo:8.3.11", getImage(__dirname)]; +const IMAGES = ["mongo:7.0.43", "mongo:8.3.11", getImage(__dirname)]; describe.each(IMAGES)("MongoDBContainer (%s)", { timeout: 240_000 }, (image) => { it("should connect", async () => { From a48b73fd0a7614ed20092b8f4ed93e228cf26724 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 13 Oct 2026 12:44:30 +0100 Subject: [PATCH 4/9] Run the images one after another and each image's tests concurrently --- .../src/elasticsearch-container.test.ts | 10 +++++----- .../modules/mongodb/src/mongodb-container.test.ts | 6 +++--- .../opensearch/src/opensearch-container.test.ts | 14 +++++++------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/modules/elasticsearch/src/elasticsearch-container.test.ts b/packages/modules/elasticsearch/src/elasticsearch-container.test.ts index 03979d232..b93e916b1 100644 --- a/packages/modules/elasticsearch/src/elasticsearch-container.test.ts +++ b/packages/modules/elasticsearch/src/elasticsearch-container.test.ts @@ -4,8 +4,8 @@ import { ElasticsearchContainer } from "./elasticsearch-container"; const IMAGES = ["elasticsearch:8.19.23", getImage(__dirname)]; -describe.each(IMAGES)("ElasticsearchContainer (%s)", { timeout: 180_000 }, (image) => { - it("should create an index", async () => { +describe.each(IMAGES)("ElasticsearchContainer (%s)", { concurrent: false, timeout: 180_000 }, (image) => { + it.concurrent("should create an index", async () => { // createIndex { await using container = await new ElasticsearchContainer(image).start(); @@ -21,7 +21,7 @@ describe.each(IMAGES)("ElasticsearchContainer (%s)", { timeout: 180_000 }, (imag }); // The 9.x client sends a compatible-with=9 media type with request bodies, which Elasticsearch 8 rejects. - it.skipIf(image.startsWith("elasticsearch:8."))("should index a document", async () => { + it.skipIf(image.startsWith("elasticsearch:8.")).concurrent("should index a document", async () => { // indexDocument { await using container = await new ElasticsearchContainer(image).start(); @@ -44,7 +44,7 @@ describe.each(IMAGES)("ElasticsearchContainer (%s)", { timeout: 180_000 }, (imag // } }); - it("should set custom password", async () => { + it.concurrent("should set custom password", async () => { // withPassword { await using container = await new ElasticsearchContainer(image).withPassword("testPassword").start(); @@ -59,7 +59,7 @@ describe.each(IMAGES)("ElasticsearchContainer (%s)", { timeout: 180_000 }, (imag expect(await client.indices.exists({ index: "people" })).toBe(true); }); - it("should work with restarted container", async () => { + it.concurrent("should work with restarted container", async () => { await using container = await new ElasticsearchContainer(image).start(); await container.restart(); diff --git a/packages/modules/mongodb/src/mongodb-container.test.ts b/packages/modules/mongodb/src/mongodb-container.test.ts index e66773c2b..67cbe39d2 100644 --- a/packages/modules/mongodb/src/mongodb-container.test.ts +++ b/packages/modules/mongodb/src/mongodb-container.test.ts @@ -4,8 +4,8 @@ import { MongoDBContainer } from "./mongodb-container"; const IMAGES = ["mongo:7.0.43", "mongo:8.3.11", getImage(__dirname)]; -describe.each(IMAGES)("MongoDBContainer (%s)", { timeout: 240_000 }, (image) => { - it("should connect", async () => { +describe.each(IMAGES)("MongoDBContainer (%s)", { concurrent: false, timeout: 240_000 }, (image) => { + it.concurrent("should connect", async () => { // connectMongo { await using container = await new MongoDBContainer(image).start(); @@ -22,7 +22,7 @@ describe.each(IMAGES)("MongoDBContainer (%s)", { timeout: 240_000 }, (image) => // } }); - it("should connect with credentials", async () => { + it.concurrent("should connect with credentials", async () => { // connectWithCredentials { await using container = await new MongoDBContainer(image) .withUsername("customUsername") diff --git a/packages/modules/opensearch/src/opensearch-container.test.ts b/packages/modules/opensearch/src/opensearch-container.test.ts index ff4d4dfda..fe909e64f 100644 --- a/packages/modules/opensearch/src/opensearch-container.test.ts +++ b/packages/modules/opensearch/src/opensearch-container.test.ts @@ -4,8 +4,8 @@ import { OpenSearchContainer } from "./opensearch-container"; const IMAGES = ["opensearchproject/opensearch:2.19.6", getImage(__dirname)]; -describe.each(IMAGES)("OpenSearchContainer (%s)", { timeout: 180_000 }, (image) => { - it("should create an index", async () => { +describe.each(IMAGES)("OpenSearchContainer (%s)", { concurrent: false, timeout: 180_000 }, (image) => { + it.concurrent("should create an index", async () => { // opensearchCreateIndex { await using container = await new OpenSearchContainer(image).start(); @@ -27,7 +27,7 @@ describe.each(IMAGES)("OpenSearchContainer (%s)", { timeout: 180_000 }, (image) // } }); - it("should index a document", async () => { + it.concurrent("should index a document", async () => { // opensearchIndexDocument { await using container = await new OpenSearchContainer(image).start(); @@ -55,7 +55,7 @@ describe.each(IMAGES)("OpenSearchContainer (%s)", { timeout: 180_000 }, (image) // } }); - it("should work with restarted container", async () => { + it.concurrent("should work with restarted container", async () => { await using container = await new OpenSearchContainer(image).start(); await container.restart(); @@ -76,11 +76,11 @@ describe.each(IMAGES)("OpenSearchContainer (%s)", { timeout: 180_000 }, (image) expect(existsResponse.body).toBe(true); }); - it("should throw when given an invalid password", () => { + it.concurrent("should throw when given an invalid password", () => { expect(() => new OpenSearchContainer(image).withPassword("weakpwd")).toThrowError(/Password "weakpwd" is too weak/); }); - it("should set custom password", async () => { + it.concurrent("should set custom password", async () => { // opensearchCustomPassword { await using container = await new OpenSearchContainer(image).withPassword("Str0ng!Passw0rd2025").start(); // } @@ -102,7 +102,7 @@ describe.each(IMAGES)("OpenSearchContainer (%s)", { timeout: 180_000 }, (image) expect(body).toBe(true); }); - it("should be reachable with security disabled", async () => { + it.concurrent("should be reachable with security disabled", async () => { // opensearchDisableSecurity { await using container = await new OpenSearchContainer(image).withSecurityEnabled(false).start(); From 29fcdc098919f8b8dde49aa93346b12af0188d6a Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 13 Oct 2026 13:04:31 +0100 Subject: [PATCH 5/9] Test MongoDB Atlas Local against both supported majors --- .../src/mongodb-atlas-local-container.test.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts index b69a73de2..3dd70b52f 100644 --- a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts +++ b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts @@ -3,7 +3,7 @@ import { IntervalRetry } from "testcontainers"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { MongoDBAtlasLocalContainer } from "./mongodb-atlas-local-container"; -const IMAGE = getImage(__dirname, 1); +const IMAGES = ["mongodb/mongodb-atlas-local:7.0.43", getImage(__dirname, 1)]; const ATLAS_SEARCH_INDEX = { mappings: { dynamic: false, @@ -23,18 +23,18 @@ const ATLAS_SEARCH_INDEX = { }, }; -describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => { - it("should provide a connection string", async () => { +describe.each(IMAGES)("MongoDBAtlasLocalContainer (%s)", { concurrent: false, timeout: 240_000 }, (image) => { + it.concurrent("should provide a connection string", async () => { // connectAtlasLocal { - await using container = await new MongoDBAtlasLocalContainer(IMAGE).start(); + await using container = await new MongoDBAtlasLocalContainer(image).start(); // } expect(container.getConnectionString()).toBe(`mongodb://${container.getHost()}:${container.getMappedPort(27017)}/`); }); - it("should provide a database connection string", async () => { + it.concurrent("should provide a database connection string", async () => { // connectAtlasLocalDatabase { - await using container = await new MongoDBAtlasLocalContainer(IMAGE).start(); + await using container = await new MongoDBAtlasLocalContainer(image).start(); // } expect(container.getDatabaseConnectionString()).toBe( @@ -42,9 +42,9 @@ describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => { ); }); - it("should provide connection strings with credentials", async () => { + it.concurrent("should provide connection strings with credentials", async () => { // connectAtlasLocalWithCredentials { - await using container = await new MongoDBAtlasLocalContainer(IMAGE) + await using container = await new MongoDBAtlasLocalContainer(image) .withUsername("customUsername") .withPassword("customPassword") .start(); @@ -58,8 +58,8 @@ describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => { ); }); - it("should connect to mongodb atlas local", async () => { - await using container = await new MongoDBAtlasLocalContainer(IMAGE).start(); + it.concurrent("should connect to mongodb atlas local", async () => { + await using container = await new MongoDBAtlasLocalContainer(image).start(); const db = await mongoose.createConnection(container.getConnectionString(), { directConnection: true }).asPromise(); @@ -73,8 +73,8 @@ describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => { await db.close(); }); - it("should connect to mongodb atlas local with credentials", async () => { - await using container = await new MongoDBAtlasLocalContainer(IMAGE) + it.concurrent("should connect to mongodb atlas local with credentials", async () => { + await using container = await new MongoDBAtlasLocalContainer(image) .withUsername("customUsername") .withPassword("customPassword") .start(); @@ -93,9 +93,9 @@ describe("MongoDBAtlasLocalContainer", { timeout: 240_000 }, () => { await db.close(); }); - it("should create an atlas search index and search it", async () => { + it.concurrent("should create an atlas search index and search it", async () => { // createAtlasIndexAndSearchIt { - await using atlasLocalContainer = await new MongoDBAtlasLocalContainer(IMAGE).start(); + await using atlasLocalContainer = await new MongoDBAtlasLocalContainer(image).start(); const db = await mongoose .createConnection(atlasLocalContainer.getConnectionString(), { From 58488b18e185142cea93baed38c43ec0a40911f6 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 13 Oct 2026 17:11:33 +0100 Subject: [PATCH 6/9] Test each Elasticsearch major with the client of the same major --- docs/modules/elasticsearch.md | 2 + package-lock.json | 66 ++++++++++++++++++- packages/modules/elasticsearch/package.json | 3 +- .../src/elasticsearch-container.test.ts | 16 +++-- 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/docs/modules/elasticsearch.md b/docs/modules/elasticsearch.md index 6347bda6c..4740befd4 100644 --- a/docs/modules/elasticsearch.md +++ b/docs/modules/elasticsearch.md @@ -14,6 +14,8 @@ These examples use the following libraries: npm install @elastic/elasticsearch +Use the client major that matches your Elasticsearch major. A 9.x client can't talk to Elasticsearch 8. + Choose an image from the [container registry](https://hub.docker.com/_/elasticsearch) and substitute `IMAGE`. ### Create an index diff --git a/package-lock.json b/package-lock.json index 7c1d8e767..1a3b130a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1738,6 +1738,69 @@ } } }, + "node_modules/@elastic/elasticsearch-8": { + "name": "@elastic/elasticsearch", + "version": "8.19.2", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.19.2.tgz", + "integrity": "sha512-LMJCju/+AZkDlJArd/MYABWTDrHi4U7j3qGTKi1hYC7+67SaiYmYFItiueACQVrj2j3ECPMcguZ+7WrZeB+Z5g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@elastic/transport": "^8.9.6", + "apache-arrow": "18.x - 21.x", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@elastic/elasticsearch-8/node_modules/@elastic/transport": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.11.0.tgz", + "integrity": "sha512-1QD1j7gQF8+tVrf+UOcOrLF1LXnhBYwBnNl4xd+fYxkQONjaOjiTcfqXcALHesKezUv8wRzv3tedEnhtYLBmhw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "1.x", + "@opentelemetry/core": "2.x", + "debug": "^4.4.1", + "hpagent": "^1.2.0", + "ms": "^2.1.3", + "secure-json-parse": "^3.0.1", + "tslib": "^2.8.1", + "undici": "^6.21.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@elastic/elasticsearch-8/node_modules/secure-json-parse": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-3.0.2.tgz", + "integrity": "sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/@elastic/elasticsearch-8/node_modules/undici": { + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", + "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/@elastic/transport": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-9.4.0.tgz", @@ -15319,7 +15382,8 @@ "testcontainers": "^12.2.1" }, "devDependencies": { - "@elastic/elasticsearch": "^9.5.1" + "@elastic/elasticsearch": "^9.5.1", + "@elastic/elasticsearch-8": "npm:@elastic/elasticsearch@^8.19.2" } }, "packages/modules/etcd": { diff --git a/packages/modules/elasticsearch/package.json b/packages/modules/elasticsearch/package.json index 7aff51b33..931673159 100644 --- a/packages/modules/elasticsearch/package.json +++ b/packages/modules/elasticsearch/package.json @@ -29,7 +29,8 @@ "build": "tsc --project tsconfig.build.json" }, "devDependencies": { - "@elastic/elasticsearch": "^9.5.1" + "@elastic/elasticsearch": "^9.5.1", + "@elastic/elasticsearch-8": "npm:@elastic/elasticsearch@^8.19.2" }, "dependencies": { "testcontainers": "^12.2.1" diff --git a/packages/modules/elasticsearch/src/elasticsearch-container.test.ts b/packages/modules/elasticsearch/src/elasticsearch-container.test.ts index b93e916b1..80217559b 100644 --- a/packages/modules/elasticsearch/src/elasticsearch-container.test.ts +++ b/packages/modules/elasticsearch/src/elasticsearch-container.test.ts @@ -1,10 +1,17 @@ -import { Client } from "@elastic/elasticsearch"; +import { Client as Client9 } from "@elastic/elasticsearch"; +import { Client as Client8 } from "@elastic/elasticsearch-8"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { ElasticsearchContainer } from "./elasticsearch-container"; -const IMAGES = ["elasticsearch:8.19.23", getImage(__dirname)]; +// A client can't talk to an older Elasticsearch major, so each image runs with the client of its own major. +const VERSIONS = [ + { image: "elasticsearch:8.19.23", Client: Client8 as unknown as typeof Client9 }, + { image: getImage(__dirname), Client: Client9 }, +]; + +describe.each(VERSIONS)("ElasticsearchContainer ($image)", { concurrent: false, timeout: 180_000 }, (version) => { + const { image, Client } = version; -describe.each(IMAGES)("ElasticsearchContainer (%s)", { concurrent: false, timeout: 180_000 }, (image) => { it.concurrent("should create an index", async () => { // createIndex { await using container = await new ElasticsearchContainer(image).start(); @@ -20,8 +27,7 @@ describe.each(IMAGES)("ElasticsearchContainer (%s)", { concurrent: false, timeou // } }); - // The 9.x client sends a compatible-with=9 media type with request bodies, which Elasticsearch 8 rejects. - it.skipIf(image.startsWith("elasticsearch:8.")).concurrent("should index a document", async () => { + it.concurrent("should index a document", async () => { // indexDocument { await using container = await new ElasticsearchContainer(image).start(); From 3831835567bad18c8f6236f905d99c8e6729275e Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 13 Oct 2026 18:51:47 +0100 Subject: [PATCH 7/9] Test Elasticsearch 8 in its own file with the 8.x client --- .../src/elasticsearch-container-v8.test.ts | 67 +++++++++++++++++++ .../src/elasticsearch-container.test.ts | 29 +++----- 2 files changed, 78 insertions(+), 18 deletions(-) create mode 100644 packages/modules/elasticsearch/src/elasticsearch-container-v8.test.ts diff --git a/packages/modules/elasticsearch/src/elasticsearch-container-v8.test.ts b/packages/modules/elasticsearch/src/elasticsearch-container-v8.test.ts new file mode 100644 index 000000000..a0f79dfa3 --- /dev/null +++ b/packages/modules/elasticsearch/src/elasticsearch-container-v8.test.ts @@ -0,0 +1,67 @@ +import { Client } from "@elastic/elasticsearch-8"; +import { ElasticsearchContainer } from "./elasticsearch-container"; + +const IMAGE = "elasticsearch:8.19.23"; + +describe("ElasticsearchContainer 8", { concurrent: false, timeout: 180_000 }, () => { + it("should create an index", async () => { + await using container = await new ElasticsearchContainer(IMAGE).start(); + + const client = new Client({ + node: container.getHttpUrl(), + auth: { username: container.getUsername(), password: container.getPassword() }, + }); + + await client.indices.create({ index: "people" }); + + expect(await client.indices.exists({ index: "people" })).toBe(true); + }); + + it("should index a document", async () => { + await using container = await new ElasticsearchContainer(IMAGE).start(); + + const client = new Client({ + node: container.getHttpUrl(), + auth: { username: container.getUsername(), password: container.getPassword() }, + }); + + const document = { + id: "1", + name: "John Doe", + }; + await client.index({ + index: "people", + id: document.id, + document, + }); + + expect((await client.get({ index: "people", id: document.id }))._source).toStrictEqual(document); + }); + + it("should set custom password", async () => { + await using container = await new ElasticsearchContainer(IMAGE).withPassword("testPassword").start(); + + const client = new Client({ + node: container.getHttpUrl(), + auth: { username: container.getUsername(), password: container.getPassword() }, + }); + + await client.indices.create({ index: "people" }); + + expect(await client.indices.exists({ index: "people" })).toBe(true); + }); + + it("should work with restarted container", async () => { + await using container = await new ElasticsearchContainer(IMAGE).start(); + await container.restart(); + + const client = new Client({ + node: container.getHttpUrl(), + auth: { username: container.getUsername(), password: container.getPassword() }, + }); + + await client.indices.create({ index: "people" }); + + expect(await client.indices.exists({ index: "people" })).toBe(true); + }); +}); diff --git a/packages/modules/elasticsearch/src/elasticsearch-container.test.ts b/packages/modules/elasticsearch/src/elasticsearch-container.test.ts index 80217559b..4690f7cbc 100644 --- a/packages/modules/elasticsearch/src/elasticsearch-container.test.ts +++ b/packages/modules/elasticsearch/src/elasticsearch-container.test.ts @@ -1,20 +1,13 @@ -import { Client as Client9 } from "@elastic/elasticsearch"; -import { Client as Client8 } from "@elastic/elasticsearch-8"; +import { Client } from "@elastic/elasticsearch"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { ElasticsearchContainer } from "./elasticsearch-container"; -// A client can't talk to an older Elasticsearch major, so each image runs with the client of its own major. -const VERSIONS = [ - { image: "elasticsearch:8.19.23", Client: Client8 as unknown as typeof Client9 }, - { image: getImage(__dirname), Client: Client9 }, -]; +const IMAGE = getImage(__dirname); -describe.each(VERSIONS)("ElasticsearchContainer ($image)", { concurrent: false, timeout: 180_000 }, (version) => { - const { image, Client } = version; - - it.concurrent("should create an index", async () => { +describe("ElasticsearchContainer", { timeout: 180_000 }, () => { + it("should create an index", async () => { // createIndex { - await using container = await new ElasticsearchContainer(image).start(); + await using container = await new ElasticsearchContainer(IMAGE).start(); const client = new Client({ node: container.getHttpUrl(), @@ -27,9 +20,9 @@ describe.each(VERSIONS)("ElasticsearchContainer ($image)", { concurrent: false, // } }); - it.concurrent("should index a document", async () => { + it("should index a document", async () => { // indexDocument { - await using container = await new ElasticsearchContainer(image).start(); + await using container = await new ElasticsearchContainer(IMAGE).start(); const client = new Client({ node: container.getHttpUrl(), @@ -50,9 +43,9 @@ describe.each(VERSIONS)("ElasticsearchContainer ($image)", { concurrent: false, // } }); - it.concurrent("should set custom password", async () => { + it("should set custom password", async () => { // withPassword { - await using container = await new ElasticsearchContainer(image).withPassword("testPassword").start(); + await using container = await new ElasticsearchContainer(IMAGE).withPassword("testPassword").start(); const client = new Client({ node: container.getHttpUrl(), @@ -65,8 +58,8 @@ describe.each(VERSIONS)("ElasticsearchContainer ($image)", { concurrent: false, expect(await client.indices.exists({ index: "people" })).toBe(true); }); - it.concurrent("should work with restarted container", async () => { - await using container = await new ElasticsearchContainer(image).start(); + it("should work with restarted container", async () => { + await using container = await new ElasticsearchContainer(IMAGE).start(); await container.restart(); const client = new Client({ From 37e496a5b26a3cc7d2cdb5fb1ebcef47222455a8 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Wed, 14 Oct 2026 13:26:26 +0100 Subject: [PATCH 8/9] List both Elasticsearch client majors in the docs --- docs/modules/elasticsearch.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/modules/elasticsearch.md b/docs/modules/elasticsearch.md index 4740befd4..d9c65bdbd 100644 --- a/docs/modules/elasticsearch.md +++ b/docs/modules/elasticsearch.md @@ -8,13 +8,15 @@ npm install @testcontainers/elasticsearch --save-dev ## Examples -These examples use the following libraries: +These examples use the following libraries. Use the client major that matches your Elasticsearch major: -- [@elastic/elasticsearch](https://www.npmjs.com/package/@elastic/elasticsearch) +- [@elastic/elasticsearch 9](https://www.npmjs.com/package/@elastic/elasticsearch) for Elasticsearch 9 - npm install @elastic/elasticsearch + npm install @elastic/elasticsearch@9 -Use the client major that matches your Elasticsearch major. A 9.x client can't talk to Elasticsearch 8. +- [@elastic/elasticsearch 8](https://www.npmjs.com/package/@elastic/elasticsearch/v/8.19.2) for Elasticsearch 8 + + npm install @elastic/elasticsearch@8 Choose an image from the [container registry](https://hub.docker.com/_/elasticsearch) and substitute `IMAGE`. From 80eb7132cb6dc6ce1aaeea09c0f449be730575f3 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Wed, 14 Oct 2026 13:32:39 +0100 Subject: [PATCH 9/9] Run the main Elasticsearch suite sequentially too --- .../modules/elasticsearch/src/elasticsearch-container.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/elasticsearch/src/elasticsearch-container.test.ts b/packages/modules/elasticsearch/src/elasticsearch-container.test.ts index 4690f7cbc..e63bd10fe 100644 --- a/packages/modules/elasticsearch/src/elasticsearch-container.test.ts +++ b/packages/modules/elasticsearch/src/elasticsearch-container.test.ts @@ -4,7 +4,7 @@ import { ElasticsearchContainer } from "./elasticsearch-container"; const IMAGE = getImage(__dirname); -describe("ElasticsearchContainer", { timeout: 180_000 }, () => { +describe("ElasticsearchContainer", { concurrent: false, timeout: 180_000 }, () => { it("should create an index", async () => { // createIndex { await using container = await new ElasticsearchContainer(IMAGE).start();