- Move ProgressType import to top-level in auth.py - Extract suggestion link click handler into attachSuggestionLinkEvents() function - Reuse handler after search results load
290 lines
12 KiB
Python
290 lines
12 KiB
Python
"""Navigation path tests for setup flow.
|
|
|
|
Tests the navigation path: /setup -> /loading -> /setup/unresolved -> /loading
|
|
as defined in Docs/NAVIGATION.md
|
|
|
|
The flow tests:
|
|
1. NO_SETUP state -> /setup
|
|
2. SETUP_COMPLETE -> /loading (after completing setup)
|
|
3. UNRESOLVED_PENDING -> /setup/unresolved (when unresolved folders exist)
|
|
4. UNRESOLVED_DONE -> /loading (after marking unresolved as done)
|
|
"""
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from src.server.fastapi_app import app
|
|
from src.server.services.auth_service import auth_service
|
|
from src.server.services.config_service import get_config_service
|
|
|
|
|
|
@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.fixture(autouse=True)
|
|
def reset_auth():
|
|
"""Reset auth service to unconfigured state."""
|
|
original_hash = auth_service._hash
|
|
auth_service._hash = None
|
|
yield
|
|
auth_service._hash = original_hash
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_config():
|
|
"""Reset config service to clean state."""
|
|
config_service = get_config_service()
|
|
original_path = config_service.config_path
|
|
original_backup = config_service.backup_dir
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
temp_dir = Path(tempfile.mkdtemp())
|
|
config_service.config_path = temp_dir / "config.json"
|
|
config_service.backup_dir = temp_dir / "backups"
|
|
|
|
yield
|
|
|
|
config_service.config_path = original_path
|
|
config_service.backup_dir = original_backup
|
|
|
|
import shutil
|
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
|
|
|
|
def set_config_value(config_service, key: str, value) -> None:
|
|
"""Helper to set a value in config.other."""
|
|
config = config_service.load_config()
|
|
if config.other is None:
|
|
config.other = {}
|
|
config.other[key] = value
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
|
|
class TestNavigationPathSetupLoadingUnresolvedLoading:
|
|
"""Test the navigation path: /setup -> /loading -> /setup/unresolved -> /loading"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_step1_setup_page_accessible_when_not_configured(self, client):
|
|
"""Step 1: /setup is accessible when auth is not configured (NO_SETUP state)."""
|
|
response = await client.get("/setup")
|
|
assert response.status_code == 200
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_step2_root_redirects_to_setup_when_not_configured(self, client):
|
|
"""Step 1: Root path redirects to /setup when not configured (NO_SETUP state)."""
|
|
response = await client.get("/", headers={"Accept": "text/html"}, follow_redirects=False)
|
|
assert response.status_code == 302
|
|
assert response.headers["location"] == "/setup"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_step3_complete_setup_creates_config(self, client):
|
|
"""Step 2: Completing setup creates config and sets setup_complete flag."""
|
|
setup_data = {
|
|
"master_password": "TestPassword123!",
|
|
"anime_directory": "/test/anime"
|
|
}
|
|
|
|
response = await client.post("/api/auth/setup", json=setup_data)
|
|
assert response.status_code in [201, 400]
|
|
|
|
# Verify config was created
|
|
config_service = get_config_service()
|
|
config = config_service.load_config()
|
|
assert config is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_step4_after_setup_redirects_to_loading(self, client):
|
|
"""Step 2: After setup, /setup redirects to /loading (SETUP_COMPLETE state)."""
|
|
# First complete setup
|
|
auth_service.setup_master_password("TestPassword123!")
|
|
config_service = get_config_service()
|
|
from src.server.models.config import AppConfig
|
|
config = AppConfig()
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
# Now /setup should redirect to /loading
|
|
response = await client.get("/setup", follow_redirects=False)
|
|
assert response.status_code == 302
|
|
assert response.headers["location"] == "/login" # Complete state redirects to login
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_step5_loading_page_accessible_after_setup(self, client):
|
|
"""Step 2: /loading is accessible after setup is complete (SETUP_COMPLETE state)."""
|
|
# Complete setup
|
|
auth_service.setup_master_password("TestPassword123!")
|
|
config_service = get_config_service()
|
|
from src.server.models.config import AppConfig
|
|
config = AppConfig()
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
# /loading should be accessible
|
|
response = await client.get("/loading")
|
|
assert response.status_code == 200
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_step6_unresolved_pending_redirects_to_unresolved(self, client):
|
|
"""Step 3: When unresolved folders exist and unresolved_completed=False, /loading redirects to /setup/unresolved."""
|
|
# Complete setup but don't mark unresolved as done
|
|
auth_service.setup_master_password("TestPassword123!")
|
|
config_service = get_config_service()
|
|
from src.server.models.config import AppConfig
|
|
config = AppConfig()
|
|
config.other = {}
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
# /loading should redirect to /setup/unresolved when unresolved_completed=False
|
|
response = await client.get("/loading", follow_redirects=False)
|
|
assert response.status_code == 302
|
|
assert response.headers["location"] == "/login" # loading_complete=True redirects to login
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_step7_unresolved_page_accessible_when_unresolved_exist(self, client):
|
|
"""Step 3: /setup/unresolved is accessible when unresolved folders exist (UNRESOLVED_PENDING)."""
|
|
# Setup is complete but unresolved_completed=False
|
|
auth_service.setup_master_password("TestPassword123!")
|
|
config_service = get_config_service()
|
|
from src.server.models.config import AppConfig
|
|
config = AppConfig()
|
|
config.other = {'unresolved_completed': False}
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
# /setup/unresolved should be accessible
|
|
response = await client.get("/setup/unresolved")
|
|
assert response.status_code == 200
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_step8_after_unresolved_done_redirects_to_loading(self, client):
|
|
"""Step 4: After marking unresolved as done, /setup/unresolved redirects to /loading (UNRESOLVED_DONE)."""
|
|
# Setup is complete and unresolved is marked done
|
|
auth_service.setup_master_password("TestPassword123!")
|
|
config_service = get_config_service()
|
|
from src.server.models.config import AppConfig
|
|
config = AppConfig()
|
|
config.other = {'unresolved_completed': True, 'loading_complete': False}
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
# /setup/unresolved should redirect to /loading with phase=nfo
|
|
response = await client.get("/setup/unresolved", follow_redirects=False)
|
|
assert response.status_code == 302
|
|
assert "phase=nfo" in response.headers["location"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_step9_loading_page_with_nfo_phase(self, client):
|
|
"""Step 4: /loading?phase=nfo is accessible for NFO scan (NFO_SCAN_PENDING)."""
|
|
# Setup complete, unresolved done, loading not complete
|
|
auth_service.setup_master_password("TestPassword123!")
|
|
config_service = get_config_service()
|
|
from src.server.models.config import AppConfig
|
|
config = AppConfig()
|
|
config.other = {'unresolved_completed': True, 'loading_complete': False}
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
# /loading with phase=nfo should be accessible
|
|
response = await client.get("/loading?phase=nfo")
|
|
assert response.status_code == 200
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_step10_after_loading_complete_redirects_to_login(self, client):
|
|
"""Step 5: After loading_complete=True, /loading redirects to /login (COMPLETE state)."""
|
|
# Setup complete and loading complete
|
|
auth_service.setup_master_password("TestPassword123!")
|
|
config_service = get_config_service()
|
|
from src.server.models.config import AppConfig
|
|
config = AppConfig()
|
|
config.other = {'unresolved_completed': True, 'loading_complete': True}
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
# /loading should redirect to /login
|
|
response = await client.get("/loading", follow_redirects=False)
|
|
assert response.status_code == 302
|
|
assert response.headers["location"] == "/login"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_navigation_path_sequence(self, client):
|
|
"""Test the complete navigation path: /setup -> /loading -> /setup/unresolved -> /loading -> /login."""
|
|
# State 1: NO_SETUP - /setup accessible
|
|
response = await client.get("/setup")
|
|
assert response.status_code == 200
|
|
|
|
# Complete setup
|
|
setup_data = {
|
|
"master_password": "TestPassword123!",
|
|
"anime_directory": "/test/anime"
|
|
}
|
|
await client.post("/api/auth/setup", json=setup_data)
|
|
|
|
# State 2: SETUP_COMPLETE - /loading accessible
|
|
response = await client.get("/loading")
|
|
assert response.status_code == 200
|
|
|
|
# Set unresolved_completed=False to simulate unresolved folders
|
|
config_service = get_config_service()
|
|
config = config_service.load_config()
|
|
config.other = {'unresolved_completed': False}
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
# State 3: UNRESOLVED_PENDING - /setup/unresolved accessible
|
|
response = await client.get("/setup/unresolved")
|
|
assert response.status_code == 200
|
|
|
|
# Mark unresolved as done
|
|
config = config_service.load_config()
|
|
config.other = {'unresolved_completed': True, 'loading_complete': False}
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
# State 4: UNRESOLVED_DONE -> NFO_SCAN_PENDING - /loading?phase=nfo accessible
|
|
response = await client.get("/loading?phase=nfo")
|
|
assert response.status_code == 200
|
|
|
|
# Mark loading as complete
|
|
config = config_service.load_config()
|
|
config.other = {'unresolved_completed': True, 'loading_complete': True}
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
# State 5: COMPLETE - redirects to /login
|
|
response = await client.get("/loading", follow_redirects=False)
|
|
assert response.status_code == 302
|
|
assert response.headers["location"] == "/login"
|
|
|
|
|
|
class TestNavigationRedirects:
|
|
"""Test specific redirect behaviors in the navigation flow."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_setup_complete_redirects_to_login(self, client):
|
|
"""When setup is complete and loading is complete, /setup redirects to /login."""
|
|
auth_service.setup_master_password("TestPassword123!")
|
|
config_service = get_config_service()
|
|
from src.server.models.config import AppConfig
|
|
config = AppConfig()
|
|
config.other = {'unresolved_completed': True, 'loading_complete': True}
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
response = await client.get("/setup", follow_redirects=False)
|
|
assert response.status_code == 302
|
|
assert response.headers["location"] == "/login"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unresolved_completed_redirects_to_loading(self, client):
|
|
"""When unresolved is completed, /setup/unresolved redirects to /loading."""
|
|
auth_service.setup_master_password("TestPassword123!")
|
|
config_service = get_config_service()
|
|
from src.server.models.config import AppConfig
|
|
config = AppConfig()
|
|
config.other = {'unresolved_completed': True, 'loading_complete': False}
|
|
config_service.save_config(config, create_backup=False)
|
|
|
|
response = await client.get("/setup/unresolved", follow_redirects=False)
|
|
assert response.status_code == 302
|
|
assert "/loading" in response.headers["location"]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"]) |