fischerX/services/api/src/modules/notification/channels/wecom-channel.service.spec.ts

364 lines
9.9 KiB
TypeScript

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<string, string> = {
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>(WeComChannelService);
configService = module.get<ConfigService>(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');
});
});
});