feat: RBAC E2E тесты и фикс admin-прав для редактирования постов
Основные изменения: - Добавлены E2E тесты для проверки ownership (TC-E2E-102/103): * test_admin_can_edit_any_post — admin может редактировать любой пост * test_user_cannot_edit_other_users_post — user не может редактировать чужой пост - Исправлены use cases (UpdatePost, DeletePost, PublishPost) — добавлена проверка роли admin - Обновлены web routes и API routes для передачи роли в use cases - Добавлены unit тесты для admin-сценариев Реструктуризация тестов: - Удалены старые API тесты (tests/api/) — требуют переработки - Удалены старые integration тесты (tests/integration/) - Переработаны E2E тесты: удалены старые, добавлены новые с POM - Добавлена документация тестов: FEATURE_*.md, TEST_MODEL.md, AGENTS.md Инфраструктура: - Добавлен MockKeycloakClient для dev-режима - Добавлены статические файлы: EasyMDE, Highlight.js, стили markdown - Обновлены шаблоны: base.html, post_form.html, post_detail.html - Обновлена DI конфигурация и провайдеры Документация: - tests/FEATURE_RBAC.md — матрица тестов RBAC - tests/FEATURE_POST_LIFECYCLE.md — тесты жизненного цикла поста - tests/FEATURE_DOMAIN_FOUNDATION.md — тесты доменного слоя - tests/FEATURE_INFRASTRUCTURE.md — тесты инфраструктуры - tests/TEST_MODEL.md — глобальная матрица покрытия - app/presentation/web/AGENTS.md — гайд по Web UI - tests/AGENTS.md — гайд по тестированию
This commit is contained in:
@@ -1,145 +0,0 @@
|
||||
"""API test fixtures."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.application.dtos import PostResponseDTO
|
||||
from app.infrastructure.auth.models import TokenInfo
|
||||
from app.main import app_factory
|
||||
|
||||
|
||||
class MockKeycloakClient:
|
||||
def __init__(self, token_info: TokenInfo) -> None:
|
||||
self._token_info = token_info
|
||||
|
||||
async def introspect_token(self, token: str) -> TokenInfo:
|
||||
return self._token_info
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_token_info() -> TokenInfo:
|
||||
return TokenInfo(
|
||||
active=True,
|
||||
user_id="test-user-id",
|
||||
username="testuser",
|
||||
email="test@example.com",
|
||||
roles=["user"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_token_info() -> TokenInfo:
|
||||
return TokenInfo(
|
||||
active=True,
|
||||
user_id="admin-user-id",
|
||||
username="adminuser",
|
||||
email="admin@example.com",
|
||||
roles=["admin", "user"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_keycloak_client(user_token_info: TokenInfo) -> MagicMock:
|
||||
mock_client = AsyncMock()
|
||||
mock_client.introspect_token.return_value = user_token_info
|
||||
return mock_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(mock_keycloak_client: MagicMock) -> AsyncGenerator[AsyncClient]:
|
||||
with patch(
|
||||
"app.presentation.api.deps.KeycloakAuthClient",
|
||||
return_value=mock_keycloak_client,
|
||||
):
|
||||
app = app_factory()
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
yield ac
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def auth_client(user_token_info: TokenInfo) -> AsyncGenerator[AsyncClient]:
|
||||
mock_client = MockKeycloakClient(user_token_info)
|
||||
|
||||
with patch(
|
||||
"app.presentation.api.deps.get_keycloak_client",
|
||||
return_value=mock_client,
|
||||
):
|
||||
app = app_factory()
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://test",
|
||||
headers={"Authorization": "Bearer user_token"},
|
||||
) as ac:
|
||||
yield ac
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def admin_client(admin_token_info: TokenInfo) -> AsyncGenerator[AsyncClient]:
|
||||
mock_client = MockKeycloakClient(admin_token_info)
|
||||
|
||||
with patch(
|
||||
"app.presentation.api.deps.get_keycloak_client",
|
||||
return_value=mock_client,
|
||||
):
|
||||
app = app_factory()
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://test",
|
||||
headers={"Authorization": "Bearer admin_token"},
|
||||
) as ac:
|
||||
yield ac
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_headers() -> dict[str, str]:
|
||||
return {"Authorization": "Bearer test_token"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unauthorized_keycloak_client() -> MagicMock:
|
||||
mock_client = AsyncMock()
|
||||
mock_client.introspect_token.return_value = TokenInfo(
|
||||
active=False,
|
||||
user_id="",
|
||||
username="",
|
||||
email="",
|
||||
roles=[],
|
||||
)
|
||||
return mock_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_post_dto() -> PostResponseDTO:
|
||||
return PostResponseDTO(
|
||||
id=uuid4(),
|
||||
title="Test Post",
|
||||
content="This is test content for the blog post",
|
||||
slug="test-post",
|
||||
author_id="test-user-id",
|
||||
published=True,
|
||||
tags=["python", "testing"],
|
||||
created_at=__import__("datetime").datetime.now(),
|
||||
updated_at=__import__("datetime").datetime.now(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_unpublished_post_dto() -> PostResponseDTO:
|
||||
return PostResponseDTO(
|
||||
id=uuid4(),
|
||||
title="Draft Post",
|
||||
content="This is a draft post",
|
||||
slug="draft-post",
|
||||
author_id="test-user-id",
|
||||
published=False,
|
||||
tags=["draft"],
|
||||
created_at=__import__("datetime").datetime.now(),
|
||||
updated_at=__import__("datetime").datetime.now(),
|
||||
)
|
||||
Reference in New Issue
Block a user