Files
blog.pyaqa.ru/app/infrastructure/di/providers.py
Sergey Vanyushkin 46cc06b596 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 — гайд по тестированию
2026-05-07 19:55:15 +03:00

263 lines
7.2 KiB
Python

"""Dishka providers for dependency injection.
This module defines Dishka providers for all application dependencies.
Providers configure how dependencies are created and scoped.
"""
from collections.abc import AsyncGenerator
from dishka import Provider, Scope, provide
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
from app.application import (
CreatePostUseCase,
DeletePostUseCase,
GetPostUseCase,
ListPostsUseCase,
PublishPostUseCase,
UpdatePostUseCase,
)
from app.application.interfaces import TransactionManager
from app.domain.repositories import PostRepository
from app.infrastructure.auth import KeycloakAuthClient, MockKeycloakClient
from app.infrastructure.config.settings import settings
from app.infrastructure.database.connection import AsyncSessionLocal, engine
from app.infrastructure.repositories.post import SQLAlchemyPostRepository
class DatabaseProvider(Provider):
"""Provider for database-related dependencies.
Provides database engine and session scoped appropriately.
Engine is application-scoped, sessions are request-scoped.
Example:
>>> provider = DatabaseProvider()
"""
@provide(scope=Scope.APP)
def get_engine(self) -> AsyncEngine:
"""Provide SQLAlchemy engine.
Returns:
AsyncEngine instance for database operations.
"""
return engine
@provide(scope=Scope.REQUEST)
async def get_session(self) -> AsyncGenerator[AsyncSession]:
"""Provide database session per request.
Yields:
AsyncSession instance for the request lifetime.
"""
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()
class RepositoryProvider(Provider):
"""Provider for repository implementations.
Provides concrete repository implementations for interfaces.
All repositories are request-scoped.
Example:
>>> provider = RepositoryProvider()
"""
@provide(scope=Scope.REQUEST)
def get_post_repository(self, session: AsyncSession) -> PostRepository:
"""Provide PostRepository implementation.
Args:
session: Database session from DI container.
Returns:
SQLAlchemyPostRepository instance.
"""
return SQLAlchemyPostRepository(session)
class TransactionManagerProvider(Provider):
"""Provider for transaction manager.
Provides transaction manager implementation for use cases.
Scoped per request for transaction isolation.
Example:
>>> provider = TransactionManagerProvider()
"""
@provide(scope=Scope.REQUEST)
def get_transaction_manager(self, session: AsyncSession) -> TransactionManager:
"""Provide TransactionManager implementation.
Args:
session: Database session from DI container.
Returns:
SessionTransactionManager instance.
"""
from app.infrastructure.di.transaction_manager import SessionTransactionManager
return SessionTransactionManager(session)
class UseCaseProvider(Provider):
"""Provider for use cases.
Provides all application use cases with their dependencies.
All use cases are request-scoped for transaction isolation.
Example:
>>> provider = UseCaseProvider()
"""
@provide(scope=Scope.REQUEST)
def get_create_post_use_case(
self,
post_repo: PostRepository,
tx_manager: TransactionManager,
) -> CreatePostUseCase:
"""Provide CreatePostUseCase.
Args:
post_repo: Post repository dependency.
tx_manager: Transaction manager dependency.
Returns:
Configured CreatePostUseCase instance.
"""
return CreatePostUseCase(
post_repo=post_repo,
tx_manager=tx_manager,
)
@provide(scope=Scope.REQUEST)
def get_get_post_use_case(
self,
post_repo: PostRepository,
tx_manager: TransactionManager,
) -> GetPostUseCase:
"""Provide GetPostUseCase.
Args:
post_repo: Post repository dependency.
tx_manager: Transaction manager dependency.
Returns:
Configured GetPostUseCase instance.
"""
return GetPostUseCase(
post_repo=post_repo,
tx_manager=tx_manager,
)
@provide(scope=Scope.REQUEST)
def get_update_post_use_case(
self,
post_repo: PostRepository,
tx_manager: TransactionManager,
) -> UpdatePostUseCase:
"""Provide UpdatePostUseCase.
Args:
post_repo: Post repository dependency.
tx_manager: Transaction manager dependency.
Returns:
Configured UpdatePostUseCase instance.
"""
return UpdatePostUseCase(
post_repo=post_repo,
tx_manager=tx_manager,
)
@provide(scope=Scope.REQUEST)
def get_delete_post_use_case(
self,
post_repo: PostRepository,
tx_manager: TransactionManager,
) -> DeletePostUseCase:
"""Provide DeletePostUseCase.
Args:
post_repo: Post repository dependency.
tx_manager: Transaction manager dependency.
Returns:
Configured DeletePostUseCase instance.
"""
return DeletePostUseCase(
post_repo=post_repo,
tx_manager=tx_manager,
)
@provide(scope=Scope.REQUEST)
def get_list_posts_use_case(
self,
post_repo: PostRepository,
tx_manager: TransactionManager,
) -> ListPostsUseCase:
"""Provide ListPostsUseCase.
Args:
post_repo: Post repository dependency.
tx_manager: Transaction manager dependency.
Returns:
Configured ListPostsUseCase instance.
"""
return ListPostsUseCase(
post_repo=post_repo,
tx_manager=tx_manager,
)
@provide(scope=Scope.REQUEST)
def get_publish_post_use_case(
self,
post_repo: PostRepository,
tx_manager: TransactionManager,
) -> PublishPostUseCase:
"""Provide PublishPostUseCase.
Args:
post_repo: Post repository dependency.
tx_manager: Transaction manager dependency.
Returns:
Configured PublishPostUseCase instance.
"""
return PublishPostUseCase(
post_repo=post_repo,
tx_manager=tx_manager,
)
class KeycloakProvider(Provider):
"""Provider for Keycloak authentication client.
Provides Keycloak client as application-scoped singleton.
In development mode uses MockKeycloakClient for local testing.
Example:
>>> provider = KeycloakProvider()
"""
@provide(scope=Scope.APP)
def get_keycloak_client(self) -> KeycloakAuthClient:
"""Provide KeycloakAuthClient or MockKeycloakClient singleton.
Returns MockKeycloakClient in dev mode for local testing
without a real Keycloak server.
Returns:
KeycloakAuthClient instance.
"""
if settings.is_dev:
return MockKeycloakClient() # type: ignore[return-value]
return KeycloakAuthClient(settings)