364 lines
9.9 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|