Files
cyclone/backend/tests/test_sftp_stub.py
T
Tyler a11e051f82 feat(sp13): paramiko-backed SftpClient wire-up
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).
2026-06-21 00:00:13 -06:00

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