kt-template-online-api/test/admin/notice/admin-notice.service.spec.ts

232 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { AdminNoticeService } from '@/modules/admin/platform-config/notice/admin-notice.service';
import { ToolsService } from '@/common';
/**
* 创建 测试断言对象或配置。
* @param overrides - Admin列表构造 Jest mock 返回值。
*/
function createRepositoryMock(overrides: Record<string, jest.Mock> = {}) {
const repository = {
createQueryBuilder: jest.fn(),
create: jest.fn((entity) => entity),
findOne: jest.fn().mockResolvedValue(null),
merge: jest.fn((target, input) => ({ ...target, ...input })),
save: jest.fn(async (entity) => ({
id: entity.id || 'notice-1',
...entity,
})),
update: jest.fn(),
...overrides,
};
return repository;
}
describe('AdminNoticeService system event notices', () => {
it('publishes API errors as unread super role notices', async () => {
const repository = createRepositoryMock();
const service = new AdminNoticeService(
repository as any,
new ToolsService(),
);
const id = await (service as any).publishSystemNotice({
content: 'Error: boom',
dedupeKey: 'api:error:GET:/boom:500',
eventType: 'api.error',
metadata: {
method: 'GET',
path: '/boom',
requestId: 'req-1',
statusCode: 500,
},
severity: 'error',
source: 'api',
summary: '500 GET /boom',
title: '接口错误GET /boom',
});
expect(id).toBe('notice-1');
expect(repository.create).toHaveBeenCalledWith(
expect.objectContaining({
content: 'Error: boom',
dedupeKey: 'api:error:GET:/boom:500',
eventType: 'api.error',
level: 3,
metadata: expect.objectContaining({
method: 'GET',
path: '/boom',
requestId: 'req-1',
statusCode: 500,
}),
notifyRoleCode: 'super',
occurrenceCount: 1,
severity: 'error',
source: 'api',
status: 1,
summary: '500 GET /boom',
title: '接口错误GET /boom',
}),
);
});
it('bounds long system notice title and dedupe key to database column sizes', async () => {
const repository = createRepositoryMock();
const service = new AdminNoticeService(
repository as any,
new ToolsService(),
);
const longPath = `/system/${'very-long-path-'.repeat(40)}`;
await (service as any).publishSystemNotice({
content: 'database unavailable',
dedupeKey: `api:error:POST:${longPath}:500`,
eventType: 'api.error',
severity: 'error',
source: 'api',
summary: `500 POST ${longPath}`,
title: `接口错误POST ${longPath}`,
});
const notice = repository.create.mock.calls[0][0];
expect(notice.dedupeKey).toHaveLength(255);
expect(notice.title.length).toBeLessThanOrEqual(255);
expect(notice.dedupeKey).toMatch(/#[a-f0-9]{12}$/);
});
it('aggregates repeated events by dedupe key instead of creating noise', async () => {
const existingNotice = {
content: 'old',
dedupeKey: 'qqbot:offline:1914728559',
id: 'notice-1',
isDeleted: false,
occurrenceCount: 2,
status: 0,
summary: 'old',
title: 'old',
};
const repository = createRepositoryMock({
findOne: jest.fn().mockResolvedValue(existingNotice),
});
const updateBuilder = {
andWhere: jest.fn().mockReturnThis(),
execute: jest.fn().mockResolvedValue({ affected: 1 }),
set: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
};
repository.createQueryBuilder.mockReturnValue(updateBuilder);
const service = new AdminNoticeService(
repository as any,
new ToolsService(),
);
const id = await (service as any).publishSystemNotice({
content: 'bot_offline/kick_offline账号已在另一台终端登录',
dedupeKey: 'qqbot:offline:1914728559',
eventType: 'qqbot.account.offline',
metadata: {
selfId: '1914728559',
},
severity: 'error',
source: 'qqbot',
summary: '账号已在另一台终端登录',
title: 'QQBot 账号已下线1914728559',
});
expect(id).toBe('notice-1');
expect(repository.create).not.toHaveBeenCalled();
expect(updateBuilder.set).toHaveBeenCalledWith(
expect.objectContaining({
content: 'bot_offline/kick_offline账号已在另一台终端登录',
metadata: expect.objectContaining({
selfId: '1914728559',
}),
occurrenceCount: expect.any(Function),
severity: 'error',
status: 1,
summary: '账号已在另一台终端登录',
title: 'QQBot 账号已下线1914728559',
}),
);
expect(updateBuilder.where).toHaveBeenCalledWith('id = :id', {
id: 'notice-1',
});
expect(updateBuilder.execute).toHaveBeenCalled();
});
it('aggregates when a concurrent insert hits the dedupe unique key', async () => {
const duplicateError = Object.assign(new Error('Duplicate entry'), {
code: 'ER_DUP_ENTRY',
});
const repository = createRepositoryMock({
findOne: jest.fn().mockResolvedValueOnce(null).mockResolvedValueOnce({
dedupeKey: 'api:error:GET:/boom:500',
id: 'notice-1',
isDeleted: false,
occurrenceCount: 1,
}),
save: jest.fn().mockRejectedValueOnce(duplicateError),
});
const updateBuilder = {
andWhere: jest.fn().mockReturnThis(),
execute: jest.fn().mockResolvedValue({ affected: 1 }),
set: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
};
repository.createQueryBuilder.mockReturnValue(updateBuilder);
const service = new AdminNoticeService(
repository as any,
new ToolsService(),
);
const id = await (service as any).publishSystemNotice({
content: 'Error: boom',
dedupeKey: 'api:error:GET:/boom:500',
eventType: 'api.error',
severity: 'error',
source: 'api',
summary: '500 GET /boom',
title: '接口错误GET /boom',
});
expect(id).toBe('notice-1');
expect(repository.findOne).toHaveBeenCalledTimes(2);
expect(updateBuilder.execute).toHaveBeenCalled();
});
it('orders event notices by the latest occurrence time', async () => {
const builder = {
addOrderBy: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
};
const repository = createRepositoryMock({
createQueryBuilder: jest.fn().mockReturnValue(builder),
});
const service = new AdminNoticeService(
repository as any,
new ToolsService(),
);
await service.page({});
expect(builder.orderBy).toHaveBeenCalledWith('notice.isTop', 'DESC');
expect(builder.addOrderBy).toHaveBeenNthCalledWith(
1,
'notice.lastSeenAt',
'DESC',
);
expect(builder.addOrderBy).toHaveBeenNthCalledWith(
2,
'notice.createTime',
'DESC',
);
});
});