113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
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);
|
||
});
|
||
});
|