fix(sp37): submit_file default factory uses paramiko directly

SftpClient wrapper lacks a stat() method, so the previous default
factory produced a client whose stat() raised AttributeError — making
the SKIPPED outcome unreachable in production and silently
misclassifying all uploads as SFTP_FAILED. The helper now opens a
paramiko SFTP session directly (same pattern as
resubmit-rejected-claims in cli.py:620-650), so stat() and write_file()
both work end-to-end.

Also: tighten typing (SftpBlock instead of Any), add PAYER_MISMATCH
test, drop duplicate _db fixture, rename misleading test, add class
docstrings to SubmitResult/SubmitOutcome.
This commit is contained in:
Nora
2026-07-07 11:06:08 -06:00
parent 5d4e06b863
commit ec065a9082
4 changed files with 154 additions and 24 deletions
+69 -14
View File
@@ -1,8 +1,8 @@
"""SP37 Task 4: submit_file owns parse → DB write → SFTP upload.
Tests use a fake SFTP client to avoid hitting the real clearhouse.
The DB path is real (autouse fixture) so the join-key wiring is
exercised end-to-end.
The DB path is real (autouse ``_auto_init_db`` fixture in conftest.py)
so the join-key wiring is exercised end-to-end.
"""
from __future__ import annotations
@@ -12,7 +12,6 @@ from unittest.mock import MagicMock
import pytest
from cyclone import db as db_mod
from cyclone.clearhouse import SftpClient
from cyclone.submission.core import submit_file
from cyclone.submission.result import SubmitOutcome
@@ -48,14 +47,6 @@ class _FakeSftp:
self.existing_files[remote_path] = len(content)
@pytest.fixture(autouse=True)
def _db(tmp_path, monkeypatch):
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db")
db_mod._reset_for_tests()
db_mod.init_db()
yield
def test_submit_file_happy_path():
"""parse → DB write → SFTP upload, all steps succeed."""
fake = _FakeSftp()
@@ -79,8 +70,14 @@ def test_submit_file_happy_path():
assert s.get(db_mod.Batch, result.batch_id) is not None
def test_submit_file_idempotent_db():
"""Re-running submit_file on the same file is a no-op on the DB side."""
def test_submit_file_skipped_when_remote_size_matches():
"""Re-running submit_file on an already-uploaded file is SKIPPED.
The ``SKIPPED`` outcome is reached via the SFTP ``stat()`` check
(not via the DB) — same-size remote file ⇒ idempotency short-circuit.
Re-runs therefore don't emit a duplicate ``clearhouse.submitted``
audit event.
"""
fake = _FakeSftp()
sftp_block = MagicMock(stub=False, paths={"outbound": "/ToHPE"})
factory = lambda _b: fake
@@ -137,4 +134,62 @@ def test_submit_file_sftp_fail():
# DB row was written before the SFTP call.
assert result.batch_id is not None
with db_mod.SessionLocal()() as s:
assert s.get(db_mod.Batch, result.batch_id) is not None
assert s.get(db_mod.Batch, result.batch_id) is not None
def test_submit_file_payer_mismatch():
"""PAYER_MISMATCH → no DB write, no SFTP call.
We monkeypatch ``parse_837_text`` to return a single claim whose
payer.id is ``OTHER_PAYER`` (the helper gates on the first
claim's payer — same posture as the legacy ``resubmit_rejected_claims``
CLI at cli.py:657-668).
"""
from unittest.mock import patch
bad_claim = MagicMock()
bad_claim.payer.id = "OTHER_PAYER"
parsed_with_bad_payer = MagicMock()
parsed_with_bad_payer.claims = [bad_claim]
parsed_with_bad_payer.parsed_at = None
parsed_with_bad_payer.envelope.transaction_set_control_number = "ST123"
fake = _FakeSftp()
sftp_block = MagicMock(stub=False, paths={"outbound": "/ToHPE"})
with patch("cyclone.submission.core.parse_837_text", return_value=parsed_with_bad_payer):
result = submit_file(
_FIXTURE,
sftp_block=sftp_block,
actor="t",
validate=True,
sftp_client_factory=lambda _b: fake,
)
assert result.outcome == SubmitOutcome.PAYER_MISMATCH
assert "OTHER_PAYER" in (result.error or "")
assert len(fake.put_calls) == 0
def test_submit_file_uses_default_paramiko_factory(monkeypatch):
"""When no factory is supplied, helper should use paramiko directly
(not SftpClient wrapper, which lacks stat()).
"""
fake_paramiko_sftp = _FakeSftp()
monkeypatch.setattr(
"cyclone.submission.core._default_sftp_factory",
lambda _block: fake_paramiko_sftp,
)
sftp_block = MagicMock()
sftp_block.stub = False
sftp_block.paths = {"outbound": "/ToHPE"}
result = submit_file(
_FIXTURE,
sftp_block=sftp_block,
actor="t",
validate=True,
# NO sftp_client_factory — should use the paramiko default
)
assert result.outcome == SubmitOutcome.SUBMITTED
assert len(fake_paramiko_sftp.put_calls) == 1