arm-scan-gateway/api/tests/unit/test_hotfolder_factory.py
Andrey Epifancev e8453159e8 Срез 0 [7.1]: горячие папки — набор *.json ответа, готовность по settle, служебный каталог Tmp
- HotFolder.poll(job_id) -> list[AnswerFile] | None: все *.json каталога <output>/<job_id>/ по возрастанию
  имени, когда набор не пуст и имена/размеры не менялись SCAN_SETTLE_SECONDS (SettleTracker: состояние по
  job_id, внедряемые часы, сброс при выдаче/опустении, предел 1024 задания, потокобезопасно);
- put: временный .<name>.part в <корень>/<SCAN_TMP_DIR>/ (создаётся), rename в Input (Local и SMB);
- настройки SCAN_TMP_DIR (Tmp) и SCAN_SETTLE_SECONDS (2, >= 0); в .env.test settle = 0 (R-23);
- воркер: переходная прослойка — берётся первый файл набора (склейка всех — Task 3, R-22).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 14:01:39 +04:00

127 lines
5.2 KiB
Python

"""Фабрика горячей папки `create_hotfolder(settings)` (R-6, D-1, slice-0-skeleton): режим из `SCAN_HOTFOLDER_MODE`,
модуль `src.gateway.smb` импортируется только в режиме `smb`."""
import subprocess
import sys
from pathlib import Path
import pytest
from src.core.config import Settings, SMBSettings, get_settings
from src.gateway import create_hotfolder
from src.gateway.hotfolder import LocalHotFolder
API_DIR = Path(__file__).resolve().parents[2]
def _local_settings(root: Path) -> Settings:
return get_settings().model_copy(
update={
"SCAN_HOTFOLDER_MODE": "local",
"SCAN_HOTFOLDER_PATH": str(root),
"SCAN_INPUT_DIR": "In",
"SCAN_TMP_DIR": "Служебный",
"SCAN_SETTLE_SECONDS": 1.5,
}
)
def _smb_settings() -> Settings:
settings = get_settings()
smb = SMBSettings(SMB_HOST="smb.example.local", SMB_PATH="share/scan", SMB_LOGIN="user", SMB_PASSWORD="secret")
return settings.model_copy(
update={
"SCAN_HOTFOLDER_MODE": "smb",
"SCAN_OUTPUT_DIR": "Out",
"SCAN_TMP_DIR": "T",
"SCAN_SETTLE_SECONDS": 3,
"OPENBAO": settings.OPENBAO.model_copy(update={"SMB": smb}),
}
)
def test_local_mode_builds_local_hotfolder(tmp_path: Path) -> None:
hf = create_hotfolder(_local_settings(tmp_path))
assert isinstance(hf, LocalHotFolder)
assert hf.root == tmp_path
assert hf.input_dir == tmp_path / "In"
assert hf.output_dir == tmp_path / "Output"
assert hf.tmp_dir == tmp_path / "Служебный"
assert hf.settle_seconds == 1.5
assert hf.healthy() is True
def test_smb_mode_builds_smb_hotfolder() -> None:
from src.gateway.smb import SmbHotFolder # noqa: PLC0415 — импорт смб-модуля только в этом тесте
hf = create_hotfolder(_smb_settings())
assert isinstance(hf, SmbHotFolder)
assert hf.root == "\\\\smb.example.local\\share\\scan"
assert hf.input_dir == "\\\\smb.example.local\\share\\scan\\Input"
assert hf.output_dir == "\\\\smb.example.local\\share\\scan\\Out"
assert hf.tmp_dir == "\\\\smb.example.local\\share\\scan\\T"
assert hf.settle_seconds == 3
def test_smb_mode_does_not_touch_share(monkeypatch: pytest.MonkeyPatch) -> None:
from src.gateway import smb # noqa: PLC0415
def forbidden(*args: object, **kwargs: object) -> None:
raise AssertionError("фабрика не должна обращаться к SMB")
for name in ("open_file", "listdir", "scandir", "mkdir", "replace", "remove", "register_session"):
monkeypatch.setattr(smb.smbclient, name, forbidden)
create_hotfolder(_smb_settings())
def test_unknown_mode_is_rejected(tmp_path: Path) -> None:
settings = _local_settings(tmp_path).model_copy(update={"SCAN_HOTFOLDER_MODE": "ftp"})
with pytest.raises(ValueError, match="SCAN_HOTFOLDER_MODE"):
create_hotfolder(settings)
def test_local_mode_does_not_import_smb_module(tmp_path: Path) -> None:
"""Отдельный интерпретатор: в процессе pytest модуль мог быть импортирован другими тестами."""
code = "\n".join(
[
"import sys",
"from src.core.config import get_settings",
"from src.gateway import create_hotfolder",
f"settings = get_settings().model_copy(update={{'SCAN_HOTFOLDER_PATH': {str(tmp_path)!r}}})",
"hf = create_hotfolder(settings)",
"assert settings.SCAN_HOTFOLDER_MODE == 'local'",
"assert type(hf).__name__ == 'LocalHotFolder', type(hf)",
"loaded = sorted(m for m in sys.modules if m.startswith('src.gateway'))",
"assert 'src.gateway.smb' not in sys.modules, loaded",
"print(loaded)",
]
)
result = subprocess.run(
[sys.executable, "-c", code], cwd=API_DIR, capture_output=True, text=True, check=False, timeout=60
)
assert result.returncode == 0, result.stderr
assert "src.gateway.hotfolder" in result.stdout
def test_smb_mode_imports_smb_module_lazily() -> None:
"""Контроль предыдущего теста: в режиме smb тот же сценарий импортирует `src.gateway.smb`."""
code = "\n".join(
[
"import sys",
"from src.core.config import SMBSettings, get_settings",
"from src.gateway import create_hotfolder",
"assert 'src.gateway.smb' not in sys.modules",
"s = get_settings()",
"smb = SMBSettings(SMB_HOST='h', SMB_PATH='share', SMB_LOGIN='u', SMB_PASSWORD='p')",
"openbao = s.OPENBAO.model_copy(update={'SMB': smb})",
"s = s.model_copy(update={'SCAN_HOTFOLDER_MODE': 'smb', 'OPENBAO': openbao})",
"hf = create_hotfolder(s)",
"assert type(hf).__name__ == 'SmbHotFolder', type(hf)",
"assert 'src.gateway.smb' in sys.modules",
]
)
result = subprocess.run(
[sys.executable, "-c", code], cwd=API_DIR, capture_output=True, text=True, check=False, timeout=60
)
assert result.returncode == 0, result.stderr