Files
ForbiddenStarsApp/backend/tests/test_config_security.py
T
NotBigGhostandClaude Opus 5 8d4f7a95df Время: один пояс приложения из .env и на сервере, и на фронте
APP_TZ_OFFSET_HOURS влиял только на «дату игры», а фронт показывал время в
захардкоженных +3 — при другом значении дата и время партии противоречили друг
другу. Теперь /api/auth/config отдаёт tz_offset_hours, App.tsx выставляет его в
format.ts до первой отрисовки страниц, и в нём же показывается всё время и
вводятся даты объявлений (запасное значение +3, если конфиг недоступен). Пояс
устройства не учитывается — решение владельца. Валидатор ограничивает смещение
−12..14. #68

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
2026-09-19 03:05:30 +03:00

160 lines
5.6 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
# ─── Пояс приложения (#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