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:
@@ -1,476 +1,299 @@
|
||||
"""E2E test fixtures with isolated test server.
|
||||
"""E2E test configuration for blog application.
|
||||
|
||||
Provides fixtures for running E2E tests with:
|
||||
- Isolated SQLite database per test session
|
||||
- In-memory fake Keycloak (no external server needed)
|
||||
- Test server on random port
|
||||
- Automatic test user creation and authentication
|
||||
Provides DevAuthProvider for cookie-based dev authentication
|
||||
and role-specific browser context fixtures.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import os
|
||||
import socket
|
||||
import tempfile
|
||||
from typing import Any
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from dishka import Provider, Scope, make_async_container, provide
|
||||
from dishka.integrations.fastapi import setup_dishka
|
||||
from fastapi import FastAPI
|
||||
from playwright.sync_api import Browser, BrowserContext
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
||||
from pytfm.auth import AuthProvider, TestUser
|
||||
|
||||
from app.infrastructure.auth import KeycloakAuthClient
|
||||
from app.infrastructure.config.settings import Settings
|
||||
from tests.e2e.fake_keycloak import FakeKeycloakClient
|
||||
if TYPE_CHECKING:
|
||||
from playwright.sync_api import Browser, BrowserContext, Page
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Disable pytest-asyncio for E2E tests.
|
||||
class DevAuthProvider(AuthProvider):
|
||||
"""Authentication provider for blog dev mode.
|
||||
|
||||
pytest-playwright manages its own event loop and conflicts
|
||||
with pytest-asyncio. We disable asyncio_mode for E2E tests.
|
||||
Bypasses real Keycloak by generating dev-specific tokens
|
||||
recognized by MockKeycloakClient.
|
||||
|
||||
Attributes:
|
||||
_users: Mapping of usernames to test users.
|
||||
"""
|
||||
if hasattr(config, "option") and hasattr(config.option, "asyncio_mode"):
|
||||
config.option.asyncio_mode = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the dev auth provider."""
|
||||
self._users: dict[str, TestUser] = {}
|
||||
|
||||
def _get_free_port() -> int:
|
||||
"""Get a free TCP port from the OS."""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
return sock.getsockname()[1]
|
||||
def create_user(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
email: str,
|
||||
roles: list[str] | None = None,
|
||||
) -> TestUser:
|
||||
"""Create a test user mapped to a dev token role.
|
||||
|
||||
Args:
|
||||
username: Login name (used as display name).
|
||||
password: Password (ignored in dev mode).
|
||||
email: Email address.
|
||||
roles: List of roles. First role determines dev token.
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def browser_type_launch_args() -> dict:
|
||||
"""Return launch args for browser - ensure headless mode."""
|
||||
return {"headless": True}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_db_path() -> str:
|
||||
"""Create temporary database file for test session."""
|
||||
fd, path = tempfile.mkstemp(suffix=".db", prefix="blog_e2e_")
|
||||
os.close(fd)
|
||||
yield path
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_database_url(test_db_path: str) -> str:
|
||||
"""Build database URL for test database."""
|
||||
return f"sqlite+aiosqlite:///{test_db_path}"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_settings(test_database_url: str) -> Settings:
|
||||
"""Create test settings with isolated database."""
|
||||
return Settings(
|
||||
environment="dev",
|
||||
app={"name": "Blog E2E Test", "debug": True, "host": "127.0.0.1", "port": 0},
|
||||
db={"url": test_database_url, "echo": False},
|
||||
kc={"server_url": "http://fake-keycloak:8080", "realm": "test", "client_id": "test"},
|
||||
security={"secret_key": "test-secret-key-not-for-production"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_engine(test_database_url: str):
|
||||
"""Create database engine for test session."""
|
||||
import asyncio
|
||||
|
||||
engine = create_async_engine(
|
||||
test_database_url,
|
||||
echo=False,
|
||||
future=True,
|
||||
)
|
||||
yield engine
|
||||
|
||||
# Cleanup
|
||||
asyncio.run(engine.dispose())
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def fake_keycloak():
|
||||
"""Create fake Keycloak client for testing."""
|
||||
client = FakeKeycloakClient(token_ttl=3600)
|
||||
yield client
|
||||
client.clear()
|
||||
|
||||
|
||||
class FakeKeycloakProvider(Provider):
|
||||
"""Provider that supplies fake Keycloak client."""
|
||||
|
||||
def __init__(self, fake_client: FakeKeycloakClient) -> None:
|
||||
"""Initialize with fake client."""
|
||||
self._fake_client = fake_client
|
||||
super().__init__()
|
||||
|
||||
@provide(scope=Scope.APP)
|
||||
def get_keycloak_client(self) -> KeycloakAuthClient:
|
||||
"""Provide fake Keycloak client."""
|
||||
return self._fake_client
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_server(
|
||||
test_settings: Settings,
|
||||
test_engine: AsyncEngine,
|
||||
fake_keycloak: FakeKeycloakClient,
|
||||
):
|
||||
"""Start test server on random port using threading for sync compatibility."""
|
||||
import threading
|
||||
import time
|
||||
|
||||
from app.infrastructure.database.models import Base
|
||||
from app.presentation import 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
|
||||
|
||||
port = _get_free_port()
|
||||
base_url = f"http://127.0.0.1:{port}"
|
||||
print(f"\n[TestServer] Starting server on port {port}")
|
||||
|
||||
# Initialize database using asyncio.run
|
||||
async def init_db():
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
try:
|
||||
asyncio.run(init_db())
|
||||
print("[TestServer] Database initialized")
|
||||
except Exception as e:
|
||||
print(f"[TestServer] Database init failed: {e}")
|
||||
raise
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from fastapi import Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.infrastructure.di.providers import (
|
||||
DatabaseProvider,
|
||||
RepositoryProvider,
|
||||
TransactionManagerProvider,
|
||||
UseCaseProvider,
|
||||
)
|
||||
|
||||
app = FastAPI(
|
||||
title=test_settings.app.name,
|
||||
debug=test_settings.app.debug,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
)
|
||||
|
||||
container = make_async_container(
|
||||
DatabaseProvider(),
|
||||
RepositoryProvider(),
|
||||
TransactionManagerProvider(),
|
||||
UseCaseProvider(),
|
||||
FakeKeycloakProvider(fake_keycloak),
|
||||
)
|
||||
setup_dishka(container, app)
|
||||
|
||||
from app.infrastructure import register_exception_handlers
|
||||
|
||||
register_exception_handlers(app)
|
||||
register_error_handlers(app)
|
||||
|
||||
@app.middleware("http")
|
||||
async def flash_middleware(
|
||||
request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
||||
) -> Response:
|
||||
await setup_flash_manager(request)
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.get("/")
|
||||
async def root_redirect() -> Response:
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
return HTMLResponse(
|
||||
content='<meta http-equiv="refresh" content="0;url=/web/">', status_code=200
|
||||
Returns:
|
||||
Created TestUser instance.
|
||||
"""
|
||||
role = (roles or ["user"])[0]
|
||||
user = TestUser(
|
||||
id=f"dev-{role}",
|
||||
username=username,
|
||||
email=email,
|
||||
password=password,
|
||||
roles=roles or ["user"],
|
||||
)
|
||||
self._users[username] = user
|
||||
return user
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check() -> dict[str, str]:
|
||||
return {"status": "ok", "env": "e2e-test"}
|
||||
def login(self, username: str, password: str) -> str:
|
||||
"""Return dev token for the user's role.
|
||||
|
||||
# Include main routers
|
||||
app.include_router(router, prefix="/api")
|
||||
app.include_router(web_router)
|
||||
Args:
|
||||
username: User login name.
|
||||
password: User password (ignored).
|
||||
|
||||
# Add fake auth routes instead of including auth_router
|
||||
async def fake_login(request: Request, redirect: str = "/web/") -> Response:
|
||||
from fastapi.responses import HTMLResponse
|
||||
Returns:
|
||||
Dev authentication token string.
|
||||
|
||||
html = f'''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Test Login</title></head>
|
||||
<body>
|
||||
<h1>Test Login Page</h1>
|
||||
<form method="post" action="/auth/callback">
|
||||
<input type="hidden" name="redirect" value="{redirect}">
|
||||
<select name="username">
|
||||
<option value="testuser">Test User</option>
|
||||
<option value="admin">Admin User</option>
|
||||
</select>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
return HTMLResponse(content=html)
|
||||
Raises:
|
||||
ValueError: If user does not exist.
|
||||
"""
|
||||
user = self._users.get(username)
|
||||
if not user:
|
||||
raise ValueError("User not found")
|
||||
role = user.roles[0] if user.roles else "user"
|
||||
return f"dev-token-{role}"
|
||||
|
||||
@app.post("/auth/callback")
|
||||
async def fake_callback(request: Request) -> Response:
|
||||
from fastapi.responses import RedirectResponse
|
||||
def build_auth_cookie(self, token: str, domain: str) -> dict[str, Any]:
|
||||
"""Build access_token cookie for blog dev auth.
|
||||
|
||||
form = await request.form()
|
||||
username = form.get("username", "testuser")
|
||||
redirect = form.get("redirect", "/web/")
|
||||
Args:
|
||||
token: Dev authentication token.
|
||||
domain: Cookie domain.
|
||||
|
||||
try:
|
||||
user = fake_keycloak.create_user(
|
||||
username=username,
|
||||
password="test",
|
||||
email=f"{username}@test.com",
|
||||
roles=["admin" if username == "admin" else "user"],
|
||||
)
|
||||
except ValueError:
|
||||
_ = fake_keycloak._users.get(username)
|
||||
|
||||
token = fake_keycloak.login(username, "test")
|
||||
|
||||
response = RedirectResponse(url=redirect, status_code=302)
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=False,
|
||||
samesite="lax",
|
||||
max_age=3600,
|
||||
)
|
||||
return response
|
||||
|
||||
@app.get("/auth/logout")
|
||||
async def fake_logout(request: Request) -> Response:
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
response = RedirectResponse(url="/web/", status_code=302)
|
||||
response.delete_cookie(key="access_token")
|
||||
return response
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
import uvicorn
|
||||
from uvicorn import Config
|
||||
|
||||
config = Config(app=app, host="127.0.0.1", port=port, log_level="warning")
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
# Run server in a separate thread with error handling
|
||||
server_exception = None
|
||||
|
||||
def run_server():
|
||||
nonlocal server_exception
|
||||
try:
|
||||
asyncio.run(server.serve())
|
||||
except Exception as e:
|
||||
server_exception = e
|
||||
print(f"[TestServer] Server error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
server_thread = threading.Thread(target=run_server, daemon=True)
|
||||
server_thread.start()
|
||||
print("[TestServer] Server thread started")
|
||||
|
||||
# Wait for server to be ready using sync httpx
|
||||
server_started = False
|
||||
last_error = None
|
||||
for attempt in range(50):
|
||||
try:
|
||||
response = httpx.get(f"{base_url}/health", timeout=0.5)
|
||||
if response.status_code == 200:
|
||||
server_started = True
|
||||
print(f"[TestServer] Server ready after {attempt + 1} attempts")
|
||||
break
|
||||
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
||||
last_error = e
|
||||
time.sleep(0.1)
|
||||
|
||||
if not server_started:
|
||||
print(f"[TestServer] Server failed to start after 50 attempts. Last error: {last_error}")
|
||||
server.should_exit = True
|
||||
raise RuntimeError(f"Test server failed to start after 50 attempts on port {port}")
|
||||
|
||||
# Test that web routes work before yielding
|
||||
try:
|
||||
test_response = httpx.get(f"{base_url}/web/", timeout=2.0, follow_redirects=True)
|
||||
print(f"[TestServer] Test /web/: {test_response.status_code}")
|
||||
except Exception as e:
|
||||
print(f"[TestServer] Test /web/ failed: {e}")
|
||||
|
||||
# Test auth redirect
|
||||
try:
|
||||
test_response = httpx.get(f"{base_url}/web/posts/new", timeout=2.0, follow_redirects=True)
|
||||
print(f"[TestServer] Test /web/posts/new: {test_response.status_code}")
|
||||
print(f"[TestServer] Final URL: {test_response.url}")
|
||||
except Exception as e:
|
||||
print(f"[TestServer] Test /web/posts/new failed: {e}")
|
||||
|
||||
print(f"[TestServer] Yielding server info: {base_url}")
|
||||
yield {"base_url": base_url, "port": port, "fake_keycloak": fake_keycloak}
|
||||
|
||||
# Cleanup
|
||||
print("[TestServer] Shutting down server...")
|
||||
server.should_exit = True
|
||||
server_thread.join(timeout=5.0)
|
||||
print("[TestServer] Server shutdown complete")
|
||||
Returns:
|
||||
Cookie dict compatible with Playwright.
|
||||
"""
|
||||
return {
|
||||
"name": "access_token",
|
||||
"value": token,
|
||||
"domain": domain,
|
||||
"path": "/",
|
||||
"httpOnly": True,
|
||||
"secure": False,
|
||||
"sameSite": "Lax",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def base_url(test_server: dict[str, Any]) -> str:
|
||||
"""Get base URL of running test server."""
|
||||
return test_server["base_url"]
|
||||
def base_url() -> str:
|
||||
"""Return the base URL for the blog application.
|
||||
|
||||
Returns:
|
||||
Application base URL.
|
||||
"""
|
||||
return "http://127.0.0.1:8000"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def keycloak_client(test_server: dict[str, Any]) -> FakeKeycloakClient:
|
||||
"""Get fake Keycloak client from test server."""
|
||||
return test_server["fake_keycloak"]
|
||||
def pytfm_auth_provider() -> DevAuthProvider:
|
||||
"""Return DevAuthProvider for blog dev mode.
|
||||
|
||||
Returns:
|
||||
DevAuthProvider instance.
|
||||
"""
|
||||
return DevAuthProvider()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_user_data() -> dict[str, str]:
|
||||
"""Generate test user data."""
|
||||
import uuid
|
||||
|
||||
unique_id = uuid.uuid4().hex[:8]
|
||||
return {
|
||||
"username": f"testuser_{unique_id}",
|
||||
"email": f"test_{unique_id}@example.com",
|
||||
"password": "TestPass123!",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_context(
|
||||
def _create_authenticated_context(
|
||||
browser: Browser,
|
||||
keycloak_client: FakeKeycloakClient,
|
||||
test_user_data: dict[str, str],
|
||||
base_url: str,
|
||||
pytfm_auth_provider: DevAuthProvider,
|
||||
role: str,
|
||||
) -> BrowserContext:
|
||||
"""Create authenticated browser context with logged-in user."""
|
||||
user = keycloak_client.create_user(
|
||||
username=test_user_data["username"],
|
||||
password=test_user_data["password"],
|
||||
email=test_user_data["email"],
|
||||
roles=["user"],
|
||||
)
|
||||
"""Create a browser context authenticated with a dev token role.
|
||||
|
||||
token = keycloak_client.login(user.username, user.password)
|
||||
Args:
|
||||
browser: Playwright Browser instance.
|
||||
base_url: Application base URL.
|
||||
pytfm_auth_provider: Dev auth provider.
|
||||
role: Dev role (user, user2, admin, guest).
|
||||
|
||||
Returns:
|
||||
Authenticated BrowserContext.
|
||||
"""
|
||||
user = pytfm_auth_provider.create_user(
|
||||
username=f"e2e_{role}",
|
||||
password="pass",
|
||||
email=f"{role}@example.com",
|
||||
roles=[role],
|
||||
)
|
||||
token = pytfm_auth_provider.login(user.username, user.password)
|
||||
|
||||
context = browser.new_context(
|
||||
viewport={"width": 1280, "height": 720},
|
||||
)
|
||||
|
||||
cookie_domain = base_url.replace("http://", "").replace("https://", "").split(":")[0]
|
||||
cookie = pytfm_auth_provider.build_auth_cookie(token, cookie_domain)
|
||||
context.add_cookies([cookie])
|
||||
|
||||
context.add_cookies(
|
||||
[
|
||||
{
|
||||
"name": "access_token",
|
||||
"value": token,
|
||||
"domain": cookie_domain,
|
||||
"path": "/",
|
||||
"httpOnly": True,
|
||||
"secure": False,
|
||||
}
|
||||
]
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_context(
|
||||
browser: Browser,
|
||||
base_url: str,
|
||||
pytfm_auth_provider: DevAuthProvider,
|
||||
) -> Generator[BrowserContext, None, None]:
|
||||
"""Create a browser context authenticated as a regular user.
|
||||
|
||||
Args:
|
||||
browser: Playwright Browser instance.
|
||||
base_url: Application base URL.
|
||||
pytfm_auth_provider: Dev auth provider.
|
||||
|
||||
Yields:
|
||||
Authenticated BrowserContext for user role.
|
||||
"""
|
||||
context = _create_authenticated_context(browser, base_url, pytfm_auth_provider, "user")
|
||||
yield context
|
||||
|
||||
context.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_page(authenticated_context: BrowserContext):
|
||||
"""Create authenticated page for testing."""
|
||||
page = authenticated_context.new_page()
|
||||
def user_page(user_context: BrowserContext) -> Generator[Page, None, None]:
|
||||
"""Create a page authenticated as a regular user.
|
||||
|
||||
Args:
|
||||
user_context: Authenticated browser context.
|
||||
|
||||
Yields:
|
||||
Authenticated Playwright Page.
|
||||
"""
|
||||
page = user_context.new_page()
|
||||
yield page
|
||||
page.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_user(keycloak_client: FakeKeycloakClient) -> dict[str, str]:
|
||||
"""Create admin user and return credentials with token."""
|
||||
import uuid
|
||||
def admin_context(
|
||||
browser: Browser,
|
||||
base_url: str,
|
||||
pytfm_auth_provider: DevAuthProvider,
|
||||
) -> Generator[BrowserContext, None, None]:
|
||||
"""Create a browser context authenticated as admin.
|
||||
|
||||
unique_id = uuid.uuid4().hex[:8]
|
||||
username = f"admin_{unique_id}"
|
||||
password = "AdminPass123!"
|
||||
Args:
|
||||
browser: Playwright Browser instance.
|
||||
base_url: Application base URL.
|
||||
pytfm_auth_provider: Dev auth provider.
|
||||
|
||||
user = keycloak_client.create_user(
|
||||
username=username,
|
||||
password=password,
|
||||
email=f"admin_{unique_id}@example.com",
|
||||
roles=["user", "admin"],
|
||||
)
|
||||
|
||||
token = keycloak_client.login(username, password)
|
||||
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"email": user.email,
|
||||
"token": token,
|
||||
"roles": user.roles,
|
||||
}
|
||||
Yields:
|
||||
Authenticated BrowserContext for admin role.
|
||||
"""
|
||||
context = _create_authenticated_context(browser, base_url, pytfm_auth_provider, "admin")
|
||||
yield context
|
||||
context.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def regular_user(keycloak_client: FakeKeycloakClient) -> dict[str, str]:
|
||||
"""Create regular user and return credentials with token."""
|
||||
import uuid
|
||||
def admin_page(admin_context: BrowserContext) -> Generator[Page, None, None]:
|
||||
"""Create a page authenticated as admin.
|
||||
|
||||
unique_id = uuid.uuid4().hex[:8]
|
||||
username = f"user_{unique_id}"
|
||||
password = "UserPass123!"
|
||||
Args:
|
||||
admin_context: Authenticated browser context.
|
||||
|
||||
user = keycloak_client.create_user(
|
||||
username=username,
|
||||
password=password,
|
||||
email=f"user_{unique_id}@example.com",
|
||||
roles=["user"],
|
||||
Yields:
|
||||
Authenticated Playwright Page.
|
||||
"""
|
||||
page = admin_context.new_page()
|
||||
yield page
|
||||
page.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user2_context(
|
||||
browser: Browser,
|
||||
base_url: str,
|
||||
pytfm_auth_provider: DevAuthProvider,
|
||||
) -> Generator[BrowserContext, None, None]:
|
||||
"""Create a browser context authenticated as a second regular user.
|
||||
|
||||
Args:
|
||||
browser: Playwright Browser instance.
|
||||
base_url: Application base URL.
|
||||
pytfm_auth_provider: Dev auth provider.
|
||||
|
||||
Yields:
|
||||
Authenticated BrowserContext for user2 role.
|
||||
"""
|
||||
context = _create_authenticated_context(browser, base_url, pytfm_auth_provider, "user2")
|
||||
yield context
|
||||
context.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user2_page(user2_context: BrowserContext) -> Generator[Page, None, None]:
|
||||
"""Create a page authenticated as a second regular user.
|
||||
|
||||
Args:
|
||||
user2_context: Authenticated browser context.
|
||||
|
||||
Yields:
|
||||
Authenticated Playwright Page.
|
||||
"""
|
||||
page = user2_context.new_page()
|
||||
yield page
|
||||
page.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def guest_context(
|
||||
browser: Browser,
|
||||
base_url: str,
|
||||
) -> Generator[BrowserContext, None, None]:
|
||||
"""Create an unauthenticated browser context.
|
||||
|
||||
Args:
|
||||
browser: Playwright Browser instance.
|
||||
base_url: Application base URL.
|
||||
|
||||
Yields:
|
||||
Unauthenticated BrowserContext.
|
||||
"""
|
||||
context = browser.new_context(
|
||||
viewport={"width": 1280, "height": 720},
|
||||
)
|
||||
yield context
|
||||
context.close()
|
||||
|
||||
token = keycloak_client.login(username, password)
|
||||
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"email": user.email,
|
||||
"token": token,
|
||||
"roles": user.roles,
|
||||
}
|
||||
@pytest.fixture
|
||||
def guest_page(guest_context: BrowserContext) -> Generator[Page, None, None]:
|
||||
"""Create an unauthenticated page.
|
||||
|
||||
Args:
|
||||
guest_context: Unauthenticated browser context.
|
||||
|
||||
Yields:
|
||||
Unauthenticated Playwright Page.
|
||||
"""
|
||||
page = guest_context.new_page()
|
||||
yield page
|
||||
page.close()
|
||||
|
||||
Reference in New Issue
Block a user