From 874f615f62549d5219c2f2ccdaa7a1f39b361d98 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Wed, 9 Jul 2025 10:26:27 +0300 Subject: [PATCH 1/4] feat(card-service): add testcontainers setup --- packages/card-service/jest.config.js | 17 +++- .../card-service/jest.custom-environment.ts | 8 ++ packages/card-service/jest.env.js | 3 + packages/card-service/jest.setup.ts | 90 +++++++++++++++++++ packages/card-service/jest.teardown.js | 13 +++ packages/card-service/package.json | 8 +- packages/card-service/scripts/init.sh | 8 ++ packages/card-service/src/tests/app.ts | 37 ++++++++ 8 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 packages/card-service/jest.custom-environment.ts create mode 100644 packages/card-service/jest.env.js create mode 100644 packages/card-service/jest.setup.ts create mode 100644 packages/card-service/jest.teardown.js create mode 100755 packages/card-service/scripts/init.sh create mode 100644 packages/card-service/src/tests/app.ts diff --git a/packages/card-service/jest.config.js b/packages/card-service/jest.config.js index a2667d704f..cdb688ff95 100644 --- a/packages/card-service/jest.config.js +++ b/packages/card-service/jest.config.js @@ -7,10 +7,23 @@ const packageName = require('./package.json').name module.exports = { ...baseConfig, clearMocks: true, + testTimeout: 30000, roots: [`/packages/${packageName}`], + setupFiles: [`/packages/${packageName}/jest.env.js`], + globalSetup: `/packages/${packageName}/jest.setup.ts`, + globalTeardown: `/packages/${packageName}/jest.teardown.js`, testRegex: `(packages/${packageName}/.*/__tests__/.*|\\.(test|spec))\\.tsx?$`, - moduleDirectories: [`node_modules`, `packages/${packageName}/node_modules`], - modulePaths: [`/packages/${packageName}/src/`], + testEnvironment: `/packages/${packageName}/jest.custom-environment.ts`, + moduleDirectories: [ + `node_modules`, + `packages/${packageName}/node_modules`, + `/node_modules` + ], + modulePaths: [ + `node_modules`, + `/packages/${packageName}/src/`, + `/node_modules` + ], id: packageName, displayName: packageName, rootDir: '../..' diff --git a/packages/card-service/jest.custom-environment.ts b/packages/card-service/jest.custom-environment.ts new file mode 100644 index 0000000000..ae02fa6899 --- /dev/null +++ b/packages/card-service/jest.custom-environment.ts @@ -0,0 +1,8 @@ +import { TestEnvironment } from 'jest-environment-node' + +export default class CustomEnvironment extends TestEnvironment { + constructor(config, context) { + super(config, context) + this.global.nock = nock + } +} diff --git a/packages/card-service/jest.env.js b/packages/card-service/jest.env.js new file mode 100644 index 0000000000..4dc9ca35cb --- /dev/null +++ b/packages/card-service/jest.env.js @@ -0,0 +1,3 @@ +// Jest environment configuration for card-service +process.env.NODE_ENV = 'test' +process.env.LOG_LEVEL = process.env.LOG_LEVEL || 'silent' diff --git a/packages/card-service/jest.setup.ts b/packages/card-service/jest.setup.ts new file mode 100644 index 0000000000..1c08afd1f5 --- /dev/null +++ b/packages/card-service/jest.setup.ts @@ -0,0 +1,90 @@ +import { knex } from 'knex' +import { GenericContainer, Wait } from 'testcontainers' +require('./jest.env') // set environment variables + +const POSTGRES_PORT = 5432 +const REDIS_PORT = 6379 + +const setup = async (globalConfig): Promise => { + const workers = globalConfig.maxWorkers + + const setupDatabase = async () => { + if (!process.env.DATABASE_URL) { + const postgresContainer = await new GenericContainer('postgres:15') + .withExposedPorts(POSTGRES_PORT) + .withBindMounts([ + { + source: __dirname + '/scripts/init.sh', + target: '/docker-entrypoint-initdb.d/init.sh' + } + ]) + .withEnvironment({ + POSTGRES_PASSWORD: 'password' + }) + .withHealthCheck({ + test: ['CMD-SHELL', 'pg_isready -d card_service_testing'], + interval: 10000, + timeout: 5000, + retries: 5 + }) + .withWaitStrategy(Wait.forHealthCheck()) + .start() + + process.env.DATABASE_URL = `postgresql://postgres:password@localhost:${postgresContainer.getMappedPort( + POSTGRES_PORT + )}/card_service_testing` + + global.__CARD_SERVICE_POSTGRES__ = postgresContainer + } + + const db = knex({ + client: 'postgresql', + connection: process.env.DATABASE_URL, + pool: { + min: 2, + max: 10 + }, + migrations: { + tableName: 'card_service_knex_migrations' + } + }) + + // node pg defaults to returning bigint as string. This ensures it parses to bigint + db.client.driver.types.setTypeParser( + db.client.driver.types.builtins.INT8, + 'text', + BigInt + ) + await db.migrate.latest({ + directory: __dirname + '/migrations' + }) + + for (let i = 1; i <= workers; i++) { + const workerDatabaseName = `card_service_testing_${i}` + + await db.raw(`DROP DATABASE IF EXISTS ${workerDatabaseName}`) + await db.raw( + `CREATE DATABASE ${workerDatabaseName} TEMPLATE card_service_testing` + ) + } + + global.__CARD_SERVICE_KNEX__ = db + } + + const setupRedis = async () => { + if (!process.env.REDIS_URL) { + const redisContainer = await new GenericContainer('redis:7') + .withExposedPorts(REDIS_PORT) + .start() + + global.__CARD_SERVICE_REDIS__ = redisContainer + process.env.REDIS_URL = `redis://localhost:${redisContainer.getMappedPort( + REDIS_PORT + )}` + } + } + + await Promise.all([setupDatabase(), setupRedis()]) +} + +export default setup diff --git a/packages/card-service/jest.teardown.js b/packages/card-service/jest.teardown.js new file mode 100644 index 0000000000..c0774b606c --- /dev/null +++ b/packages/card-service/jest.teardown.js @@ -0,0 +1,13 @@ +module.exports = async () => { + await global.__CARD_SERVICE_KNEX__.migrate.rollback( + { directory: __dirname + '/migrations' }, + true + ) + await global.__CARD_SERVICE_KNEX__.destroy() + if (global.__CARD_SERVICE_POSTGRES__) { + await global.__CARD_SERVICE_POSTGRES__.stop() + } + if (global.__CARD_SERVICE_REDIS__) { + await global.__CARD_SERVICE_REDIS__.stop() + } +} diff --git a/packages/card-service/package.json b/packages/card-service/package.json index 574742fcf6..df216a4d22 100644 --- a/packages/card-service/package.json +++ b/packages/card-service/package.json @@ -8,7 +8,9 @@ "scripts": { "build": "pnpm clean && tsc --build tsconfig.json", "clean": "rm -fr dist/", - "test": "jest", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests --maxWorkers=50%", + "test:ci": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests --maxWorkers=2", + "test:cov": "pnpm test -- --coverage", "dev": "ts-node-dev --inspect=0.0.0.0:9229 --respawn --transpile-only src/index.ts", "knex": "knex" }, @@ -17,8 +19,8 @@ "@koa/cors": "^5.0.0", "@koa/router": "^12.0.2", "koa-bodyparser": "^4.4.1", - "knex": "^3.1.0", "koa": "^2.15.4", + "knex": "^3.1.0", "objection": "^3.1.5", "pg": "^8.11.3", "objection-db-errors": "^1.1.2", @@ -31,6 +33,8 @@ "@types/koa__cors": "^5.0.0", "@types/koa__router": "^12.0.4", "@types/uuid": "^9.0.8", + "jest-environment-node": "^29.7.0", + "testcontainers": "^10.16.0", "ts-node-dev": "^2.0.0" } } diff --git a/packages/card-service/scripts/init.sh b/packages/card-service/scripts/init.sh new file mode 100755 index 0000000000..3aee40a8d9 --- /dev/null +++ b/packages/card-service/scripts/init.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + DROP DATABASE IF EXISTS CARD_SERVICE_TESTING; + CREATE DATABASE card_service_testing; + CREATE DATABASE card_service_development; +EOSQL diff --git a/packages/card-service/src/tests/app.ts b/packages/card-service/src/tests/app.ts new file mode 100644 index 0000000000..0175e0a4fb --- /dev/null +++ b/packages/card-service/src/tests/app.ts @@ -0,0 +1,37 @@ +import { Knex } from 'knex' +import { IocContract } from '@adonisjs/fold' + +import { App, AppServices } from '../app' + +export interface TestContainer { + cardServicePort: number + app: App + knex: Knex + connectionUrl: string + shutdown: () => Promise + container: IocContract +} + +export const createTestApp = async ( + container: IocContract +): Promise => { + const config = await container.use('config') + config.cardServicePort = 0 + + const app = new App(container) + await app.boot() + await app.startCardServiceServer(config.cardServicePort) + + const knex = global.__CARD_SERVICE_KNEX__ + + return { + app, + cardServicePort: app.getCardServicePort(), + knex, + connectionUrl: process.env.DATABASE_URL || '', + shutdown: async () => { + await app.shutdown() + }, + container + } +} From 1b8e0a887e2295b4e8e4cf4c25f00bd11d53b7a8 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Wed, 9 Jul 2025 10:36:05 +0300 Subject: [PATCH 2/4] chore(cards-service): lockfile --- pnpm-lock.yaml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d2c3a6a31..98f1bf981c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -599,6 +599,12 @@ importers: '@types/uuid': specifier: ^9.0.8 version: 9.0.8 + jest-environment-node: + specifier: ^29.7.0 + version: 29.7.0 + testcontainers: + specifier: ^10.16.0 + version: 10.16.0 ts-node-dev: specifier: ^2.0.0 version: 2.0.0(@swc/core@1.11.29)(@types/node@20.14.15)(typescript@5.8.3) @@ -13530,10 +13536,6 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - /graceful-fs@4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - dev: true - /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -15324,7 +15326,7 @@ packages: '@types/node': 20.14.15 chalk: 4.1.2 ci-info: 3.8.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 picomatch: 2.3.1 dev: true From cdd37fad3b69fc9fc05b61f18a0a72c301d5cad9 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Wed, 9 Jul 2025 13:00:21 +0300 Subject: [PATCH 3/4] feat(cards-service): fix test containers setup --- .../card-service/jest.custom-environment.ts | 1 + packages/card-service/jest.setup.ts | 12 +++--- packages/card-service/package.json | 3 +- packages/card-service/scripts/init.sh | 8 ++-- .../card-service/src/tests/tableManager.ts | 42 +++++++++++++++++++ pnpm-lock.yaml | 3 ++ 6 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 packages/card-service/src/tests/tableManager.ts diff --git a/packages/card-service/jest.custom-environment.ts b/packages/card-service/jest.custom-environment.ts index ae02fa6899..d725c84b67 100644 --- a/packages/card-service/jest.custom-environment.ts +++ b/packages/card-service/jest.custom-environment.ts @@ -1,4 +1,5 @@ import { TestEnvironment } from 'jest-environment-node' +import nock from 'nock' export default class CustomEnvironment extends TestEnvironment { constructor(config, context) { diff --git a/packages/card-service/jest.setup.ts b/packages/card-service/jest.setup.ts index 1c08afd1f5..108cd08a3a 100644 --- a/packages/card-service/jest.setup.ts +++ b/packages/card-service/jest.setup.ts @@ -22,7 +22,7 @@ const setup = async (globalConfig): Promise => { POSTGRES_PASSWORD: 'password' }) .withHealthCheck({ - test: ['CMD-SHELL', 'pg_isready -d card_service_testing'], + test: ['CMD-SHELL', 'pg_isready -d testing'], interval: 10000, timeout: 5000, retries: 5 @@ -32,7 +32,7 @@ const setup = async (globalConfig): Promise => { process.env.DATABASE_URL = `postgresql://postgres:password@localhost:${postgresContainer.getMappedPort( POSTGRES_PORT - )}/card_service_testing` + )}/testing` global.__CARD_SERVICE_POSTGRES__ = postgresContainer } @@ -45,7 +45,7 @@ const setup = async (globalConfig): Promise => { max: 10 }, migrations: { - tableName: 'card_service_knex_migrations' + tableName: 'knex_migrations' } }) @@ -60,12 +60,10 @@ const setup = async (globalConfig): Promise => { }) for (let i = 1; i <= workers; i++) { - const workerDatabaseName = `card_service_testing_${i}` + const workerDatabaseName = `testing_${i}` await db.raw(`DROP DATABASE IF EXISTS ${workerDatabaseName}`) - await db.raw( - `CREATE DATABASE ${workerDatabaseName} TEMPLATE card_service_testing` - ) + await db.raw(`CREATE DATABASE ${workerDatabaseName} TEMPLATE testing`) } global.__CARD_SERVICE_KNEX__ = db diff --git a/packages/card-service/package.json b/packages/card-service/package.json index df216a4d22..4a57e1b30d 100644 --- a/packages/card-service/package.json +++ b/packages/card-service/package.json @@ -35,6 +35,7 @@ "@types/uuid": "^9.0.8", "jest-environment-node": "^29.7.0", "testcontainers": "^10.16.0", - "ts-node-dev": "^2.0.0" + "ts-node-dev": "^2.0.0", + "nock": "14.0.0-beta.19" } } diff --git a/packages/card-service/scripts/init.sh b/packages/card-service/scripts/init.sh index 3aee40a8d9..a1fa255c7d 100755 --- a/packages/card-service/scripts/init.sh +++ b/packages/card-service/scripts/init.sh @@ -2,7 +2,7 @@ set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL - DROP DATABASE IF EXISTS CARD_SERVICE_TESTING; - CREATE DATABASE card_service_testing; - CREATE DATABASE card_service_development; -EOSQL + DROP DATABASE IF EXISTS TESTING; + CREATE DATABASE testing; + CREATE DATABASE development; +EOSQL diff --git a/packages/card-service/src/tests/tableManager.ts b/packages/card-service/src/tests/tableManager.ts new file mode 100644 index 0000000000..f8536c724b --- /dev/null +++ b/packages/card-service/src/tests/tableManager.ts @@ -0,0 +1,42 @@ +import { IocContract } from '@adonisjs/fold' +import { Knex } from 'knex' +import { AppServices } from '../app' + +export async function truncateTable( + knex: Knex, + tableName: string +): Promise { + const RAW = `TRUNCATE TABLE "${tableName}" RESTART IDENTITY CASCADE` + await knex.raw(RAW) +} + +export async function truncateTables( + deps: IocContract +): Promise { + const knex = await deps.use('knex') + + const ignoreTables = [ + 'knex_migrations', + 'knex_migrations_lock', + 'card_service_knex_migrations', + 'card_service_knex_migrations_lock' + ] + + const tables = await getTables(knex, ignoreTables) + if (tables.length > 0) { + const RAW = `TRUNCATE TABLE "${tables.join('","')}" RESTART IDENTITY CASCADE` + await knex.raw(RAW) + } +} + +async function getTables( + knex: Knex, + ignoredTables: string[] +): Promise { + const result = await knex.raw( + `SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname='public'` + ) + return result.rows + .map((val: { tablename: string }) => val.tablename) + .filter((tableName: string) => !ignoredTables.includes(tableName)) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98f1bf981c..6ae15240d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -602,6 +602,9 @@ importers: jest-environment-node: specifier: ^29.7.0 version: 29.7.0 + nock: + specifier: 14.0.0-beta.19 + version: 14.0.0-beta.19 testcontainers: specifier: ^10.16.0 version: 10.16.0 From 7eaa8bb432385d3074179e4874c703e22950e6e7 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Wed, 9 Jul 2025 14:42:45 +0300 Subject: [PATCH 4/4] fix(cards-service): fix type issue --- packages/card-service/src/tests/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/card-service/src/tests/app.ts b/packages/card-service/src/tests/app.ts index 0175e0a4fb..34e4ca37a3 100644 --- a/packages/card-service/src/tests/app.ts +++ b/packages/card-service/src/tests/app.ts @@ -22,7 +22,7 @@ export const createTestApp = async ( await app.boot() await app.startCardServiceServer(config.cardServicePort) - const knex = global.__CARD_SERVICE_KNEX__ + const knex = await container.use('knex') return { app,