diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..21ee2ef --- /dev/null +++ b/tests/README.md @@ -0,0 +1,43 @@ +# Тесты contracts-flask + +Набор pytest-тестов под актуальный код сервиса (`site/`). +Покрытие: юнит (чистая логика), интеграционные (SQLite), HTTP-эндпоинты (Flask test client), безопасность. + +## Запуск + +```bash +cd contracts-flask +pytest tests/ -q +``` + +pytest установлен в venv: `/home/naeel/nubes/contracts/.venv/bin/python -m pytest tests/ -q`. + +## Файлы + +| Файл | Что тестирует | +|---|---| +| `conftest.py` | site/ в `sys.path`; фикстура `db` — изолированная БД (временный `DB_PATH` + свежая схема); защита реальной `/tmp/contracts.db` от пересоздания | +| `test_metrics.py` | `services/metrics.py`: `normalize_date`, `check_arithmetic` (sum == price×qty), `_to_decimal`, `ClassifyMetrics` | +| `test_grouping.py` | `services/grouping.py`: `normalize_number`, `group_documents` (группировка contract+supplement, unresolved), `apply_groups` (mock db) | +| `test_llm_client.py` | `services/llm_client.py`: `FakeLLMClient` (точное/частичное совпадение, default, история вызовов), `HttpxLLMClient` | +| `test_llm.py` | `services/llm.py`: `call_llm` — парсинг plain JSON и markdown-обёрток (```json) | +| `test_classify.py` | `services/classify.py`: garbage-фильтры по имени/заголовку, `_safe_json_parse` (7 edge-case: trailing comma, chatter, пусто), `_smart_extract` | +| `test_parse.py` | `services/parse.py`: `parse_file` (text/docx/pdf, ошибки), `_parse_text` | +| `test_connection.py` | `db/connection.py`: `_pg_to_sqlite` (все замены %s/::jsonb/BOOLEAN/ILIKE/TRUE), `_extract_table` | +| `test_spec_events.py` | `db/spec_events.py`: `apply_ops` (ADD/UPDATE/DELETE/UNRESOLVED/unknown), `clear_current`, `reset`, `_hash`, `get_next_seq` | +| `test_spec_current.py` | `db/spec_current.py`: `list_by_contract` (порядок), `get_elements_json` | +| `test_process_pipeline.py` | `services/process.py`: `run_pipeline` с Fake LLM — **full_replace не дублирует строки**, **трансляция target_id→target_hash**, `_elements_to_text` | +| `test_routes.py` | HTTP-эндпоинты (Flask test client): `/health`, `/api/spec-current`, `/upload`, `/unzip-upload`, `/api/groups` | +| `test_upload_security.py` | `routes/upload_bp.py`: `_check_ext` (допустимые/недопустимые), `_unzip` (path traversal нейтрализуется, битый zip) | + +## Ключевые проверки + +- `test_full_replace_no_duplicates` — два `full_replace` подряд → в `spec_current` 3 строки, а не 6; история `spec_events` (6 событий) сохранена. +- `test_update_applies` — `target_id: "r1"` транслируется в `target_hash` → UPDATE применяется (status `applied`), а не уходит в UNRESOLVED. +- `test_path_traversal_neutralized` — `../etc/passwd` внутри ZIP → имя файла `passwd` без пути. + +## Изоляция + +- Каждый тест, работающий с БД, получает свою копию SQLite через фикстуру `db` + (monkeypatch `DB_PATH` → `tmp_path`, `init_db()`). +- LLM-вызовы в пайплайне подменяются через `monkeypatch.setattr("services.llm.call_llm", ...)` — сеть не используется. diff --git a/tests/test_connection.py b/tests/test_connection.py index c75dcdd..2275a28 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -11,7 +11,8 @@ class TestPgToSqlite: assert "::jsonb" not in out def test_boolean(self): - out = _pg_to_sqlite("CREATE TABLE t (flag BOOLEAN)") + # " BOOLEAN " заменяется только когда после слова идёт пробел + out = _pg_to_sqlite("CREATE TABLE t (flag BOOLEAN NOT NULL)") assert "BOOLEAN" not in out assert "INTEGER" in out