assets, move memories to proper location
This commit is contained in:
319
packages/report/src/report.service.test.ts
Normal file
319
packages/report/src/report.service.test.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
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',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user