create issues fix
Gitea Actions / Deploy (push) Canceled after 5m13s

issue observers

Наблюдатели задачи видят её и комментируют, но не редактируют.

- таблица issue_observer, наблюдатели — часть сущности Issue
- видимость: reporter, assignee или наблюдатель (задачи, история, SSE)
- правка/удаление задачи и вложений — только reporter/assignee (403 наблюдателю)
- observers в create/update, DELETE /issues/:id/observers/me, фильтр role
- история изменений и уведомления новым наблюдателям

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vadimandClaude Opus 5.5 committed 2026-10-25 22:23:49 +03:00
1 parent 46903e9ec5
commit b1f6b61994
54 files changed
+901 -69

No files matched your search

@@ -0,0 +1,75 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { IIssueRepository } from '../../../domain/repositories/issue.repository.interface.js';
import { Issue } from '../../../domain/entities/issue.entity.js';
import { IssueNotFoundException } from '../../../domain/exceptions/issue-not-found.exception.js';
import { IssueEventsService } from '../../services/issue-events.service.js';
import { UnobserveIssueUseCase } from './unobserve-issue.use-case.js';
import { UnobserveIssueCommand } from './unobserve-issue.command.js';
const buildIssue = () =>
Issue.reconstitute({
id: 1,
key: 'ISSUE-1',
title: 'Title',
content: null,
status: 'OPEN',
priority: 0,
type: 'TASK',
assignee: null,
reporter: 'user-1',
observers: ['user-5', 'user-6'],
projectId: 'project-1',
dueDate: null,
createdAt: new Date(),
updatedAt: new Date(),
});
describe('UnobserveIssueUseCase', () => {
let repository: IIssueRepository;
let issueEventsService: IssueEventsService;
let useCase: UnobserveIssueUseCase;
beforeEach(() => {
repository = {
findById: vi.fn(),
findByCKey: vi.fn(),
findAll: vi.fn(),
findByDueDateRange: vi.fn(),
countByStatus: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
};
issueEventsService = new IssueEventsService();
useCase = new UnobserveIssueUseCase(repository, issueEventsService);
});
it('removes the current user from observers on behalf of themselves', async () => {
vi.mocked(repository.findById).mockResolvedValue(buildIssue());
const received: unknown[] = [];
issueEventsService.onIssueUpdated().subscribe((issue) => received.push(issue));
await useCase.execute(new UnobserveIssueCommand(1, 'user-5'));
const [saved, actorId] = vi.mocked(repository.update).mock.calls[0]!;
expect(saved.observers).toEqual(['user-6']);
expect(actorId).toBe('user-5');
expect(received).toEqual([expect.objectContaining({ observerIds: ['user-6'] })]);
});
it('does nothing for a user who is not an observer', async () => {
vi.mocked(repository.findById).mockResolvedValue(buildIssue());
await useCase.execute(new UnobserveIssueCommand(1, 'user-1'));
expect(repository.update).not.toHaveBeenCalled();
});
it('throws when the issue is not visible', async () => {
vi.mocked(repository.findById).mockResolvedValue(null);
await expect(useCase.execute(new UnobserveIssueCommand(1, 'user-9'))).rejects.toBeInstanceOf(
IssueNotFoundException,
);
});
});