multi-user auth (CLI-created accounts), per-user history, test suite

- server/users.py replaces credentials.py: users.json (0600), PBKDF2,
  one-time migration of the legacy single password to user admin
  (history.json -> history/admin.json)
- accounts are CLI-managed only: python -m server.users add|rm|list
  (no self-signup on an internet-exposed app); /signup route removed,
  login page back to username + password
- per-user 30-day signed session cookie (payload carries the
  username), per-user lockout, GET /api/me, POST /account/delete
- per-user watch history: server/data/history/<user>.json; ANIWEB_DATA
  env override for the data dir
- header chip: logged-in user, logout, delete account
- tests/: throwaway-server suite (port 8399, temp data dir) — 9 HTTP
  tests + 2 Playwright browser tests (login UI, real video playback
  behind auth); runs via python tests/run.py
This commit is contained in:
jozamudi committed 2026-08-19 16:17:56 +00:00
1 parent 76c45a8f29
commit c697500c7d
12 files changed
+595 -122

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
"""Browser tests for ani-web (Playwright, headless Chromium).
Run via tests/run.py or standalone: python tests/test_browser.py <base_url>
Covers what HTTP tests can't: the login UI, the user chip, and real
video playback behind auth. Needs: pip install playwright && playwright install chromium
"""
import asyncio
import sys
from playwright.async_api import async_playwright
BASE = "http://127.0.0.1:8399"
LAUNCH_ARGS = ["--autoplay-policy=no-user-gesture-required"]
async def _login(page, user, pw):
await page.goto(BASE + "/")
await page.wait_for_selector("input#pw")
assert "create account" not in await page.content(), "self-signup UI must be absent"
await page.fill("#user", user)
await page.fill("#pw", pw)
await page.click("#go")
await page.wait_for_selector("#q", timeout=15000)
await page.wait_for_function(
f"document.getElementById('me').textContent.includes('{user}')", timeout=5000)
async def test_login_ui():
async with async_playwright() as p:
browser = await p.chromium.launch(args=LAUNCH_ARGS)
page = await (await browser.new_context()).new_page()
errors = []
page.on("pageerror", lambda e: errors.append(str(e)))
# unauth -> login form
await page.goto(BASE + "/")
await page.wait_for_selector("input#pw")
assert "/login" in page.url, page.url
# wrong password -> inline error
await page.fill("#user", "admin")
await page.fill("#pw", "wrong")
await page.click("#go")
await page.wait_for_function("document.querySelector('#err').textContent.length > 0",
timeout=10000)
assert "wrong" in await page.inner_text("#err")
# correct -> home with user chip
await page.fill("#pw", "adminpw12345")
await page.click("#go")
await _login_settled(page)
assert await page.inner_text("#me") == "hi, admin"
assert "Recently played" in await page.inner_text("#main"), "seeded history must show"
assert not errors, errors
await browser.close()
print("ok test_login_ui")
async def _login_settled(page):
await page.wait_for_selector("#q", timeout=15000)
await page.wait_for_function(
"document.getElementById('me').textContent.length > 0", timeout=5000)
async def test_video_plays_behind_auth():
async with async_playwright() as p:
browser = await p.chromium.launch(args=LAUNCH_ARGS)
page = await (await browser.new_context()).new_page()
errors = []
page.on("pageerror", lambda e: errors.append(str(e)))
page.on("console", lambda m: errors.append(m.text) if m.type == "error" else None)
await _login(page, "alice", "alicepw12345")
await page.fill("#q", "chainsaw")
await page.keyboard.press("Enter")
await page.wait_for_selector(".card", timeout=30000)
await page.click(".card")
await page.wait_for_selector("video", timeout=30000)
await page.wait_for_function(
"document.querySelector('video').readyState >= 4 && document.querySelector('video').currentTime > 1",
timeout=60000)
real_errors = [e for e in errors if "401" not in e]
assert not real_errors, real_errors
await browser.close()
print("ok test_video_plays_behind_auth")
if __name__ == "__main__":
if len(sys.argv) > 1:
BASE = sys.argv[1]
asyncio.run(test_login_ui())
asyncio.run(test_video_plays_behind_auth())