# #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