feat(sp9): multi-payer, multi-NPI, SFTP stub for dzinesco/TOC
- providers table seeded with 3 NPIs (Montrose, Delta, Salida) - payers + payer_configs (per-tx config) join table - clearhouse singleton (dzinesco identity, SFTP block, filename block) - config/payers.yaml loader with Pydantic schema validation - cyclone/edi/filenames.py: HCPF X12 File Naming Standards helpers - cyclone/clearhouse/sftp.py: stub that copies to ./var/sftp/staging/... - cyclone/secrets.py: macOS Keychain wrapper via keyring - 11 new validation rules R200-R210 (CO MAP + HCPF naming) - 6 new API endpoints (/api/clearhouse/*, /api/config/*, /api/admin/reload-config) - migration 0007 - 72 new tests (646 total passing, was 574) See spec: docs/superpowers/specs/2026-06-20-cyclone-multi-payer-npi-sftp-design.md
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
"""SP9 — macOS Keychain secret accessor tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cyclone import secrets
|
||||
from cyclone.secrets import STUB_SECRET, get_secret, has_keyring, set_secret
|
||||
|
||||
|
||||
def test_has_keyring_true_when_lib_present():
|
||||
# The test env has keyring installed
|
||||
assert has_keyring() is True
|
||||
|
||||
|
||||
def test_get_secret_returns_none_when_keyring_missing(monkeypatch):
|
||||
monkeypatch.setattr(secrets, "_HAS_KEYRING", False)
|
||||
monkeypatch.setattr(secrets, "keyring", None)
|
||||
assert get_secret("anything") is None
|
||||
|
||||
|
||||
def test_get_secret_returns_keychain_value():
|
||||
with patch("cyclone.secrets.keyring") as mock_kr:
|
||||
mock_kr.get_password.return_value = "p@ssw0rd"
|
||||
v = get_secret("sftp.gainwell.password")
|
||||
assert v == "p@ssw0rd"
|
||||
mock_kr.get_password.assert_called_once_with("cyclone", "sftp.gainwell.password")
|
||||
|
||||
|
||||
def test_get_secret_returns_none_on_keychain_exception():
|
||||
with patch("cyclone.secrets.keyring") as mock_kr:
|
||||
mock_kr.get_password.side_effect = RuntimeError("keychain locked")
|
||||
v = get_secret("x")
|
||||
assert v is None
|
||||
|
||||
|
||||
def test_set_secret_returns_true_on_success():
|
||||
with patch("cyclone.secrets.keyring") as mock_kr:
|
||||
assert set_secret("a", "b") is True
|
||||
mock_kr.set_password.assert_called_once_with("cyclone", "a", "b")
|
||||
|
||||
|
||||
def test_set_secret_returns_false_when_keyring_missing(monkeypatch):
|
||||
monkeypatch.setattr(secrets, "_HAS_KEYRING", False)
|
||||
monkeypatch.setattr(secrets, "keyring", None)
|
||||
assert set_secret("a", "b") is False
|
||||
|
||||
|
||||
def test_set_secret_returns_false_on_keychain_exception():
|
||||
with patch("cyclone.secrets.keyring") as mock_kr:
|
||||
mock_kr.set_password.side_effect = RuntimeError("denied")
|
||||
assert set_secret("a", "b") is False
|
||||
|
||||
|
||||
def test_stub_secret_is_distinct_string():
|
||||
assert STUB_SECRET == "<stub-secret>"
|
||||
assert STUB_SECRET != ""
|
||||
Reference in New Issue
Block a user