admin_api.py called load_dotenv() unconditionally at module level, so merely importing it (e.g. pytest collecting tests/unit/test_admin_effective_config.py) ran python-dotenv's upward find_dotenv() search, loaded the real repo .env (secrets included), and polluted os.environ for the rest of the test session. Gate the load_dotenv() call behind `if __name__ == "__main__":` at the same source location, so it still runs before the module-level os.environ.get(...) constants when run as the entrypoint (`python admin_api.py` is unchanged) but never fires on a plain import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
79 lines
3.1 KiB
Python
79 lines
3.1 KiB
Python
"""Admin effective-config endpoint: masking, drift, and fail-closed auth (plan 007 step 6)."""
|
|
|
|
import importlib
|
|
import os
|
|
|
|
import admin_api
|
|
|
|
# A key defined only in the real repo .env (never set by tests/CI) — used to
|
|
# prove import-time isolation without ever reading/asserting its value.
|
|
_ENV_ONLY_SENTINEL_KEY = "DATABENTO_API_KEY"
|
|
|
|
|
|
def test_importing_admin_api_does_not_load_dotenv():
|
|
"""Importing (or reloading) admin_api must be env-neutral: it must not
|
|
read the repo .env. A bare module-level load_dotenv() used to run on
|
|
every import — including pytest collection — leaking real secrets into
|
|
the whole test session. Only running admin_api as __main__ may load it."""
|
|
before = os.environ.pop(_ENV_ONLY_SENTINEL_KEY, None)
|
|
try:
|
|
importlib.reload(admin_api)
|
|
# bool-ify before asserting: keeps a real secret value out of any
|
|
# pytest assertion-rewrite diff if this ever fails again.
|
|
leaked = _ENV_ONLY_SENTINEL_KEY in os.environ
|
|
assert leaked is False, f"{_ENV_ONLY_SENTINEL_KEY} was set by importing admin_api"
|
|
finally:
|
|
if before is not None:
|
|
os.environ[_ENV_ONLY_SENTINEL_KEY] = before
|
|
else:
|
|
os.environ.pop(_ENV_ONLY_SENTINEL_KEY, None)
|
|
|
|
|
|
def test_effective_config_masks_secret_values(monkeypatch):
|
|
monkeypatch.setenv("ALPACA_API_KEY", "supersecretvalue")
|
|
monkeypatch.setenv("TRADING_MIN_QUALITY_SCORE", "0.60")
|
|
cfg = admin_api.effective_config()
|
|
masked = cfg["ALPACA_API_KEY"]["value_masked"]
|
|
assert masked.startswith("***") and "supersecret" not in masked
|
|
# non-secret editable knob is shown verbatim, sourced from the process env
|
|
assert cfg["TRADING_MIN_QUALITY_SCORE"]["value_masked"] == "0.60"
|
|
assert cfg["TRADING_MIN_QUALITY_SCORE"]["source"] == "compose"
|
|
|
|
|
|
def test_mask_value_covers_database_url():
|
|
masked = admin_api._mask_value("DATABASE_URL", "postgresql://u:pw@host:5432/db")
|
|
assert masked.startswith("***") and "pw@host" not in masked
|
|
|
|
|
|
def test_overrides_source_wins(monkeypatch):
|
|
monkeypatch.setattr(admin_api.env_overrides, "read_overrides",
|
|
lambda: {"TRADING_PRICE_MAX": "20.0"})
|
|
monkeypatch.setenv("TRADING_PRICE_MAX", "20.0")
|
|
cfg = admin_api.effective_config()
|
|
assert cfg["TRADING_PRICE_MAX"]["source"] == "overrides.env"
|
|
|
|
|
|
def test_missing_required_key_is_drift(monkeypatch):
|
|
monkeypatch.delenv("POLYGON_API_KEY", raising=False) # documented in .env.example
|
|
cfg = admin_api.effective_config()
|
|
row = cfg.get("POLYGON_API_KEY", {})
|
|
assert row.get("drift") is True
|
|
assert row.get("source") == "missing"
|
|
|
|
|
|
def test_admin_auth_fail_closed(monkeypatch):
|
|
h = admin_api.Handler.__new__(admin_api.Handler)
|
|
monkeypatch.setattr(admin_api, "ADMIN_TOKEN", "")
|
|
h.headers = {}
|
|
assert h._authed() is False # no token configured -> deny
|
|
monkeypatch.setattr(admin_api, "ADMIN_TOKEN", "tok")
|
|
h.headers = {"X-Admin-Token": "tok"}
|
|
assert h._authed() is True
|
|
h.headers = {"X-Admin-Token": "nope"}
|
|
assert h._authed() is False
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
import pytest
|
|
pytest.main([__file__, "-q"])
|