d9abbc1c99
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
171 lines
7.1 KiB
Python
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"
|