All checks were successful
Deploy on push / deploy (push) Successful in 5m47s
- **🔍 Comprehensive authentication documentation refactoring**: Полная переработка документации аутентификации
- Обновлена таблица содержания в README.md
- Исправлены архитектурные диаграммы - токены хранятся только в Redis
- Добавлены практические примеры кода для микросервисов
- Консолидирована OAuth документация
846 lines
27 KiB
Markdown
846 lines
27 KiB
Markdown
# 🧪 Тестирование системы аутентификации
|
||
|
||
## 🎯 Обзор
|
||
|
||
Комплексная стратегия тестирования системы аутентификации с unit, integration и E2E тестами.
|
||
|
||
## 🏗️ Структура тестов
|
||
|
||
```
|
||
tests/auth/
|
||
├── unit/
|
||
│ ├── test_session_manager.py
|
||
│ ├── test_oauth_manager.py
|
||
│ ├── test_batch_operations.py
|
||
│ ├── test_monitoring.py
|
||
│ └── test_utils.py
|
||
├── integration/
|
||
│ ├── test_redis_integration.py
|
||
│ ├── test_oauth_flow.py
|
||
│ ├── test_middleware.py
|
||
│ └── test_decorators.py
|
||
├── e2e/
|
||
│ ├── test_login_flow.py
|
||
│ ├── test_oauth_flow.py
|
||
│ └── test_session_management.py
|
||
└── fixtures/
|
||
├── auth_fixtures.py
|
||
├── redis_fixtures.py
|
||
└── oauth_fixtures.py
|
||
```
|
||
|
||
## 🔧 Unit Tests
|
||
|
||
### SessionTokenManager Tests
|
||
|
||
```python
|
||
import pytest
|
||
from unittest.mock import AsyncMock, patch
|
||
from auth.tokens.sessions import SessionTokenManager
|
||
|
||
class TestSessionTokenManager:
|
||
|
||
@pytest.fixture
|
||
def session_manager(self):
|
||
return SessionTokenManager()
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_session(self, session_manager):
|
||
"""Тест создания сессии"""
|
||
with patch('auth.tokens.sessions.redis') as mock_redis:
|
||
mock_redis.hset = AsyncMock()
|
||
mock_redis.sadd = AsyncMock()
|
||
mock_redis.expire = AsyncMock()
|
||
|
||
token = await session_manager.create_session(
|
||
user_id="123",
|
||
username="testuser"
|
||
)
|
||
|
||
assert token is not None
|
||
assert len(token) > 20
|
||
mock_redis.hset.assert_called()
|
||
mock_redis.sadd.assert_called()
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_verify_session_valid(self, session_manager):
|
||
"""Тест проверки валидной сессии"""
|
||
with patch('auth.jwtcodec.decode_jwt') as mock_decode:
|
||
mock_decode.return_value = {
|
||
"user_id": "123",
|
||
"username": "testuser",
|
||
"exp": int(time.time()) + 3600
|
||
}
|
||
|
||
with patch('auth.tokens.sessions.redis') as mock_redis:
|
||
mock_redis.exists.return_value = True
|
||
|
||
payload = await session_manager.verify_session("valid_token")
|
||
|
||
assert payload is not None
|
||
assert payload["user_id"] == "123"
|
||
assert payload["username"] == "testuser"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_verify_session_invalid(self, session_manager):
|
||
"""Тест проверки невалидной сессии"""
|
||
with patch('auth.jwtcodec.decode_jwt') as mock_decode:
|
||
mock_decode.return_value = None
|
||
|
||
payload = await session_manager.verify_session("invalid_token")
|
||
|
||
assert payload is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_revoke_session_token(self, session_manager):
|
||
"""Тест отзыва токена сессии"""
|
||
with patch('auth.tokens.sessions.redis') as mock_redis:
|
||
mock_redis.delete = AsyncMock(return_value=1)
|
||
mock_redis.srem = AsyncMock()
|
||
|
||
result = await session_manager.revoke_session_token("test_token")
|
||
|
||
assert result is True
|
||
mock_redis.delete.assert_called()
|
||
mock_redis.srem.assert_called()
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_user_sessions(self, session_manager):
|
||
"""Тест получения сессий пользователя"""
|
||
with patch('auth.tokens.sessions.redis') as mock_redis:
|
||
mock_redis.smembers.return_value = {b"token1", b"token2"}
|
||
mock_redis.hgetall.return_value = {
|
||
b"user_id": b"123",
|
||
b"username": b"testuser",
|
||
b"last_activity": b"1640995200"
|
||
}
|
||
|
||
sessions = await session_manager.get_user_sessions("123")
|
||
|
||
assert len(sessions) == 2
|
||
assert sessions[0]["token"] == "token1"
|
||
assert sessions[0]["user_id"] == "123"
|
||
```
|
||
|
||
### OAuthTokenManager Tests
|
||
|
||
```python
|
||
import pytest
|
||
from unittest.mock import AsyncMock, patch
|
||
from auth.tokens.oauth import OAuthTokenManager
|
||
|
||
class TestOAuthTokenManager:
|
||
|
||
@pytest.fixture
|
||
def oauth_manager(self):
|
||
return OAuthTokenManager()
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_store_oauth_tokens(self, oauth_manager):
|
||
"""Тест сохранения OAuth токенов"""
|
||
with patch('auth.tokens.oauth.redis') as mock_redis:
|
||
mock_redis.setex = AsyncMock()
|
||
|
||
await oauth_manager.store_oauth_tokens(
|
||
user_id="123",
|
||
provider="google",
|
||
access_token="access_token_123",
|
||
refresh_token="refresh_token_123",
|
||
expires_in=3600
|
||
)
|
||
|
||
# Проверяем, что токены сохранены
|
||
assert mock_redis.setex.call_count == 2 # access + refresh
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_token(self, oauth_manager):
|
||
"""Тест получения OAuth токена"""
|
||
with patch('auth.tokens.oauth.redis') as mock_redis:
|
||
mock_redis.get.return_value = b'{"token": "access_token_123", "expires_in": 3600}'
|
||
|
||
token_data = await oauth_manager.get_token("123", "google", "oauth_access")
|
||
|
||
assert token_data is not None
|
||
assert token_data["token"] == "access_token_123"
|
||
assert token_data["expires_in"] == 3600
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_revoke_oauth_tokens(self, oauth_manager):
|
||
"""Тест отзыва OAuth токенов"""
|
||
with patch('auth.tokens.oauth.redis') as mock_redis:
|
||
mock_redis.delete = AsyncMock(return_value=2)
|
||
|
||
result = await oauth_manager.revoke_oauth_tokens("123", "google")
|
||
|
||
assert result is True
|
||
mock_redis.delete.assert_called()
|
||
```
|
||
|
||
### BatchTokenOperations Tests
|
||
|
||
```python
|
||
import pytest
|
||
from unittest.mock import AsyncMock, patch
|
||
from auth.tokens.batch import BatchTokenOperations
|
||
|
||
class TestBatchTokenOperations:
|
||
|
||
@pytest.fixture
|
||
def batch_operations(self):
|
||
return BatchTokenOperations()
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_batch_validate_tokens(self, batch_operations):
|
||
"""Тест массовой валидации токенов"""
|
||
tokens = ["token1", "token2", "token3"]
|
||
|
||
with patch.object(batch_operations, '_validate_token_batch') as mock_validate:
|
||
mock_validate.return_value = {
|
||
"token1": True,
|
||
"token2": False,
|
||
"token3": True
|
||
}
|
||
|
||
results = await batch_operations.batch_validate_tokens(tokens)
|
||
|
||
assert results["token1"] is True
|
||
assert results["token2"] is False
|
||
assert results["token3"] is True
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_batch_revoke_tokens(self, batch_operations):
|
||
"""Тест массового отзыва токенов"""
|
||
tokens = ["token1", "token2", "token3"]
|
||
|
||
with patch.object(batch_operations, '_revoke_token_batch') as mock_revoke:
|
||
mock_revoke.return_value = 2 # 2 токена отозваны
|
||
|
||
revoked_count = await batch_operations.batch_revoke_tokens(tokens)
|
||
|
||
assert revoked_count == 2
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_cleanup_expired_tokens(self, batch_operations):
|
||
"""Тест очистки истекших токенов"""
|
||
with patch('auth.tokens.batch.redis') as mock_redis:
|
||
# Мокаем поиск истекших токенов
|
||
mock_redis.scan_iter.return_value = [
|
||
"session:123:expired_token1",
|
||
"session:456:expired_token2"
|
||
]
|
||
mock_redis.ttl.return_value = -1 # Истекший токен
|
||
mock_redis.delete = AsyncMock(return_value=1)
|
||
|
||
cleaned_count = await batch_operations.cleanup_expired_tokens()
|
||
|
||
assert cleaned_count >= 0
|
||
```
|
||
|
||
## 🔗 Integration Tests
|
||
|
||
### Redis Integration Tests
|
||
|
||
```python
|
||
import pytest
|
||
import asyncio
|
||
from storage.redis import redis
|
||
from auth.tokens.sessions import SessionTokenManager
|
||
|
||
class TestRedisIntegration:
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_redis_connection(self):
|
||
"""Тест подключения к Redis"""
|
||
result = await redis.ping()
|
||
assert result is True
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_session_lifecycle(self):
|
||
"""Тест полного жизненного цикла сессии"""
|
||
sessions = SessionTokenManager()
|
||
|
||
# Создаем сессию
|
||
token = await sessions.create_session(
|
||
user_id="test_user",
|
||
username="testuser"
|
||
)
|
||
|
||
assert token is not None
|
||
|
||
# Проверяем сессию
|
||
payload = await sessions.verify_session(token)
|
||
assert payload is not None
|
||
assert payload["user_id"] == "test_user"
|
||
|
||
# Получаем сессии пользователя
|
||
user_sessions = await sessions.get_user_sessions("test_user")
|
||
assert len(user_sessions) >= 1
|
||
|
||
# Отзываем сессию
|
||
revoked = await sessions.revoke_session_token(token)
|
||
assert revoked is True
|
||
|
||
# Проверяем, что сессия отозвана
|
||
payload = await sessions.verify_session(token)
|
||
assert payload is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_concurrent_sessions(self):
|
||
"""Тест множественных сессий"""
|
||
sessions = SessionTokenManager()
|
||
|
||
# Создаем несколько сессий одновременно
|
||
tasks = []
|
||
for i in range(5):
|
||
task = sessions.create_session(
|
||
user_id="concurrent_user",
|
||
username=f"user_{i}"
|
||
)
|
||
tasks.append(task)
|
||
|
||
tokens = await asyncio.gather(*tasks)
|
||
|
||
# Проверяем, что все токены созданы
|
||
assert len(tokens) == 5
|
||
assert all(token is not None for token in tokens)
|
||
|
||
# Проверяем, что все сессии валидны
|
||
for token in tokens:
|
||
payload = await sessions.verify_session(token)
|
||
assert payload is not None
|
||
|
||
# Очищаем тестовые данные
|
||
for token in tokens:
|
||
await sessions.revoke_session_token(token)
|
||
```
|
||
|
||
### OAuth Flow Integration Tests
|
||
|
||
```python
|
||
import pytest
|
||
from unittest.mock import AsyncMock, patch
|
||
from auth.oauth import oauth_login_http, oauth_callback_http
|
||
|
||
class TestOAuthIntegration:
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_oauth_state_flow(self):
|
||
"""Тест OAuth state flow"""
|
||
from auth.oauth import store_oauth_state, get_oauth_state
|
||
|
||
# Сохраняем state
|
||
state = "test_state_123"
|
||
redirect_uri = "http://localhost:3000"
|
||
|
||
await store_oauth_state(state, redirect_uri)
|
||
|
||
# Получаем state
|
||
stored_data = await get_oauth_state(state)
|
||
|
||
assert stored_data is not None
|
||
assert stored_data["redirect_uri"] == redirect_uri
|
||
|
||
# Проверяем, что state удален после использования
|
||
stored_data_again = await get_oauth_state(state)
|
||
assert stored_data_again is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_oauth_login_redirect(self):
|
||
"""Тест OAuth login redirect"""
|
||
mock_request = AsyncMock()
|
||
mock_request.query_params = {
|
||
"provider": "google",
|
||
"state": "test_state",
|
||
"redirect_uri": "http://localhost:3000"
|
||
}
|
||
|
||
with patch('auth.oauth.store_oauth_state') as mock_store:
|
||
with patch('auth.oauth.generate_provider_url') as mock_generate:
|
||
mock_generate.return_value = "https://accounts.google.com/oauth/authorize?..."
|
||
|
||
response = await oauth_login_http(mock_request)
|
||
|
||
assert response.status_code == 307 # Redirect
|
||
mock_store.assert_called_once()
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_oauth_callback_success(self):
|
||
"""Тест успешного OAuth callback"""
|
||
mock_request = AsyncMock()
|
||
mock_request.query_params = {
|
||
"code": "auth_code_123",
|
||
"state": "test_state"
|
||
}
|
||
|
||
with patch('auth.oauth.get_oauth_state') as mock_get_state:
|
||
mock_get_state.return_value = {
|
||
"redirect_uri": "http://localhost:3000"
|
||
}
|
||
|
||
with patch('auth.oauth.exchange_code_for_user_data') as mock_exchange:
|
||
mock_exchange.return_value = {
|
||
"id": "123",
|
||
"email": "test@example.com",
|
||
"name": "Test User"
|
||
}
|
||
|
||
with patch('auth.oauth._create_or_update_user') as mock_create_user:
|
||
mock_create_user.return_value = AsyncMock(id=123)
|
||
|
||
response = await oauth_callback_http(mock_request)
|
||
|
||
assert response.status_code == 307 # Redirect
|
||
assert "access_token=" in response.headers["location"]
|
||
```
|
||
|
||
## 🌐 E2E Tests
|
||
|
||
### Login Flow E2E Tests
|
||
|
||
```python
|
||
import pytest
|
||
from httpx import AsyncClient
|
||
from main import app
|
||
|
||
class TestLoginFlowE2E:
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_complete_login_flow(self):
|
||
"""Тест полного flow входа в систему"""
|
||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||
|
||
# 1. Регистрация пользователя
|
||
register_response = await client.post("/auth/register", json={
|
||
"email": "test@example.com",
|
||
"password": "TestPassword123!",
|
||
"name": "Test User"
|
||
})
|
||
|
||
assert register_response.status_code == 200
|
||
|
||
# 2. Вход в систему
|
||
login_response = await client.post("/auth/login", json={
|
||
"email": "test@example.com",
|
||
"password": "TestPassword123!"
|
||
})
|
||
|
||
assert login_response.status_code == 200
|
||
data = login_response.json()
|
||
assert data["success"] is True
|
||
assert "token" in data
|
||
|
||
# Проверяем установку cookie
|
||
cookies = login_response.cookies
|
||
assert "session_token" in cookies
|
||
|
||
# 3. Проверка защищенного endpoint с cookie
|
||
session_response = await client.get("/auth/session", cookies={
|
||
"session_token": cookies["session_token"]
|
||
})
|
||
|
||
assert session_response.status_code == 200
|
||
session_data = session_response.json()
|
||
assert session_data["user"]["email"] == "test@example.com"
|
||
|
||
# 4. Выход из системы
|
||
logout_response = await client.post("/auth/logout", cookies={
|
||
"session_token": cookies["session_token"]
|
||
})
|
||
|
||
assert logout_response.status_code == 200
|
||
|
||
# 5. Проверка, что сессия недоступна после выхода
|
||
invalid_session_response = await client.get("/auth/session", cookies={
|
||
"session_token": cookies["session_token"]
|
||
})
|
||
|
||
assert invalid_session_response.status_code == 401
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_bearer_token_auth(self):
|
||
"""Тест аутентификации через Bearer token"""
|
||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||
|
||
# Вход в систему
|
||
login_response = await client.post("/auth/login", json={
|
||
"email": "test@example.com",
|
||
"password": "TestPassword123!"
|
||
})
|
||
|
||
token = login_response.json()["token"]
|
||
|
||
# Использование Bearer token
|
||
protected_response = await client.get("/auth/session", headers={
|
||
"Authorization": f"Bearer {token}"
|
||
})
|
||
|
||
assert protected_response.status_code == 200
|
||
data = protected_response.json()
|
||
assert data["user"]["email"] == "test@example.com"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_invalid_credentials(self):
|
||
"""Тест входа с неверными данными"""
|
||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||
|
||
response = await client.post("/auth/login", json={
|
||
"email": "test@example.com",
|
||
"password": "WrongPassword"
|
||
})
|
||
|
||
assert response.status_code == 401
|
||
data = response.json()
|
||
assert data["success"] is False
|
||
assert "error" in data
|
||
```
|
||
|
||
### OAuth E2E Tests
|
||
|
||
```python
|
||
import pytest
|
||
from unittest.mock import patch
|
||
from httpx import AsyncClient
|
||
from main import app
|
||
|
||
class TestOAuthFlowE2E:
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_oauth_google_flow(self):
|
||
"""Тест OAuth flow с Google"""
|
||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||
|
||
# 1. Инициация OAuth
|
||
oauth_response = await client.get(
|
||
"/auth/oauth/google",
|
||
params={
|
||
"state": "test_state_123",
|
||
"redirect_uri": "http://localhost:3000"
|
||
},
|
||
follow_redirects=False
|
||
)
|
||
|
||
assert oauth_response.status_code == 307
|
||
assert "accounts.google.com" in oauth_response.headers["location"]
|
||
|
||
# 2. Мокаем OAuth callback
|
||
with patch('auth.oauth.exchange_code_for_user_data') as mock_exchange:
|
||
mock_exchange.return_value = {
|
||
"id": "google_user_123",
|
||
"email": "user@gmail.com",
|
||
"name": "Google User"
|
||
}
|
||
|
||
callback_response = await client.get(
|
||
"/auth/oauth/google/callback",
|
||
params={
|
||
"code": "auth_code_123",
|
||
"state": "test_state_123"
|
||
},
|
||
follow_redirects=False
|
||
)
|
||
|
||
assert callback_response.status_code == 307
|
||
location = callback_response.headers["location"]
|
||
assert "access_token=" in location
|
||
|
||
# Извлекаем токен из redirect URL
|
||
import urllib.parse
|
||
parsed = urllib.parse.urlparse(location)
|
||
query_params = urllib.parse.parse_qs(parsed.query)
|
||
access_token = query_params["access_token"][0]
|
||
|
||
# 3. Проверяем, что токен работает
|
||
session_response = await client.get("/auth/session", headers={
|
||
"Authorization": f"Bearer {access_token}"
|
||
})
|
||
|
||
assert session_response.status_code == 200
|
||
data = session_response.json()
|
||
assert data["user"]["email"] == "user@gmail.com"
|
||
```
|
||
|
||
## 🧰 Test Fixtures
|
||
|
||
### Auth Fixtures
|
||
|
||
```python
|
||
import pytest
|
||
import asyncio
|
||
from auth.tokens.sessions import SessionTokenManager
|
||
from auth.tokens.oauth import OAuthTokenManager
|
||
|
||
@pytest.fixture
|
||
async def session_manager():
|
||
"""Фикстура SessionTokenManager"""
|
||
return SessionTokenManager()
|
||
|
||
@pytest.fixture
|
||
async def oauth_manager():
|
||
"""Фикстура OAuthTokenManager"""
|
||
return OAuthTokenManager()
|
||
|
||
@pytest.fixture
|
||
async def test_user_token(session_manager):
|
||
"""Фикстура для создания тестового токена"""
|
||
token = await session_manager.create_session(
|
||
user_id="test_user_123",
|
||
username="testuser"
|
||
)
|
||
|
||
yield token
|
||
|
||
# Cleanup
|
||
await session_manager.revoke_session_token(token)
|
||
|
||
@pytest.fixture
|
||
async def authenticated_client():
|
||
"""Фикстура для аутентифицированного клиента"""
|
||
from httpx import AsyncClient
|
||
from main import app
|
||
|
||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# Создаем пользователя и получаем токен
|
||
login_response = await client.post("/auth/login", json={
|
||
"email": "test@example.com",
|
||
"password": "TestPassword123!"
|
||
})
|
||
|
||
token = login_response.json()["token"]
|
||
|
||
# Настраиваем клиент с токеном
|
||
client.headers.update({"Authorization": f"Bearer {token}"})
|
||
|
||
yield client
|
||
|
||
@pytest.fixture
|
||
async def oauth_tokens(oauth_manager):
|
||
"""Фикстура для OAuth токенов"""
|
||
await oauth_manager.store_oauth_tokens(
|
||
user_id="test_user_123",
|
||
provider="google",
|
||
access_token="test_access_token",
|
||
refresh_token="test_refresh_token",
|
||
expires_in=3600
|
||
)
|
||
|
||
yield {
|
||
"user_id": "test_user_123",
|
||
"provider": "google",
|
||
"access_token": "test_access_token",
|
||
"refresh_token": "test_refresh_token"
|
||
}
|
||
|
||
# Cleanup
|
||
await oauth_manager.revoke_oauth_tokens("test_user_123", "google")
|
||
```
|
||
|
||
### Redis Fixtures
|
||
|
||
```python
|
||
import pytest
|
||
from storage.redis import redis
|
||
|
||
@pytest.fixture(scope="session")
|
||
async def redis_client():
|
||
"""Фикстура Redis клиента"""
|
||
yield redis
|
||
|
||
# Cleanup после всех тестов
|
||
await redis.flushdb()
|
||
|
||
@pytest.fixture
|
||
async def clean_redis():
|
||
"""Фикстура для очистки Redis перед тестом"""
|
||
# Очищаем тестовые ключи
|
||
test_keys = await redis.keys("test:*")
|
||
if test_keys:
|
||
await redis.delete(*test_keys)
|
||
|
||
yield
|
||
|
||
# Очищаем после теста
|
||
test_keys = await redis.keys("test:*")
|
||
if test_keys:
|
||
await redis.delete(*test_keys)
|
||
```
|
||
|
||
## 📊 Test Configuration
|
||
|
||
### pytest.ini
|
||
|
||
```ini
|
||
[tool:pytest]
|
||
asyncio_mode = auto
|
||
testpaths = tests
|
||
python_files = test_*.py
|
||
python_classes = Test*
|
||
python_functions = test_*
|
||
addopts =
|
||
-v
|
||
--tb=short
|
||
--strict-markers
|
||
--disable-warnings
|
||
--cov=auth
|
||
--cov-report=html
|
||
--cov-report=term-missing
|
||
--cov-fail-under=80
|
||
|
||
markers =
|
||
unit: Unit tests
|
||
integration: Integration tests
|
||
e2e: End-to-end tests
|
||
slow: Slow tests
|
||
redis: Tests requiring Redis
|
||
oauth: OAuth related tests
|
||
```
|
||
|
||
### conftest.py
|
||
|
||
```python
|
||
import pytest
|
||
import asyncio
|
||
from unittest.mock import AsyncMock
|
||
from httpx import AsyncClient
|
||
from main import app
|
||
|
||
# Настройка asyncio для тестов
|
||
@pytest.fixture(scope="session")
|
||
def event_loop():
|
||
"""Создает event loop для всей сессии тестов"""
|
||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||
yield loop
|
||
loop.close()
|
||
|
||
# Мок Redis для unit тестов
|
||
@pytest.fixture
|
||
def mock_redis():
|
||
"""Мок Redis клиента"""
|
||
mock = AsyncMock()
|
||
mock.ping.return_value = True
|
||
mock.get.return_value = None
|
||
mock.set.return_value = True
|
||
mock.delete.return_value = 1
|
||
mock.exists.return_value = False
|
||
mock.ttl.return_value = -1
|
||
mock.hset.return_value = 1
|
||
mock.hgetall.return_value = {}
|
||
mock.sadd.return_value = 1
|
||
mock.smembers.return_value = set()
|
||
mock.srem.return_value = 1
|
||
mock.expire.return_value = True
|
||
mock.setex.return_value = True
|
||
return mock
|
||
|
||
# Test client
|
||
@pytest.fixture
|
||
async def test_client():
|
||
"""Тестовый HTTP клиент"""
|
||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||
yield client
|
||
```
|
||
|
||
## 🚀 Running Tests
|
||
|
||
### Команды запуска
|
||
|
||
```bash
|
||
# Все тесты
|
||
pytest
|
||
|
||
# Unit тесты
|
||
pytest tests/auth/unit/ -m unit
|
||
|
||
# Integration тесты
|
||
pytest tests/auth/integration/ -m integration
|
||
|
||
# E2E тесты
|
||
pytest tests/auth/e2e/ -m e2e
|
||
|
||
# Тесты с покрытием
|
||
pytest --cov=auth --cov-report=html
|
||
|
||
# Параллельный запуск
|
||
pytest -n auto
|
||
|
||
# Только быстрые тесты
|
||
pytest -m "not slow"
|
||
|
||
# Конкретный тест
|
||
pytest tests/auth/unit/test_session_manager.py::TestSessionTokenManager::test_create_session
|
||
```
|
||
|
||
### CI/CD Integration
|
||
|
||
```yaml
|
||
# .github/workflows/tests.yml
|
||
name: Tests
|
||
|
||
on: [push, pull_request]
|
||
|
||
jobs:
|
||
test:
|
||
runs-on: ubuntu-latest
|
||
|
||
services:
|
||
redis:
|
||
image: redis:6.2
|
||
ports:
|
||
- 6379:6379
|
||
options: >-
|
||
--health-cmd "redis-cli ping"
|
||
--health-interval 10s
|
||
--health-timeout 5s
|
||
--health-retries 5
|
||
|
||
steps:
|
||
- uses: actions/checkout@v3
|
||
|
||
- name: Set up Python
|
||
uses: actions/setup-python@v4
|
||
with:
|
||
python-version: '3.12'
|
||
|
||
- name: Install dependencies
|
||
run: |
|
||
pip install -r requirements.dev.txt
|
||
|
||
- name: Run unit tests
|
||
run: |
|
||
pytest tests/auth/unit/ -m unit --cov=auth
|
||
|
||
- name: Run integration tests
|
||
run: |
|
||
pytest tests/auth/integration/ -m integration
|
||
env:
|
||
REDIS_URL: redis://localhost:6379/0
|
||
|
||
- name: Run E2E tests
|
||
run: |
|
||
pytest tests/auth/e2e/ -m e2e
|
||
env:
|
||
REDIS_URL: redis://localhost:6379/0
|
||
JWT_SECRET: test_secret_key_for_ci
|
||
|
||
- name: Upload coverage
|
||
uses: codecov/codecov-action@v3
|
||
```
|
||
|
||
## 📈 Test Metrics
|
||
|
||
### Coverage Goals
|
||
- **Unit Tests**: ≥ 90% coverage
|
||
- **Integration Tests**: ≥ 80% coverage
|
||
- **E2E Tests**: Critical paths covered
|
||
- **Overall**: ≥ 85% coverage
|
||
|
||
### Performance Benchmarks
|
||
- **Unit Tests**: < 100ms per test
|
||
- **Integration Tests**: < 1s per test
|
||
- **E2E Tests**: < 10s per test
|
||
- **Total Test Suite**: < 5 minutes
|
||
|
||
### Quality Metrics
|
||
- **Test Reliability**: ≥ 99% pass rate
|
||
- **Flaky Tests**: < 1% of total tests
|
||
- **Test Maintenance**: Regular updates with code changes
|