210 lines
6.8 KiB
Python
210 lines
6.8 KiB
Python
from __future__ import annotations
|
||
|
||
import shutil
|
||
from pathlib import Path
|
||
|
||
from app.pipeline.file_manager import (
|
||
SMBClientConnection,
|
||
check_dir_write_access,
|
||
create_run_context,
|
||
move_file_to_error,
|
||
next_run_number,
|
||
scan_input_files,
|
||
)
|
||
|
||
|
||
class FakeSamba:
|
||
def __init__(self, root: Path) -> None:
|
||
self.root = root
|
||
|
||
def __enter__(self):
|
||
return self
|
||
|
||
def __exit__(self, exc_type, exc, tb): # noqa: ANN001
|
||
return False
|
||
|
||
def _to_local(self, remote_path: str) -> Path:
|
||
cleaned = remote_path.replace("/", "\\").strip("\\")
|
||
return self.root / Path(cleaned)
|
||
|
||
def path_exists(self, path: str) -> bool:
|
||
return self._to_local(path).exists()
|
||
|
||
def listdir(self, path: str) -> list[str]:
|
||
target = self._to_local(path)
|
||
if not target.exists():
|
||
return []
|
||
return [item.name for item in target.iterdir()]
|
||
|
||
def is_file(self, path: str) -> bool:
|
||
return self._to_local(path).is_file()
|
||
|
||
def mkdir(self, path: str, parent: bool = False) -> None:
|
||
target = self._to_local(path)
|
||
if parent:
|
||
target.mkdir(parents=True, exist_ok=True)
|
||
else:
|
||
target.mkdir(exist_ok=True)
|
||
|
||
def copy(self, path_from: str, path_to: str, replace: bool = False) -> None:
|
||
source = self._to_local(path_from)
|
||
target = self._to_local(path_to)
|
||
target.parent.mkdir(parents=True, exist_ok=True)
|
||
if target.exists() and not replace:
|
||
raise FileExistsError(path_to)
|
||
shutil.copy2(source, target)
|
||
|
||
def delete(self, path: str) -> None:
|
||
target = self._to_local(path)
|
||
if target.exists():
|
||
target.unlink()
|
||
|
||
def rename(self, path_from: str, path_to: str, replace: bool = False) -> None:
|
||
source = self._to_local(path_from)
|
||
target = self._to_local(path_to)
|
||
target.parent.mkdir(parents=True, exist_ok=True)
|
||
if target.exists() and not replace:
|
||
raise FileExistsError(path_to)
|
||
source.rename(target)
|
||
|
||
def open(self, path: str, mode: str = "rb"):
|
||
target = self._to_local(path)
|
||
target.parent.mkdir(parents=True, exist_ok=True)
|
||
return target.open(mode)
|
||
|
||
|
||
def test_scan_input_files_splits_valid_and_invalid(tmp_path: Path) -> None:
|
||
samba = FakeSamba(tmp_path)
|
||
in_dir = "Exchange\\SMBDEMO\\test"
|
||
local_dir = tmp_path / "Exchange" / "SMBDEMO" / "test"
|
||
local_dir.mkdir(parents=True)
|
||
(local_dir / "SKO115FZ_01_123456789_20260616_X00001.xml").write_text(
|
||
"ok", encoding="utf-8"
|
||
)
|
||
(local_dir / "wrong_name.xml").write_text("bad", encoding="utf-8")
|
||
|
||
result = scan_input_files(in_dir, samba)
|
||
assert len(result.valid_files) == 1
|
||
assert len(result.invalid_files) == 1
|
||
assert len(result.skipped_files) == 0
|
||
|
||
|
||
def test_scan_input_files_skips_already_processed(tmp_path: Path) -> None:
|
||
samba = FakeSamba(tmp_path)
|
||
in_dir = "Exchange\\SMBDEMO\\test"
|
||
processed_dir = "Exchange\\SMBDEMO\\Processed"
|
||
local_in_dir = tmp_path / "Exchange" / "SMBDEMO" / "test"
|
||
local_processed_dir = tmp_path / "Exchange" / "SMBDEMO" / "Processed"
|
||
local_in_dir.mkdir(parents=True)
|
||
local_processed_dir.mkdir(parents=True)
|
||
|
||
file_name = "SKO115FZ_01_123456789_20260616_X00001.xml"
|
||
(local_in_dir / file_name).write_text("ok", encoding="utf-8")
|
||
(local_processed_dir / file_name).write_text("already done", encoding="utf-8")
|
||
|
||
result = scan_input_files(in_dir, samba, processed_dir=processed_dir)
|
||
assert len(result.valid_files) == 0
|
||
assert len(result.invalid_files) == 0
|
||
assert [item.name for item in result.skipped_files] == [file_name]
|
||
|
||
|
||
def test_next_run_number_reads_existing_dirs(tmp_path: Path) -> None:
|
||
samba = FakeSamba(tmp_path)
|
||
out_day = (
|
||
tmp_path / "Exchange" / "SMBDEMO" / "Out" / "2026" / "06_2026" / "20260616"
|
||
)
|
||
(out_day / "1").mkdir(parents=True)
|
||
(out_day / "2").mkdir(parents=True)
|
||
|
||
number = next_run_number("Exchange\\SMBDEMO\\Out\\2026\\06_2026\\20260616", samba)
|
||
assert number == 3
|
||
|
||
|
||
def test_create_run_context_creates_paths(tmp_path: Path) -> None:
|
||
samba = FakeSamba(tmp_path)
|
||
context = create_run_context("Exchange\\SMBDEMO\\test", samba)
|
||
|
||
assert context.run_number == 1
|
||
assert (tmp_path / Path(context.run_dir)).exists()
|
||
assert (tmp_path / Path(context.error_dir)).exists()
|
||
assert (tmp_path / Path(context.processed_dir)).exists()
|
||
|
||
|
||
def test_move_file_to_error_adds_suffix_on_collision(tmp_path: Path) -> None:
|
||
samba = FakeSamba(tmp_path)
|
||
error_dir = tmp_path / "Exchange" / "SMBDEMO" / "Err"
|
||
error_dir.mkdir(parents=True)
|
||
(error_dir / "file.xml").write_text("existing", encoding="utf-8")
|
||
source = tmp_path / "Exchange" / "SMBDEMO" / "test" / "file.xml"
|
||
source.parent.mkdir(parents=True)
|
||
source.write_text("new", encoding="utf-8")
|
||
|
||
target = move_file_to_error(
|
||
file_path="Exchange\\SMBDEMO\\test\\file.xml",
|
||
file_name="file.xml",
|
||
error_dir="Exchange\\SMBDEMO\\Err",
|
||
samba_conn=samba,
|
||
)
|
||
assert target.endswith("file_1.xml")
|
||
assert (error_dir / "file_1.xml").exists()
|
||
|
||
|
||
def test_check_dir_write_access_success(tmp_path: Path) -> None:
|
||
samba = FakeSamba(tmp_path)
|
||
target_dir = tmp_path / "Exchange" / "SMBDEMO" / "Out"
|
||
target_dir.mkdir(parents=True)
|
||
|
||
ok, error = check_dir_write_access(
|
||
target_dir="Exchange\\SMBDEMO\\Out",
|
||
samba_conn=samba,
|
||
source_name="Out",
|
||
)
|
||
|
||
assert ok is True
|
||
assert error is None
|
||
|
||
|
||
def test_check_dir_write_access_fails_without_open_or_upload(tmp_path: Path) -> None:
|
||
class NoWriteSamba(FakeSamba):
|
||
open = None # type: ignore[assignment]
|
||
|
||
samba = NoWriteSamba(tmp_path)
|
||
target_dir = tmp_path / "Exchange" / "SMBDEMO" / "Out"
|
||
target_dir.mkdir(parents=True)
|
||
|
||
ok, error = check_dir_write_access(
|
||
target_dir="Exchange\\SMBDEMO\\Out",
|
||
samba_conn=samba,
|
||
source_name="Out",
|
||
)
|
||
|
||
assert ok is False
|
||
assert error is not None
|
||
|
||
|
||
def test_smb_connection_registers_sessions_for_multiple_servers(monkeypatch) -> None:
|
||
class FakeSmbClient:
|
||
def __init__(self) -> None:
|
||
self.registered: list[str] = []
|
||
|
||
def register_session(self, server: str, username: str, password: str) -> None:
|
||
_ = username, password
|
||
self.registered.append(server)
|
||
|
||
def stat(self, _path: str) -> None:
|
||
return None
|
||
|
||
fake = FakeSmbClient()
|
||
monkeypatch.setattr("app.pipeline.file_manager._import_smbclient", lambda: fake)
|
||
|
||
conn = SMBClientConnection(
|
||
smb_base_path="//sgo-fc01-r06/sfm/ОТЧЕТ_7081У",
|
||
user_params={"user_name": "user", "password": "pass"},
|
||
)
|
||
with conn:
|
||
pass
|
||
conn.path_exists("//sgo-fc01-r13/inbox-intech/Exchange/Raykov/Input")
|
||
|
||
assert "sgo-fc01-r06" in fake.registered
|
||
assert "sgo-fc01-r13" in fake.registered
|