This commit is contained in:
@@ -68,10 +68,8 @@ uv run pytest --cov=services,utils,orm,resolvers
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run ruff linter
|
# Run ruff linter
|
||||||
uv run ruff check .
|
uv run ruff check . --select I
|
||||||
|
uv run ruff format --line-length=120
|
||||||
# Run ruff formatter
|
|
||||||
uv run ruff format .
|
|
||||||
|
|
||||||
# Run mypy type checker
|
# Run mypy type checker
|
||||||
uv run mypy .
|
uv run mypy .
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ async def get_auth_token(request: Any) -> Optional[str]:
|
|||||||
# 2. Проверяем наличие auth_token в scope (приоритет)
|
# 2. Проверяем наличие auth_token в scope (приоритет)
|
||||||
if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth_token" in request.scope:
|
if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth_token" in request.scope:
|
||||||
token = request.scope.get("auth_token")
|
token = request.scope.get("auth_token")
|
||||||
|
if token is not None:
|
||||||
token_len = len(token) if hasattr(token, "__len__") else "unknown"
|
token_len = len(token) if hasattr(token, "__len__") else "unknown"
|
||||||
logger.debug(f"[decorators] Токен получен из request.scope['auth_token']: {token_len}")
|
logger.debug(f"[decorators] Токен получен из request.scope['auth_token']: {token_len}")
|
||||||
return token
|
return token
|
||||||
@@ -147,7 +148,8 @@ async def get_auth_token(request: Any) -> Optional[str]:
|
|||||||
if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth" in request.scope:
|
if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth" in request.scope:
|
||||||
auth_info = request.scope.get("auth", {})
|
auth_info = request.scope.get("auth", {})
|
||||||
if isinstance(auth_info, dict) and "token" in auth_info:
|
if isinstance(auth_info, dict) and "token" in auth_info:
|
||||||
token = auth_info["token"]
|
token = auth_info.get("token")
|
||||||
|
if token is not None:
|
||||||
token_len = len(token) if hasattr(token, "__len__") else "unknown"
|
token_len = len(token) if hasattr(token, "__len__") else "unknown"
|
||||||
logger.debug(f"[decorators] Токен получен из request.scope['auth']: {token_len}")
|
logger.debug(f"[decorators] Токен получен из request.scope['auth']: {token_len}")
|
||||||
return token
|
return token
|
||||||
@@ -164,6 +166,7 @@ async def get_auth_token(request: Any) -> Optional[str]:
|
|||||||
logger.debug(f"[decorators] Токен получен из заголовка {SESSION_TOKEN_HEADER}: {token_len}")
|
logger.debug(f"[decorators] Токен получен из заголовка {SESSION_TOKEN_HEADER}: {token_len}")
|
||||||
return token
|
return token
|
||||||
token = auth_header.strip()
|
token = auth_header.strip()
|
||||||
|
if token:
|
||||||
token_len = len(token) if hasattr(token, "__len__") else "unknown"
|
token_len = len(token) if hasattr(token, "__len__") else "unknown"
|
||||||
logger.debug(f"[decorators] Прямой токен получен из заголовка {SESSION_TOKEN_HEADER}: {token_len}")
|
logger.debug(f"[decorators] Прямой токен получен из заголовка {SESSION_TOKEN_HEADER}: {token_len}")
|
||||||
return token
|
return token
|
||||||
@@ -173,6 +176,7 @@ async def get_auth_token(request: Any) -> Optional[str]:
|
|||||||
auth_header = headers.get("authorization", "")
|
auth_header = headers.get("authorization", "")
|
||||||
if auth_header and auth_header.startswith("Bearer "):
|
if auth_header and auth_header.startswith("Bearer "):
|
||||||
token = auth_header[7:].strip()
|
token = auth_header[7:].strip()
|
||||||
|
if token:
|
||||||
token_len = len(token) if hasattr(token, "__len__") else "unknown"
|
token_len = len(token) if hasattr(token, "__len__") else "unknown"
|
||||||
logger.debug(f"[decorators] Токен получен из заголовка Authorization: {token_len}")
|
logger.debug(f"[decorators] Токен получен из заголовка Authorization: {token_len}")
|
||||||
return token
|
return token
|
||||||
@@ -232,6 +236,7 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Если аутентификации нет в request.auth, пробуем получить ее из scope
|
# Если аутентификации нет в request.auth, пробуем получить ее из scope
|
||||||
|
token: Optional[str] = None
|
||||||
if hasattr(request, "scope") and "auth" in request.scope:
|
if hasattr(request, "scope") and "auth" in request.scope:
|
||||||
auth_cred = request.scope.get("auth")
|
auth_cred = request.scope.get("auth")
|
||||||
if isinstance(auth_cred, AuthCredentials) and getattr(auth_cred, "logged_in", False):
|
if isinstance(auth_cred, AuthCredentials) and getattr(auth_cred, "logged_in", False):
|
||||||
@@ -261,7 +266,8 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Логируем информацию о найденном токене
|
# Логируем информацию о найденном токене
|
||||||
logger.debug(f"[validate_graphql_context] Токен найден, длина: {len(token)}")
|
token_len = len(token) if hasattr(token, "__len__") else 0
|
||||||
|
logger.debug(f"[validate_graphql_context] Токен найден, длина: {token_len}")
|
||||||
|
|
||||||
# Используем единый механизм проверки токена из auth.internal
|
# Используем единый механизм проверки токена из auth.internal
|
||||||
auth_state = await authenticate(request)
|
auth_state = await authenticate(request)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
from ariadne.asgi.handlers import GraphQLHTTPHandler
|
from ariadne.asgi.handlers import GraphQLHTTPHandler
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
@@ -62,20 +64,22 @@ class EnhancedGraphQLHTTPHandler(GraphQLHTTPHandler):
|
|||||||
# Добавляем данные авторизации только если они доступны
|
# Добавляем данные авторизации только если они доступны
|
||||||
# Проверяем наличие данных авторизации в scope
|
# Проверяем наличие данных авторизации в scope
|
||||||
if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth" in request.scope:
|
if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth" in request.scope:
|
||||||
auth_cred = request.scope.get("auth")
|
auth_cred: Any | None = request.scope.get("auth")
|
||||||
context["auth"] = auth_cred
|
context["auth"] = auth_cred
|
||||||
# Безопасно логируем информацию о типе объекта auth
|
# Безопасно логируем информацию о типе объекта auth
|
||||||
logger.debug(f"[graphql] Добавлены данные авторизации в контекст из scope: {type(auth_cred).__name__}")
|
logger.debug(f"[graphql] Добавлены данные авторизации в контекст из scope: {type(auth_cred).__name__}")
|
||||||
|
|
||||||
# Проверяем, есть ли токен в auth_cred
|
# Проверяем, есть ли токен в auth_cred
|
||||||
if hasattr(auth_cred, "token") and auth_cred.token:
|
if auth_cred is not None and hasattr(auth_cred, "token") and getattr(auth_cred, "token"):
|
||||||
logger.debug(f"[graphql] Токен найден в auth_cred: {len(auth_cred.token)}")
|
token_val = auth_cred.token
|
||||||
|
token_len = len(token_val) if hasattr(token_val, "__len__") else 0
|
||||||
|
logger.debug(f"[graphql] Токен найден в auth_cred: {token_len}")
|
||||||
else:
|
else:
|
||||||
logger.debug("[graphql] Токен НЕ найден в auth_cred")
|
logger.debug("[graphql] Токен НЕ найден в auth_cred")
|
||||||
|
|
||||||
# Добавляем author_id в контекст для RBAC
|
# Добавляем author_id в контекст для RBAC
|
||||||
author_id = None
|
author_id = None
|
||||||
if hasattr(auth_cred, "author_id") and auth_cred.author_id:
|
if auth_cred is not None and hasattr(auth_cred, "author_id") and getattr(auth_cred, "author_id"):
|
||||||
author_id = auth_cred.author_id
|
author_id = auth_cred.author_id
|
||||||
elif isinstance(auth_cred, dict) and "author_id" in auth_cred:
|
elif isinstance(auth_cred, dict) and "author_id" in auth_cred:
|
||||||
author_id = auth_cred["author_id"]
|
author_id = auth_cred["author_id"]
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class AuthMiddleware:
|
|||||||
|
|
||||||
# Роли пользователя будут определяться в контексте конкретной операции
|
# Роли пользователя будут определяться в контексте конкретной операции
|
||||||
# через RBAC систему, а не здесь
|
# через RBAC систему, а не здесь
|
||||||
roles = []
|
roles: list[str] = []
|
||||||
|
|
||||||
# Обновляем last_seen
|
# Обновляем last_seen
|
||||||
author.last_seen = int(time.time())
|
author.last_seen = int(time.time())
|
||||||
|
|||||||
@@ -55,12 +55,16 @@ class BatchTokenOperations(BaseTokenManager):
|
|||||||
valid_tokens = []
|
valid_tokens = []
|
||||||
|
|
||||||
for token, payload in zip(token_batch, decoded_payloads):
|
for token, payload in zip(token_batch, decoded_payloads):
|
||||||
if isinstance(payload, Exception) or not payload:
|
if isinstance(payload, Exception) or payload is None:
|
||||||
results[token] = False
|
results[token] = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# payload может быть словарем или объектом, обрабатываем оба случая
|
# payload может быть словарем или объектом, обрабатываем оба случая
|
||||||
user_id = payload.user_id if hasattr(payload, "user_id") else payload.get("user_id")
|
user_id = (
|
||||||
|
payload.user_id
|
||||||
|
if hasattr(payload, "user_id")
|
||||||
|
else (payload.get("user_id") if isinstance(payload, dict) else None)
|
||||||
|
)
|
||||||
if not user_id:
|
if not user_id:
|
||||||
results[token] = False
|
results[token] = False
|
||||||
continue
|
continue
|
||||||
@@ -119,10 +123,18 @@ class BatchTokenOperations(BaseTokenManager):
|
|||||||
# Декодируем токены и подготавливаем операции
|
# Декодируем токены и подготавливаем операции
|
||||||
for token in token_batch:
|
for token in token_batch:
|
||||||
payload = await self._safe_decode_token(token)
|
payload = await self._safe_decode_token(token)
|
||||||
if payload:
|
if payload is not None:
|
||||||
# payload может быть словарем или объектом, обрабатываем оба случая
|
# payload может быть словарем или объектом, обрабатываем оба случая
|
||||||
user_id = payload.user_id if hasattr(payload, "user_id") else payload.get("user_id")
|
user_id = (
|
||||||
username = payload.username if hasattr(payload, "username") else payload.get("username")
|
payload.user_id
|
||||||
|
if hasattr(payload, "user_id")
|
||||||
|
else (payload.get("user_id") if isinstance(payload, dict) else None)
|
||||||
|
)
|
||||||
|
username = (
|
||||||
|
payload.username
|
||||||
|
if hasattr(payload, "username")
|
||||||
|
else (payload.get("username") if isinstance(payload, dict) else None)
|
||||||
|
)
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -652,25 +652,11 @@ class CommunityAuthor(BaseModel):
|
|||||||
Returns:
|
Returns:
|
||||||
Словарь со статистикой ролей
|
Словарь со статистикой ролей
|
||||||
"""
|
"""
|
||||||
|
# Загружаем список авторов сообщества (одним способом вне зависимости от сессии)
|
||||||
if session is None:
|
if session is None:
|
||||||
with local_session() as s:
|
with local_session() as s:
|
||||||
community_authors = s.query(cls).where(cls.community_id == community_id).all()
|
community_authors = s.query(cls).where(cls.community_id == community_id).all()
|
||||||
|
else:
|
||||||
role_counts: dict[str, int] = {}
|
|
||||||
total_members = len(community_authors)
|
|
||||||
|
|
||||||
for ca in community_authors:
|
|
||||||
for role in ca.role_list:
|
|
||||||
role_counts[role] = role_counts.get(role, 0) + 1
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total_members": total_members,
|
|
||||||
"role_counts": role_counts,
|
|
||||||
"roles_distribution": {
|
|
||||||
role: count / total_members if total_members > 0 else 0 for role, count in role_counts.items()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
community_authors = session.query(cls).where(cls.community_id == community_id).all()
|
community_authors = session.query(cls).where(cls.community_id == community_id).all()
|
||||||
|
|
||||||
role_counts: dict[str, int] = {}
|
role_counts: dict[str, int] = {}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from sqlalchemy.pool import StaticPool
|
|||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from starlette.testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
import requests
|
||||||
|
|
||||||
from services.redis import redis
|
from services.redis import redis
|
||||||
from orm.base import BaseModel as Base
|
from orm.base import BaseModel as Base
|
||||||
@@ -27,6 +28,22 @@ def get_test_client():
|
|||||||
return app
|
return app
|
||||||
|
|
||||||
return TestClient(_import_app())
|
return TestClient(_import_app())
|
||||||
|
@pytest.fixture(autouse=True, scope="session")
|
||||||
|
def _set_requests_default_timeout():
|
||||||
|
"""Глобально задаем таймаут по умолчанию для requests в тестах, чтобы исключить зависания.
|
||||||
|
|
||||||
|
🪓 Упрощение: мокаем методы requests, добавляя timeout=10, если он не указан.
|
||||||
|
"""
|
||||||
|
original_request = requests.sessions.Session.request
|
||||||
|
|
||||||
|
def request_with_default_timeout(self, method, url, **kwargs): # type: ignore[override]
|
||||||
|
if "timeout" not in kwargs:
|
||||||
|
kwargs["timeout"] = 10
|
||||||
|
return original_request(self, method, url, **kwargs)
|
||||||
|
|
||||||
|
requests.sessions.Session.request = request_with_default_timeout # type: ignore[assignment]
|
||||||
|
yield
|
||||||
|
requests.sessions.Session.request = original_request # type: ignore[assignment]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ class TestCommunityDeleteE2EBrowser:
|
|||||||
# В CI/CD используем uv run python
|
# В CI/CD используем uv run python
|
||||||
backend_process = subprocess.Popen(
|
backend_process = subprocess.Popen(
|
||||||
["uv", "run", "python", "dev.py"],
|
["uv", "run", "python", "dev.py"],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.DEVNULL,
|
||||||
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,11 +80,7 @@ class TestCommunityDeleteE2EBrowser:
|
|||||||
# Если сервер не запустился, выводим логи и завершаем тест
|
# Если сервер не запустился, выводим логи и завершаем тест
|
||||||
print("❌ Бэкенд сервер не запустился за 20 секунд")
|
print("❌ Бэкенд сервер не запустился за 20 секунд")
|
||||||
|
|
||||||
# Получаем логи процесса
|
# Логи процесса не собираем, чтобы не блокировать выполнение
|
||||||
if backend_process:
|
|
||||||
stdout, stderr = backend_process.communicate()
|
|
||||||
print(f"📋 STDOUT: {stdout.decode()}")
|
|
||||||
print(f"📋 STDERR: {stderr.decode()}")
|
|
||||||
|
|
||||||
raise Exception("Бэкенд сервер не запустился за 20 секунд")
|
raise Exception("Бэкенд сервер не запустился за 20 секунд")
|
||||||
|
|
||||||
@@ -128,8 +124,8 @@ class TestCommunityDeleteE2EBrowser:
|
|||||||
try:
|
try:
|
||||||
frontend_process = subprocess.Popen(
|
frontend_process = subprocess.Popen(
|
||||||
["npm", "run", "dev"],
|
["npm", "run", "dev"],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.DEVNULL,
|
||||||
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -149,11 +145,7 @@ class TestCommunityDeleteE2EBrowser:
|
|||||||
# Если фронтенд не запустился, выводим логи
|
# Если фронтенд не запустился, выводим логи
|
||||||
print("❌ Фронтенд сервер не запустился за 15 секунд")
|
print("❌ Фронтенд сервер не запустился за 15 секунд")
|
||||||
|
|
||||||
# Получаем логи процесса
|
# Логи процесса не собираем, чтобы не блокировать выполнение
|
||||||
if frontend_process:
|
|
||||||
stdout, stderr = frontend_process.communicate()
|
|
||||||
print(f"📋 STDOUT: {stdout.decode()}")
|
|
||||||
print(f"📋 STDERR: {stderr.decode()}")
|
|
||||||
|
|
||||||
print("⚠️ Продолжаем тест без фронтенда (только API тесты)")
|
print("⚠️ Продолжаем тест без фронтенда (только API тесты)")
|
||||||
frontend_process = None
|
frontend_process = None
|
||||||
|
|||||||
Reference in New Issue
Block a user