from __future__ import annotations from unittest.mock import AsyncMock, patch from django.test import TestCase from django.urls import reverse from core.models import ChatSession, Message, Person, PersonIdentifier, User class ComposeReactTests(TestCase): def setUp(self): self.user = User.objects.create_user("compose-react", "react@example.com", "pw") self.client.force_login(self.user) def _build_message(self, *, service: str, identifier: str, source_message_id: str = ""): person = Person.objects.create(user=self.user, name=f"{service} person") person_identifier = PersonIdentifier.objects.create( user=self.user, person=person, service=service, identifier=identifier, ) session = ChatSession.objects.create( user=self.user, identifier=person_identifier, ) message = Message.objects.create( user=self.user, session=session, ts=1770000000123, sender_uuid="sender-1", text="hello", source_service=service, source_message_id=source_message_id or "", source_chat_id=identifier, receipt_payload={}, ) return person, person_identifier, message @patch("core.views.compose.history.apply_reaction", new_callable=AsyncMock) @patch("core.views.compose.transport.send_reaction", new_callable=AsyncMock) def test_signal_react_uses_numeric_source_message_id_timestamp( self, mocked_send_reaction, mocked_apply_reaction ): person, _, message = self._build_message( service="signal", identifier="+15551230000", source_message_id="1771234567000", ) mocked_send_reaction.return_value = True mocked_apply_reaction.return_value = message response = self.client.post( reverse("compose_react"), { "service": "signal", "identifier": "+15551230000", "person": str(person.id), "message_id": str(message.id), "emoji": "👍", }, ) self.assertEqual(200, response.status_code) payload = response.json() self.assertTrue(payload["ok"]) self.assertEqual("👍", payload["emoji"]) self.assertFalse(payload["remove"]) mocked_send_reaction.assert_awaited_once() _, kwargs = mocked_send_reaction.await_args self.assertEqual("signal", mocked_send_reaction.await_args.args[0]) self.assertEqual("+15551230000", mocked_send_reaction.await_args.args[1]) self.assertEqual(1771234567000, kwargs["target_timestamp"]) self.assertEqual("sender-1", kwargs["target_author"]) self.assertEqual("", kwargs["target_message_id"]) @patch("core.views.compose.history.apply_reaction", new_callable=AsyncMock) @patch("core.views.compose.transport.send_reaction", new_callable=AsyncMock) def test_whatsapp_react_uses_upstream_message_id( self, mocked_send_reaction, mocked_apply_reaction ): person, _, message = self._build_message( service="whatsapp", identifier="12345@s.whatsapp.net", source_message_id="wamid.ABC123", ) mocked_send_reaction.return_value = True mocked_apply_reaction.return_value = message response = self.client.post( reverse("compose_react"), { "service": "whatsapp", "identifier": "12345@s.whatsapp.net", "person": str(person.id), "message_id": str(message.id), "emoji": "❤️", }, ) self.assertEqual(200, response.status_code) payload = response.json() self.assertTrue(payload["ok"]) self.assertEqual("wamid.ABC123", payload["target_upstream_id"]) mocked_send_reaction.assert_awaited_once() _, kwargs = mocked_send_reaction.await_args self.assertEqual("wamid.ABC123", kwargs["target_message_id"]) @patch("core.views.compose.transport.send_reaction", new_callable=AsyncMock) def test_toggle_removes_existing_actor_reaction(self, mocked_send_reaction): person, _, message = self._build_message( service="signal", identifier="+15551230000", source_message_id="1771234567000", ) message.receipt_payload = { "reactions": [ { "emoji": "👍", "actor": f"web:{self.user.id}:signal", "source_service": "signal", "removed": False, } ] } message.save(update_fields=["receipt_payload"]) mocked_send_reaction.return_value = True with patch( "core.views.compose.history.apply_reaction", new=AsyncMock(return_value=message), ): response = self.client.post( reverse("compose_react"), { "service": "signal", "identifier": "+15551230000", "person": str(person.id), "message_id": str(message.id), "emoji": "👍", }, ) self.assertEqual(200, response.status_code) payload = response.json() self.assertTrue(payload["ok"]) self.assertTrue(payload["remove"]) _, kwargs = mocked_send_reaction.await_args self.assertTrue(kwargs["remove"]) def test_unsupported_service_returns_error(self): response = self.client.post( reverse("compose_react"), { "service": "xmpp", "identifier": "someone@example.com", "message_id": "does-not-matter", "emoji": "👍", }, ) self.assertEqual(200, response.status_code) self.assertEqual( {"ok": False, "error": "service_not_supported"}, response.json(), ) def test_missing_whatsapp_target_returns_error(self): person, _, message = self._build_message( service="whatsapp", identifier="12345@s.whatsapp.net", source_message_id="", ) response = self.client.post( reverse("compose_react"), { "service": "whatsapp", "identifier": "12345@s.whatsapp.net", "person": str(person.id), "message_id": str(message.id), "emoji": "😂", }, ) self.assertEqual(200, response.status_code) self.assertEqual( {"ok": False, "error": "whatsapp_target_unresolvable"}, response.json(), ) @patch("core.views.compose.transport.send_reaction", new_callable=AsyncMock) def test_whatsapp_web_local_message_without_bridge_is_unresolvable( self, mocked_send_reaction ): person, _, message = self._build_message( service="whatsapp", identifier="12345@s.whatsapp.net", source_message_id="1771234567000", ) message.source_service = "web" message.save(update_fields=["source_service"]) response = self.client.post( reverse("compose_react"), { "service": "whatsapp", "identifier": "12345@s.whatsapp.net", "person": str(person.id), "message_id": str(message.id), "emoji": "😮", }, ) self.assertEqual(200, response.status_code) self.assertEqual( {"ok": False, "error": "whatsapp_target_unresolvable"}, response.json(), ) mocked_send_reaction.assert_not_awaited() def test_compose_page_renders_reaction_actions_for_signal(self): person, _, _ = self._build_message( service="signal", identifier="+15551230000", source_message_id="1771234567000", ) response = self.client.get( reverse("compose_page"), { "service": "signal", "identifier": "+15551230000", "person": str(person.id), }, ) self.assertEqual(200, response.status_code) content = response.content.decode("utf-8") self.assertIn("data-react-url=", content) self.assertIn("compose-reaction-actions", content) def test_compose_page_hides_reaction_actions_for_unsupported_service(self): person, _, _ = self._build_message( service="xmpp", identifier="person@example.com", source_message_id="msg-1", ) response = self.client.get( reverse("compose_page"), { "service": "xmpp", "identifier": "person@example.com", "person": str(person.id), }, ) self.assertEqual(200, response.status_code) self.assertNotIn( 'class="compose-reaction-actions"', response.content.decode("utf-8"), )