test: improve coverage for process_with_ocr, api/settings, and settings_service to 90%+
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user