Aniworld/tests/integration/test_frontend_auth_integration.py
Lukas 0957a6e183 feat: Complete frontend-backend integration with JWT authentication
Implemented full JWT-based authentication integration between frontend and backend:

Frontend Changes:
- Updated login.html to store JWT tokens in localStorage after successful login
- Updated setup.html to use correct API payload format (master_password)
- Modified app.js and queue.js to include Bearer tokens in all authenticated requests
- Updated makeAuthenticatedRequest() to add Authorization header with JWT token
- Enhanced checkAuthentication() to verify token and redirect on 401 responses
- Updated logout() to clear tokens from localStorage

API Endpoint Updates:
- Mapped queue API endpoints to new backend structure
- /api/queue/clear → /api/queue/completed (DELETE) for clearing completed
- /api/queue/remove → /api/queue/{item_id} (DELETE) for single removal
- /api/queue/retry payload changed to {item_ids: []} array format
- /api/download/pause|resume|cancel → /api/queue/pause|resume|stop

Testing:
- Created test_frontend_integration_smoke.py with JWT token validation tests
- Verified login returns access_token, token_type, and expires_at
- Tested Bearer token authentication on protected endpoints
- Smoke tests passing for authentication flow

Documentation:
- Updated infrastructure.md with JWT authentication implementation details
- Documented token storage, API endpoint changes, and response formats
- Marked Frontend Integration task as completed in instructions.md
- Added frontend integration testing section

WebSocket:
- Verified WebSocket integration with new backend (already functional)
- Dual event handlers support both old and new message types
- Room-based subscriptions working correctly

This completes Task 7: Frontend Integration from the development instructions.
2025-10-17 19:27:52 +02:00

239 lines
8.5 KiB
Python

"""
Tests for frontend authentication integration.
These smoke tests verify that the key authentication and API endpoints
work correctly with JWT tokens as expected by the frontend.
"""
import pytest
from httpx import ASGITransport, AsyncClient
from src.server.fastapi_app import app
from src.server.services.auth_service import auth_service
@pytest.fixture(autouse=True)
def reset_auth():
"""Reset authentication state before each test."""
# Reset auth service state
original_hash = auth_service._hash
auth_service._hash = None
auth_service._failed.clear()
yield
# Restore
auth_service._hash = original_hash
auth_service._failed.clear()
@pytest.fixture
async def client():
"""Create an async test client."""
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
class TestFrontendAuthIntegration:
"""Test authentication integration matching frontend expectations."""
async def test_setup_returns_ok_status(self, client):
"""Test setup endpoint returns expected format for frontend."""
response = await client.post(
"/api/auth/setup",
json={"master_password": "StrongP@ss123"}
)
assert response.status_code == 201
data = response.json()
# Frontend expects 'status': 'ok'
assert data["status"] == "ok"
async def test_login_returns_access_token(self, client):
"""Test login flow and verify JWT token is returned."""
# Setup master password first
client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
# Login with correct password
response = client.post(
"/api/auth/login",
json={"password": "StrongP@ss123"}
)
assert response.status_code == 200
data = response.json()
# Verify token is returned
assert "access_token" in data
assert data["token_type"] == "bearer"
assert "expires_at" in data
# Verify token can be used for authenticated requests
token = data["access_token"]
headers = {"Authorization": f"Bearer {token}"}
response = client.get("/api/auth/status", headers=headers)
assert response.status_code == 200
data = response.json()
assert data["authenticated"] is True
def test_login_with_wrong_password(self, client):
"""Test login with incorrect password."""
# Setup master password first
client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
# Login with wrong password
response = client.post(
"/api/auth/login",
json={"password": "WrongPassword"}
)
assert response.status_code == 401
data = response.json()
assert "detail" in data
def test_logout_clears_session(self, client):
"""Test logout functionality."""
# Setup and login
client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
login_response = client.post(
"/api/auth/login",
json={"password": "StrongP@ss123"}
)
token = login_response.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
# Logout
response = client.post("/api/auth/logout", headers=headers)
assert response.status_code == 200
assert response.json()["status"] == "ok"
def test_authenticated_request_without_token_returns_401(self, client):
"""Test that authenticated endpoints reject requests without tokens."""
# Setup master password
client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
# Try to access authenticated endpoint without token
response = client.get("/api/v1/anime")
assert response.status_code == 401
def test_authenticated_request_with_invalid_token_returns_401(self, client):
"""Test that authenticated endpoints reject invalid tokens."""
# Setup master password
client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
# Try to access authenticated endpoint with invalid token
headers = {"Authorization": "Bearer invalid_token_here"}
response = client.get("/api/v1/anime", headers=headers)
assert response.status_code == 401
def test_remember_me_extends_token_expiry(self, client):
"""Test that remember_me flag affects token expiry."""
# Setup master password
client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
# Login without remember me
response1 = client.post(
"/api/auth/login",
json={"password": "StrongP@ss123", "remember": False}
)
data1 = response1.json()
# Login with remember me
response2 = client.post(
"/api/auth/login",
json={"password": "StrongP@ss123", "remember": True}
)
data2 = response2.json()
# Both should return tokens with expiry
assert "expires_at" in data1
assert "expires_at" in data2
def test_setup_fails_if_already_configured(self, client):
"""Test that setup fails if master password is already set."""
# Setup once
client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
# Try to setup again
response = client.post(
"/api/auth/setup",
json={"master_password": "AnotherPassword123!"}
)
assert response.status_code == 400
assert "already configured" in response.json()["detail"].lower()
def test_weak_password_validation_in_setup(self, client):
"""Test that setup rejects weak passwords."""
# Try with short password
response = client.post(
"/api/auth/setup",
json={"master_password": "short"}
)
assert response.status_code == 400
# Try with all lowercase
response = client.post(
"/api/auth/setup",
json={"master_password": "alllowercase"}
)
assert response.status_code == 400
# Try without special characters
response = client.post(
"/api/auth/setup",
json={"master_password": "NoSpecialChars123"}
)
assert response.status_code == 400
class TestTokenAuthenticationFlow:
"""Test JWT token-based authentication workflow."""
def test_full_authentication_workflow(self, client):
"""Test complete authentication workflow with token management."""
# 1. Check initial status
response = client.get("/api/auth/status")
assert not response.json()["configured"]
# 2. Setup master password
client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
# 3. Login and get token
response = client.post(
"/api/auth/login",
json={"password": "StrongP@ss123"}
)
token = response.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
# 4. Access authenticated endpoint
response = client.get("/api/auth/status", headers=headers)
assert response.json()["authenticated"] is True
# 5. Logout
response = client.post("/api/auth/logout", headers=headers)
assert response.json()["status"] == "ok"
def test_token_included_in_all_authenticated_requests(self, client):
"""Test that token must be included in authenticated API requests."""
# Setup and login
client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
response = client.post(
"/api/auth/login",
json={"password": "StrongP@ss123"}
)
token = response.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
# Test various authenticated endpoints
endpoints = [
"/api/v1/anime",
"/api/queue/status",
"/api/config",
]
for endpoint in endpoints:
# Without token - should fail
response = client.get(endpoint)
assert response.status_code == 401, f"Endpoint {endpoint} should require auth"
# With token - should work or return expected response
response = client.get(endpoint, headers=headers)
# Some endpoints may return 503 if services not configured, that's ok
assert response.status_code in [200, 503], f"Endpoint {endpoint} failed with token"