Files
blog.pyaqa.ru/AGENTS.md
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

14 KiB

Blog AGENTS.md

Generated: 2026-05-03 22:15 UTC
Commit: 41f2a3d
Branch: feature/tests

Stack

  • Python 3.13+, FastAPI, pydantic, uvicorn
  • SQLAlchemy 2.0 (async), aiosqlite
  • Package manager: uv
  • CI: Woodpecker (lint, test, type on push/PR to dev)

Commands

uv sync --group dev         # Install all dev dependencies
uv run pytest               # Run tests (coverage >= 70% required)
uv run pytest tests/unit/   # Run single test directory
uv run ruff check . --fix   # Lint
uv run ruff format          # Format
uv run isort .              # Sort imports
uv run mypy .               # Type check (strict mode)
uv run blog                 # Start dev server (port 8000)

Pre-commit order

ruff check --fixruff formatisortmypy

DDD Architecture

Layer Structure

app/
├── domain/                   # Domain Layer - business logic, no dependencies
│   ├── entities/            # Domain entities (Post, User, etc.)
│   │   ├── base.py          # Base entity class
│   │   └── post.py          # Post entity with business logic
│   ├── value_objects/       # Value objects (Title, Content, Slug)
│   │   ├── base.py
│   │   ├── title.py
│   │   ├── content.py
│   │   └── slug.py
│   ├── repositories/        # Repository interfaces (abstract)
│   │   ├── base.py
│   │   └── post.py
│   └── exceptions.py        # Domain exceptions
│
├── application/             # Application Layer - use cases
│   ├── dtos/               # Data Transfer Objects
│   │   └── post.py
│   ├── interfaces/         # Abstract interfaces (UoW)
│   │   └── unit_of_work.py
│   └── use_cases/          # Use cases (CQRS-like)
│       ├── create_post.py
│       ├── get_post.py
│       ├── update_post.py
│       ├── delete_post.py
│       ├── list_posts.py
│       └── publish_post.py
│
├── infrastructure/          # Infrastructure Layer - external concerns
│   ├── config/             # Configuration
│   │   └── settings.py
│   ├── database/           # Database connection & ORM models
│   │   ├── connection.py
│   │   └── models.py
│   ├── repositories/       # Repository implementations
│   │   ├── post.py         # SQLAlchemyPostRepository
│   │   └── unit_of_work.py # SQLAlchemyUnitOfWork
│   ├── di/                 # Dependency Injection
│   │   └── container.py
│   └── middleware/         # Exception handlers
│       └── error_handler.py
│
├── presentation/            # Presentation Layer - API
│   ├── api/                # FastAPI routes
│   │   ├── v1/            # API version 1
│   │   │   ├── __init__.py
│   │   │   └── posts.py   # Posts endpoints
│   │   ├── deps.py        # FastAPI dependencies
│   │   └── __init__.py
│   └── schemas/           # Pydantic schemas
│       └── post.py
│
└── main.py                # Application entry point

tests/
├── unit/                  # Unit tests (domain, use cases)
│   ├── domain/           # Domain layer tests
│   ├── application/      # Application layer tests
│   └── infrastructure/   # Infrastructure tests
├── integration/          # Integration tests (DB, repos)
├── api/                  # API endpoint tests
└── e2e/                  # End-to-end tests

Where to Look

Task Location Notes
Add a new use case app/application/use_cases/ Follow naming: {action}_post.py
Add a new API endpoint app/presentation/api/v1/posts.py Or create new module in v1/
Add a new web page app/presentation/web/routes.py Integrate real use cases, not mocks
Add a domain entity app/domain/entities/ Inherit from BaseEntity, add to domain/__init__.py
Add a repository method app/infrastructure/repositories/post.py Mirror in app/domain/repositories/post.py
Configure DI provider app/infrastructure/di/providers.py Add to existing provider class or create new one
Change database schema app/infrastructure/database/models.py Mirror changes in domain entity
Add/modify tests tests/unit/{layer}/ Mirror app/ structure exactly
Run linting uv run ruff check . --fix Pre-commit: ruff → ruff format → isort → mypy
Run tests uv run pytest Coverage auto-collected, HTML report at htmlcov/
Run type check uv run mypy . Strict mode; excludes tests/e2e

Code Map

Symbol Type Location Refs Role
app_factory Function app/main.py:50 3 FastAPI app factory with DI lifespan
SQLAlchemyPostRepository Class app/infrastructure/repositories/post.py:18 1 Concrete repository implementation
Post Class app/domain/entities/post.py:17 1 Core domain entity
PostRepository Class app/domain/repositories/post.py:13 1 Repository interface
CreatePostUseCase Class app/application/use_cases/create_post.py:14 1 Use case for creating posts
home Function app/presentation/web/routes.py:189 1 Web home page route
create_post Function app/presentation/api/v1/posts.py:35 1 API create post endpoint

Key Conventions

Dependency Rule

  • Domain layer has NO dependencies on other layers
  • Application layer depends only on Domain
  • Infrastructure depends on Domain and Application
  • Presentation depends on all other layers

Testing

  • Unit tests: Test domain logic without DB/external services

Code Patterns

  • Use dataclasses for entities and value objects
  • Use frozen dataclasses for value objects (immutable)
  • Use Unit of Work pattern for transactions
  • Use Repository pattern for data access
  • Use Dependency Injection via FastAPI's Depends()

Anti-Patterns (This Project)

  • NO inline comments — Self-documenting code only; Google-style docstrings required
  • NO type suppression — Never use typing.Any casts or # type: ignore to bypass mypy strict mode
  • Dead code: create_container() in app/infrastructure/di/container.py is defined but never used; main.py calls make_async_container() directly
  • Empty directories: app/domain/exceptions/ and app/presentation/api/deps/ are empty dirs that co-exist with .py files of the same name — import ambiguity risk
  • Missing __main__.py: python -m app fails; use uv run blog or python app/main.py
  • Stale config: pyproject.toml excludes tests/e2e but the directory does not exist
  • Unused dependency: black is in [dependency-groups] lints but never invoked; ruff format is used instead
  • Pre-commit excludes __init__.py: All __init__.py files skip linting and import sorting

AI Code Generation Requirements

Documentation Standards

  • Write self-documenting code - use clear, descriptive variable names and function names
  • NO inline comments - code should be readable without explanatory comments
  • Google-style docstrings are REQUIRED for all modules, classes, and functions

Docstring Requirements

Modules

Every module must have a module-level docstring:

"""Module for managing blog posts.

This module provides use cases for creating, updating, and deleting
blog posts in the application layer.
"""

Classes

Every class must have a detailed docstring:

class CreatePostUseCase:
    """Use case for creating a new blog post.

    This class encapsulates the business logic for creating posts,
    including validation and slug generation.

    Attributes:
        uow: Unit of Work for database transactions.
        slug_service: Service for generating URL-friendly slugs.

    Example:
        >>> use_case = CreatePostUseCase(uow, slug_service)
        >>> result = await use_case.execute(dto)
    """

Functions/Methods

Every function must have a detailed docstring with Args, Returns, Raises:

async def execute(self, dto: CreatePostDTO) -> PostDTO:
    """Execute the use case to create a new post.

    Args:
        dto: Data transfer object containing post creation data
            including title, content, and author information.

    Returns:
        PostDTO: The created post data transfer object with
            generated ID and slug.

    Raises:
        TitleValidationError: If the title is empty or too long.
        ContentValidationError: If the content is empty.
        DuplicateSlugError: If a post with the same slug exists.

    Note:
        This method is idempotent - calling it multiple times with
        the same data will create separate posts with unique slugs.
    """

Google-Style Docstring Format

Use the following sections as appropriate:

  • Args - Parameter descriptions with types
  • Returns - Return value description with type
  • Raises - Exceptions that may be raised
  • Yields - For generator functions
  • Example - Usage examples
  • Note - Additional important information
  • Warning - Critical warnings
  • Attributes - For class attributes
  • See Also - References to related code

UI Development Requirements

HTML Templates (Jinja2)

  • All HTML templates use Jinja2 templating engine
  • Templates are located in app/presentation/templates/
  • Base template: base.html with theme support (light/dark)

data-testid Attributes (REQUIRED)

Every interactive and significant HTML element MUST have a data-testid attribute for automated testing.

Required Elements:

  • Navigation: data-testid="nav-link-{name}", data-testid="nav-logo"
  • Buttons: data-testid="btn-{action}" (e.g., btn-create, btn-save, btn-delete)
  • Forms: data-testid="form-{name}", data-testid="input-{field}", data-testid="submit-{action}"
  • Cards/Posts: data-testid="post-card-{id}", data-testid="post-title", data-testid="post-content"
  • Lists: data-testid="list-{name}", data-testid="list-item-{index}"
  • Theme Switcher: data-testid="theme-toggle", data-testid="theme-{light|dark}"
  • Messages/Alerts: data-testid="alert-{type}", `data-testid="alert-message"

Example:

<button data-testid="btn-create-post" class="btn btn-primary">
    Create Post
</button>

<article data-testid="post-card-{{ post.id }}" class="card">
    <h2 data-testid="post-title">{{ post.title }}</h2>
    <p data-testid="post-content">{{ post.content }}</p>
</article>

CSS Architecture (Gitea-inspired)

  • Theme files: static/css/themes/theme-{light|dark}.css with CSS variables
  • Base styles: static/css/base.css - reset, typography, CSS variables usage
  • Components: static/css/components.css - buttons, cards, forms, inputs
  • Layout: static/css/layout.css - grid, navigation, containers

Theme Support

  • Light and dark themes based on Gitea color scheme
  • Theme switching via data-theme attribute on <html> element
  • LocalStorage persistence for user preference
  • All colors use CSS custom properties (variables)

Static Assets

  • All assets are local - no external CDN dependencies
  • Location: static/ directory at project root
  • Served via FastAPI StaticFiles middleware

Authentication & Authorization

Web UI Authentication

  • Token storage: HTTP-only secure cookies
  • Login flow: Redirect to Keycloak login page → Callback → Set cookie → Redirect back
  • Registration: Only through Keycloak admin interface
  • Profile: Read-only display of user info

Authorization Rules

Post Visibility

Role Published Posts Own Drafts Other Drafts
GUEST (unauthenticated)
USER
ADMIN

UI Elements by Role

Element GUEST USER ADMIN
"New Post" button
"Edit" button on own posts
"Edit" button on other posts
"Delete" button on own posts
"Delete" button on other posts
Draft badges Own only All
User menu in header
Profile page access

Auth Routes

  • GET /auth/login - Redirect to Keycloak
  • GET /auth/callback - OAuth callback handler
  • GET /auth/logout - Clear cookie and logout
  • GET /profile - User profile page (read-only)
response.set_cookie(
    key="access_token",
    value=token,
    httponly=True,
    secure=True,  # In production
    samesite="lax",
    max_age=3600,  # 1 hour
)

DDD Concepts Used

Entities

  • Have identity (UUID)
  • Mutable state
  • Business logic methods (publish, update_title, etc.)
  • Example: Post entity

Value Objects

  • Immutable
  • Defined by attributes
  • Validated on creation
  • Examples: Title, Content, Slug

Aggregates & Repositories

  • Post is an aggregate root
  • PostRepository interface in Domain
  • SQLAlchemyPostRepository implementation in Infrastructure

Domain Events

  • Placeholder for future implementation
  • Can be added via event bus in application layer

Configuration

  • .env file loaded by pydantic-settings
  • Settings available via app.infrastructure.config.settings

Database

  • SQLAlchemy 2.0 with async support
  • SQLite by default (aiosqlite)
  • Tables auto-created on startup
  • Use init_db() and close_db() in lifespan

Notes

  • Web routes (app/presentation/web/routes.py) currently use MockPost and MOCK_POSTS instead of real use cases — integrate with actual use cases when ready
  • alembic/ directory exists but is non-functional (no alembic.ini, no migration scripts)
  • tests/integration/, tests/api/, tests/e2e/ are documented in architecture but do not exist yet
  • app/domain/roles.py exists but its symbols are not exported in app/domain/__init__.py
  • Woodpecker CI uses .woodpecker/ directory (3 separate YAML files) instead of single .woodpecker.yml — valid but non-standard
  • CI pipelines have copy-paste boilerplate; test.yaml uses --group tests while lint.yaml and type.yaml use --only-group <X>