Files
blog.pyaqa.ru/app/main.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

144 lines
3.9 KiB
Python

"""Application entry point with DDD architecture.
This module is the main entry point for the FastAPI application.
Configures DI container, middleware, and routes following DDD principles.
"""
from collections.abc import AsyncGenerator, Awaitable, Callable
from contextlib import asynccontextmanager
import uvicorn
from dishka import make_async_container
from dishka.integrations.fastapi import setup_dishka
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from app.infrastructure import close_db, init_db, register_exception_handlers, settings
from app.infrastructure.di.providers import (
DatabaseProvider,
KeycloakProvider,
RepositoryProvider,
TransactionManagerProvider,
UseCaseProvider,
)
from app.presentation import router
from app.presentation.web import auth_router
from app.presentation.web import router as web_router
from app.presentation.web.error_handlers import register_error_handlers
from app.presentation.web.flash import setup_flash_manager
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
"""Application lifespan manager.
Handles startup and shutdown tasks for the application.
Args:
app: FastAPI application instance.
Yields:
None during application runtime.
"""
await init_db()
yield
await close_db()
def app_factory() -> FastAPI:
"""Create and configure FastAPI application.
Sets up DI container, exception handlers, middleware, and routes.
Returns:
Configured FastAPI application instance.
"""
app = FastAPI(
title=settings.app.name,
debug=settings.app.debug,
lifespan=lifespan,
docs_url="/docs" if settings.is_dev else None,
redoc_url="/redoc" if settings.is_dev else None,
)
container = make_async_container(
DatabaseProvider(),
RepositoryProvider(),
TransactionManagerProvider(),
UseCaseProvider(),
KeycloakProvider(),
)
setup_dishka(container, app)
register_exception_handlers(app)
register_error_handlers(app)
@app.middleware("http")
async def flash_middleware(
request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
"""Middleware to setup flash manager for each request."""
await setup_flash_manager(request)
response = await call_next(request)
if hasattr(request.state, "flash_manager"):
request.state.flash_manager.set_cookie(response)
return response
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(router, prefix="/api")
app.include_router(web_router)
app.include_router(auth_router)
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/", response_class=HTMLResponse)
async def root_redirect() -> HTMLResponse:
"""Redirect root URL to web UI.
Returns:
HTMLResponse with redirect to web interface.
"""
return HTMLResponse(
content='<meta http-equiv="refresh" content="0;url=/web/">', status_code=200
)
@app.get("/health", tags=["health"])
async def health_check() -> dict[str, str]:
"""Health check endpoint.
Returns:
Status information dictionary.
"""
return {
"status": "ok",
"app": settings.app.name,
"env": settings.environment.value,
}
return app
def main() -> None:
"""Run the application.
Starts uvicorn server with application factory.
"""
uvicorn.run(
app_factory,
factory=True,
host=settings.app.host,
port=settings.app.port,
)
if __name__ == "__main__":
main()