diff --git a/packages/api-nest/src/app/app.module.ts b/packages/api-nest/src/app/app.module.ts index 8dd4f0833..aa4e6cd73 100644 --- a/packages/api-nest/src/app/app.module.ts +++ b/packages/api-nest/src/app/app.module.ts @@ -20,7 +20,10 @@ import { configValidationSchema } from '../config/config.schema' // Global configuration with Joi validation ConfigModule.forRoot({ isGlobal: true, - envFilePath: ['.env.local', '.env'], + envFilePath: [ + `.env.${process.env.NODE_ENV || 'development'}`, // .env.test, .env.development, .env.production + '.env', // Fallback for any missing vars + ], validationSchema: configValidationSchema, validationOptions: { allowUnknown: true, // Allow other env vars not in schema diff --git a/packages/api-nest/src/config/test.config.ts b/packages/api-nest/src/config/test.config.ts index 281946e47..fbe13992e 100644 --- a/packages/api-nest/src/config/test.config.ts +++ b/packages/api-nest/src/config/test.config.ts @@ -13,13 +13,80 @@ import { EntityLabel } from '../label/entities/entity-label.entity' import { HighlightEntity } from '../highlight/entities/highlight.entity' import { ReadingProgressEntity } from '../reading-progress/entities/reading-progress.entity' -export const testDatabaseConfig: TypeOrmModuleOptions = { - type: 'postgres', - host: process.env.TEST_DATABASE_HOST, - port: Number.parseInt(process.env.TEST_DATABASE_PORT || '5432'), - username: process.env.TEST_DATABASE_USER, - password: process.env.TEST_DATABASE_PASSWORD, - database: process.env.TEST_DATABASE_NAME, +/** + * Validates test database configuration to prevent accidental production DB connections + * @param dbName - The database name to validate + * @returns The validated database name + * @throws Error if attempting to connect to production database or if name is missing + */ +function validateTestDatabaseName(dbName: string | undefined): string { + // Production database names that must never be used in tests + const FORBIDDEN_DB_NAMES = [ + 'omnivore', // Main production DB + 'omnivore_prod', // Production variant + 'omnivore_production', // Production variant + ] + + // If no database name provided, throw error + if (!dbName) { + throw new Error( + '🚨 CRITICAL: TEST_DATABASE_NAME is not set!\n' + + 'Tests cannot run without a database configuration.\n\n' + + 'For E2E tests with testcontainers (recommended):\n' + + ' - This should be set automatically by global-setup.ts\n' + + ' - Check that Jest globalSetup is configured correctly in jest-e2e.json\n\n' + + 'For manual test database:\n' + + ' - Copy .env.test.example to .env.test\n' + + ' - Set TEST_DATABASE_NAME=omnivore_test\n' + + ' - Create database: psql -U postgres -c "CREATE DATABASE omnivore_test;"\n' + + ' - Run migrations: TEST_DATABASE_NAME=omnivore_test npm run migration:run\n' + ) + } + + // Block production database names + const normalizedDbName = dbName.toLowerCase() + if (FORBIDDEN_DB_NAMES.includes(normalizedDbName)) { + throw new Error( + `🚨 CRITICAL: Tests attempting to connect to PRODUCTION database "${dbName}"!\n\n` + + `This would corrupt production data. Tests have been BLOCKED.\n\n` + + `Use a dedicated test database instead:\n` + + ` - For testcontainers: Global setup handles this automatically\n` + + ` - For manual DB: Set TEST_DATABASE_NAME=omnivore_test in .env.test\n\n` + + `Forbidden database names: ${FORBIDDEN_DB_NAMES.join(', ')}` + ) + } + + // Warn if database name doesn't contain 'test' (suspicious but not blocked) + if (!normalizedDbName.includes('test')) { + console.warn( + `āš ļø WARNING: Test database name "${dbName}" doesn't contain "test".\n` + + ` This might not be a dedicated test database. Recommended: omnivore_test\n` + ) + } + + return dbName +} + +/** + * Test database configuration for E2E tests + * Supports two modes: + * 1. Testcontainers (default): Ephemeral PostgreSQL container, auto-configured by global-setup.ts + * 2. Manual DB: Static test database specified in .env.test file + */ +const getTestDatabaseConfig = (): TypeOrmModuleOptions => { + const host = process.env.TEST_DATABASE_HOST || 'localhost' + const port = Number.parseInt(process.env.TEST_DATABASE_PORT || '5432') + const username = process.env.TEST_DATABASE_USER || 'postgres' + const password = process.env.TEST_DATABASE_PASSWORD || '' + const database = validateTestDatabaseName(process.env.TEST_DATABASE_NAME) + + return { + type: 'postgres', + host, + port, + username, + password, + database, entities: [ User, UserProfile, @@ -34,10 +101,13 @@ export const testDatabaseConfig: TypeOrmModuleOptions = { HighlightEntity, ReadingProgressEntity, ], - synchronize: false, - logging: false, + synchronize: false, // Schema managed by migrations + logging: false, // Reduce test noise + } } +export const testDatabaseConfig = getTestDatabaseConfig() + export const createTestDataSource = () => { return new DataSource(testDatabaseConfig as DataSourceOptions) } diff --git a/packages/api-nest/src/main.ts b/packages/api-nest/src/main.ts index 909753a42..669042699 100644 --- a/packages/api-nest/src/main.ts +++ b/packages/api-nest/src/main.ts @@ -1,7 +1,8 @@ -import { NestFactory } from '@nestjs/core' import { Logger, ValidationPipe } from '@nestjs/common' import { ConfigService } from '@nestjs/config' +import { NestFactory } from '@nestjs/core' import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' + import { AppModule } from './app/app.module' import { EnvVariables } from './config/env-variables' diff --git a/packages/api-nest/src/queue/queue.constants.ts b/packages/api-nest/src/queue/queue.constants.ts index 19a859311..85e7b8c09 100644 --- a/packages/api-nest/src/queue/queue.constants.ts +++ b/packages/api-nest/src/queue/queue.constants.ts @@ -5,6 +5,8 @@ * to avoid magic strings throughout the codebase. */ +import { isTestEnvironment } from '../utils/env.utils' + /** * Queue Names - Define all queues used in the system */ @@ -83,7 +85,8 @@ export const REDIS_CONFIG = { ENABLE_READY_CHECK: true, ENABLE_OFFLINE_QUEUE: true, - KEY_PREFIX: process.env.NODE_ENV === 'test' ? 'omnivore:test:' : 'omnivore:', + // Use test-specific key prefix to avoid polluting production Redis during tests + KEY_PREFIX: isTestEnvironment() ? 'omnivore:test:' : 'omnivore:', } as const /** diff --git a/packages/api-nest/src/utils/env.utils.ts b/packages/api-nest/src/utils/env.utils.ts new file mode 100644 index 000000000..66546870b --- /dev/null +++ b/packages/api-nest/src/utils/env.utils.ts @@ -0,0 +1,96 @@ +/** + * Environment Detection Utilities + * + * Provides helper functions for detecting different runtime environments. + * These utilities handle both current and potential future test environment types. + */ + +/** + * Check if running in any test environment + * + * Covers both unit tests (NODE_ENV=test) and potential future test types (e2e, integration). + * Use this for general test-specific behavior like disabling external API calls, + * using test Redis keys, or enabling verbose logging. + * + * @returns true if NODE_ENV indicates a test environment + * + * @example + * ```typescript + * if (isTestEnvironment()) { + * // Use test Redis keys + * KEY_PREFIX = 'omnivore:test:' + * } + * ``` + */ +export function isTestEnvironment(): boolean { + const testEnvs = ['test', 'e2e', 'integration'] // Future-proof + return testEnvs.includes(process.env.NODE_ENV || '') +} + +/** + * Check if running E2E tests specifically + * + * E2E tests use testcontainers with full infrastructure (database, Redis, etc.). + * They set TEST_DATABASE_* environment variables at runtime. + * + * @returns true if running E2E tests with testcontainer + * + * @example + * ```typescript + * if (isE2EEnvironment()) { + * console.log('Running with testcontainer infrastructure') + * } + * ``` + */ +export function isE2EEnvironment(): boolean { + // E2E tests currently use NODE_ENV=test but can be distinguished + // by presence of TEST_DATABASE_* variables set by testcontainer + return process.env.NODE_ENV === 'test' && !!process.env.TEST_DATABASE_NAME +} + +/** + * Check if running unit tests specifically + * + * Unit tests mock all dependencies and don't use real infrastructure. + * They don't have TEST_DATABASE_* variables. + * + * @returns true if running unit tests (not E2E) + * + * @example + * ```typescript + * if (isUnitTestEnvironment()) { + * // All services should be mocked + * expect(mockService).toHaveBeenCalled() + * } + * ``` + */ +export function isUnitTestEnvironment(): boolean { + return process.env.NODE_ENV === 'test' && !process.env.TEST_DATABASE_NAME +} + +/** + * Check if running in development environment + * + * @returns true if NODE_ENV is 'development' + */ +export function isDevelopmentEnvironment(): boolean { + return process.env.NODE_ENV === 'development' +} + +/** + * Check if running in production environment + * + * @returns true if NODE_ENV is 'production' + */ +export function isProductionEnvironment(): boolean { + return process.env.NODE_ENV === 'production' +} + +/** + * Get the current environment name + * + * @returns The current NODE_ENV value or 'development' as default + */ +export function getEnvironment(): string { + return process.env.NODE_ENV || 'development' +} diff --git a/packages/api-nest/test/auth.e2e-spec.ts b/packages/api-nest/test/auth.e2e-spec.ts index a3142739c..24aa48fd0 100644 --- a/packages/api-nest/test/auth.e2e-spec.ts +++ b/packages/api-nest/test/auth.e2e-spec.ts @@ -1,10 +1,6 @@ -import { Test, TestingModule } from '@nestjs/testing' import { INestApplication } from '@nestjs/common' -import { ValidationPipe } from '@nestjs/common' -import { TypeOrmModule } from '@nestjs/typeorm' import request from 'supertest' -import { AppModule } from '../src/app/app.module' -import { testDatabaseConfig } from '../src/config/test.config' +import { createE2EApp } from './helpers/create-e2e-app' import { TEST_PERSONAS, INVALID_CREDENTIALS, @@ -31,26 +27,7 @@ describe('Authentication E2E Tests', () => { } beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }) - .overrideModule(TypeOrmModule) - .useModule(TypeOrmModule.forRoot(testDatabaseConfig)) - .compile() - - app = moduleFixture.createNestApplication() - - // Apply the same pipes as main application - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - }), - ) - - app.setGlobalPrefix('api/v2') - await app.init() + app = await createE2EApp() // Create the main test user that login tests will use const mainTestUser = generateTestUser('main') diff --git a/packages/api-nest/test/factories-example.e2e-spec.ts b/packages/api-nest/test/factories-example.e2e-spec.ts index 914715b5f..9fd408d27 100644 --- a/packages/api-nest/test/factories-example.e2e-spec.ts +++ b/packages/api-nest/test/factories-example.e2e-spec.ts @@ -7,16 +7,16 @@ * Run with: yarn test:e2e --testPathPattern=factories-example */ +import { HighlightColor } from '../src/highlight/entities/highlight.entity' +import { StatusType } from '../src/user/entities/user.entity' +import { UserRole } from '../src/user/enums/user-role.enum' import { - UserFactory, - LibraryItemFactory, HighlightFactory, LabelFactory, + LibraryItemFactory, + UserFactory, } from './factories' import { getTestDataSource } from './setup/test-datasource' -import { UserRole } from '../src/user/enums/user-role.enum' -import { StatusType } from '../src/user/entities/user.entity' -import { HighlightColor } from '../src/highlight/entities/highlight.entity' describe('Factory Pattern Example (e2e)', () => { it('should create test data using factories', async () => { diff --git a/packages/api-nest/test/factories/base.factory.ts b/packages/api-nest/test/factories/base.factory.ts index e55853055..f12b37d2a 100644 --- a/packages/api-nest/test/factories/base.factory.ts +++ b/packages/api-nest/test/factories/base.factory.ts @@ -1,4 +1,6 @@ +import { INestApplication } from '@nestjs/common' import { DeepPartial, Repository } from 'typeorm' +import { getRepositoryToken } from '@nestjs/typeorm' import { getTestDataSource } from '../setup/test-datasource' /** @@ -7,13 +9,22 @@ import { getTestDataSource } from '../setup/test-datasource' * - build(): Creates entity in memory (for unit tests with mocks) * - create(): Saves entity to database (for integration/E2E tests) * + * Factory Initialization Modes: + * 1. E2E tests: Call FactoryRegistry.setApp(app) to use NestJS DI + * 2. Integration tests: Uses globalThis.__TEST_DATASOURCE__ automatically + * * @example * ```typescript + * // E2E test setup (recommended) + * beforeAll(async () => { + * app = await createE2EApp() + * FactoryRegistry.setApp(app) + * }) + * + * const user = await UserFactory.create({ email: 'test@example.com' }) + * * // Unit test (no database) * const user = UserFactory.build({ email: 'test@example.com' }) - * - * // Integration test (with database) - * const user = await UserFactory.create({ email: 'test@example.com' }) * ``` */ export abstract class BaseFactory { @@ -102,16 +113,64 @@ export abstract class BaseFactory { protected abstract getRepository(): Repository } +/** + * Factory Registry - Manages NestJS app instance for E2E tests + * + * E2E tests should call FactoryRegistry.setApp(app) in beforeAll + * to allow factories to use NestJS DI for repository access. + */ +export class FactoryRegistry { + private static app: INestApplication | null = null + + /** + * Set the NestJS app instance for E2E tests + * Call this in beforeAll() after creating your test app + * + * @param app - NestJS application instance + */ + static setApp(app: INestApplication): void { + FactoryRegistry.app = app + } + + /** + * Clear the app instance (call in afterAll) + */ + static clearApp(): void { + FactoryRegistry.app = null + } + + /** + * Get the current app instance + * @internal + */ + static getApp(): INestApplication | null { + return FactoryRegistry.app + } +} + /** * Helper function to get a repository from the test DataSource * Used by factory implementations * + * Behavior: + * - If E2E app is set (via FactoryRegistry.setApp), uses NestJS DI + * - Otherwise, falls back to globalThis.__TEST_DATASOURCE__ + * * @param entityClass - Entity class to get repository for * @returns TypeORM Repository instance */ export function getTestRepository( entityClass: new () => Entity, ): Repository { + const app = FactoryRegistry.getApp() + + if (app) { + // E2E test mode: Use NestJS DI (proper abstraction layer) + const repositoryToken = getRepositoryToken(entityClass) + return app.get>(repositoryToken) + } + + // Integration/migration test mode: Use global DataSource const dataSource = getTestDataSource() return dataSource.getRepository(entityClass) } diff --git a/packages/api-nest/test/graphql.e2e-spec.ts b/packages/api-nest/test/graphql.e2e-spec.ts index e0c9e32b8..95322db5e 100644 --- a/packages/api-nest/test/graphql.e2e-spec.ts +++ b/packages/api-nest/test/graphql.e2e-spec.ts @@ -1,33 +1,13 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { INestApplication, ValidationPipe } from '@nestjs/common' -import { TypeOrmModule } from '@nestjs/typeorm' +import { INestApplication } from '@nestjs/common' import request from 'supertest' -import { AppModule } from '../src/app/app.module' -import { testDatabaseConfig } from '../src/config/test.config' +import { createE2EApp } from './helpers/create-e2e-app' describe('GraphQL Module (e2e)', () => { let app: INestApplication let authToken: string beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }) - .overrideModule(TypeOrmModule) - .useModule(TypeOrmModule.forRoot(testDatabaseConfig)) - .compile() - - app = moduleFixture.createNestApplication() - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - }), - ) - - app.setGlobalPrefix('api/v2') - await app.init() + app = await createE2EApp() const registerResponse = await request(app.getHttpServer()) .post('/api/v2/auth/register') diff --git a/packages/api-nest/test/health.e2e-spec.ts b/packages/api-nest/test/health.e2e-spec.ts index f67655e5a..8eab0b135 100644 --- a/packages/api-nest/test/health.e2e-spec.ts +++ b/packages/api-nest/test/health.e2e-spec.ts @@ -1,24 +1,12 @@ -import { Test, TestingModule } from '@nestjs/testing' import { INestApplication } from '@nestjs/common' -import { TypeOrmModule } from '@nestjs/typeorm' import request from 'supertest' -import { AppModule } from '../src/app/app.module' -import { testDatabaseConfig } from '../src/config/test.config' +import { createE2EApp } from './helpers/create-e2e-app' describe('Health E2E Tests', () => { let app: INestApplication beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }) - .overrideModule(TypeOrmModule) - .useModule(TypeOrmModule.forRoot(testDatabaseConfig)) - .compile() - - app = moduleFixture.createNestApplication() - app.setGlobalPrefix('api/v2') - await app.init() + app = await createE2EApp() }) afterAll(async () => { diff --git a/packages/api-nest/test/helpers/create-e2e-app.ts b/packages/api-nest/test/helpers/create-e2e-app.ts new file mode 100644 index 000000000..6eb502999 --- /dev/null +++ b/packages/api-nest/test/helpers/create-e2e-app.ts @@ -0,0 +1,159 @@ +import { INestApplication, ValidationPipe } from '@nestjs/common' +import { Test, TestingModule, TestingModuleBuilder } from '@nestjs/testing' +import { ConfigService } from '@nestjs/config' +import { AppModule } from '../../src/app/app.module' +import { TestConfigService } from './test-config.service' + +/** + * Create E2E Test Application + * + * Creates a fully configured NestJS application for E2E testing with: + * - TestConfigService override (redirects DATABASE_* to TEST_DATABASE_*) + * - Global validation pipes + * - API prefix + * + * This centralizes all E2E test setup to avoid repetition across test files. + * + * @returns Initialized NestJS application ready for testing + * + * @example + * ```typescript + * describe('My E2E Tests', () => { + * let app: INestApplication + * + * beforeAll(async () => { + * app = await createE2EApp() + * }) + * + * afterAll(async () => { + * await app.close() + * }) + * + * it('should work', async () => { + * const response = await request(app.getHttpServer()) + * .get('/api/v2/health') + * .expect(200) + * }) + * }) + * ``` + */ +export async function createE2EApp(): Promise { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }) + .overrideProvider(ConfigService) + .useClass(TestConfigService) + .compile() + + const app = moduleFixture.createNestApplication() + + // Apply global validation pipes (same as main.ts) + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ) + + // Set API prefix (same as main.ts) + app.setGlobalPrefix('api/v2') + + // Initialize the application + await app.init() + + return app +} + +/** + * Create E2E Test Application with Custom Configuration + * + * Same as createE2EApp() but allows customizing the TestingModule before compilation. + * Use this when you need to override additional providers or add custom setup. + * + * @param customize - Function to customize the TestingModuleBuilder + * @returns Initialized NestJS application ready for testing + * + * @example + * ```typescript + * const app = await createE2EAppWithCustomization(builder => + * builder + * .overrideProvider(MyService) + * .useValue(mockService) + * ) + * ``` + */ +export async function createE2EAppWithCustomization( + customize: (builder: TestingModuleBuilder) => TestingModuleBuilder, +): Promise { + let builder = Test.createTestingModule({ + imports: [AppModule], + }) + .overrideProvider(ConfigService) + .useClass(TestConfigService) + + // Apply custom modifications + builder = customize(builder) + + const moduleFixture: TestingModule = await builder.compile() + + const app = moduleFixture.createNestApplication() + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ) + + app.setGlobalPrefix('api/v2') + + await app.init() + + return app +} + +/** + * Get module fixture for accessing providers directly + * + * Use this when you need access to repositories or services in your tests. + * + * @returns Module fixture and initialized application + * + * @example + * ```typescript + * const { app, moduleFixture } = await createE2EAppWithModule() + * + * const userRepository = moduleFixture.get>( + * getRepositoryToken(User) + * ) + * ``` + */ +export async function createE2EAppWithModule(): Promise<{ + app: INestApplication + moduleFixture: TestingModule +}> { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }) + .overrideProvider(ConfigService) + .useClass(TestConfigService) + .compile() + + const app = moduleFixture.createNestApplication() + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ) + + app.setGlobalPrefix('api/v2') + + await app.init() + + return { app, moduleFixture } +} diff --git a/packages/api-nest/test/helpers/test-config.service.ts b/packages/api-nest/test/helpers/test-config.service.ts new file mode 100644 index 000000000..88b3b4f40 --- /dev/null +++ b/packages/api-nest/test/helpers/test-config.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' + +/** + * Test ConfigService that redirects DATABASE_* queries to TEST_DATABASE_* values + * + * This ensures that when DatabaseModule reads DATABASE_* from ConfigService, + * it actually gets TEST_DATABASE_* values from the testcontainer. + * + * Usage in E2E tests: + * ```typescript + * .overrideProvider(ConfigService) + * .useClass(TestConfigService) + * ``` + */ +@Injectable() +export class TestConfigService extends ConfigService { + /** + * Intercept get() calls and redirect DATABASE_* to TEST_DATABASE_* + */ + get(propertyPath: string, defaultValue?: T): T { + // Redirect DATABASE_* to TEST_DATABASE_* + const redirections: Record = { + DATABASE_HOST: 'TEST_DATABASE_HOST', + DATABASE_PORT: 'TEST_DATABASE_PORT', + DATABASE_NAME: 'TEST_DATABASE_NAME', + DATABASE_USER: 'TEST_DATABASE_USER', + DATABASE_PASSWORD: 'TEST_DATABASE_PASSWORD', + } + + // If asking for DATABASE_*, return TEST_DATABASE_* instead + if (redirections[propertyPath]) { + const testKey = redirections[propertyPath] + const testValue = super.get(testKey, defaultValue) + + // Log for debugging (only in test environment) + if (process.env.NODE_ENV === 'test' && process.env.DEBUG_TEST_CONFIG) { + console.log( + `[TestConfigService] Redirecting ${propertyPath} → ${testKey} = ${testValue}`, + ) + } + + return testValue + } + + // For all other keys, use normal behavior + return super.get(propertyPath, defaultValue) + } +} diff --git a/packages/api-nest/test/helpers/test-datasource.helper.ts b/packages/api-nest/test/helpers/test-datasource.helper.ts new file mode 100644 index 000000000..df7bc78a8 --- /dev/null +++ b/packages/api-nest/test/helpers/test-datasource.helper.ts @@ -0,0 +1,154 @@ +import { DataSource } from 'typeorm' +import { INestApplication } from '@nestjs/common' + +/** + * Get Test DataSource from E2E App + * + * Returns the TypeORM DataSource configured for the test database (testcontainer). + * This is safer than importing test.config.ts directly. + * + * @param app - The E2E test application + * @returns DataSource connected to test database + * + * @example + * ```typescript + * describe('My E2E Tests', () => { + * let app: INestApplication + * let dataSource: DataSource + * + * beforeAll(async () => { + * app = await createE2EApp() + * dataSource = getTestDataSource(app) + * }) + * + * it('should insert test data', async () => { + * await dataSource.query( + * `INSERT INTO omnivore.library_item (...) VALUES (...)`, + * [...] + * ) + * }) + * }) + * ``` + */ +export function getTestDataSource(app: INestApplication): DataSource { + return app.get(DataSource) +} + +/** + * Execute Raw SQL Query in Test Database + * + * Convenience wrapper for executing raw SQL in E2E tests. + * + * @param app - The E2E test application + * @param query - SQL query to execute + * @param parameters - Query parameters (optional) + * @returns Query results + * + * @example + * ```typescript + * await executeTestQuery( + * app, + * `UPDATE omnivore.user SET status = $1 WHERE id = $2`, + * ['ACTIVE', userId] + * ) + * ``` + */ +export async function executeTestQuery( + app: INestApplication, + query: string, + parameters?: any[], +): Promise { + const dataSource = getTestDataSource(app) + return dataSource.query(query, parameters) +} + +/** + * Activate Test User (Skip Email Confirmation) + * + * Common helper to activate a newly registered test user without email confirmation. + * + * @param app - The E2E test application + * @param userId - User ID to activate + * + * @example + * ```typescript + * const registerResponse = await request(app.getHttpServer()) + * .post('/api/v2/auth/register') + * .send({ email, password, name }) + * + * await activateTestUser(app, registerResponse.body.user.id) + * ``` + */ +export async function activateTestUser( + app: INestApplication, + userId: string, +): Promise { + await executeTestQuery( + app, + `UPDATE omnivore.user SET status = 'ACTIVE' WHERE id = $1`, + [userId], + ) +} + +/** + * Clean Test Data + * + * Helper to clean up test data in the correct order (respecting foreign keys). + * + * @param app - The E2E test application + * @param userId - User ID to clean up data for + * + * @example + * ```typescript + * afterAll(async () => { + * await cleanTestData(app, userId) + * await app.close() + * }) + * ``` + */ +export async function cleanTestData( + app: INestApplication, + userId: string, +): Promise { + const dataSource = getTestDataSource(app) + + // Delete in correct order (child tables first, then parent) + await dataSource.query( + `DELETE FROM omnivore.entity_labels WHERE library_item_id IN + (SELECT id FROM omnivore.library_item WHERE user_id = $1)`, + [userId], + ) + + await dataSource.query( + `DELETE FROM omnivore.highlights WHERE library_item_id IN + (SELECT id FROM omnivore.library_item WHERE user_id = $1)`, + [userId], + ) + + await dataSource.query( + `DELETE FROM omnivore.reading_progress WHERE library_item_id IN + (SELECT id FROM omnivore.library_item WHERE user_id = $1)`, + [userId], + ) + + await dataSource.query( + `DELETE FROM omnivore.library_item WHERE user_id = $1`, + [userId], + ) + + await dataSource.query(`DELETE FROM omnivore.labels WHERE user_id = $1`, [ + userId, + ]) + + await dataSource.query( + `DELETE FROM omnivore.user_personalization WHERE user_id = $1`, + [userId], + ) + + await dataSource.query(`DELETE FROM omnivore.user_profile WHERE user_id = $1`, [ + userId, + ]) + + await dataSource.query(`DELETE FROM omnivore.user WHERE id = $1`, [userId]) +} + diff --git a/packages/api-nest/test/highlight.e2e-spec.ts b/packages/api-nest/test/highlight.e2e-spec.ts index 1efb13b1b..7b1cd420f 100644 --- a/packages/api-nest/test/highlight.e2e-spec.ts +++ b/packages/api-nest/test/highlight.e2e-spec.ts @@ -1,11 +1,9 @@ import { randomUUID } from 'crypto' -import { Test, TestingModule } from '@nestjs/testing' -import { INestApplication, ValidationPipe } from '@nestjs/common' -import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm' +import { INestApplication } from '@nestjs/common' +import { getRepositoryToken } from '@nestjs/typeorm' import request from 'supertest' import { Repository } from 'typeorm' -import { AppModule } from '../src/app/app.module' -import { testDatabaseConfig } from '../src/config/test.config' +import { createE2EAppWithModule } from './helpers/create-e2e-app' import { ContentReaderType, LibraryItemEntity, @@ -98,24 +96,8 @@ describe('Highlight GraphQL (e2e)', () => { process.env.GOOGLE_CLIENT_SECRET = 'test-client-secret' process.env.JWT_SECRET = 'test-jwt-secret' - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }) - .overrideModule(TypeOrmModule) - .useModule(TypeOrmModule.forRoot(testDatabaseConfig)) - .compile() - - app = moduleFixture.createNestApplication() - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - }), - ) - - app.setGlobalPrefix('api/v2') - await app.init() + const { app: testApp, moduleFixture } = await createE2EAppWithModule() + app = testApp libraryRepository = moduleFixture.get>( getRepositoryToken(LibraryItemEntity), @@ -891,7 +873,9 @@ describe('Highlight GraphQL (e2e)', () => { // Verify fallback to textQuote selector from quote/prefix/suffix // GraphQL returns selectors as object, not string - expect(response.body.data.createHighlight.selectors.textQuote).toMatchObject({ + expect( + response.body.data.createHighlight.selectors.textQuote, + ).toMatchObject({ exact: 'simple highlight', prefix: 'before ', suffix: ' after', diff --git a/packages/api-nest/test/label.e2e-spec.ts b/packages/api-nest/test/label.e2e-spec.ts index b43f6b998..47cace7ad 100644 --- a/packages/api-nest/test/label.e2e-spec.ts +++ b/packages/api-nest/test/label.e2e-spec.ts @@ -1,10 +1,10 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { INestApplication, ValidationPipe } from '@nestjs/common' -import request from 'supertest' +import { INestApplication } from '@nestjs/common' import { randomUUID } from 'crypto' -import { AppModule } from '../src/app/app.module' -import { ConfigService } from '@nestjs/config' -import { DataSource } from 'typeorm' +import request from 'supertest' +import { createE2EApp } from './helpers/create-e2e-app' +import { FactoryRegistry } from './factories/base.factory' +import { LibraryItemFactory } from './factories/library-item.factory' +import { LibraryItemState } from '../src/library/entities/library-item.entity' describe('Label E2E Tests', () => { let app: INestApplication @@ -14,23 +14,8 @@ describe('Label E2E Tests', () => { let testLibraryItemId: string beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile() - - app = moduleFixture.createNestApplication() - - // Use the same validation pipe configuration as main.ts - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - }), - ) - - app.setGlobalPrefix('api/v2') - await app.init() + app = await createE2EApp() + FactoryRegistry.setApp(app) // Enable factories to use NestJS DI // Create a test user and get auth token const testEmail = `test-label-${Date.now()}@example.com` @@ -48,75 +33,20 @@ describe('Label E2E Tests', () => { authToken = registerResponse.body.accessToken userId = registerResponse.body.user.id - // Get the config service to skip email confirmation - const configService = app.get(ConfigService) - const requireEmailConfirmation = configService.get( - 'AUTH_REQUIRE_EMAIL_CONFIRMATION', - ) - - // If email confirmation is required, confirm the email - if (requireEmailConfirmation) { - const dataSource = app.get(DataSource) - await dataSource.query( - `UPDATE omnivore.user SET status = 'ACTIVE' WHERE id = $1`, - [userId], - ) - } - - // Create a test library item for label associations - const libraryItemResponse = await executeQuery( - ` - mutation { - __typename - } - `, - {}, - ) - - // Use DataSource to create a library item directly - const dataSource = app.get(DataSource) - const libraryItemResult = await dataSource.query( - ` - INSERT INTO omnivore.library_item (id, user_id, title, slug, original_url, state, folder, saved_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) - RETURNING id - `, - [ - randomUUID(), - userId, - 'Test Article for Labels', - 'test-article-labels', - 'https://example.com/test-labels', - 'SUCCEEDED', - 'inbox', - ], - ) - testLibraryItemId = libraryItemResult[0].id + // Create a test library item for label associations using factory + const libraryItem = await LibraryItemFactory.create({ + userId, + title: 'Test Article for Labels', + slug: `test-article-labels-${Date.now()}`, + originalUrl: 'https://example.com/test-labels', + state: LibraryItemState.SUCCEEDED, + folder: 'inbox', + }) + testLibraryItemId = libraryItem.id }) afterAll(async () => { - // Clean up test data - if (userId) { - const dataSource = app.get(DataSource) - - // Delete in correct order due to foreign key constraints - await dataSource.query( - `DELETE FROM omnivore.entity_labels WHERE library_item_id = $1`, - [testLibraryItemId], - ) - await dataSource.query( - `DELETE FROM omnivore.labels WHERE user_id = $1`, - [userId], - ) - await dataSource.query( - `DELETE FROM omnivore.library_item WHERE user_id = $1`, - [userId], - ) - await dataSource.query(`DELETE FROM omnivore.user WHERE id = $1`, [ - userId, - ]) - } - + FactoryRegistry.clearApp() await app.close() }, 30000) // 30 second timeout for graceful BullMQ worker shutdown @@ -307,7 +237,7 @@ describe('Label E2E Tests', () => { expect(errorMessage).toBeDefined() expect( errorMessage.includes('hex color') || - errorMessage.includes('Bad Request') + errorMessage.includes('Bad Request'), ).toBe(true) }) diff --git a/packages/api-nest/test/library.e2e-spec.ts b/packages/api-nest/test/library.e2e-spec.ts index 50dc8081b..8758aa582 100644 --- a/packages/api-nest/test/library.e2e-spec.ts +++ b/packages/api-nest/test/library.e2e-spec.ts @@ -1,11 +1,9 @@ import { randomUUID } from 'crypto' -import { Test, TestingModule } from '@nestjs/testing' -import { INestApplication, ValidationPipe } from '@nestjs/common' -import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm' +import { INestApplication } from '@nestjs/common' +import { getRepositoryToken } from '@nestjs/typeorm' import request from 'supertest' import { Repository } from 'typeorm' -import { AppModule } from '../src/app/app.module' -import { testDatabaseConfig } from '../src/config/test.config' +import { createE2EAppWithModule } from './helpers/create-e2e-app' import { ContentReaderType, LibraryItemEntity, @@ -142,24 +140,8 @@ describe('Library GraphQL (e2e)', () => { process.env.GOOGLE_CLIENT_SECRET = 'test-client-secret' process.env.JWT_SECRET = 'test-jwt-secret' - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }) - .overrideModule(TypeOrmModule) - .useModule(TypeOrmModule.forRoot(testDatabaseConfig)) - .compile() - - app = moduleFixture.createNestApplication() - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - }), - ) - - app.setGlobalPrefix('api/v2') - await app.init() + const { app: testApp, moduleFixture } = await createE2EAppWithModule() + app = testApp libraryRepository = moduleFixture.get>( getRepositoryToken(LibraryItemEntity), diff --git a/packages/api-nest/test/notebook.e2e-spec.ts b/packages/api-nest/test/notebook.e2e-spec.ts index 963fe1dde..b289cffaf 100644 --- a/packages/api-nest/test/notebook.e2e-spec.ts +++ b/packages/api-nest/test/notebook.e2e-spec.ts @@ -1,11 +1,9 @@ import { randomUUID } from 'crypto' -import { Test, TestingModule } from '@nestjs/testing' -import { INestApplication, ValidationPipe } from '@nestjs/common' -import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm' +import { INestApplication } from '@nestjs/common' +import { getRepositoryToken } from '@nestjs/typeorm' import request from 'supertest' import { Repository } from 'typeorm' -import { AppModule } from '../src/app/app.module' -import { testDatabaseConfig } from '../src/config/test.config' +import { createE2EAppWithModule } from './helpers/create-e2e-app' import { ContentReaderType, LibraryItemEntity, @@ -46,24 +44,8 @@ describe('Notebook GraphQL (e2e)', () => { process.env.GOOGLE_CLIENT_SECRET = 'test-client-secret' process.env.JWT_SECRET = 'test-jwt-secret' - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }) - .overrideModule(TypeOrmModule) - .useModule(TypeOrmModule.forRoot(testDatabaseConfig)) - .compile() - - app = moduleFixture.createNestApplication() - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - }), - ) - - app.setGlobalPrefix('api/v2') - await app.init() + const { app: testApp, moduleFixture } = await createE2EAppWithModule() + app = testApp libraryRepository = moduleFixture.get>( getRepositoryToken(LibraryItemEntity), @@ -86,7 +68,10 @@ describe('Notebook GraphQL (e2e)', () => { await app.close() }, 30000) - const executeQuery = (query: string, variables: Record = {}) => + const executeQuery = ( + query: string, + variables: Record = {}, + ) => request(app.getHttpServer()) .post('/api/graphql') .set('Authorization', `Bearer ${authToken}`) @@ -117,7 +102,8 @@ describe('Notebook GraphQL (e2e)', () => { }) it('creates a new notebook for a library item', async () => { - const noteContent = '# My Thoughts\n\nThis is an interesting article about TypeScript.' + const noteContent = + '# My Thoughts\n\nThis is an interesting article about TypeScript.' const response = await executeQuery(UPDATE_NOTEBOOK_MUTATION, { id: testItemId, @@ -268,7 +254,9 @@ describe('Notebook GraphQL (e2e)', () => { secondResponse.body.data.updateNotebook.noteUpdatedAt, ) - expect(secondTimestamp.getTime()).toBeGreaterThan(firstTimestamp.getTime()) + expect(secondTimestamp.getTime()).toBeGreaterThan( + firstTimestamp.getTime(), + ) }) it('preserves notebook when updating other library item fields', async () => { diff --git a/packages/api-nest/test/reading-progress.e2e-spec.ts b/packages/api-nest/test/reading-progress.e2e-spec.ts index e01262d1c..62b0b285b 100644 --- a/packages/api-nest/test/reading-progress.e2e-spec.ts +++ b/packages/api-nest/test/reading-progress.e2e-spec.ts @@ -1,11 +1,9 @@ import { randomUUID } from 'crypto' -import { Test, TestingModule } from '@nestjs/testing' -import { INestApplication, ValidationPipe } from '@nestjs/common' -import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm' +import { INestApplication } from '@nestjs/common' +import { getRepositoryToken } from '@nestjs/typeorm' import request from 'supertest' import { Repository } from 'typeorm' -import { AppModule } from '../src/app/app.module' -import { testDatabaseConfig } from '../src/config/test.config' +import { createE2EAppWithModule } from './helpers/create-e2e-app' import { ContentReaderType, LibraryItemEntity, @@ -57,24 +55,8 @@ describe('ReadingProgress GraphQL (e2e)', () => { process.env.GOOGLE_CLIENT_SECRET = 'test-client-secret' process.env.JWT_SECRET = 'test-jwt-secret' - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }) - .overrideModule(TypeOrmModule) - .useModule(TypeOrmModule.forRoot(testDatabaseConfig)) - .compile() - - app = moduleFixture.createNestApplication() - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - }), - ) - - app.setGlobalPrefix('api/v2') - await app.init() + const { app: testApp, moduleFixture } = await createE2EAppWithModule() + app = testApp libraryRepository = moduleFixture.get>( getRepositoryToken(LibraryItemEntity), diff --git a/packages/api-nest/test/save-url.e2e-spec.ts b/packages/api-nest/test/save-url.e2e-spec.ts index 6c082b096..7ed90457d 100644 --- a/packages/api-nest/test/save-url.e2e-spec.ts +++ b/packages/api-nest/test/save-url.e2e-spec.ts @@ -1,10 +1,7 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { INestApplication, ValidationPipe } from '@nestjs/common' +import { INestApplication } from '@nestjs/common' import request from 'supertest' -import { randomUUID } from 'crypto' -import { AppModule } from '../src/app/app.module' -import { ConfigService } from '@nestjs/config' -import { DataSource } from 'typeorm' +import { createE2EApp } from './helpers/create-e2e-app' +import { FactoryRegistry } from './factories/base.factory' import { FOLDERS } from '../src/constants/folders.constants' describe('SaveUrl E2E Tests', () => { @@ -14,23 +11,8 @@ describe('SaveUrl E2E Tests', () => { let createdLibraryItemIds: string[] = [] beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile() - - app = moduleFixture.createNestApplication() - - // Use the same validation pipe configuration as main.ts - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - }), - ) - - app.setGlobalPrefix('api/v2') - await app.init() + app = await createE2EApp() + FactoryRegistry.setApp(app) // Enable factories to use NestJS DI // Create a test user and get auth token const testEmail = `test-saveurl-${Date.now()}@example.com` @@ -48,37 +30,12 @@ describe('SaveUrl E2E Tests', () => { authToken = registerResponse.body.accessToken userId = registerResponse.body.user.id - // Get the config service to skip email confirmation - const configService = app.get(ConfigService) - const requireEmailConfirmation = configService.get( - 'AUTH_REQUIRE_EMAIL_CONFIRMATION', - ) - - // If email confirmation is required, confirm the email - if (requireEmailConfirmation) { - const dataSource = app.get(DataSource) - await dataSource.query( - `UPDATE omnivore.user SET status = 'ACTIVE' WHERE id = $1`, - [userId], - ) - } + // Note: In test mode, users are ACTIVE by default (no email confirmation required) + // This is configured via TEST_AUTH_REQUIRE_EMAIL_CONFIRMATION=false }) afterAll(async () => { - // Clean up test data - if (userId) { - const dataSource = app.get(DataSource) - - // Delete in correct order due to foreign key constraints - await dataSource.query( - `DELETE FROM omnivore.library_item WHERE user_id = $1`, - [userId], - ) - await dataSource.query(`DELETE FROM omnivore.user WHERE id = $1`, [ - userId, - ]) - } - + FactoryRegistry.clearApp() await app.close() }, 30000) // 30 second timeout for graceful BullMQ worker shutdown diff --git a/packages/api-nest/test/setup/global-setup.ts b/packages/api-nest/test/setup/global-setup.ts index 7f117f17b..ecc5bfaa6 100644 --- a/packages/api-nest/test/setup/global-setup.ts +++ b/packages/api-nest/test/setup/global-setup.ts @@ -1,4 +1,5 @@ import { setupTestContainer } from './testcontainers' +import { TEST_DB_ENV_VARS } from './test-db-constants' /** * Jest Global Setup @@ -9,15 +10,35 @@ export default async function globalSetup() { console.log('\nšŸš€ Jest Global Setup - Starting test infrastructure...\n') try { + // Validate environment before starting + if (process.env.NODE_ENV && process.env.NODE_ENV !== 'test') { + console.warn( + `\nāš ļø WARNING: NODE_ENV is "${process.env.NODE_ENV}" but should be "test".\n` + + ` E2E tests expect NODE_ENV=test for library compatibility.\n` + + ` Setting NODE_ENV=test now...\n`, + ) + } + + // Set NODE_ENV to test FIRST so ConfigModule loads .env.test + process.env.NODE_ENV = 'test' + console.log('āœ… Environment: NODE_ENV=test (E2E test mode)') + // Start container and initialize database const { container, dataSource } = await setupTestContainer() // Store connection details in environment variables for test workers - process.env.TEST_DB_HOST = container.getHost() - process.env.TEST_DB_PORT = container.getPort().toString() - process.env.TEST_DB_DATABASE = container.getDatabase() - process.env.TEST_DB_USERNAME = container.getUsername() - process.env.TEST_DB_PASSWORD = container.getPassword() + // Using TEST_DATABASE_* naming (from TEST_DB_ENV_VARS constants) + process.env[TEST_DB_ENV_VARS.HOST] = container.getHost() + process.env[TEST_DB_ENV_VARS.PORT] = container.getPort().toString() + process.env[TEST_DB_ENV_VARS.NAME] = container.getDatabase() + process.env[TEST_DB_ENV_VARS.USER] = container.getUsername() + process.env[TEST_DB_ENV_VARS.PASSWORD] = container.getPassword() + + console.log('\nšŸ”§ Test database connection:') + console.log(` Host: ${container.getHost()}`) + console.log(` Port: ${container.getPort()}`) + console.log(` Database: ${container.getDatabase()}`) + console.log(` User: ${container.getUsername()}\n`) // Store for global teardown // @ts-ignore - globalThis extension diff --git a/packages/api-nest/test/setup/jest-environment-setup.ts b/packages/api-nest/test/setup/jest-environment-setup.ts index 87460dcf0..b993db4c3 100644 --- a/packages/api-nest/test/setup/jest-environment-setup.ts +++ b/packages/api-nest/test/setup/jest-environment-setup.ts @@ -22,15 +22,23 @@ import { HighlightEntity } from '../../src/highlight/entities/highlight.entity' * This runs once per test worker */ async function initializeTestDataSource() { - // Get connection details set by global setup - const host = process.env.TEST_DB_HOST - const port = parseInt(process.env.TEST_DB_PORT || '5432') - const database = process.env.TEST_DB_DATABASE - const username = process.env.TEST_DB_USERNAME - const password = process.env.TEST_DB_PASSWORD + // Get connection details set by global setup (testcontainer mode) + // or from .env.test file (manual DB mode) + const host = process.env.TEST_DATABASE_HOST + const port = Number(process.env.TEST_DATABASE_PORT) + const database = process.env.TEST_DATABASE_NAME + const username = process.env.TEST_DATABASE_USER + const password = process.env.TEST_DATABASE_PASSWORD - if (!host || !database || !username || !password) { - throw new Error('Test database connection details not found. Make sure globalSetup ran successfully.') + if (!host || !database || !username) { + throw new Error( + 'Test database connection details not found.\n' + + 'Expected environment variables: TEST_DATABASE_HOST, TEST_DATABASE_PORT, TEST_DATABASE_NAME, TEST_DATABASE_USER, TEST_DATABASE_PASSWORD\n\n' + + 'Make sure:\n' + + '1. Global setup ran successfully (testcontainer mode), OR\n' + + '2. .env.test file exists with correct values (manual DB mode)\n\n' + + 'See .env.test.example for configuration template.', + ) } // Create and initialize DataSource diff --git a/packages/api-nest/test/setup/test-db-constants.ts b/packages/api-nest/test/setup/test-db-constants.ts new file mode 100644 index 000000000..00bed11f1 --- /dev/null +++ b/packages/api-nest/test/setup/test-db-constants.ts @@ -0,0 +1,61 @@ +/** + * Test Database Constants + * + * Centralized constants for testcontainer database configuration. + * These values are used by: + * - global-setup.ts (to create testcontainer) + * - Tests (if needed for assertions) + * + * DO NOT change these values unless you have a good reason. + * They are specifically chosen for test isolation. + */ + +export const TEST_DB_CONSTANTS = { + /** + * Database name for test container + * Must contain 'test' for safety validation + */ + DATABASE_NAME: 'test_omnivore', + + /** + * Username for test database + * Non-privileged user for security + */ + USERNAME: 'test_user', + + /** + * Password for test database + * Simple password is fine for ephemeral containers + */ + PASSWORD: 'test_password', + + /** + * PostgreSQL version + * Match production for accurate testing + */ + POSTGRES_VERSION: 'postgres:15-alpine', + + /** + * Container port + * Standard PostgreSQL port + */ + PORT: 5432, + + /** + * Schema name + * Must match production schema + */ + SCHEMA: 'omnivore', +} as const + +/** + * Environment variable names for test database configuration + * These are set by global-setup.ts and read by TestConfigService + */ +export const TEST_DB_ENV_VARS = { + HOST: 'TEST_DATABASE_HOST', + PORT: 'TEST_DATABASE_PORT', + NAME: 'TEST_DATABASE_NAME', + USER: 'TEST_DATABASE_USER', + PASSWORD: 'TEST_DATABASE_PASSWORD', +} as const diff --git a/packages/api-nest/test/setup/testcontainers.ts b/packages/api-nest/test/setup/testcontainers.ts index ebe93a2dc..315d47d70 100644 --- a/packages/api-nest/test/setup/testcontainers.ts +++ b/packages/api-nest/test/setup/testcontainers.ts @@ -1,4 +1,8 @@ -import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql' +import { TEST_DB_CONSTANTS } from './test-db-constants' +import { + PostgreSqlContainer, + StartedPostgreSqlContainer, +} from '@testcontainers/postgresql' import { DataSource } from 'typeorm' import { User } from '../../src/user/entities/user.entity' import { UserProfile } from '../../src/user/entities/profile.entity' @@ -66,9 +70,11 @@ export async function setupTestContainer(): Promise<{ console.log('šŸ”Œ Initializing database connection...') await dataSource.initialize() - console.log('šŸ“ Creating omnivore schema...') + console.log(`šŸ“ Creating ${TEST_DB_CONSTANTS.SCHEMA} schema...`) // Create the omnivore schema that our entities use - await dataSource.query('CREATE SCHEMA IF NOT EXISTS omnivore') + await dataSource.query( + `CREATE SCHEMA IF NOT EXISTS ${TEST_DB_CONSTANTS.SCHEMA}`, + ) console.log('šŸ”„ Synchronizing database schema...') // Now synchronize will create tables in the omnivore schema