feat(sp25): secrets.get_secret() env-var-first lookup
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""SP25 — env-var fallback in cyclone.secrets.get_secret().
|
||||
|
||||
The scheduler's ``SftpClient._connect`` calls ``get_secret(name)`` to
|
||||
fetch the MFT password. Today the lookup is macOS-Keychain-only via
|
||||
the ``keyring`` library. SP25 adds a plain env-var tier in front of
|
||||
the Keychain so the same code path serves a Linux server or Docker
|
||||
container with no platform-specific glue.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def secrets_module(monkeypatch):
|
||||
"""Reload cyclone.secrets with a clean keyring state per test."""
|
||||
# Force a fresh import so module-level keyring cache is clean.
|
||||
import cyclone.secrets as secrets_mod
|
||||
importlib.reload(secrets_mod)
|
||||
yield secrets_mod
|
||||
importlib.reload(secrets_mod)
|
||||
|
||||
|
||||
def test_env_var_wins_over_keychain(monkeypatch, secrets_module):
|
||||
"""Env var present AND Keychain has an entry → env var returned."""
|
||||
monkeypatch.setenv("cyclone.test.password", "from-env")
|
||||
monkeypatch.setattr(
|
||||
secrets_module.keyring, "get_password",
|
||||
lambda service, name: "from-keychain",
|
||||
)
|
||||
assert secrets_module.get_secret("cyclone.test.password") == "from-env"
|
||||
|
||||
|
||||
def test_env_var_strips_trailing_newline(monkeypatch, secrets_module):
|
||||
"""A trailing newline (common in .env files) is stripped."""
|
||||
monkeypatch.setenv("cyclone.test.password", "s3cret\n")
|
||||
assert secrets_module.get_secret("cyclone.test.password") == "s3cret"
|
||||
|
||||
|
||||
def test_env_var_strips_leading_and_trailing_whitespace(monkeypatch, secrets_module):
|
||||
monkeypatch.setenv("cyclone.test.password", " s3cret \n")
|
||||
assert secrets_module.get_secret("cyclone.test.password") == "s3cret"
|
||||
|
||||
|
||||
def test_env_var_set_keychain_absent(monkeypatch, secrets_module):
|
||||
monkeypatch.setenv("cyclone.test.password", "from-env")
|
||||
monkeypatch.setattr(
|
||||
secrets_module.keyring, "get_password", lambda service, name: None,
|
||||
)
|
||||
assert secrets_module.get_secret("cyclone.test.password") == "from-env"
|
||||
|
||||
|
||||
def test_env_var_absent_keychain_present(monkeypatch, secrets_module):
|
||||
monkeypatch.delenv("cyclone.test.password", raising=False)
|
||||
monkeypatch.setattr(
|
||||
secrets_module.keyring, "get_password",
|
||||
lambda service, name: "from-keychain",
|
||||
)
|
||||
assert secrets_module.get_secret("cyclone.test.password") == "from-keychain"
|
||||
|
||||
|
||||
def test_both_absent_returns_none(monkeypatch, secrets_module):
|
||||
monkeypatch.delenv("cyclone.test.password", raising=False)
|
||||
monkeypatch.setattr(
|
||||
secrets_module.keyring, "get_password", lambda service, name: None,
|
||||
)
|
||||
assert secrets_module.get_secret("cyclone.test.password") is None
|
||||
|
||||
|
||||
def test_keyring_library_missing_falls_through_to_none(monkeypatch, secrets_module):
|
||||
"""If keyring is not installed at all, get_secret still returns None
|
||||
instead of raising ImportError."""
|
||||
monkeypatch.setenv("cyclone.test.password", "from-env")
|
||||
monkeypatch.setattr(secrets_module, "_HAS_KEYRING", False)
|
||||
# Even if keyring is missing, the env-var tier still wins.
|
||||
assert secrets_module.get_secret("cyclone.test.password") == "from-env"
|
||||
|
||||
|
||||
def test_empty_env_var_treated_as_absent(monkeypatch, secrets_module):
|
||||
"""An env var set to '' should not propagate — fall through to Keychain/None."""
|
||||
monkeypatch.setenv("cyclone.test.password", "")
|
||||
monkeypatch.setattr(
|
||||
secrets_module.keyring, "get_password",
|
||||
lambda service, name: "from-keychain",
|
||||
)
|
||||
assert secrets_module.get_secret("cyclone.test.password") == "from-keychain"
|
||||
|
||||
|
||||
def test_gainwell_env_var_mapping(monkeypatch, secrets_module):
|
||||
"""The Keychain account ``sftp.gainwell.password`` maps to the env
|
||||
var ``CYCLONE_SFTP_PASSWORD`` via the internal name table. Without
|
||||
this, an operator setting ``CYCLONE_SFTP_PASSWORD`` on a Linux box
|
||||
would not get the MFT password."""
|
||||
monkeypatch.setenv("CYCLONE_SFTP_PASSWORD", "real-mft-password")
|
||||
monkeypatch.setattr(
|
||||
secrets_module.keyring, "get_password", lambda service, name: None,
|
||||
)
|
||||
assert secrets_module.get_secret("sftp.gainwell.password") == "real-mft-password"
|
||||
Reference in New Issue
Block a user