Files
ulitka/src/modules/comment/domain/entities/comment.entity.spec.ts
T
vadimandClaude Opus 5.5 60fb39f98f Add attachments to comments
- CommentAttachment link table (comment <-> file) + migration
- fileIds in create/update comment; update replaces the whole set,
  synced in one transaction with the comment
- CommentDto.attachments; empty for deleted comments
- Soft delete drops attachments so GC can collect the files
- Max 10 files per comment, ordered by upload time (uuid v7)
- Registers comments as a file owner
- Rename CommentAuthorExpanderService -> CommentExpanderService
  (expands authors and files)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:18:08 +03:00

123 lines
4.1 KiB
TypeScript

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([]);
});
});
});