Merge pull request 'В test/prod нет штатного способа сменить пароль администратора (#73)' (#95) from issue-73-admin-password into dev

Reviewed-on: #95
This commit was merged in pull request #95.
This commit is contained in:
2026-09-19 00:10:36 +03:00
5 changed files with 141 additions and 9 deletions
@@ -0,0 +1,77 @@
"""Ротация пароля администратора из .env (#73): `python -m app.bootstrap --reset-admin-password`."""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from sqlmodel import Session, select
from app import bootstrap
from app.core.security import verify_password
from app.models import User
from tests.conftest import csrf_headers
OLD, NEW = "secret123", "brand-new-admin-pw"
def _admin(engine) -> User:
with Session(engine) as s:
return s.exec(select(User).where(User.role == "admin")).one()
def _login(client: TestClient, password: str):
return client.post(
"/api/admin/auth/login",
json={"username": "admin", "password": password},
headers=csrf_headers(client),
)
def test_reset_applies_env_password_and_revokes_sessions(
client: TestClient, engine, make_admin, monkeypatch
):
make_admin("admin", OLD)
assert _login(client, OLD).status_code == 200
assert client.get("/api/admin/me").status_code == 200
version = _admin(engine).token_version
monkeypatch.setattr(bootstrap.settings, "admin_password", NEW)
with Session(engine) as s:
bootstrap.reset_admin_password(s)
admin = _admin(engine)
assert verify_password(NEW, admin.password_hash)
assert admin.token_version == version + 1
# Прежняя админская cookie больше не действует (в т.ч. у того, кто украл пароль).
assert client.get("/api/admin/me").status_code == 401
assert _login(client, OLD).status_code == 401
assert _login(client, NEW).status_code == 200
def test_regular_bootstrap_keeps_admin_password_outside_dev(engine, make_admin, monkeypatch):
"""Обычный старт в prod по-прежнему не берёт пароль из .env — только ротация."""
make_admin("admin", OLD)
monkeypatch.setattr(bootstrap.settings, "app_env", "production")
monkeypatch.setattr(bootstrap.settings, "admin_password", NEW)
with Session(engine) as s:
bootstrap._ensure_admin(s)
assert verify_password(OLD, _admin(engine).password_hash)
def test_reset_refuses_without_admin_or_password(engine, make_admin, monkeypatch):
monkeypatch.setattr(bootstrap.settings, "admin_password", NEW)
with Session(engine) as s, pytest.raises(RuntimeError, match="Администратора ещё нет"):
bootstrap.reset_admin_password(s)
make_admin("admin", OLD)
monkeypatch.setattr(bootstrap.settings, "admin_password", " ")
with Session(engine) as s, pytest.raises(RuntimeError, match="ADMIN_PASSWORD пуст"):
bootstrap.reset_admin_password(s)
assert verify_password(OLD, _admin(engine).password_hash)
def test_reset_refuses_when_bootstrap_disabled(engine, make_admin, monkeypatch):
make_admin("admin", OLD)
monkeypatch.setattr(bootstrap.settings, "admin_bootstrap_enabled", False)
monkeypatch.setattr(bootstrap.settings, "admin_password", NEW)
with Session(engine) as s, pytest.raises(RuntimeError, match="ADMIN_BOOTSTRAP_ENABLED"):
bootstrap.reset_admin_password(s)