import { Test, TestingModule } from '@nestjs/testing'; import { ConfigService } from '@nestjs/config'; import { WeComChannelService } from './wecom-channel.service'; import { NotificationPayload } from './notification-channel.interface'; describe('WeComChannelService', () => { let service: WeComChannelService; let configService: ConfigService; const defaultConfig: Record = { WECOM_WEBHOOK_URL: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=test-key', WECOM_WEBHOOK_URL_ALERT: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=alert-key', NOTIFICATION_MOCK_MODE: 'false', }; const mockConfigGet = jest.fn((key: string, defaultValue?: string) => { return defaultConfig[key] ?? defaultValue; }); beforeEach(async () => { mockConfigGet.mockClear(); mockConfigGet.mockImplementation((key: string, defaultValue?: string) => { return defaultConfig[key] ?? defaultValue; }); const module: TestingModule = await Test.createTestingModule({ providers: [ WeComChannelService, { provide: ConfigService, useValue: { get: mockConfigGet }, }, ], }).compile(); service = module.get(WeComChannelService); configService = module.get(ConfigService); }); it('should be defined', () => { expect(service).toBeDefined(); }); it('should have correct name and type', () => { expect(service.name).toBe('WeCom'); expect(service.type).toBe('wecom'); }); describe('send text message', () => { it('should send text message successfully', async () => { const httpPostSpy = jest.spyOn(service as any, 'httpPost').mockResolvedValue({ errcode: 0, errmsg: 'ok', }); const payload: NotificationPayload = { type: 'info', title: 'Test', content: 'Hello from WeCom', channel: 'wecom', }; const result = await service.send(payload); expect(result.success).toBe(true); expect(httpPostSpy).toHaveBeenCalledWith( 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=test-key', { msgtype: 'text', text: { content: '【Test】\nHello from WeCom', mentioned_list: [], }, }, ); }); it('should include title in text message content', async () => { const httpPostSpy = jest.spyOn(service as any, 'httpPost').mockResolvedValue({ errcode: 0, errmsg: 'ok', }); const payload: NotificationPayload = { type: 'info', title: 'Alert Title', content: 'Detail content', channel: 'wecom', }; await service.send(payload); expect(httpPostSpy).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ msgtype: 'text', text: expect.objectContaining({ content: expect.stringContaining('Alert Title'), }), }), ); }); }); describe('send markdown message', () => { it('should send markdown message when contentHtml is provided', async () => { const httpPostSpy = jest.spyOn(service as any, 'httpPost').mockResolvedValue({ errcode: 0, errmsg: 'ok', }); const payload: NotificationPayload = { type: 'info', title: 'Markdown Test', content: 'Plain text', contentHtml: '# Heading\n> Quote\n**Bold**', channel: 'wecom', }; const result = await service.send(payload); expect(result.success).toBe(true); expect(httpPostSpy).toHaveBeenCalledWith( expect.any(String), { msgtype: 'markdown', markdown: { content: '# Heading\n> Quote\n**Bold**', }, }, ); }); it('should send markdown message when metadata.msgtype is markdown', async () => { const httpPostSpy = jest.spyOn(service as any, 'httpPost').mockResolvedValue({ errcode: 0, errmsg: 'ok', }); const payload: NotificationPayload = { type: 'info', title: 'Markdown Test', content: '# Heading\n**Bold text**', channel: 'wecom', metadata: { msgtype: 'markdown' }, }; const result = await service.send(payload); expect(result.success).toBe(true); expect(httpPostSpy).toHaveBeenCalledWith( expect.any(String), { msgtype: 'markdown', markdown: { content: '# Heading\n**Bold text**', }, }, ); }); }); describe('mention users', () => { it('should mention specified users in text message', async () => { const httpPostSpy = jest.spyOn(service as any, 'httpPost').mockResolvedValue({ errcode: 0, errmsg: 'ok', }); const payload: NotificationPayload = { type: 'info', title: 'Mention Test', content: 'Please check this', channel: 'wecom', metadata: { mentioned_list: ['zhangsan', 'lisi'], }, }; await service.send(payload); expect(httpPostSpy).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ msgtype: 'text', text: expect.objectContaining({ mentioned_list: ['zhangsan', 'lisi'], }), }), ); }); it('should mention @all when specified', async () => { const httpPostSpy = jest.spyOn(service as any, 'httpPost').mockResolvedValue({ errcode: 0, errmsg: 'ok', }); const payload: NotificationPayload = { type: 'warning', title: 'Urgent', content: 'Everyone attention', channel: 'wecom', metadata: { mentioned_list: ['@all'], }, }; await service.send(payload); expect(httpPostSpy).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ text: expect.objectContaining({ mentioned_list: ['@all'], }), }), ); }); }); describe('multiple webhooks', () => { it('should use alert webhook when metadata.webhookKey is specified', async () => { const httpPostSpy = jest.spyOn(service as any, 'httpPost').mockResolvedValue({ errcode: 0, errmsg: 'ok', }); const payload: NotificationPayload = { type: 'error', title: 'Alert', content: 'System error', channel: 'wecom', metadata: { webhookKey: 'WECOM_WEBHOOK_URL_ALERT', }, }; await service.send(payload); expect(httpPostSpy).toHaveBeenCalledWith( 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=alert-key', expect.any(Object), ); }); }); describe('error handling', () => { it('should throw error when webhook URL is not configured', async () => { mockConfigGet.mockReturnValue(undefined); const payload: NotificationPayload = { type: 'info', title: 'Test', content: 'Hello', channel: 'wecom', }; const result = await service.send(payload); expect(result.success).toBe(false); expect(result.error).toContain('Webhook URL not configured'); }); it('should return failure when WeCom API returns error', async () => { jest.spyOn(service as any, 'httpPost').mockResolvedValue({ errcode: 40001, errmsg: 'invalid token', }); const payload: NotificationPayload = { type: 'info', title: 'Test', content: 'Hello', channel: 'wecom', }; const result = await service.send(payload); expect(result.success).toBe(false); expect(result.error).toContain('invalid token'); }); it('should handle network errors gracefully', async () => { jest.spyOn(service as any, 'httpPost').mockRejectedValue( new Error('Network timeout'), ); const payload: NotificationPayload = { type: 'info', title: 'Test', content: 'Hello', channel: 'wecom', }; const result = await service.send(payload); expect(result.success).toBe(false); expect(result.error).toContain('Network timeout'); }); }); describe('validate', () => { it('should validate payload with title and content', async () => { const payload: NotificationPayload = { type: 'info', title: 'Test', content: 'Hello', channel: 'wecom', }; const isValid = await service.validate(payload); expect(isValid).toBe(true); }); it('should invalidate payload without title or content', async () => { const payload: NotificationPayload = { type: 'info', title: '', content: '', channel: 'wecom', }; const isValid = await service.validate(payload); expect(isValid).toBe(false); }); }); describe('isAvailable', () => { it('should return true when webhook URL is configured', async () => { const available = await service.isAvailable(); expect(available).toBe(true); }); it('should return false when webhook URL is not configured', async () => { mockConfigGet.mockReturnValue(undefined); const available = await service.isAvailable(); expect(available).toBe(false); }); }); describe('mock mode', () => { it('should return mock result when NOTIFICATION_MOCK_MODE is true', async () => { mockConfigGet.mockImplementation((key: string) => { if (key === 'NOTIFICATION_MOCK_MODE') return 'true'; if (key === 'WECOM_WEBHOOK_URL') return 'https://example.com'; return undefined; }); const payload: NotificationPayload = { type: 'info', title: 'Test', content: 'Hello', channel: 'wecom', }; const result = await service.send(payload); expect(result.success).toBe(true); expect(result.message).toContain('mock'); }); }); });