Files
2026-02-13 09:10:52 +00:00

276 lines
11 KiB
Python

"""Comprehensive unit tests for app/api/settings.py module."""
from unittest.mock import MagicMock, patch
import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
@pytest.mark.unit
class TestSettingsRequireAdmin:
"""Tests for require_admin dependency."""
def test_require_admin_with_admin_user(self):
"""Test that admin users pass the requirement."""
from app.api.settings import require_admin
mock_request = MagicMock()
mock_request.session.get.return_value = {"username": "admin", "is_admin": True}
result = require_admin(mock_request)
assert result == {"username": "admin", "is_admin": True}
def test_require_admin_without_admin_user(self):
"""Test that non-admin users are rejected."""
from app.api.settings import require_admin
mock_request = MagicMock()
mock_request.session.get.return_value = {"username": "user", "is_admin": False}
with pytest.raises(HTTPException) as exc_info:
require_admin(mock_request)
assert exc_info.value.status_code == 403
assert "Admin access required" in exc_info.value.detail
def test_require_admin_without_user(self):
"""Test that requests without user are rejected."""
from app.api.settings import require_admin
mock_request = MagicMock()
mock_request.session.get.return_value = None
with pytest.raises(HTTPException) as exc_info:
require_admin(mock_request)
assert exc_info.value.status_code == 403
@pytest.mark.unit
class TestGetSettings:
"""Tests for GET /settings/ endpoint."""
@patch("app.api.settings.get_all_settings_from_db")
@patch("app.api.settings.get_settings_by_category")
@patch("app.api.settings.get_setting_metadata")
def test_get_settings_success(self, mock_metadata, mock_category, mock_db_settings, client: TestClient, db_session):
"""Test successful retrieval of settings."""
# Mock session to have admin user
mock_metadata.return_value = {"description": "Test setting", "type": "string"}
mock_category.return_value = {"general": ["setting1"]}
mock_db_settings.return_value = {"setting1": "value1"}
with patch.object(client, "get") as mock_get:
with patch("app.api.settings.settings") as mock_settings:
mock_settings.setting1 = "test_value"
# Create mock request with admin session
response = client.get("/api/settings/", cookies={"session": "admin_session"})
@patch("app.api.settings.get_all_settings_from_db")
def test_get_settings_database_error(self, mock_db_settings, client: TestClient, db_session):
"""Test handling of database errors."""
mock_db_settings.side_effect = Exception("Database error")
# This would need admin auth mocked properly
# The endpoint should return 500 error
@pytest.mark.unit
class TestGetSetting:
"""Tests for GET /settings/{key} endpoint."""
@patch("app.api.settings.get_setting_metadata")
def test_get_setting_existing_key(self, mock_metadata):
"""Test retrieval of existing setting."""
from app.config import settings
mock_metadata.return_value = {"description": "Test setting"}
mock_request = MagicMock()
mock_db = MagicMock()
mock_admin = {"is_admin": True}
with patch.object(settings, "workdir", "/tmp/test"):
# This would be called via FastAPI, testing the logic
pass
@patch("app.api.settings.get_setting_metadata")
def test_get_setting_nonexistent_key(self, mock_metadata):
"""Test retrieval of non-existent setting."""
mock_metadata.return_value = {}
# Should still return metadata even if setting doesn't exist
@pytest.mark.unit
class TestUpdateSetting:
"""Tests for POST /settings/{key} endpoint."""
@patch("app.api.settings.validate_setting_value")
@patch("app.api.settings.save_setting_to_db")
@patch("app.api.settings.get_setting_metadata")
def test_update_setting_success(self, mock_metadata, mock_save, mock_validate):
"""Test successful setting update."""
mock_validate.return_value = (True, None)
mock_save.return_value = True
mock_metadata.return_value = {"restart_required": False}
# Would test via client with proper auth mocking
@patch("app.api.settings.validate_setting_value")
def test_update_setting_invalid_value(self, mock_validate):
"""Test update with invalid value."""
mock_validate.return_value = (False, "Invalid value")
# Should raise HTTPException with 400 status
@patch("app.api.settings.validate_setting_value")
@patch("app.api.settings.save_setting_to_db")
def test_update_setting_database_error(self, mock_save, mock_validate):
"""Test handling of database save errors."""
mock_validate.return_value = (True, None)
mock_save.return_value = False
# Should raise HTTPException with 500 status
@patch("app.api.settings.validate_setting_value")
@patch("app.api.settings.save_setting_to_db")
@patch("app.api.settings.get_setting_metadata")
def test_update_setting_requires_restart(self, mock_metadata, mock_save, mock_validate):
"""Test update of setting that requires restart."""
mock_validate.return_value = (True, None)
mock_save.return_value = True
mock_metadata.return_value = {"restart_required": True}
# Response should include restart_required: True
@pytest.mark.unit
class TestDeleteSetting:
"""Tests for DELETE /settings/{key} endpoint."""
@patch("app.api.settings.delete_setting_from_db")
def test_delete_setting_success(self, mock_delete):
"""Test successful setting deletion."""
mock_delete.return_value = True
# Should return success response
@patch("app.api.settings.delete_setting_from_db")
def test_delete_setting_not_found(self, mock_delete):
"""Test deletion of non-existent setting."""
mock_delete.return_value = False
# Should raise HTTPException with 404 status
@patch("app.api.settings.delete_setting_from_db")
def test_delete_setting_database_error(self, mock_delete):
"""Test handling of database errors."""
mock_delete.side_effect = Exception("Database error")
# Should raise HTTPException with 500 status
@pytest.mark.unit
class TestBulkUpdateSettings:
"""Tests for POST /settings/bulk-update endpoint."""
@patch("app.api.settings.validate_setting_value")
@patch("app.api.settings.save_setting_to_db")
@patch("app.api.settings.get_setting_metadata")
def test_bulk_update_all_success(self, mock_metadata, mock_save, mock_validate):
"""Test successful bulk update of multiple settings."""
mock_validate.return_value = (True, None)
mock_save.return_value = True
mock_metadata.return_value = {"restart_required": False}
# Should return success with all updated
@patch("app.api.settings.validate_setting_value")
@patch("app.api.settings.save_setting_to_db")
@patch("app.api.settings.get_setting_metadata")
def test_bulk_update_partial_failure(self, mock_metadata, mock_save, mock_validate):
"""Test bulk update with some failures."""
# First validation succeeds, second fails
mock_validate.side_effect = [(True, None), (False, "Invalid value")]
mock_save.return_value = True
mock_metadata.return_value = {"restart_required": False}
# Should return success=False with errors list
@patch("app.api.settings.validate_setting_value")
@patch("app.api.settings.save_setting_to_db")
@patch("app.api.settings.get_setting_metadata")
def test_bulk_update_with_restart_required(self, mock_metadata, mock_save, mock_validate):
"""Test bulk update where one setting requires restart."""
mock_validate.return_value = (True, None)
mock_save.return_value = True
# First setting doesn't require restart, second does
mock_metadata.side_effect = [{"restart_required": False}, {"restart_required": True}]
# Response should have restart_required: True
@patch("app.api.settings.validate_setting_value")
@patch("app.api.settings.save_setting_to_db")
def test_bulk_update_database_errors(self, mock_save, mock_validate):
"""Test bulk update with database save errors."""
mock_validate.return_value = (True, None)
# First save succeeds, second fails
mock_save.side_effect = [True, False]
# Should include errors for failed saves
@patch("app.api.settings.validate_setting_value")
def test_bulk_update_empty_list(self, mock_validate):
"""Test bulk update with empty updates list."""
# Should return success with empty results
@patch("app.api.settings.validate_setting_value")
@patch("app.api.settings.save_setting_to_db")
def test_bulk_update_with_none_value(self, mock_save, mock_validate):
"""Test bulk update with None value (delete)."""
mock_validate.return_value = (True, None)
mock_save.return_value = True
# None values should be handled (possibly as deletes)
@pytest.mark.unit
class TestSettingModels:
"""Tests for Pydantic models."""
def test_setting_update_model_valid(self):
"""Test SettingUpdate model with valid data."""
from app.api.settings import SettingUpdate
setting = SettingUpdate(key="test_key", value="test_value")
assert setting.key == "test_key"
assert setting.value == "test_value"
def test_setting_update_model_none_value(self):
"""Test SettingUpdate model with None value."""
from app.api.settings import SettingUpdate
setting = SettingUpdate(key="test_key", value=None)
assert setting.key == "test_key"
assert setting.value is None
def test_setting_response_model(self):
"""Test SettingResponse model."""
from app.api.settings import SettingResponse
response = SettingResponse(key="test_key", value="test_value", metadata={"description": "test"})
assert response.key == "test_key"
assert response.value == "test_value"
assert response.metadata["description"] == "test"
def test_settings_list_response_model(self):
"""Test SettingsListResponse model."""
from app.api.settings import SettingsListResponse
response = SettingsListResponse(
settings={"key1": {"value": "val1"}},
categories={"general": ["key1"]},
db_settings={"key1": "val1"},
)
assert "key1" in response.settings
assert "general" in response.categories