Срез 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:
parent
d1bcff8f5c
commit
b5ac55084d
@ -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
25
api/.env.test
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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 == ""
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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. Контракт
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user