arm-web/src/services/api/files.test.ts
Andrey Epifancev 4424a2b8e8 feat: типы DTO и сервисы API для файлов, документов, форм и свода (#3)
- Дополнены типы в src/types/api.ts (Page, UploadResult, FileOut, DocumentOut, StatementOut, ConsolidatedRowOut, etc.)
- Реализованы сервисы: uploadFiles, listLegalEntityFiles, fetchFileContent, getDocument, getStatement, getAllEntries, buildOsvQuery, getConsolidatedOsv, getOsvSummary, exportOsv
- Переписаны consolidations.ts (getVersionCard вместо getVersionData) и company_groups.ts (fetchRelatedGroups, fetchAllLegalEntities)
- Обновлены вызовы функций в компонентах и slices
- Добавлены unit-тесты files.test.ts и osv.test.ts (11 тестов, все PASSED)
- Типизация strict, без any, все тесты и сборка зелёные

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
2026-10-08 00:59:20 +04:00

87 lines
3.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 { 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');
});
});