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