- 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
233 lines
8.9 KiB
Python
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
|