a11e051f82
Replace SftpClient stub write_file/list_inbound/read_file implementations with real paramiko SSHClient + SFTPClient calls. The public API (SftpClient.write_file, list_inbound, read_file, get_secret) is unchanged from SP9 — same signature, same return types — so the API layer needs no changes. Real-mode behavior: * _connect() returns a context manager yielding (ssh, sftp); closes both on exit. Lazy-imports paramiko so the stub-only test path doesn't need the dependency. * Auth resolves from SftpBlock.auth: password_keychain_account (MFT model) or key_file + optional key_passphrase_keychain_account. Missing Keychain entries fail loud (RuntimeError) rather than silently attempting empty-password auth. * write_file: opens sftp.open(remote, 'wb') and writes bytes; mkdirs the parent dir (idempotent — MFT pre-creates FromHPE/ToHPE). * list_inbound: listdir_attr + per-file download into local staging cache; skips directory entries (0o040000 mask). * read_file: download via shutil.copyfileobj into BytesIO. Stub mode is unchanged. AutoAddPolicy for first-time MFT host fingerprint; operator should pin the key for production. Adds tests/test_sftp_paramiko.py: 9 tests covering * stub still works * real-mode connect builds correct paramiko call from password_keychain_account, raises on missing Keychain, raises on missing auth config, raises on STUB_SECRET * write_file opens 'wb' on the right path and writes bytes * list_inbound translates attrs into InboundFile records and caches files locally; skips dirs Removes 2 obsolete tests in test_sftp_stub.py that expected SP13-mode to raise NotImplementedError. pyproject.toml: new optional 'sftp' extra (paramiko>=3.4,<6).
79 lines
2.6 KiB
Python
79 lines
2.6 KiB
Python
"""SP9 — SFTP stub tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from cyclone.clearhouse import SftpClient
|
|
from cyclone.providers import SftpBlock
|
|
|
|
|
|
@pytest.fixture
|
|
def sftp_block(tmp_path):
|
|
staging = tmp_path / "staging"
|
|
return SftpBlock(
|
|
host="mft.gainwelltechnologies.com",
|
|
port=22,
|
|
username="colorado-fts\\coxix_prod_11525703",
|
|
paths={
|
|
"outbound": "/CO XIX/PROD/coxix_prod_11525703/FromHPE",
|
|
"inbound": "/CO XIX/PROD/coxix_prod_11525703/ToHPE",
|
|
},
|
|
stub=True,
|
|
staging_dir=str(staging),
|
|
poll_seconds=300,
|
|
auth={"method": "keychain", "secret_ref": "sftp.gainwell.password"},
|
|
)
|
|
|
|
|
|
def test_stub_writes_preserving_remote_path(sftp_block, tmp_path):
|
|
client = SftpClient(sftp_block)
|
|
remote = "/CO XIX/PROD/coxix_prod_11525703/FromHPE/11525703-837P-20260620132243505-1of1.x12"
|
|
target = client.write_file(remote, b"ISA*00*...~IEA*1*1~")
|
|
assert target.exists()
|
|
assert target.read_bytes() == b"ISA*00*...~IEA*1*1~"
|
|
# Confirm the full nested MFT path is preserved under staging
|
|
rel = target.relative_to(sftp_block.staging_dir)
|
|
assert str(rel) == "CO XIX/PROD/coxix_prod_11525703/FromHPE/11525703-837P-20260620132243505-1of1.x12"
|
|
|
|
|
|
def test_stub_creates_parent_dirs(sftp_block):
|
|
client = SftpClient(sftp_block)
|
|
remote = "/deep/nested/path/file.x12"
|
|
target = client.write_file(remote, b"data")
|
|
assert target.exists()
|
|
assert target.parent.is_dir()
|
|
|
|
|
|
def test_stub_list_inbound_empty_when_no_local_files(sftp_block):
|
|
client = SftpClient(sftp_block)
|
|
assert client.list_inbound() == []
|
|
|
|
|
|
def test_stub_list_inbound_returns_local_files(sftp_block):
|
|
# Simulate operator dropping a file in the inbound staging dir
|
|
inbound_dir = Path(sftp_block.staging_dir) / "CO XIX/PROD/coxix_prod_11525703/ToHPE"
|
|
inbound_dir.mkdir(parents=True, exist_ok=True)
|
|
(inbound_dir / "TP11525703-837P_M019048402-20260520231513488-1of1_999.x12").write_bytes(b"X")
|
|
client = SftpClient(sftp_block)
|
|
files = client.list_inbound()
|
|
assert len(files) == 1
|
|
assert files[0].name.startswith("TP11525703-837P_M019048402")
|
|
assert files[0].size == 1
|
|
|
|
|
|
def test_stub_get_secret_returns_stub_when_keychain_empty(sftp_block):
|
|
client = SftpClient(sftp_block)
|
|
# Keychain is empty on the test box, so we get the stub sentinel
|
|
secret = client.get_secret("sftp.gainwell.password")
|
|
assert secret == "<stub-secret>"
|
|
|
|
|
|
def test_stub_read_file_raises(sftp_block):
|
|
client = SftpClient(sftp_block)
|
|
with pytest.raises(RuntimeError, match="Stub SFTP cannot read"):
|
|
client.read_file("/x.x12")
|