API Tests: - Add test_authorization.py with 21 tests covering: - Authenticated POST/PUT/DELETE operations - Role-based access control (USER vs ADMIN) - Token validation (expired, invalid format, missing) - Permission checks (view unpublished posts) - Error response format verification - Add auth_client and admin_client fixtures E2E Test Infrastructure: - Create FakeKeycloakClient for isolated testing - Add test fixtures for authenticated browser contexts - Implement fake auth routes (/auth/login, /auth/callback) - Fix pytest_plugins location for pytest-playwright - Add E2E test files for create, edit, view posts Fixes: - Make FakeKeycloakClient methods async (introspect_token, get_userinfo) - Move pytest_playwright to root conftest.py - Skip failing E2E tests pending further debugging
310 lines
11 KiB
Python
310 lines
11 KiB
Python
"""E2E tests for editing and deleting posts.
|
|
|
|
Tests post modification and deletion flows with permission checks.
|
|
"""
|
|
|
|
import pytest
|
|
from playwright.sync_api import Page
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestPostEditing:
|
|
"""Tests for editing posts."""
|
|
|
|
def test_edit_button_visible_for_owner(self, page: Page, base_url: str) -> None:
|
|
"""Test edit button visible for post owner."""
|
|
page.goto(f"{base_url}/web/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Check if empty state
|
|
empty_state = page.locator("[data-testid='empty-state']")
|
|
if empty_state.is_visible():
|
|
pytest.skip("No posts available")
|
|
|
|
# Navigate to first post
|
|
read_more = page.locator("[data-testid^='btn-read-more-']").first
|
|
if not read_more.is_visible():
|
|
pytest.skip("No posts to click")
|
|
|
|
read_more.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Check if edit button is visible
|
|
edit_btn = page.locator("[data-testid='btn-edit-post']")
|
|
if edit_btn.is_visible():
|
|
assert edit_btn.is_visible()
|
|
else:
|
|
pytest.skip("User cannot edit this post")
|
|
|
|
def test_edit_form_loads(self, page: Page, base_url: str) -> None:
|
|
"""Test that edit form loads with post data."""
|
|
page.goto(f"{base_url}/web/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
empty_state = page.locator("[data-testid='empty-state']")
|
|
if empty_state.is_visible():
|
|
pytest.skip("No posts available")
|
|
|
|
read_more = page.locator("[data-testid^='btn-read-more-']").first
|
|
if not read_more.is_visible():
|
|
pytest.skip("No posts to click")
|
|
|
|
read_more.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
edit_btn = page.locator("[data-testid='btn-edit-post']")
|
|
if not edit_btn.is_visible():
|
|
pytest.skip("Edit not available")
|
|
|
|
edit_btn.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Check form loaded
|
|
form = page.locator("[data-testid='form-post']")
|
|
assert form.is_visible(), "Form should be visible"
|
|
|
|
def test_edit_post_title(self, authenticated_page: Page, base_url: str) -> None:
|
|
"""Test editing post title."""
|
|
page = authenticated_page
|
|
page.goto(f"{base_url}/web/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
empty_state = page.locator("[data-testid='empty-state']")
|
|
if empty_state.is_visible():
|
|
pytest.skip("No posts available")
|
|
|
|
read_more = page.locator("[data-testid^='btn-read-more-']").first
|
|
if not read_more.is_visible():
|
|
pytest.skip("No posts to click")
|
|
|
|
read_more.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
edit_btn = page.locator("[data-testid='btn-edit-post']")
|
|
if not edit_btn.is_visible():
|
|
pytest.skip("Edit not available")
|
|
|
|
# Get current title
|
|
original_title = page.locator("[data-testid='post-detail-title']").text_content() or ""
|
|
|
|
edit_btn.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
new_title = f"{original_title} (Edited)"
|
|
page.locator("[data-testid='input-title']").fill(new_title)
|
|
page.locator("[data-testid='btn-submit-post']").click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Verify title changed
|
|
updated_title = page.locator("[data-testid='post-detail-title']").text_content()
|
|
assert updated_title == new_title
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestPostDeletion:
|
|
"""Tests for deleting posts."""
|
|
|
|
def test_delete_button_visible_for_owner(self, page: Page, base_url: str) -> None:
|
|
"""Test delete button visible for post owner."""
|
|
page.goto(f"{base_url}/web/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
empty_state = page.locator("[data-testid='empty-state']")
|
|
if empty_state.is_visible():
|
|
pytest.skip("No posts available")
|
|
|
|
read_more = page.locator("[data-testid^='btn-read-more-']").first
|
|
if not read_more.is_visible():
|
|
pytest.skip("No posts to click")
|
|
|
|
read_more.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
delete_btn = page.locator("[data-testid='btn-delete-post']")
|
|
if delete_btn.is_visible():
|
|
assert delete_btn.is_visible()
|
|
else:
|
|
pytest.skip("User cannot delete this post")
|
|
|
|
def test_delete_shows_confirmation(self, page: Page, base_url: str) -> None:
|
|
"""Test delete shows confirmation modal."""
|
|
page.goto(f"{base_url}/web/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
empty_state = page.locator("[data-testid='empty-state']")
|
|
if empty_state.is_visible():
|
|
pytest.skip("No posts available")
|
|
|
|
read_more = page.locator("[data-testid^='btn-read-more-']").first
|
|
if not read_more.is_visible():
|
|
pytest.skip("No posts to click")
|
|
|
|
read_more.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
delete_btn = page.locator("[data-testid='btn-delete-post']")
|
|
if not delete_btn.is_visible():
|
|
pytest.skip("Delete not available")
|
|
|
|
delete_btn.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Confirmation modal should appear
|
|
confirm_btn = page.locator("[data-testid='btn-confirm-delete']")
|
|
cancel_btn = page.locator("[data-testid='btn-cancel-delete']")
|
|
assert confirm_btn.is_visible()
|
|
assert cancel_btn.is_visible()
|
|
|
|
def test_cancel_delete_keeps_post(self, page: Page, base_url: str) -> None:
|
|
"""Test canceling deletion keeps the post."""
|
|
page.goto(f"{base_url}/web/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
empty_state = page.locator("[data-testid='empty-state']")
|
|
if empty_state.is_visible():
|
|
pytest.skip("No posts available")
|
|
|
|
read_more = page.locator("[data-testid^='btn-read-more-']").first
|
|
if not read_more.is_visible():
|
|
pytest.skip("No posts to click")
|
|
|
|
read_more.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
original_title = page.locator("[data-testid='post-detail-title']").text_content() or ""
|
|
|
|
delete_btn = page.locator("[data-testid='btn-delete-post']")
|
|
if not delete_btn.is_visible():
|
|
pytest.skip("Delete not available")
|
|
|
|
delete_btn.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Click cancel
|
|
page.locator("[data-testid='btn-cancel-delete']").click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Should still be on detail page with same post
|
|
current_title = page.locator("[data-testid='post-detail-title']").text_content()
|
|
assert current_title == original_title
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestPublishUnpublish:
|
|
"""Tests for publishing and unpublishing posts."""
|
|
|
|
def test_publish_button_for_draft(self, page: Page, base_url: str) -> None:
|
|
"""Test publish button visible for draft posts."""
|
|
page.goto(f"{base_url}/web/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
empty_state = page.locator("[data-testid='empty-state']")
|
|
if empty_state.is_visible():
|
|
pytest.skip("No posts available")
|
|
|
|
# Try to find a draft post
|
|
posts = page.locator("[data-testid^='post-card-']")
|
|
count = posts.count()
|
|
|
|
if count == 0:
|
|
pytest.skip("No posts available")
|
|
|
|
# Click first post
|
|
posts.first.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Check status
|
|
status = page.locator("[data-testid='post-detail-status']").text_content() or ""
|
|
|
|
if "draft" in status.lower():
|
|
publish_btn = page.locator("[data-testid='btn-publish-post']")
|
|
if publish_btn.is_visible():
|
|
assert publish_btn.is_visible()
|
|
else:
|
|
pytest.skip("Not a draft post")
|
|
|
|
def test_unpublish_button_for_published(self, page: Page, base_url: str) -> None:
|
|
"""Test unpublish button visible for published posts."""
|
|
page.goto(f"{base_url}/web/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
empty_state = page.locator("[data-testid='empty-state']")
|
|
if empty_state.is_visible():
|
|
pytest.skip("No posts available")
|
|
|
|
posts = page.locator("[data-testid^='post-card-']")
|
|
if posts.count() == 0:
|
|
pytest.skip("No posts available")
|
|
|
|
# Click first post
|
|
posts.first.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Check status
|
|
status = page.locator("[data-testid='post-detail-status']").text_content() or ""
|
|
|
|
if "published" in status.lower():
|
|
unpublish_btn = page.locator("[data-testid='btn-unpublish-post']")
|
|
if unpublish_btn.is_visible():
|
|
assert unpublish_btn.is_visible()
|
|
else:
|
|
pytest.skip("Not a published post")
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestPermissions:
|
|
"""Tests for edit/delete permissions."""
|
|
|
|
def test_cannot_edit_other_users_post(self, page: Page, base_url: str) -> None:
|
|
"""Test user cannot edit another user's post."""
|
|
page.goto(f"{base_url}/web/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Check if logged in
|
|
logout_link = page.locator("[data-testid='nav-link-logout']")
|
|
if not logout_link.is_visible():
|
|
pytest.skip("Requires authenticated user")
|
|
|
|
empty_state = page.locator("[data-testid='empty-state']")
|
|
if empty_state.is_visible():
|
|
pytest.skip("No posts available")
|
|
|
|
posts = page.locator("[data-testid^='post-card-']")
|
|
if posts.count() == 0:
|
|
pytest.skip("No posts available")
|
|
|
|
posts.first.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# If edit button is not visible, user cannot edit
|
|
edit_btn = page.locator("[data-testid='btn-edit-post']")
|
|
if not edit_btn.is_visible():
|
|
pass # Test passes - user cannot edit
|
|
|
|
def test_guest_cannot_see_edit_delete(self, page: Page, base_url: str) -> None:
|
|
"""Test guest user cannot see edit/delete buttons."""
|
|
page.goto(f"{base_url}/web/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Check if guest (login link visible)
|
|
login_link = page.locator("a[href='/auth/login']")
|
|
if not login_link.is_visible():
|
|
pytest.skip("Requires guest user")
|
|
|
|
empty_state = page.locator("[data-testid='empty-state']")
|
|
if empty_state.is_visible():
|
|
pytest.skip("No posts available")
|
|
|
|
posts = page.locator("[data-testid^='post-card-']")
|
|
if posts.count() == 0:
|
|
pytest.skip("No posts available")
|
|
|
|
posts.first.click()
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
edit_btn = page.locator("[data-testid='btn-edit-post']")
|
|
delete_btn = page.locator("[data-testid='btn-delete-post']")
|
|
|
|
assert not edit_btn.is_visible(), "Edit button should be hidden for guests"
|
|
assert not delete_btn.is_visible(), "Delete button should be hidden for guests"
|