Files
pic/tests/e2e/ui/test_admin_login.py
T
roof 420dced9ff fix: WireGuard peer sync, privileged mode, E2E and integration test correctness
- api/app.py: sync WireGuard server config on peer add/remove (non-fatal)
- docker-compose.yml: add privileged:true to wireguard service
- E2E tests: fix logout selector, DNS IP lookup, wg config DNS line, VIP skip guards,
  badge text selectors, heading .first, async logout wait
- Integration tests: fix 4 tests that sent unauthenticated requests expecting 400
  (now use authenticated session helpers); accept 401 as valid in webui proxy test;
  add password field to service_access validation test
- Remove stale tracked config templates (config/api/api/*, config/api/cell.env, etc.)
  that no longer exist on disk after config layout was reorganised

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 06:04:40 -04:00

122 lines
5.5 KiB
Python

"""
Admin login / session tests.
Scenarios covered:
1. Correct credentials → redirected away from /login (dashboard renders)
2. Wrong password → error text "Invalid username or password." stays on /login
3. Lockout (5 consecutive bad attempts) → API returns 423; skipped for UI
(covered in API unit tests; creating a throwaway user risks collateral damage)
4. Logout → redirected back to /login
5. Session persistence: page reload while logged in → stays on dashboard
"""
import pytest
pytestmark = pytest.mark.ui
# ── 1. Successful login ──────────────────────────────────────────────────────
def test_login_success_redirects_to_dashboard(page, webui_base, admin_user, admin_password):
"""Valid credentials navigate away from /login."""
page.goto(f"{webui_base}/login")
page.wait_for_load_state('networkidle')
page.fill('input[autocomplete="username"]', admin_user)
page.fill('input[autocomplete="current-password"]', admin_password)
page.click('button[type="submit"]')
page.wait_for_url(lambda url: '/login' not in url, timeout=10000)
assert '/login' not in page.url
def test_login_success_shows_dashboard_heading(page, webui_base, admin_user, admin_password):
"""After login the page title/heading contains 'Dashboard' or 'Personal Internet Cell'."""
page.goto(f"{webui_base}/login")
page.fill('input[autocomplete="username"]', admin_user)
page.fill('input[autocomplete="current-password"]', admin_password)
page.click('button[type="submit"]')
page.wait_for_url(lambda url: '/login' not in url, timeout=10000)
page.wait_for_load_state('networkidle')
# The sidebar renders the app title twice (mobile + desktop); use first.
assert (
page.locator('h1:has-text("Personal Internet Cell")').first.is_visible()
or page.locator('h1:has-text("Dashboard")').first.is_visible()
)
# ── 2. Wrong password ────────────────────────────────────────────────────────
def test_login_wrong_password_shows_error(page, webui_base, admin_user):
"""Wrong password keeps user on /login and shows an error message."""
page.goto(f"{webui_base}/login")
page.wait_for_load_state('networkidle')
page.fill('input[autocomplete="username"]', admin_user)
page.fill('input[autocomplete="current-password"]', 'WrongPassword999!')
page.click('button[type="submit"]')
# Login.jsx renders the error in a <p> with class text-red-400
page.wait_for_selector('text=Invalid username or password.', timeout=5000)
assert '/login' in page.url
def test_login_wrong_password_error_text_exact(page, webui_base, admin_user):
"""The exact error message from Login.jsx is shown (not a generic network error)."""
page.goto(f"{webui_base}/login")
page.fill('input[autocomplete="username"]', admin_user)
page.fill('input[autocomplete="current-password"]', 'BadPass0000!')
page.click('button[type="submit"]')
error_el = page.wait_for_selector('p.text-red-400', timeout=5000)
assert 'Invalid username' in error_el.inner_text()
# ── 3. Lockout (deferred to API layer) ──────────────────────────────────────
def test_login_lockout_deferred():
"""
Lockout behavior (HTTP 423 → 'Account locked' banner) is covered by the
API-layer unit tests (test_auth_routes.py). Creating a throwaway account
purely to lock it in the browser risks side-effects; skip here.
"""
pytest.skip("Lockout UI scenario deferred — covered in test_auth_routes.py")
# ── 4. Logout ────────────────────────────────────────────────────────────────
def test_logout_redirects_to_login(admin_page, webui_base):
"""Clicking 'Sign out' in the sidebar redirects to /login."""
page = admin_page
from helpers.playwright_login import do_logout
do_logout(page, webui_base)
assert '/login' in page.url
def test_logout_clears_session(admin_page, webui_base):
"""After logout, navigating to '/' redirects back to /login (no lingering session)."""
page = admin_page
from helpers.playwright_login import do_logout
do_logout(page, webui_base)
page.goto(f"{webui_base}/")
# React auth check is async — wait for the redirect to /login
try:
page.wait_for_url(lambda url: '/login' in url, timeout=8000)
except Exception:
pass
assert '/login' in page.url
# ── 5. Session persistence ───────────────────────────────────────────────────
def test_session_persists_after_page_reload(admin_page, webui_base):
"""Reloading the page while logged in should keep the user authenticated."""
page = admin_page
page.reload()
page.wait_for_load_state('networkidle')
assert '/login' not in page.url
def test_session_persists_after_navigating_back(admin_page, webui_base):
"""Browser back-navigation from an inner page should not trigger a re-login."""
page = admin_page
page.goto(f"{webui_base}/settings")
page.wait_for_load_state('networkidle')
page.go_back()
page.wait_for_load_state('networkidle')
assert '/login' not in page.url