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:
2026-05-07 19:55:15 +03:00
parent 41f2a3d98e
commit 46cc06b596
58 changed files with 4234 additions and 4014 deletions

View File

@@ -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(),
)