Files
customerTicket/tests/test_dispatch_opening_postgres.py
T

104 lines
5.0 KiB
Python

from concurrent.futures import ThreadPoolExecutor
from threading import Barrier
import pytest
from django.db import connection, connections
from django.db.migrations.executor import MigrationExecutor
from apps.accounts.models import User
from apps.messaging.models import Message
from apps.notifications.dispatch_opening import ensure_opening_template
from apps.notifications.models import NotificationJob
from apps.tickets.dispatch_responses import OPENING_EVENT, open_dispatch_responses, render_opening_sms
from apps.tickets.models import DispatchResponseControl, GroupDispatch
from apps.tickets.services import reply_ticket
from tests.test_dispatch_response_opening import opening_case
pytestmark = pytest.mark.django_db(transaction=True)
def _postgres_only():
if connection.vendor != "postgresql":
pytest.skip("Real row-lock concurrency requires PostgreSQL")
def test_two_simultaneous_openings_commit_one_event_and_one_outbox_job(opening_case):
_postgres_only()
c = opening_case
ensure_opening_template()
text = render_opening_sms(c["dispatch"])
barrier = Barrier(2)
def open_once(user_id):
connections.close_all()
try:
user = User.objects.get(pk=user_id)
dispatch = GroupDispatch.objects.get(pk=c["dispatch"].pk)
barrier.wait(timeout=10)
return open_dispatch_responses(dispatch, user, text)[1]
finally:
connections.close_all()
with ThreadPoolExecutor(max_workers=2) as executor:
futures = [executor.submit(open_once, c[actor].pk) for actor in ("staff", "admin")]
assert sorted(future.result(timeout=20) for future in futures) == [False, True]
assert NotificationJob.objects.filter(event_type=OPENING_EVENT).count() == 1
assert DispatchResponseControl.objects.get(dispatch=c["dispatch"]).opened_at
def test_reply_racing_opening_matches_the_atomic_recipient_snapshot(opening_case):
_postgres_only()
c = opening_case
ensure_opening_template()
text = render_opening_sms(c["dispatch"])
barrier = Barrier(2)
def run(action):
connections.close_all()
try:
barrier.wait(timeout=10)
if action == "reply":
return reply_ticket(ticket_id=c["tickets"][1].pk, user=User.objects.get(pk=c["customers"][1].pk), body="پاسخ هم‌زمان")
return open_dispatch_responses(c["dispatch"], User.objects.get(pk=c["staff"].pk), text)
finally:
connections.close_all()
with ThreadPoolExecutor(max_workers=2) as executor:
futures = [executor.submit(run, action) for action in ("reply", "open")]
for future in futures:
future.result(timeout=20)
control = DispatchResponseControl.objects.get(dispatch=c["dispatch"])
expected = set(Message.objects.filter(ticket__group_dispatch=c["dispatch"], author_id__in=[u.pk for u in c["customers"]], created_at__lte=control.opened_at).values_list("author_id", flat=True))
assert set(control.respondents_snapshot) == {str(value) for value in User.objects.filter(pk__in=expected).values_list("public_id", flat=True)}
assert set(NotificationJob.objects.filter(event_type=OPENING_EVENT).values_list("customer_id", flat=True)) == expected
def test_additive_migrations_preserve_existing_rows_and_legacy_response_visibility(opening_case):
c = opening_case
# Turn this fixture into a pre-feature dispatch before moving the test schema back.
DispatchResponseControl.objects.filter(dispatch=c["dispatch"]).delete()
before_dispatch = list(GroupDispatch.objects.values())
before_messages = list(Message.objects.values())
before_users = list(User.objects.values())
old_targets = [("tickets", "0017_ticketaccessgrant_first_viewed_at_and_more"), ("notifications", "0020_receipt_rejection_template")]
executor = MigrationExecutor(connection)
current_targets = executor.loader.graph.leaf_nodes()
try:
executor.migrate(old_targets)
MigrationExecutor(connection).migrate(current_targets)
assert list(GroupDispatch.objects.values()) == before_dispatch
assert list(Message.objects.values()) == before_messages
assert list(User.objects.values()) == before_users
assert not DispatchResponseControl.objects.filter(dispatch=c["dispatch"]).exists()
from apps.tickets.staff_conversation import visible_messages
assert visible_messages(c["tickets"][0], c["staff"]).filter(pk=c["message"].pk).exists()
finally:
MigrationExecutor(connection).migrate(current_targets)
def test_migration_refuses_to_drop_response_confidentiality_after_use(opening_case):
from django.apps import apps
from django.db.migrations.exceptions import IrreversibleError
from types import SimpleNamespace
import importlib
guard = importlib.import_module("apps.tickets.migrations.0018_dispatch_response_opening").preserve_used_response_controls
with pytest.raises(IrreversibleError):
guard(apps, SimpleNamespace(connection=connection))
assert DispatchResponseControl.objects.filter(dispatch=opening_case["dispatch"]).exists()