# #region Test.ProfileUtils [C:3] [TYPE Module] [SEMANTICS test,profile,utils,pure-functions] # @BRIEF Tests for profile_utils.py — sanitize, normalize, mask, validate, default preference, owner tokens, exceptions. # @RELATION BINDS_TO -> [ProfileUtils] # @TEST_EDGE: missing_field -> None/empty inputs handled gracefully # @TEST_EDGE: invalid_type -> Invalid values produce validation errors # @TEST_EDGE: external_fail -> N/A (pure functions, no external deps) from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).parent.parent / "src")) import pytest from src.services.profile_utils import ( EnvironmentNotFoundError, ProfileAuthorizationError, ProfileValidationError, build_default_preference, mask_secret_value, normalize_density, normalize_owner_tokens, normalize_start_page, normalize_username, sanitize_secret, sanitize_text, sanitize_username, validate_update_payload, ) def _validate(**overrides): defaults = dict( superset_username="admin", show_only_my_dashboards=False, git_email=None, start_page="dashboards", dashboards_table_density="comfortable", ) defaults.update(overrides) return validate_update_payload(**defaults) # --- sanitize_text --- # #region test_sanitize_text [C:2] [TYPE Function] # @BRIEF Verify sanitize_text with None, empty, whitespace, and valid inputs. @pytest.mark.parametrize("value, expected", [ (None, None), ("", None), (" ", None), (" hello ", "hello"), ("world", "world"), ]) def test_sanitize_text(value, expected): assert sanitize_text(value) == expected # #endregion test_sanitize_text # --- sanitize_secret --- # #region test_sanitize_secret [C:2] [TYPE Function] # @BRIEF Verify sanitize_secret with None, empty, whitespace, and valid secrets. @pytest.mark.parametrize("value, expected", [ (None, None), ("", None), (" ", None), (" my-token-abc ", "my-token-abc"), ]) def test_sanitize_secret(value, expected): assert sanitize_secret(value) == expected # #endregion test_sanitize_secret # --- sanitize_username --- # #region test_sanitize_username [C:2] [TYPE Function] # @BRIEF Verify sanitize_username delegates to sanitize_text. @pytest.mark.parametrize("value, expected", [ (None, None), (" admin ", "admin"), ]) def test_sanitize_username(value, expected): assert sanitize_username(value) == expected # #endregion test_sanitize_username # --- normalize_username --- # #region test_normalize_username [C:2] [TYPE Function] # @BRIEF Verify trim+lower normalization for actor matching. @pytest.mark.parametrize("value, expected", [ (None, None), ("", None), (" AdminUser ", "adminuser"), ("guest", "guest"), ]) def test_normalize_username(value, expected): assert normalize_username(value) == expected # #endregion test_normalize_username # --- normalize_start_page --- # #region test_normalize_start_page [C:2] [TYPE Function] # @BRIEF Verify start page alias mapping to canonical values. @pytest.mark.parametrize("value, expected", [ ("reports-logs", "reports"), ("dashboards", "dashboards"), ("datasets", "datasets"), ("reports", "reports"), ("settings", "dashboards"), (None, "dashboards"), ("DATASETS", "datasets"), ]) def test_normalize_start_page(value, expected): assert normalize_start_page(value) == expected # #endregion test_normalize_start_page # --- normalize_density --- # #region test_normalize_density [C:2] [TYPE Function] # @BRIEF Verify density alias mapping to canonical values. @pytest.mark.parametrize("value, expected", [ ("free", "comfortable"), ("compact", "compact"), ("comfortable", "comfortable"), ("spacious", "comfortable"), (None, "comfortable"), ]) def test_normalize_density(value, expected): assert normalize_density(value) == expected # #endregion test_normalize_density # --- mask_secret_value --- # #region test_mask_secret_value [C:2] [TYPE Function] # @BRIEF Verify safe display masking for secrets of various lengths. @pytest.mark.parametrize("value, expected", [ (None, None), ("", None), ("ab", "***"), ("abcd", "***"), ("my-token-value", "my***ue"), (" ab ", "***"), ]) def test_mask_secret_value(value, expected): assert mask_secret_value(value) == expected # #endregion test_mask_secret_value # --- validate_update_payload --- # #region test_validate_valid_payload [C:2] [TYPE Function] # @BRIEF All-valid input returns empty error list. def test_validate_valid_payload(): assert _validate() == [] # #endregion test_validate_valid_payload # #region test_validate_username_with_space [C:2] [TYPE Function] # @BRIEF Username containing space produces validation error. def test_validate_username_with_space(): errors = _validate(superset_username="admin user") assert any("space" in e.lower() for e in errors) # #endregion test_validate_username_with_space # #region test_validate_show_only_requires_username [C:2] [TYPE Function] # @BRIEF show_only_my_dashboards=True without username produces error. def test_validate_show_only_requires_username(): errors = _validate(superset_username=None, show_only_my_dashboards=True) assert any("username" in e.lower() and "required" in e.lower() for e in errors) # #endregion test_validate_show_only_requires_username # #region test_validate_git_email_missing_at [C:2] [TYPE Function] # @BRIEF Git email without @ produces validation error. def test_validate_git_email_missing_at(): errors = _validate(git_email="not-an-email") assert any("git email" in e.lower() for e in errors) # #endregion test_validate_git_email_missing_at # #region test_validate_git_email_with_space [C:2] [TYPE Function] # @BRIEF Git email containing space produces validation error. def test_validate_git_email_with_space(): errors = _validate(git_email="admin @x.com") assert any("git email" in e.lower() for e in errors) # #endregion test_validate_git_email_with_space # #region test_validate_git_email_starts_with_at [C:2] [TYPE Function] # @BRIEF Git email starting with @ produces validation error. def test_validate_git_email_starts_with_at(): errors = _validate(git_email="@example.com") assert any("git email" in e.lower() for e in errors) # #endregion test_validate_git_email_starts_with_at # #region test_validate_git_email_ends_with_at [C:2] [TYPE Function] # @BRIEF Git email ending with @ produces validation error. def test_validate_git_email_ends_with_at(): errors = _validate(git_email="admin@") assert any("git email" in e.lower() for e in errors) # #endregion test_validate_git_email_ends_with_at # #region test_validate_invalid_start_page [C:2] [TYPE Function] # @BRIEF Unsupported start_page value produces validation error. def test_validate_invalid_start_page(): errors = _validate(start_page="settings") assert any("start page" in e.lower() for e in errors) # #endregion test_validate_invalid_start_page # #region test_validate_invalid_density [C:2] [TYPE Function] # @BRIEF Unsupported density value produces validation error. def test_validate_invalid_density(): errors = _validate(dashboards_table_density="spacious") assert any("density" in e.lower() for e in errors) # #endregion test_validate_invalid_density # #region test_validate_invalid_notification_email [C:2] [TYPE Function] # @BRIEF Invalid notification email variants all produce validation error. @pytest.mark.parametrize("bad_email", ["bad-email", "@x.com", "a@", "a b@c.com"]) def test_validate_invalid_notification_email(bad_email): errors = _validate(email_address=bad_email) assert any("notification email" in e.lower() for e in errors) # #endregion test_validate_invalid_notification_email # #region test_validate_valid_notification_email [C:2] [TYPE Function] # @BRIEF Valid notification email produces no notification error. def test_validate_valid_notification_email(): errors = _validate(email_address="user@example.com") assert not any("notification email" in e.lower() for e in errors) # #endregion test_validate_valid_notification_email # #region test_validate_multiple_errors [C:2] [TYPE Function] # @BRIEF Multiple invalid fields produce multiple accumulated errors. def test_validate_multiple_errors(): errors = _validate( superset_username="admin user", show_only_my_dashboards=True, git_email="bad", start_page="unknown", dashboards_table_density="spacious", ) assert len(errors) >= 4 # #endregion test_validate_multiple_errors # --- build_default_preference --- # #region test_build_default_preference [C:2] [TYPE Function] # @BRIEF Verify default preference DTO has correct initial values for all fields. def test_build_default_preference(): pref = build_default_preference("user-1") assert pref.user_id == "user-1" assert pref.superset_username is None assert pref.superset_username_normalized is None assert pref.show_only_my_dashboards is False assert pref.show_only_slug_dashboards is True assert pref.git_username is None assert pref.git_email is None assert pref.has_git_personal_access_token is False assert pref.git_personal_access_token_masked is None assert pref.start_page == "dashboards" assert pref.auto_open_task_drawer is True assert pref.dashboards_table_density == "comfortable" assert pref.telegram_id is None assert pref.email_address is None assert pref.notify_on_fail is True assert pref.created_at == pref.updated_at # #endregion test_build_default_preference # --- normalize_owner_tokens --- # #region test_owner_tokens_none [C:2] [TYPE Function] # @BRIEF None input returns empty list. def test_owner_tokens_none(): assert normalize_owner_tokens(None) == [] # #endregion test_owner_tokens_none # #region test_owner_tokens_empty [C:2] [TYPE Function] # @BRIEF Empty iterable returns empty list. def test_owner_tokens_empty(): assert normalize_owner_tokens([]) == [] # #endregion test_owner_tokens_empty # #region test_owner_tokens_dict_username [C:2] [TYPE Function] # @BRIEF Dict with username field produces normalized token. def test_owner_tokens_dict_username(): tokens = normalize_owner_tokens([{"username": "AdminUser"}]) assert "adminuser" in tokens # #endregion test_owner_tokens_dict_username # #region test_owner_tokens_dict_names [C:2] [TYPE Function] # @BRIEF Dict with first/last name produces individual and combined tokens. def test_owner_tokens_dict_names(): tokens = normalize_owner_tokens([{"first_name": "John", "last_name": "Doe"}]) assert "john" in tokens assert "doe" in tokens assert "john_doe" in tokens # #endregion test_owner_tokens_dict_names # #region test_owner_tokens_string [C:2] [TYPE Function] # @BRIEF Plain string items are normalized directly. def test_owner_tokens_string(): tokens = normalize_owner_tokens([" Admin "]) assert "admin" in tokens # #endregion test_owner_tokens_string # #region test_owner_tokens_dedup [C:2] [TYPE Function] # @BRIEF Duplicate tokens after normalization are collapsed. def test_owner_tokens_dedup(): tokens = normalize_owner_tokens(["admin", " Admin "]) assert tokens == ["admin"] # #endregion test_owner_tokens_dedup # #region test_owner_tokens_skip_empty [C:2] [TYPE Function] # @BRIEF Empty/whitespace/None items produce no tokens. def test_owner_tokens_skip_empty(): assert normalize_owner_tokens(["", " ", None]) == [] # #endregion test_owner_tokens_skip_empty # #region test_owner_tokens_dict_email [C:2] [TYPE Function] # @BRIEF Dict with email field produces normalized email token. def test_owner_tokens_dict_email(): tokens = normalize_owner_tokens([{"email": "User@Example.com"}]) assert "user@example.com" in tokens # #endregion test_owner_tokens_dict_email # #region test_owner_tokens_dict_multi [C:2] [TYPE Function] # @BRIEF Dict with multiple fields produces all valid candidate tokens. def test_owner_tokens_dict_multi(): owners = [{"username": "jdoe", "first_name": "John", "last_name": "Doe", "email": "john@example.com"}] tokens = normalize_owner_tokens(owners) assert "jdoe" in tokens assert "john" in tokens assert "doe" in tokens assert "john@example.com" in tokens # #endregion test_owner_tokens_dict_multi # --- exception classes --- # #region test_exception_profile_validation_error [C:2] [TYPE Function] # @BRIEF ProfileValidationError stores errors list and has expected message. def test_exception_profile_validation_error(): err = ProfileValidationError(["field1 bad", "field2 bad"]) assert err.errors == ["field1 bad", "field2 bad"] assert "validation failed" in str(err).lower() with pytest.raises(ProfileValidationError): raise ProfileValidationError(["test"]) # #endregion test_exception_profile_validation_error # #region test_exception_environment_not_found [C:2] [TYPE Function] # @BRIEF EnvironmentNotFoundError can be raised and caught as Exception. def test_exception_environment_not_found(): with pytest.raises(EnvironmentNotFoundError): raise EnvironmentNotFoundError("env-xyz") # #endregion test_exception_environment_not_found # #region test_exception_profile_authorization [C:2] [TYPE Function] # @BRIEF ProfileAuthorizationError can be raised and caught as Exception. def test_exception_profile_authorization(): with pytest.raises(ProfileAuthorizationError): raise ProfileAuthorizationError("cross-user") # #endregion test_exception_profile_authorization # #endregion Test.ProfileUtils