"""Integration tests for configuration API endpoints.""" import tempfile from pathlib import Path from unittest.mock import patch import pytest from httpx import ASGITransport, AsyncClient from src.server.fastapi_app import app from src.server.models.config import AppConfig from src.server.services.auth_service import auth_service from src.server.services.config_service import ConfigService @pytest.fixture def temp_config_dir(): """Create temporary directory for test config files.""" with tempfile.TemporaryDirectory() as tmpdir: yield Path(tmpdir) @pytest.fixture def config_service(temp_config_dir): """Create ConfigService instance with temporary paths.""" config_path = temp_config_dir / "config.json" backup_dir = temp_config_dir / "backups" return ConfigService( config_path=config_path, backup_dir=backup_dir, max_backups=3 ) @pytest.fixture def mock_config_service(config_service): """Mock get_config_service to return test instance.""" with patch( "src.server.api.config.get_config_service", return_value=config_service ): yield config_service @pytest.fixture async def client(): """Create async test client.""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac @pytest.fixture async def authenticated_client(): """Create authenticated async test client.""" # Setup auth if not configured if not auth_service.is_configured(): auth_service.setup_master_password("TestPass123!") transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: # Login to get token r = await ac.post("/api/auth/login", json={"password": "TestPass123!"}) if r.status_code == 200: token = r.json()["access_token"] ac.headers["Authorization"] = f"Bearer {token}" yield ac @pytest.mark.asyncio async def test_get_config_public(authenticated_client, mock_config_service): """Test getting configuration.""" resp = await authenticated_client.get("/api/config") assert resp.status_code == 200 data = resp.json() assert "name" in data assert "data_dir" in data @pytest.mark.asyncio async def test_validate_config(authenticated_client, mock_config_service): """Test configuration validation.""" cfg = { "name": "Aniworld", "data_dir": "data", "scheduler": {"enabled": True, "interval_minutes": 30}, "logging": {"level": "INFO"}, "backup": {"enabled": False}, "other": {}, } resp = await authenticated_client.post("/api/config/validate", json=cfg) assert resp.status_code == 200 body = resp.json() assert body.get("valid") is True @pytest.mark.asyncio async def test_validate_invalid_config(authenticated_client, mock_config_service): """Test validation of invalid configuration.""" cfg = { "name": "Aniworld", "backup": {"enabled": True, "path": None}, # Invalid } resp = await authenticated_client.post("/api/config/validate", json=cfg) assert resp.status_code == 200 body = resp.json() assert body.get("valid") is False assert len(body.get("errors", [])) > 0 @pytest.mark.asyncio async def test_update_config_unauthorized(client): """Test that update requires authentication.""" update = {"scheduler": {"enabled": False}} resp = await client.put("/api/config", json=update) assert resp.status_code in (401, 422) @pytest.mark.asyncio async def test_list_backups(authenticated_client, mock_config_service): """Test listing configuration backups.""" # Create a sample config first sample_config = AppConfig(name="TestApp", data_dir="test_data") mock_config_service.save_config(sample_config, create_backup=False) mock_config_service.create_backup(name="test_backup") resp = await authenticated_client.get("/api/config/backups") assert resp.status_code == 200 backups = resp.json() assert isinstance(backups, list) if len(backups) > 0: assert "name" in backups[0] assert "size_bytes" in backups[0] assert "created_at" in backups[0] @pytest.mark.asyncio async def test_create_backup(authenticated_client, mock_config_service): """Test creating a configuration backup.""" # Create a sample config first sample_config = AppConfig(name="TestApp", data_dir="test_data") mock_config_service.save_config(sample_config, create_backup=False) resp = await authenticated_client.post("/api/config/backups") assert resp.status_code == 200 data = resp.json() assert "name" in data assert "message" in data @pytest.mark.asyncio async def test_restore_backup(authenticated_client, mock_config_service): """Test restoring configuration from backup.""" # Create initial config and backup sample_config = AppConfig(name="TestApp", data_dir="test_data") mock_config_service.save_config(sample_config, create_backup=False) mock_config_service.create_backup(name="restore_test") # Modify config sample_config.name = "Modified" mock_config_service.save_config(sample_config, create_backup=False) # Restore from backup resp = await authenticated_client.post("/api/config/backups/restore_test.json/restore") assert resp.status_code == 200 data = resp.json() assert data["name"] == "TestApp" # Original name restored @pytest.mark.asyncio async def test_delete_backup(authenticated_client, mock_config_service): """Test deleting a configuration backup.""" # Create a sample config and backup sample_config = AppConfig(name="TestApp", data_dir="test_data") mock_config_service.save_config(sample_config, create_backup=False) mock_config_service.create_backup(name="delete_test") resp = await authenticated_client.delete("/api/config/backups/delete_test.json") assert resp.status_code == 200 data = resp.json() assert "deleted successfully" in data["message"] @pytest.mark.asyncio async def test_config_persistence(authenticated_client, mock_config_service): """Test end-to-end configuration persistence.""" # Get initial config resp = await authenticated_client.get("/api/config") assert resp.status_code == 200 initial = resp.json() # Validate it can be loaded again resp2 = await authenticated_client.get("/api/config") assert resp2.status_code == 200 assert resp2.json() == initial