420dced9ff
- 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>
122 lines
5.5 KiB
Python
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
|