arm-web/src/services/api/files.test.ts
Andrey Epifancev 79278589d6 api: загрузка и список файлов по маршрутам консолидации (#26)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 00:53:59 +04:00

109 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { afterEach, describe, expect, it, vi } from 'vitest';
import { ApiError } from '../../api/client';
import type { FileOut, UploadResult } from '../../types/api';
import { filesPath, hasPendingFiles, isUploadResult, listLegalEntityFiles, 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,
consolidation_id: 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('filesPath', () => {
it('с консолидацией — маршрут консолидации, без неё — маршрут ЮЛ', () => {
expect(filesPath('le', 'c1')).toBe('/consolidations/c1/legal-entities/le/files');
expect(filesPath('le', null)).toBe('/legal-entities/le/files');
});
});
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')], 'c1')).resolves.toEqual({ accepted: [], rejected: result.rejected });
});
it('422 с detail и 409 пробрасываются', async () => {
const client = await import('../../api/client');
const error = new ApiError(409, 'CONSOLIDATION_NOT_EDITABLE', 'нет версии в работе', {
detail: { code: 'CONSOLIDATION_NOT_EDITABLE' },
});
vi.spyOn(client, 'apiFetch').mockRejectedValue(error);
await expect(uploadFiles('le', [new File(['a'], 'a.xlsx')], 'c1')).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')], 'c1');
const [path, init] = spy.mock.calls[0];
expect(path).toBe('/consolidations/c1/legal-entities/le/files');
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');
});
});
describe('listLegalEntityFiles', () => {
afterEach(() => vi.restoreAllMocks());
it('стол консолидации и список ЮЛ', async () => {
const client = await import('../../api/client');
const spy = vi.spyOn(client, 'apiFetch').mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
await listLegalEntityFiles('le', 'c1');
await listLegalEntityFiles('le', null);
expect(spy.mock.calls[0][0]).toBe('/consolidations/c1/legal-entities/le/files?limit=100');
expect(spy.mock.calls[1][0]).toBe('/legal-entities/le/files?limit=100');
});
});