Files
customerTicket/tests/test_staff_ticket_short_links.py
T
2026-10-13 16:09:45 +03:30

194 lines
9.1 KiB
Python

import re
from datetime import timedelta
from urllib.parse import parse_qs, urlsplit
from unittest.mock import Mock
import pytest
from django.contrib import admin
from django.contrib.auth.models import Permission
from django.urls import reverse
from django.utils import timezone
from apps.accounts.models import TicketMagicLink, User
from apps.integrations.sms.base import SMSResult
from apps.notifications.management.commands.process_notifications import _send_sms
from apps.notifications.models import NotificationJob, SMSTemplate
from apps.staff.models import OrganizationalUnit, StaffProfile
from apps.tickets.models import StaffTicketShortLink, TicketCategory
from apps.tickets.services import create_ticket, delete_ticket
from apps.tickets.short_links import append_supervisor_ticket_link
@pytest.fixture
def short_link_case(db):
unit = OrganizationalUnit.objects.create(
name="واحد لینک کوتاه", slug="short-link-unit", supervisor_mobile_1="09121112233",
supervisor_mobile_2="09121112234", supervisor_mobile_3="09121112235",
supervisor_mobile_4="09121112236", supervisor_mobile_5="09121112237",
supervisor_sms_ticket_link_enabled=True,
)
unit.permissions.add(
Permission.objects.get(content_type__app_label="tickets", codename="view_ticket"),
Permission.objects.get(content_type__app_label="messaging", codename="add_message"),
)
category = TicketCategory.objects.create(unit=unit, title="درخواست لینک", slug="short-link-category")
customer = User.objects.create_user(username="short-customer", mobile="09121112238", account_type="CUSTOMER")
staff = User.objects.create_user(
username="short-staff", password="Staff-password-123", mobile="09121112239", account_type="STAFF",
)
StaffProfile.objects.create(user=staff, primary_unit=unit, display_name="کارشناس لینک")
ticket = create_ticket(customer=customer, category=category, subject="موضوع لینک کوتاه", body="متن درخواست")
return unit, customer, staff, ticket
def issue_link(ticket):
append_supervisor_ticket_link(ticket, "اعلان")
link = StaffTicketShortLink.objects.get(ticket=ticket)
return link, reverse("staff_ticket_short_link", args=[link.token])
@pytest.mark.django_db
def test_all_five_supervisors_receive_same_short_link_without_magic_access(short_link_case, monkeypatch, settings):
unit, _, _, ticket = short_link_case
settings.PUBLIC_BASE_URL = "https://tickets.example.com/"
provider = Mock()
provider.send_message.return_value = SMSResult(True, "short-link-test")
monkeypatch.setattr("apps.notifications.management.commands.process_notifications.get_sms_provider", lambda: provider)
SMSTemplate.objects.create(
unit=unit, event_type=SMSTemplate.Event.UNIT_SUPERVISOR_NEW_TICKET,
title="الگوی لینک کوتاه", body="تیکت {ticket_number}",
)
jobs = list(NotificationJob.objects.filter(ticket=ticket, event_type="UNIT_SUPERVISOR_NEW_TICKET").order_by("pk"))
before = timezone.now()
for job in jobs:
result, magic_link = _send_sms(job)
assert result.success and magic_link is None
sent = [call.args for call in provider.send_message.call_args_list]
link, path = issue_link(ticket)
assert before + timedelta(days=7) <= link.expires_at <= timezone.now() + timedelta(days=7)
assert re.fullmatch(r"/vt/[a-zA-Z0-9]{10}", path)
assert [mobile for mobile, _ in sent] == list(unit.supervisor_mobiles)
assert {message for _, message in sent} == {
f"تیکت {ticket.ticket_number}\nمشاهده و پاسخ: https://tickets.example.com{path}"
}
_send_sms(jobs[0])
link.refresh_from_db()
assert StaffTicketShortLink.objects.filter(ticket=ticket).count() == 1
assert not TicketMagicLink.objects.filter(ticket=ticket).exists()
assert not ticket.access_grants.exists()
@pytest.mark.django_db
def test_unit_toggle_preserves_plain_sms_and_does_not_issue_link(short_link_case):
unit, _, _, ticket = short_link_case
unit.supervisor_sms_ticket_link_enabled = False
unit.save()
ticket.refresh_from_db()
assert append_supervisor_ticket_link(ticket, "اعلان") == "اعلان"
assert not StaffTicketShortLink.objects.filter(ticket=ticket).exists()
assert OrganizationalUnit._meta.get_field("supervisor_sms_ticket_link_enabled").default is False
@pytest.mark.django_db
def test_anonymous_login_returns_to_real_ticket_and_can_reply(short_link_case, client):
_, _, staff, ticket = short_link_case
_, path = issue_link(ticket)
response = client.get(path)
assert response.status_code == 302
assert urlsplit(response.url).path == "/staff/login/"
assert parse_qs(urlsplit(response.url).query)["next"] == [path]
assert "no-store" in response["Cache-Control"]
client.get(response.url)
# Destination also survives a failed attempt and a POST without query params.
response = client.post("/staff/login/", {"username": staff.username, "password": "incorrect"})
assert response.status_code == 200
response = client.post("/staff/login/", {"username": staff.username, "password": "Staff-password-123"})
assert response.url == path
response = client.get(path)
detail = reverse("staff_tickets:detail", args=[ticket.public_id])
assert response.url == detail
assert client.get(detail).status_code == 200
response = client.post(reverse("staff_tickets:reply", args=[ticket.public_id]), {"body": "پاسخ از لینک کوتاه"})
assert response.status_code == 302
assert ticket.ticket_messages.filter(author=staff, body="پاسخ از لینک کوتاه").exists()
assert "ticket_magic_link_id" not in client.session
assert not ticket.access_grants.exists()
@pytest.mark.django_db
@pytest.mark.parametrize("expired", [False, True])
def test_authenticated_staff_redirect_or_expired_link(short_link_case, client, expired):
_, _, staff, ticket = short_link_case
link, path = issue_link(ticket)
if expired:
StaffTicketShortLink.objects.filter(pk=link.pk).update(expires_at=timezone.now() - timedelta(seconds=1))
client.force_login(staff)
response = client.get(path)
assert response.status_code == (404 if expired else 302)
assert "no-store" in response["Cache-Control"]
if not expired:
assert response.url == reverse("staff_tickets:detail", args=[ticket.public_id])
else:
assert append_supervisor_ticket_link(ticket, "اعلان") == "اعلان"
link.refresh_from_db()
assert link.expires_at <= timezone.now()
@pytest.mark.django_db
def test_expiry_is_rechecked_after_login(short_link_case, client):
_, _, staff, ticket = short_link_case
link, path = issue_link(ticket)
client.get(client.get(path).url)
StaffTicketShortLink.objects.filter(pk=link.pk).update(expires_at=timezone.now())
client.post("/staff/login/", {"username": staff.username, "password": "Staff-password-123"})
assert client.get(path).status_code == 404
@pytest.mark.django_db
def test_short_link_cannot_bypass_customer_or_other_unit_permissions(short_link_case, client):
_, customer, _, ticket = short_link_case
_, path = issue_link(ticket)
client.force_login(customer)
assert client.get(path).status_code == 403
outsider = User.objects.create_user(username="outsider", mobile="09121112240", account_type="STAFF")
other_unit = OrganizationalUnit.objects.create(name="واحد دیگر", slug="short-other-unit")
StaffProfile.objects.create(user=outsider, primary_unit=other_unit, display_name="کارشناس دیگر")
client.force_login(outsider)
assert client.get(path).status_code == 403
assert not ticket.access_grants.exists()
@pytest.mark.django_db
def test_missing_link_deleted_ticket_and_post_are_rejected(short_link_case, client):
_, _, staff, ticket = short_link_case
_, path = issue_link(ticket)
client.force_login(staff)
assert client.get("/vt/unknown").status_code == 404
assert client.post(path).status_code == 405
staff.is_superuser = True
staff.save()
delete_ticket(ticket_id=ticket.pk, user=staff)
assert client.get(path).status_code == 404
@pytest.mark.django_db
@pytest.mark.parametrize("target", ["https://evil.example/vt/abc", "//evil.example/vt/abc", "/\\evil.example/vt/abc", "/login/", "/staff/login/"])
def test_staff_login_rejects_unsafe_or_unrelated_next(short_link_case, client, target):
_, _, staff, _ = short_link_case
response = client.post("/staff/login/", {
"username": staff.username, "password": "Staff-password-123", "next": target,
})
assert response.url == reverse("staff_tickets:list")
@pytest.mark.django_db
def test_unit_admin_exposes_toggle(short_link_case, client):
unit, _, staff, _ = short_link_case
staff.is_superuser = staff.is_staff = True
staff.save()
client.force_login(staff)
response = client.get(reverse("admin:staff_organizationalunit_change", args=[unit.pk]))
assert response.status_code == 200
assert b'name="supervisor_sms_ticket_link_enabled"' in response.content
assert "supervisor_sms_ticket_link_enabled" in admin.site._registry[OrganizationalUnit].list_filter