Files
ShieldAI/packages/report/src/report.service.test.ts

320 lines
9.6 KiB
TypeScript

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(() => '<html>Mock HTML</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: '<html>Mock HTML</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',
}),
})
);
});
});
});