init
Gitea Actions Demo / Deploy (push) Failing after 18s

This commit is contained in:
vadim committed 2026-09-24 10:34:27 +03:00
commit 7b999a670c
243 files changed
+24818

No files matched your search

@@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { Observable, Subject } from 'rxjs';
import { IssueDto } from '../dto/issue.dto.js';
/**
* In-process шина событий issue-модуля для SSE-подписчиков.
* Живёт как синглтон-провайдер, поэтому все запросы в рамках
* одного инстанса приложения получают одни и те же события.
*/
@Injectable()
export class IssueEventsService {
private readonly issueCreated$ = new Subject<IssueDto>();
private readonly issueUpdated$ = new Subject<IssueDto>();
private readonly issueDeleted$ = new Subject<IssueDto>();
emitIssueCreated(issue: IssueDto): void {
this.issueCreated$.next(issue);
}
onIssueCreated(): Observable<IssueDto> {
return this.issueCreated$.asObservable();
}
emitIssueUpdated(issue: IssueDto): void {
this.issueUpdated$.next(issue);
}
onIssueUpdated(): Observable<IssueDto> {
return this.issueUpdated$.asObservable();
}
emitIssueDeleted(issue: IssueDto): void {
this.issueDeleted$.next(issue);
}
onIssueDeleted(): Observable<IssueDto> {
return this.issueDeleted$.asObservable();
}
}
@@ -0,0 +1,94 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Issue } from '../../domain/entities/issue.entity.js';
import { IssueUserExpanderService } from './issue-user-expander.service.js';
import type { GetUsersUseCase } from '../../../user/application/use-cases/get-users/get-users.use-case.js';
import type { UserDto } from '../../../user/application/dto/user.dto.js';
const FIXED_DATE = new Date('2026-01-01T00:00:00.000Z');
const buildUser = (id: string): UserDto => ({
id,
name: id,
email: `${id}@example.com`,
emailVerified: true,
image: null,
createdAt: FIXED_DATE,
updatedAt: FIXED_DATE,
});
const buildIssue = (overrides: Partial<{ assignee: string | null; reporter: string }> = {}) =>
Issue.reconstitute({
id: 1,
key: 'ISSUE-1',
title: 'Fix login bug',
content: null,
status: 'OPEN',
priority: 0,
assignee: 'assignee' in overrides ? (overrides.assignee ?? null) : 'user-2',
reporter: overrides.reporter ?? 'user-1',
projectId: 'project-1',
createdAt: new Date(),
updatedAt: new Date(),
});
describe('IssueUserExpanderService', () => {
let getUsersUseCase: GetUsersUseCase;
let service: IssueUserExpanderService;
beforeEach(() => {
getUsersUseCase = { execute: vi.fn() } as unknown as GetUsersUseCase;
service = new IssueUserExpanderService(getUsersUseCase);
});
it('resolves assignee and reporter to full users', async () => {
vi.mocked(getUsersUseCase.execute).mockResolvedValue([buildUser('user-1'), buildUser('user-2')]);
const dto = await service.expand(buildIssue());
expect(getUsersUseCase.execute).toHaveBeenCalledWith(expect.objectContaining({ ids: ['user-1', 'user-2'] }));
expect(dto.reporterId).toBe('user-1');
expect(dto.assigneeId).toBe('user-2');
expect(dto.reporter).toEqual(buildUser('user-1'));
expect(dto.assignee).toEqual(buildUser('user-2'));
});
it('resolves assignee to null when the issue has no assignee', async () => {
vi.mocked(getUsersUseCase.execute).mockResolvedValue([buildUser('user-1')]);
const dto = await service.expand(buildIssue({ assignee: null }));
expect(getUsersUseCase.execute).toHaveBeenCalledWith(expect.objectContaining({ ids: ['user-1'] }));
expect(dto.assignee).toBeNull();
});
it('requests each user id only once when reporter and assignee are the same person', async () => {
vi.mocked(getUsersUseCase.execute).mockResolvedValue([buildUser('user-1')]);
await service.expand(buildIssue({ assignee: 'user-1', reporter: 'user-1' }));
expect(getUsersUseCase.execute).toHaveBeenCalledWith(expect.objectContaining({ ids: ['user-1'] }));
});
describe('expandAll', () => {
it('resolves assignee/reporter for every issue in a single batched lookup', async () => {
vi.mocked(getUsersUseCase.execute).mockResolvedValue([buildUser('user-1'), buildUser('user-2')]);
const [first, second] = await service.expandAll([
buildIssue({ assignee: 'user-2', reporter: 'user-1' }),
buildIssue({ assignee: null, reporter: 'user-1' }),
]);
expect(getUsersUseCase.execute).toHaveBeenCalledTimes(1);
expect(getUsersUseCase.execute).toHaveBeenCalledWith(expect.objectContaining({ ids: ['user-1', 'user-2'] }));
expect(first!.assignee).toEqual(buildUser('user-2'));
expect(second!.assignee).toBeNull();
});
it('returns an empty array without calling GetUsersUseCase when there are no issues', async () => {
const result = await service.expandAll([]);
expect(result).toEqual([]);
expect(getUsersUseCase.execute).not.toHaveBeenCalled();
});
});
});
@@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import { Issue } from '../../domain/entities/issue.entity.js';
import { IssueWithUsersDto } from '../dto/issue-with-users.dto.js';
import { IssueApplicationMapper } from '../mappers/issue-application.mapper.js';
import { GetUsersUseCase } from '../../../user/application/use-cases/get-users/get-users.use-case.js';
import { GetUsersQuery } from '../../../user/application/use-cases/get-users/get-users.query.js';
/** Раскрывает assignee/reporter задачи (или списка задач) в полноценных пользователей */
@Injectable()
export class IssueUserExpanderService {
constructor(private readonly getUsersUseCase: GetUsersUseCase) {}
async expand(issue: Issue): Promise<IssueWithUsersDto> {
const [dto] = await this.expandAll([issue]);
return dto!;
}
async expandAll(issues: Issue[]): Promise<IssueWithUsersDto[]> {
if (issues.length === 0) return [];
const userIds = [
...new Set(issues.flatMap((issue) => [issue.reporter, issue.assignee]).filter((id): id is string => Boolean(id))),
];
const users = await this.getUsersUseCase.execute(new GetUsersQuery(userIds));
const usersById = new Map(users.map((user) => [user.id, user]));
return issues.map((issue) => IssueApplicationMapper.toDtoWithUsers(issue, usersById));
}
}