feat(v2.7): 增加通知与评论协作模块

This commit is contained in:
2026-07-08 16:13:33 +08:00
parent ad36ffda17
commit 95523cd4a1
13 changed files with 586 additions and 1 deletions

View File

@@ -0,0 +1,112 @@
import { BadRequestException } from '@nestjs/common';
import { AuditService } from '../../common/audit/audit.service';
import { NotificationService } from '../notification/notification.service';
import { CommentService } from './comment.service';
describe('CommentService', () => {
const makeService = () => {
const prisma = {
comment: {
create: jest.fn(),
findUnique: jest.fn(),
update: jest.fn(),
},
projectMember: {
findMany: jest.fn(),
},
};
const notifications = {
createMany: jest.fn(),
} as unknown as NotificationService;
const audit = {
record: jest.fn(),
} as unknown as AuditService;
return {
prisma,
notifications,
audit,
service: new CommentService(prisma as any, notifications, audit),
};
};
it('creates a polymorphic comment, extracts @mentions, notifies mentioned members, and writes audit', async () => {
const { prisma, notifications, audit, service } = makeService();
prisma.projectMember.findMany.mockResolvedValue([
{ userId: 'm-alice', user: { id: 'm-alice', name: 'Alice' } },
{ userId: 'm-bob', user: { id: 'm-bob', name: 'Bob' } },
]);
prisma.comment.create.mockResolvedValue({
id: 'comment-1',
entityType: 'dev_task',
entityId: 'task-1',
mentionedMemberIds: ['m-alice', 'm-bob'],
});
const result = await service.create({
actorId: 'm-author',
entityType: 'dev_task',
entityId: 'task-1',
entityVersionId: 'ver-1',
projectId: 'project-1',
versionId: 'ver-1',
content: '请 @Alice 看一下接口Bob 也同步一下',
mentionMemberIds: ['m-bob'],
});
expect(result.id).toBe('comment-1');
expect(prisma.comment.create).toHaveBeenCalledWith({
data: expect.objectContaining({
entityType: 'dev_task',
entityId: 'task-1',
authorId: 'm-author',
mentionedMemberIds: ['m-alice', 'm-bob'],
}),
});
expect((notifications.createMany as jest.Mock)).toHaveBeenCalledWith([
expect.objectContaining({ recipientId: 'm-alice', type: 'mention', resourceType: 'comment', resourceId: 'comment-1' }),
expect.objectContaining({ recipientId: 'm-bob', type: 'mention', resourceType: 'comment', resourceId: 'comment-1' }),
]);
expect((audit.record as jest.Mock)).toHaveBeenCalledWith(expect.objectContaining({
actorId: 'm-author',
action: 'comment.created',
resourceType: 'comment',
resourceId: 'comment-1',
}));
});
it('soft deletes a comment and writes audit', async () => {
const { prisma, audit, service } = makeService();
prisma.comment.findUnique.mockResolvedValue({
id: 'comment-1',
authorId: 'm-author',
entityType: 'bug',
entityId: 'bug-1',
deletedAt: null,
});
prisma.comment.update.mockResolvedValue({ id: 'comment-1', deletedAt: new Date('2026-07-08T08:00:00.000Z') });
await service.remove('comment-1', 'm-author');
expect(prisma.comment.update).toHaveBeenCalledWith({
where: { id: 'comment-1' },
data: { deletedAt: expect.any(Date) },
});
expect((audit.record as jest.Mock)).toHaveBeenCalledWith(expect.objectContaining({
actorId: 'm-author',
action: 'comment.deleted',
resourceType: 'comment',
resourceId: 'comment-1',
}));
});
it('rejects unsupported comment entity types', async () => {
const { service } = makeService();
await expect(service.create({
actorId: 'm-author',
entityType: 'task',
entityId: 'task-1',
content: 'legacy task comment',
} as any)).rejects.toBeInstanceOf(BadRequestException);
});
});