"""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"])