import { afterEach, describe, expect, it, vi } from 'vitest'; import { ApiError } from '../../api/client'; import type { FileOut, UploadResult } from '../../types/api'; import { hasPendingFiles, isUploadResult, summarizeUpload, uploadFiles } from './files'; const result: UploadResult = { accepted: [{ file_id: 'f1', job_id: 'j1', name: 'osv.xlsx' }], rejected: [{ name: 'dup.xlsx', code: 'DUPLICATE_FILE', message: 'Файл уже загружен', file_id: 'f0' }], }; describe('isUploadResult', () => { it('отличает UploadResult от detail-ошибки', () => { expect(isUploadResult(result)).toBe(true); expect(isUploadResult({ detail: { code: 'ENTITY_NOT_IN_PERIMETER', message: 'x' } })).toBe(false); expect(isUploadResult(null)).toBe(false); }); }); describe('summarizeUpload', () => { it('имена и причины', () => { expect(summarizeUpload(result)).toEqual({ accepted: ['osv.xlsx'], rejected: [{ name: 'dup.xlsx', reason: 'Файл уже загружен' }], }); }); }); describe('hasPendingFiles', () => { const file = (status: FileOut['status']): FileOut => ({ id: status, legal_entity_id: 'le', original_name: null, file_type: 'PDF', status, version_no: 1, size_bytes: null, uploaded_at: null, job: null, documents: [], }); it('UPLOADED/PROCESSING — ждём', () => { expect(hasPendingFiles([file('PROCESSED'), file('PROCESSING')])).toBe(true); expect(hasPendingFiles([file('UPLOADED')])).toBe(true); }); it('остальные — нет', () => { expect(hasPendingFiles([file('PROCESSED'), file('ERROR')])).toBe(false); expect(hasPendingFiles([])).toBe(false); }); }); describe('uploadFiles', () => { afterEach(() => vi.restoreAllMocks()); it('422 с UploadResult возвращается как результат', async () => { const client = await import('../../api/client'); vi.spyOn(client, 'apiFetch').mockRejectedValue(new ApiError(422, undefined, 'x', { accepted: [], rejected: result.rejected })); await expect(uploadFiles('le', [new File(['a'], 'dup.xlsx')], null)).resolves.toEqual({ accepted: [], rejected: result.rejected }); }); it('422 с detail пробрасывается', async () => { const client = await import('../../api/client'); const error = new ApiError(422, 'ENTITY_NOT_IN_PERIMETER', 'ЮЛ вне периметра', { detail: { code: 'ENTITY_NOT_IN_PERIMETER' } }); vi.spyOn(client, 'apiFetch').mockRejectedValue(error); await expect(uploadFiles('le', [new File(['a'], 'a.xlsx')], 'v1')).rejects.toBe(error); }); it('версия попадает в query, лишних полей формы нет', async () => { const client = await import('../../api/client'); const spy = vi.spyOn(client, 'apiFetch').mockResolvedValue(result); await uploadFiles('le', [new File(['a'], 'a.xlsx'), new File(['b'], 'b.pdf')], 'v1'); const [path, init] = spy.mock.calls[0]; expect(path).toBe('/legal-entities/le/files?consolidation_version_id=v1'); const form = init?.body as FormData; expect(form.getAll('files')).toHaveLength(2); const keys: string[] = []; form.forEach((_, key) => keys.push(key)); expect(keys.every((k) => k === 'files')).toBe(true); }); it('без версии — без query', async () => { const client = await import('../../api/client'); const spy = vi.spyOn(client, 'apiFetch').mockResolvedValue(result); await uploadFiles('le', [new File(['a'], 'a.xlsx')], null); expect(spy.mock.calls[0][0]).toBe('/legal-entities/le/files'); }); });