Aniworld/tests/api/test_config_endpoints.py

194 lines
6.3 KiB
Python

"""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.anyio
async def test_get_config_public(client, mock_config_service):
"""Test getting configuration."""
resp = await client.get("/api/config")
assert resp.status_code == 200
data = resp.json()
assert "name" in data
assert "data_dir" in data
@pytest.mark.anyio
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.anyio
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.anyio
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.anyio
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.anyio
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.anyio
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.anyio
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.anyio
async def test_config_persistence(client, mock_config_service):
"""Test end-to-end configuration persistence."""
# Get initial config
resp = await client.get("/api/config")
assert resp.status_code == 200
initial = resp.json()
# Validate it can be loaded again
resp2 = await client.get("/api/config")
assert resp2.status_code == 200
assert resp2.json() == initial