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:
+86
-49
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user