Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ TestContainers (https://github.com/testcontainers/testcontainers-node) serves to
Following tools are required to run the test suite

- [NodeJS](https://nodejs.org/en/) as a runtime environment
- recommended version is 16, in other versions you can get errors like `Unable to detect compiler type`
- recommended version is 22.
- [Node Version Manager (nvm)](https://github.com/nvm-sh/nvm) is recommended optional tool to install & manage multiple Node environments
- [npx](https://github.com/npm/npx) CLI tool used to exeute binaries from project's `node_modules` directly (instead of providing absolute/relative path to the commannds). It is used in multiple build steps.
- [Podman](https://podman.io) | [Docker](https://www.docker.com) as a container runtime used by TestContainers. Note that when using Podman as container runtime you may need to export following environment variables and start podman socket:
Expand Down
28 changes: 28 additions & 0 deletions config/containers/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PullPolicy, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
import { DatabaseConfig } from "../interfaces";
import { handleContainerError } from "../helpers";

export function startDatabaseContainer(
config: DatabaseConfig,
startedContainersMap: Map<string, StartedTestContainer>,
): Promise<StartedTestContainer> {
const containerBuilder = new GenericContainer(config.image)
.withPullPolicy(PullPolicy.alwaysPull())
.withName(config.name)
.withNetworkAliases(config.name)
.withNetworkMode(config.networkName)
.withExposedPorts(config.port)
.withEnvironment(config.environmentProperties)
.withWaitStrategy(Wait.forLogMessage(new RegExp(config.waitLogMessage)));

return containerBuilder
.start()
.then((container) => {
console.log(config.successMessage);
startedContainersMap.set(config.containerMapKey, container);
return container;
})
.catch((err: unknown) => {
throw handleContainerError(err);
});
}
2 changes: 2 additions & 0 deletions config/containers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./database";
export * from "./wildfly";
126 changes: 126 additions & 0 deletions config/containers/wildfly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import axios from "axios";
import { GenericContainer, StartedTestContainer } from "testcontainers";
import { findAPortNotInUse } from "portscanner";
import {
WILDFLY_MANAGEMENT_PORT,
WILDFLY_PORT_RANGE,
DEFAULT_WILDFLY_CONFIG,
WILDFLY_READY_TIMEOUT_MS,
WILDFLY_POLL_INTERVAL_MS,
JBOSS_CLI_PATH,
MANAGEMENT_INTERFACE_ADDRESS,
} from "../../cypress.config";
import { WildflyManagementResponse } from "../interfaces";
import { buildLocalhostUrl } from "../helpers";

export function pollWildflyState(managementApi: string, container: StartedTestContainer): Promise<string> {
const startTime = new Date().getTime();
return new Promise<string>((resolve, reject) => {
const interval = setInterval(() => {
if (new Date().getTime() - startTime > WILDFLY_READY_TIMEOUT_MS) {
clearInterval(interval);
reject(new Error("Timeout waiting for WildFly to start"));
}
axios
.post(managementApi, {
operation: "read-attribute",
name: "server-state",
})
.then((response: WildflyManagementResponse) => {
if (response.data.result === "running") {
clearInterval(interval);
const wildflyServer = buildLocalhostUrl(container.getMappedPort(WILDFLY_MANAGEMENT_PORT));
resolve(wildflyServer);
}
})
.catch(() => {
console.log("WildFly server is not ready yet");
});
}, WILDFLY_POLL_INTERVAL_MS);
});
}

export function executeJBossCLI(
container: StartedTestContainer,
managementPort: number,
command: string,
): Promise<string> {
return container
.exec([
`/bin/sh`,
`-c`,
`${JBOSS_CLI_PATH} --connect --controller=localhost:${managementPort} --command="${command}"`,
])
.then((result) => result.output);
}

export function configureWildflyNetworkMode(
wildfly: GenericContainer,
configuration: string,
useHostMode: boolean,
networkName?: string,
): Promise<{ portOffset: number }> {
if (useHostMode) {
console.log("host mode");
return findAPortNotInUse(WILDFLY_PORT_RANGE.min, WILDFLY_PORT_RANGE.max).then((freePort) => {
const portOffset = freePort - WILDFLY_PORT_RANGE.min;
wildfly
.withNetworkMode("host")
.withCommand([
"-c",
configuration || DEFAULT_WILDFLY_CONFIG,
`-Djboss.socket.binding.port-offset=${portOffset.toString()}`,
"-Djboss.node.name=localhost",
] as string[]);
return { portOffset };
});
} else {
console.log(`default network mode, network name: ${networkName}`);
wildfly
.withNetworkMode(networkName!)
.withNetworkAliases("wildfly")
.withExposedPorts(WILDFLY_MANAGEMENT_PORT)
.withCommand(["-c", configuration || DEFAULT_WILDFLY_CONFIG] as string[]);
return Promise.resolve({ portOffset: 0 });
}
}

export function configureWildflyPostStart(
container: StartedTestContainer,
halPort: string,
useHostMode: boolean,
managementPort?: number,
): Promise<string> {
if (useHostMode) {
const effectiveManagementPort = managementPort!;
return executeJBossCLI(
container,
effectiveManagementPort,
`/core-service=management/management-interface=http-interface:list-add(name=allowed-origins,value=${buildLocalhostUrl(Number(halPort))}`,
)
.then(() => executeJBossCLI(container, effectiveManagementPort, "reload"))
.then(() => executeJBossCLI(container, effectiveManagementPort, "read-attribute server-state"))
.then((output) => {
if (output.includes("running")) {
return buildLocalhostUrl(effectiveManagementPort);
}
throw new Error("WildFly did not reach running state");
});
} else {
const managementApi = buildLocalhostUrl(container.getMappedPort(WILDFLY_MANAGEMENT_PORT), "/management");

return axios
.post(managementApi, {
operation: "list-add",
address: MANAGEMENT_INTERFACE_ADDRESS,
name: "allowed-origins",
value: buildLocalhostUrl(Number(halPort)),
})
.then(() =>
axios.post(managementApi, {
operation: "reload",
}),
)
.then(() => pollWildflyState(managementApi, container));
}
}
14 changes: 14 additions & 0 deletions config/helpers/container-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { hostname } from "os";
import { WILDFLY_MANAGEMENT_PORT, LOCALHOST_IP } from "../../cypress.config";

export function calculateManagementPort(portOffset: number): number {
return portOffset + WILDFLY_MANAGEMENT_PORT;
}

export function getHostnameMapping(): Array<{ host: string; ipAddress: string }> {
return [{ host: hostname(), ipAddress: LOCALHOST_IP }];
}

export function buildKeycloakStartCommand(port: number): string[] {
return ["start-dev", "--db=dev-mem", `--http-port=${port.toString()}`, "--import-realm"];
}
4 changes: 4 additions & 0 deletions config/helpers/error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function handleContainerError(err: unknown): Error {
console.log(err);
return err instanceof Error ? err : new Error(JSON.stringify(err));
}
3 changes: 3 additions & 0 deletions config/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./error-handler";
export * from "./url-builder";
export * from "./container-helpers";
4 changes: 4 additions & 0 deletions config/helpers/url-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function buildLocalhostUrl(port: number, path?: string): string {
const baseUrl = `http://localhost:${port}`;
return path ? `${baseUrl}${path}` : baseUrl;
}
51 changes: 51 additions & 0 deletions config/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Environment } from "testcontainers/build/types";

export interface DatabaseConfig {
name: string;
image: string;
port: number;
waitLogMessage: string;
environmentProperties: Environment;
networkName: string;
containerMapKey: string;
successMessage: string;
}

export interface WildflyManagementResponse {
data: {
result: string;
};
}

export interface AxiosErrorResponse {
response: {
data: string;
};
}

export interface StartWildflyContainerParams {
name: string;
configuration: string;
useNetworkHostMode?: boolean;
}

export interface StartKeycloakContainerParams {
name: string;
}

export interface StartDatabaseContainerParams {
name: string;
environmentProperties: Environment;
}

export interface ExecuteInContainerParams {
containerName: string;
command: string;
}

export interface ExecuteCliParams {
managementApi: string;
operation: string;
address: string[];
[key: string]: unknown;
}
18 changes: 18 additions & 0 deletions config/tasks/cli-tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import axios from "axios";
import { ExecuteCliParams, AxiosErrorResponse } from "../interfaces";

export function createExecuteCli() {
return ({ managementApi, operation, address, ...args }: ExecuteCliParams) => {
return axios
.post(managementApi, {
operation,
address,
...args,
})
.then((response) => response.data as unknown)
.catch((err: AxiosErrorResponse) => {
console.log(err);
throw new Error(err.response.data);
});
};
}
89 changes: 89 additions & 0 deletions config/tasks/database-tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { StartedTestContainer } from "testcontainers";
import {
DEFAULT_POSTGRES_IMAGE,
DEFAULT_MYSQL_IMAGE,
DEFAULT_MARIADB_IMAGE,
DEFAULT_SQLSERVER_IMAGE,
POSTGRES_PORT,
MYSQL_PORT,
MARIADB_PORT,
SQLSERVER_PORT,
POSTGRES_STARTED_MSG,
MYSQL_STARTED_MSG,
MARIADB_STARTED_MSG,
SQLSERVER_STARTED_MSG,
} from "../../cypress.config";
import { StartDatabaseContainerParams } from "../interfaces";
import { startDatabaseContainer } from "../containers";

export function createPostgresContainer(startedContainers: Map<string, StartedTestContainer>, networkName: string) {
return ({ name, environmentProperties }: StartDatabaseContainerParams) => {
return startDatabaseContainer(
{
name,
image: process.env.POSTGRES_IMAGE || DEFAULT_POSTGRES_IMAGE,
port: POSTGRES_PORT,
waitLogMessage: POSTGRES_STARTED_MSG,
environmentProperties,
networkName,
containerMapKey: "postgres",
successMessage: "PostgreSQL started successfully",
},
startedContainers,
);
};
}

export function createMysqlContainer(startedContainers: Map<string, StartedTestContainer>, networkName: string) {
return ({ name, environmentProperties }: StartDatabaseContainerParams) => {
return startDatabaseContainer(
{
name,
image: process.env.MYSQL_IMAGE || DEFAULT_MYSQL_IMAGE,
port: MYSQL_PORT,
waitLogMessage: MYSQL_STARTED_MSG,
environmentProperties,
networkName,
containerMapKey: "mysql",
successMessage: "MySQL started successfully",
},
startedContainers,
);
};
}

export function createMariadbContainer(startedContainers: Map<string, StartedTestContainer>, networkName: string) {
return ({ name, environmentProperties }: StartDatabaseContainerParams) => {
return startDatabaseContainer(
{
name,
image: process.env.MARIADB_IMAGE || DEFAULT_MARIADB_IMAGE,
port: MARIADB_PORT,
waitLogMessage: MARIADB_STARTED_MSG,
environmentProperties,
networkName,
containerMapKey: "mariadb",
successMessage: "Mariadb started successfully",
},
startedContainers,
);
};
}

export function createSqlserverContainer(startedContainers: Map<string, StartedTestContainer>, networkName: string) {
return ({ name, environmentProperties }: StartDatabaseContainerParams) => {
return startDatabaseContainer(
{
name,
image: process.env.MSSQL_IMAGE || DEFAULT_SQLSERVER_IMAGE,
port: SQLSERVER_PORT,
waitLogMessage: SQLSERVER_STARTED_MSG,
environmentProperties,
networkName,
containerMapKey: "sqlserver",
successMessage: "SQL server started successfully",
},
startedContainers,
);
};
}
4 changes: 4 additions & 0 deletions config/tasks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./wildfly-tasks";
export * from "./keycloak-tasks";
export * from "./database-tasks";
export * from "./cli-tasks";
Loading