104 lines
5.0 KiB
Python
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()
|