Skip to content

[testcontainers-mongo] Random 0–9999 DB name suffix collides across a run, causing E11000 duplicate key failures #3424

Description

const seed = Math.floor(Math.random() * 10000);
const {dbName = "db-test", ...otherOpts} = opts;
const url = `${getMongoUrl()}/${dbName}-${seed}`;

### Information

- **Package:** `@tsed/testcontainers-mongo`
- **Version:** 8.18.0
- **Node:** 20.x
- **MongoDB image:** mongo:7.0.14 (also reproducible on 6.x)

### Description

`getMongoConnectionOptions()` derives the per-test database name from a random integer
in the range **0–9999**:

```js
// packages/orm/testcontainers-mongo/src/services/ContainerUtils.ts
export function getMongoConnectionOptions(id = "", opts = {}) {
    const seed = Math.floor(Math.random() * 10000); // <-- only 10k possible names
    const { dbName = "db-test", ...otherOpts } = opts;
    const url = `${getMongoUrl()}/${dbName}-${seed}`;
    ...
}

Meanwhile startMongoServer() starts one container per process and caches it on
global[KEY], so every bootstrap() / create() in a run shares that single container:

// ContainerUtils.ts
const container = getEnvironment(KEY) || (await createMongoContainer(image));

And reset() (the documented teardown) does not clear data unless a single collection
name is explicitly passed:

// TestContainersMongo.ts
static async reset(collectionName) {
    if (typeof collectionName === "string") {
        await TestContainersMongo.cleanCollection(collectionName);
    }
    return PlatformTest.reset(); // only disconnects; data is left in place
}

Consequence: because the name space is only 10,000 and databases are never dropped or
emptied for the run, two bootstrap() calls eventually generate the same db-test-<n>.
A reused database still contains the previous suite's documents, so any test that seeds a
fixed/hard-coded _id fails on insert with:

MongoServerError: E11000 duplicate key error collection: db-test-<n>.<collection>
index: _id_ dup key: { _id: ObjectId('...') }

The probability grows with the number of suites in a run, so this shows up
as intermittent / flaky failures — and is far more likely in serial CI runs
(--no-file-parallelism, no Vitest retry) than in parallel local runs where a retry happens to
draw a different random name and masks the problem.

Expected behavior

Each test bootstrap should get an isolated database, and/or teardown should reliably clean
the data so a reused name can never leak documents into the next suite.

Suggested fixes (any one would resolve it)

  1. Guarantee uniqueness instead of low-entropy randomness. Use a monotonic counter, a
    UUID/crypto suffix, or the worker id (e.g. process.env.VITEST_POOL_ID /
    VITEST_WORKER_ID, JEST_WORKER_ID) so names cannot collide within a run:
    const seed = `${process.pid}-${counter++}`; // or randomUUID()
  2. Make reset() clean the whole database by default (drop or deleteMany({}) across all
    collections), not only when a single collection name is supplied.
  3. At minimum, document that reset() does not clear data and that DB names are random
    with a tiny keyspace, so consumers know they must clean up themselves.

Reproduction

With the shared container, force two bootstraps to receive the same suffix (e.g. pin
Math.random), seed a document with a fixed _id in each, and run them sequentially — the
second insert throws E11000. Restoring unique names (or clearing collections on teardown)
makes it pass deterministically.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions