""" Tests for Story 6.6: Environment configuration and fail-fast validation (NFR10). - With ENV=development (or unset): no fail-fast, validate_required_env returns []. - With ENV=production and missing required vars: validate_required_env returns list of missing names. - Startup exit message format (tested via validation output; full main import may lack deps in test env). """ import os import sys import subprocess import tempfile # Project root for cwd and PYTHONPATH ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def test_validate_required_env_development_returns_empty(): """In development, no required vars are enforced (defaults/warnings allowed).""" env = os.environ.copy() env.pop("ENV", None) env.pop("ENVIRONMENT", None) env["ENV"] = "development" result = subprocess.run( [sys.executable, "-c", "from config import config; print(repr(config.validate_required_env()))"], capture_output=True, text=True, cwd=ROOT, env=env, ) assert result.returncode == 0 out = (result.stdout or "").strip() assert out == "[]", f"Expected [] in development, got {out}" def test_validate_required_env_production_reports_missing(): """In production with no .env loaded, all required vars are reported missing.""" with tempfile.TemporaryDirectory() as tmp: env = { "ENV": "production", "PATH": os.environ.get("PATH", ""), "PYTHONPATH": ROOT, } # Run from empty dir so load_dotenv() does not load project .env result = subprocess.run( [ sys.executable, "-c", "from config import config; m = config.validate_required_env(); print(','.join(sorted(m)))", ], capture_output=True, text=True, cwd=tmp, env=env, ) assert result.returncode == 0 out = (result.stdout or "").strip() required_names = {"ADMIN_PASSWORD or ADMIN_PASSWORD_HASH", "ADMIN_TOKEN_SECRET", "ADMIN_USERNAME", "DATABASE_URL", "JWT_SECRET_KEY", "REDIS_URL"} reported = set(out.split(",")) if out else set() assert required_names == reported, f"Expected {required_names}, got {reported}" def test_fail_fast_message_lists_vars_and_env_example(): """With ENV=production and some vars missing, message lists them and mentions .env.example.""" with tempfile.TemporaryDirectory() as tmp: env = {"ENV": "production", "PATH": os.environ.get("PATH", ""), "PYTHONPATH": ROOT} result = subprocess.run( [ sys.executable, "-c", "from config import config; m = config.validate_required_env(); " "msg = 'Missing required env: ' + ', '.join(m) + '. Set them in .env or environment. See .env.example.' if m else ''; " "print(msg)", ], capture_output=True, text=True, cwd=tmp, env=env, ) assert result.returncode == 0 out = (result.stdout or "").strip() assert "Missing required env" in out assert ".env.example" in out assert "JWT_SECRET_KEY" in out or "DATABASE_URL" in out or "REDIS_URL" in out def test_validate_required_env_production_postgres_star_satisfies_database_url(): """When POSTGRES_* are set (and DATABASE_URL is not), DATABASE_URL is not reported missing (AC #1).""" with tempfile.TemporaryDirectory() as tmp: env = { "ENV": "production", "PATH": os.environ.get("PATH", ""), "PYTHONPATH": ROOT, "POSTGRES_HOST": "localhost", "POSTGRES_PORT": "5432", "POSTGRES_USER": "u", "POSTGRES_PASSWORD": "p", "POSTGRES_DB": "db", "JWT_SECRET_KEY": "x", "ADMIN_USERNAME": "a", "ADMIN_PASSWORD": "b", "ADMIN_TOKEN_SECRET": "c", "RATE_LIMIT_ENABLED": "false", } result = subprocess.run( [ sys.executable, "-c", "from config import config; m = config.validate_required_env(); print(','.join(sorted(m)))", ], capture_output=True, text=True, cwd=tmp, env=env, ) assert result.returncode == 0 out = (result.stdout or "").strip() assert out == "", f"Expected no missing vars when POSTGRES_* set, got {out}" def test_startup_exits_with_code_1_when_production_missing_required_env(): """App startup must exit with code 1 when ENV=production and required vars are missing (Story 6.6).""" with tempfile.TemporaryDirectory() as tmp: env = { "ENV": "production", "PATH": os.environ.get("PATH", ""), "PYTHONPATH": ROOT, } result = subprocess.run( [sys.executable, "-c", "import main"], capture_output=True, text=True, cwd=tmp, env=env, ) assert result.returncode == 1, f"Expected exit 1, got {result.returncode}. stderr: {result.stderr}" assert "Missing required env" in (result.stderr or "") assert ".env.example" in (result.stderr or "")