- Дополнены типы в 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
87 lines
3.5 KiB
TypeScript
87 lines
3.5 KiB
TypeScript
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');
|
||
});
|
||
});
|