From 5080b4d647f4b81f2ed407d95636f888fe7d93a7 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Mon, 12 Oct 2026 17:31:31 +0100 Subject: [PATCH 1/4] Enforce function length, nullish coalescing and no-console with Biome --- AGENTS.md | 1 + biome.json | 37 ++++- .../modules/azurite/src/azurite-container.ts | 60 ++++--- .../src/cloudstorage-emulator-container.ts | 4 +- .../postgresql/src/postgresql-container.ts | 4 +- .../modules/valkey/src/valkey-container.ts | 2 +- .../src/container-runtime/clients/client.ts | 38 +++-- .../container/docker-container-client.ts | 70 ++++---- .../docker-compose-environment.ts | 157 +++++++++++------- .../generic-container/generic-container.ts | 73 ++++---- .../started-generic-container.ts | 2 +- .../src/wait-strategies/http-wait-strategy.ts | 50 +++--- 12 files changed, 309 insertions(+), 189 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 32afa6fdf..5b7038b04 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,7 @@ If a skill or this file turns out to be wrong or incomplete, update it in the sa - Import boundaries are enforced by Biome (`biome.json`): - Modules import core only from the `testcontainers` entry point. The one allowed exception is the core test helper (`../../../testcontainers/src/utils/test-helper`). - `noImportCycles` is an error. Prefer breaking a new cycle (for example with `import type` or a leaf module) over adding a `biome-ignore` suppression. +- Biome also enforces a 50-line limit on non-test functions, `??` over `||` for defaults, and no `console` in package source. Fix a violation rather than suppressing it. ### Dependencies diff --git a/biome.json b/biome.json index 5cd43e809..016d92450 100644 --- a/biome.json +++ b/biome.json @@ -23,11 +23,20 @@ "enabled": true, "rules": { "preset": "recommended", + "complexity": { + "noExcessiveLinesPerFunction": { + "level": "error", + "options": { + "maxLines": 50 + } + } + }, "correctness": { "noUndeclaredDependencies": "error" }, "nursery": { - "noFloatingPromises": "error" + "noFloatingPromises": "error", + "useNullishCoalescing": "error" }, "suspicious": { "noImportCycles": "error" @@ -39,12 +48,38 @@ "includes": ["**/*.test.ts"], "linter": { "rules": { + "complexity": { + "noExcessiveLinesPerFunction": "off" + }, "correctness": { "noUndeclaredDependencies": "off" } } } }, + { + "includes": ["packages/**/src/**", "!packages/testcontainers/src/common/logger.ts"], + "linter": { + "rules": { + "suspicious": { + "noConsole": "error" + } + } + } + }, + { + "includes": ["packages/modules/couchbase/src/couchbase-container.ts"], + "linter": { + "rules": { + "complexity": { + "noExcessiveLinesPerFunction": "off" + }, + "nursery": { + "useNullishCoalescing": "off" + } + } + } + }, { "includes": ["packages/modules/**"], "linter": { diff --git a/packages/modules/azurite/src/azurite-container.ts b/packages/modules/azurite/src/azurite-container.ts index 2a6e98612..d9cc78c83 100755 --- a/packages/modules/azurite/src/azurite-container.ts +++ b/packages/modules/azurite/src/azurite-container.ts @@ -159,6 +159,31 @@ export class AzuriteContainer extends GenericContainer { } public override async start(): Promise { + const command = this.createCommand(); + + this.withCommand(command).withExposedPorts(this.blobPort, this.queuePort, this.tablePort); + + if (this.accountName !== DEFAULT_ACCOUNT_NAME || this.accountKey !== DEFAULT_ACCOUNT_KEY) { + this.withEnvironment({ + AZURITE_ACCOUNTS: `${this.accountName}:${this.accountKey}`, + }); + } + + const protocol: Protocol = this.cert === undefined ? "http" : "https"; + const startedContainer = await super.start(); + + return new StartedAzuriteContainer( + startedContainer, + this.accountName, + this.accountKey, + this.blobPort, + this.queuePort, + this.tablePort, + protocol + ); + } + + private createCommand(): string[] { const command = [ "--blobHost", "0.0.0.0", @@ -190,6 +215,16 @@ export class AzuriteContainer extends GenericContainer { command.push("--skipApiVersionCheck"); } + this.configureSsl(command); + + if (this.oauthEnabled) { + command.push("--oauth", "basic"); + } + + return command; + } + + private configureSsl(command: string[]): void { if (this.cert !== undefined && this.certPath !== undefined) { const contentsToCopy = [ { @@ -216,31 +251,6 @@ export class AzuriteContainer extends GenericContainer { command.push("--pwd", this.password); } } - - if (this.oauthEnabled) { - command.push("--oauth", "basic"); - } - - this.withCommand(command).withExposedPorts(this.blobPort, this.queuePort, this.tablePort); - - if (this.accountName !== DEFAULT_ACCOUNT_NAME || this.accountKey !== DEFAULT_ACCOUNT_KEY) { - this.withEnvironment({ - AZURITE_ACCOUNTS: `${this.accountName}:${this.accountKey}`, - }); - } - - const protocol: Protocol = this.cert === undefined ? "http" : "https"; - const startedContainer = await super.start(); - - return new StartedAzuriteContainer( - startedContainer, - this.accountName, - this.accountKey, - this.blobPort, - this.queuePort, - this.tablePort, - protocol - ); } } diff --git a/packages/modules/gcloud/src/cloudstorage-emulator-container.ts b/packages/modules/gcloud/src/cloudstorage-emulator-container.ts index 9b857b07a..070f2e705 100644 --- a/packages/modules/gcloud/src/cloudstorage-emulator-container.ts +++ b/packages/modules/gcloud/src/cloudstorage-emulator-container.ts @@ -1,5 +1,5 @@ import type { StartedTestContainer } from "testcontainers"; -import { AbstractStartedContainer, GenericContainer, Wait } from "testcontainers"; +import { AbstractStartedContainer, GenericContainer, log, Wait } from "testcontainers"; const PORT = 4443; @@ -101,7 +101,7 @@ export class StartedCloudStorageEmulatorContainer extends AbstractStartedContain }); if (!response.ok) { - console.warn(`error updating fake-gcs-server with external url, response status code: ${response.status}`); + log.warn(`error updating fake-gcs-server with external url, response status code: ${response.status}`); } } diff --git a/packages/modules/postgresql/src/postgresql-container.ts b/packages/modules/postgresql/src/postgresql-container.ts index 9e7281c0f..4e3f7f622 100755 --- a/packages/modules/postgresql/src/postgresql-container.ts +++ b/packages/modules/postgresql/src/postgresql-container.ts @@ -1,4 +1,4 @@ -import { AbstractStartedContainer, GenericContainer, type StartedTestContainer, Wait } from "testcontainers"; +import { AbstractStartedContainer, GenericContainer, log, type StartedTestContainer, Wait } from "testcontainers"; const POSTGRES_PORT = 5432; const POSTGRES_SSL_PATH = "/tmp/testcontainers-node/postgres"; @@ -251,7 +251,7 @@ export class StartedPostgreSqlContainer extends AbstractStartedContainer { throw new Error(`Command failed with exit code ${result.exitCode}: ${result.output}`); } } catch (error) { - console.error(`Failed to execute command: ${command}`, error); + log.error(`Failed to execute command: ${command}: ${error}`); throw error; } } diff --git a/packages/modules/valkey/src/valkey-container.ts b/packages/modules/valkey/src/valkey-container.ts index 92a922a84..913abf0a2 100644 --- a/packages/modules/valkey/src/valkey-container.ts +++ b/packages/modules/valkey/src/valkey-container.ts @@ -77,7 +77,7 @@ export class ValkeyContainer extends GenericContainer { } private async importInitialData(container: StartedTestContainer) { - const re = await container.exec(`/tmp/import.sh ${this.password || ""}`); + const re = await container.exec(`/tmp/import.sh ${this.password ?? ""}`); if (re.exitCode !== 0 || re.output.includes("ERR")) { throw Error(`Could not import initial data from ${this.initialImportScriptFile}: ${re.output}`); } diff --git a/packages/testcontainers/src/container-runtime/clients/client.ts b/packages/testcontainers/src/container-runtime/clients/client.ts index f9a1a8a3a..4821f393d 100644 --- a/packages/testcontainers/src/container-runtime/clients/client.ts +++ b/packages/testcontainers/src/container-runtime/clients/client.ts @@ -17,7 +17,9 @@ import { DockerImageClient } from "./image/docker-image-client"; import type { ImageClient } from "./image/image-client"; import { DockerNetworkClient } from "./network/docker-network-client"; import type { NetworkClient } from "./network/network-client"; -import type { ContainerRuntimeInfo, Info, NodeInfo } from "./types"; +import type { ContainerRuntimeInfo, HostIp, Info, NodeInfo } from "./types"; + +type DockerodeInfo = Awaited>; export class ContainerRuntimeClient { constructor( @@ -94,12 +96,6 @@ async function initStrategy(strategy: ContainerRuntimeClientStrategy): Promise): Promise { - const execOptions: ExecCreateOptions = { - Cmd: command, - AttachStdout: true, - AttachStderr: true, - }; - - if (opts?.env !== undefined) { - execOptions.Env = Object.entries(opts.env).map(([key, value]) => `${key}=${value}`); - } - if (opts?.workingDir !== undefined) { - execOptions.WorkingDir = opts.workingDir; - } - if (opts?.user !== undefined) { - execOptions.User = opts.user; - } + const execOptions = this.createExecOptions(command, opts); const outputChunks: string[] = []; const stdoutChunks: string[] = []; @@ -259,18 +245,7 @@ export class DockerContainerClient implements ContainerClient { processStream(stdoutStream, stdoutChunks); processStream(stderrStream, stderrChunks); - try { - await new Promise((res, rej) => { - stream.on("end", res); - stream.on("error", rej); - }); - - stdoutStream.end(); - stderrStream.end(); - await Promise.all([finished(stdoutStream), finished(stderrStream)]); - } finally { - stream.destroy(); - } + await this.waitForExecStreamToEnd(stream, stdoutStream, stderrStream); const inspectResult = await exec.inspect(); const exitCode = inspectResult.ExitCode ?? -1; @@ -290,6 +265,45 @@ export class DockerContainerClient implements ContainerClient { } } + private createExecOptions(command: string[], opts?: Partial): ExecCreateOptions { + const execOptions: ExecCreateOptions = { + Cmd: command, + AttachStdout: true, + AttachStderr: true, + }; + + if (opts?.env !== undefined) { + execOptions.Env = Object.entries(opts.env).map(([key, value]) => `${key}=${value}`); + } + if (opts?.workingDir !== undefined) { + execOptions.WorkingDir = opts.workingDir; + } + if (opts?.user !== undefined) { + execOptions.User = opts.user; + } + + return execOptions; + } + + private async waitForExecStreamToEnd( + stream: Duplex, + stdoutStream: PassThrough, + stderrStream: PassThrough + ): Promise { + try { + await new Promise((res, rej) => { + stream.on("end", res); + stream.on("error", rej); + }); + + stdoutStream.end(); + stderrStream.end(); + await Promise.all([finished(stdoutStream), finished(stderrStream)]); + } finally { + stream.destroy(); + } + } + async restart(container: Container, opts?: { timeout: number }): Promise { try { log.debug(`Restarting container...`, { containerId: container.id }); diff --git a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts index 2d22e3f89..c1356bda4 100644 --- a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts +++ b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts @@ -1,6 +1,11 @@ import type { ContainerInfo } from "dockerode"; import { containerLog, log, RandomUuid, type Uuid } from "../common"; -import { type ComposeOptions, getContainerRuntimeClient, parseComposeContainerName } from "../container-runtime"; +import { + type ComposeOptions, + type ContainerRuntimeClient, + getContainerRuntimeClient, + parseComposeContainerName, +} from "../container-runtime"; import { StartedGenericContainer } from "../generic-container/started-generic-container"; import { getReaper } from "../reaper/reaper"; import type { Environment } from "../types"; @@ -119,12 +124,40 @@ export class DockerComposeEnvironment { projectName: this.projectName, }; + const shouldPull = this.pullPolicy.shouldPull(); + const commandOptions = this.createCommandOptions(clientCommandOptions, shouldPull); + const composeOptions = this.createComposeOptions(clientComposeOptions); + + if (shouldPull === true) { + await client.compose.pull(options, services); + } + await client.compose.up( + { + ...options, + commandOptions, + composeOptions, + environment: { ...this.environment }, + }, + services + ); + + const startedGenericContainers = await this.waitForStartedContainers(client, options); + + log.info(`DockerCompose environment started`); + + return new StartedDockerComposeEnvironment(startedGenericContainers, { + ...options, + composeOptions, + environment: this.environment, + }); + } + + private createCommandOptions(clientCommandOptions: string[], shouldPull: boolean | "never"): string[] { const commandOptions = [...clientCommandOptions]; if (this.build) { commandOptions.push("--build"); } - const shouldPull = this.pullPolicy.shouldPull(); if (shouldPull === "never") { commandOptions.push("--pull", "never"); } @@ -132,6 +165,10 @@ export class DockerComposeEnvironment { commandOptions.push("--no-recreate"); } + return commandOptions; + } + + private createComposeOptions(clientComposeOptions: string[]): string[] { const composeOptions = [...clientComposeOptions]; if (this.environmentFile) { composeOptions.push("--env-file", this.environmentFile); @@ -140,19 +177,13 @@ export class DockerComposeEnvironment { composeOptions.push("--profile", profile); }); - if (shouldPull === true) { - await client.compose.pull(options, services); - } - await client.compose.up( - { - ...options, - commandOptions, - composeOptions, - environment: { ...this.environment }, - }, - services - ); + return composeOptions; + } + private async waitForStartedContainers( + client: ContainerRuntimeClient, + options: ComposeOptions + ): Promise<{ [containerName: string]: StartedGenericContainer }> { const startedContainers = (await client.container.list()).filter( (container) => container.Labels["com.docker.compose.project"] === this.projectName ); @@ -168,51 +199,9 @@ export class DockerComposeEnvironment { ); this.warnForUnusedWaitStrategies(startedContainerNameSet); - const startedGenericContainers = ( + return ( await Promise.all( - startedContainers.map(async (startedContainer) => { - const container = client.container.getById(startedContainer.Id); - const containerName = parseComposeContainerName(this.projectName, startedContainer.Names[0]); - - const inspectResult = await client.container.inspect(container); - const mappedInspectResult = mapInspectResult(inspectResult); - const boundPorts = BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, mappedInspectResult); - const waitStrategy = await selectWaitStrategy({ - client, - inspectResult, - waitStrategy: this.waitStrategy[containerName] ?? this.defaultWaitStrategy, - }); - if (this.startupTimeoutMs !== undefined) { - waitStrategy.withStartupTimeout(this.startupTimeoutMs); - } - - if (containerLog.enabled()) { - (await client.container.logs(container)) - .on("data", (data) => containerLog.trace(`${containerName}: ${data.trim()}`)) - .on("err", (data) => containerLog.error(`${containerName}: ${data.trim()}`)); - } - - try { - await waitForContainer(client, container, waitStrategy, boundPorts); - } catch (err) { - try { - await client.compose.down(options, { removeVolumes: true, timeout: 0 }); - } catch { - log.warn(`Failed to stop DockerCompose environment after failed up`); - } - throw err; - } - - return new StartedGenericContainer( - container, - client.info.containerRuntime.host, - inspectResult, - boundPorts, - containerName, - waitStrategy, - true - ); - }) + startedContainers.map((startedContainer) => this.toStartedGenericContainer(client, options, startedContainer)) ) ).reduce<{ [containerName: string]: StartedGenericContainer }>((map, startedGenericContainer) => { // defineProperty rather than assignment: a container named "__proto__" would otherwise @@ -225,14 +214,54 @@ export class DockerComposeEnvironment { }); return map; }, {}); + } - log.info(`DockerCompose environment started`); - - return new StartedDockerComposeEnvironment(startedGenericContainers, { - ...options, - composeOptions, - environment: this.environment, + private async toStartedGenericContainer( + client: ContainerRuntimeClient, + options: ComposeOptions, + startedContainer: ContainerInfo + ): Promise { + const container = client.container.getById(startedContainer.Id); + const containerName = parseComposeContainerName(this.projectName, startedContainer.Names[0]); + + const inspectResult = await client.container.inspect(container); + const mappedInspectResult = mapInspectResult(inspectResult); + const boundPorts = BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, mappedInspectResult); + const waitStrategy = await selectWaitStrategy({ + client, + inspectResult, + waitStrategy: this.waitStrategy[containerName] ?? this.defaultWaitStrategy, }); + if (this.startupTimeoutMs !== undefined) { + waitStrategy.withStartupTimeout(this.startupTimeoutMs); + } + + if (containerLog.enabled()) { + (await client.container.logs(container)) + .on("data", (data) => containerLog.trace(`${containerName}: ${data.trim()}`)) + .on("err", (data) => containerLog.error(`${containerName}: ${data.trim()}`)); + } + + try { + await waitForContainer(client, container, waitStrategy, boundPorts); + } catch (err) { + try { + await client.compose.down(options, { removeVolumes: true, timeout: 0 }); + } catch { + log.warn(`Failed to stop DockerCompose environment after failed up`); + } + throw err; + } + + return new StartedGenericContainer( + container, + client.info.containerRuntime.host, + inspectResult, + boundPorts, + containerName, + waitStrategy, + true + ); } private warnForUnusedWaitStrategies(startedContainerNames: Set): void { diff --git a/packages/testcontainers/src/generic-container/generic-container.ts b/packages/testcontainers/src/generic-container/generic-container.ts index d9b2f8ffd..c38bcac44 100644 --- a/packages/testcontainers/src/generic-container/generic-container.ts +++ b/packages/testcontainers/src/generic-container/generic-container.ts @@ -199,26 +199,8 @@ export class GenericContainer implements TestContainer { private async startContainer(client: ContainerRuntimeClient): Promise { const container = await client.container.create({ ...this.createOpts, HostConfig: this.hostConfig }); - if (!this.isHelperContainer() && PortForwarderInstance.isRunning()) { - await this.connectContainerToPortForwarder(client, container); - } - - if (this.networkMode && this.networkAliases.length > 0) { - const network = client.network.getById(this.networkMode); - await client.container.connectToNetwork(container, network, this.networkAliases); - } - - if (this.filesToCopy.length > 0 || this.directoriesToCopy.length > 0 || this.contentsToCopy.length > 0) { - const archive = await this.createArchiveToCopyToContainer(); - await Promise.all([ - archive.finalize(), - client.container.putArchive(container, archive, "/", this.copyToContainerOptions), - ]); - } - - for (const archive of this.archivesToCopy) { - await client.container.putArchive(container, archive.tar, archive.target, this.copyToContainerOptions); - } + await this.connectContainerToNetworks(client, container); + await this.copyToCreatedContainer(client, container); log.info(`Starting container for image "${this.createOpts.Image}"...`, { containerId: container.id }); if (this.containerCreated) { @@ -237,17 +219,7 @@ export class GenericContainer implements TestContainer { this.exposedPorts ); - if (containerLog.enabled() || this.logConsumer !== undefined) { - if (this.logConsumer !== undefined) { - this.logConsumer(await client.container.logs(container)); - } - - if (containerLog.enabled()) { - (await client.container.logs(container)) - .on("data", (data) => containerLog.trace(data.trim(), { containerId: container.id })) - .on("err", (data) => containerLog.error(data.trim(), { containerId: container.id })); - } - } + await this.consumeContainerLogs(client, container); if (this.containerStarting) { await this.containerStarting(mappedInspectResult, false); @@ -277,6 +249,17 @@ export class GenericContainer implements TestContainer { return startedContainer; } + private async connectContainerToNetworks(client: ContainerRuntimeClient, container: Container): Promise { + if (!this.isHelperContainer() && PortForwarderInstance.isRunning()) { + await this.connectContainerToPortForwarder(client, container); + } + + if (this.networkMode && this.networkAliases.length > 0) { + const network = client.network.getById(this.networkMode); + await client.container.connectToNetwork(container, network, this.networkAliases); + } + } + private async connectContainerToPortForwarder(client: ContainerRuntimeClient, container: Container) { const portForwarder = await PortForwarderInstance.getInstance(); const portForwarderNetworkId = portForwarder.getNetworkId(); @@ -288,6 +271,20 @@ export class GenericContainer implements TestContainer { } } + private async copyToCreatedContainer(client: ContainerRuntimeClient, container: Container): Promise { + if (this.filesToCopy.length > 0 || this.directoriesToCopy.length > 0 || this.contentsToCopy.length > 0) { + const archive = await this.createArchiveToCopyToContainer(); + await Promise.all([ + archive.finalize(), + client.container.putArchive(container, archive, "/", this.copyToContainerOptions), + ]); + } + + for (const archive of this.archivesToCopy) { + await client.container.putArchive(container, archive.tar, archive.target, this.copyToContainerOptions); + } + } + private async createArchiveToCopyToContainer(): Promise { const tar = archiver("tar"); const filesToCopyWithStats = await Promise.all( @@ -310,6 +307,20 @@ export class GenericContainer implements TestContainer { return tar; } + private async consumeContainerLogs(client: ContainerRuntimeClient, container: Container): Promise { + if (containerLog.enabled() || this.logConsumer !== undefined) { + if (this.logConsumer !== undefined) { + this.logConsumer(await client.container.logs(container)); + } + + if (containerLog.enabled()) { + (await client.container.logs(container)) + .on("data", (data) => containerLog.trace(data.trim(), { containerId: container.id })) + .on("err", (data) => containerLog.error(data.trim(), { containerId: container.id })); + } + } + } + protected containerStarted?( container: StartedTestContainer, inspectResult: InspectResult, diff --git a/packages/testcontainers/src/generic-container/started-generic-container.ts b/packages/testcontainers/src/generic-container/started-generic-container.ts index fa23559ad..6539caf40 100644 --- a/packages/testcontainers/src/generic-container/started-generic-container.ts +++ b/packages/testcontainers/src/generic-container/started-generic-container.ts @@ -63,7 +63,7 @@ export class StartedGenericContainer implements StartedTestContainer { client: ContainerRuntimeClient; }): Promise { const { deleteOnExit, client } = options; - const changes = options.changes || []; + const changes = options.changes ?? []; if (deleteOnExit) { let sessionId = this.getLabels()[LABEL_TESTCONTAINERS_SESSION_ID]; if (!sessionId) { diff --git a/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts b/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts index b707ba564..a4eecc633 100644 --- a/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts @@ -100,14 +100,7 @@ export class HttpWaitStrategy extends AbstractWaitStrategy { } } - return undiciResponseToFetchResponse( - await request(url, { - method: this.method, - signal: AbortSignal.timeout(this.readTimeoutMs), - headers: this.headers, - dispatcher: agent, - }) - ); + return await this.sendRequest(url, agent); } catch { return undefined; } @@ -117,19 +110,7 @@ export class HttpWaitStrategy extends AbstractWaitStrategy { return true; } - if (response === undefined) { - return false; - } else if (!this.predicates.length) { - return response.ok; - } else { - for (const predicate of this.predicates) { - const result = await predicate(response); - if (!result) { - return false; - } - } - return true; - } + return this.matchesPredicates(response); }, () => { const message = `URL ${this.path} not accessible after ${this.startupTimeoutMs}ms`; @@ -151,6 +132,33 @@ export class HttpWaitStrategy extends AbstractWaitStrategy { log.debug(`HTTP wait strategy complete`, { containerId: container.id }); } + private async sendRequest(url: string, agent: Agent | undefined): Promise { + return undiciResponseToFetchResponse( + await request(url, { + method: this.method, + signal: AbortSignal.timeout(this.readTimeoutMs), + headers: this.headers, + dispatcher: agent, + }) + ); + } + + private async matchesPredicates(response: Response | undefined): Promise { + if (response === undefined) { + return false; + } else if (!this.predicates.length) { + return response.ok; + } else { + for (const predicate of this.predicates) { + const result = await predicate(response); + if (!result) { + return false; + } + } + return true; + } + } + private async handleContainerExit(container: Dockerode.Container) { const tail = 50; const lastLogs: string[] = []; From a54d4f08fa55a61ecf0104339755d7cd7fc5afa1 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Mon, 12 Oct 2026 19:27:35 +0100 Subject: [PATCH 2/4] Remove the Couchbase override and split createBuckets --- biome.json | 13 - .../couchbase/src/couchbase-container.ts | 358 ++++++++++-------- 2 files changed, 200 insertions(+), 171 deletions(-) diff --git a/biome.json b/biome.json index 016d92450..44bb21126 100644 --- a/biome.json +++ b/biome.json @@ -67,19 +67,6 @@ } } }, - { - "includes": ["packages/modules/couchbase/src/couchbase-container.ts"], - "linter": { - "rules": { - "complexity": { - "noExcessiveLinesPerFunction": "off" - }, - "nursery": { - "useNullishCoalescing": "off" - } - } - } - }, { "includes": ["packages/modules/**"], "linter": { diff --git a/packages/modules/couchbase/src/couchbase-container.ts b/packages/modules/couchbase/src/couchbase-container.ts index 6b1232f58..fff37c978 100644 --- a/packages/modules/couchbase/src/couchbase-container.ts +++ b/packages/modules/couchbase/src/couchbase-container.ts @@ -387,121 +387,131 @@ export class CouchbaseContainer extends GenericContainer { for (const bucket of this.buckets) { log.debug(`Creating bucket ${bucket.getName()}`); - const body = new URLSearchParams(); - body.set("name", bucket.getName()); - body.set("ramQuotaMB", bucket.getQuota().toString()); - body.set("flushEnabled", bucket.hasFlushEnabled() ? "1" : "0"); - body.set("replicaNumber", bucket.getNumOfReplicas().toString()); - - const response = await this.doHttpRequest( - startedTestContainer.getHost(), - startedTestContainer.getMappedPort(PORTS.MGMT_PORT), - "/pools/default/buckets", - "POST", - body, - true + await this.createBucket(startedTestContainer, bucket); + await this.waitUntilBucketHasEnabledServices( + client, + inspectResult, + startedTestContainer, + bucket, + remainingStartupMs() ); - - await this.checkResponse(response, `Could not create bucket ${bucket.getName()}`); - - await Wait.forHttp(`/pools/default/b/${bucket.getName()}`, PORTS.MGMT_PORT) - .withBasicCredentials(this.username, this.password) - .forStatusCode(200) - .withStartupTimeout(remainingStartupMs()) - .forResponsePredicate((response) => { - try { - const jsonResponse = JSON.parse(response); - const services = jsonResponse.nodesExt[0].services; - const serviceNames = Object.keys(services); - - let found = false; - for (const enabledService of this.enabledServices) { - if (serviceNames.some((s) => s.startsWith(enabledService.getIdentifier()))) { - found = true; - } else { - found = false; - log.trace(`Service ${enabledService.getIdentifier()} not yet part of config, retrying.`); - break; - } - } - return found; - } catch (e) { - log.error(`Unable to parse response: ${response} ${e}`); - return false; - } - }) - .waitUntilReady( - client.container.getById(startedTestContainer.getId()), - BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, inspectResult) - ); - - await Wait.forHttp(`/pools/default/buckets/${bucket.getName()}`, PORTS.MGMT_PORT) - .withBasicCredentials(this.username, this.password) - .forStatusCode(200) - .withStartupTimeout(remainingStartupMs()) - .forResponsePredicate((response) => { - try { - const jsonResponse = JSON.parse(response); - return jsonResponse.nodes[0]?.status === "healthy"; - } catch (e) { - log.error(`Unable to parse response: ${response}, error: ${e}`); - return false; - } - }) - .waitUntilReady( - client.container.getById(startedTestContainer.getId()), - BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, inspectResult) - ); + await this.waitUntilBucketIsHealthy(client, inspectResult, startedTestContainer, bucket, remainingStartupMs()); if (this.enabledServices.has(CouchbaseService.QUERY)) { - const timeoutMs = remainingStartupMs(); - - await new IntervalRetry(1000).retryUntil( - async () => { - try { - const body = new URLSearchParams(); - body.set( - "statement", - `SELECT COUNT(*) > 0 as present FROM system:keyspaces WHERE name = "${bucket.getName()}"` - ); - - const response = await this.doHttpRequest( - startedTestContainer.getHost(), - startedTestContainer.getMappedPort(PORTS.QUERY_PORT), - "/query/service", - "POST", - body, - true - ); - - await this.checkResponse(response, `Could not poll query service state for bucket: ${bucket.getName()}`); - - return response; - } catch (_e) { - return undefined; - } - }, - async (response) => { - if (response === undefined) { - return false; - } - const jsonResponse = (await response.json()) as { results: Array<{ present: boolean }> }; - return jsonResponse.results[0].present; - }, - () => { - const message = `URL /query/service not accessible after ${timeoutMs}ms`; - log.error(message, { containerId: client.container.getById(startedTestContainer.getId()).id }); - - throw new Error(message); - }, - timeoutMs - ); + await this.waitUntilKeyspaceIsPresent(client, startedTestContainer, bucket, remainingStartupMs()); } if (bucket.hasPrimaryIndex()) { if (this.enabledServices.has(CouchbaseService.QUERY)) { + await this.createPrimaryIndex(startedTestContainer, bucket); + await this.waitUntilPrimaryIndexIsOnline(client, startedTestContainer, bucket, remainingStartupMs()); + } else { + log.info( + `Primary index creation for bucket ${bucket.getName()} ignored, since QUERY service is not present.` + ); + } + } + } + } + + private async createBucket(startedTestContainer: StartedTestContainer, bucket: BucketDefinition) { + const body = new URLSearchParams(); + body.set("name", bucket.getName()); + body.set("ramQuotaMB", bucket.getQuota().toString()); + body.set("flushEnabled", bucket.hasFlushEnabled() ? "1" : "0"); + body.set("replicaNumber", bucket.getNumOfReplicas().toString()); + + const response = await this.doHttpRequest( + startedTestContainer.getHost(), + startedTestContainer.getMappedPort(PORTS.MGMT_PORT), + "/pools/default/buckets", + "POST", + body, + true + ); + + await this.checkResponse(response, `Could not create bucket ${bucket.getName()}`); + } + + private async waitUntilBucketHasEnabledServices( + client: ContainerRuntimeClient, + inspectResult: InspectResult, + startedTestContainer: StartedTestContainer, + bucket: BucketDefinition, + timeoutMs: number + ) { + await Wait.forHttp(`/pools/default/b/${bucket.getName()}`, PORTS.MGMT_PORT) + .withBasicCredentials(this.username, this.password) + .forStatusCode(200) + .withStartupTimeout(timeoutMs) + .forResponsePredicate((response) => { + try { + const jsonResponse = JSON.parse(response); + const services = jsonResponse.nodesExt[0].services; + const serviceNames = Object.keys(services); + + let found = false; + for (const enabledService of this.enabledServices) { + if (serviceNames.some((s) => s.startsWith(enabledService.getIdentifier()))) { + found = true; + } else { + found = false; + log.trace(`Service ${enabledService.getIdentifier()} not yet part of config, retrying.`); + break; + } + } + return found; + } catch (e) { + log.error(`Unable to parse response: ${response} ${e}`); + return false; + } + }) + .waitUntilReady( + client.container.getById(startedTestContainer.getId()), + BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, inspectResult) + ); + } + + private async waitUntilBucketIsHealthy( + client: ContainerRuntimeClient, + inspectResult: InspectResult, + startedTestContainer: StartedTestContainer, + bucket: BucketDefinition, + timeoutMs: number + ) { + await Wait.forHttp(`/pools/default/buckets/${bucket.getName()}`, PORTS.MGMT_PORT) + .withBasicCredentials(this.username, this.password) + .forStatusCode(200) + .withStartupTimeout(timeoutMs) + .forResponsePredicate((response) => { + try { + const jsonResponse = JSON.parse(response); + return jsonResponse.nodes[0]?.status === "healthy"; + } catch (e) { + log.error(`Unable to parse response: ${response}, error: ${e}`); + return false; + } + }) + .waitUntilReady( + client.container.getById(startedTestContainer.getId()), + BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, inspectResult) + ); + } + + private async waitUntilKeyspaceIsPresent( + client: ContainerRuntimeClient, + startedTestContainer: StartedTestContainer, + bucket: BucketDefinition, + timeoutMs: number + ) { + await new IntervalRetry(1000).retryUntil( + async () => { + try { const body = new URLSearchParams(); - body.set("statement", `CREATE PRIMARY INDEX on \`${bucket.getName()}\``); //TODO: check here! + body.set( + "statement", + `SELECT COUNT(*) > 0 as present FROM system:keyspaces WHERE name = "${bucket.getName()}"` + ); const response = await this.doHttpRequest( startedTestContainer.getHost(), @@ -512,60 +522,92 @@ export class CouchbaseContainer extends GenericContainer { true ); - await this.checkResponse(response, `Could not create primary index for bucket ${bucket.getName()}`); - - const timeoutMs = remainingStartupMs(); - - await new IntervalRetry(1000).retryUntil( - async () => { - try { - const body = new URLSearchParams(); - body.set( - "statement", - `SELECT COUNT(*) > 0 as online FROM system:indexes WHERE keyspace_id = "${bucket.getName()}" and is_primary = true and state = "online"` - ); - - const response = await this.doHttpRequest( - startedTestContainer.getHost(), - startedTestContainer.getMappedPort(PORTS.QUERY_PORT), - "/query/service", - "POST", - body, - true - ); - - await this.checkResponse( - response, - `Could not poll primary index state for bucket: ${bucket.getName()}` - ); - - return response; - } catch (_e) { - return undefined; - } - }, - async (response) => { - if (response === undefined) { - return false; - } - const jsonResponse = (await response.json()) as { results: Array<{ online: boolean }> }; - return jsonResponse.results[0].online; - }, - () => { - const message = `URL /query/service not accessible after ${timeoutMs}ms`; - log.error(message, { containerId: client.container.getById(startedTestContainer.getId()).id }); - - throw new Error(message); - }, - timeoutMs + await this.checkResponse(response, `Could not poll query service state for bucket: ${bucket.getName()}`); + + return response; + } catch (_e) { + return undefined; + } + }, + async (response) => { + if (response === undefined) { + return false; + } + const jsonResponse = (await response.json()) as { results: Array<{ present: boolean }> }; + return jsonResponse.results[0].present; + }, + () => { + const message = `URL /query/service not accessible after ${timeoutMs}ms`; + log.error(message, { containerId: client.container.getById(startedTestContainer.getId()).id }); + + throw new Error(message); + }, + timeoutMs + ); + } + + private async createPrimaryIndex(startedTestContainer: StartedTestContainer, bucket: BucketDefinition) { + const body = new URLSearchParams(); + body.set("statement", `CREATE PRIMARY INDEX on \`${bucket.getName()}\``); //TODO: check here! + + const response = await this.doHttpRequest( + startedTestContainer.getHost(), + startedTestContainer.getMappedPort(PORTS.QUERY_PORT), + "/query/service", + "POST", + body, + true + ); + + await this.checkResponse(response, `Could not create primary index for bucket ${bucket.getName()}`); + } + + private async waitUntilPrimaryIndexIsOnline( + client: ContainerRuntimeClient, + startedTestContainer: StartedTestContainer, + bucket: BucketDefinition, + timeoutMs: number + ) { + await new IntervalRetry(1000).retryUntil( + async () => { + try { + const body = new URLSearchParams(); + body.set( + "statement", + `SELECT COUNT(*) > 0 as online FROM system:indexes WHERE keyspace_id = "${bucket.getName()}" and is_primary = true and state = "online"` ); - } else { - log.info( - `Primary index creation for bucket ${bucket.getName()} ignored, since QUERY service is not present.` + + const response = await this.doHttpRequest( + startedTestContainer.getHost(), + startedTestContainer.getMappedPort(PORTS.QUERY_PORT), + "/query/service", + "POST", + body, + true ); + + await this.checkResponse(response, `Could not poll primary index state for bucket: ${bucket.getName()}`); + + return response; + } catch (_e) { + return undefined; } - } - } + }, + async (response) => { + if (response === undefined) { + return false; + } + const jsonResponse = (await response.json()) as { results: Array<{ online: boolean }> }; + return jsonResponse.results[0].online; + }, + () => { + const message = `URL /query/service not accessible after ${timeoutMs}ms`; + log.error(message, { containerId: client.container.getById(startedTestContainer.getId()).id }); + + throw new Error(message); + }, + timeoutMs + ); } protected override async containerStarted( From f31b1bc4a0da7604fb43cb4638ae613dbb806754 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Mon, 12 Oct 2026 19:39:25 +0100 Subject: [PATCH 3/4] Replace the Biome rule list in AGENTS.md with one line The rules live in biome.json. AGENTS.md keeps only the guidance that config can't express: fix a violation rather than suppressing it. --- AGENTS.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7ad10adb4..43d0826b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,10 +35,7 @@ If a skill or this file turns out to be wrong or incomplete, update it in the sa - Fix the root cause. A change that only makes a failure less likely is not a fix. - Arbitrary delays (`sleep`), repeating a check, or adding retries do not remove a race; they hide it. - For readiness problems, wait on a deterministic signal of the state you need. If no such signal exists, say so rather than shipping a mitigation as a fix. -- Import boundaries are enforced by Biome (`biome.json`): - - Modules import core only from the `testcontainers` entry point. The one allowed exception is the core test helper (`../../../testcontainers/src/utils/test-helper`). - - `noImportCycles` is an error. Prefer breaking a new cycle (for example with `import type` or a leaf module) over adding a `biome-ignore` suppression. -- Biome also enforces a 50-line limit on non-test functions, `??` over `||` for defaults, and no `console` in package source. Fix a violation rather than suppressing it. +- Fix a lint violation rather than suppressing it with `biome-ignore`. ### Dependencies From f1466956e5e79971ae2f4bdaf3aec769abbc30f7 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Mon, 12 Oct 2026 19:52:44 +0100 Subject: [PATCH 4/4] Enforce complexity, shadowing, block statement, useless else and readonly rules with Biome --- biome.json | 9 +++++- .../src/azure-cosmosdb-emulator-container.ts | 6 ++-- .../modules/azurite/src/azurite-container.ts | 15 ++++----- .../couchbase/src/couchbase-container.ts | 24 ++++++++------ .../cloudstorage-emulator-container.test.ts | 12 +++++-- .../gcloud/src/emulator-flags-manager.ts | 11 +++++-- .../gcloud/src/spanner-emulator-helper.ts | 4 +-- .../hivemq/src/hivemq-container.test.ts | 2 +- .../kurrentdb/src/kurrentdb-container.test.ts | 4 +-- .../src/mongodb-atlas-local-container.ts | 8 +++-- .../modules/mongodb/src/mongodb-container.ts | 32 +++++++++++++------ .../mosquitto/src/mosquitto-container.test.ts | 4 +-- .../mosquitto/src/mosquitto-container.ts | 8 +++-- .../mssqlserver/src/mssqlserver-container.ts | 2 +- packages/modules/nats/src/nats-container.ts | 7 ++-- .../postgresql/src/postgresql-container.ts | 16 +++++++--- packages/modules/redis/src/redis-container.ts | 3 +- .../toxiproxy/src/toxiproxy-container.ts | 4 +-- .../testcontainers/src/common/retry.test.ts | 6 ++-- .../auth/credential-provider.test.ts | 4 +-- .../container-runtime/auth/get-auth-config.ts | 10 +++--- .../compose/parse-compose-container-name.ts | 10 +++--- .../container/docker-container-client.ts | 9 +++--- .../src/container-runtime/image-name.ts | 28 +++++++++------- .../strategies/configuration-strategy.test.ts | 12 +++++-- .../rootless-unix-socket-strategy.ts | 3 +- .../utils/lookup-host-ips.ts | 3 +- .../docker-compose-environment.test.ts | 4 +-- .../docker-compose-environment.ts | 2 +- .../generic-container-logs.test.ts | 2 +- .../generic-container.test.ts | 2 +- .../inspect-container-util-ports-exposed.ts | 4 ++- .../src/port-forwarder/port-forwarder.ts | 2 +- packages/testcontainers/src/reaper/reaper.ts | 19 ++++++----- .../src/socat/socat-container.ts | 2 +- .../testcontainers/src/utils/bound-ports.ts | 3 +- .../src/utils/map-inspect-result.ts | 3 +- packages/testcontainers/src/utils/port.ts | 12 +++---- .../testcontainers/src/utils/test-helper.ts | 6 ++-- .../src/wait-strategies/http-wait-strategy.ts | 16 +++++----- .../src/wait-strategies/log-wait-strategy.ts | 3 +- .../startup-check-strategy.test.ts | 3 +- .../wait-strategies/startup-check-strategy.ts | 5 +-- .../utils/wait-strategy-selector.ts | 20 +++++++++--- 44 files changed, 214 insertions(+), 150 deletions(-) diff --git a/biome.json b/biome.json index 44bb21126..0dff2a5b9 100644 --- a/biome.json +++ b/biome.json @@ -24,6 +24,7 @@ "rules": { "preset": "recommended", "complexity": { + "noExcessiveCognitiveComplexity": "error", "noExcessiveLinesPerFunction": { "level": "error", "options": { @@ -38,8 +39,14 @@ "noFloatingPromises": "error", "useNullishCoalescing": "error" }, + "style": { + "noUselessElse": "error", + "useBlockStatements": "error", + "useReadonlyClassProperties": "error" + }, "suspicious": { - "noImportCycles": "error" + "noImportCycles": "error", + "noShadow": "error" } } }, diff --git a/packages/modules/azurecosmosdb/src/azure-cosmosdb-emulator-container.ts b/packages/modules/azurecosmosdb/src/azure-cosmosdb-emulator-container.ts index df405de7d..db706a50c 100644 --- a/packages/modules/azurecosmosdb/src/azure-cosmosdb-emulator-container.ts +++ b/packages/modules/azurecosmosdb/src/azure-cosmosdb-emulator-container.ts @@ -17,11 +17,11 @@ const DEFAULT_EXPLORER_ENABLED = false; const COSMOS_READY_LOG_MESSAGE = "Now listening on: "; export class AzureCosmosDbEmulatorContainer extends GenericContainer { - private key = DEFAULT_KEY; + private readonly key = DEFAULT_KEY; private protocol: Protocol = DEFAULT_PROTOCOL; private telemetryEnabled = DEFAULT_TELEMETRY_ENABLED; - private explorerEnabled = DEFAULT_EXPLORER_ENABLED; - private portGenerator: PortGenerator; + private readonly explorerEnabled = DEFAULT_EXPLORER_ENABLED; + private readonly portGenerator: PortGenerator; constructor(image: string) { super(image); diff --git a/packages/modules/azurite/src/azurite-container.ts b/packages/modules/azurite/src/azurite-container.ts index d9cc78c83..66d015d81 100755 --- a/packages/modules/azurite/src/azurite-container.ts +++ b/packages/modules/azurite/src/azurite-container.ts @@ -278,28 +278,25 @@ export class StartedAzuriteContainer extends AbstractStartedContainer { public getBlobPort(): number { if (hasHostBinding(this.blobPort)) { return this.blobPort.host; - } else { - const containerPort = getContainerPort(this.blobPort); - return this.getMappedPort(containerPort); } + const containerPort = getContainerPort(this.blobPort); + return this.getMappedPort(containerPort); } public getQueuePort(): number { if (hasHostBinding(this.queuePort)) { return this.queuePort.host; - } else { - const containerPort = getContainerPort(this.queuePort); - return this.getMappedPort(containerPort); } + const containerPort = getContainerPort(this.queuePort); + return this.getMappedPort(containerPort); } public getTablePort(): number { if (hasHostBinding(this.tablePort)) { return this.tablePort.host; - } else { - const containerPort = getContainerPort(this.tablePort); - return this.getMappedPort(containerPort); } + const containerPort = getContainerPort(this.tablePort); + return this.getMappedPort(containerPort); } public getBlobEndpoint(): string { diff --git a/packages/modules/couchbase/src/couchbase-container.ts b/packages/modules/couchbase/src/couchbase-container.ts index fff37c978..c7db831c7 100644 --- a/packages/modules/couchbase/src/couchbase-container.ts +++ b/packages/modules/couchbase/src/couchbase-container.ts @@ -311,6 +311,20 @@ export class CouchbaseContainer extends GenericContainer { body.set("mgmtSSL", container.getMappedPort(PORTS.MGMT_SSL_PORT).toString()); } + this.setServicePorts(body, container); + + const response = await this.doHttpRequest( + container.getHost(), + container.getMappedPort(PORTS.MGMT_PORT), + "/node/controller/setupAlternateAddresses/external", + "PUT", + body, + true + ); + await this.checkResponse(response, "Could not configure external ports"); + } + + private setServicePorts(body: URLSearchParams, container: StartedTestContainer) { if (this.enabledServices.has(CouchbaseService.KV)) { body.set("kv", container.getMappedPort(PORTS.KV_PORT).toString()); body.set("capi", container.getMappedPort(PORTS.VIEW_PORT).toString()); @@ -348,16 +362,6 @@ export class CouchbaseContainer extends GenericContainer { body.set("eventingSSL", container.getMappedPort(PORTS.EVENTING_SSL_PORT).toString()); } } - - const response = await this.doHttpRequest( - container.getHost(), - container.getMappedPort(PORTS.MGMT_PORT), - "/node/controller/setupAlternateAddresses/external", - "PUT", - body, - true - ); - await this.checkResponse(response, "Could not configure external ports"); } private async configureIndexer(container: StartedTestContainer) { diff --git a/packages/modules/gcloud/src/cloudstorage-emulator-container.test.ts b/packages/modules/gcloud/src/cloudstorage-emulator-container.test.ts index e9c5934b5..07a14e3f0 100644 --- a/packages/modules/gcloud/src/cloudstorage-emulator-container.test.ts +++ b/packages/modules/gcloud/src/cloudstorage-emulator-container.test.ts @@ -15,7 +15,9 @@ async function getRequestBodyFromReadableStream(stream: ReadableStream { - if (request.url.includes("/_internal/config")) configUpdated = true; + if (request.url.includes("/_internal/config")) { + configUpdated = true; + } }); await using container = await new CloudStorageEmulatorContainer(IMAGE).start(); @@ -126,7 +130,9 @@ describe("CloudStorageEmulatorContainer", { concurrent: false, timeout: 240_000 let configUpdated = false; server.events.on("request:start", ({ request }) => { - if (request.url.includes("/_internal/config")) configUpdated = true; + if (request.url.includes("/_internal/config")) { + configUpdated = true; + } }); await using container = await new CloudStorageEmulatorContainer(IMAGE).withAutoUpdateExternalUrl(false).start(); diff --git a/packages/modules/gcloud/src/emulator-flags-manager.ts b/packages/modules/gcloud/src/emulator-flags-manager.ts index cc7a43d2b..e0cbe6f47 100644 --- a/packages/modules/gcloud/src/emulator-flags-manager.ts +++ b/packages/modules/gcloud/src/emulator-flags-manager.ts @@ -9,9 +9,14 @@ export class EmulatorFlagsManager { * @returns this instance for chaining. */ public withFlag(name: string, value: string): this { - if (!name) throw new Error("Flag name must be set."); - if (name.startsWith("--")) this.flags[name] = value; - else this.flags[`--${name}`] = value; + if (!name) { + throw new Error("Flag name must be set."); + } + if (name.startsWith("--")) { + this.flags[name] = value; + } else { + this.flags[`--${name}`] = value; + } return this; } diff --git a/packages/modules/gcloud/src/spanner-emulator-helper.ts b/packages/modules/gcloud/src/spanner-emulator-helper.ts index 7d9161df9..d4de789a7 100644 --- a/packages/modules/gcloud/src/spanner-emulator-helper.ts +++ b/packages/modules/gcloud/src/spanner-emulator-helper.ts @@ -18,8 +18,8 @@ export class SpannerEmulatorHelper { * Lazily get or create the Spanner client. */ public async client(): Promise { - const { Spanner } = await import("@google-cloud/spanner"); - this.clientInstance ??= new Spanner({ + const { Spanner: SpannerClient } = await import("@google-cloud/spanner"); + this.clientInstance ??= new SpannerClient({ projectId: this.emulator.getProjectId(), apiEndpoint: this.emulator.getHost(), port: this.emulator.getGrpcPort(), diff --git a/packages/modules/hivemq/src/hivemq-container.test.ts b/packages/modules/hivemq/src/hivemq-container.test.ts index 3d6530f71..132527f3c 100644 --- a/packages/modules/hivemq/src/hivemq-container.test.ts +++ b/packages/modules/hivemq/src/hivemq-container.test.ts @@ -13,7 +13,7 @@ describe("HiveMQContainer", { timeout: 240_000 }, () => { const mqttClient = await mqtt.connectAsync(container.getConnectionString()); const firstMessagePromise = new Promise<{ topic: string; message: Buffer }>((resolve, reject) => { - mqttClient.once("message", (topic, message) => resolve({ topic, message })); + mqttClient.once("message", (topic, payload) => resolve({ topic, message: payload })); mqttClient.once("error", (err) => reject(err)); }); diff --git a/packages/modules/kurrentdb/src/kurrentdb-container.test.ts b/packages/modules/kurrentdb/src/kurrentdb-container.test.ts index 695d12b7f..97b9a5af3 100644 --- a/packages/modules/kurrentdb/src/kurrentdb-container.test.ts +++ b/packages/modules/kurrentdb/src/kurrentdb-container.test.ts @@ -89,8 +89,8 @@ describe("KurrentDbContainer", { concurrent: false, timeout: 240_000 }, () => { await stream.unsubscribe(); await client.dispose(); - async function getStreamFirstEvent(stream: StreamSubscription): Promise { - for await (const event of stream) { + async function getStreamFirstEvent(subscription: StreamSubscription): Promise { + for await (const event of subscription) { return event; } } diff --git a/packages/modules/mongodb/src/mongodb-atlas-local-container.ts b/packages/modules/mongodb/src/mongodb-atlas-local-container.ts index 36031ce85..981c5159f 100644 --- a/packages/modules/mongodb/src/mongodb-atlas-local-container.ts +++ b/packages/modules/mongodb/src/mongodb-atlas-local-container.ts @@ -15,13 +15,17 @@ export class MongoDBAtlasLocalContainer extends GenericContainer { } public withUsername(username: string): this { - if (!username) throw new Error("Username should not be empty."); + if (!username) { + throw new Error("Username should not be empty."); + } this.username = username; return this; } public withPassword(password: string): this { - if (!password) throw new Error("Password should not be empty."); + if (!password) { + throw new Error("Password should not be empty."); + } this.password = password; return this; } diff --git a/packages/modules/mongodb/src/mongodb-container.ts b/packages/modules/mongodb/src/mongodb-container.ts index 7b66ff344..5b4f3e60a 100644 --- a/packages/modules/mongodb/src/mongodb-container.ts +++ b/packages/modules/mongodb/src/mongodb-container.ts @@ -13,20 +13,26 @@ export class MongoDBContainer extends GenericContainer { } public withUsername(username: string): this { - if (!username) throw new Error("Username should not be empty."); + if (!username) { + throw new Error("Username should not be empty."); + } this.username = username; return this; } public withPassword(password: string): this { - if (!password) throw new Error("Password should not be empty."); + if (!password) { + throw new Error("Password should not be empty."); + } this.password = password; return this; } public override async start(): Promise { const cmdArgs = ["--replSet", "rs0"]; - if (!this.healthCheck) this.withWaitForRsHealthCheck(); + if (!this.healthCheck) { + this.withWaitForRsHealthCheck(); + } if (this.username && this.password) { cmdArgs.push("--keyFile", "/data/db/key.txt"); this.withEnvironment({ @@ -58,18 +64,23 @@ export class MongoDBContainer extends GenericContainer { private buildMongoEvalCommand(command: string) { const args = []; - if (this.isV5OrLater()) args.push("mongosh"); - else args.push("mongo", "admin"); - if (this.username && this.password) args.push("-u", this.username, "-p", this.password); + if (this.isV5OrLater()) { + args.push("mongosh"); + } else { + args.push("mongo", "admin"); + } + if (this.username && this.password) { + args.push("-u", this.username, "-p", this.password); + } args.push("--quiet", "--eval", command); return args.join(" "); } private getRsInitCmd() { - if (this.isV5OrLater()) + if (this.isV5OrLater()) { return `'try { rs.status(); } catch (e) { rs.initiate(); } while (db.runCommand({isMaster: 1}).ismaster==false) { sleep(100); }'`; - else - return `'try { rs.initiate(); } catch (e) {} while (db.runCommand({isMaster: 1}).ismaster==false) { sleep(100); }'`; + } + return `'try { rs.initiate(); } catch (e) {} while (db.runCommand({isMaster: 1}).ismaster==false) { sleep(100); }'`; } private isV5OrLater() { @@ -92,8 +103,9 @@ export class StartedMongoDBContainer extends AbstractStartedContainer { } public getConnectionString(): string { - if (this.username && this.password) + if (this.username && this.password) { return `mongodb://${this.username}:${this.password}@${this.getHost()}:${this.getMappedPort(MONGODB_PORT)}?authSource=admin`; + } return `mongodb://${this.getHost()}:${this.getMappedPort(MONGODB_PORT)}`; } } diff --git a/packages/modules/mosquitto/src/mosquitto-container.test.ts b/packages/modules/mosquitto/src/mosquitto-container.test.ts index defd8bb58..b241fd402 100644 --- a/packages/modules/mosquitto/src/mosquitto-container.test.ts +++ b/packages/modules/mosquitto/src/mosquitto-container.test.ts @@ -15,7 +15,7 @@ describe("MosquittoContainer", { timeout: 240_000 }, () => { const mqttClient = await mqtt.connectAsync(container.getConnectionString()); const firstMessagePromise = new Promise<{ topic: string; message: Buffer }>((resolve, reject) => { - mqttClient.once("message", (topic, message) => resolve({ topic, message })); + mqttClient.once("message", (topic, payload) => resolve({ topic, message: payload })); mqttClient.once("error", (err) => reject(err)); }); @@ -43,7 +43,7 @@ describe("MosquittoContainer", { timeout: 240_000 }, () => { const mqttClient = await mqtt.connectAsync(container.getConnectionString()); const firstMessagePromise = new Promise<{ topic: string; message: Buffer }>((resolve, reject) => { - mqttClient.once("message", (topic, message) => resolve({ topic, message })); + mqttClient.once("message", (topic, payload) => resolve({ topic, message: payload })); mqttClient.once("error", (err) => reject(err)); }); diff --git a/packages/modules/mosquitto/src/mosquitto-container.ts b/packages/modules/mosquitto/src/mosquitto-container.ts index 371f5ec6c..09f5944e3 100644 --- a/packages/modules/mosquitto/src/mosquitto-container.ts +++ b/packages/modules/mosquitto/src/mosquitto-container.ts @@ -14,13 +14,17 @@ export class MosquittoContainer extends GenericContainer { } public withUsername(username: string): this { - if (!username) throw new Error("Username should not be empty."); + if (!username) { + throw new Error("Username should not be empty."); + } this.username = username; return this; } public withPassword(password: string): this { - if (!password) throw new Error("Password should not be empty."); + if (!password) { + throw new Error("Password should not be empty."); + } this.password = password; return this; } diff --git a/packages/modules/mssqlserver/src/mssqlserver-container.ts b/packages/modules/mssqlserver/src/mssqlserver-container.ts index f5b68f356..1742eb78f 100644 --- a/packages/modules/mssqlserver/src/mssqlserver-container.ts +++ b/packages/modules/mssqlserver/src/mssqlserver-container.ts @@ -4,7 +4,7 @@ const MSSQL_PORT = 1433; export class MSSQLServerContainer extends GenericContainer { private database = "master"; - private username = "sa"; + private readonly username = "sa"; private password = "Passw0rd"; private acceptEula = "N"; private message: string | RegExp = /.*Recovery is complete.*/; diff --git a/packages/modules/nats/src/nats-container.ts b/packages/modules/nats/src/nats-container.ts index e86c8fcfe..6c141775e 100755 --- a/packages/modules/nats/src/nats-container.ts +++ b/packages/modules/nats/src/nats-container.ts @@ -8,8 +8,8 @@ const USER_ARGUMENT_KEY = "--user"; const PASS_ARGUMENT_KEY = "--pass"; export class NatsContainer extends GenericContainer { - private args = new Set(); - private values = new Map(); + private readonly args = new Set(); + private readonly values = new Map(); constructor(image: string) { super(image); @@ -62,9 +62,8 @@ export class NatsContainer extends GenericContainer { if (name.length === 1) { return `-${name}`; - } else { - return `--${name}`; } + return `--${name}`; } public override async start(): Promise { diff --git a/packages/modules/postgresql/src/postgresql-container.ts b/packages/modules/postgresql/src/postgresql-container.ts index 4e3f7f622..469476df5 100755 --- a/packages/modules/postgresql/src/postgresql-container.ts +++ b/packages/modules/postgresql/src/postgresql-container.ts @@ -60,9 +60,15 @@ export class PostgreSqlContainer extends GenericContainer { } public withSSL(certFile: string, keyFile: string, caCertFile?: string): this { - if (!certFile) throw new Error("SSL certificate file should not be empty."); - if (!keyFile) throw new Error("SSL key file should not be empty."); - if (caCertFile !== undefined && !caCertFile) throw new Error("SSL CA certificate file should not be empty."); + if (!certFile) { + throw new Error("SSL certificate file should not be empty."); + } + if (!keyFile) { + throw new Error("SSL key file should not be empty."); + } + if (caCertFile !== undefined && !caCertFile) { + throw new Error("SSL CA certificate file should not be empty."); + } const filesToCopy = [ { source: certFile, target: POSTGRES_SSL_CERT_PATH, mode: 0o600 }, @@ -91,7 +97,9 @@ export class PostgreSqlContainer extends GenericContainer { } public withSSLCert(caCertFile: string, certFile: string, keyFile: string): this { - if (!caCertFile) throw new Error("SSL CA certificate file should not be empty."); + if (!caCertFile) { + throw new Error("SSL CA certificate file should not be empty."); + } return this.withSSL(certFile, keyFile, caCertFile); } diff --git a/packages/modules/redis/src/redis-container.ts b/packages/modules/redis/src/redis-container.ts index 874cb7ce5..2f1a3fe78 100644 --- a/packages/modules/redis/src/redis-container.ts +++ b/packages/modules/redis/src/redis-container.ts @@ -83,8 +83,9 @@ export class RedisContainer extends GenericContainer { private async importInitialData(container: StartedTestContainer) { const re = await container.exec(`/tmp/import.sh ${this.password}`); - if (re.exitCode !== 0 || re.output.includes("ERR")) + if (re.exitCode !== 0 || re.output.includes("ERR")) { throw Error(`Could not import initial data from ${this.initialImportScriptFile}: ${re.output}`); + } } } diff --git a/packages/modules/toxiproxy/src/toxiproxy-container.ts b/packages/modules/toxiproxy/src/toxiproxy-container.ts index 78564cf3d..c7e094354 100644 --- a/packages/modules/toxiproxy/src/toxiproxy-container.ts +++ b/packages/modules/toxiproxy/src/toxiproxy-container.ts @@ -51,8 +51,8 @@ export class StartedToxiProxyContainer extends AbstractStartedContainer { const proxies = await this.client.getAll(); const usedPorts = PORT_ARRAY.reduce( - (acc, port) => { - acc[port] = false; + (acc, candidatePort) => { + acc[candidatePort] = false; return acc; }, {} as Record diff --git a/packages/testcontainers/src/common/retry.test.ts b/packages/testcontainers/src/common/retry.test.ts index 6674e7228..3755aa974 100644 --- a/packages/testcontainers/src/common/retry.test.ts +++ b/packages/testcontainers/src/common/retry.test.ts @@ -7,7 +7,7 @@ describe("Retry", () => { const result = await new IntervalRetry(1).retryUntil( () => fnMock(), - (result) => result, + (value) => value, () => new Error(), 10 ); @@ -21,7 +21,7 @@ describe("Retry", () => { const result = await new IntervalRetry(1).retryUntil( () => fnMock(), - (result) => result, + (value) => value, () => new Error(), 10 ); @@ -36,7 +36,7 @@ describe("Retry", () => { const result = await new IntervalRetry(1).retryUntil( () => fnMock(), - (result) => result, + (value) => value, () => timeoutMock(), 10 ); diff --git a/packages/testcontainers/src/container-runtime/auth/credential-provider.test.ts b/packages/testcontainers/src/container-runtime/auth/credential-provider.test.ts index 41939126d..a7d74c841 100644 --- a/packages/testcontainers/src/container-runtime/auth/credential-provider.test.ts +++ b/packages/testcontainers/src/container-runtime/auth/credential-provider.test.ts @@ -74,9 +74,9 @@ describe("CredentialProvider", { concurrent: false }, () => { }); it("should return undefined when provider name not provided", async () => { - const credentialProvider = new TestCredentialProvider("name", undefined); + const unnamedCredentialProvider = new TestCredentialProvider("name", undefined); - expect(await credentialProvider.getAuthConfig("registry", containerRuntimeConfig)).toBeUndefined(); + expect(await unnamedCredentialProvider.getAuthConfig("registry", containerRuntimeConfig)).toBeUndefined(); }); it("should return undefined when get credentials fails because we lookup optimistically", async () => { diff --git a/packages/testcontainers/src/container-runtime/auth/get-auth-config.ts b/packages/testcontainers/src/container-runtime/auth/get-auth-config.ts index 57ea3bd0a..cdf56c976 100644 --- a/packages/testcontainers/src/container-runtime/auth/get-auth-config.ts +++ b/packages/testcontainers/src/container-runtime/auth/get-auth-config.ts @@ -16,15 +16,15 @@ const dockerConfigFile = path.resolve(dockerConfigLocation, "config.json"); const readDockerConfig = async (): Promise => { if (process.env.DOCKER_AUTH_CONFIG) { return parseDockerConfig(process.env.DOCKER_AUTH_CONFIG); - } else if (existsSync(dockerConfigFile)) { + } + if (existsSync(dockerConfigFile)) { return parseDockerConfig((await readFile(dockerConfigFile)).toString()); - } else { - return Promise.resolve({}); } + return Promise.resolve({}); }; -function parseDockerConfig(dockerConfig: string) { - const object = JSON.parse(dockerConfig); +function parseDockerConfig(json: string) { + const object = JSON.parse(json); return { credsStore: object.credsStore, diff --git a/packages/testcontainers/src/container-runtime/clients/compose/parse-compose-container-name.ts b/packages/testcontainers/src/container-runtime/clients/compose/parse-compose-container-name.ts index fc195acc0..e0cd487f0 100644 --- a/packages/testcontainers/src/container-runtime/clients/compose/parse-compose-container-name.ts +++ b/packages/testcontainers/src/container-runtime/clients/compose/parse-compose-container-name.ts @@ -1,11 +1,11 @@ export function parseComposeContainerName(projectName: string, containerName: string): string { if (containerName.startsWith(`/${projectName}-`)) { return containerName.substring(`/${projectName}-`.length); - } else if (containerName.startsWith("/")) { + } + if (containerName.startsWith("/")) { return containerName.substring(1); - } else { - throw new Error( - `Unable to resolve container name for container name: "${containerName}", project name: "${projectName}"` - ); } + throw new Error( + `Unable to resolve container name for container name: "${containerName}", project name: "${projectName}"` + ); } diff --git a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts index 8ef6c7b7d..bcf21d381 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts @@ -53,10 +53,9 @@ export class DockerContainerClient implements ContainerClient { if (containers.length === 0) { log.debug(`No container found with label "${labelName}=${labelValue}"`); return undefined; - } else { - log.debug(`Fetched container by label "${labelName}=${labelValue}"`); - return this.getById(containers[0].Id); } + log.debug(`Fetched container by label "${labelName}=${labelValue}"`); + return this.getById(containers[0].Id); } catch (err) { log.error(`Failed to fetch container by label "${labelName}=${labelValue}": ${err}`); throw err; @@ -227,8 +226,8 @@ export class DockerContainerClient implements ContainerClient { this.dockerode.modem.demuxStream(stream, stdoutStream, stderrStream); - const processStream = (stream: Readable, chunks: string[]) => { - stream.on("data", (chunk) => { + const processStream = (source: Readable, chunks: string[]) => { + source.on("data", (chunk) => { chunks.push(chunk.toString()); outputChunks.push(chunk.toString()); diff --git a/packages/testcontainers/src/container-runtime/image-name.ts b/packages/testcontainers/src/container-runtime/image-name.ts index a49c7b1a2..53dc8bffc 100644 --- a/packages/testcontainers/src/container-runtime/image-name.ts +++ b/packages/testcontainers/src/container-runtime/image-name.ts @@ -36,26 +36,30 @@ export class ImageName { } log.info(message); } + this.string = this.buildString(); + } + + private buildString(): string { const tagAndDigest = this.digest ? `${this.tag}@${this.digest}` : this.tag; if (this.registry) { if (this.tag.startsWith("sha256:")) { - this.string = `${this.registry}/${this.image}@${this.tag}`; - } else { - this.string = `${this.registry}/${this.image}:${tagAndDigest}`; + return `${this.registry}/${this.image}@${this.tag}`; } - } else if (this.tag === "latest" && ImageName.hexRE.test(this.image)) { + return `${this.registry}/${this.image}:${tagAndDigest}`; + } + if (this.tag === "latest" && ImageName.hexRE.test(this.image)) { // 64 byte hex string. This refers to an image sha256 directly. // Do not put the tag as the docker does not accept it. // It will fail with: // // invalid repository name (), cannot specify 64-byte hexadecimal strings. // - this.string = this.image; - } else if (this.tag.startsWith("sha256:")) { - this.string = `${this.image}@${this.tag}`; - } else { - this.string = `${this.image}:${tagAndDigest}`; + return this.image; } + if (this.tag.startsWith("sha256:")) { + return `${this.image}@${this.tag}`; + } + return `${this.image}:${tagAndDigest}`; } public equals(other: ImageName): boolean { @@ -76,11 +80,11 @@ export class ImageName { if (imageAndTag.includes(":")) { const [image, tag] = imageAndTag.split(":"); return new ImageName(registry, image, tag, digest); - } else if (digest) { + } + if (digest) { return new ImageName(registry, imageAndTag, digest); - } else { - return new ImageName(registry, imageAndTag, "latest"); } + return new ImageName(registry, imageAndTag, "latest"); } private static getRegistry(string: string): string | undefined { diff --git a/packages/testcontainers/src/container-runtime/strategies/configuration-strategy.test.ts b/packages/testcontainers/src/container-runtime/strategies/configuration-strategy.test.ts index a844eb209..e044d8a9e 100644 --- a/packages/testcontainers/src/container-runtime/strategies/configuration-strategy.test.ts +++ b/packages/testcontainers/src/container-runtime/strategies/configuration-strategy.test.ts @@ -69,9 +69,15 @@ describe("ConfigurationStrategy", { concurrent: false }, () => { dockerCertPath: "/certs", }); mockReadFile.mockImplementation(async (path) => { - if (path === "/certs/ca.pem") return "ca"; - if (path === "/certs/cert.pem") return "cert"; - if (path === "/certs/key.pem") return "key"; + if (path === "/certs/ca.pem") { + return "ca"; + } + if (path === "/certs/cert.pem") { + return "cert"; + } + if (path === "/certs/key.pem") { + return "key"; + } throw new Error(`Unexpected path: ${path}`); }); diff --git a/packages/testcontainers/src/container-runtime/strategies/rootless-unix-socket-strategy.ts b/packages/testcontainers/src/container-runtime/strategies/rootless-unix-socket-strategy.ts index 677e61ef1..143c751d1 100644 --- a/packages/testcontainers/src/container-runtime/strategies/rootless-unix-socket-strategy.ts +++ b/packages/testcontainers/src/container-runtime/strategies/rootless-unix-socket-strategy.ts @@ -46,9 +46,8 @@ export class RootlessUnixSocketStrategy implements ContainerRuntimeClientStrateg if (xdgRuntimeDir !== undefined) { return path.join(xdgRuntimeDir, "docker.sock"); - } else { - return undefined; } + return undefined; } private getSocketPathFromHomeRunDir(): string { diff --git a/packages/testcontainers/src/container-runtime/utils/lookup-host-ips.ts b/packages/testcontainers/src/container-runtime/utils/lookup-host-ips.ts index 85283636d..89be6e868 100644 --- a/packages/testcontainers/src/container-runtime/utils/lookup-host-ips.ts +++ b/packages/testcontainers/src/container-runtime/utils/lookup-host-ips.ts @@ -5,7 +5,6 @@ import type { HostIp } from "../clients/types"; export const lookupHostIps = async (host: string): Promise => { if (net.isIP(host) === 0) { return await dnsLookup(host, { all: true }); - } else { - return [{ address: host, family: net.isIP(host) }]; } + return [{ address: host, family: net.isIP(host) }]; }; diff --git a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts index c7980286f..79c117ecd 100644 --- a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts +++ b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts @@ -56,7 +56,7 @@ describe("DockerComposeEnvironment", { timeout: 180_000 }, () => { { await using dockerEventStream = await getDockerEventStream(); const dockerPullEventPromise = waitForDockerEvent(dockerEventStream.events, "pull"); - await using _ = await env.withPullPolicy(PullPolicy.alwaysPull()).up(); + await using _pulled = await env.withPullPolicy(PullPolicy.alwaysPull()).up(); await dockerPullEventPromise; } }); @@ -69,7 +69,7 @@ describe("DockerComposeEnvironment", { timeout: 180_000 }, () => { { await using dockerEventStream = await getDockerEventStream(); const dockerPullEventPromise = waitForDockerEvent(dockerEventStream.events, "pull"); - await using _ = await env.withPullPolicy(PullPolicy.alwaysPull()).up(["service-b"]); + await using _pulled = await env.withPullPolicy(PullPolicy.alwaysPull()).up(["service-b"]); await dockerPullEventPromise; } }); diff --git a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts index c1356bda4..8a6c29b74 100644 --- a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts +++ b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts @@ -30,7 +30,7 @@ export class DockerComposeEnvironment { private environment: Environment = {}; private pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy(); private defaultWaitStrategy: WaitStrategy | undefined; - private waitStrategy: { [containerName: string]: WaitStrategy } = {}; + private readonly waitStrategy: { [containerName: string]: WaitStrategy } = {}; private startupTimeoutMs?: number; private clientOptions: Partial = {}; diff --git a/packages/testcontainers/src/generic-container/generic-container-logs.test.ts b/packages/testcontainers/src/generic-container/generic-container-logs.test.ts index 65b7027d4..05159631a 100644 --- a/packages/testcontainers/src/generic-container/generic-container-logs.test.ts +++ b/packages/testcontainers/src/generic-container/generic-container-logs.test.ts @@ -7,7 +7,7 @@ describe("GenericContainer logs", { timeout: 180_000 }, () => { const line = await new Promise((resolve) => { return new GenericContainer("cristianrgreco/testcontainer:1.1.14") .withExposedPorts(8080) - .withLogConsumer((stream) => stream.on("data", (line) => resolve(line))) + .withLogConsumer((stream) => stream.on("data", (chunk) => resolve(chunk))) .start() .then((startedContainer) => startedContainer.stop()); }); diff --git a/packages/testcontainers/src/generic-container/generic-container.test.ts b/packages/testcontainers/src/generic-container/generic-container.test.ts index eccf94c0b..805de6c97 100644 --- a/packages/testcontainers/src/generic-container/generic-container.test.ts +++ b/packages/testcontainers/src/generic-container/generic-container.test.ts @@ -339,7 +339,7 @@ describe("GenericContainer", { timeout: 180_000 }, () => { { await using dockerEventStream = await getDockerEventStream(); const dockerPullEventPromise = waitForDockerEvent(dockerEventStream.events, "pull"); - await using _ = await container.withPullPolicy(PullPolicy.alwaysPull()).start(); + await using _pulled = await container.withPullPolicy(PullPolicy.alwaysPull()).start(); await dockerPullEventPromise; } }); diff --git a/packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.ts b/packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.ts index 964db47ec..c7d1ba1db 100644 --- a/packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.ts +++ b/packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.ts @@ -10,7 +10,9 @@ export async function inspectContainerUntilPortsExposed( () => inspectFn(), (inspectResult) => { const portBindings = inspectResult?.HostConfig?.PortBindings; - if (!portBindings) return true; + if (!portBindings) { + return true; + } const expectedlyBoundPorts = Object.keys(portBindings); return expectedlyBoundPorts.every((exposedPort) => inspectResult.NetworkSettings.Ports[exposedPort]?.length > 0); }, diff --git a/packages/testcontainers/src/port-forwarder/port-forwarder.ts b/packages/testcontainers/src/port-forwarder/port-forwarder.ts index ba64dc8a8..1a2a070aa 100644 --- a/packages/testcontainers/src/port-forwarder/port-forwarder.ts +++ b/packages/testcontainers/src/port-forwarder/port-forwarder.ts @@ -97,7 +97,7 @@ export class PortForwarderInstance { log.debug(`Reusing existing PortForwarder for session "${sessionId}"...`); const host = client.info.containerRuntime.host; - const port = container.Ports.find((port) => port.PrivatePort === 22)?.PublicPort; + const port = container.Ports.find((mapping) => mapping.PrivatePort === 22)?.PublicPort; if (!port) { throw new Error("Expected PortForwarder to map exposed port 22"); } diff --git a/packages/testcontainers/src/reaper/reaper.ts b/packages/testcontainers/src/reaper/reaper.ts index 99f3ad395..364981bea 100644 --- a/packages/testcontainers/src/reaper/reaper.ts +++ b/packages/testcontainers/src/reaper/reaper.ts @@ -32,7 +32,7 @@ export interface Reaper { } let reaper: Reaper; -let sessionId: string; +let reaperSessionId: string; export async function getReaper(client: ContainerRuntimeClient): Promise { if (reaper) { @@ -44,14 +44,14 @@ export async function getReaper(client: ContainerRuntimeClient): Promise const reaperContainers = await findReaperContainers(client); if (process.env.TESTCONTAINERS_RYUK_DISABLED === "true") { - sessionId = new RandomUuid().nextUuid(); - return new DisabledReaper(sessionId, ""); + reaperSessionId = new RandomUuid().nextUuid(); + return new DisabledReaper(reaperSessionId, ""); } for (const reaperContainer of reaperContainers) { try { - sessionId = reaperContainer.Labels[LABEL_TESTCONTAINERS_SESSION_ID]; - return await useExistingReaper(reaperContainer, sessionId, client.info.containerRuntime.host); + reaperSessionId = reaperContainer.Labels[LABEL_TESTCONTAINERS_SESSION_ID]; + return await useExistingReaper(reaperContainer, reaperSessionId, client.info.containerRuntime.host); } catch (error) { const message = error instanceof Error ? error.message : String(error); log.warn(`Failed to reuse existing Reaper: ${message}. Trying another Reaper...`, { @@ -60,11 +60,11 @@ export async function getReaper(client: ContainerRuntimeClient): Promise } } - sessionId = new RandomUuid().nextUuid(); - return await createNewReaper(sessionId, client.info.containerRuntime.remoteSocketPath); + reaperSessionId = new RandomUuid().nextUuid(); + return await createNewReaper(reaperSessionId, client.info.containerRuntime.remoteSocketPath); }); - reaper.addSession(sessionId); + reaper.addSession(reaperSessionId); return reaper; } @@ -167,9 +167,8 @@ async function connectToReaperSocket(host: string, port: number, containerId: st if (retryResult instanceof Socket) { return retryResult; - } else { - throw retryResult; } + throw retryResult; } class RyukReaper implements Reaper { diff --git a/packages/testcontainers/src/socat/socat-container.ts b/packages/testcontainers/src/socat/socat-container.ts index fa9f7844b..ba76e7559 100644 --- a/packages/testcontainers/src/socat/socat-container.ts +++ b/packages/testcontainers/src/socat/socat-container.ts @@ -3,7 +3,7 @@ import { AbstractStartedContainer } from "../generic-container/abstract-started- import { GenericContainer } from "../generic-container/generic-container"; export class SocatContainer extends GenericContainer { - private targets: { [exposePort: number]: string } = {}; + private readonly targets: { [exposePort: number]: string } = {}; constructor(image = "alpine/socat:1.7.4.3-r0") { super(image); diff --git a/packages/testcontainers/src/utils/bound-ports.ts b/packages/testcontainers/src/utils/bound-ports.ts index 14cba5ac2..0127d2a13 100644 --- a/packages/testcontainers/src/utils/bound-ports.ts +++ b/packages/testcontainers/src/utils/bound-ports.ts @@ -30,9 +30,8 @@ export class BoundPorts { if (!firstBinding) { throw new Error("No port bindings found"); - } else { - return firstBinding; } + return firstBinding; } public setBinding(key: string | number, value: number, protocol: string = "tcp"): void { diff --git a/packages/testcontainers/src/utils/map-inspect-result.ts b/packages/testcontainers/src/utils/map-inspect-result.ts index 378217266..e9bb1d2d6 100644 --- a/packages/testcontainers/src/utils/map-inspect-result.ts +++ b/packages/testcontainers/src/utils/map-inspect-result.ts @@ -44,9 +44,8 @@ function mapHealthCheckStatus(inspectResult: ContainerInspectInfo): HealthCheckS if (status === undefined) { return "none"; - } else { - return status; } + return status; } function mapNetworkSettings(inspectResult: ContainerInspectInfo): { [networkName: string]: NetworkSettings } { diff --git a/packages/testcontainers/src/utils/port.ts b/packages/testcontainers/src/utils/port.ts index dfa50e344..159502367 100644 --- a/packages/testcontainers/src/utils/port.ts +++ b/packages/testcontainers/src/utils/port.ts @@ -11,15 +11,15 @@ const portWithProtocolRegex = RegExp(/^(\d+)(?:\/(udp|tcp))?$/i); export const getContainerPort = (port: PortWithOptionalBinding): number => { if (typeof port === "number") { return port; - } else if (typeof port === "string") { + } + if (typeof port === "string") { const match = portWithProtocolRegex.exec(port); if (match) { return parseInt(match[1], 10); } throw new Error(`Invalid port format: ${port}`); - } else { - return port.container; } + return port.container; }; export const hasHostBinding = (port: PortWithOptionalBinding): port is PortWithBinding => { @@ -29,13 +29,13 @@ export const hasHostBinding = (port: PortWithOptionalBinding): port is PortWithB export const getProtocol = (port: PortWithOptionalBinding): string => { if (typeof port === "number") { return "tcp"; - } else if (typeof port === "string") { + } + if (typeof port === "string") { const match = portWithProtocolRegex.exec(port); if (match?.[2]) { return match[2].toLowerCase(); } return "tcp"; - } else { - return port.protocol ? port.protocol.toLowerCase() : "tcp"; } + return port.protocol ? port.protocol.toLowerCase() : "tcp"; }; diff --git a/packages/testcontainers/src/utils/test-helper.ts b/packages/testcontainers/src/utils/test-helper.ts index 5f6b1baf6..57911b019 100644 --- a/packages/testcontainers/src/utils/test-helper.ts +++ b/packages/testcontainers/src/utils/test-helper.ts @@ -153,12 +153,14 @@ export async function stopStartingContainer(container: GenericContainer, name: s .inspect() .then((i) => i.State.Running) .catch(() => false), - (status) => status, + (running) => running, () => false, 10000 ); - if (!status) throw Error("failed start container"); + if (!status) { + throw Error("failed start container"); + } await client.container.getById(name).stop(); await containerStartPromise; diff --git a/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts b/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts index 1f851024b..d0a8e5332 100644 --- a/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts @@ -147,17 +147,17 @@ export class HttpWaitStrategy extends AbstractWaitStrategy { private async matchesPredicates(response: Response | undefined): Promise { if (response === undefined) { return false; - } else if (!this.predicates.length) { + } + if (!this.predicates.length) { return response.ok; - } else { - for (const predicate of this.predicates) { - const result = await predicate(response); - if (!result) { - return false; - } + } + for (const predicate of this.predicates) { + const result = await predicate(response); + if (!result) { + return false; } - return true; } + return true; } private async handleContainerExit(container: Dockerode.Container) { diff --git a/packages/testcontainers/src/wait-strategies/log-wait-strategy.ts b/packages/testcontainers/src/wait-strategies/log-wait-strategy.ts index 51ca97bed..51af54586 100644 --- a/packages/testcontainers/src/wait-strategies/log-wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/log-wait-strategy.ts @@ -36,9 +36,8 @@ export class LogWaitStrategy extends AbstractWaitStrategy { this.message.lastIndex = 0; } return this.message.test(line); - } else { - return line.includes(this.message); } + return line.includes(this.message); }; let count = 0; diff --git a/packages/testcontainers/src/wait-strategies/startup-check-strategy.test.ts b/packages/testcontainers/src/wait-strategies/startup-check-strategy.test.ts index bd8f3f438..0def1a895 100644 --- a/packages/testcontainers/src/wait-strategies/startup-check-strategy.test.ts +++ b/packages/testcontainers/src/wait-strategies/startup-check-strategy.test.ts @@ -10,9 +10,8 @@ describe("StartupCheckStrategy", { timeout: 180_000 }, () => { if (this.count < 1) { this.count++; return "PENDING"; - } else { - return "SUCCESS"; } + return "SUCCESS"; } })(); diff --git a/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts b/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts index 7ba419516..27341e3b1 100644 --- a/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts @@ -13,7 +13,7 @@ export abstract class StartupCheckStrategy extends AbstractWaitStrategy { const startupStatus = await new IntervalRetry(1000).retryUntil( async () => await this.checkStartupState(client.container.dockerode, container.id), - (startupStatus) => startupStatus === "SUCCESS" || startupStatus === "FAIL", + (status) => status === "SUCCESS" || status === "FAIL", () => { const message = `Container not accessible after ${this.startupTimeoutMs}ms`; log.error(message, { containerId: container.id }); @@ -24,7 +24,8 @@ export abstract class StartupCheckStrategy extends AbstractWaitStrategy { if (startupStatus instanceof Error) { throw startupStatus; - } else if (startupStatus === "FAIL") { + } + if (startupStatus === "FAIL") { throw new Error(`Container failed to start for ${container.id}`); } } diff --git a/packages/testcontainers/src/wait-strategies/utils/wait-strategy-selector.ts b/packages/testcontainers/src/wait-strategies/utils/wait-strategy-selector.ts index 86e1032e2..47369ab9a 100644 --- a/packages/testcontainers/src/wait-strategies/utils/wait-strategy-selector.ts +++ b/packages/testcontainers/src/wait-strategies/utils/wait-strategy-selector.ts @@ -26,11 +26,21 @@ export const selectWaitStrategy = async ({ healthCheck, imageNames = getImageNames(inspectResult), }: WaitStrategySelectorOptions): Promise => { - if (waitStrategy) return waitStrategy; - if (hasHealthCheck(healthCheck)) return Wait.forHealthCheck(); - if (hasDisabledHealthCheckConfig(inspectResult)) return Wait.forListeningPorts(); - if (hasHealthCheckConfig(inspectResult) || hasHealthCheckStatus(inspectResult)) return Wait.forHealthCheck(); - if (await imageHasHealthCheck(client, imageNames)) return Wait.forHealthCheck(); + if (waitStrategy) { + return waitStrategy; + } + if (hasHealthCheck(healthCheck)) { + return Wait.forHealthCheck(); + } + if (hasDisabledHealthCheckConfig(inspectResult)) { + return Wait.forListeningPorts(); + } + if (hasHealthCheckConfig(inspectResult) || hasHealthCheckStatus(inspectResult)) { + return Wait.forHealthCheck(); + } + if (await imageHasHealthCheck(client, imageNames)) { + return Wait.forHealthCheck(); + } return Wait.forListeningPorts(); };