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)
This commit is contained in:
@@ -1,12 +1,52 @@
|
||||
"""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, SchedulerConfig
|
||||
|
||||
client = TestClient(app)
|
||||
from src.server.models.config import AppConfig
|
||||
from src.server.services.config_service import ConfigService
|
||||
|
||||
|
||||
def test_get_config_public():
|
||||
@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()
|
||||
@@ -14,7 +54,8 @@ def test_get_config_public():
|
||||
assert "data_dir" in data
|
||||
|
||||
|
||||
def test_validate_config():
|
||||
def test_validate_config(client, mock_config_service):
|
||||
"""Test configuration validation."""
|
||||
cfg = {
|
||||
"name": "Aniworld",
|
||||
"data_dir": "data",
|
||||
@@ -29,8 +70,95 @@ def test_validate_config():
|
||||
assert body.get("valid") is True
|
||||
|
||||
|
||||
def test_update_config_unauthorized():
|
||||
# update requires auth; without auth should be 401
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user