154 lines
8.6 KiB
Python
154 lines
8.6 KiB
Python
import json
|
|
import uuid
|
|
from datetime import timedelta
|
|
|
|
import pytest
|
|
from django.test import Client
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
|
|
from apps.accounts.models import TicketMagicLink
|
|
from apps.messaging.models import Message, MessageReadReceipt
|
|
from apps.tickets.models import TicketReadState
|
|
from apps.tickets.sharing import revoke_staff_share, share_staff_message, share_staff_ticket
|
|
from tests.test_staff_sharing import make_sharing_case
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
|
|
@pytest.fixture
|
|
def seen_case(settings):
|
|
settings.PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
|
|
case = make_sharing_case()
|
|
case['reply'] = Message.objects.create(ticket=case['ticket'], author=case['sender'], body='پاسخ قابل مشاهده')
|
|
return case
|
|
|
|
|
|
@pytest.mark.parametrize('route', ['detail', 'messages', 'poll'])
|
|
def test_staff_get_requests_never_mark_messages_or_ticket_read(client, seen_case, route):
|
|
c = seen_case
|
|
share_staff_ticket(c['ticket'], c['sender'], c['recipient'], 'VIEW_ONLY')
|
|
client.force_login(c['recipient'])
|
|
response = client.get(reverse('staff_tickets:' + route, args=[c['ticket'].public_id]))
|
|
assert response.status_code == 200
|
|
assert not MessageReadReceipt.objects.filter(user=c['recipient']).exists()
|
|
assert not TicketReadState.objects.filter(user=c['recipient']).exists()
|
|
|
|
|
|
def test_staff_acknowledgement_is_scoped_idempotent_and_rechecked_after_revocation(client, seen_case):
|
|
c = seen_case
|
|
grant = share_staff_message(c['selected'], c['sender'], c['recipient'])
|
|
client.force_login(c['recipient'])
|
|
url = reverse('staff_tickets:read_shares', args=[c['ticket'].public_id])
|
|
values = [str(c['selected'].public_id), str(c['reply'].public_id), str(uuid.uuid4()), 'bad-uuid']
|
|
for _ in range(2):
|
|
response = client.post(url, {'message_ids': values})
|
|
assert response.status_code == 200
|
|
assert response.json()['read_ids'] == [str(c['selected'].public_id)]
|
|
assert list(MessageReadReceipt.objects.filter(user=c['recipient']).values_list('message_id', flat=True)) == [c['selected'].pk]
|
|
assert not TicketReadState.objects.filter(user=c['recipient']).exists()
|
|
grant.refresh_from_db()
|
|
assert grant.view_count == 1
|
|
revoke_staff_share(grant, c['sender'])
|
|
assert client.post(url, {'message_ids': values}).status_code == 403
|
|
assert client.get(reverse('staff_tickets:poll', args=[c['ticket'].public_id])).status_code == 403
|
|
assert not MessageReadReceipt.objects.filter(message=c['reply'], user=c['recipient']).exists()
|
|
|
|
|
|
def test_staff_ticket_read_state_requires_acknowledging_the_latest_message(client, seen_case):
|
|
c = seen_case
|
|
client.force_login(c['sender'])
|
|
url = reverse('staff_tickets:read_shares', args=[c['ticket'].public_id])
|
|
client.post(url, {'message_ids': [str(c['selected'].public_id)]})
|
|
assert not TicketReadState.objects.filter(user=c['sender']).exists()
|
|
client.post(url, {'message_ids': [str(c['reply'].public_id)]})
|
|
assert TicketReadState.objects.get(user=c['sender']).last_read_message_id == c['reply'].pk
|
|
assert not MessageReadReceipt.objects.filter(message=c['reply'], user=c['sender']).exists()
|
|
|
|
|
|
def test_another_colleagues_read_state_does_not_hide_my_unread_ticket(seen_case):
|
|
from apps.tickets.selectors import filter_staff_tickets, staff_ticket_rows
|
|
c = seen_case
|
|
share_staff_ticket(c['ticket'], c['sender'], c['recipient'], 'VIEW_ONLY')
|
|
TicketReadState.objects.create(ticket=c['ticket'], user=c['sender'], last_read_at=timezone.now(), last_read_message=c['reply'])
|
|
unread = filter_staff_tickets(staff_ticket_rows(c['recipient']), c['recipient'], {'unread': '1'})
|
|
assert unread.filter(pk=c['ticket'].pk).exists()
|
|
own_state = TicketReadState.objects.create(ticket=c['ticket'], user=c['recipient'], last_read_at=timezone.now(), last_read_message=c['reply'])
|
|
assert not unread.filter(pk=c['ticket'].pk).exists()
|
|
own_state.last_read_at = c['ticket'].last_activity_at - timedelta(seconds=1)
|
|
own_state.save(update_fields=['last_read_at'])
|
|
assert unread.filter(pk=c['ticket'].pk).exists()
|
|
|
|
|
|
def test_customer_receipts_exclude_own_deleted_internal_and_other_ticket_messages(client, seen_case):
|
|
c = seen_case
|
|
deleted = Message.objects.create(ticket=c['ticket'], author=c['sender'], body='حذفشده', deleted_at=timezone.now())
|
|
from apps.tickets.services import create_ticket
|
|
other = create_ticket(customer=c['customer'], category=c['ticket'].category, subject='تیکت دیگر', body='پیام تیکت دیگر')
|
|
foreign = Message.objects.create(ticket=other, author=c['sender'], body='پاسخ خصوصی تیکت دیگر')
|
|
client.force_login(c['customer'])
|
|
for route in ['detail', 'messages', 'poll']:
|
|
assert client.get(reverse('tickets:' + route, args=[c['ticket'].public_id])).status_code == 200
|
|
assert not MessageReadReceipt.objects.filter(user=c['customer']).exists()
|
|
identifiers = [str(value) for value in c['ticket'].ticket_messages.values_list('public_id', flat=True)]
|
|
identifiers += [str(foreign.public_id), str(uuid.uuid4()), 'not-a-uuid', 42, None]
|
|
url = reverse('tickets:read', args=[c['ticket'].public_id])
|
|
response = client.post(url, json.dumps({'message_ids': identifiers}), content_type='application/json')
|
|
assert response.status_code == 200
|
|
assert list(MessageReadReceipt.objects.filter(user=c['customer']).values_list('message_id', flat=True)) == [c['reply'].pk]
|
|
assert not MessageReadReceipt.objects.filter(message=deleted).exists()
|
|
|
|
|
|
@pytest.mark.parametrize('payload', ['not json', 'null', '[]', '{"message_ids":"bad"}', '{"message_ids":{}}'])
|
|
@pytest.mark.parametrize('staff', [True, False])
|
|
def test_malformed_read_payloads_return_400_without_recording_reads(client, seen_case, payload, staff):
|
|
c = seen_case
|
|
client.force_login(c['sender'] if staff else c['customer'])
|
|
url = reverse('staff_tickets:read_shares' if staff else 'tickets:read', args=[c['ticket'].public_id])
|
|
assert client.post(url, payload, content_type='application/json').status_code == 400
|
|
assert not MessageReadReceipt.objects.exists()
|
|
values = [str(c['reply'].public_id)] * 101
|
|
assert client.post(url, json.dumps({'message_ids': values}), content_type='application/json').status_code == 400
|
|
|
|
|
|
def test_read_acknowledgements_keep_csrf_and_magic_link_scope(seen_case):
|
|
c = seen_case
|
|
strict = Client(enforce_csrf_checks=True)
|
|
strict.force_login(c['sender'])
|
|
staff_url = reverse('staff_tickets:read_shares', args=[c['ticket'].public_id])
|
|
assert strict.post(staff_url, {'message_ids': [str(c['selected'].public_id)]}).status_code == 403
|
|
anonymous = Client()
|
|
token = TicketMagicLink.issue(c['ticket'])
|
|
assert anonymous.get(reverse('accounts:magic', args=[token])).status_code == 302
|
|
url = reverse('tickets:read', args=[c['ticket'].public_id])
|
|
assert anonymous.post(url, json.dumps({'message_ids': [str(c['reply'].public_id)]}), content_type='application/json').status_code == 200
|
|
assert MessageReadReceipt.objects.filter(message=c['reply'], user=c['customer']).exists()
|
|
assert '_auth_user_id' not in anonymous.session
|
|
|
|
|
|
def test_polling_backlog_does_not_skip_messages_after_the_first_batch(client, seen_case):
|
|
c = seen_case
|
|
client.force_login(c['sender'])
|
|
url = reverse('staff_tickets:poll', args=[c['ticket'].public_id])
|
|
initial = client.get(url).json()
|
|
batch = [Message.objects.create(ticket=c['ticket'], author=c['customer'], body=f'پیام صف {i}') for i in range(55)]
|
|
first = client.get(url, {'version': initial['version'], 'cursor': initial['cursor']}).json()
|
|
assert not first['replace'] and first['cursor'] == str(batch[49].public_id)
|
|
second = client.get(url, {'version': first['version'], 'cursor': first['cursor']}).json()
|
|
assert not second['replace'] and second['cursor'] == str(batch[-1].public_id)
|
|
assert batch[-1].body in second['html']
|
|
assert not MessageReadReceipt.objects.exists()
|
|
|
|
|
|
def test_polling_detects_editing_an_older_message_even_with_a_new_reply(client, seen_case):
|
|
from apps.messaging.services import edit_message
|
|
c = seen_case
|
|
client.force_login(c['customer'])
|
|
url = reverse('tickets:poll', args=[c['ticket'].public_id])
|
|
initial = client.get(url).json()
|
|
edit_message(c['selected'].pk, c['customer'], 'متن اصلاحشده پیام قبلی')
|
|
Message.objects.create(ticket=c['ticket'], author=c['sender'], body='پاسخ همزمان')
|
|
response = client.get(url, {'version': initial['version'], 'cursor': initial['cursor']}).json()
|
|
assert response['changed'] and response['replace']
|
|
assert 'متن اصلاحشده پیام قبلی' in response['html'] and 'پاسخ همزمان' in response['html']
|