Files
gh-christianlouis-docuelevate/tests/test_settings_service_extended.py

171 lines
7.1 KiB
Python

"""Tests for uncovered paths in app/utils/settings_service.py.
Covers:
- save_setting_to_db: decryption of old_storage_value for sensitive keys (lines 1247-1252)
- delete_setting_from_db: decryption of stored value for sensitive keys (lines 1329-1334)
- get_audit_log: SQLAlchemyError handling (lines 1469-1471)
- get_setting_history: SQLAlchemyError handling (lines 1510-1512)
- rollback_setting: SQLAlchemyError handling (lines 1553-1556)
- get_settings_for_export: DB value takes precedence in effective export (line 1582)
"""
from unittest.mock import patch
import pytest
from sqlalchemy.exc import SQLAlchemyError
from app.models import ApplicationSettings, SettingsAuditLog
from app.utils.settings_service import (
delete_setting_from_db,
get_audit_log,
get_setting_history,
get_settings_for_export,
rollback_setting,
save_setting_to_db,
)
@pytest.mark.unit
class TestSaveSettingDecryptsOldValue:
"""Tests for save_setting_to_db decrypting old value for audit log."""
@patch("app.utils.settings_service.get_setting_metadata")
def test_save_sensitive_setting_decrypts_old_value_for_audit(self, mock_metadata, db_session):
"""When updating a sensitive setting, old value is decrypted for the audit log."""
mock_metadata.return_value = {"sensitive": True}
# First, save an encrypted value directly
setting = ApplicationSettings(key="openai_api_key", value="enc:old_encrypted")
db_session.add(setting)
db_session.commit()
with (
patch("app.utils.encryption.is_encryption_available", return_value=True),
patch("app.utils.encryption.encrypt_value", return_value="enc:new_encrypted"),
patch("app.utils.encryption.decrypt_value", return_value="old_plaintext_key") as mock_decrypt,
):
result = save_setting_to_db(db_session, "openai_api_key", "new_api_key", changed_by="admin")
assert result is True
mock_decrypt.assert_called_once_with("enc:old_encrypted")
# Verify audit log has the decrypted old value
audit = db_session.query(SettingsAuditLog).filter_by(key="openai_api_key").first()
assert audit is not None
assert audit.old_value == "old_plaintext_key"
@patch("app.utils.settings_service.get_setting_metadata")
def test_save_sensitive_setting_decryption_fails_uses_raw(self, mock_metadata, db_session):
"""When decryption of old value fails, the raw stored value is used."""
mock_metadata.return_value = {"sensitive": True}
setting = ApplicationSettings(key="openai_api_key", value="enc:corrupted")
db_session.add(setting)
db_session.commit()
with (
patch("app.utils.encryption.is_encryption_available", return_value=True),
patch("app.utils.encryption.encrypt_value", return_value="enc:new"),
patch("app.utils.encryption.decrypt_value", side_effect=Exception("Decryption failed")),
):
result = save_setting_to_db(db_session, "openai_api_key", "new_key", changed_by="admin")
assert result is True
# Audit log should have the raw encrypted value as fallback
audit = db_session.query(SettingsAuditLog).filter_by(key="openai_api_key").first()
assert audit is not None
assert audit.old_value == "enc:corrupted"
@pytest.mark.unit
class TestDeleteSettingDecryptsOldValue:
"""Tests for delete_setting_from_db decrypting stored value for audit log."""
@patch("app.utils.settings_service.get_setting_metadata")
def test_delete_sensitive_setting_decrypts_for_audit(self, mock_metadata, db_session):
"""When deleting a sensitive setting, old value is decrypted for the audit log."""
mock_metadata.return_value = {"sensitive": True}
setting = ApplicationSettings(key="openai_api_key", value="enc:secret_value")
db_session.add(setting)
db_session.commit()
with patch("app.utils.encryption.decrypt_value", return_value="decrypted_secret") as mock_decrypt:
result = delete_setting_from_db(db_session, "openai_api_key", changed_by="admin")
assert result is True
mock_decrypt.assert_called_once_with("enc:secret_value")
audit = db_session.query(SettingsAuditLog).filter_by(key="openai_api_key").first()
assert audit is not None
assert audit.old_value == "decrypted_secret"
assert audit.action == "delete"
@patch("app.utils.settings_service.get_setting_metadata")
def test_delete_sensitive_setting_decryption_fails_uses_raw(self, mock_metadata, db_session):
"""When decryption fails during delete, the raw value is used in audit."""
mock_metadata.return_value = {"sensitive": True}
setting = ApplicationSettings(key="openai_api_key", value="enc:bad_data")
db_session.add(setting)
db_session.commit()
with patch("app.utils.encryption.decrypt_value", side_effect=Exception("Bad key")):
result = delete_setting_from_db(db_session, "openai_api_key", changed_by="admin")
assert result is True
audit = db_session.query(SettingsAuditLog).filter_by(key="openai_api_key").first()
assert audit is not None
assert audit.old_value == "enc:bad_data"
@pytest.mark.unit
class TestGetAuditLogError:
"""Tests for get_audit_log SQLAlchemyError handling."""
def test_get_audit_log_returns_empty_on_db_error(self, db_session):
"""get_audit_log returns empty list when a database error occurs."""
with patch.object(db_session, "query", side_effect=SQLAlchemyError("Connection lost")):
result = get_audit_log(db_session)
assert result == []
@pytest.mark.unit
class TestGetSettingHistoryError:
"""Tests for get_setting_history SQLAlchemyError handling."""
def test_get_setting_history_returns_empty_on_db_error(self, db_session):
"""get_setting_history returns empty list when a database error occurs."""
with patch.object(db_session, "query", side_effect=SQLAlchemyError("Timeout")):
result = get_setting_history(db_session, "debug")
assert result == []
@pytest.mark.unit
class TestRollbackSettingError:
"""Tests for rollback_setting SQLAlchemyError handling."""
def test_rollback_setting_returns_false_on_db_error(self, db_session):
"""rollback_setting returns False when a database error occurs."""
with patch.object(db_session, "query", side_effect=SQLAlchemyError("DB locked")):
result = rollback_setting(db_session, "debug", 1, changed_by="admin")
assert result is False
@pytest.mark.unit
class TestGetSettingsForExport:
"""Tests for get_settings_for_export effective mode."""
def test_effective_export_db_value_takes_precedence(self, db_session):
"""In effective mode, DB value takes precedence over ENV/default."""
# Save a value in the DB
save_setting_to_db(db_session, "debug", "true", changed_by="test")
with patch("app.utils.settings_service.SETTING_METADATA", {"debug": {"type": "bool"}}):
result = get_settings_for_export(db_session, source="effective")
assert "DEBUG" in result
assert result["DEBUG"] == "true"