fix queue

This commit is contained in:
javad committed 2026-09-11 18:04:51 +03:30
1 parent 049cc41afc
commit 9a1c99cbd3
14 files changed
+237 -32

No files matched your search

@@ -132,7 +132,7 @@ def test_e2e_03_warehouse_staff_cannot_see_sales_tickets(live_server, browser_pa
users, units = org_data
buyer = make_customer("e2e-scope-buyer", "09120008101")
create_ticket(customer=buyer, unit=units["sales"], category=TicketCategory.objects.get(slug="sales-general"), subject="تیکت محرمانه فروش E2E", body="شرح")
create_ticket(customer=buyer, unit=units["warehouse-loading"], category=TicketCategory.objects.get(slug="warehouse-loading-general"), subject="تیکت مجاز بارگیری E2E", body="شرح")
create_ticket(customer=buyer, created_by=users["ms_sabzali"], unit=units["warehouse-loading"], category=TicketCategory.objects.get(slug="warehouse-loading-general"), subject="تیکت مجاز بارگیری E2E", body="شرح")
with browser_page() as page:
login_staff(page, live_server, users["ms_sabzali"])
content = page.locator("body")
@@ -144,7 +144,7 @@ def test_e2e_04_accounting_sees_only_finance_scope(live_server, browser_page, or
users, units = org_data
buyer = make_customer("e2e-finance-buyer", "09120008102")
create_ticket(customer=buyer, unit=units["finance"], category=TicketCategory.objects.get(slug="finance-general"), subject="تیکت مالی مجاز E2E", body="شرح")
create_ticket(customer=buyer, unit=units["warehouse-loading"], category=TicketCategory.objects.get(slug="warehouse-loading-general"), subject="تیکت انبار نامجاز E2E", body="شرح")
create_ticket(customer=buyer, created_by=users["ms_sabzali"], unit=units["warehouse-loading"], category=TicketCategory.objects.get(slug="warehouse-loading-general"), subject="تیکت انبار نامجاز E2E", body="شرح")
with browser_page() as page:
login_staff(page, live_server, users["fereshteh_yavari"])
expect(page.locator("body")).to_contain_text("تیکت مالی مجاز E2E")
@@ -238,8 +238,8 @@ def test_unit_change_renders_experts_when_a_legacy_cached_payload_has_no_queue(l
page.route("**/tickets/new/options/**", remove_queue_from_response)
page.goto(f"{live_server.url}{reverse('tickets:new')}", wait_until="domcontentloaded")
ticket_page = PublicTicketCreatePage(page, live_server.url)
payload = ticket_page.select_unit(units["warehouse-loading"].pk)
payload = ticket_page.select_unit(units["finance"].pk)
assert payload.get("queue") is None
expect(ticket_page.expert_picker).to_contain_text("خانم سبزعلی")
expect(ticket_page.expert_picker).to_contain_text("آقای مهدی عظمی")
expect(ticket_page.expert_picker).to_contain_text("خانم فرشته یاوری")
expect(ticket_page.expert_picker).not_to_contain_text("کیمیا غفاری")
expect(page.get_by_role("alert")).to_have_count(0)
+14 -4
View File
@@ -9,14 +9,19 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from apps.attachments.models import Attachment, AttachmentPolicy
from apps.accounts.models import User, normalize_mobile
from apps.notifications.models import NotificationJob
from apps.staff.models import StaffProfile
from apps.staff.models import OrganizationalUnit, StaffProfile
from apps.tickets.models import Ticket, TicketCategory
from apps.tickets.policies import TicketPolicy
from apps.tickets.services import claim_ticket, create_ticket, delete_ticket, reply_ticket
@pytest.fixture
def category(db):
return TicketCategory.objects.create(title="قیمت", slug="price")
unit = OrganizationalUnit.objects.create(name="واحد تست دامنه", slug="domain-test-unit")
unit.permissions.add(
Permission.objects.get(codename="view_ticket", content_type__app_label="tickets"),
Permission.objects.get(codename="add_message", content_type__app_label="messaging"),
)
return TicketCategory.objects.create(unit=unit, title="قیمت", slug="domain-test-price")
@pytest.fixture
def customer(db):
@@ -25,6 +30,8 @@ def customer(db):
def make_staff(username):
mobile = "09120000001" if username == "a" else "09120000002"
user = User.objects.create_user(username=username, mobile=mobile, account_type="STAFF")
unit = OrganizationalUnit.objects.filter(slug="domain-test-unit").first()
StaffProfile.objects.create(user=user, primary_unit=unit, display_name=username)
for code in ["view_ticket", "claim_ticket", "change_ticket"]:
user.user_permissions.add(Permission.objects.get(codename=code, content_type__app_label="tickets"))
return user
@@ -122,14 +129,17 @@ def test_bale_ticket_excerpt_is_compact_and_whitespace_normalized(category,custo
def test_ticket_full_delete_requires_manager_permission_and_denies_sales_expert(category, customer):
ticket = create_ticket(customer=customer, category=category, subject="حذف پرونده", body="متن اولیه")
expert = User.objects.create_user(username="delete-expert", mobile="09120000003", account_type="STAFF")
StaffProfile.objects.create(user=expert, display_name="کارشناس", role=StaffProfile.Role.SALES_EXPERT)
StaffProfile.objects.create(user=expert, primary_unit=category.unit, display_name="کارشناس", role=StaffProfile.Role.SALES_EXPERT)
expert.user_permissions.add(Permission.objects.get(codename="delete_ticket", content_type__app_label="tickets"))
with pytest.raises(PermissionDenied):
delete_ticket(ticket_id=ticket.pk, user=expert)
manager = User.objects.create_user(username="delete-manager", mobile="09120000004", account_type="STAFF")
StaffProfile.objects.create(user=manager, display_name="مدیر", role=StaffProfile.Role.SALES_MANAGER)
StaffProfile.objects.create(user=manager, primary_unit=category.unit, display_name="مدیر", role=StaffProfile.Role.SALES_MANAGER)
for codename in ("delete_ticket", "view_all_tickets"):
manager.user_permissions.add(Permission.objects.get(codename=codename, content_type__app_label="tickets"))
manager.unit_memberships.get(unit=category.unit).permissions.add(
Permission.objects.get(codename="delete_ticket", content_type__app_label="tickets")
)
delete_ticket(ticket_id=ticket.pk, user=manager)
assert not Ticket.objects.filter(pk=ticket.pk).exists()
assert not NotificationJob.objects.filter(ticket_id=ticket.pk).exists()
+3 -3
View File
@@ -28,7 +28,7 @@ def make_contact(username, mobile):
@pytest.mark.django_db
def test_unit_permission_isolates_ticket_queries_and_object_access():
sales = OrganizationalUnit.objects.create(name="فروش", slug="sales")
sales = OrganizationalUnit.objects.create(name="فروش ایزوله", slug="isolated-sales")
logistics = OrganizationalUnit.objects.create(name="بارگیری ایزوله", slug="isolated-logistics")
view_permission = Permission.objects.get(codename="view_ticket", content_type__app_label="tickets")
sales.permissions.add(view_permission)
@@ -114,7 +114,7 @@ def test_magic_link_records_every_open_with_ip_and_user_agent(client):
@pytest.mark.django_db
def test_dispatch_pages_are_available_only_to_the_dispatch_unit(client):
unit = OrganizationalUnit.objects.create(name="انبار دسترسی", slug="access-warehouse")
other_unit = OrganizationalUnit.objects.create(name="فروش", slug="sales-2")
other_unit = OrganizationalUnit.objects.create(name="فروش دسترسی تست", slug="sales-2")
for codename in ("view_ticket", "dispatch_group_tickets"):
permission = Permission.objects.get(codename=codename, content_type__app_label="tickets")
unit.permissions.add(permission)
@@ -123,7 +123,7 @@ def test_dispatch_pages_are_available_only_to_the_dispatch_unit(client):
customer = make_contact("warehouse-contact", "09120000131")
group = ContactGroup.objects.create(name="تامین‌کنندگان", unit=unit)
customer.customer_profile.groups.add(group)
category = TicketCategory.objects.create(title="موجودی", slug="inventory")
category = TicketCategory.objects.create(unit=unit, title="موجودی", slug="test-inventory-access")
client.force_login(staff)
assert client.get(reverse("staff_tickets:dispatch_create")).status_code == 200
+53
View File
@@ -135,6 +135,59 @@ def test_ticket_visibility_obeys_self_unit_and_global_scopes(organization):
assert set(staff_ticket_rows(ceo)) == {public_sales, selected_sales, kimia_owned, warehouse_ticket, finance_ticket}
def test_each_public_queue_and_staff_message_endpoint_is_strictly_unit_scoped(client, organization):
sales, finance = organization["sales"], organization["finance"]
sales_staff = User.objects.get(username="kimiya_ghaffari")
other_sales_staff = User.objects.get(username="zahra_jamali")
finance_staff = User.objects.get(username="fereshteh_yavari")
buyer = customer("isolated-queue-buyer", "09120007111")
sales_ticket = create_ticket(
customer=buyer,
unit=sales,
category=TicketCategory.objects.get(slug="sales-general"),
subject="صف عمومی فقط فروش",
body="پیام فروش",
)
finance_ticket = create_ticket(
customer=buyer,
unit=finance,
category=TicketCategory.objects.get(slug="finance-general"),
subject="صف عمومی فقط مالی",
body="پیام مالی",
)
assert set(staff_ticket_rows(sales_staff)) == {sales_ticket}
assert finance_ticket not in staff_ticket_rows(sales_staff)
assert set(staff_ticket_rows(finance_staff)) == {finance_ticket}
assert sales_ticket not in staff_ticket_rows(finance_staff)
client.force_login(sales_staff)
assert client.get(reverse("staff_tickets:detail", args=[finance_ticket.public_id])).status_code == 403
assert client.get(reverse("staff_tickets:messages", args=[finance_ticket.public_id])).status_code == 403
assert client.get(reverse("staff_tickets:poll", args=[finance_ticket.public_id])).status_code == 403
routed_ticket = create_ticket(
customer=buyer,
unit=sales,
category=TicketCategory.objects.get(slug="sales-general"),
subject="صف منتقل‌شده از فروش به مالی",
body="پیام منتقل‌شده",
)
routed_ticket.assigned_unit = finance
routed_ticket.save(update_fields=["assigned_unit", "updated_at"])
assert routed_ticket not in staff_ticket_rows(sales_staff)
assert not TicketPolicy.can_view(sales_staff, routed_ticket)
assert routed_ticket in staff_ticket_rows(finance_staff)
assert TicketPolicy.can_view(finance_staff, routed_ticket)
# Once a SELF-scope queue item is claimed, it is no longer visible to a
# different expert merely because both experts belong to the same unit.
sales_ticket.owner = other_sales_staff
sales_ticket.save(update_fields=["owner", "updated_at"])
assert sales_ticket not in staff_ticket_rows(sales_staff)
assert not TicketPolicy.can_view(sales_staff, sales_ticket)
def test_staff_ticket_form_locks_primary_unit_and_rejects_tampered_unit_category_and_group(client, organization):
kimia = User.objects.get(username="kimiya_ghaffari")
sales, finance = organization["sales"], organization["finance"]
+19
View File
@@ -5,11 +5,14 @@ import pytest
from django.contrib.auth.models import Permission
from django.core.management import call_command
from django.db import connection
from django.utils import timezone
from apps.accounts.models import User
from apps.customers.models import CustomerProfile
from apps.core.models import ProductionSeedState
from apps.notifications.models import NotificationJob
from apps.staff.models import OrganizationalUnit, StaffProfile
from apps.tickets.models import Ticket, TicketCategory
@pytest.mark.django_db(transaction=True)
@@ -26,6 +29,19 @@ def test_seed_bundle_round_trip_is_complete_idempotent_and_excludes_outbox(tmp_p
account_type=User.AccountType.STAFF,
)
StaffProfile.objects.create(user=user, primary_unit=unit, display_name="کارشناس Seed")
customer = User.objects.create_user(
username="seed-customer", mobile="09120009991", account_type=User.AccountType.CUSTOMER
)
CustomerProfile.objects.create(user=customer, first_name="مشتری Seed")
category = TicketCategory.objects.create(
unit=unit, title="دسته Seed", slug="seed-category"
)
legacy_ticket = Ticket.objects.create(
customer=customer,
category=category,
subject="تیکت قدیمی بدون واحد",
last_activity_at=timezone.now(),
)
permission = Permission.objects.order_by("pk").first()
permission_key = (permission.content_type.app_label, permission.codename)
unit.permissions.add(permission)
@@ -69,4 +85,7 @@ def test_seed_bundle_round_trip_is_complete_idempotent_and_excludes_outbox(tmp_p
assert restored_membership.unit.permissions.filter(**permission_filter).exists()
assert ProductionSeedState.objects.count() == 1
assert NotificationJob.objects.count() == 0
restored_ticket = Ticket.objects.get(pk=legacy_ticket.pk)
assert restored_ticket.owning_unit.slug == "seed-unit"
assert restored_ticket.assigned_unit.slug == "seed-unit"
assert (target_media / "branding" / "logo" / "current.png").read_bytes() == b"seed-media"
+45 -8
View File
@@ -23,12 +23,16 @@ def test_customer_detail_is_forbidden_to_other_customer(client):
@pytest.mark.django_db
def test_locked_public_ticket_only_leaks_metadata(client):
category = TicketCategory.objects.create(title="قیمت", slug="test-locked-price")
unit = OrganizationalUnit.objects.create(name="فروش قفل‌شده", slug="locked-sales")
unit.permissions.add(
Permission.objects.get(content_type__app_label="tickets", codename="view_ticket")
)
category = TicketCategory.objects.create(unit=unit, title="قیمت", slug="test-locked-price")
customer = User.objects.create_user(username="customer", mobile="09123333333", account_type="CUSTOMER")
expert = User.objects.create_user(username="expert", mobile="09124444444", account_type="STAFF")
observer = User.objects.create_user(username="observer", mobile="09125555555", account_type="STAFF")
StaffProfile.objects.create(user=expert, display_name="کارشناس اول")
StaffProfile.objects.create(user=observer, display_name="کارشناس دوم")
StaffProfile.objects.create(user=expert, primary_unit=unit, display_name="کارشناس اول")
StaffProfile.objects.create(user=observer, primary_unit=unit, display_name="کارشناس دوم")
for user in (expert, observer):
for code in ("view_ticket", "claim_ticket"):
user.user_permissions.add(Permission.objects.get(content_type__app_label="tickets", codename=code))
@@ -36,7 +40,7 @@ def test_locked_public_ticket_only_leaks_metadata(client):
claim_ticket(ticket.pk, expert)
client.force_login(observer)
listing = client.get(reverse("staff_tickets:list"))
assert normalize_persian_text("موضوع قابل مشاهده") in listing.content.decode()
assert normalize_persian_text("موضوع قابل مشاهده") not in listing.content.decode()
assert "محتوای محرمانه" not in listing.content.decode()
assert client.get(reverse("staff_tickets:detail", args=[ticket.public_id])).status_code == 403
@@ -70,11 +74,13 @@ def test_staff_filters_keep_selected_values_and_active_tab(client):
@pytest.mark.django_db
def test_staff_desk_prioritizes_fresh_actionable_activity_and_renders_distinct_statuses(client):
category=TicketCategory.objects.create(title="پیگیری فروش",slug="staff-priority-order")
unit=OrganizationalUnit.objects.create(name="فروش اولویت",slug="priority-sales")
unit.permissions.add(Permission.objects.get(content_type__app_label="tickets",codename="view_ticket"))
category=TicketCategory.objects.create(unit=unit,title="پیگیری فروش",slug="staff-priority-order")
customer=User.objects.create_user(username="priority-customer",mobile="09130000010",account_type="CUSTOMER")
CustomerProfile.objects.create(user=customer,first_name="مشتری",last_name="نمونه")
expert=User.objects.create_user(username="priority-expert",mobile="09130000011",account_type="STAFF")
StaffProfile.objects.create(user=expert,display_name="کارشناس اولویت")
StaffProfile.objects.create(user=expert,primary_unit=unit,display_name="کارشناس اولویت")
expert.user_permissions.add(Permission.objects.get(content_type__app_label="tickets",codename="view_ticket"))
waiting_customer=create_ticket(customer=customer,category=category,subject="منتظر پاسخ مشتری",body="شرح اول")
fresh_customer_reply=create_ticket(customer=customer,category=category,subject="پاسخ تازه مشتری",body="شرح دوم")
@@ -224,6 +230,32 @@ def test_staff_sidebar_hides_admin_link_and_shows_developer_credit(client):
assert "برنامه نویسی و توسعه: محمد جواد فصاحت" in content
assert "09202202129" in content
assert "واحد فناوری اطلاعات" not in content
assert "الگوهای پیامک" not in content
assert reverse("staff_tickets:sms_templates") not in content
@pytest.mark.django_db
def test_root_redirects_to_login_or_authenticated_workspace(client):
response = client.get("/")
assert response.status_code == 302
assert response.url == reverse("accounts:login")
customer = User.objects.create_user(
username="root-customer", mobile="09120000991", account_type="CUSTOMER"
)
client.force_login(customer)
assert client.get("/").url == reverse("tickets:list")
staff = User.objects.create_user(
username="root-staff", mobile="09120000992", account_type="STAFF"
)
StaffProfile.objects.create(user=staff, display_name="کارشناس ریشه")
client.force_login(staff)
assert client.get("/").url == reverse("staff_tickets:list")
@pytest.mark.django_db
def test_admin_is_only_mounted_at_private_path(client):
assert reverse("admin:index") == "/x5admin/"
assert client.get("/admin/").status_code == 404
@pytest.mark.django_db
def test_customer_can_set_password_and_login(client):
@@ -266,10 +298,15 @@ def test_magic_link_is_reusable_for_seven_days_and_only_grants_one_ticket(client
@pytest.mark.django_db
def test_ticket_polling_is_versioned_incremental_and_permission_safe(client):
category=TicketCategory.objects.create(title="Polling",slug="polling")
unit=OrganizationalUnit.objects.create(name="فروش Polling",slug="polling-sales")
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="Polling",slug="polling")
customer=User.objects.create_user(username="poll-customer",mobile="09129990001",account_type="CUSTOMER")
expert=User.objects.create_user(username="poll-expert",mobile="09129990002",account_type="STAFF")
StaffProfile.objects.create(user=expert,display_name="کارشناس پولینگ")
StaffProfile.objects.create(user=expert,primary_unit=unit,display_name="کارشناس پولینگ")
expert.user_permissions.add(Permission.objects.get(content_type__app_label="tickets",codename="view_ticket"))
ticket=create_ticket(customer=customer,category=category,subject="بررسی پاسخ تازه",body="پیام نخست")
client.force_login(customer)