Срез 0 [2.2]: .env.example дополнен, .env.test, тесты на .env.test (T2.6), отметка задачи

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-04 02:38:46 +04:00
parent d1bcff8f5c
commit b5ac55084d
7 changed files with 121 additions and 36 deletions

View File

@ -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_имя) и «Исходящие» (оттуда забирается <UUID>/<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

25
api/.env.test Normal file
View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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 == ""

View File

@ -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`

View File

@ -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. Контракт