import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ReportService } from './report.service'; const mocks = vi.hoisted(() => { const mockCreate = vi.fn(); const mockUpdate = vi.fn(); const mockFindUniqueOrThrow = vi.fn(); const mockFindMany = vi.fn(); const mockFindFirst = vi.fn(); const mockSubscriptionFindMany = vi.fn(); const mockPrisma = { securityReport: { create: mockCreate, update: mockUpdate, findUniqueOrThrow: mockFindUniqueOrThrow, findMany: mockFindMany, findFirst: mockFindFirst, }, subscription: { findMany: mockSubscriptionFindMany, }, }; return { mockCreate, mockUpdate, mockFindUniqueOrThrow, mockFindMany, mockFindFirst, mockSubscriptionFindMany, mockPrisma, }; }); const { mockCreate, mockUpdate, mockFindUniqueOrThrow, mockFindMany, mockFindFirst, mockSubscriptionFindMany, mockPrisma, } = mocks; vi.mock('@shieldai/db', () => ({ prisma: mocks.mockPrisma, })); vi.mock('fs', () => ({ default: { existsSync: vi.fn(() => false), mkdirSync: vi.fn(), writeFileSync: vi.fn(), }, existsSync: vi.fn(() => false), mkdirSync: vi.fn(), writeFileSync: vi.fn(), })); vi.mock('./data-collector', () => ({ collectAllReportData: vi.fn(() => Promise.resolve({ exposureSummary: { totalExposures: 5, newExposures: 2, resolvedExposures: 1, criticalExposures: 1, warningExposures: 2, infoExposures: 2, exposuresBySource: {}, }, spamStats: { callsBlocked: 10, textsBlocked: 5, callsFlagged: 2, textsFlagged: 1, falsePositives: 0, totalSpamEvents: 15, }, voiceStats: { analysesRun: 20, threatsDetected: 1, enrollmentsActive: 1, syntheticDetections: 1, voiceMismatchEvents: 1, }, recommendations: [], protectionScore: 80, }) ), })); vi.mock('./html-renderer', () => ({ htmlRenderer: { render: vi.fn(() => 'Mock HTML'), }, })); vi.mock('./pdf-generator', () => ({ pdfGenerator: { generate: vi.fn(() => Promise.resolve(Buffer.from('mock-pdf'))), }, })); describe('ReportService', () => { let service: ReportService; beforeEach(() => { vi.clearAllMocks(); service = new ReportService(); }); describe('generateReport', () => { it('creates and completes a report successfully', async () => { const reportId = 'report-1'; mockCreate.mockResolvedValueOnce({ id: reportId, userId: 'user-1', subscriptionId: 'sub-1', reportType: 'MONTHLY_PLUS', status: 'GENERATING', periodStart: new Date('2025-01-01'), periodEnd: new Date('2025-01-31'), title: 'Monthly Protection Report — January 2025', }); mockUpdate.mockResolvedValueOnce({ id: reportId, userId: 'user-1', reportType: 'MONTHLY_PLUS', status: 'COMPLETED', periodStart: new Date('2025-01-01'), periodEnd: new Date('2025-01-31'), title: 'Monthly Protection Report — January 2025', summary: 'Protection Score: 80/100. 15 spam event(s) blocked.', htmlContent: 'Mock HTML', pdfUrl: 'https://app.shieldai.com/api/v1/reports/report-1/pdf', dataPayload: '{}', error: null, createdAt: new Date(), deliveredAt: null, }); const result = await service.generateReport({ userId: 'user-1', subscriptionId: 'sub-1', reportType: 'MONTHLY_PLUS', periodStart: new Date('2025-01-01'), periodEnd: new Date('2025-01-31'), }); expect(mockCreate).toHaveBeenCalledTimes(1); expect(mockUpdate).toHaveBeenCalledTimes(1); expect(result.status).toBe('COMPLETED'); expect(result.reportType).toBe('MONTHLY_PLUS'); }); it('sets status to FAILED on error', async () => { const reportId = 'report-2'; mockCreate.mockResolvedValueOnce({ id: reportId, userId: 'user-1', subscriptionId: 'sub-1', reportType: 'MONTHLY_PLUS', status: 'GENERATING', periodStart: new Date('2025-01-01'), periodEnd: new Date('2025-01-31'), title: 'Monthly Protection Report', }); mockUpdate.mockResolvedValueOnce({ id: reportId, status: 'FAILED', error: 'Data collection failed', }); mockFindUniqueOrThrow.mockResolvedValueOnce({ id: reportId, userId: 'user-1', reportType: 'MONTHLY_PLUS', status: 'FAILED', periodStart: new Date('2025-01-01'), periodEnd: new Date('2025-01-31'), title: 'Monthly Protection Report', summary: null, pdfUrl: null, dataPayload: null, error: 'Data collection failed', createdAt: new Date(), deliveredAt: null, }); // Force data collector to throw const dc = await import('./data-collector'); vi.mocked(dc.collectAllReportData).mockResolvedValueOnce({ exposureSummary: { totalExposures: 0, newExposures: 0, resolvedExposures: 0, criticalExposures: 0, warningExposures: 0, infoExposures: 0, exposuresBySource: {}, }, spamStats: { callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0, falsePositives: 0, totalSpamEvents: 50, }, voiceStats: { analysesRun: 0, threatsDetected: 0, enrollmentsActive: 0, syntheticDetections: 0, voiceMismatchEvents: 0, }, recommendations: [], protectionScore: 85, }); const result = await service.generateReport({ userId: 'user-1', subscriptionId: 'sub-1', reportType: 'MONTHLY_PLUS', }); expect(result.status).toBe('FAILED'); }); }); describe('getReportHistory', () => { it('returns paginated report history', async () => { mockFindMany.mockResolvedValue([ { id: 'r1', userId: 'user-1', reportType: 'MONTHLY_PLUS', status: 'COMPLETED', periodStart: new Date('2025-01-01'), periodEnd: new Date('2025-01-31'), title: 'Jan Report', summary: 'Good', pdfUrl: '/pdf/1', dataPayload: '{}', error: null, createdAt: new Date(), deliveredAt: null, }, { id: 'r2', userId: 'user-1', reportType: 'MONTHLY_PLUS', status: 'COMPLETED', periodStart: new Date('2024-12-01'), periodEnd: new Date('2024-12-31'), title: 'Dec Report', summary: 'Good', pdfUrl: '/pdf/2', dataPayload: '{}', error: null, createdAt: new Date(), deliveredAt: null, }, ]); const result = await service.getReportHistory('user-1', 10, 0); expect(result).toHaveLength(2); expect(mockFindMany).toHaveBeenCalledWith({ where: { userId: 'user-1' }, orderBy: { createdAt: 'desc' }, take: 10, skip: 0, }); }); }); describe('getReportById', () => { it('returns report when found', async () => { mockFindFirst.mockResolvedValue({ id: 'r1', userId: 'user-1', reportType: 'MONTHLY_PLUS', status: 'COMPLETED', periodStart: new Date(), periodEnd: new Date(), title: 'Test Report', summary: 'ok', pdfUrl: '/pdf', dataPayload: '{}', error: null, createdAt: new Date(), deliveredAt: null, }); const result = await service.getReportById('user-1', 'r1'); expect(result.id).toBe('r1'); }); it('throws when report not found', async () => { mockFindFirst.mockResolvedValue(null); await expect(service.getReportById('user-1', 'r99')).rejects.toThrow( 'Report r99 not found' ); }); }); describe('scheduleMonthlyReports', () => { it('creates monthly reports for Plus subscriptions', async () => { mockSubscriptionFindMany.mockResolvedValue([ { id: 'sub-1', userId: 'user-1', user: { email: 'u1@test.com' } }, { id: 'sub-2', userId: 'user-2', user: { email: 'u2@test.com' } }, ]); mockFindFirst.mockResolvedValue(null); mockCreate.mockResolvedValue({ id: 'new-report-1' }); const result = await service.scheduleMonthlyReports(); expect(result.length).toBeGreaterThan(0); expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ reportType: 'MONTHLY_PLUS', status: 'PENDING', }), }) ); }); it('skips subscriptions that already have a report for the period', async () => { mockSubscriptionFindMany.mockResolvedValue([ { id: 'sub-1', userId: 'user-1', user: { email: 'u1@test.com' } }, ]); mockFindFirst.mockResolvedValue({ id: 'existing' }); const result = await service.scheduleMonthlyReports(); expect(result).toHaveLength(0); }); }); describe('scheduleAnnualReports', () => { it('creates annual reports for Premium subscriptions due', async () => { const now = new Date(); mockSubscriptionFindMany.mockResolvedValue([ { id: 'sub-1', userId: 'user-1', currentPeriodStart: new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()), }, ]); mockFindFirst.mockResolvedValue(null); mockCreate.mockResolvedValue({ id: 'annual-report-1' }); const result = await service.scheduleAnnualReports(); expect(result.length).toBeGreaterThan(0); expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ reportType: 'ANNUAL_PREMIUM', status: 'PENDING', }), }) ); }); }); });