- 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)
466 lines
20 KiB
Python
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
|