diff --git a/api/.env.example b/api/.env.example index 28ffd93..e8dd0f1 100644 --- a/api/.env.example +++ b/api/.env.example @@ -1,20 +1,57 @@ # Локальное окружение scan-gateway. Скопируйте в .env: cp .env.example .env (.env в git не попадает) -# Значения совпадают с ../docs/dev/docker-compose.yml и ../docs/dev/.env (compose: NATS 4222, MinIO 9000, бакет arm-ka). -# Файл окружения можно выбрать переменной ENV_FILE (по умолчанию .env, путь от каталога api/). +# Значения совпадают с ../docs/dev/docker-compose.yml и ../docs/dev/.env (compose: NATS 4222, MinIO 9000, бакет arm-ka); +# это только dev-умолчания. Файл окружения можно выбрать переменной ENV_FILE (по умолчанию .env, путь от каталога api/); +# переменные окружения приоритетнее файла. Тестовый набор — .env.test (его подключает pytest.ini). DEBUG=true -# MinIO: адрес без схемы (host:port) — так ждёт miniopy-async; MinIO из compose работает по http +# --- MinIO (S3): единственный бакет, в нём лежат входные файлы, raw/{job_id}.json и results/ --- +# Адрес без схемы (host:port) — так ждёт miniopy-async; MinIO из compose работает по http, поэтому MINIO_SECURE=false. +# На платформе MINIO_SECURE не задаётся (по умолчанию true — https). OPENBAO__MINIO__MINIO_URL=localhost:9000 OPENBAO__MINIO__MINIO_ADMIN_LOGIN=minioadmin OPENBAO__MINIO__MINIO_ADMIN_PASSWORD=minioadmin OPENBAO__MINIO__MINIO_BUCKET=arm-ka OPENBAO__MINIO__MINIO_SECURE=false -# NATS без JetStream; NATS_SEED/NATS_JWT не задаются (пусто — подключение без nkeys) +# --- NATS (core, без JetStream) --- +# NATS_SEED / NATS_JWT пустые: подключение без nkeys (локальная разработка, тесты). На платформе их выдаёт OpenBAO. OPENBAO__NATS__NATS_URL=nats://localhost:4222 +OPENBAO__NATS__NATS_SEED= +OPENBAO__NATS__NATS_JWT= -# Горячие папки ИС Сканирование: local — обычный каталог (каталог должен существовать: mkdir -p .hotfolder). -# Режим smb требует блока OPENBAO__SMB__SMB_HOST / SMB_PATH / SMB_LOGIN / SMB_PASSWORD. +# Включает подписку на recognition.image.request. false — воркер не запускается (сервис поднимется без NATS). +NATS_ENABLED=true +# Попытки публикации ack/результата в NATS и размер LRU обработанных job_id (защита от повторной доставки запроса) +PUBLISH_RETRIES=3 +DONE_CACHE_SIZE=1000 + +# --- Горячие папки ИС Сканирование --- +# local — обычный каталог (dev, тесты, эмулятор); smb — SMB-шара платформы (нужен блок OPENBAO__SMB__* ниже). SCAN_HOTFOLDER_MODE=local +# Корень горячей папки в режиме local: каталог должен существовать (mkdir -p .hotfolder). +# Путь считается от каталога api/; его же читает эмулятор. SCAN_HOTFOLDER_PATH=./.hotfolder +# Подкаталоги корня: «Входящие» (сюда кладётся UUID_имя) и «Исходящие» (оттуда забирается /.json) +SCAN_INPUT_DIR=Input +SCAN_OUTPUT_DIR=Output +# Период опроса «Исходящих», секунды +SCAN_POLL_SECONDS=5 +# Потолок ожидания ответа ИС, секунды (дополнительно ограничен reply_deadline запроса); повторной отправки нет +SCAN_TIMEOUT_SECONDS=300 +# Сколько заданий обрабатывается одновременно +SCAN_MAX_INFLIGHT=4 + +# --- SMB (только при SCAN_HOTFOLDER_MODE=smb; в режиме local весь блок не нужен) --- +# OPENBAO__SMB__SMB_HOST=smb.example.local +# OPENBAO__SMB__SMB_PATH=share/scan +# OPENBAO__SMB__SMB_LOGIN=user +# OPENBAO__SMB__SMB_PASSWORD=password + +# --- Эмулятор ИС Сканирование (python -m src.scan_emulator, make emulator; только dev) --- +# Эти параметры читает сам эмулятор, сервис шлюза их не использует. Корень горячей папки и подкаталоги +# эмулятор берёт из SCAN_HOTFOLDER_PATH / SCAN_INPUT_DIR / SCAN_OUTPUT_DIR выше. +# Каталог с фикстурами ответов ИС (aep_N.json); путь от api/ (репозиторий fixtures лежит рядом с workspace) +SCAN_FIXTURES_DIR=../../fixtures/ocr +# Задержка перед выдачей ответа, секунды (имитация работы ИС) +SCAN_EMULATOR_DELAY=2 diff --git a/api/.env.test b/api/.env.test new file mode 100644 index 0000000..6634e6b --- /dev/null +++ b/api/.env.test @@ -0,0 +1,25 @@ +# Окружение тестов (pytest.ini: ENV_FILE=.env.test, путь от каталога api/). Только dev-значения, секретов нет. +# Инфраструктура — compose-проект arm-ka на 127.0.0.1 (make up); модульные тесты в неё не ходят. +DEBUG=true + +OPENBAO__MINIO__MINIO_URL=127.0.0.1:9000 +OPENBAO__MINIO__MINIO_ADMIN_LOGIN=minioadmin +OPENBAO__MINIO__MINIO_ADMIN_PASSWORD=minioadmin +OPENBAO__MINIO__MINIO_BUCKET=arm-ka +OPENBAO__MINIO__MINIO_SECURE=false + +OPENBAO__NATS__NATS_URL=nats://127.0.0.1:4222 +OPENBAO__NATS__NATS_SEED= +OPENBAO__NATS__NATS_JWT= + +NATS_ENABLED=true + +# Корень горячей папки задаётся строкой и не обязан существовать: тесты, которым нужна папка, создают tmp-каталог +# и подставляют его сами +SCAN_HOTFOLDER_MODE=local +SCAN_HOTFOLDER_PATH=./.hotfolder-test +SCAN_INPUT_DIR=Input +SCAN_OUTPUT_DIR=Output +# Укороченные интервалы, чтобы тесты не ждали по 5 секунд +SCAN_POLL_SECONDS=0.1 +SCAN_TIMEOUT_SECONDS=5 diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 79ca8ed..59dedcc 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -1,29 +1,36 @@ -"""Общие фикстуры. Минимальное окружение local-режима задаётся здесь, чтобы `import src` (он создаёт приложение) -работал без `.env.test` и без инфраструктуры; значения можно переопределить переменными окружения.""" - -import os +"""Общие фикстуры. Окружение тестов — `api/.env.test`: его подключает pytest.ini (`ENV_FILE=.env.test`, путь от +каталога api/), поэтому `import src` (он создаёт приложение) работает без своей настройки окружения +и без инфраструктуры.""" import pytest -_MINIMAL_ENV = { - "OPENBAO__MINIO__MINIO_URL": "localhost:9000", - "OPENBAO__MINIO__MINIO_ADMIN_LOGIN": "minioadmin", - "OPENBAO__MINIO__MINIO_ADMIN_PASSWORD": "minioadmin", - "OPENBAO__MINIO__MINIO_BUCKET": "arm-ka", - "OPENBAO__NATS__NATS_URL": "nats://localhost:4222", - "SCAN_HOTFOLDER_MODE": "local", - "SCAN_HOTFOLDER_PATH": "/tmp/scan-gateway-test-hotfolder", -} -for _key, _value in _MINIMAL_ENV.items(): - os.environ.setdefault(_key, _value) - @pytest.fixture(autouse=True) def _fresh_settings(): """get_settings кэшируется (lru_cache): сбрасываем кэш до и после каждого теста.""" - # Импорт отложен намеренно: `import src` создаёт приложение и требует окружения, заданного выше + # Импорт отложен намеренно: `import src` создаёт приложение и читает окружение from src.core.config import get_settings # noqa: PLC0415 get_settings.cache_clear() yield get_settings.cache_clear() + + +@pytest.fixture +def minimal_env_file(tmp_path, monkeypatch): + """Env-файл только с обязательными настройками (без `.env.test`): для проверки значений по умолчанию.""" + env_file = tmp_path / "minimal.env" + env_file.write_text( + "\n".join( + [ + "OPENBAO__MINIO__MINIO_URL=localhost:9000", + "OPENBAO__MINIO__MINIO_ADMIN_LOGIN=minioadmin", + "OPENBAO__MINIO__MINIO_ADMIN_PASSWORD=minioadmin", + "OPENBAO__MINIO__MINIO_BUCKET=arm-ka", + "OPENBAO__NATS__NATS_URL=nats://localhost:4222", + "SCAN_HOTFOLDER_PATH=/tmp/scan-gateway-test-hotfolder", + ] + ) + ) + monkeypatch.setenv("ENV_FILE", str(env_file)) + return env_file diff --git a/api/tests/unit/test_app.py b/api/tests/unit/test_app.py index 9837c24..b3ffae6 100644 --- a/api/tests/unit/test_app.py +++ b/api/tests/unit/test_app.py @@ -13,7 +13,6 @@ def _client(app) -> httpx.AsyncClient: async def test_healthcheck_returns_uptime_and_version_without_infrastructure(monkeypatch): - monkeypatch.setenv("ENV_FILE", "/nonexistent/.env") app = create_app(get_settings()) # lifespan не запускается: подключений к NATS/S3 нет @@ -28,7 +27,6 @@ async def test_healthcheck_returns_uptime_and_version_without_infrastructure(mon async def test_lifespan_does_not_register_smb_session_in_local_mode(monkeypatch): - monkeypatch.setenv("ENV_FILE", "/nonexistent/.env") monkeypatch.setenv("SCAN_HOTFOLDER_MODE", "local") register = MagicMock() monkeypatch.setattr(smbclient, "register_session", register) @@ -41,7 +39,6 @@ async def test_lifespan_does_not_register_smb_session_in_local_mode(monkeypatch) async def test_lifespan_registers_smb_session_in_smb_mode(monkeypatch): - monkeypatch.setenv("ENV_FILE", "/nonexistent/.env") monkeypatch.setenv("SCAN_HOTFOLDER_MODE", "smb") monkeypatch.setenv("OPENBAO__SMB__SMB_HOST", "smb.example") monkeypatch.setenv("OPENBAO__SMB__SMB_PATH", "share") @@ -61,7 +58,6 @@ async def test_lifespan_registers_smb_session_in_smb_mode(monkeypatch): async def test_unhandled_error_has_json_500(monkeypatch): - monkeypatch.setenv("ENV_FILE", "/nonexistent/.env") app = create_app(get_settings()) @app.get("/boom") diff --git a/api/tests/unit/test_config.py b/api/tests/unit/test_config.py index 14e67a4..29bb5b1 100644 --- a/api/tests/unit/test_config.py +++ b/api/tests/unit/test_config.py @@ -17,9 +17,8 @@ def _clean_env(monkeypatch): monkeypatch.delenv(key, raising=False) -def test_defaults_local_mode_without_smb_and_nats_credentials(monkeypatch): +def test_defaults_local_mode_without_smb_and_nats_credentials(monkeypatch, minimal_env_file): _clean_env(monkeypatch) - monkeypatch.setenv("ENV_FILE", "/nonexistent/.env") s = get_settings() @@ -41,7 +40,6 @@ def test_defaults_local_mode_without_smb_and_nats_credentials(monkeypatch): def test_local_mode_requires_hotfolder_path(monkeypatch): _clean_env(monkeypatch) - monkeypatch.setenv("ENV_FILE", "/nonexistent/.env") monkeypatch.setenv("SCAN_HOTFOLDER_PATH", "") with pytest.raises(ValidationError, match="SCAN_HOTFOLDER_PATH"): @@ -50,7 +48,6 @@ def test_local_mode_requires_hotfolder_path(monkeypatch): def test_smb_mode_without_smb_block_fails_with_clear_error(monkeypatch): _clean_env(monkeypatch) - monkeypatch.setenv("ENV_FILE", "/nonexistent/.env") monkeypatch.setenv("SCAN_HOTFOLDER_MODE", "smb") with pytest.raises(ValidationError, match="OPENBAO__SMB"): @@ -59,7 +56,6 @@ def test_smb_mode_without_smb_block_fails_with_clear_error(monkeypatch): def test_smb_mode_with_smb_block_ok(monkeypatch): _clean_env(monkeypatch) - monkeypatch.setenv("ENV_FILE", "/nonexistent/.env") monkeypatch.setenv("SCAN_HOTFOLDER_MODE", "smb") for key, value in SMB_ENV.items(): monkeypatch.setenv(key, value) @@ -70,7 +66,6 @@ def test_smb_mode_with_smb_block_ok(monkeypatch): def test_unknown_hotfolder_mode_rejected(monkeypatch): - monkeypatch.setenv("ENV_FILE", "/nonexistent/.env") monkeypatch.setenv("SCAN_HOTFOLDER_MODE", "ftp") with pytest.raises(ValidationError): @@ -117,6 +112,31 @@ def test_env_file_selected_by_env_file_variable(monkeypatch, tmp_path): def test_get_settings_is_cached(monkeypatch): - monkeypatch.setenv("ENV_FILE", "/nonexistent/.env") assert get_settings() is get_settings() assert isinstance(get_settings(), Settings) + + +def test_env_test_file_loads_for_tests(): + """pytest.ini выбирает `.env.test`: он должен давать рабочие настройки local-режима с укороченными интервалами.""" + s = get_settings() + + assert s.SCAN_HOTFOLDER_MODE == "local" + assert s.SCAN_HOTFOLDER_PATH == "./.hotfolder-test" + assert s.SCAN_POLL_SECONDS == 0.1 + assert s.SCAN_TIMEOUT_SECONDS == 5 + assert s.OPENBAO.MINIO.MINIO_URL == "127.0.0.1:9000" + assert s.OPENBAO.MINIO.MINIO_SECURE is False + assert s.OPENBAO.NATS.NATS_URL == "nats://127.0.0.1:4222" + assert s.OPENBAO.NATS.NATS_SEED == "" + + +def test_env_example_loads(monkeypatch): + """`.env.example` — рабочий dev-набор: Settings читает его без ошибок.""" + monkeypatch.setenv("ENV_FILE", ".env.example") + + s = get_settings() + + assert s.SCAN_HOTFOLDER_MODE == "local" + assert s.SCAN_INPUT_DIR == "Input" + assert s.OPENBAO.MINIO.MINIO_BUCKET == "arm-ka" + assert s.OPENBAO.NATS.NATS_JWT == "" diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton.md index 3cb442c..642b99b 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton.md @@ -36,7 +36,7 @@ ### Task 5: [2.2] `Dockerfile` (`python -- [ ] `Dockerfile` (`python:3.11-slim`; `CMD python run_server.py`, эмулятор — через `command` в compose), `.env.example`, `.env.test`; проверка: `docker build` успешен без Nexus +- [x] `Dockerfile` (`python:3.11-slim`; `CMD python run_server.py`, эмулятор — через `command` в compose), `.env.example`, `.env.test`; проверка: `docker build` успешен без Nexus ### Task 6: [3.1] `api/contracts/` — копии схем и `examples/` из `../docs/contracts` diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 666bba3..2deb1de 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -22,7 +22,7 @@ - [x] 2.1 `api/pyproject.toml` (ruff, mypy), `requirements-dev.txt` = `-r requirements.txt` + ruff, mypy, pytest, pytest-asyncio, pytest-env, httpx, jsonschema; `Makefile`: `venv` (python3.11, без `raisa_*`), `lint`, `test`, `test-integration`, `check`, `check-full`, `up`, `emulator`; проверка: `make venv && make lint` зелёные -- [ ] 2.2 `Dockerfile` (`python:3.11-slim`; `CMD python run_server.py`, эмулятор — через `command` в compose), +- [x] 2.2 `Dockerfile` (`python:3.11-slim`; `CMD python run_server.py`, эмулятор — через `command` в compose), `.env.example`, `.env.test`; проверка: `docker build` успешен без Nexus ## 3. Контракт