import { logger } from "@medusajs/framework/logger" import { ContainerRegistrationKeys, DmlEntity, isSharedConnectionSymbol, loadModels, Modules, ModulesSdkUtils, normalizeImportPathWithSource, toMikroOrmEntities, } from "@medusajs/framework/utils" import * as fs from "fs" import { getDatabaseURL, getMikroOrmWrapper, TestDatabase } from "./database" import { initModules, InitModulesOptions } from "./init-modules" import { default as MockEventBusService } from "./mock-event-bus-service" import { ulid } from "ulid" export interface SuiteOptions { MikroOrmWrapper: TestDatabase medusaApp: any service: TService dbConfig: { schema: string clientUrl: string } } interface ModuleTestRunnerConfig { moduleName: string moduleModels?: any[] moduleOptions?: Record moduleDependencies?: string[] joinerConfig?: any[] schema?: string dbName?: string injectedDependencies?: Record resolve?: string debug?: boolean cwd?: string hooks?: { beforeModuleInit?: () => Promise afterModuleInit?: (medusaApp: any, service: TService) => Promise } } function createMikroOrmWrapper(options: { moduleModels?: (Function | DmlEntity)[] resolve?: string dbConfig: any cwd?: string }): { MikroOrmWrapper: TestDatabase models: (Function | DmlEntity)[] } { let moduleModels: (Function | DmlEntity)[] = options.moduleModels ?? [] if (!options.moduleModels) { const basePath = normalizeImportPathWithSource( options.resolve ?? options.cwd ?? process.cwd() ) const modelsPath = fs.existsSync(`${basePath}/dist/models`) ? "/dist/models" : fs.existsSync(`${basePath}/models`) ? "/models" : "" if (modelsPath) { moduleModels = loadModels(`${basePath}${modelsPath}`) } else { moduleModels = [] } } moduleModels = toMikroOrmEntities(moduleModels) const MikroOrmWrapper = getMikroOrmWrapper({ mikroOrmEntities: moduleModels, clientUrl: options.dbConfig.clientUrl, schema: options.dbConfig.schema, }) return { MikroOrmWrapper, models: moduleModels } } class ModuleTestRunner { private moduleName: string private schema: string private dbName: string private dbConfig: { clientUrl: string schema: string debug: boolean } private debug: boolean private resolve?: string private cwd?: string private moduleOptions: Record private moduleDependencies?: string[] private joinerConfig: any[] private injectedDependencies: Record private hooks: ModuleTestRunnerConfig["hooks"] = {} private connection: any = null private MikroOrmWrapper!: TestDatabase private moduleModels: (Function | DmlEntity)[] = [] private modulesConfig: any = {} private moduleOptionsConfig!: InitModulesOptions private shutdown: () => Promise = async () => void 0 private moduleService: any = null private medusaApp: any = {} constructor(config: ModuleTestRunnerConfig) { const tempName = parseInt(process.env.JEST_WORKER_ID || "1") this.moduleName = config.moduleName const moduleName = this.moduleName ?? ulid() this.dbName = config.dbName ?? `medusa-${moduleName.toLowerCase()}-integration-${tempName}` this.schema = config.schema ?? "public" this.debug = config.debug ?? false this.resolve = config.resolve this.cwd = config.cwd this.moduleOptions = config.moduleOptions ?? {} this.moduleDependencies = config.moduleDependencies this.joinerConfig = config.joinerConfig ?? [] this.injectedDependencies = config.injectedDependencies ?? {} this.hooks = config.hooks ?? {} this.dbConfig = { clientUrl: getDatabaseURL(this.dbName), schema: this.schema, debug: this.debug, } this.setupProcessHandlers() this.initializeConfig(config.moduleModels) } private setupProcessHandlers(): void { process.on("SIGTERM", async () => { await this.cleanup() process.exit(0) }) process.on("SIGINT", async () => { await this.cleanup() process.exit(0) }) } private initializeConfig(moduleModels?: any[]): void { const moduleSdkImports = require("@medusajs/framework/modules-sdk") // Use a unique connection for all the entire suite this.connection = ModulesSdkUtils.createPgConnection(this.dbConfig) const { MikroOrmWrapper, models } = createMikroOrmWrapper({ moduleModels, resolve: this.resolve, dbConfig: this.dbConfig, cwd: this.cwd, }) this.MikroOrmWrapper = MikroOrmWrapper this.moduleModels = models this.modulesConfig = { [this.moduleName]: { definition: moduleSdkImports.ModulesDefinition[this.moduleName], resolve: this.resolve, dependencies: this.moduleDependencies, options: { database: this.dbConfig, ...this.moduleOptions, [isSharedConnectionSymbol]: true, }, }, } this.moduleOptionsConfig = { injectedDependencies: { [ContainerRegistrationKeys.PG_CONNECTION]: this.connection, [Modules.EVENT_BUS]: new MockEventBusService(), [ContainerRegistrationKeys.LOGGER]: console, [ContainerRegistrationKeys.CONFIG_MODULE]: { modules: this.modulesConfig, }, ...this.injectedDependencies, }, modulesConfig: this.modulesConfig, databaseConfig: this.dbConfig, joinerConfig: this.joinerConfig, preventConnectionDestroyWarning: true, cwd: this.cwd, } } private createMedusaAppProxy(): any { return new Proxy( {}, { get: (target, prop) => { return this.medusaApp?.[prop] }, } ) } private createServiceProxy(): any { return new Proxy( {}, { get: (target, prop) => { return this.moduleService?.[prop] }, } ) } public async beforeAll(): Promise { try { this.setupProcessHandlers() process.env.LOG_LEVEL = "error" } catch (error) { await this.cleanup() throw error } } public async beforeEach(): Promise { try { if (this.moduleModels.length) { await this.MikroOrmWrapper.setupDatabase() } if (this.hooks?.beforeModuleInit) { await this.hooks.beforeModuleInit() } const output = await initModules(this.moduleOptionsConfig) this.shutdown = output.shutdown this.medusaApp = output.medusaApp this.moduleService = output.medusaApp.modules[this.moduleName] if (this.hooks?.afterModuleInit) { await this.hooks.afterModuleInit(this.medusaApp, this.moduleService) } } catch (error) { logger.error("Error in beforeEach:", error?.message) await this.cleanup() throw error } } public async afterEach(): Promise { try { if (this.moduleModels.length) { await this.MikroOrmWrapper.clearDatabase() } await this.shutdown() this.moduleService = {} this.medusaApp = {} } catch (error) { logger.error("Error in afterEach:", error?.message) throw error } } public async cleanup(): Promise { try { process.removeAllListeners("SIGTERM") process.removeAllListeners("SIGINT") await (this.connection as any)?.context?.destroy() await (this.connection as any)?.destroy() this.moduleService = null this.medusaApp = null this.connection = null if (global.gc) { global.gc() } } catch (error) { logger.error("Error during cleanup:", error?.message) } } public getOptions(): SuiteOptions { return { MikroOrmWrapper: this.MikroOrmWrapper, medusaApp: this.createMedusaAppProxy(), service: this.createServiceProxy(), dbConfig: { schema: this.schema, clientUrl: this.dbConfig.clientUrl, }, } } } export function moduleIntegrationTestRunner({ moduleName, moduleModels, moduleOptions = {}, moduleDependencies, joinerConfig = [], schema = "public", dbName, debug = false, testSuite, resolve, injectedDependencies = {}, cwd, hooks, }: { moduleName: string moduleModels?: any[] moduleOptions?: Record moduleDependencies?: string[] joinerConfig?: any[] schema?: string dbName?: string injectedDependencies?: Record resolve?: string debug?: boolean cwd?: string hooks?: ModuleTestRunnerConfig["hooks"] testSuite: (options: SuiteOptions) => void }) { const runner = new ModuleTestRunner({ moduleName, moduleModels, moduleOptions, moduleDependencies, joinerConfig, schema, dbName, debug, resolve, injectedDependencies, cwd, hooks, }) return describe("", () => { let testOptions: SuiteOptions beforeAll(async () => { await runner.beforeAll() testOptions = runner.getOptions() }) beforeEach(async () => { await runner.beforeEach() }) afterEach(async () => { await runner.afterEach() }) afterAll(async () => { // Run main cleanup await runner.cleanup() // Clean references to the test options for (const key in testOptions) { if (typeof testOptions[key] === "function") { testOptions[key] = null } else if ( typeof testOptions[key] === "object" && testOptions[key] !== null ) { Object.keys(testOptions[key]).forEach((k) => { testOptions[key][k] = null }) testOptions[key] = null } } // Encourage garbage collection // @ts-ignore testOptions = null if (global.gc) { global.gc() } }) // Run test suite with options testSuite(runner.getOptions()) }) }