import { describe, expect, it } from 'vitest'; import { Comment } from './comment.entity.js'; import { CommentForbiddenException } from '../exceptions/comment-forbidden.exception.js'; const buildComment = (overrides: Partial<{ authorId: string | null; deletedAt: Date | null }> = {}) => Comment.reconstitute({ id: 'comment-1', content: 'Original', issueId: 1, authorId: overrides.authorId === undefined ? 'user-1' : overrides.authorId, editedAt: null, deletedAt: overrides.deletedAt ?? null, createdAt: new Date('2026-01-01T00:00:00.000Z'), updatedAt: new Date('2026-01-01T00:00:00.000Z'), }); describe('Comment', () => { describe('create', () => { it('creates a new comment without id and edit/delete marks', () => { const comment = Comment.create({ issueId: 1, authorId: 'user-1', content: ' Hello ' }); expect(comment.id).toBeNull(); expect(comment.content).toBe('Hello'); expect(comment.editedAt).toBeNull(); expect(comment.isDeleted).toBe(false); }); it('rejects empty content', () => { expect(() => Comment.create({ issueId: 1, authorId: 'user-1', content: '' })).toThrow(); }); }); describe('edit', () => { it('changes content and sets editedAt', () => { const comment = buildComment(); expect(comment.edit('Changed', 'user-1')).toBe(true); expect(comment.content).toBe('Changed'); expect(comment.editedAt).toBeInstanceOf(Date); }); it('is a no-op when content is the same', () => { const comment = buildComment(); expect(comment.edit(' Original ', 'user-1')).toBe(false); expect(comment.editedAt).toBeNull(); }); it('forbids editing by someone other than the author', () => { expect(() => buildComment().edit('Changed', 'user-2')).toThrow(CommentForbiddenException); }); it('forbids editing when the author account is deleted', () => { expect(() => buildComment({ authorId: null }).edit('Changed', 'user-1')).toThrow(CommentForbiddenException); }); it('forbids editing a deleted comment', () => { expect(() => buildComment({ deletedAt: new Date() }).edit('Changed', 'user-1')).toThrow(); }); }); describe('delete', () => { it('soft-deletes the comment', () => { const comment = buildComment(); comment.delete('user-1'); expect(comment.isDeleted).toBe(true); expect(comment.deletedAt).toBeInstanceOf(Date); }); it('forbids deletion by someone other than the author', () => { expect(() => buildComment().delete('user-2')).toThrow(CommentForbiddenException); }); }); describe('attachments', () => { it('creates a comment with deduplicated files', () => { const comment = Comment.create({ issueId: 1, authorId: 'user-1', content: 'Hi', fileIds: ['a', 'b', 'a'] }); expect(comment.fileIds).toEqual(['a', 'b']); }); it('limits the number of files', () => { const fileIds = Array.from({ length: Comment.MAX_ATTACHMENTS + 1 }, (_, i) => `f${i}`); expect(() => Comment.create({ issueId: 1, authorId: 'user-1', content: 'Hi', fileIds })).toThrow(); }); it('treats a changed file set as an edit, even with the same text', () => { const comment = buildComment(); expect(comment.edit('Original', 'user-1', ['a'])).toBe(true); expect(comment.fileIds).toEqual(['a']); expect(comment.editedAt).toBeInstanceOf(Date); }); it('keeps files when fileIds is not passed', () => { const comment = buildComment(); comment.edit('Original', 'user-1', ['a']); expect(comment.edit('Changed', 'user-1')).toBe(true); expect(comment.fileIds).toEqual(['a']); }); it('reports no change for the same text and files', () => { const comment = buildComment(); comment.edit('Original', 'user-1', ['a', 'b']); expect(comment.edit('Original', 'user-1', ['a', 'b'])).toBe(false); }); it('drops files on delete so GC can collect them', () => { const comment = buildComment(); comment.edit('Original', 'user-1', ['a']); comment.delete('user-1'); expect(comment.fileIds).toEqual([]); }); }); });