1
27import { GenericContainer, type Container } from "testcontainers";
28
29const PG_USER = "factory";
30const PG_PASSWORD = "factory";
31const PG_DB = "factory";
32const PG_PORT = 5432;
33const REDIS_PORT = 6379;
34
35let postgresPromise: Promise<Container> | null = null;
36let redisPromise: Promise<Container> | null = null;
37let opensearchPromise: Promise<Container> | null = null;
38const OPENSEARCH_PORT = 9200;
39
40
45export function getPostgres(): Promise<Container> {
46 if (!postgresPromise) {
47 postgresPromise = new GenericContainer("postgres:16-alpine")
48 .withEnvironment({
49 POSTGRES_USER: PG_USER,
50 POSTGRES_PASSWORD: PG_PASSWORD,
51 POSTGRES_DB: PG_DB,
52 })
53 .withExposedPorts(PG_PORT)
54 .withHealthCheck({
55 test: ["CMD-SHELL", `pg_isready -U ${PG_USER} -d ${PG_DB}`],
56 interval: 2_000,
57 timeout: 3_000,
58 retries: 30,
59 })
60 .start()
61 .catch((err: unknown) => {
62 postgresPromise = null;
63 throw err;
64 });
65 }
66 return postgresPromise;
67}
68
69
70export function getRedis(): Promise<Container> {
71 if (!redisPromise) {
72 redisPromise = new GenericContainer("redis:7-alpine")
73 .withExposedPorts(REDIS_PORT)
74 .start()
75 .catch((err: unknown) => {
76 redisPromise = null;
77 throw err;
78 });
79 }
80 return redisPromise;
81}
82
83
90export function getOpenSearch(): Promise<Container> {
91 if (!opensearchPromise) {
92 opensearchPromise = new GenericContainer("opensearchproject/opensearch:2")
93 .withEnvironment({
94 "discovery.type": "single-node",
95 "plugins.security.disabled": "true",
96 OPENSEARCH_JAVA_OPTS: "-Xms512m -Xmx512m",
97 })
98 .withExposedPorts(OPENSEARCH_PORT)
99 .withHealthCheck({
100 test: ["CMD-SHELL", `curl -sf http://localhost:${OPENSEARCH_PORT}/_cluster/health || exit 1`],
101 interval: 5_000,
102 timeout: 10_000,
103 retries: 60,
104 })
105 .start()
106 .catch((err: unknown) => {
107 opensearchPromise = null;
108 throw err;
109 });
110 }
111 return opensearchPromise;
112}
113
114
116async function containerHost(c: Container): Promise<string> {
117 return await c.getHost();
118}
119
120
121export async function getDatabaseUrl(): Promise<string> {
122 const pg = await getPostgres();
123 const host = await containerHost(pg);
124 const port = await pg.getMappedPort(PG_PORT);
125 return `postgresql://${PG_USER}:${PG_PASSWORD}@${host}:${port}/${PG_DB}`;
126}
127
128
129export async function getRedisUrl(): Promise<string> {
130 const redis = await getRedis();
131 const host = await containerHost(redis);
132 const port = await redis.getMappedPort(REDIS_PORT);
133 return `redis://${host}:${port}`;
134}
135
136
137export async function getOpenSearchUrl(): Promise<string> {
138 const os = await getOpenSearch();
139 const host = await containerHost(os);
140 const port = await os.getMappedPort(OPENSEARCH_PORT);
141 return `http://${host}:${port}`;
142}
143
144
148export async function stopAllContainers(): Promise<void> {
149 const pg = postgresPromise ? await postgresPromise.catch(() => null) : null;
150 const r = redisPromise ? await redisPromise.catch(() => null) : null;
151 const os = opensearchPromise ? await opensearchPromise.catch(() => null) : null;
152 postgresPromise = null;
153 redisPromise = null;
154 opensearchPromise = null;
155 await pg?.stop().catch(() => {});
156 await r?.stop().catch(() => {});
157 await os?.stop().catch(() => {});
158}
159