""" Setup API Endpoint Tests Tests for the POST /api/setup endpoint that handles initial configuration Tests setup completion, validation, redirect behavior, and persistence """ 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 state before each test.""" # Note: This is a simplified approach # In real tests, you might need to backup/restore the actual state initial_state = auth_service.is_configured() yield # Restore state after test # This is placeholder - actual implementation depends on auth_service structure class TestSetupEndpoint: """Tests for the POST /api/setup endpoint.""" async def test_setup_endpoint_exists(self, client): """Test that the setup endpoint responds.""" # Prepare minimal valid setup data setup_data = { "master_password": "TestPassword123!", "anime_directory": "/test/anime" } response = await client.post("/api/auth/setup", json=setup_data) # Should not return 404 assert response.status_code != 404 async def test_setup_with_valid_data(self, client): """Test setup with all valid configuration data.""" setup_data = { "master_password": "StrongPassword123!", "anime_directory": "/path/to/anime", "name": "Test Aniworld", "data_dir": "test_data", "scheduler_enabled": True, "scheduler_interval_minutes": 60, "logging_level": "INFO", "logging_file": "app.log", "logging_max_bytes": 10485760, "logging_backup_count": 5, "backup_enabled": True, "backup_path": "backups", "backup_keep_days": 30, "nfo_tmdb_api_key": "test_api_key_12345", "nfo_auto_create": True, "nfo_update_on_scan": False, "nfo_download_poster": True, "nfo_download_logo": True, "nfo_download_fanart": True, "nfo_image_size": "original" } response = await client.post("/api/auth/setup", json=setup_data) # Should succeed (or return appropriate status if already configured) assert response.status_code in [201, 400] if response.status_code == 201: data = response.json() assert "message" in data or "status" in data async def test_setup_requires_master_password(self, client): """Test that setup requires a master password.""" setup_data = { "anime_directory": "/test/anime" # Missing master_password } response = await client.post("/api/auth/setup", json=setup_data) # Should return validation error assert response.status_code == 422 async def test_setup_with_weak_password(self, client): """Test that weak passwords are rejected.""" setup_data = { "master_password": "weak", "anime_directory": "/test/anime" } response = await client.post("/api/auth/setup", json=setup_data) # Should return validation error or bad request assert response.status_code in [400, 422] async def test_setup_rejects_when_already_configured(self, client): """Test that setup endpoint rejects requests when already configured.""" if not auth_service.is_configured(): pytest.skip("Auth not configured, cannot test rejection") setup_data = { "master_password": "TestPassword123!", "anime_directory": "/test/anime" } response = await client.post("/api/auth/setup", json=setup_data) # Should return 400 Bad Request assert response.status_code == 400 data = response.json() assert "already configured" in data["detail"].lower() async def test_setup_validates_scheduler_interval(self, client): """Test that invalid scheduler intervals are rejected.""" setup_data = { "master_password": "TestPassword123!", "anime_directory": "/test/anime", "scheduler_interval_minutes": -10 # Invalid negative value } response = await client.post("/api/auth/setup", json=setup_data) # Should return validation error assert response.status_code == 422 async def test_setup_validates_logging_level(self, client): """Test that invalid logging levels are rejected.""" setup_data = { "master_password": "TestPassword123!", "anime_directory": "/test/anime", "logging_level": "INVALID_LEVEL" } response = await client.post("/api/auth/setup", json=setup_data) # Should return validation error assert response.status_code in [400, 422] async def test_setup_with_optional_fields_only(self, client): """Test setup with only required fields.""" setup_data = { "master_password": "MinimalPassword123!", "anime_directory": "/minimal/anime" } response = await client.post("/api/auth/setup", json=setup_data) # Should succeed or indicate already configured assert response.status_code in [201, 400] async def test_setup_saves_configuration(self, client): """Test that setup persists configuration to config.json.""" if auth_service.is_configured(): pytest.skip("Auth already configured, cannot test setup") setup_data = { "master_password": "PersistentPassword123!", "anime_directory": "/persistent/anime", "name": "Persistent Test", "scheduler_enabled": False } response = await client.post("/api/auth/setup", json=setup_data) if response.status_code == 201: # Verify config was saved config_service = get_config_service() config = config_service.load_config() assert config is not None assert config.name == "Persistent Test" assert config.scheduler.enabled == False class TestSetupValidation: """Tests for setup endpoint validation logic.""" async def test_password_minimum_length_validation(self, client): """Test that passwords shorter than 8 characters are rejected.""" setup_data = { "master_password": "short", "anime_directory": "/test/anime" } response = await client.post("/api/auth/setup", json=setup_data) assert response.status_code == 422 data = response.json() assert any("password" in str(error).lower() for error in data.get("detail", [])) async def test_anime_directory_required(self, client): """Test that anime directory is required.""" setup_data = { "master_password": "ValidPassword123!" # Missing anime_directory } response = await client.post("/api/auth/setup", json=setup_data) # May require directory depending on implementation # At minimum should not crash assert response.status_code in [201, 400, 422] async def test_invalid_json_rejected(self, client): """Test that malformed JSON is rejected.""" response = await client.post( "/api/auth/setup", content="invalid json {", headers={"Content-Type": "application/json"} ) assert response.status_code == 422 async def test_empty_request_rejected(self, client): """Test that empty request body is rejected.""" response = await client.post("/api/auth/setup", json={}) assert response.status_code == 422 async def test_scheduler_interval_positive_validation(self, client): """Test that scheduler interval must be positive.""" setup_data = { "master_password": "ValidPassword123!", "anime_directory": "/test/anime", "scheduler_interval_minutes": 0 } response = await client.post("/api/auth/setup", json=setup_data) # Should reject zero or negative intervals assert response.status_code in [400, 422] async def test_backup_keep_days_validation(self, client): """Test that backup keep days is validated.""" setup_data = { "master_password": "ValidPassword123!", "anime_directory": "/test/anime", "backup_keep_days": -5 # Invalid negative value } response = await client.post("/api/auth/setup", json=setup_data) assert response.status_code == 422 async def test_nfo_image_size_validation(self, client): """Test that NFO image size is validated.""" setup_data = { "master_password": "ValidPassword123!", "anime_directory": "/test/anime", "nfo_image_size": "invalid_size" } response = await client.post("/api/auth/setup", json=setup_data) # Should validate image size options assert response.status_code in [400, 422] class TestSetupRedirect: """Tests for setup page redirect behavior.""" async def test_redirect_to_setup_when_not_configured(self, client): """Test that accessing root redirects to setup when not configured.""" if auth_service.is_configured(): pytest.skip("Auth already configured, cannot test redirect") response = await client.get("/", follow_redirects=False) # Should redirect to setup if response.status_code in [301, 302, 303, 307, 308]: assert "/setup" in response.headers.get("location", "") async def test_setup_page_accessible_when_not_configured(self, client): """Test that setup page is accessible when not configured.""" response = await client.get("/setup") # Should be accessible assert response.status_code in [200, 302] async def test_redirect_to_login_after_setup(self, client): """Test that setup redirects to login/loading page after completion.""" if auth_service.is_configured(): pytest.skip("Auth already configured, cannot test post-setup redirect") setup_data = { "master_password": "TestPassword123!", "anime_directory": "/test/anime" } response = await client.post("/api/auth/setup", json=setup_data, follow_redirects=False) if response.status_code == 201: # Check for redirect information in response data = response.json() # Response may contain redirect URL or loading page info assert "redirect" in data or "message" in data or "status" in data class TestSetupPersistence: """Tests for setup configuration persistence.""" async def test_setup_creates_config_file(self, client): """Test that setup creates the configuration file.""" if auth_service.is_configured(): pytest.skip("Auth already configured, cannot test config creation") setup_data = { "master_password": "PersistenceTest123!", "anime_directory": "/test/anime", "name": "Persistence Test" } response = await client.post("/api/auth/setup", json=setup_data) if response.status_code == 201: # Verify config file exists config_service = get_config_service() config = config_service.load_config() assert config is not None async def test_setup_persists_all_settings(self, client): """Test that all provided settings are persisted.""" if auth_service.is_configured(): pytest.skip("Auth already configured") setup_data = { "master_password": "CompleteTest123!", "anime_directory": "/complete/anime", "name": "Complete Setup", "scheduler_enabled": True, "scheduler_interval_minutes": 120, "backup_enabled": True, "nfo_auto_create": True } response = await client.post("/api/auth/setup", json=setup_data) if response.status_code == 201: config_service = get_config_service() config = config_service.load_config() assert config.name == "Complete Setup" assert config.scheduler.enabled == True assert config.scheduler.interval_minutes == 120 assert config.backup.enabled == True assert config.nfo.auto_create == True async def test_setup_stores_password_hash(self, client): """Test that setup stores password hash, not plaintext.""" if auth_service.is_configured(): pytest.skip("Auth already configured") password = "SecurePassword123!" setup_data = { "master_password": password, "anime_directory": "/secure/anime" } response = await client.post("/api/auth/setup", json=setup_data) if response.status_code == 201: # Verify password is hashed config_service = get_config_service() config = config_service.load_config() stored_hash = config.other.get('master_password_hash', '') # Hash should not match plaintext password assert stored_hash != password # Hash should exist and be non-empty assert len(stored_hash) > 20 class TestSetupEdgeCases: """Tests for edge cases in setup endpoint.""" async def test_setup_with_special_characters_in_paths(self, client): """Test that special characters in paths are handled.""" setup_data = { "master_password": "TestPassword123!", "anime_directory": "/path/with spaces/and-dashes/and_underscores" } response = await client.post("/api/auth/setup", json=setup_data) # Should handle special characters gracefully assert response.status_code in [201, 400, 422] async def test_setup_with_unicode_in_name(self, client): """Test that Unicode characters in name are handled.""" setup_data = { "master_password": "TestPassword123!", "anime_directory": "/test/anime", "name": "アニメ Manager 日本語" } response = await client.post("/api/auth/setup", json=setup_data) # Should handle Unicode gracefully assert response.status_code in [201, 400, 422] async def test_setup_with_very_long_values(self, client): """Test that very long input values are handled.""" setup_data = { "master_password": "a" * 1000, # Very long password "anime_directory": "/test/anime" } response = await client.post("/api/auth/setup", json=setup_data) # Should handle or reject gracefully assert response.status_code in [201, 400, 422] async def test_setup_with_null_values(self, client): """Test that null values are handled appropriately.""" setup_data = { "master_password": "TestPassword123!", "anime_directory": "/test/anime", "name": None, "logging_level": None } response = await client.post("/api/auth/setup", json=setup_data) # Should handle null values (use defaults or reject) assert response.status_code in [201, 400, 422]