Files
ss-tools/backend/tests/services/test_profile_preference_service.py
busya 442596a3dc test(services): add unit tests for profile_preference, resource, validation_service
- profile_preference_service: 19 tests, 100% coverage (CRUD, validation, encryption,
  DTO conversion with mocked AuthRepository and EncryptionManager)
- resource_service: 50 tests, 100% coverage (dashboard/dataset enrichment with
  git/task status, pagination, activity summary, datetime normalization)
- validation_service: 63 tests across 3 files, 97% coverage (provider validation,
  environment validation, source resolution, run/record conversion, trigger_run,
  create/update/delete tasks, list/filter runs, get_run_detail)
2026-06-10 14:57:11 +03:00

466 lines
20 KiB
Python

# #region Test.ProfilePreferenceService [C:3] [TYPE Module] [SEMANTICS test,profile,preference,crud]
# @BRIEF Tests for profile_preference_service.py — get, update, validation, encryption.
# @RELATION BINDS_TO -> [ProfilePreferenceService]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from unittest.mock import MagicMock, patch, ANY
from datetime import UTC, datetime
import pytest
# Set ENCRYPTION_KEY before importing anything that uses EncryptionManager
import os
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing")
os.environ.setdefault("ENCRYPTION_KEY", "MldUHg5kwSAcPnnYxmhWDS6ASb6e_bWQRV5gtwHrjQ0=")
from src.schemas.profile import ProfilePreferenceUpdateRequest
def make_user(user_id="user-1", **kwargs):
from src.models.auth import User
user = MagicMock(spec=User)
user.id = user_id
user.roles = []
user.auth_source = "database"
for k, v in kwargs.items():
setattr(user, k, v)
return user
def make_preference_row(user_id="user-1", **overrides):
from src.models.profile import UserDashboardPreference
pref = MagicMock(spec=UserDashboardPreference)
pref.user_id = user_id
pref.superset_username = None
pref.superset_username_normalized = None
pref.show_only_my_dashboards = False
pref.show_only_slug_dashboards = True
pref.git_username = None
pref.git_email = None
pref.git_personal_access_token_encrypted = None
pref.start_page = "dashboards"
pref.auto_open_task_drawer = True
pref.dashboards_table_density = "comfortable"
pref.telegram_id = None
pref.email_address = None
pref.notify_on_fail = True
pref.created_at = datetime.now(UTC)
pref.updated_at = datetime.now(UTC)
for k, v in overrides.items():
setattr(pref, k, v)
return pref
class TestGetMyPreference:
"""get_my_preference — return user preference or default."""
@pytest.fixture
def _make_security_summary(self):
from src.schemas.profile import ProfileSecuritySummary
return ProfileSecuritySummary(read_only=True, auth_source="database")
@pytest.fixture
def service(self, _make_security_summary):
from src.services.profile_preference_service import ProfilePreferenceService
db = MagicMock()
config_mgr = MagicMock()
with patch("src.services.profile_preference_service.AuthRepository") as mock_auth_repo_cls, \
patch("src.services.profile_preference_service.EncryptionManager"), \
patch("src.services.profile_preference_service.SecurityBadgeService") as mock_badge_cls:
mock_auth = MagicMock()
mock_auth_repo_cls.return_value = mock_auth
mock_badge = MagicMock()
mock_badge.build_security_summary.return_value = _make_security_summary
mock_badge_cls.return_value = mock_badge
svc = ProfilePreferenceService(db, config_mgr)
svc.auth_repository = mock_auth
svc.security_badge_service = mock_badge
yield svc
def test_no_preference_returns_default(self, service):
service.auth_repository.get_user_dashboard_preference.return_value = None
user = make_user()
result = service.get_my_preference(user)
assert result.status == "success"
assert "not configured" in (result.message or "").lower()
assert result.preference.user_id == "user-1"
assert result.preference.superset_username is None
assert result.preference.show_only_my_dashboards is False
def test_existing_preference(self, service):
pref = make_preference_row(
superset_username="admin",
superset_username_normalized="admin",
show_only_my_dashboards=True,
)
service.auth_repository.get_user_dashboard_preference.return_value = pref
user = make_user()
result = service.get_my_preference(user)
assert result.status == "success"
assert result.preference.show_only_my_dashboards is True
assert result.preference.superset_username_normalized == "admin"
def test_security_summary_included(self, service, _make_security_summary):
service.auth_repository.get_user_dashboard_preference.return_value = None
user = make_user()
result = service.get_my_preference(user)
assert result.security is not None
service.security_badge_service.build_security_summary.assert_called_once_with(user)
class TestGetDashboardFilterBinding:
"""get_dashboard_filter_binding — dashboard filter fields only."""
@pytest.fixture
def service(self):
from src.services.profile_preference_service import ProfilePreferenceService
db = MagicMock()
config_mgr = MagicMock()
with patch("src.services.profile_preference_service.AuthRepository") as mock_auth_repo_cls, \
patch("src.services.profile_preference_service.EncryptionManager"), \
patch("src.services.profile_preference_service.SecurityBadgeService"):
mock_auth = MagicMock()
mock_auth_repo_cls.return_value = mock_auth
svc = ProfilePreferenceService(db, config_mgr)
svc.auth_repository = mock_auth
yield svc
def test_no_preference(self, service):
service.auth_repository.get_user_dashboard_preference.return_value = None
user = make_user()
result = service.get_dashboard_filter_binding(user)
assert result["superset_username"] is None
assert result["superset_username_normalized"] is None
assert result["show_only_my_dashboards"] is False
assert result["show_only_slug_dashboards"] is True
def test_with_preference(self, service):
pref = make_preference_row(
superset_username=" Admin ",
show_only_my_dashboards=True,
show_only_slug_dashboards=False,
)
service.auth_repository.get_user_dashboard_preference.return_value = pref
user = make_user()
result = service.get_dashboard_filter_binding(user)
assert result["superset_username"] == "Admin"
assert result["superset_username_normalized"] == "admin"
assert result["show_only_my_dashboards"] is True
assert result["show_only_slug_dashboards"] is False
class TestUpdateMyPreference:
"""update_my_preference — validate and persist preference."""
@pytest.fixture
def _make_security_summary(self):
from src.schemas.profile import ProfileSecuritySummary
return ProfileSecuritySummary(read_only=True, auth_source="database")
@pytest.fixture
def service(self, _make_security_summary):
from src.services.profile_preference_service import ProfilePreferenceService
db = MagicMock()
config_mgr = MagicMock()
with patch("src.services.profile_preference_service.AuthRepository") as mock_auth_repo_cls, \
patch("src.services.profile_preference_service.EncryptionManager") as mock_enc_cls, \
patch("src.services.profile_preference_service.SecurityBadgeService") as mock_badge_cls:
mock_auth = MagicMock()
mock_auth_repo_cls.return_value = mock_auth
mock_enc = MagicMock()
mock_enc.encrypt.return_value = "encrypted-token"
mock_enc.decrypt.return_value = "decrypted-token"
mock_enc_cls.return_value = mock_enc
mock_badge = MagicMock()
mock_badge.build_security_summary.return_value = _make_security_summary
mock_badge_cls.return_value = mock_badge
svc = ProfilePreferenceService(db, config_mgr)
svc.auth_repository = mock_auth
svc.encryption = mock_enc
svc.security_badge_service = mock_badge
yield svc
def test_cross_user_blocked(self, service):
from src.services.profile_utils import ProfileAuthorizationError
user = make_user("user-1")
payload = MagicMock(spec=ProfilePreferenceUpdateRequest)
payload.superset_username = None
payload.show_only_my_dashboards = None
with pytest.raises(ProfileAuthorizationError):
service.update_my_preference(user, payload, target_user_id="user-other")
def test_validation_error(self, service):
from src.services.profile_utils import ProfileValidationError
service.auth_repository.get_user_dashboard_preference.return_value = None
user = make_user("user-1")
payload = MagicMock(spec=ProfilePreferenceUpdateRequest)
payload.superset_username = "admin user" # has space
payload.show_only_my_dashboards = None
payload.show_only_slug_dashboards = None
payload.git_username = None
payload.git_email = None
payload.git_personal_access_token = None
payload.start_page = None
payload.auto_open_task_drawer = None
payload.dashboards_table_density = "spacious" # invalid
payload.telegram_id = None
payload.email_address = None
payload.notify_on_fail = None
payload.model_fields_set = {"superset_username", "dashboards_table_density"}
with pytest.raises(ProfileValidationError) as exc:
service.update_my_preference(user, payload)
assert len(exc.value.errors) >= 1
def test_successful_update_new_preference(self, service):
service.auth_repository.get_user_dashboard_preference.return_value = None
saved_pref = make_preference_row(
superset_username="admin",
superset_username_normalized="admin",
show_only_my_dashboards=True,
git_personal_access_token_encrypted="encrypted-token",
)
service.auth_repository.save_user_dashboard_preference.return_value = saved_pref
user = make_user("user-1")
payload = MagicMock(spec=ProfilePreferenceUpdateRequest)
payload.superset_username = "admin"
payload.show_only_my_dashboards = True
payload.show_only_slug_dashboards = None
payload.git_username = None
payload.git_email = None
payload.git_personal_access_token = "new-token"
payload.start_page = None
payload.auto_open_task_drawer = None
payload.dashboards_table_density = None
payload.telegram_id = None
payload.email_address = None
payload.notify_on_fail = None
payload.model_fields_set = {"superset_username", "show_only_my_dashboards", "git_personal_access_token"}
result = service.update_my_preference(user, payload)
assert result.status == "success"
assert result.preference.show_only_my_dashboards is True
assert result.preference.has_git_personal_access_token is True
service.encryption.encrypt.assert_called_once_with("new-token")
def test_clear_git_token(self, service):
# When git_personal_access_token is empty string, clear existing token
existing = make_preference_row(
git_personal_access_token_encrypted="existing-encrypted",
)
service.auth_repository.get_user_dashboard_preference.return_value = existing
saved_pref = make_preference_row(
git_personal_access_token_encrypted=None,
)
service.auth_repository.save_user_dashboard_preference.return_value = saved_pref
user = make_user("user-1")
payload = MagicMock(spec=ProfilePreferenceUpdateRequest)
payload.superset_username = None
payload.show_only_my_dashboards = None
payload.show_only_slug_dashboards = None
payload.git_username = None
payload.git_email = None
payload.git_personal_access_token = ""
payload.start_page = None
payload.auto_open_task_drawer = None
payload.dashboards_table_density = None
payload.telegram_id = None
payload.email_address = None
payload.notify_on_fail = None
payload.model_fields_set = {"git_personal_access_token"}
result = service.update_my_preference(user, payload)
assert result.status == "success"
def test_update_existing_fields(self, service):
existing = make_preference_row(
superset_username="old_user",
superset_username_normalized="old_user",
start_page="reports",
dashboards_table_density="compact",
telegram_id="@old",
email_address="old@x.com",
notify_on_fail=False,
)
service.auth_repository.get_user_dashboard_preference.return_value = existing
saved_pref = make_preference_row(
superset_username="new_user",
superset_username_normalized="new_user",
)
service.auth_repository.save_user_dashboard_preference.return_value = saved_pref
user = make_user("user-1")
payload = MagicMock(spec=ProfilePreferenceUpdateRequest)
payload.superset_username = "new_user"
payload.show_only_my_dashboards = None
payload.show_only_slug_dashboards = None
payload.git_username = None
payload.git_email = None
payload.git_personal_access_token = None
payload.start_page = None
payload.auto_open_task_drawer = None
payload.dashboards_table_density = None
payload.telegram_id = "@new_telegram"
payload.email_address = None
payload.notify_on_fail = None
payload.model_fields_set = {"superset_username", "telegram_id"}
result = service.update_my_preference(user, payload)
assert result.status == "success"
def test_all_fields_update(self, service):
"""Cover all provided_fields branches: show_only_slug, git_username, git_email,
start_page, auto_open_task_drawer, email_address, notify_on_fail."""
existing = make_preference_row(
superset_username="old", superset_username_normalized="old",
show_only_my_dashboards=False, show_only_slug_dashboards=False,
git_username="old_git", git_email="old@x.com",
start_page="reports", auto_open_task_drawer=False,
dashboards_table_density="compact",
telegram_id="@old", email_address="old@x.com", notify_on_fail=False,
)
service.auth_repository.get_user_dashboard_preference.return_value = existing
saved_pref = make_preference_row(
superset_username="new", superset_username_normalized="new",
)
service.auth_repository.save_user_dashboard_preference.return_value = saved_pref
user = make_user("user-1")
payload = MagicMock(spec=ProfilePreferenceUpdateRequest)
payload.superset_username = None
payload.show_only_my_dashboards = True
payload.show_only_slug_dashboards = True
payload.git_username = "new_git"
payload.git_email = "new@x.com"
payload.git_personal_access_token = None
payload.start_page = "datasets"
payload.auto_open_task_drawer = True
payload.dashboards_table_density = "compact"
payload.telegram_id = None
payload.email_address = "new_email@x.com"
payload.notify_on_fail = True
payload.model_fields_set = {
"show_only_my_dashboards", "show_only_slug_dashboards",
"git_username", "git_email", "start_page",
"auto_open_task_drawer", "email_address", "notify_on_fail",
}
result = service.update_my_preference(user, payload)
assert result.status == "success"
class TestToPreferencePayload:
"""_to_preference_payload — ORM row to API DTO."""
@pytest.fixture
def service(self):
from src.services.profile_preference_service import ProfilePreferenceService
db = MagicMock()
config_mgr = MagicMock()
with patch("src.services.profile_preference_service.AuthRepository"), \
patch("src.services.profile_preference_service.EncryptionManager") as mock_enc_cls, \
patch("src.services.profile_preference_service.SecurityBadgeService"):
mock_enc = MagicMock()
mock_enc.decrypt.return_value = "my-secret-token"
mock_enc_cls.return_value = mock_enc
svc = ProfilePreferenceService(db, config_mgr)
svc.encryption = mock_enc
yield svc
def test_maps_all_fields(self, service):
pref = make_preference_row(
superset_username="admin",
superset_username_normalized="admin",
git_username="jdoe",
git_email="j@x.com",
git_personal_access_token_encrypted="encrypted-val",
start_page="datasets",
telegram_id="@admin",
email_address="admin@x.com",
)
result = service._to_preference_payload(pref, "user-1")
assert result.user_id == "user-1"
assert result.superset_username == "admin"
assert result.git_username == "jdoe"
assert result.git_email == "j@x.com"
assert result.has_git_personal_access_token is True
assert result.start_page == "datasets"
assert result.telegram_id == "@admin"
assert result.email_address == "admin@x.com"
def test_no_token(self, service):
pref = make_preference_row(
git_personal_access_token_encrypted=None,
)
result = service._to_preference_payload(pref, "user-1")
assert result.has_git_personal_access_token is False
assert result.git_personal_access_token_masked is None
def test_decryption_failure_fallback(self, service):
service.encryption.decrypt.side_effect = Exception("Decrypt failed")
pref = make_preference_row(
git_personal_access_token_encrypted="corrupted",
)
result = service._to_preference_payload(pref, "user-1")
assert result.has_git_personal_access_token is True
assert result.git_personal_access_token_masked == "***"
class TestGetOrCreatePreferenceRow:
"""_get_or_create_preference_row — returns existing or new."""
@pytest.fixture
def service(self):
from src.services.profile_preference_service import ProfilePreferenceService
db = MagicMock()
config_mgr = MagicMock()
with patch("src.services.profile_preference_service.AuthRepository") as mock_auth_repo_cls, \
patch("src.services.profile_preference_service.EncryptionManager"), \
patch("src.services.profile_preference_service.SecurityBadgeService"):
mock_auth = MagicMock()
mock_auth_repo_cls.return_value = mock_auth
svc = ProfilePreferenceService(db, config_mgr)
svc.auth_repository = mock_auth
yield svc
def test_existing_returns_row(self, service):
pref = make_preference_row()
service.auth_repository.get_user_dashboard_preference.return_value = pref
result = service._get_or_create_preference_row("user-1")
assert result is pref
def test_missing_creates_new(self, service):
from src.models.profile import UserDashboardPreference
service.auth_repository.get_user_dashboard_preference.return_value = None
result = service._get_or_create_preference_row("user-1")
assert isinstance(result, UserDashboardPreference)
assert result.user_id == "user-1"
class TestBuildDefaultPreference:
"""_build_default_preference — delegates to profile_utils."""
def test_delegates_correctly(self):
from src.services.profile_preference_service import ProfilePreferenceService
db = MagicMock()
config_mgr = MagicMock()
with patch("src.services.profile_preference_service.AuthRepository"), \
patch("src.services.profile_preference_service.EncryptionManager"), \
patch("src.services.profile_preference_service.SecurityBadgeService"), \
patch("src.services.profile_preference_service.build_default_preference") as mock_build:
mock_build.return_value = MagicMock()
svc = ProfilePreferenceService(db, config_mgr)
result = svc._build_default_preference("user-1")
assert result is mock_build.return_value
mock_build.assert_called_once_with("user-1")
# #endregion Test.ProfilePreferenceService