refactor: move import to module level and extract event handler

- Move ProgressType import to top-level in auth.py
- Extract suggestion link click handler into attachSuggestionLinkEvents() function
- Reuse handler after search results load
This commit is contained in:
2026-06-07 21:51:49 +02:00
parent 14f7b2f28a
commit e0be00dce6
4 changed files with 368 additions and 54 deletions

View File

@@ -0,0 +1,290 @@
"""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"])