При LOCAL_PUBLIC=vps dev доступен на forbidden-stars.ru, а fail-fast по SECRET_KEY/ADMIN_PASSWORD работал только в production: снаружи оставались общеизвестный ключ JWT (подделка любого токена, включая админский) и пароль админки. Теперь проверка срабатывает при is_published — у прода и у dev на домене; на нём же cookie_secure. Dev-инструменты и Swagger на опубликованном dev остаются (решение владельца): лаунчеры и лог старта перечисляют, что открыто любому посетителю. В .env.example — что открывает vps и что у dev и prod должны быть разные SECRET_KEY. #69 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
143 lines
4.8 KiB
Python
143 lines
4.8 KiB
Python
"""Fail-fast конфигурации: опубликованное приложение (production и dev на домене) не
|
|
стартует с дефолтными секретами (#59, F4, #69)."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from app.core import config
|
|
|
|
_STRONG_SECRET = "k" * 40
|
|
_STRONG_ADMIN_PW = "a-strong-admin-password"
|
|
|
|
|
|
def test_production_rejects_default_secret_key():
|
|
with pytest.raises(ValidationError):
|
|
config.Settings(
|
|
app_env="production",
|
|
secret_key=config._DEFAULT_SECRET_KEY,
|
|
admin_password=_STRONG_ADMIN_PW,
|
|
)
|
|
|
|
|
|
def test_production_rejects_short_secret_key():
|
|
with pytest.raises(ValidationError):
|
|
config.Settings(
|
|
app_env="production",
|
|
secret_key="too-short",
|
|
admin_password=_STRONG_ADMIN_PW,
|
|
)
|
|
|
|
|
|
def test_production_rejects_default_admin_password():
|
|
with pytest.raises(ValidationError):
|
|
config.Settings(
|
|
app_env="production",
|
|
secret_key=_STRONG_SECRET,
|
|
admin_bootstrap_enabled=True,
|
|
admin_password=config._DEFAULT_ADMIN_PASSWORD,
|
|
)
|
|
|
|
|
|
def test_production_accepts_strong_secrets():
|
|
s = config.Settings(
|
|
app_env="production",
|
|
secret_key=_STRONG_SECRET,
|
|
admin_password=_STRONG_ADMIN_PW,
|
|
)
|
|
assert s.is_production
|
|
|
|
|
|
def test_production_skips_admin_check_when_bootstrap_disabled():
|
|
# Админ управляется вручную (bootstrap off) — дефолтный ADMIN_PASSWORD не блокирует старт.
|
|
s = config.Settings(
|
|
app_env="production",
|
|
secret_key=_STRONG_SECRET,
|
|
admin_bootstrap_enabled=False,
|
|
admin_password=config._DEFAULT_ADMIN_PASSWORD,
|
|
)
|
|
assert s.is_production
|
|
|
|
|
|
def test_development_allows_defaults():
|
|
s = config.Settings(
|
|
app_env="development",
|
|
local_public="local",
|
|
secret_key=config._DEFAULT_SECRET_KEY,
|
|
admin_password=config._DEFAULT_ADMIN_PASSWORD,
|
|
)
|
|
assert s.is_development
|
|
assert not s.is_published and not s.cookie_secure
|
|
|
|
|
|
# ─── Dev, опубликованный на домен (LOCAL_PUBLIC=vps, #69) ─────────────────────
|
|
# Снаружи он так же доступен, как прод: общеизвестный ключ JWT и пароль админа там
|
|
# открывают админку и подделку любого токена.
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"secret_key,admin_password",
|
|
[
|
|
(config._DEFAULT_SECRET_KEY, _STRONG_ADMIN_PW),
|
|
("too-short", _STRONG_ADMIN_PW),
|
|
(_STRONG_SECRET, config._DEFAULT_ADMIN_PASSWORD),
|
|
],
|
|
ids=["default-secret", "short-secret", "default-admin-password"],
|
|
)
|
|
def test_published_dev_rejects_weak_secrets(secret_key, admin_password):
|
|
with pytest.raises(ValidationError, match="LOCAL_PUBLIC=vps"):
|
|
config.Settings(
|
|
app_env="development",
|
|
local_public="vps",
|
|
secret_key=secret_key,
|
|
admin_password=admin_password,
|
|
)
|
|
|
|
|
|
def test_published_dev_accepts_strong_secrets():
|
|
s = config.Settings(
|
|
app_env="development",
|
|
local_public="vps",
|
|
secret_key=_STRONG_SECRET,
|
|
admin_password=_STRONG_ADMIN_PW,
|
|
)
|
|
assert s.is_development and s.is_published and s.cookie_secure
|
|
|
|
|
|
def test_published_dev_warns_on_startup(monkeypatch, caplog):
|
|
"""Dev-инструменты на опубликованном dev остаются (решение владельца) — но старт
|
|
громко перечисляет, что открыто любому посетителю домена."""
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app import main
|
|
|
|
monkeypatch.setattr(main.settings, "local_public", "vps")
|
|
with caplog.at_level(logging.WARNING, logger="fs"), TestClient(main.create_app()):
|
|
pass
|
|
assert "DEV ОПУБЛИКОВАН НАРУЖУ" in caplog.text
|
|
assert "вход по нику без пароля" in caplog.text
|
|
|
|
|
|
def test_local_dev_starts_quietly(caplog):
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app import main
|
|
|
|
with caplog.at_level(logging.WARNING, logger="fs"), TestClient(main.create_app()):
|
|
pass
|
|
assert "DEV ОПУБЛИКОВАН НАРУЖУ" not in caplog.text
|
|
|
|
|
|
@pytest.mark.parametrize("app_env", ["test", "staging", ""])
|
|
def test_unknown_app_env_rejected(app_env):
|
|
# Отдельного test-контура больше нет: такое значение не должно молча включать прод-пути.
|
|
with pytest.raises(ValidationError):
|
|
config.Settings(app_env=app_env)
|
|
|
|
|
|
def test_app_env_case_insensitive():
|
|
s = config.Settings(app_env="Development")
|
|
assert s.is_development
|