diff --git a/server/src/services/__tests__/consultaHistoryService.test.ts b/server/src/services/__tests__/consultaHistoryService.test.ts new file mode 100644 index 0000000..ac670ee --- /dev/null +++ b/server/src/services/__tests__/consultaHistoryService.test.ts @@ -0,0 +1,69 @@ +import { ConsultaHistory } from '../../models/ConsultaHistory'; +import { ConsultaHistoryService } from '../consultaHistoryService'; + +jest.mock('../../models/ConsultaHistory', () => ({ + ConsultaHistory: { + countDocuments: jest.fn(), + aggregate: jest.fn() + } +})); + +describe('ConsultaHistoryService.getConsultaStats', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('guards daily stats aggregation by converting invalid createdAt values safely', async () => { + const tenantId = 'tenant-test'; + const countDocumentsMock = ConsultaHistory.countDocuments as jest.Mock; + const aggregateMock = ConsultaHistory.aggregate as jest.Mock; + + countDocumentsMock + .mockResolvedValueOnce(2) + .mockResolvedValueOnce(1) + .mockResolvedValueOnce(1) + .mockResolvedValueOnce(1) + .mockResolvedValueOnce(1); + + aggregateMock + .mockResolvedValueOnce([{ totalCredits: 1 }]) + .mockResolvedValueOnce([{ avgTime: 150 }]) + .mockResolvedValueOnce([{ _id: '2026-01-02', count: 1 }]) + .mockResolvedValueOnce([{ _id: '/api/rut/lookup', count: 2 }]); + + const stats = await ConsultaHistoryService.getConsultaStats(tenantId); + + expect(stats.totalConsultations).toBe(2); + expect(stats.consultationsByDay).toEqual([{ date: '2026-01-02', count: 1 }]); + expect(stats.consultationsByEndpoint).toEqual([{ endpoint: '/api/rut/lookup', count: 2 }]); + + expect(aggregateMock).toHaveBeenCalledTimes(4); + + const dailyStatsPipeline = aggregateMock.mock.calls[2][0]; + expect(dailyStatsPipeline).toEqual( + expect.arrayContaining([ + { $match: { tenantId } }, + { + $addFields: { + createdAtDate: { + $convert: { + input: '$createdAt', + to: 'date', + onError: null, + onNull: null + } + } + } + }, + { $match: { createdAtDate: { $ne: null } } } + ]) + ); + const groupStage = dailyStatsPipeline.find((stage: any) => stage.$group); + expect(groupStage.$group._id).toEqual({ + $dateToString: { + format: '%Y-%m-%d', + date: '$createdAtDate' + } + }); + }); +}); diff --git a/server/src/services/consultaHistoryService.ts b/server/src/services/consultaHistoryService.ts index 0d570cd..9caab70 100644 --- a/server/src/services/consultaHistoryService.ts +++ b/server/src/services/consultaHistoryService.ts @@ -1,6 +1,5 @@ import { ConsultaHistory, IConsultaHistory, IConsultaHistoryWithUser } from '../models/ConsultaHistory'; import { AuthenticatedRequest } from '../types/auth'; -import { Request, Response } from 'express'; import { EvaluationJob, EvaluationResult } from '../models/evaluation'; export interface ConsultaLogData { @@ -286,9 +285,22 @@ export class ConsultaHistoryService { ]), ConsultaHistory.aggregate([ { $match: query }, + { + $addFields: { + createdAtDate: { + $convert: { + input: '$createdAt', + to: 'date', + onError: null, + onNull: null + } + } + } + }, + { $match: { createdAtDate: { $ne: null } } }, { $group: { - _id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } }, + _id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAtDate' } }, count: { $sum: 1 } } }, diff --git a/server/src/setupTests.ts b/server/src/setupTests.ts index a62596d..3d15779 100644 --- a/server/src/setupTests.ts +++ b/server/src/setupTests.ts @@ -4,7 +4,7 @@ import dotenv from 'dotenv'; import { TextEncoder, TextDecoder } from 'util'; // Polyfills for Node.js environment -global.TextEncoder = TextEncoder; +global.TextEncoder = TextEncoder as any; global.TextDecoder = TextDecoder as any; // Load test environment variables @@ -57,8 +57,13 @@ jest.mock('../services/notificationService', () => ({ // Global test database setup let mongoServer: MongoMemoryServer; +const shouldStartMongoMemoryServer = process.env.SKIP_MONGO_MEMORY_SERVER !== 'true'; beforeAll(async () => { + if (!shouldStartMongoMemoryServer) { + return; + } + // Start in-memory MongoDB instance mongoServer = await MongoMemoryServer.create(); const mongoUri = mongoServer.getUri(); @@ -68,6 +73,10 @@ beforeAll(async () => { }); beforeEach(async () => { + if (!shouldStartMongoMemoryServer || mongoose.connection.readyState !== 1) { + return; + } + // Clear all collections before each test const collections = mongoose.connection.collections; for (const key in collections) { @@ -77,6 +86,10 @@ beforeEach(async () => { }); afterAll(async () => { + if (!shouldStartMongoMemoryServer || !mongoServer || mongoose.connection.readyState === 0) { + return; + } + // Cleanup await mongoose.connection.dropDatabase(); await mongoose.connection.close(); @@ -148,4 +161,4 @@ export const createMockResponse = () => { res.cookie = jest.fn().mockReturnValue(res); res.clearCookie = jest.fn().mockReturnValue(res); return res; -}; \ No newline at end of file +};