From 3981581c45f7342c95fb8dc1260d1f7982cb3815 Mon Sep 17 00:00:00 2001 From: albert Date: Mon, 28 Jul 2025 16:30:28 +0200 Subject: [PATCH] * adds complete tests for database * adds healtCheck and connection status related methods DatabaseConnection class --- src/__tests__/database.test.ts | 224 ++++++++++++++++++++++++++++++++- src/config/database.ts | 134 +++++++++++++------- 2 files changed, 306 insertions(+), 52 deletions(-) diff --git a/src/__tests__/database.test.ts b/src/__tests__/database.test.ts index 79e8580..807bd6b 100644 --- a/src/__tests__/database.test.ts +++ b/src/__tests__/database.test.ts @@ -1,6 +1,220 @@ -// test temporal para probar que jest funciona +import mongoose from 'mongoose'; +import { DatabaseConnection } from '../config/database.js'; +import { Logger } from '../utils/logger.js'; + +// Mock mongoose +jest.mock('mongoose', () => { + const mockConnection = { + readyState: 0, + on: jest.fn(), + once: jest.fn() + }; + + return { + connect: jest.fn(), + disconnect: jest.fn(), + connection: mockConnection + }; +}); + +// Mock Logger +jest.mock('../utils/logger.js', () => ({ + Logger: { + database: { + connected: jest.fn(), + disconnected: jest.fn(), + reconnected: jest.fn(), + alreadyConnected: jest.fn(), + notConnected: jest.fn() + }, + error: jest.fn() + } +})); + +// Mock config +jest.mock('../config/config.js', () => ({ + config: { + mongodbUri: 'mongodb://localhost:27017/test' + } +})); + describe('DatabaseConnection', () => { - describe('Singleton Pattern', () => { - test('should return the same instance', () => {}) - }) -}) \ No newline at end of file + let mockMongoose: jest.Mocked; + let mockLogger: jest.Mocked; + let databaseConnection: DatabaseConnection; + + beforeEach(() => { + jest.clearAllMocks(); + mockMongoose = mongoose as jest.Mocked; + mockLogger = Logger as jest.Mocked; + + // Reset singleton instance + (DatabaseConnection as any).instance = null; + databaseConnection = DatabaseConnection.getInstance(); + }); + + describe('Singleton Pattern', () => { + test('should return the same instance', () => { + const instance1 = DatabaseConnection.getInstance(); + const instance2 = DatabaseConnection.getInstance(); + expect(instance1).toBe(instance2); + }); + }); + + describe('connect', () => { + test('should connect to MongoDB successfully', async () => { + mockMongoose.connect.mockResolvedValueOnce(mockMongoose); + Object.defineProperty(mockMongoose.connection, 'readyState', { value: 1, writable: true }); + + await databaseConnection.connect(); + + expect(mockMongoose.connect).toHaveBeenCalledWith('mongodb://localhost:27017/test', { + maxPoolSize: 10, + serverSelectionTimeoutMS: 5000, + socketTimeoutMS: 45000, + bufferCommands: false + }); + expect(mockLogger.database.connected).toHaveBeenCalled(); + }); + + test('should not connect if already connected', async () => { + // Set as already connected + (databaseConnection as any).isConnected = true; + + await databaseConnection.connect(); + + expect(mockMongoose.connect).not.toHaveBeenCalled(); + expect(mockLogger.database.alreadyConnected).toHaveBeenCalled(); + }); + + test('should handle connection errors', async () => { + const error = new Error('Connection failed'); + mockMongoose.connect.mockRejectedValueOnce(error); + + await expect(databaseConnection.connect()).rejects.toThrow('Connection failed'); + expect(mockLogger.error).toHaveBeenCalledWith('Failed to connect to MongoDB', { error }); + }); + + test('should set up connection event listeners', async () => { + mockMongoose.connect.mockResolvedValueOnce(mockMongoose); + Object.defineProperty(mockMongoose.connection, 'readyState', { value: 1, writable: true }); + + await databaseConnection.connect(); + + expect(mockMongoose.connection.on).toHaveBeenCalledWith('error', expect.any(Function)); + expect(mockMongoose.connection.on).toHaveBeenCalledWith('disconnected', expect.any(Function)); + expect(mockMongoose.connection.on).toHaveBeenCalledWith('reconnected', expect.any(Function)); + }); + }); + + describe('disconnect', () => { + test('should disconnect from MongoDB successfully', async () => { + // Set as connected + (databaseConnection as any).isConnected = true; + mockMongoose.disconnect.mockResolvedValueOnce(undefined); + + await databaseConnection.disconnect(); + + expect(mockMongoose.disconnect).toHaveBeenCalled(); + expect(mockLogger.database.disconnected).toHaveBeenCalled(); + expect((databaseConnection as any).isConnected).toBe(false); + }); + + test('should not disconnect if not connected', async () => { + // Set as not connected + (databaseConnection as any).isConnected = false; + + await databaseConnection.disconnect(); + + expect(mockMongoose.disconnect).not.toHaveBeenCalled(); + expect(mockLogger.database.notConnected).toHaveBeenCalled(); + }); + + test('should handle disconnection errors', async () => { + const error = new Error('Disconnection failed'); + (databaseConnection as any).isConnected = true; + mockMongoose.disconnect.mockRejectedValueOnce(error); + + await expect(databaseConnection.disconnect()).rejects.toThrow('Disconnection failed'); + expect(mockLogger.error).toHaveBeenCalledWith('Error disconnecting from MongoDB', { error }); + }); + }); + + describe('isConnected', () => { + test('should return true when connected', () => { + (databaseConnection as any).isConnected = true; + expect(databaseConnection.getConnectionStatus()).toBe(true); + }); + + test('should return false when not connected', () => { + (databaseConnection as any).isConnected = false; + expect(databaseConnection.getConnectionStatus()).toBe(false); + }); + }); + + describe('Event Handlers', () => { + test('should handle connection error event', async () => { + mockMongoose.connect.mockResolvedValueOnce(mockMongoose); + Object.defineProperty(mockMongoose.connection, 'readyState', { value: 1, writable: true }); + + let errorHandler: Function; + (mockMongoose.connection.on as jest.Mock).mockImplementation((event, handler) => { + if (event === 'error') { + errorHandler = handler; + } + return mockMongoose.connection; + }); + + await databaseConnection.connect(); + + // Simulate error event + const testError = new Error('Connection error'); + errorHandler!(testError); + + expect(mockLogger.error).toHaveBeenCalledWith('MongoDB connection error', { error: testError }); + expect((databaseConnection as any).isConnected).toBe(false); + }); + + test('should handle disconnected event', async () => { + mockMongoose.connect.mockResolvedValueOnce(mockMongoose); + Object.defineProperty(mockMongoose.connection, 'readyState', { value: 1, writable: true }); + + let disconnectedHandler: Function; + (mockMongoose.connection.on as jest.Mock).mockImplementation((event, handler) => { + if (event === 'disconnected') { + disconnectedHandler = handler; + } + return mockMongoose.connection; + }); + + await databaseConnection.connect(); + + // Simulate disconnected event + disconnectedHandler!(); + + expect(mockLogger.database.disconnected).toHaveBeenCalled(); + expect((databaseConnection as any).isConnected).toBe(false); + }); + + test('should handle reconnected event', async () => { + mockMongoose.connect.mockResolvedValueOnce(mockMongoose); + Object.defineProperty(mockMongoose.connection, 'readyState', { value: 1, writable: true }); + + let reconnectedHandler: Function; + (mockMongoose.connection.on as jest.Mock).mockImplementation((event, handler) => { + if (event === 'reconnected') { + reconnectedHandler = handler; + } + return mockMongoose.connection; + }); + + await databaseConnection.connect(); + + // Simulate reconnected event + reconnectedHandler!(); + + expect(mockLogger.database.reconnected).toHaveBeenCalled(); + expect((databaseConnection as any).isConnected).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/src/config/database.ts b/src/config/database.ts index 84a8e54..f566719 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -1,55 +1,95 @@ import mongoose from 'mongoose'; -import config from './config' +import { config } from '../config/config.js'; +import { Logger } from '../utils/logger.js'; export class DatabaseConnection { - private static instance: DatabaseConnection; - private isConnected: boolean = false; // a implementar - - private constructor() {} - - public static getInstance(): DatabaseConnection { - if (!DatabaseConnection.instance) { - DatabaseConnection.instance = new DatabaseConnection(); - } - return DatabaseConnection.instance; + private static instance: DatabaseConnection; + private isConnected: boolean = false; + + private constructor() {} + + public static getInstance(): DatabaseConnection { + if (!DatabaseConnection.instance) { + DatabaseConnection.instance = new DatabaseConnection(); + } + return DatabaseConnection.instance; + } + + public async connect(): Promise { + if (this.isConnected) { + Logger.database.alreadyConnected(); + return; } - public async connect(): Promise { - if (this.isConnected) { - console.log("database is already connected") - return; - } - - try { - await mongoose.connect(config.mongodbUri, { - maxPoolSize: 10, - serverSelectionTimeoutMS: 5000, - socketTimeoutMS: 45000, - bufferCommands: false, - }); - - this.isConnected = true; - console.log("database connected") - - mongoose.connection.on('error', (error) => { - console.log('MongoDB connection error', { error }); - this.isConnected = false; - }); - - mongoose.connection.on('disconnected', () => { - console.log('MongoDB connection disconnected'); - this.isConnected = false; - }); - - mongoose.connection.on('reconnected', () => { - console.log('MongoDB connection reconnected'); - this.isConnected = true; - }); - - } catch (error) { - console.log('MongoDB Failed to connect', { error }); + try { + await mongoose.connect(config.mongodbUri, { + maxPoolSize: 10, + serverSelectionTimeoutMS: 5000, + socketTimeoutMS: 45000, + bufferCommands: false, + }); + + this.isConnected = true; + Logger.database.connected(); + + mongoose.connection.on('error', (error) => { + Logger.error('MongoDB connection error', { error }); this.isConnected = false; - throw error; - } + }); + + mongoose.connection.on('disconnected', () => { + Logger.database.disconnected(); + this.isConnected = false; + }); + + mongoose.connection.on('reconnected', () => { + Logger.database.reconnected(); + this.isConnected = true; + }); + + } catch (error) { + Logger.error('Failed to connect to MongoDB', { error }); + this.isConnected = false; + throw error; } + } + + public async disconnect(): Promise { + if (!this.isConnected) { + Logger.database.notConnected(); + return; + } + + try { + await mongoose.disconnect(); + this.isConnected = false; + Logger.database.disconnected(); + } catch (error) { + Logger.error('Error disconnecting from MongoDB', { error }); + throw error; + } + } + + public getConnectionStatus(): boolean { + return this.isConnected && mongoose.connection.readyState === 1; + } + + public async healthCheck(): Promise<{ status: string; message: string }> { + try { + if (!this.isConnected) { + return { status: 'error', message: 'Database not connected' }; + } + + await mongoose.connection.db?.admin().ping(); + return { status: 'ok', message: 'Database connection is healthy' }; + } catch (error) { + return { + status: 'error', + message: `Database health check failed: ${error instanceof Error ? error.message : 'Unknown error'}` + }; + } + } } + +export const database = DatabaseConnection.getInstance(); +export default database; \ No newline at end of file