bottleneckcrm/backend/cases/tests/test_watchers_mentions.py
Ashwin e3b5181b19 Refactor comments for clarity and consistency across multiple screens and services
- Updated comments in LeadsListScreen, MoreScreen, TaskDetailScreen, TaskFormScreen, TasksListScreen, TicketAnalyticsScreen, TicketDetailScreen, TicketsListScreen, ApiService, AuthService, CrashReporting, DealCard, TicketCard, MultiSelectSheet, KanbanColumn, StageStepper, TicketApprovalPanel, TicketPropertiesCard, and TicketTimePanel to improve readability and maintain a consistent style.
- Adjusted punctuation and phrasing for better flow and understanding.
2026-08-03 09:36:48 +05:30

239 lines
9 KiB
Python

"""Mention parser + comment-driven notification dispatch tests."""
import pytest
from datetime import timedelta
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from cases.models import Case, CaseWatcher
from cases.notifications import (
MENTION_RE,
case_link,
dispatch_for_comment,
parse_mentions,
resolve_mentions,
)
from common.models import Comment, Notification, Profile, User
def _make_profile(org, *, email):
user, _ = User.objects.get_or_create(email=email, defaults={"password": "x"})
return Profile.objects.create(user=user, org=org, role="USER", is_active=True)
def _comment(case, body, by):
return Comment.objects.create(
comment=body,
content_type=ContentType.objects.get_for_model(Case),
object_id=case.id,
commented_by=by,
org=case.org,
)
@pytest.mark.django_db
class TestParseMentions:
def test_basic_mention(self):
assert parse_mentions("hey @alice please look") == ["alice"]
def test_multiple_unique_lowercase(self):
out = parse_mentions("@Bob @alice @bob")
# de-duped, lowered, order preserved
assert out == ["bob", "alice"]
def test_does_not_match_email_addresses(self):
# The @ in 'foo@bar.com' is preceded by an alphanumeric, should be skipped.
assert parse_mentions("send mail to alice@example.com") == []
def test_punctuation_chars_in_username(self):
assert parse_mentions("ping @first.last and @x_y-z") == [
"first.last",
"x_y-z",
]
def test_no_match_inside_word(self):
# `\u`@x`. Preceded by alphanumeric, skip
assert parse_mentions("u@x") == []
def test_empty_body(self):
assert parse_mentions("") == []
assert parse_mentions(None) == []
@pytest.mark.django_db
class TestResolveMentions:
def test_resolves_email_local_part(self, org_a):
p = _make_profile(org_a, email="alice@example.com")
out = resolve_mentions(["alice"], org_a.id)
assert out == [p]
def test_unresolved_dropped(self, org_a):
_make_profile(org_a, email="alice@example.com")
out = resolve_mentions(["bob"], org_a.id)
assert out == []
def test_does_not_cross_org(self, org_a, org_b):
_make_profile(org_a, email="alice@a.com")
p_b = _make_profile(org_b, email="alice@b.com")
out = resolve_mentions(["alice"], org_a.id)
# Should match the org_a profile, never the org_b one even though
# they share a username.
assert len(out) == 1
assert out[0].org_id == org_a.id
assert out[0].id != p_b.id
def test_inactive_profile_skipped(self, org_a):
p = _make_profile(org_a, email="alice@x.com")
p.is_active = False
p.save()
assert resolve_mentions(["alice"], org_a.id) == []
@pytest.mark.django_db
class TestDispatchForComment:
def test_mention_creates_notification_and_auto_watcher(
self, case_a, admin_profile, org_a
):
bob = _make_profile(org_a, email="bob@org.com")
c = _comment(case_a, "hey @bob please review", by=admin_profile)
# Sanity: notifications were dispatched via the signal handler.
assert Notification.objects.filter(
recipient=bob, verb="case.mentioned"
).count() == 1
# Auto-watch row.
watch = CaseWatcher.objects.get(case=case_a, profile=bob)
assert watch.subscribed_via == "mention"
def test_actor_is_not_self_mentioned(self, case_a, admin_profile, org_a):
# admin's email local-part is "admin"
c = _comment(case_a, "I'm just thinking out loud @admin", by=admin_profile)
assert not Notification.objects.filter(
recipient=admin_profile, verb="case.mentioned"
).exists()
def test_unknown_username_silently_ignored(
self, case_a, admin_profile
):
_comment(case_a, "@nosuchuser ping?", by=admin_profile)
# No new mention notifications, no extra watcher rows
assert Notification.objects.filter(verb="case.mentioned").count() == 0
def test_email_addresses_in_body_do_not_create_mentions(
self, case_a, admin_profile, org_a
):
bob = _make_profile(org_a, email="bob@org.com")
_comment(
case_a, "Sent to bob@org.com about this", by=admin_profile
)
# @ preceded by alphanumeric, must NOT be parsed as a mention.
assert not Notification.objects.filter(
recipient=bob, verb="case.mentioned"
).exists()
def test_mention_rate_limited_per_recipient_per_case(
self, case_a, admin_profile, org_a
):
bob = _make_profile(org_a, email="bob@org.com")
_comment(case_a, "@bob first", by=admin_profile)
# Second mention within the rate-limit window should be skipped.
_comment(case_a, "@bob again immediately", by=admin_profile)
assert Notification.objects.filter(
recipient=bob, verb="case.mentioned"
).count() == 1
def test_mention_rate_limit_does_not_block_after_window(
self, case_a, admin_profile, org_a
):
bob = _make_profile(org_a, email="bob@org.com")
first = _comment(case_a, "@bob first", by=admin_profile)
# Move the prior notification's created_at back 2 minutes.
Notification.objects.filter(recipient=bob, verb="case.mentioned").update(
created_at=timezone.now() - timedelta(minutes=2)
)
_comment(case_a, "@bob again", by=admin_profile)
assert Notification.objects.filter(
recipient=bob, verb="case.mentioned"
).count() == 2
def test_watchers_get_case_commented_notification(
self, case_a, admin_profile, org_a
):
watcher = _make_profile(org_a, email="watcher@org.com")
CaseWatcher.objects.create(case=case_a, profile=watcher, org=org_a)
_comment(case_a, "important update", by=admin_profile)
assert Notification.objects.filter(
recipient=watcher, verb="case.commented"
).count() == 1
def test_actor_does_not_get_their_own_comment_notification(
self, case_a, admin_profile, org_a
):
# Actor is also a watcher, should still NOT receive case.commented.
CaseWatcher.objects.create(
case=case_a, profile=admin_profile, org=org_a
)
_comment(case_a, "self note", by=admin_profile)
assert not Notification.objects.filter(
recipient=admin_profile, verb="case.commented"
).exists()
def test_mentioned_user_does_not_also_get_case_commented(
self, case_a, admin_profile, org_a
):
bob = _make_profile(org_a, email="bob@org.com")
# Pre-existing watch shouldn't double up either.
CaseWatcher.objects.create(case=case_a, profile=bob, org=org_a)
_comment(case_a, "@bob look at this", by=admin_profile)
assert Notification.objects.filter(
recipient=bob, verb="case.mentioned"
).count() == 1
assert not Notification.objects.filter(
recipient=bob, verb="case.commented"
).exists()
@pytest.mark.django_db
class TestNotificationLinkIsReachable:
"""`Notification.link` is a client path, and it has to be one a client serves.
The producer wrote `/cases/<id>` for the entire life of the feature. No
client has ever had a `/cases` route, the SvelteKit app serves tickets at
`/tickets/<id>`, and its notification panel assigns `link` straight to
`window.location.href`, so every notification ever sent landed on a 404.
"""
def test_case_link_points_at_the_route_clients_serve(self):
assert case_link("abc-123") == "/tickets/abc-123"
def test_case_link_is_not_an_api_path(self):
"""It is consumed by a router, not by fetch(). An /api/ prefix here
would send the browser to JSON."""
assert not case_link("abc-123").startswith("/api/")
def test_mention_notification_links_to_the_ticket(
self, case_a, admin_profile, org_a
):
bob = _make_profile(org_a, email="bob@org.com")
_comment(case_a, "hey @bob please review", by=admin_profile)
notif = Notification.objects.get(recipient=bob, verb="case.mentioned")
assert notif.link == f"/tickets/{case_a.id}"
def test_watcher_notification_links_to_the_ticket(
self, case_a, admin_profile, org_a
):
bob = _make_profile(org_a, email="bob@org.com")
CaseWatcher.objects.create(
case=case_a, profile=bob, org=org_a, subscribed_via="manual"
)
_comment(case_a, "no mentions in this one", by=admin_profile)
notif = Notification.objects.get(recipient=bob, verb="case.commented")
assert notif.link == f"/tickets/{case_a.id}"
def test_no_notification_still_points_at_cases(self, case_a, admin_profile, org_a):
"""The other direction: nothing anywhere may emit the dead prefix."""
bob = _make_profile(org_a, email="bob@org.com")
_comment(case_a, "hey @bob", by=admin_profile)
assert not Notification.objects.filter(link__startswith="/cases/").exists()