"""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 # ─── Пояс приложения (#68) ──────────────────────────────────────────────────── @pytest.mark.parametrize("offset", [-13, 15]) def test_tz_offset_out_of_range_rejected(offset): with pytest.raises(ValidationError, match="APP_TZ_OFFSET_HOURS"): config.Settings(app_env="development", app_tz_offset_hours=offset) def test_auth_config_exposes_app_tz_offset(client, monkeypatch): """Фронт показывает время в поясе приложения — смещение приходит из настроек.""" from app.routers import auth monkeypatch.setattr(auth.settings, "app_tz_offset_hours", 5) assert client.get("/api/auth/config").json()["tz_offset_hours"] == 5