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

@@ -86,14 +86,20 @@ async def exchange_code_for_token(code: str, redirect_uri: str) -> dict[str, Any
@router.get("/login")
async def login(request: Request) -> RedirectResponse:
"""Redirect to Keycloak login page.
"""Redirect to Keycloak login page or dev login in development mode.
In development mode redirects to the local dev login page
instead of the external Keycloak server.
Args:
request: HTTP request object.
Returns:
RedirectResponse to Keycloak authorization endpoint.
RedirectResponse to Keycloak or dev login endpoint.
"""
if settings.is_dev:
return RedirectResponse(url="/auth/dev-login")
callback_url = str(request.base_url).rstrip("/") + "/auth/callback"
login_url = get_keycloak_login_url(callback_url)
return RedirectResponse(url=login_url)
@@ -142,16 +148,196 @@ async def callback(request: Request, code: str | None = None) -> Response:
async def logout(request: Request) -> Response:
"""Logout user and clear token cookie.
In development mode redirects directly to home page.
In production redirects to Keycloak logout endpoint.
Args:
request: HTTP request object.
Returns:
RedirectResponse to Keycloak logout with cookie cleared.
RedirectResponse with cookie cleared.
"""
home_url = str(request.base_url).rstrip("/") + "/web/"
logout_url = get_keycloak_logout_url(home_url)
response = RedirectResponse(url=logout_url)
response = RedirectResponse(url=home_url)
response.delete_cookie(key="access_token")
if not settings.is_dev:
logout_url = get_keycloak_logout_url(home_url)
response = RedirectResponse(url=logout_url)
response.delete_cookie(key="access_token")
return response
@router.get("/dev-login")
async def dev_login(request: Request) -> Response:
"""Show dev login page for development mode.
Only available in development mode. Provides a simple form
to select role and log in without a real Keycloak server.
Args:
request: HTTP request object.
Returns:
HTMLResponse with dev login form.
Raises:
HTTPException: If accessed outside development mode.
"""
if not settings.is_dev:
raise HTTPException(status_code=404, detail="Not found")
from fastapi.responses import HTMLResponse
return HTMLResponse(
content="""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dev Login - Blog</title>
<style>
:root {
--bg: #f5f5f5;
--card: #fff;
--text: #333;
--border: #ddd;
--primary: #0366d6;
--primary-text: #fff;
--error: #d73a49;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0d1117;
--card: #161b22;
--text: #c9d1d9;
--border: #30363d;
--primary: #58a6ff;
--primary-text: #0d1117;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 2rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 2rem;
max-width: 400px;
width: 100%;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
h1 { margin: 0 0 0.5rem; font-size: 1.5rem; }
.badge {
display: inline-block;
background: var(--error);
color: #fff;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
font-size: 0.875rem;
}
input, select {
width: 100%;
padding: 0.625rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--text);
font-size: 0.9375rem;
margin-bottom: 1rem;
}
button {
width: 100%;
padding: 0.75rem;
background: var(--primary);
color: var(--primary-text);
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
button:hover { opacity: 0.9; }
.hint {
margin-top: 1.5rem;
font-size: 0.8125rem;
color: var(--text);
opacity: 0.7;
}
</style>
</head>
<body>
<div class="card">
<h1>Development Login</h1>
<span class="badge">DEV ONLY</span>
<form method="POST" action="/auth/dev-login">
<label for="username">Username</label>
<input type="text" id="username" name="username" value="Dev User" required>
<label for="role">Role</label>
<select id="role" name="role">
<option value="user">User</option>
<option value="user2">Test User</option>
<option value="admin">Admin</option>
<option value="guest">Guest (unauthenticated)</option>
</select>
<button type="submit">Sign In</button>
</form>
<p class="hint">This bypasses Keycloak for local development only.</p>
</div>
</body>
</html>"""
)
@router.post("/dev-login")
async def dev_login_submit(request: Request) -> Response:
"""Handle dev login form submission.
Sets a dev-specific cookie that MockKeycloakClient recognizes.
Args:
request: HTTP request object with form data.
Returns:
RedirectResponse to home page with dev token cookie set.
Raises:
HTTPException: If accessed outside development mode.
"""
if not settings.is_dev:
raise HTTPException(status_code=404, detail="Not found")
form = await request.form()
role = str(form.get("role", "user")).strip()
token = f"dev-token-{role}"
response = RedirectResponse(url="/web/", status_code=302)
response.set_cookie(
key="access_token",
value=token,
httponly=True,
secure=False,
samesite="lax",
max_age=86400,
)
return response