tests upgrade
This commit is contained in:
25
tests/auth/conftest.py
Normal file
25
tests/auth/conftest.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import pytest
|
||||
from typing import Dict
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oauth_settings() -> Dict[str, Dict[str, str]]:
|
||||
"""Тестовые настройки OAuth"""
|
||||
return {
|
||||
"GOOGLE": {"id": "test_google_id", "key": "test_google_secret"},
|
||||
"GITHUB": {"id": "test_github_id", "key": "test_github_secret"},
|
||||
"FACEBOOK": {"id": "test_facebook_id", "key": "test_facebook_secret"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def frontend_url() -> str:
|
||||
"""URL фронтенда для тестов"""
|
||||
return "http://localhost:3000"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_settings(monkeypatch, oauth_settings, frontend_url):
|
||||
"""Подменяем настройки для тестов"""
|
||||
monkeypatch.setattr("auth.oauth.OAUTH_CLIENTS", oauth_settings)
|
||||
monkeypatch.setattr("auth.oauth.FRONTEND_URL", frontend_url)
|
224
tests/auth/test_oauth.py
Normal file
224
tests/auth/test_oauth.py
Normal file
@@ -0,0 +1,224 @@
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from starlette.responses import JSONResponse, RedirectResponse
|
||||
|
||||
from auth.oauth import get_user_profile, oauth_login, oauth_callback
|
||||
|
||||
# Подменяем настройки для тестов
|
||||
with (
|
||||
patch("auth.oauth.FRONTEND_URL", "http://localhost:3000"),
|
||||
patch(
|
||||
"auth.oauth.OAUTH_CLIENTS",
|
||||
{
|
||||
"GOOGLE": {"id": "test_google_id", "key": "test_google_secret"},
|
||||
"GITHUB": {"id": "test_github_id", "key": "test_github_secret"},
|
||||
"FACEBOOK": {"id": "test_facebook_id", "key": "test_facebook_secret"},
|
||||
},
|
||||
),
|
||||
):
|
||||
|
||||
@pytest.fixture
|
||||
def mock_request():
|
||||
"""Фикстура для мока запроса"""
|
||||
request = MagicMock()
|
||||
request.session = {}
|
||||
request.path_params = {}
|
||||
request.query_params = {}
|
||||
return request
|
||||
|
||||
@pytest.fixture
|
||||
def mock_oauth_client():
|
||||
"""Фикстура для мока OAuth клиента"""
|
||||
client = AsyncMock()
|
||||
client.authorize_redirect = AsyncMock()
|
||||
client.authorize_access_token = AsyncMock()
|
||||
client.get = AsyncMock()
|
||||
return client
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_profile_google():
|
||||
"""Тест получения профиля из Google"""
|
||||
client = AsyncMock()
|
||||
token = {
|
||||
"userinfo": {
|
||||
"sub": "123",
|
||||
"email": "test@gmail.com",
|
||||
"name": "Test User",
|
||||
"picture": "https://lh3.googleusercontent.com/photo=s96",
|
||||
}
|
||||
}
|
||||
|
||||
profile = await get_user_profile("google", client, token)
|
||||
|
||||
assert profile["id"] == "123"
|
||||
assert profile["email"] == "test@gmail.com"
|
||||
assert profile["name"] == "Test User"
|
||||
assert profile["picture"] == "https://lh3.googleusercontent.com/photo=s600"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_profile_github():
|
||||
"""Тест получения профиля из GitHub"""
|
||||
client = AsyncMock()
|
||||
client.get.side_effect = [
|
||||
MagicMock(
|
||||
json=lambda: {
|
||||
"id": 456,
|
||||
"login": "testuser",
|
||||
"name": "Test User",
|
||||
"avatar_url": "https://github.com/avatar.jpg",
|
||||
}
|
||||
),
|
||||
MagicMock(
|
||||
json=lambda: [
|
||||
{"email": "other@github.com", "primary": False},
|
||||
{"email": "test@github.com", "primary": True},
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
profile = await get_user_profile("github", client, {})
|
||||
|
||||
assert profile["id"] == "456"
|
||||
assert profile["email"] == "test@github.com"
|
||||
assert profile["name"] == "Test User"
|
||||
assert profile["picture"] == "https://github.com/avatar.jpg"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_profile_facebook():
|
||||
"""Тест получения профиля из Facebook"""
|
||||
client = AsyncMock()
|
||||
client.get.return_value = MagicMock(
|
||||
json=lambda: {
|
||||
"id": "789",
|
||||
"name": "Test User",
|
||||
"email": "test@facebook.com",
|
||||
"picture": {"data": {"url": "https://facebook.com/photo.jpg"}},
|
||||
}
|
||||
)
|
||||
|
||||
profile = await get_user_profile("facebook", client, {})
|
||||
|
||||
assert profile["id"] == "789"
|
||||
assert profile["email"] == "test@facebook.com"
|
||||
assert profile["name"] == "Test User"
|
||||
assert profile["picture"] == "https://facebook.com/photo.jpg"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_login_success(mock_request, mock_oauth_client):
|
||||
"""Тест успешного начала OAuth авторизации"""
|
||||
mock_request.path_params["provider"] = "google"
|
||||
|
||||
# Настраиваем мок для authorize_redirect
|
||||
redirect_response = RedirectResponse(url="http://example.com")
|
||||
mock_oauth_client.authorize_redirect.return_value = redirect_response
|
||||
|
||||
with patch("auth.oauth.oauth.create_client", return_value=mock_oauth_client):
|
||||
response = await oauth_login(mock_request)
|
||||
|
||||
assert isinstance(response, RedirectResponse)
|
||||
assert mock_request.session["provider"] == "google"
|
||||
assert "code_verifier" in mock_request.session
|
||||
assert "state" in mock_request.session
|
||||
|
||||
mock_oauth_client.authorize_redirect.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_login_invalid_provider(mock_request):
|
||||
"""Тест с неправильным провайдером"""
|
||||
mock_request.path_params["provider"] = "invalid"
|
||||
|
||||
response = await oauth_login(mock_request)
|
||||
|
||||
assert isinstance(response, JSONResponse)
|
||||
assert response.status_code == 400
|
||||
assert "Invalid provider" in response.body.decode()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_callback_success(mock_request, mock_oauth_client):
|
||||
"""Тест успешного OAuth callback"""
|
||||
mock_request.session = {
|
||||
"provider": "google",
|
||||
"code_verifier": "test_verifier",
|
||||
"state": "test_state",
|
||||
}
|
||||
mock_request.query_params["state"] = "test_state"
|
||||
|
||||
mock_oauth_client.authorize_access_token.return_value = {
|
||||
"userinfo": {"sub": "123", "email": "test@gmail.com", "name": "Test User"}
|
||||
}
|
||||
|
||||
with (
|
||||
patch("auth.oauth.oauth.create_client", return_value=mock_oauth_client),
|
||||
patch("auth.oauth.local_session") as mock_session,
|
||||
patch("auth.oauth.TokenStorage.create_session", return_value="test_token"),
|
||||
):
|
||||
# Мокаем сессию базы данных
|
||||
session = MagicMock()
|
||||
session.query.return_value.filter.return_value.first.return_value = None
|
||||
mock_session.return_value.__enter__.return_value = session
|
||||
|
||||
response = await oauth_callback(mock_request)
|
||||
|
||||
assert isinstance(response, RedirectResponse)
|
||||
assert response.status_code == 307
|
||||
assert "auth/success" in response.headers["location"]
|
||||
|
||||
# Проверяем cookie
|
||||
cookies = response.headers.getlist("set-cookie")
|
||||
assert any("session_token=test_token" in cookie for cookie in cookies)
|
||||
assert any("httponly" in cookie.lower() for cookie in cookies)
|
||||
assert any("secure" in cookie.lower() for cookie in cookies)
|
||||
|
||||
# Проверяем очистку сессии
|
||||
assert "code_verifier" not in mock_request.session
|
||||
assert "provider" not in mock_request.session
|
||||
assert "state" not in mock_request.session
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_callback_invalid_state(mock_request):
|
||||
"""Тест с неправильным state параметром"""
|
||||
mock_request.session = {"provider": "google", "state": "correct_state"}
|
||||
mock_request.query_params["state"] = "wrong_state"
|
||||
|
||||
response = await oauth_callback(mock_request)
|
||||
|
||||
assert isinstance(response, JSONResponse)
|
||||
assert response.status_code == 400
|
||||
assert "Invalid state" in response.body.decode()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_callback_existing_user(mock_request, mock_oauth_client):
|
||||
"""Тест OAuth callback с существующим пользователем"""
|
||||
mock_request.session = {
|
||||
"provider": "google",
|
||||
"code_verifier": "test_verifier",
|
||||
"state": "test_state",
|
||||
}
|
||||
mock_request.query_params["state"] = "test_state"
|
||||
|
||||
mock_oauth_client.authorize_access_token.return_value = {
|
||||
"userinfo": {"sub": "123", "email": "test@gmail.com", "name": "Test User"}
|
||||
}
|
||||
|
||||
with (
|
||||
patch("auth.oauth.oauth.create_client", return_value=mock_oauth_client),
|
||||
patch("auth.oauth.local_session") as mock_session,
|
||||
patch("auth.oauth.TokenStorage.create_session", return_value="test_token"),
|
||||
):
|
||||
# Мокаем существующего пользователя
|
||||
existing_user = MagicMock()
|
||||
session = MagicMock()
|
||||
session.query.return_value.filter.return_value.first.return_value = (
|
||||
existing_user
|
||||
)
|
||||
mock_session.return_value.__enter__.return_value = session
|
||||
|
||||
response = await oauth_callback(mock_request)
|
||||
|
||||
assert isinstance(response, RedirectResponse)
|
||||
assert response.status_code == 307
|
||||
|
||||
# Проверяем обновление существующего пользователя
|
||||
assert existing_user.name == "Test User"
|
||||
assert existing_user.oauth == "google:123"
|
||||
assert existing_user.email_verified is True
|
9
tests/auth/test_settings.py
Normal file
9
tests/auth/test_settings.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Тестовые настройки для OAuth"""
|
||||
|
||||
FRONTEND_URL = "http://localhost:3000"
|
||||
|
||||
OAUTH_CLIENTS = {
|
||||
"GOOGLE": {"id": "test_google_id", "key": "test_google_secret"},
|
||||
"GITHUB": {"id": "test_github_id", "key": "test_github_secret"},
|
||||
"FACEBOOK": {"id": "test_facebook_id", "key": "test_facebook_secret"},
|
||||
}
|
@@ -1,17 +1,7 @@
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from main import app
|
||||
from services.db import Base
|
||||
from services.redis import redis
|
||||
|
||||
# Use SQLite for testing
|
||||
TEST_DB_URL = "sqlite:///test.db"
|
||||
from tests.test_config import get_test_client
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -23,38 +13,36 @@ def event_loop():
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_engine():
|
||||
"""Create a test database engine."""
|
||||
engine = create_engine(TEST_DB_URL)
|
||||
Base.metadata.create_all(engine)
|
||||
yield engine
|
||||
Base.metadata.drop_all(engine)
|
||||
os.remove("test.db")
|
||||
def test_app():
|
||||
"""Create a test client and session factory."""
|
||||
client, SessionLocal = get_test_client()
|
||||
return client, SessionLocal
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_session(test_engine):
|
||||
def db_session(test_app):
|
||||
"""Create a new database session for a test."""
|
||||
connection = test_engine.connect()
|
||||
transaction = connection.begin()
|
||||
session = Session(bind=connection)
|
||||
_, SessionLocal = test_app
|
||||
session = SessionLocal()
|
||||
|
||||
yield session
|
||||
|
||||
session.rollback()
|
||||
session.close()
|
||||
transaction.rollback()
|
||||
connection.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_client(test_app):
|
||||
"""Get the test client."""
|
||||
client, _ = test_app
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def redis_client():
|
||||
"""Create a test Redis client."""
|
||||
await redis.connect()
|
||||
await redis.flushall() # Очищаем Redis перед каждым тестом
|
||||
yield redis
|
||||
await redis.flushall() # Очищаем после теста
|
||||
await redis.disconnect()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_client():
|
||||
"""Create a TestClient instance."""
|
||||
return TestClient(app)
|
||||
|
67
tests/test_config.py
Normal file
67
tests/test_config.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Конфигурация для тестов
|
||||
"""
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from starlette.applications import Starlette
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
# Используем in-memory SQLite для тестов
|
||||
TEST_DB_URL = "sqlite:///:memory:"
|
||||
|
||||
|
||||
class DatabaseMiddleware(BaseHTTPMiddleware):
|
||||
"""Middleware для внедрения сессии БД"""
|
||||
|
||||
def __init__(self, app, session_maker):
|
||||
super().__init__(app)
|
||||
self.session_maker = session_maker
|
||||
|
||||
async def dispatch(self, request, call_next):
|
||||
session = self.session_maker()
|
||||
request.state.db = session
|
||||
try:
|
||||
response = await call_next(request)
|
||||
finally:
|
||||
session.close()
|
||||
return response
|
||||
|
||||
|
||||
def create_test_app():
|
||||
"""Create a test Starlette application."""
|
||||
from services.db import Base
|
||||
|
||||
# Создаем движок и таблицы
|
||||
engine = create_engine(
|
||||
TEST_DB_URL,
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
echo=False,
|
||||
)
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Создаем фабрику сессий
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
|
||||
# Создаем middleware для сессий
|
||||
middleware = [Middleware(DatabaseMiddleware, session_maker=SessionLocal)]
|
||||
|
||||
# Создаем тестовое приложение
|
||||
app = Starlette(
|
||||
debug=True,
|
||||
middleware=middleware,
|
||||
routes=[], # Здесь можно добавить тестовые маршруты если нужно
|
||||
)
|
||||
|
||||
return app, SessionLocal
|
||||
|
||||
|
||||
def get_test_client():
|
||||
"""Get a test client with initialized database."""
|
||||
app, SessionLocal = create_test_app()
|
||||
return TestClient(app), SessionLocal
|
@@ -53,7 +53,11 @@ async def test_create_reaction(test_client, db_session, test_setup):
|
||||
}
|
||||
""",
|
||||
"variables": {
|
||||
"reaction": {"shout": test_setup["shout"].id, "kind": ReactionKind.LIKE.value, "body": "Great post!"}
|
||||
"reaction": {
|
||||
"shout": test_setup["shout"].id,
|
||||
"kind": ReactionKind.LIKE.value,
|
||||
"body": "Great post!",
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -61,4 +65,6 @@ async def test_create_reaction(test_client, db_session, test_setup):
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "error" not in data
|
||||
assert data["data"]["create_reaction"]["reaction"]["kind"] == ReactionKind.LIKE.value
|
||||
assert (
|
||||
data["data"]["create_reaction"]["reaction"]["kind"] == ReactionKind.LIKE.value
|
||||
)
|
||||
|
@@ -1,70 +0,0 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from auth.validations import (
|
||||
AuthInput,
|
||||
AuthResponse,
|
||||
TokenPayload,
|
||||
UserRegistrationInput,
|
||||
)
|
||||
|
||||
|
||||
class TestAuthValidations:
|
||||
def test_auth_input(self):
|
||||
"""Test basic auth input validation"""
|
||||
# Valid case
|
||||
auth = AuthInput(user_id="123", username="testuser", token="1234567890abcdef1234567890abcdef")
|
||||
assert auth.user_id == "123"
|
||||
assert auth.username == "testuser"
|
||||
|
||||
# Invalid cases
|
||||
with pytest.raises(ValidationError):
|
||||
AuthInput(user_id="", username="test", token="x" * 32)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
AuthInput(user_id="123", username="t", token="x" * 32)
|
||||
|
||||
def test_user_registration(self):
|
||||
"""Test user registration validation"""
|
||||
# Valid case
|
||||
user = UserRegistrationInput(email="test@example.com", password="SecurePass123!", name="Test User")
|
||||
assert user.email == "test@example.com"
|
||||
assert user.name == "Test User"
|
||||
|
||||
# Test email validation
|
||||
with pytest.raises(ValidationError) as exc:
|
||||
UserRegistrationInput(email="invalid-email", password="SecurePass123!", name="Test")
|
||||
assert "Invalid email format" in str(exc.value)
|
||||
|
||||
# Test password validation
|
||||
with pytest.raises(ValidationError) as exc:
|
||||
UserRegistrationInput(email="test@example.com", password="weak", name="Test")
|
||||
assert "String should have at least 8 characters" in str(exc.value)
|
||||
|
||||
def test_token_payload(self):
|
||||
"""Test token payload validation"""
|
||||
now = datetime.utcnow()
|
||||
exp = now + timedelta(hours=1)
|
||||
|
||||
payload = TokenPayload(user_id="123", username="testuser", exp=exp, iat=now)
|
||||
assert payload.user_id == "123"
|
||||
assert payload.username == "testuser"
|
||||
assert payload.scopes == [] # Default empty list
|
||||
|
||||
def test_auth_response(self):
|
||||
"""Test auth response validation"""
|
||||
# Success case
|
||||
success_resp = AuthResponse(success=True, token="valid_token", user={"id": "123", "name": "Test"})
|
||||
assert success_resp.success is True
|
||||
assert success_resp.token == "valid_token"
|
||||
|
||||
# Error case
|
||||
error_resp = AuthResponse(success=False, error="Invalid credentials")
|
||||
assert error_resp.success is False
|
||||
assert error_resp.error == "Invalid credentials"
|
||||
|
||||
# Invalid case - отсутствует обязательное поле token при success=True
|
||||
with pytest.raises(ValidationError):
|
||||
AuthResponse(success=True, user={"id": "123", "name": "Test"})
|
Reference in New Issue
Block a user