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); }); });