diff --git a/src/datasources/db/v2/database-migrator.service.spec.ts b/src/datasources/db/v2/database-migrator.service.spec.ts index 377b21d384..a36a315401 100644 --- a/src/datasources/db/v2/database-migrator.service.spec.ts +++ b/src/datasources/db/v2/database-migrator.service.spec.ts @@ -1,8 +1,6 @@ import { join } from 'path'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { Test, type TestingModule } from '@nestjs/testing'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { postgresConfig } from '@/config/entities/postgres.config'; +import { ConfigModule } from '@nestjs/config'; import { ConfigurationModule } from '@/config/configuration.module'; import configuration from '@/config/entities/__tests__/configuration'; import { TestLoggingModule } from '@/logging/__tests__/test.logging.module'; @@ -12,14 +10,18 @@ import { LoggingService, type ILoggingService, } from '@/logging/logging.interface'; -import { DatabaseShutdownHook } from '@/datasources/db/v2/database-shutdown.hook'; -import { DatabaseInitializeHook } from '@/datasources/db/v2/database-initialize.hook'; +import type { DataSource } from 'typeorm'; +import { TestPostgresDatabaseModuleV2 } from '@/datasources/db/v2/test.postgres-database.module'; describe('PostgresDatabaseService', () => { let moduleRef: TestingModule; let postgresDatabaseService: PostgresDatabaseService; let databaseMigratorService: DatabaseMigrator; let loggingService: ILoggingService; + let connection: DataSource; + const NUMBER_OF_RETRIES = 2; + const truncateLockQuery = 'TRUNCATE TABLE "_lock";'; + const insertLockQuery = 'INSERT INTO "_lock" (status) VALUES ($1);'; beforeAll(async () => { // We should not require an SSL connection if using the database provided @@ -30,6 +32,11 @@ describe('PostgresDatabaseService', () => { ...baseConfiguration, db: { ...baseConfiguration.db, + migrator: { + numberOfRetries: NUMBER_OF_RETRIES, + migrationsExecute: false, + retryAfter: 10, + }, connection: { postgres: { ...baseConfiguration.db.connection.postgres, @@ -45,36 +52,12 @@ describe('PostgresDatabaseService', () => { moduleRef = await Test.createTestingModule({ imports: [ - TypeOrmModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (configService: ConfigService) => { - const typeormConfig = configService.getOrThrow('db.orm'); - const postgresConfigObject = postgresConfig( - configService.getOrThrow('db.connection.postgres'), - ); - - return { - ...typeormConfig, - ...postgresConfigObject, - ...{ - keepAlive: true, - migrations: ['dist/migrations/test/*.js'], - }, - }; - }, - inject: [ConfigService], - }), TestLoggingModule, ConfigurationModule.register(testConfiguration), ConfigModule, + TestPostgresDatabaseModuleV2, ], - providers: [ - PostgresDatabaseService, - DatabaseMigrator, - DatabaseInitializeHook, - DatabaseShutdownHook, - // DatabaseMigrationHook, - ], + providers: [DatabaseMigrator], }).compile(); databaseMigratorService = moduleRef.get(DatabaseMigrator); @@ -83,14 +66,19 @@ describe('PostgresDatabaseService', () => { ); loggingService = moduleRef.get(LoggingService); - await postgresDatabaseService.initializeDatabaseConnection(); + connection = postgresDatabaseService.getDataSource(); }); afterAll(async () => { - await postgresDatabaseService.destroyDatabaseConnection(); await moduleRef.close(); }); + beforeEach(() => {}); + + afterEach(() => { + jest.clearAllMocks(); + }); + describe('migrate()', () => { it('Should log the start and end of the migration process', async () => { jest.spyOn(loggingService, 'info'); @@ -101,6 +89,52 @@ describe('PostgresDatabaseService', () => { 'Migrations: Running...', ); expect(loggingService.info).toHaveBeenCalledWith('Migrations: Finished.'); + expect(loggingService.info).toHaveBeenCalledTimes(3); + }); + + it('Should run migration if no lock exists', async () => { + jest.spyOn(connection, 'query').mockImplementation(jest.fn()); + + await databaseMigratorService.migrate(); + + expect(connection.runMigrations).toHaveBeenCalled(); + expect(connection.query).toHaveBeenCalledWith(insertLockQuery, [1]); + + expect(connection.query).toHaveBeenCalledWith(truncateLockQuery); + }); + + it('Should throw an error if retries are exhausted', async () => { + connection.query = jest.fn().mockResolvedValue([{ id: 1, status: 1 }]); + + await expect(databaseMigratorService.migrate()).rejects.toThrow( + 'Migrations: Migrations are still running in another instance!', + ); + + expect(loggingService.info).toHaveBeenCalledWith( + 'Migrations: Running in another instance...', + ); + expect(loggingService.info).toHaveBeenCalledTimes(2); + }); + + it('Should not truncate locks if an error occurs', async () => { + connection.query = jest.fn().mockResolvedValue([{ id: 1, status: 1 }]); + + await expect(databaseMigratorService.migrate()).rejects.toThrow( + 'Migrations: Migrations are still running in another instance!', + ); + + expect(connection.query).not.toHaveBeenCalledWith(truncateLockQuery); + }); + + it('Should truncate locks after migrations are successful', async () => { + connection.query = jest.fn().mockResolvedValue(jest.fn()); + + await databaseMigratorService.migrate(); + + expect(loggingService.info).toHaveBeenCalledTimes(3); + expect(loggingService.info).toHaveBeenCalledWith('Migrations: Finished.'); + + expect(connection.query).toHaveBeenCalledWith(truncateLockQuery); }); }); }); diff --git a/src/datasources/db/v2/database-migrator.service.ts b/src/datasources/db/v2/database-migrator.service.ts index 60f47b612b..6764c32b96 100644 --- a/src/datasources/db/v2/database-migrator.service.ts +++ b/src/datasources/db/v2/database-migrator.service.ts @@ -40,11 +40,12 @@ export class DatabaseMigrator { public async migrate(): Promise { this.loggingService.info('Migrations: Running...'); - const connection = this.databaseService.getDataSource(); + const connection = + await this.databaseService.initializeDatabaseConnection(); await this.createLockTableIfNotExists(connection); let numberOfIterations = 0; - const numberOfRetries = this.configService.getOrThrow( + const numberOfRetries = await this.configService.getOrThrow( 'db.migrator.numberOfRetries', ); while (numberOfRetries >= numberOfIterations) { @@ -114,10 +115,6 @@ export class DatabaseMigrator { /** * Selects and retrieves locks from the database. * - * `pg_advisory_lock` are not being used since they locks are session-based, - * For our usecase this case they are quite unreliable. - * If the session is terminated, the lock is released, which could potentially cause issues in the database. - * * @param {DataSource} connection The database connection to select locks from. * * @returns {Promise>} A promise that resolves to an array of LockSchema objects. diff --git a/src/datasources/db/v2/test.postgres-database.module.ts b/src/datasources/db/v2/test.postgres-database.module.ts index 83069b3b83..ca34ed8a61 100644 --- a/src/datasources/db/v2/test.postgres-database.module.ts +++ b/src/datasources/db/v2/test.postgres-database.module.ts @@ -1,10 +1,17 @@ import { Module } from '@nestjs/common'; import { PostgresDatabaseService } from '@/datasources/db/v2/postgres-database.service'; +const postgresDataSourceMock = jest.fn().mockReturnValue({ + query: jest.fn(), + runMigrations: jest.fn(), +}); + const postgresDatabaseServiceMock = { - getDataSource: jest.fn(), + getDataSource: jest.fn().mockImplementation(postgresDataSourceMock), isInitialized: jest.fn(), - initializeDatabaseConnection: jest.fn(), + initializeDatabaseConnection: jest + .fn() + .mockImplementation(postgresDataSourceMock), destroyDatabaseConnection: jest.fn(), getRepository: jest.fn(), transaction: jest.fn(),