Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
138 lines
5.2 KiB
Python
138 lines
5.2 KiB
Python
"""
|
|
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 "")
|