- Created SetupRedirectMiddleware to redirect unconfigured apps to /setup - Enhanced /api/auth/setup endpoint to save anime_directory to config - Updated SetupRequest model to accept optional anime_directory parameter - Modified setup.html to send anime_directory in setup API call - Added @pytest.mark.requires_clean_auth marker for tests needing unconfigured state - Modified conftest.py to conditionally setup auth based on test marker - Fixed all test failures (846/846 tests now passing) - Updated instructions.md to mark setup tasks as complete This implementation ensures users are guided through initial setup before accessing the application, while maintaining test isolation and preventing auth state leakage between tests.
247 lines
8.6 KiB
Python
247 lines
8.6 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
|
|
|
|
|
|
@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
|
|
|
|
|
|
@pytest.mark.requires_clean_auth
|
|
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
|
|
await client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
|
|
|
|
# Login with correct password
|
|
response = await 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 = await client.get("/api/auth/status", headers=headers)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["authenticated"] is True
|
|
|
|
async def test_login_with_wrong_password(self, client):
|
|
"""Test login with incorrect password."""
|
|
# Setup master password first
|
|
await client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
|
|
|
|
# Login with wrong password
|
|
response = await client.post(
|
|
"/api/auth/login",
|
|
json={"password": "WrongPassword"}
|
|
)
|
|
assert response.status_code == 401
|
|
data = response.json()
|
|
assert "detail" in data
|
|
|
|
async def test_logout_clears_session(self, client):
|
|
"""Test logout functionality."""
|
|
# Setup and login
|
|
await client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
|
|
login_response = await client.post(
|
|
"/api/auth/login",
|
|
json={"password": "StrongP@ss123"}
|
|
)
|
|
token = login_response.json()["access_token"]
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
# Logout
|
|
response = await client.post("/api/auth/logout", headers=headers)
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "ok"
|
|
|
|
async def test_authenticated_request_without_token_returns_401(self, client):
|
|
"""Test that authenticated endpoints reject requests without tokens."""
|
|
# Setup master password
|
|
await client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"})
|
|
|
|
# Try to access authenticated endpoint without token
|
|
response = await client.get("/api/anime/")
|
|
assert response.status_code == 401
|
|
|
|
async def test_authenticated_request_with_invalid_token_returns_401(
|
|
self, client
|
|
):
|
|
"""Test that authenticated endpoints reject invalid tokens."""
|
|
# Setup master password
|
|
await 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 = await client.get("/api/anime/", headers=headers)
|
|
assert response.status_code == 401
|
|
|
|
async def test_remember_me_extends_token_expiry(self, client):
|
|
"""Test that remember_me flag affects token expiry."""
|
|
# Setup master password
|
|
await client.post(
|
|
"/api/auth/setup", json={"master_password": "StrongP@ss123"}
|
|
)
|
|
|
|
# Login without remember me
|
|
response1 = await client.post(
|
|
"/api/auth/login",
|
|
json={"password": "StrongP@ss123", "remember": False}
|
|
)
|
|
data1 = response1.json()
|
|
|
|
# Login with remember me
|
|
response2 = await 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
|
|
|
|
async def test_setup_fails_if_already_configured(self, client):
|
|
"""Test that setup fails if master password is already set."""
|
|
# Setup once
|
|
await client.post(
|
|
"/api/auth/setup", json={"master_password": "StrongP@ss123"}
|
|
)
|
|
|
|
# Try to setup again
|
|
response = await client.post(
|
|
"/api/auth/setup",
|
|
json={"master_password": "AnotherPassword123!"}
|
|
)
|
|
assert response.status_code == 400
|
|
assert (
|
|
"already configured" in response.json()["detail"].lower()
|
|
)
|
|
|
|
async def test_weak_password_validation_in_setup(self, client):
|
|
"""Test that setup rejects weak passwords."""
|
|
# Try with short password
|
|
response = await client.post(
|
|
"/api/auth/setup",
|
|
json={"master_password": "short"}
|
|
)
|
|
assert response.status_code in [400, 422]
|
|
|
|
# Try with all lowercase
|
|
response = await client.post(
|
|
"/api/auth/setup",
|
|
json={"master_password": "alllowercase"}
|
|
)
|
|
assert response.status_code in [400, 422]
|
|
|
|
# Try without special characters
|
|
response = await client.post(
|
|
"/api/auth/setup",
|
|
json={"master_password": "NoSpecialChars123"}
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
|
|
@pytest.mark.requires_clean_auth
|
|
class TestTokenAuthenticationFlow:
|
|
"""Test JWT token-based authentication workflow."""
|
|
|
|
async def test_full_authentication_workflow(self, client):
|
|
"""Test complete authentication workflow with token management."""
|
|
# 1. Check initial status
|
|
response = await client.get("/api/auth/status")
|
|
assert not response.json()["configured"]
|
|
|
|
# 2. Setup master password
|
|
await client.post(
|
|
"/api/auth/setup", json={"master_password": "StrongP@ss123"}
|
|
)
|
|
|
|
# 3. Login and get token
|
|
response = await client.post(
|
|
"/api/auth/login",
|
|
json={"password": "StrongP@ss123"}
|
|
)
|
|
token = response.json()["access_token"]
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
# 4. Access authenticated endpoint
|
|
response = await client.get("/api/auth/status", headers=headers)
|
|
assert response.json()["authenticated"] is True
|
|
|
|
# 5. Logout
|
|
response = await client.post("/api/auth/logout", headers=headers)
|
|
assert response.json()["status"] == "ok"
|
|
|
|
async def test_token_included_in_all_authenticated_requests(
|
|
self, client
|
|
):
|
|
"""Test that token must be included in authenticated API requests."""
|
|
# Setup and login
|
|
await client.post(
|
|
"/api/auth/setup", json={"master_password": "StrongP@ss123"}
|
|
)
|
|
response = await 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/anime/",
|
|
"/api/queue/status",
|
|
"/api/config",
|
|
]
|
|
|
|
for endpoint in endpoints:
|
|
# Without token - should fail
|
|
response = await client.get(endpoint)
|
|
assert response.status_code == 401, (
|
|
f"Endpoint {endpoint} should require auth"
|
|
)
|
|
|
|
# With token - should work or return expected response
|
|
response = await client.get(endpoint, headers=headers)
|
|
# Some endpoints may return 503 if services not configured
|
|
assert response.status_code in [200, 503], (
|
|
f"Endpoint {endpoint} failed with token"
|
|
)
|