diff --git a/helpdesk/email.py b/helpdesk/email.py index b21def77..4311d35f 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -1102,7 +1102,7 @@ def extract_email_metadata( other_queues = Queue.objects.exclude(id=queue.id).filter( email_box_type__isnull=False ) - + for other_queue in other_queues: ticket_id = get_ticket_id_from_subject_slug( other_queue.slug, subject, logger @@ -1115,11 +1115,13 @@ def extract_email_metadata( ) queue = other_queue break - + # If no ticket ID was found in any queue, revert to the original queue for new ticket if ticket_id is None: queue = original_queue - logger.info(f"No existing ticket found, reverting to original queue {queue.slug}") + logger.info( + f"No existing ticket found, reverting to original queue {queue.slug}" + ) files = [] # first message in thread, we save full body to avoid losing forwards and things like that diff --git a/helpdesk/tests/test_get_email.py b/helpdesk/tests/test_get_email.py index ff5ad737..3b6d405e 100644 --- a/helpdesk/tests/test_get_email.py +++ b/helpdesk/tests/test_get_email.py @@ -553,6 +553,97 @@ class GetEmailCommonTests(TestCase): "Email attachment file not found in ticket attachment for empty body.", ) + @patch("helpdesk.email.get_ticket_id_from_subject_slug") + @patch("helpdesk.email.create_object_from_email_message") + def test_ticket_id_lookup_across_queues( + self, mock_create_object, mock_get_ticket_id + ): + """ + Tests the logic for finding a ticket ID: + 1. Not found in the current queue. + 2. Found in another queue, leading to ticket association with that queue. + 3. Not found in any queue, leading to a new ticket in the original queue. + """ + # Create additional queues for testing + queue_other1 = Queue.objects.create( + title="Other Queue 1", slug="other1", email_box_type="local" + ) + queue_other2 = Queue.objects.create( + title="Other Queue 2", slug="other2", email_box_type="local" + ) + + # Scenario 1: Ticket ID not found in current queue, then found in another queue + mock_get_ticket_id.side_effect = [ + None, # Not found in self.queue_public + 123, # Found in queue_other1 + ] + + # We need to mock Queue.objects.exclude().filter() to return our specific other queues + with patch("helpdesk.models.Queue.objects") as mock_queue_objects: + mock_queue_objects.exclude.return_value.filter.return_value = [ + queue_other1, + queue_other2, + ] + + message, _, _ = utils.generate_email_with_subject( + subject="[other1-123] Test Subject" + ) + ticket = extract_email_metadata( + message.as_string(), self.queue_public, self.logger + ) # noqa + + # Assert get_ticket_id_from_subject_slug was called for current and then other1 + mock_get_ticket_id.assert_has_calls( + [ + mock.call(self.queue_public.slug, mock.ANY, self.logger), + mock.call(queue_other1.slug, mock.ANY, self.logger), + ] + ) + + # Assert that create_object_from_email_message was called with the ticket ID and the other queue + mock_create_object.assert_called_once() + args, kwargs = mock_create_object.call_args + self.assertEqual(args[1], 123) # ticket_id + self.assertEqual(args[2]["queue"], queue_other1) # payload + + mock_get_ticket_id.reset_mock() + mock_create_object.reset_mock() + + # Scenario 2: Ticket ID not found in any queue, leading to a new ticket in the original queue + mock_get_ticket_id.side_effect = [ + None, # Not found in self.queue_public + None, # Not found in queue_other1 + None, # Not found in queue_other2 + ] + + with patch("helpdesk.models.Queue.objects") as mock_queue_objects: + mock_queue_objects.exclude.return_value.filter.return_value = [ + queue_other1, + queue_other2, + ] + + message, _, _ = utils.generate_email_with_subject( + subject="[nonexistent-456] New Ticket Subject" + ) + ticket = extract_email_metadata( # noqa + message.as_string(), self.queue_public, self.logger + ) + + # Assert get_ticket_id_from_subject_slug was called for all queues + mock_get_ticket_id.assert_has_calls( + [ + mock.call(self.queue_public.slug, mock.ANY, self.logger), + mock.call(queue_other1.slug, mock.ANY, self.logger), + mock.call(queue_other2.slug, mock.ANY, self.logger), + ] + ) + + # Assert that create_object_from_email_message was called with None ticket_id and the original queue + mock_create_object.assert_called_once() + args, kwargs = mock_create_object.call_args + self.assertIsNone(args[1]) + self.assertEqual(args[2]["queue"], self.queue_public) + class EmailTaskTests(TestCase): def setUp(self): diff --git a/helpdesk/tests/utils.py b/helpdesk/tests/utils.py index 98984668..5f7e9aca 100644 --- a/helpdesk/tests/utils.py +++ b/helpdesk/tests/utils.py @@ -369,3 +369,43 @@ def generate_html_email( msg, locale=locale, use_short_email=use_short_email ) return msg, from_meta, to_meta + + +def generate_email_with_subject( + subject: str, + locale: str = "en_US", + use_short_email: bool = False, + body: str = None, + html_body: str = None, +) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]: + """ + Generates an email with a specified subject, and optional plain and HTML bodies. + + :param subject: The desired subject for the email. + :param locale: Change this to generate locale-specific "real names" and body content. + :param use_short_email: Produces a "To" or "From" that is only the email address if True. + :param body: Optional plain text body for the email. If None, a fake text body is generated. + :param html_body: Optional HTML body for the email. If None and a plain body is used, a fake HTML body is generated. + """ + from_meta = generate_email_address(locale, use_short_email=use_short_email) + to_meta = generate_email_address(locale, use_short_email=use_short_email) + + if body is None and html_body is None: + body = get_fake("text", locale=locale, min_length=1024) + html_body = get_fake_html(locale=locale, wrap_in_body_tag=True) + msg = MIMEMultipart("alternative") + msg.attach(MIMEText(body, "plain")) + msg.attach(MIMEText(html_body, "html")) + elif body is not None and html_body is None: + msg = MIMEText(body, "plain") + elif body is None and html_body is not None: + msg = MIMEText(html_body, "html") + else: # Both body and html_body are provided + msg = MIMEMultipart("alternative") + msg.attach(MIMEText(body, "plain")) + msg.attach(MIMEText(html_body, "html")) + + msg["Subject"] = subject + msg["From"] = from_meta[0] + msg["To"] = to_meta[0] + return msg, from_meta, to_meta