import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { ICommentRepository } from '../../../domain/repositories/comment.repository.interface.js'; import type { IIssueRepository } from '../../../../issue/domain/repositories/issue.repository.interface.js'; import { CommentNotFoundException } from '../../../domain/exceptions/comment-not-found.exception.js'; import { CommentForbiddenException } from '../../../domain/exceptions/comment-forbidden.exception.js'; import { CommentEventsService } from '../../services/comment-events.service.js'; import { CommentAccessService } from '../../services/comment-access.service.js'; import { UpdateCommentUseCase } from './update-comment.use-case.js'; import { UpdateCommentCommand } from './update-comment.command.js'; import { buildComment, buildIssue, mockCommentExpander, mockCommentRepository, mockFileAccess, mockIssueRepository, } from '../test-helpers.js'; import { FileNotFoundException } from '../../../../file/domain/exceptions/file-not-found.exception.js'; import type { FileAccessService } from '../../../../file/application/services/file-access.service.js'; describe('UpdateCommentUseCase', () => { let commentRepository: ICommentRepository; let issueRepository: IIssueRepository; let commentEventsService: CommentEventsService; let useCase: UpdateCommentUseCase; let received: unknown[]; let fileAccess: FileAccessService; beforeEach(() => { commentRepository = mockCommentRepository(); issueRepository = mockIssueRepository(); commentEventsService = new CommentEventsService(); fileAccess = mockFileAccess({ gone: new FileNotFoundException('gone') }); useCase = new UpdateCommentUseCase( commentRepository, new CommentAccessService(commentRepository, issueRepository), mockCommentExpander(), commentEventsService, fileAccess, ); received = []; commentEventsService.onEvents().subscribe((event) => received.push(event)); vi.mocked(issueRepository.findById).mockResolvedValue(buildIssue()); }); it('updates the content and emits comment.updated', async () => { vi.mocked(commentRepository.findById).mockResolvedValue(buildComment()); const dto = await useCase.execute(new UpdateCommentCommand('comment-1', 'user-1', 'Changed')); expect(dto.content).toBe('Changed'); expect(dto.editedAt).toBeInstanceOf(Date); expect(commentRepository.update).toHaveBeenCalledTimes(1); expect(received).toEqual([{ type: 'comment.updated', comment: dto }]); }); it('skips saving and events when content did not change', async () => { vi.mocked(commentRepository.findById).mockResolvedValue(buildComment()); await useCase.execute(new UpdateCommentCommand('comment-1', 'user-1', 'Original')); expect(commentRepository.update).not.toHaveBeenCalled(); expect(received).toEqual([]); }); it('throws CommentForbiddenException for a non-author', async () => { vi.mocked(commentRepository.findById).mockResolvedValue(buildComment()); await expect(useCase.execute(new UpdateCommentCommand('comment-1', 'user-2', 'Changed'))).rejects.toThrow( CommentForbiddenException, ); expect(commentRepository.update).not.toHaveBeenCalled(); }); it('throws CommentNotFoundException for a deleted comment', async () => { vi.mocked(commentRepository.findById).mockResolvedValue(buildComment({ deletedAt: new Date() })); await expect(useCase.execute(new UpdateCommentCommand('comment-1', 'user-1', 'Changed'))).rejects.toThrow( CommentNotFoundException, ); }); it('throws CommentNotFoundException when the issue is not visible to the user', async () => { vi.mocked(commentRepository.findById).mockResolvedValue(buildComment()); vi.mocked(issueRepository.findById).mockResolvedValue(null); await expect(useCase.execute(new UpdateCommentCommand('comment-1', 'user-1', 'Changed'))).rejects.toThrow( CommentNotFoundException, ); }); it('replaces attachments, checking only newly added files', async () => { const comment = buildComment(); comment.edit('Original', 'user-1', ['kept', 'removed']); vi.mocked(commentRepository.findById).mockResolvedValue(comment); await useCase.execute(new UpdateCommentCommand('comment-1', 'user-1', 'Original', ['kept', 'new'])); expect(fileAccess.getAttachableFile).toHaveBeenCalledExactlyOnceWith('new', 'user-1'); expect(vi.mocked(commentRepository.update).mock.calls[0]![0].fileIds).toEqual(['kept', 'new']); expect(received).toHaveLength(1); }); it('does not save when an added file is gone', async () => { vi.mocked(commentRepository.findById).mockResolvedValue(buildComment()); await expect( useCase.execute(new UpdateCommentCommand('comment-1', 'user-1', 'Original', ['gone'])), ).rejects.toThrow(FileNotFoundException); expect(commentRepository.update).not.toHaveBeenCalled(); }); });