Files
django-helpdesk/tests/test_followup_copy.py
2026-10-06 15:53:45 +08:00

225 lines
9.5 KiB
Python

import tempfile
from datetime import timedelta
from pathlib import Path
from unittest import mock
from django.contrib.auth.models import Permission
from django.core import mail
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, override_settings
from django.urls import reverse
from helpdesk import settings as helpdesk_settings
from helpdesk.models import FollowUp, FollowUpAttachment, Queue, Ticket, TicketChange
from .helpers import get_user
class FollowUpCopyTests(TestCase):
def setUp(self):
enabled = mock.patch.object(
helpdesk_settings, "HELPDESK_ENABLE_FOLLOWUP_COPY", True
)
enabled.start()
self.addCleanup(enabled.stop)
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.media_root = Path(directory.name)
settings = override_settings(MEDIA_ROOT=directory.name)
settings.enable()
self.addCleanup(settings.disable)
self.user = get_user(username="copier", is_staff=True)
self.client.force_login(self.user)
self.queue = Queue.objects.create(title="Support", slug="support")
self.source = Ticket.objects.create(title="Original request", queue=self.queue)
self.target = Ticket.objects.create(title="Related request", queue=self.queue)
self.followup = FollowUp.objects.create(
ticket=self.source,
title="Investigation",
comment="Restart the device.",
user=None,
public=True,
message_id="<original@example.com>",
new_status=Ticket.CLOSED_STATUS,
time_spent=timedelta(minutes=45),
email_recipients=["customer@example.com"],
)
self.url = reverse(
"helpdesk:followup_edit", args=[self.source.pk, self.followup.pk]
)
def post(self, **data):
return self.client.post(
self.url,
{
"ticket": self.target.pk,
"title": self.followup.title,
"comment": self.followup.comment,
"copy_to_ticket": "on",
**data,
},
)
def test_get_keeps_existing_ticket_choices_and_does_not_copy(self):
closed = Ticket.objects.create(
title="Closed", queue=self.queue, status=Ticket.CLOSED_STATUS
)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertQuerySetEqual(
response.context["form"].fields["ticket"].queryset,
[self.target, self.source],
)
self.assertEqual(FollowUp.objects.count(), 1)
self.assertNotIn(closed, response.context["form"].fields["ticket"].queryset)
def test_copy_keeps_source_and_does_not_replay_history(self):
TicketChange.objects.create(
followup=self.followup, field="Status", old_value="Open", new_value="Closed"
)
source_date = self.followup.date
response = self.post()
copied = self.target.followup_set.get()
self.assertRedirects(response, copied.get_absolute_url())
self.followup.refresh_from_db()
self.assertEqual(self.followup.ticket, self.source)
self.assertEqual(self.followup.date, source_date)
self.assertEqual(copied.comment, self.followup.comment)
self.assertIn(f"#{self.source.pk}", copied.title)
self.assertEqual(copied.user, self.user)
self.assertFalse(copied.public)
self.assertIsNone(copied.message_id)
self.assertIsNone(copied.new_status)
self.assertIsNone(copied.time_spent)
self.assertEqual(copied.email_recipients, [])
self.assertFalse(copied.ticketchange_set.exists())
self.assertEqual(len(mail.outbox), 0)
self.target.refresh_from_db()
self.assertEqual(self.target.status, Ticket.OPEN_STATUS)
def test_copy_respects_public_field(self):
self.post(public="on")
self.assertTrue(self.target.followup_set.get().public)
def attachment(self, name="notes.txt"):
return FollowUpAttachment.objects.create(
followup=self.followup,
file=SimpleUploadedFile(name, b"investigation notes"),
filename=name,
mime_type="text/plain",
size=19,
)
def test_attachments_are_independent_files(self):
original = self.attachment()
self.post()
copied = self.target.followup_set.get().followupattachment_set.get()
self.assertNotEqual(original.file.name, copied.file.name)
with copied.file.open("rb") as content:
self.assertEqual(content.read(), b"investigation notes")
self.assertEqual(copied.filename, original.filename)
original.file.delete(save=False)
self.assertTrue(copied.file.storage.exists(copied.file.name))
def test_partial_attachment_failure_rolls_back_rows_and_files(self):
self.attachment("a.txt")
missing = self.attachment("b.txt")
missing.file.delete(save=False)
before = {p for p in self.media_root.rglob("*") if p.is_file()}
response = self.post()
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Unable to copy the attachments")
self.assertFalse(self.target.followup_set.exists())
self.assertEqual(FollowUpAttachment.objects.count(), 2)
self.assertEqual(before, {p for p in self.media_root.rglob("*") if p.is_file()})
def test_rejects_self_closed_and_missing_target(self):
closed = Ticket.objects.create(
title="Closed", queue=self.queue, status=Ticket.CLOSED_STATUS
)
for target in (self.source.pk, closed.pk, 99999, ""):
with self.subTest(target=target):
response = self.post(ticket=target)
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["form"].errors)
self.assertEqual(FollowUp.objects.count(), 1)
def test_followup_must_belong_to_source_ticket(self):
url = reverse("helpdesk:followup_edit", args=[self.target.pk, self.followup.pk])
self.assertEqual(
self.client.post(url, {"ticket": self.source.pk}).status_code, 404
)
def test_queue_permissions_apply_to_source_and_target(self):
other_queue = Queue.objects.create(title="Restricted", slug="restricted")
forbidden = Ticket.objects.create(title="Secret", queue=other_queue)
self.user.user_permissions.add(
Permission.objects.get(codename=self.queue.permission_name.split(".", 1)[1])
)
with mock.patch.object(
helpdesk_settings, "HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION", True
):
response = self.client.get(self.url)
self.assertNotIn(
forbidden, response.context["form"].fields["ticket"].queryset
)
response = self.post(ticket=forbidden.pk)
self.assertTrue(response.context["form"].errors)
self.source.queue = other_queue
self.source.save()
self.assertEqual(self.post().status_code, 403)
self.assertEqual(FollowUp.objects.count(), 1)
def test_non_staff_and_anonymous_cannot_copy(self):
self.client.force_login(get_user(username="customer", is_staff=False))
self.assertIn(self.post().status_code, (302, 403))
self.client.logout()
self.assertIn(self.post().status_code, (302, 403))
self.assertEqual(FollowUp.objects.count(), 1)
def test_does_not_duplicate_automatically_calculated_time(self):
with mock.patch.object(helpdesk_settings, "FOLLOWUP_TIME_SPENT_AUTO", True):
self.post()
self.assertIsNone(self.target.followup_set.get().time_spent)
def test_unchecked_copy_preserves_existing_move_behavior(self):
response = self.post(copy_to_ticket="")
self.assertEqual(response.status_code, 302)
self.followup.refresh_from_db()
self.assertEqual(self.followup.ticket, self.target)
self.assertEqual(FollowUp.objects.count(), 1)
self.assertEqual(self.followup.message_id, "<original@example.com>")
def test_copy_uses_edited_content_without_modifying_source(self):
self.post(title="Updated title", comment="Updated comment")
copied = self.target.followup_set.get()
self.assertIn("Updated title", copied.title)
self.assertEqual(copied.comment, "Updated comment")
self.followup.refresh_from_db()
self.assertEqual(self.followup.title, "Investigation")
self.assertEqual(self.followup.comment, "Restart the device.")
def test_disabled_hides_copy_option(self):
with mock.patch.object(
helpdesk_settings, "HELPDESK_ENABLE_FOLLOWUP_COPY", False
):
response = self.client.get(self.url)
self.assertNotContains(response, 'name="copy_to_ticket"')
def test_disabled_rejects_copy_without_moving_original(self):
with mock.patch.object(
helpdesk_settings, "HELPDESK_ENABLE_FOLLOWUP_COPY", False
):
response = self.post()
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["form"].errors)
self.followup.refresh_from_db()
self.assertEqual(self.followup.ticket, self.source)
self.assertEqual(FollowUp.objects.count(), 1)
def test_disabled_preserves_normal_move(self):
with mock.patch.object(
helpdesk_settings, "HELPDESK_ENABLE_FOLLOWUP_COPY", False
):
self.test_unchecked_copy_preserves_existing_move_behavior()