Aniworld/tests/api/test_config_endpoints.py
Lukas 0d6cade56c feat: Add comprehensive configuration persistence system
- Implemented ConfigService with file-based JSON persistence
  - Atomic file writes using temporary files
  - Configuration validation with detailed error reporting
  - Schema versioning with migration support
  - Singleton pattern for global access

- Added backup management functionality
  - Automatic backup creation before updates
  - Manual backup creation with custom names
  - Backup restoration with pre-restore backup
  - Backup listing and deletion
  - Automatic cleanup of old backups (max 10)

- Updated configuration API endpoints
  - GET /api/config - Retrieve configuration
  - PUT /api/config - Update with automatic backup
  - POST /api/config/validate - Validation without applying
  - GET /api/config/backups - List all backups
  - POST /api/config/backups - Create manual backup
  - POST /api/config/backups/{name}/restore - Restore backup
  - DELETE /api/config/backups/{name} - Delete backup

- Comprehensive test coverage
  - 27 unit tests for ConfigService (all passing)
  - Integration tests for API endpoints
  - Tests for validation, persistence, backups, and error handling

- Updated documentation
  - Added ConfigService documentation to infrastructure.md
  - Marked task as completed in instructions.md

Files changed:
- src/server/services/config_service.py (new)
- src/server/api/config.py (refactored)
- tests/unit/test_config_service.py (new)
- tests/api/test_config_endpoints.py (enhanced)
- infrastructure.md (updated)
- instructions.md (updated)
2025-10-17 20:26:40 +02:00

165 lines
5.1 KiB
Python

"""Integration tests for configuration API endpoints."""
import tempfile
from pathlib import Path
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
from src.server.fastapi_app import app
from src.server.models.config import AppConfig
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
def client():
"""Create test client."""
return TestClient(app)
def test_get_config_public(client, mock_config_service):
"""Test getting configuration."""
resp = client.get("/api/config")
assert resp.status_code == 200
data = resp.json()
assert "name" in data
assert "data_dir" in data
def test_validate_config(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 = client.post("/api/config/validate", json=cfg)
assert resp.status_code == 200
body = resp.json()
assert body.get("valid") is True
def test_validate_invalid_config(client, mock_config_service):
"""Test validation of invalid configuration."""
cfg = {
"name": "Aniworld",
"backup": {"enabled": True, "path": None}, # Invalid
}
resp = 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
def test_update_config_unauthorized(client):
"""Test that update requires authentication."""
update = {"scheduler": {"enabled": False}}
resp = client.put("/api/config", json=update)
assert resp.status_code in (401, 422)
def test_list_backups(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 = 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]
def test_create_backup(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 = client.post("/api/config/backups")
assert resp.status_code == 200
data = resp.json()
assert "name" in data
assert "message" in data
def test_restore_backup(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 = client.post("/api/config/backups/restore_test.json/restore")
assert resp.status_code == 200
data = resp.json()
assert data["name"] == "TestApp" # Original name restored
def test_delete_backup(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 = client.delete("/api/config/backups/delete_test.json")
assert resp.status_code == 200
data = resp.json()
assert "deleted successfully" in data["message"]
def test_config_persistence(client, mock_config_service):
"""Test end-to-end configuration persistence."""
# Get initial config
resp = client.get("/api/config")
assert resp.status_code == 200
initial = resp.json()
# Validate it can be loaded again
resp2 = client.get("/api/config")
assert resp2.status_code == 200
assert resp2.json() == initial