import { SignatureRetirementService } from './signature-retirement.service'; describe('daily application messages and date formatting', () => { function setup() { const detections = [ { id: 'a', tenantId: 't1', applicationId: 'app1', dimensionType: 'enterprise' }, { id: 'b', tenantId: 't1', applicationId: 'app1', dimensionType: 'channel' }, { id: 'c', tenantId: 't1', applicationId: 'app2', dimensionType: 'enterprise' }, { id: 'd', tenantId: 't1', applicationId: null, dimensionType: 'enterprise' }, { id: 'e', tenantId: 't2', applicationId: null, dimensionType: 'enterprise' }, ].map((item) => ({ ...item, cycleId: `cycle-${item.id}`, notificationTitle: '预警', notificationContent: `冻结正文${item.id}`, })); const prisma = { signatureAnalyticsRun: { findUnique: jest.fn().mockResolvedValue({ state: 'succeeded' }) }, signatureRetirementDetection: { findMany: jest.fn().mockResolvedValue(detections) }, smsApplication: { findMany: jest.fn().mockResolvedValue([ { id: 'app1', name: '应用一' }, { id: 'app2', name: '应用二' }, ]), }, signatureRetirementMessage: { findFirst: jest.fn().mockResolvedValue(null), create: jest.fn().mockResolvedValue({}), findMany: jest.fn().mockResolvedValue([]), }, signatureRetirementWebhook: { findMany: jest.fn().mockResolvedValue([]) }, }; return { prisma, service: new SignatureRetirementService(prisma as never) }; } it('creates one frozen message per application per day including all dimensions and separate unbound tenants', async () => { const { prisma, service } = setup(); expect(await service.publishNotifications('2026-09-09')).toEqual({ notificationDate: '2026-09-09', created: 4 }); const data = prisma.signatureRetirementMessage.create.mock.calls.map(([arg]) => arg.data); expect(data[0]).toEqual( expect.objectContaining({ title: '应用一 · 签名清退预警', detectionIds: ['a', 'b'], content: '冻结正文a\n冻结正文b', }), ); expect(new Set(data.map((item) => item.dailyGroupKey)).size).toBe(4); expect(data.every((item) => item.notificationDate.toISOString() === '2026-09-09T00:00:00.000Z')).toBe(true); }); it('does not regenerate already-published or historical single-detection messages', async () => { const { prisma, service } = setup(); prisma.signatureRetirementMessage.findFirst.mockResolvedValue({ id: 'existing' }); expect((await service.publishNotifications('2026-09-09')).created).toBe(0); expect(prisma.signatureRetirementMessage.create).not.toHaveBeenCalled(); }); it.each([ ['2024-03-01', '2024-02-29'], ['2026-01-01', '2025-12-31'], ['2026-09-01', '2026-08-31'], ])('keeps T-1 semantics for %s without constructing formatters per row', async (date, expected) => { const prisma = { signatureAnalyticsRun: { findUnique: jest.fn().mockResolvedValue({ state: 'succeeded' }) }, signatureRetirementDetection: { findMany: jest.fn().mockResolvedValue( Array.from({ length: 100 }, (_, i) => ({ id: String(i), detectionDate: new Date(`${date}T00:00:00Z`), signatureId: 's', tenantId: 't', })), ), }, smsSignature: { findMany: jest.fn().mockResolvedValue([]) }, smsChannel: { findMany: jest.fn().mockResolvedValue([]) }, tenant: { findMany: jest.fn().mockResolvedValue([]) }, channelSignatureReportTask: { findMany: jest.fn().mockResolvedValue([]) }, }; const spy = jest.spyOn(Intl, 'DateTimeFormat'); try { const result = await new SignatureRetirementService(prisma as never).heatmap(date); expect(result.items.every((item) => item.activityDate === expected)).toBe(true); expect(spy).not.toHaveBeenCalled(); } finally { spy.mockRestore(); } }); });