import { SmsConfigService } from './sms-config.service'; function createPrismaMock() { return { smsApplication: { findMany: jest.fn().mockResolvedValue([{ id: 'app-1', tenantId: 'tenant-1', name: '应用A', status: 'active', tenant: { id: 'tenant-1', name: '租户A', code: 'TENANT-A' }, messageRecords: [{ status: 'delivered' }, { status: 'undelivered' }], }]), findUnique: jest.fn().mockResolvedValue({ id: 'app-1', tenantId: 'tenant-1', name: '应用A', status: 'active', secretHash: 'secret-hash', tenant: { id: 'tenant-1', name: '租户A', code: 'TENANT-A' }, }), }, smsSignature: { findUnique: jest.fn().mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', auditStatus: 'pending' }), update: jest.fn(), }, smsTemplate: { findUnique: jest.fn().mockResolvedValue({ id: 'tpl-1', tenantId: 'tenant-1', auditStatus: 'pending' }), update: jest.fn(), }, auditRecord: { create: jest.fn(), findMany: jest.fn(), }, user: { findUnique: jest.fn().mockResolvedValue(null), }, cmppConnectionState: { findMany: jest.fn().mockResolvedValue([{ channelId: 'channel-1', connectionId: 'conn-a', tenantId: 'tenant-1', status: 'online', currentConnections: 1, desiredConnections: 1 }]), findFirst: jest.fn().mockResolvedValue({ channelId: 'channel-1', connectionId: 'conn-a', tenantId: 'tenant-1' }), update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'conn-state-1', ...data })), }, smsChannel: { findFirst: jest.fn().mockResolvedValue({ id: 'channel-1', gatewayHost: '127.0.0.1', gatewayPort: 17890, enterpriseCode: 'EC', account: 'sp', passwordCipher: 'cipher', srcId: '10690000', cmppVersion: '3.0', config: { maxConnections: 2 }, }), }, operationLog: { create: jest.fn(), }, }; } describe('SmsConfigService', () => { it('rejects unknown reviewer ids before writing audit records', async () => { const prisma = createPrismaMock(); const service = new SmsConfigService(prisma as never); await expect(service.approveSignature('sig-1', { reviewerId: 'missing-user' })).rejects.toThrow( 'reviewerId does not reference an existing user', ); expect(prisma.smsSignature.update).not.toHaveBeenCalled(); expect(prisma.auditRecord.create).not.toHaveBeenCalled(); }); it('lists enterprise applications with real CMPP connection state', async () => { const prisma = createPrismaMock(); const service = new SmsConfigService(prisma as never); await expect(service.listApplications({ includeConnections: true })).resolves.toEqual([ expect.objectContaining({ id: 'app-1', cmppStatus: 'connected', sentToday: 2, deliveryRate: 50, cmppConnections: [expect.objectContaining({ connectionId: 'conn-a' })], }), ]); }); it('returns CMPP params from persisted application and channel config', async () => { const prisma = createPrismaMock(); const service = new SmsConfigService(prisma as never); await expect(service.getApplicationCmppParams('app-1')).resolves.toEqual(expect.objectContaining({ applicationId: 'app-1', tenantName: '租户A', gatewayHost: '127.0.0.1', gatewayPort: 17890, maxConnections: 2, })); }); it('disconnects application CMPP connections and writes operation logs', async () => { const prisma = createPrismaMock(); const service = new SmsConfigService(prisma as never); await service.disconnectApplicationConnection('app-1', 'conn-a', { status: 'disconnected', reason: 'manual' }); expect(prisma.cmppConnectionState.update).toHaveBeenCalledWith({ where: { channelId_connectionId: { channelId: 'channel-1', connectionId: 'conn-a' } }, data: expect.objectContaining({ status: 'disconnected', currentConnections: 0, lastError: 'manual' }), }); expect(prisma.operationLog.create).toHaveBeenCalledWith({ data: expect.objectContaining({ action: 'cmpp_connection.disconnected', resource: 'cmpp_connection', resourceId: 'channel-1:conn-a', }), }); }); });