From 036279c35a962244d54c51c72f15c7720c46d9dc Mon Sep 17 00:00:00 2001 From: geoffg-sentry <165922362+geoffg-sentry@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:17:37 -0400 Subject: [PATCH 1/5] Add filtering to app and org display names --- .../api/serializers/models/organization.py | 7 ++ .../sentry_apps/api/parsers/sentry_app.py | 7 ++ src/sentry/utils/display_name_filter.py | 74 +++++++++++++++++++ .../core/endpoints/test_organization_index.py | 13 ++++ .../sentry/utils/test_display_name_filter.py | 69 +++++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 src/sentry/utils/display_name_filter.py create mode 100644 tests/sentry/utils/test_display_name_filter.py diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index d1310c7c784530..545820d908b50d 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -175,6 +175,13 @@ def validate_name(self, value: str) -> str: raise serializers.ValidationError( "Organization name cannot contain URL schemes (e.g. http:// or https://)." ) + + from sentry.utils.display_name_filter import check_spam_display_name + + spam_error = check_spam_display_name(value) + if spam_error: + raise serializers.ValidationError(spam_error) + return value def validate_slug(self, value: str) -> str: diff --git a/src/sentry/sentry_apps/api/parsers/sentry_app.py b/src/sentry/sentry_apps/api/parsers/sentry_app.py index 64f473eed51e46..67c8c5d6f0271f 100644 --- a/src/sentry/sentry_apps/api/parsers/sentry_app.py +++ b/src/sentry/sentry_apps/api/parsers/sentry_app.py @@ -164,6 +164,13 @@ def validate_name(self, value): max_length = 64 - UUID_CHARS_IN_SLUG - 1 # -1 comes from the - before the UUID bit if len(value) > max_length: raise ValidationError("Cannot exceed %d characters" % max_length) + + from sentry.utils.display_name_filter import check_spam_display_name + + spam_error = check_spam_display_name(value) + if spam_error: + raise ValidationError(spam_error) + return value def validate_allowedOrigins(self, value): diff --git a/src/sentry/utils/display_name_filter.py b/src/sentry/utils/display_name_filter.py new file mode 100644 index 00000000000000..92d875fc0aea2e --- /dev/null +++ b/src/sentry/utils/display_name_filter.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from collections.abc import Callable + +CURRENCY_SIGNALS: list[str] = [ + "$", + "\U0001f4b2", # 💲 Heavy Dollar Sign + "\U0001f4b0", # 💰 Money Bag + "\U0001f4b5", # 💵 Dollar Banknote + "\U0001f48e", # 💎 Gem Stone + "\U0001fa99", # 🪙 Coin + "btc", + "eth", + "usdt", + "crypto", + "compensation", + "bitcoin", + "ethereum", + "litecoin", + "ltc", + "xrp", + "doge", + "dogecoin", + "bnb", + "solana", + "sol", + "airdrop", +] + +CTA_VERBS: list[str] = ["click", "claim", "collect", "withdraw", "act", "pay"] +CTA_URGENCY: list[str] = ["now", "here", "your", "link"] + +SHORT_URL_SIGNALS: list[str] = [ + "2g.tel", + "bit.ly", + "t.co", + "tinyurl", + "rb.gy", + "cutt.ly", + "shorturl", + "is.gd", + "v.gd", + "ow.ly", + "bl.ink", +] + + +def _has_any(lowered: str, signals: list[str]) -> bool: + return any(s in lowered for s in signals) + + +def _has_cta(lowered: str) -> bool: + return _has_any(lowered, CTA_VERBS) and _has_any(lowered, CTA_URGENCY) + + +_CATEGORIES: list[tuple[str, Callable[[str], bool]]] = [ + ("cryptocurrency terminology", lambda val: _has_any(val, CURRENCY_SIGNALS)), + ("call-to-action phrases", _has_cta), + ("URL shortener domains", lambda val: _has_any(val, SHORT_URL_SIGNALS)), +] + + +def check_spam_display_name(name: str) -> str | None: + """Check if a user-controlled display name matches known spam patterns. + + Returns a user-facing error string listing the matched categories + if 2+ distinct signal categories fire, or None if the name is clean. + """ + lowered = name.lower() + matched_labels: list[str] = [label for label, check in _CATEGORIES if check(lowered)] + if len(matched_labels) >= 2: + joined = " and ".join(matched_labels) + return f"This name contains disallowed content ({joined}). Please choose a different name." + return None diff --git a/tests/sentry/core/endpoints/test_organization_index.py b/tests/sentry/core/endpoints/test_organization_index.py index 67602f65306fcb..a2d15e4b6f4f43 100644 --- a/tests/sentry/core/endpoints/test_organization_index.py +++ b/tests/sentry/core/endpoints/test_organization_index.py @@ -272,6 +272,19 @@ def test_name_with_url_scheme_rejected(self) -> None: ) self.get_error_response(name="http://evil.com", slug="legit-slug-2", status_code=400) + def test_name_with_spam_signals_rejected(self) -> None: + response = self.get_error_response( + name="Win $50 ETH bit.ly/offer Claim Now", + slug="spam-org", + status_code=400, + ) + assert "disallowed content" in str(response.data) + + def test_name_with_single_signal_allowed(self) -> None: + response = self.get_success_response(name="BTC Analytics", slug="btc-analytics") + org = Organization.objects.get(id=response.data["id"]) + assert org.name == "BTC Analytics" + def test_name_with_periods_allowed(self) -> None: response = self.get_success_response(name="Acme Inc.", slug="acme-inc") org = Organization.objects.get(id=response.data["id"]) diff --git a/tests/sentry/utils/test_display_name_filter.py b/tests/sentry/utils/test_display_name_filter.py new file mode 100644 index 00000000000000..45600e4bb089d0 --- /dev/null +++ b/tests/sentry/utils/test_display_name_filter.py @@ -0,0 +1,69 @@ +from sentry.utils.display_name_filter import check_spam_display_name + + +class TestCheckSpamDisplayName: + def test_clean_name_passes(self) -> None: + assert check_spam_display_name("My Company Inc.") is None + + def test_single_currency_signal_passes(self) -> None: + assert check_spam_display_name("BTC Analytics") is None + + def test_single_cta_verb_passes(self) -> None: + assert check_spam_display_name("Click Studios") is None + + def test_single_cta_urgency_passes(self) -> None: + assert check_spam_display_name("Do It Now Labs") is None + + def test_cta_verb_plus_urgency_alone_passes(self) -> None: + assert check_spam_display_name("Click Here Studios") is None + + def test_single_shorturl_signal_passes(self) -> None: + assert check_spam_display_name("bit.ly team") is None + + def test_currency_plus_cta_rejected(self) -> None: + result = check_spam_display_name("Free BTC - Click Here") + assert result is not None + assert "cryptocurrency terminology" in result + assert "call-to-action phrases" in result + + def test_currency_plus_shorturl_rejected(self) -> None: + result = check_spam_display_name("Earn $100 via 2g.tel/promo") + assert result is not None + assert "cryptocurrency terminology" in result + assert "URL shortener domains" in result + + def test_cta_plus_shorturl_rejected(self) -> None: + result = check_spam_display_name("Click Here: bit.ly/free") + assert result is not None + assert "call-to-action phrases" in result + assert "URL shortener domains" in result + + def test_all_three_categories_rejected(self) -> None: + result = check_spam_display_name("Win $50 ETH bit.ly/offer Claim Now") + assert result is not None + + def test_case_insensitive(self) -> None: + result = check_spam_display_name("FREE BTC - CLICK HERE") + assert result is not None + + def test_currency_emoji_detected(self) -> None: + result = check_spam_display_name( + "\U0001f4b2Compensation Btc: 2g.tel/x Click Your Pay Link." + ) + assert result is not None + + def test_single_currency_emoji_passes(self) -> None: + assert check_spam_display_name("My \U0001f4b0 Company") is None + + def test_cta_novel_combo_rejected(self) -> None: + result = check_spam_display_name("Withdraw Now - Free BTC") + assert result is not None + + def test_empty_string_passes(self) -> None: + assert check_spam_display_name("") is None + + def test_error_message_format(self) -> None: + result = check_spam_display_name("Free BTC - Click Here") + assert result is not None + assert "disallowed content" in result + assert "Please choose a different name" in result From 49788c73c76abefb341af9960e62fc34cd212886 Mon Sep 17 00:00:00 2001 From: geoffg-sentry <165922362+geoffg-sentry@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:20:50 -0400 Subject: [PATCH 2/5] comment brevity --- src/sentry/utils/display_name_filter.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/sentry/utils/display_name_filter.py b/src/sentry/utils/display_name_filter.py index 92d875fc0aea2e..bb7a595be036e7 100644 --- a/src/sentry/utils/display_name_filter.py +++ b/src/sentry/utils/display_name_filter.py @@ -61,11 +61,7 @@ def _has_cta(lowered: str) -> bool: def check_spam_display_name(name: str) -> str | None: - """Check if a user-controlled display name matches known spam patterns. - - Returns a user-facing error string listing the matched categories - if 2+ distinct signal categories fire, or None if the name is clean. - """ + """Return an error string if the name matches 2+ spam categories, else None.""" lowered = name.lower() matched_labels: list[str] = [label for label, check in _CATEGORIES if check(lowered)] if len(matched_labels) >= 2: From 9c70b12047d6804e44777f8700ab9e74d57ce38c Mon Sep 17 00:00:00 2001 From: geoffg-sentry <165922362+geoffg-sentry@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:34:32 -0400 Subject: [PATCH 3/5] better word boundaries and trailing slashes --- src/sentry/utils/display_name_filter.py | 57 ++++++++++++++----- .../sentry/utils/test_display_name_filter.py | 23 +++++++- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/sentry/utils/display_name_filter.py b/src/sentry/utils/display_name_filter.py index bb7a595be036e7..0591f8b68de998 100644 --- a/src/sentry/utils/display_name_filter.py +++ b/src/sentry/utils/display_name_filter.py @@ -31,32 +31,59 @@ CTA_URGENCY: list[str] = ["now", "here", "your", "link"] SHORT_URL_SIGNALS: list[str] = [ - "2g.tel", - "bit.ly", - "t.co", - "tinyurl", - "rb.gy", - "cutt.ly", - "shorturl", - "is.gd", - "v.gd", - "ow.ly", - "bl.ink", + "2g.tel/", + "bit.ly/", + "t.co/", + "tinyurl.com/", + "rb.gy/", + "cutt.ly/", + "shorturl.at/", + "is.gd/", + "v.gd/", + "ow.ly/", + "bl.ink/", ] -def _has_any(lowered: str, signals: list[str]) -> bool: +def _is_word_at(text: str, pos: int, length: int) -> bool: + """Check that the match at text[pos:pos+length] is bounded by non-alphanumeric chars.""" + if pos > 0 and text[pos - 1].isalnum(): + return False + end = pos + length + if end < len(text) and text[end].isalnum(): + return False + return True + + +def _has_substring(lowered: str, signals: list[str]) -> bool: return any(s in lowered for s in signals) +def _has_word(lowered: str, signals: list[str]) -> bool: + for signal in signals: + pos = lowered.find(signal) + while pos != -1: + if _is_word_at(lowered, pos, len(signal)): + return True + pos = lowered.find(signal, pos + 1) + return False + + +def _has_signal(lowered: str, signals: list[str]) -> bool: + """Use word-boundary matching for alphabetic signals, substring for the rest.""" + alpha = [s for s in signals if s.isalpha()] + other = [s for s in signals if not s.isalpha()] + return _has_word(lowered, alpha) or _has_substring(lowered, other) + + def _has_cta(lowered: str) -> bool: - return _has_any(lowered, CTA_VERBS) and _has_any(lowered, CTA_URGENCY) + return _has_signal(lowered, CTA_VERBS) and _has_signal(lowered, CTA_URGENCY) _CATEGORIES: list[tuple[str, Callable[[str], bool]]] = [ - ("cryptocurrency terminology", lambda val: _has_any(val, CURRENCY_SIGNALS)), + ("cryptocurrency terminology", lambda val: _has_signal(val, CURRENCY_SIGNALS)), ("call-to-action phrases", _has_cta), - ("URL shortener domains", lambda val: _has_any(val, SHORT_URL_SIGNALS)), + ("URL shortener domains", lambda val: _has_signal(val, SHORT_URL_SIGNALS)), ] diff --git a/tests/sentry/utils/test_display_name_filter.py b/tests/sentry/utils/test_display_name_filter.py index 45600e4bb089d0..7ca3de4e317454 100644 --- a/tests/sentry/utils/test_display_name_filter.py +++ b/tests/sentry/utils/test_display_name_filter.py @@ -18,7 +18,7 @@ def test_cta_verb_plus_urgency_alone_passes(self) -> None: assert check_spam_display_name("Click Here Studios") is None def test_single_shorturl_signal_passes(self) -> None: - assert check_spam_display_name("bit.ly team") is None + assert check_spam_display_name("bit.ly/promo team") is None def test_currency_plus_cta_rejected(self) -> None: result = check_spam_display_name("Free BTC - Click Here") @@ -32,12 +32,18 @@ def test_currency_plus_shorturl_rejected(self) -> None: assert "cryptocurrency terminology" in result assert "URL shortener domains" in result + def test_shorturl_without_slash_not_matched(self) -> None: + assert check_spam_display_name("support.com Solutions") is None + def test_cta_plus_shorturl_rejected(self) -> None: result = check_spam_display_name("Click Here: bit.ly/free") assert result is not None assert "call-to-action phrases" in result assert "URL shortener domains" in result + def test_bare_shorturl_domain_without_path_passes(self) -> None: + assert check_spam_display_name("Free BTC bit.ly") is None + def test_all_three_categories_rejected(self) -> None: result = check_spam_display_name("Win $50 ETH bit.ly/offer Claim Now") assert result is not None @@ -59,6 +65,21 @@ def test_cta_novel_combo_rejected(self) -> None: result = check_spam_display_name("Withdraw Now - Free BTC") assert result is not None + def test_substring_sol_in_solutions_not_matched(self) -> None: + assert check_spam_display_name("Impactful Solutions") is None + + def test_substring_eth_in_method_not_matched(self) -> None: + assert check_spam_display_name("Method Analytics") is None + + def test_substring_act_in_contact_not_matched(self) -> None: + assert check_spam_display_name("Contact Knowledge Solutions") is None + + def test_substring_now_in_knowledge_not_matched(self) -> None: + assert check_spam_display_name("Knowledge Now Platform") is None + + def test_substring_here_in_where_not_matched(self) -> None: + assert check_spam_display_name("Where We Shine") is None + def test_empty_string_passes(self) -> None: assert check_spam_display_name("") is None From 0119ca13dc5866bb034d848e2679fa9c7d5e9d9e Mon Sep 17 00:00:00 2001 From: geoffg-sentry <165922362+geoffg-sentry@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:45:01 -0400 Subject: [PATCH 4/5] simplify error messaging --- src/sentry/utils/display_name_filter.py | 7 +++---- tests/sentry/utils/test_display_name_filter.py | 15 +++------------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/sentry/utils/display_name_filter.py b/src/sentry/utils/display_name_filter.py index 0591f8b68de998..1fe3f5a44d4e4a 100644 --- a/src/sentry/utils/display_name_filter.py +++ b/src/sentry/utils/display_name_filter.py @@ -90,8 +90,7 @@ def _has_cta(lowered: str) -> bool: def check_spam_display_name(name: str) -> str | None: """Return an error string if the name matches 2+ spam categories, else None.""" lowered = name.lower() - matched_labels: list[str] = [label for label, check in _CATEGORIES if check(lowered)] - if len(matched_labels) >= 2: - joined = " and ".join(matched_labels) - return f"This name contains disallowed content ({joined}). Please choose a different name." + matched = sum(1 for _, check in _CATEGORIES if check(lowered)) + if matched >= 2: + return "This name contains disallowed content. Please choose a different name." return None diff --git a/tests/sentry/utils/test_display_name_filter.py b/tests/sentry/utils/test_display_name_filter.py index 7ca3de4e317454..810f98e2090487 100644 --- a/tests/sentry/utils/test_display_name_filter.py +++ b/tests/sentry/utils/test_display_name_filter.py @@ -21,25 +21,16 @@ def test_single_shorturl_signal_passes(self) -> None: assert check_spam_display_name("bit.ly/promo team") is None def test_currency_plus_cta_rejected(self) -> None: - result = check_spam_display_name("Free BTC - Click Here") - assert result is not None - assert "cryptocurrency terminology" in result - assert "call-to-action phrases" in result + assert check_spam_display_name("Free BTC - Click Here") is not None def test_currency_plus_shorturl_rejected(self) -> None: - result = check_spam_display_name("Earn $100 via 2g.tel/promo") - assert result is not None - assert "cryptocurrency terminology" in result - assert "URL shortener domains" in result + assert check_spam_display_name("Earn $100 via 2g.tel/promo") is not None def test_shorturl_without_slash_not_matched(self) -> None: assert check_spam_display_name("support.com Solutions") is None def test_cta_plus_shorturl_rejected(self) -> None: - result = check_spam_display_name("Click Here: bit.ly/free") - assert result is not None - assert "call-to-action phrases" in result - assert "URL shortener domains" in result + assert check_spam_display_name("Click Here: bit.ly/free") is not None def test_bare_shorturl_domain_without_path_passes(self) -> None: assert check_spam_display_name("Free BTC bit.ly") is None From 1cba7e1faa02641f68c0522fee3038b6d30074be Mon Sep 17 00:00:00 2001 From: geoffg-sentry <165922362+geoffg-sentry@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:04:35 -0400 Subject: [PATCH 5/5] move import, fix return --- .../api/serializers/models/organization.py | 10 ++-- .../sentry_apps/api/parsers/sentry_app.py | 10 ++-- src/sentry/utils/display_name_filter.py | 8 +-- .../sentry/utils/test_display_name_filter.py | 60 ++++++++----------- 4 files changed, 37 insertions(+), 51 deletions(-) diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index 545820d908b50d..96c39a2c9739c6 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -89,6 +89,7 @@ from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser from sentry.users.services.user.service import user_service +from sentry.utils.display_name_filter import is_spam_display_name if TYPE_CHECKING: from sentry.api.serializers.models.project import OrganizationProjectResponse @@ -176,11 +177,10 @@ def validate_name(self, value: str) -> str: "Organization name cannot contain URL schemes (e.g. http:// or https://)." ) - from sentry.utils.display_name_filter import check_spam_display_name - - spam_error = check_spam_display_name(value) - if spam_error: - raise serializers.ValidationError(spam_error) + if is_spam_display_name(value): + raise serializers.ValidationError( + "This name contains disallowed content. Please choose a different name." + ) return value diff --git a/src/sentry/sentry_apps/api/parsers/sentry_app.py b/src/sentry/sentry_apps/api/parsers/sentry_app.py index 67c8c5d6f0271f..5c121d7b1d6f4e 100644 --- a/src/sentry/sentry_apps/api/parsers/sentry_app.py +++ b/src/sentry/sentry_apps/api/parsers/sentry_app.py @@ -11,6 +11,7 @@ from sentry.sentry_apps.api.parsers.schema import validate_ui_element_schema from sentry.sentry_apps.models.sentry_app import REQUIRED_EVENT_PERMISSIONS, UUID_CHARS_IN_SLUG from sentry.sentry_apps.utils.webhooks import VALID_EVENT_RESOURCES +from sentry.utils.display_name_filter import is_spam_display_name @extend_schema_field(build_typed_list(OpenApiTypes.STR)) @@ -165,11 +166,10 @@ def validate_name(self, value): if len(value) > max_length: raise ValidationError("Cannot exceed %d characters" % max_length) - from sentry.utils.display_name_filter import check_spam_display_name - - spam_error = check_spam_display_name(value) - if spam_error: - raise ValidationError(spam_error) + if is_spam_display_name(value): + raise ValidationError( + "This name contains disallowed content. Please choose a different name." + ) return value diff --git a/src/sentry/utils/display_name_filter.py b/src/sentry/utils/display_name_filter.py index 1fe3f5a44d4e4a..faf01b2d78df97 100644 --- a/src/sentry/utils/display_name_filter.py +++ b/src/sentry/utils/display_name_filter.py @@ -87,10 +87,8 @@ def _has_cta(lowered: str) -> bool: ] -def check_spam_display_name(name: str) -> str | None: - """Return an error string if the name matches 2+ spam categories, else None.""" +def is_spam_display_name(name: str) -> bool: + """Return True if the name matches 2+ spam signal categories.""" lowered = name.lower() matched = sum(1 for _, check in _CATEGORIES if check(lowered)) - if matched >= 2: - return "This name contains disallowed content. Please choose a different name." - return None + return matched >= 2 diff --git a/tests/sentry/utils/test_display_name_filter.py b/tests/sentry/utils/test_display_name_filter.py index 810f98e2090487..28099ba84be017 100644 --- a/tests/sentry/utils/test_display_name_filter.py +++ b/tests/sentry/utils/test_display_name_filter.py @@ -1,81 +1,69 @@ -from sentry.utils.display_name_filter import check_spam_display_name +from sentry.utils.display_name_filter import is_spam_display_name -class TestCheckSpamDisplayName: +class TestIsSpamDisplayName: def test_clean_name_passes(self) -> None: - assert check_spam_display_name("My Company Inc.") is None + assert not is_spam_display_name("My Company Inc.") def test_single_currency_signal_passes(self) -> None: - assert check_spam_display_name("BTC Analytics") is None + assert not is_spam_display_name("BTC Analytics") def test_single_cta_verb_passes(self) -> None: - assert check_spam_display_name("Click Studios") is None + assert not is_spam_display_name("Click Studios") def test_single_cta_urgency_passes(self) -> None: - assert check_spam_display_name("Do It Now Labs") is None + assert not is_spam_display_name("Do It Now Labs") def test_cta_verb_plus_urgency_alone_passes(self) -> None: - assert check_spam_display_name("Click Here Studios") is None + assert not is_spam_display_name("Click Here Studios") def test_single_shorturl_signal_passes(self) -> None: - assert check_spam_display_name("bit.ly/promo team") is None + assert not is_spam_display_name("bit.ly/promo team") def test_currency_plus_cta_rejected(self) -> None: - assert check_spam_display_name("Free BTC - Click Here") is not None + assert is_spam_display_name("Free BTC - Click Here") def test_currency_plus_shorturl_rejected(self) -> None: - assert check_spam_display_name("Earn $100 via 2g.tel/promo") is not None + assert is_spam_display_name("Earn $100 via 2g.tel/promo") def test_shorturl_without_slash_not_matched(self) -> None: - assert check_spam_display_name("support.com Solutions") is None + assert not is_spam_display_name("support.com Solutions") def test_cta_plus_shorturl_rejected(self) -> None: - assert check_spam_display_name("Click Here: bit.ly/free") is not None + assert is_spam_display_name("Click Here: bit.ly/free") def test_bare_shorturl_domain_without_path_passes(self) -> None: - assert check_spam_display_name("Free BTC bit.ly") is None + assert not is_spam_display_name("Free BTC bit.ly") def test_all_three_categories_rejected(self) -> None: - result = check_spam_display_name("Win $50 ETH bit.ly/offer Claim Now") - assert result is not None + assert is_spam_display_name("Win $50 ETH bit.ly/offer Claim Now") def test_case_insensitive(self) -> None: - result = check_spam_display_name("FREE BTC - CLICK HERE") - assert result is not None + assert is_spam_display_name("FREE BTC - CLICK HERE") def test_currency_emoji_detected(self) -> None: - result = check_spam_display_name( - "\U0001f4b2Compensation Btc: 2g.tel/x Click Your Pay Link." - ) - assert result is not None + assert is_spam_display_name("\U0001f4b2Compensation Btc: 2g.tel/x Click Your Pay Link.") def test_single_currency_emoji_passes(self) -> None: - assert check_spam_display_name("My \U0001f4b0 Company") is None + assert not is_spam_display_name("My \U0001f4b0 Company") def test_cta_novel_combo_rejected(self) -> None: - result = check_spam_display_name("Withdraw Now - Free BTC") - assert result is not None + assert is_spam_display_name("Withdraw Now - Free BTC") def test_substring_sol_in_solutions_not_matched(self) -> None: - assert check_spam_display_name("Impactful Solutions") is None + assert not is_spam_display_name("Impactful Solutions") def test_substring_eth_in_method_not_matched(self) -> None: - assert check_spam_display_name("Method Analytics") is None + assert not is_spam_display_name("Method Analytics") def test_substring_act_in_contact_not_matched(self) -> None: - assert check_spam_display_name("Contact Knowledge Solutions") is None + assert not is_spam_display_name("Contact Knowledge Solutions") def test_substring_now_in_knowledge_not_matched(self) -> None: - assert check_spam_display_name("Knowledge Now Platform") is None + assert not is_spam_display_name("Knowledge Now Platform") def test_substring_here_in_where_not_matched(self) -> None: - assert check_spam_display_name("Where We Shine") is None + assert not is_spam_display_name("Where We Shine") def test_empty_string_passes(self) -> None: - assert check_spam_display_name("") is None - - def test_error_message_format(self) -> None: - result = check_spam_display_name("Free BTC - Click Here") - assert result is not None - assert "disallowed content" in result - assert "Please choose a different name" in result + assert not is_spam_display_name("")