Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

feat(testcontainers): Add sticky-testcontainers package #45

Merged
merged 4 commits into from
Dec 2, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
46 changes: 46 additions & 0 deletions packages/sticky-testcontainers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: Test Containers
sections: ['Testcontainers']
---

`@birthdayresearch/sticky-testcontainers`

Providing opinionated containers that follows the `testcontainers-node` Fluent API design.

- `PostgreSqlContainer` - for a Postgres database docker container
- `LocalstackContainer` - for a Localstack cloud docker container
- `RedisContainer` - for a Redis docker container

Example with PostgresSqlContainer:

```ts
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@birthdayresearch/sticky-testcontainers';

let postgres: StartedPostgreSqlContainer;

beforeAll(async () => {
postgres = await new PostgreSqlContainer().start();
});

afterAll(async () => {
await postgres.stop();
});
```

With network:

```ts
import { PostgreSqlContainer, RedisContainer, Network } from '@birthdayresearch/sticky-testcontainers';

beforeAll(async () => {
const network = await new Network().start();
const postgres = await new PostgreSqlContainer().withNetwork(network).start();
const redis = await new RedisContainer().withNetwork(network).start();
});

afterAll(async () => {
await postgres.stop();
await redis.stop();
await network.stop();
});
```
47 changes: 47 additions & 0 deletions packages/sticky-testcontainers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@birthdayresearch/sticky-testcontainers",
"version": "0.0.0",
"main": "./dist/index.js",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"files": [
"dist/**"
],
"scripts": {
"build": "tsc -b ./tsconfig.build.json",
"clean": "rm -rf dist",
"lint": "eslint .",
"test": "jest"
},
"eslintConfig": {
"parserOptions": {
"project": "./tsconfig.json"
},
"extends": [
"@birthdayresearch"
]
},
"jest": {
"displayName": "test",
"preset": "@birthdayresearch/sticky-turbo-jest"
},
"dependencies": {
"testcontainers": "^9.0.1",
"uuid": "^9.0.0"
},
"devDependencies": {
"@birthdayresearch/sticky-turbo-jest": "workspace:*",
"@birthdayresearch/sticky-typescript": "workspace:*",
"@types/uuid": "^8.3.4",
"node-fetch": "2.6.7",
"@types/node-fetch": "2.6.2"
},
"lint-staged": {
"*": [
"prettier --write --ignore-unknown"
],
"*.{js,jsx,ts,tsx}": [
"eslint --fix"
]
}
}
40 changes: 40 additions & 0 deletions packages/sticky-testcontainers/src/GenericContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { GenericContainer as AbstractGenericContainer } from 'testcontainers';
import { AbstractStartedContainer } from 'testcontainers/dist/modules/abstract-started-container';
import { v4 as uuidv4 } from 'uuid';

export type { StartedTestContainer } from 'testcontainers';

export class GenericContainer extends AbstractGenericContainer {
protected override name: string = `artemys-generic-container-${uuidv4()}`;

public override withName(name: string): this {
this.name = name;
return this;
}

constructor(image: string) {
super(image);
this.withName(`${image.replaceAll(/[^a-zA-Z0-9.-]/g, '-')}-${uuidv4()}`);
}
}

export abstract class GenericStartedContainer extends AbstractStartedContainer {
/**
* @return {number} container port
*/
public abstract getContainerPort(): number;

/**
* @return {number} host port
*/
public getHostPort(): number {
return this.getMappedPort(this.getContainerPort());
}

/**
* @return {string} schemaless host address with port
*/
public getHostAddress(): string {
return `${this.getHost()}:${this.getHostPort()}`;
}
}
67 changes: 67 additions & 0 deletions packages/sticky-testcontainers/src/LocalstackContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Wait } from 'testcontainers';

import { GenericContainer, GenericStartedContainer } from './GenericContainer';

const LOCALSTACK_EDGE_PORT = 4566;

export class LocalstackContainer extends GenericContainer {
protected region = 'ap-southeast-1';

protected bindMountHost = '/var/run/docker.sock';

public withRegion(region: string): this {
this.region = region;
return this;
}

public withBindMountHost(bindMountHost: string): this {
this.bindMountHost = bindMountHost;
return this;
}

constructor(image: string = 'localstack/localstack:1.2.0') {
super(image);
}

public async start(): Promise<StartedLocalstackContainer> {
this.withStartupTimeout(120_000)
.withEnvironment({
MAIN_CONTAINER_NAME: this.name,
DEBUG: '1',
DEFAULT_REGION: this.region,
})
.withBindMounts([
{
source: this.bindMountHost,
target: '/var/run/docker.sock',
},
])
.withExposedPorts(...this.getLocalstackServicePorts())
.withHealthCheck({
test: ['CMD-SHELL', `curl -s http://localhost:${LOCALSTACK_EDGE_PORT} || jq .status == 'running'`],
})
.withWaitStrategy(Wait.forHealthCheck());

return new StartedLocalstackContainer(await super.start());
}

/**
* Generates all required services from localstack
* 4510-4559 for all services
* 4566 for localstack edge proxy
* @returns ports that should be exposed
*/
public getLocalstackServicePorts(): number[] {
return [...Array(50).keys()].map((_, i) => 4510 + i).concat(LOCALSTACK_EDGE_PORT);
}
}

export class StartedLocalstackContainer extends GenericStartedContainer {
public getContainerPort(): number {
return LOCALSTACK_EDGE_PORT;
}

public getEndpoint(): string {
return `http://${this.getHostAddress()}`;
}
}
28 changes: 28 additions & 0 deletions packages/sticky-testcontainers/src/LocalstackContainer.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { LocalstackContainer } from './LocalstackContainer';

describe('Localstack Ports', () => {
it('should return numeric value when mapped ports are requested', async () => {
const localstack = new LocalstackContainer();
const startedLocalstack = await localstack.start();

localstack.getLocalstackServicePorts().forEach((port) => {
expect(startedLocalstack.getMappedPort(port)).toEqual(expect.any(Number));
});
});
});

describe('Localstack Custom Env Variables - Region', () => {
it('should default to ap-southeast-1 region', async () => {
const localstack = new LocalstackContainer();
const startedLocalstack = await localstack.start();

expect((await startedLocalstack.exec(['printenv', 'DEFAULT_REGION'])).output.trimEnd()).toEqual('ap-southeast-1');
});

it('should be able to set aws region', async () => {
const localstack = new LocalstackContainer().withRegion('us-west-1');
const startedLocalstack = await localstack.start();

expect((await startedLocalstack.exec(['printenv', 'DEFAULT_REGION'])).output.trimEnd()).toEqual('us-west-1');
});
});
74 changes: 74 additions & 0 deletions packages/sticky-testcontainers/src/PostgreSqlContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { v4 as uuidv4 } from 'uuid';

import { GenericContainer, GenericStartedContainer, StartedTestContainer } from './GenericContainer';

const POSTGRES_PORT = 5432;

export class PostgreSqlContainer extends GenericContainer {
protected database = 'test';

protected username = uuidv4();

protected password = uuidv4();

constructor(image: string = 'postgres:14.5') {
super(image);
this.withExposedPorts(POSTGRES_PORT).withStartupTimeout(120_000);
}

public withDatabase(database: string): this {
this.database = database;
return this;
}

public withUsername(username: string): this {
this.username = username;
return this;
}

public withPassword(password: string): this {
this.password = password;
return this;
}

public async start(): Promise<StartedPostgreSqlContainer> {
this.withEnvironment({
POSTGRES_DB: this.database,
POSTGRES_USER: this.username,
POSTGRES_PASSWORD: this.password,
});

return new StartedPostgreSqlContainer(await super.start(), this.database, this.username, this.password);
}
}

export class StartedPostgreSqlContainer extends GenericStartedContainer {
constructor(
started: StartedTestContainer,
private readonly database: string,
private readonly username: string,
private readonly password: string,
) {
super(started);
}

public getContainerPort(): number {
return POSTGRES_PORT;
}

public getPort(): number {
return this.getMappedPort(this.getContainerPort());
}

public getDatabase(): string {
return this.database;
}

public getUsername(): string {
return this.username;
}

public getPassword(): string {
return this.password;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { PostgreSqlContainer } from './PostgreSqlContainer';

it('should start and stop gracefully', async () => {
const container = new PostgreSqlContainer();
const started = await container.start();
expect(started.getContainerPort()).toStrictEqual(5432);
await started.stop();
});
24 changes: 24 additions & 0 deletions packages/sticky-testcontainers/src/RedisContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { GenericContainer, GenericStartedContainer } from './GenericContainer';

const REDIS_PORT = 6379;

export class RedisContainer extends GenericContainer {
constructor(image: string = 'redis:7.0.4-alpine') {
super(image);
this.withExposedPorts(REDIS_PORT).withStartupTimeout(120_000);
}

public async start(): Promise<StartedRedisContainer> {
return new StartedRedisContainer(await super.start());
}
}

export class StartedRedisContainer extends GenericStartedContainer {
public getContainerPort(): number {
return REDIS_PORT;
}

public getPort(): number {
return this.getHostPort();
}
}
8 changes: 8 additions & 0 deletions packages/sticky-testcontainers/src/RedisContainer.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { RedisContainer } from './RedisContainer';

it('should start and stop gracefully', async () => {
const container = new RedisContainer();
const started = await container.start();
expect(started.getContainerPort()).toStrictEqual(6379);
await started.stop();
});
6 changes: 6 additions & 0 deletions packages/sticky-testcontainers/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './GenericContainer';
export * from './LocalstackContainer';
export * from './PostgreSqlContainer';
export * from './RedisContainer';
export type { StoppedTestContainer } from 'testcontainers';
export { Network, StartedNetwork, StoppedNetwork, Wait } from 'testcontainers';
9 changes: 9 additions & 0 deletions packages/sticky-testcontainers/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["./src"],
"exclude": ["**/*.unit.ts"]
}
8 changes: 8 additions & 0 deletions packages/sticky-testcontainers/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "@birthdayresearch/sticky-typescript/tsconfig.json",
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist"
},
"include": ["src"]
}
Loading