Files
openrouter-watcher/tests/unit/routers/test_web.py
Luca Sacchi Ricciardi a605b7f29e feat(frontend): T47-T54 implement web interface routes
- Add web router with all frontend pages
- Login/Register pages with form validation
- Dashboard with stats cards and Chart.js
- API Keys management with CRUD operations
- Stats page with filtering and pagination
- API Tokens management with generation/revocation
- User profile with password change and account deletion
- Add shared templates_config.py to avoid circular imports
- Add CSRF protection middleware
- Add get_current_user_optional dependency for web routes

All routes verified working:
- GET /login, POST /login
- GET /register, POST /register
- POST /logout
- GET /dashboard
- GET /keys, POST /keys, DELETE /keys/{id}
- GET /stats
- GET /tokens, POST /tokens, DELETE /tokens/{id}
- GET /profile, POST /profile/password, DELETE /profile
2026-04-07 18:15:26 +02:00

233 lines
8.9 KiB
Python

"""Tests for Web Router (T47-T54).
TDD: RED → GREEN → REFACTOR
"""
import pytest
from fastapi.testclient import TestClient
class TestLoginPage:
"""Test login page routes (T47)."""
def test_login_page_get(self, client):
"""Test GET /login returns login page."""
response = client.get("/login")
assert response.status_code == 200
assert "text/html" in response.headers.get("content-type", "")
assert "Login" in response.text or "login" in response.text.lower()
def test_login_page_redirects_when_authenticated(self, authorized_client):
"""Test GET /login redirects to dashboard when already logged in."""
response = authorized_client.get("/login", follow_redirects=False)
assert response.status_code == 302
assert "/dashboard" in response.headers.get("location", "")
def test_login_post_valid_credentials(self, client, db_session):
"""Test POST /login with valid credentials."""
# Create a test user first
from openrouter_monitor.models import User
from openrouter_monitor.services.password import hash_password
user = User(
email="testlogin@example.com",
hashed_password=hash_password("TestPassword123!")
)
db_session.add(user)
db_session.commit()
# Attempt login
response = client.post(
"/login",
data={
"email": "testlogin@example.com",
"password": "TestPassword123!"
},
follow_redirects=False
)
assert response.status_code == 302
assert "access_token" in response.cookies
assert "/dashboard" in response.headers.get("location", "")
def test_login_post_invalid_credentials(self, client):
"""Test POST /login with invalid credentials shows error."""
response = client.post(
"/login",
data={
"email": "nonexistent@example.com",
"password": "WrongPassword123!"
}
)
assert response.status_code == 401
assert "Invalid" in response.text or "error" in response.text.lower()
class TestRegisterPage:
"""Test registration page routes (T48)."""
def test_register_page_get(self, client):
"""Test GET /register returns registration page."""
response = client.get("/register")
assert response.status_code == 200
assert "text/html" in response.headers.get("content-type", "")
assert "Register" in response.text or "register" in response.text.lower()
def test_register_post_valid_data(self, client, db_session):
"""Test POST /register with valid data creates user."""
from openrouter_monitor.models import User
response = client.post(
"/register",
data={
"email": "newuser@example.com",
"password": "NewPassword123!",
"password_confirm": "NewPassword123!"
},
follow_redirects=False
)
assert response.status_code == 302
assert "/login" in response.headers.get("location", "")
# Verify user was created
user = db_session.query(User).filter(User.email == "newuser@example.com").first()
assert user is not None
def test_register_post_passwords_mismatch(self, client):
"""Test POST /register with mismatched passwords shows error."""
response = client.post(
"/register",
data={
"email": "test@example.com",
"password": "Password123!",
"password_confirm": "DifferentPassword123!"
}
)
assert response.status_code == 400
assert "match" in response.text.lower() or "error" in response.text.lower()
def test_register_post_duplicate_email(self, client, db_session):
"""Test POST /register with existing email shows error."""
from openrouter_monitor.models import User
from openrouter_monitor.services.password import hash_password
# Create existing user
existing = User(
email="existing@example.com",
hashed_password=hash_password("Password123!")
)
db_session.add(existing)
db_session.commit()
response = client.post(
"/register",
data={
"email": "existing@example.com",
"password": "Password123!",
"password_confirm": "Password123!"
}
)
assert response.status_code == 400
assert "already" in response.text.lower() or "registered" in response.text.lower()
class TestLogout:
"""Test logout route (T49)."""
def test_logout_clears_cookie(self, authorized_client):
"""Test POST /logout clears access token cookie."""
response = authorized_client.post("/logout", follow_redirects=False)
assert response.status_code == 302
assert "/login" in response.headers.get("location", "")
# Cookie should be deleted
assert response.cookies.get("access_token") == ""
class TestDashboard:
"""Test dashboard route (T50)."""
def test_dashboard_requires_auth(self, client):
"""Test GET /dashboard redirects to login when not authenticated."""
response = client.get("/dashboard", follow_redirects=False)
assert response.status_code == 302
assert "/login" in response.headers.get("location", "")
def test_dashboard_renders_for_authenticated_user(self, authorized_client):
"""Test GET /dashboard renders for authenticated user."""
response = authorized_client.get("/dashboard")
assert response.status_code == 200
assert "text/html" in response.headers.get("content-type", "")
assert "dashboard" in response.text.lower() or "Dashboard" in response.text
class TestApiKeys:
"""Test API keys management routes (T51)."""
def test_keys_page_requires_auth(self, client):
"""Test GET /keys redirects to login when not authenticated."""
response = client.get("/keys", follow_redirects=False)
assert response.status_code == 302
assert "/login" in response.headers.get("location", "")
def test_keys_page_renders_for_authenticated_user(self, authorized_client):
"""Test GET /keys renders for authenticated user."""
response = authorized_client.get("/keys")
assert response.status_code == 200
assert "text/html" in response.headers.get("content-type", "")
assert "key" in response.text.lower() or "API" in response.text
class TestStats:
"""Test stats page routes (T52)."""
def test_stats_page_requires_auth(self, client):
"""Test GET /stats redirects to login when not authenticated."""
response = client.get("/stats", follow_redirects=False)
assert response.status_code == 302
assert "/login" in response.headers.get("location", "")
def test_stats_page_renders_for_authenticated_user(self, authorized_client):
"""Test GET /stats renders for authenticated user."""
response = authorized_client.get("/stats")
assert response.status_code == 200
assert "text/html" in response.headers.get("content-type", "")
assert "stat" in response.text.lower() or "Stats" in response.text
class TestTokens:
"""Test API tokens management routes (T53)."""
def test_tokens_page_requires_auth(self, client):
"""Test GET /tokens redirects to login when not authenticated."""
response = client.get("/tokens", follow_redirects=False)
assert response.status_code == 302
assert "/login" in response.headers.get("location", "")
def test_tokens_page_renders_for_authenticated_user(self, authorized_client):
"""Test GET /tokens renders for authenticated user."""
response = authorized_client.get("/tokens")
assert response.status_code == 200
assert "text/html" in response.headers.get("content-type", "")
assert "token" in response.text.lower() or "Token" in response.text
class TestProfile:
"""Test profile page routes (T54)."""
def test_profile_page_requires_auth(self, client):
"""Test GET /profile redirects to login when not authenticated."""
response = client.get("/profile", follow_redirects=False)
assert response.status_code == 302
assert "/login" in response.headers.get("location", "")
def test_profile_page_renders_for_authenticated_user(self, authorized_client):
"""Test GET /profile renders for authenticated user."""
response = authorized_client.get("/profile")
assert response.status_code == 200
assert "text/html" in response.headers.get("content-type", "")
assert "profile" in response.text.lower() or "Profile" in response.text