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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user