test(server): add comprehensive test suite (47 tests), fix DB lock and schema bugs

- Add pytest test suite: test_auth.py, test_admin.py, test_pass.py,
  test_proxy.py, test_rate_limit.py, test_client_contract.py
- Fix SQLite 'database is locked' errors: moved log_audit() calls outside
  with get_db() blocks in register, login, logout, refresh, activate_pass
- Enable WAL mode and busy_timeout in get_db() for concurrent access
- Fix /admin/me: removed non-existent 'email' column from query
- Fix /admin/users list: disambiguated activated_at column in JOIN query
- Fix /auth/refresh: now returns refresh_token + expires_in + username/uuid/role
  to match AuthManager.AuthSession expectations; revokes old refresh token
- Fix conftest.py: unique usernames per test to avoid conflicts
- All 47 tests passing
This commit is contained in:
SashegDev
2026-05-04 22:14:06 +00:00
parent c96b502ad4
commit c0310ed573
9 changed files with 808 additions and 52 deletions
+86 -49
View File
@@ -106,6 +106,8 @@ def get_db():
"""Контекстный менеджер для БД"""
conn = sqlite3.connect(str(AUTH_DB), check_same_thread=False, timeout=10)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA busy_timeout=5000")
try:
yield conn
conn.commit()
@@ -118,6 +120,7 @@ def get_db():
def init_db():
"""Инициализация основной БД"""
with get_db() as conn:
conn.executescript("PRAGMA journal_mode=WAL;")
conn.executescript("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -431,12 +434,12 @@ async def register(body: RegisterRequest, request: Request):
"INSERT INTO refresh_tokens (user_id, token_hash, jti, expires_at, created_at) VALUES (?, ?, ?, ?, ?)",
(user_id, refresh_hash, secrets.token_hex(16), now + (REFRESH_TOKEN_EXPIRE_DAYS * 86400), now)
)
log_audit(user_id, "register", f"User registered from {ip}", ip)
logger.info("User registered", username=body.username, user_id=user_id, ip=ip)
from roles import ROLE_NAMES
return TokenResponse(
log_audit(user_id, "register", f"User registered from {ip}", ip)
logger.info("User registered", username=body.username, user_id=user_id, ip=ip)
from roles import ROLE_NAMES
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
@@ -463,7 +466,6 @@ async def login(body: LoginRequest, request: Request):
if not user or not verify_password(body.password, user["password_hash"]):
record_login_attempt(ip, False)
log_audit(0, "login_failed", f"Failed login for {body.username} from {ip}", ip)
raise HTTPException(401, "Неверное имя пользователя или пароль")
if not user["is_active"]:
@@ -508,19 +510,23 @@ async def login(body: LoginRequest, request: Request):
(user["id"], refresh_hash, secrets.token_hex(16), now + (REFRESH_TOKEN_EXPIRE_DAYS * 86400), now)
)
log_audit(user["id"], "login", f"User logged in from {ip}", ip)
logger.info("User logged in", username=user["username"], user_id=user["id"], ip=ip)
from roles import ROLE_NAMES
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
username=user["username"],
uuid=user["uuid"],
role=user["role"],
role_name=ROLE_NAMES.get(user["role"], "Неизвестно")
)
user_id = user["id"]
username = user["username"]
user_role = user["role"]
log_audit(user_id, "login", f"User logged in from {ip}", ip)
logger.info("User logged in", username=username, user_id=user_id, ip=ip)
from roles import ROLE_NAMES
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
username=username,
uuid=user["uuid"],
role=user_role,
role_name=ROLE_NAMES.get(user_role, "Неизвестно")
)
@router.post("/logout")
async def logout(current_user: dict = Depends(get_current_user), request: Request = None):
@@ -536,8 +542,8 @@ async def logout(current_user: dict = Depends(get_current_user), request: Reques
"UPDATE refresh_tokens SET revoked = 1 WHERE user_id = ?",
(current_user["id"],)
)
log_audit(current_user["id"], "logout", f"User logged out from {ip}", ip)
log_audit(current_user["id"], "logout", f"User logged out from {ip}", ip)
logger.info("User logged out", user_id=current_user["id"], ip=ip)
return {"success": True}
@@ -589,12 +595,41 @@ async def refresh(body: dict, request: Request):
"jti": session_token
})
log_audit(user["id"], "refresh_token", f"Token refreshed from {ip}", ip)
new_refresh_token = create_jwt({
"sub": user["id"],
"type": "refresh",
"jti": secrets.token_hex(16)
}, expires_in=REFRESH_TOKEN_EXPIRE_DAYS * 86400)
return {
"access_token": new_access_token,
"token_type": "bearer"
}
new_refresh_hash = hashlib.sha256(new_refresh_token.encode()).hexdigest()
conn.execute(
"INSERT INTO refresh_tokens (user_id, token_hash, jti, expires_at, created_at) VALUES (?, ?, ?, ?, ?)",
(user["id"], new_refresh_hash, secrets.token_hex(16), now + (REFRESH_TOKEN_EXPIRE_DAYS * 86400), now)
)
# Revoke old refresh token
conn.execute(
"UPDATE refresh_tokens SET revoked = 1 WHERE token_hash = ?",
(token_hash,)
)
uid = user["id"]
uname = user["username"]
urole = user["role"]
uuuid = user["uuid"]
log_audit(uid, "refresh_token", f"Token refreshed from {ip}", ip)
from roles import ROLE_NAMES
return {
"access_token": new_access_token,
"refresh_token": new_refresh_token,
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60,
"username": uname,
"uuid": uuuid,
"role": urole,
"role_name": ROLE_NAMES.get(urole, "Неизвестно"),
}
@router.post("/validate")
async def validate_token(request: Request, credentials: HTTPAuthorizationCredentials = Depends(bearer)):
@@ -711,25 +746,27 @@ async def activate_pass(
(current_user["id"],),
)
conn.commit()
log_audit(
current_user["id"],
"pass_activated",
f"Pass activated: {body.pass_code[:8]}...",
ip,
)
logger.info(
"Pass activated",
user=current_user["username"],
user_id=current_user["id"],
pass_code=body.pass_code,
ip=ip,
)
return {
"success": True,
"message": f"Проходка активирована для {current_user['username']}",
"role": 1,
}
uid = current_user["id"]
uname = current_user["username"]
pcode = body.pass_code
log_audit(
uid,
"pass_activated",
f"Pass activated: {pcode[:8]}...",
ip,
)
logger.info(
"Pass activated",
user=uname,
user_id=uid,
pass_code=pcode,
ip=ip,
)
return {
"success": True,
"message": f"Проходка активирована для {uname}",
"role": 1,
}