diff --git a/backend/tests/apps/slack/events/user_joined_channel/__init__.py b/backend/tests/apps/slack/admin/__init__.py similarity index 100% rename from backend/tests/apps/slack/events/user_joined_channel/__init__.py rename to backend/tests/apps/slack/admin/__init__.py diff --git a/backend/tests/apps/slack/admin/member_test.py b/backend/tests/apps/slack/admin/member_test.py new file mode 100644 index 0000000000..f0e5e3bd37 --- /dev/null +++ b/backend/tests/apps/slack/admin/member_test.py @@ -0,0 +1,69 @@ +from unittest.mock import MagicMock + +import pytest +from django.contrib import messages +from django.contrib.admin.sites import AdminSite + +from apps.slack.admin.member import MemberAdmin +from apps.slack.models.member import Member + + +@pytest.fixture +def admin_instance(): + return MemberAdmin(model=Member, admin_site=AdminSite()) + + +class TestMemberAdmin: + def test_approve_suggested_users_success(self, admin_instance): + request = MagicMock() + mock_suggested_user = MagicMock() + mock_member = MagicMock() + + mock_member.suggested_users.all.return_value.count.return_value = 1 + mock_member.suggested_users.all.return_value.first.return_value = mock_suggested_user + + admin_instance.message_user = MagicMock() + queryset = [mock_member] + + admin_instance.approve_suggested_users(request, queryset) + + assert mock_member.user == mock_suggested_user + mock_member.save.assert_called_once() + admin_instance.message_user.assert_called_with( + request, pytest.approx(f" assigned user for {mock_member}."), messages.SUCCESS + ) + + def test_approve_suggested_users_multiple_error(self, admin_instance): + request = MagicMock() + mock_member = MagicMock() + + mock_member.suggested_users.all.return_value.count.return_value = 2 + + admin_instance.message_user = MagicMock() + queryset = [mock_member] + + admin_instance.approve_suggested_users(request, queryset) + + mock_member.save.assert_not_called() + expected_message = ( + f"Error: Multiple suggested users found for {mock_member}. " + f"Only one user can be assigned due to the one-to-one constraint." + ) + + admin_instance.message_user.assert_called_with(request, expected_message, messages.ERROR) + + def test_approve_suggested_users_none_warning(self, admin_instance): + request = MagicMock() + mock_member = MagicMock() + + mock_member.suggested_users.all.return_value.count.return_value = 0 + + admin_instance.message_user = MagicMock() + queryset = [mock_member] + + admin_instance.approve_suggested_users(request, queryset) + + mock_member.save.assert_not_called() + admin_instance.message_user.assert_called_with( + request, f"No suggested users found for {mock_member}.", messages.WARNING + ) diff --git a/backend/tests/apps/slack/commands/command_test.py b/backend/tests/apps/slack/commands/command_test.py new file mode 100644 index 0000000000..61783b231e --- /dev/null +++ b/backend/tests/apps/slack/commands/command_test.py @@ -0,0 +1,99 @@ +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from apps.slack.blocks import DIVIDER, SECTION_BREAK +from apps.slack.commands.command import CommandBase + + +class Command(CommandBase): + pass + + +class TestCommandBase: + @pytest.fixture + def command_instance(self): + return Command() + + @pytest.fixture + def mock_command_payload(self): + return {"user_id": "U123ABC"} + + @patch("apps.slack.commands.command.logger") + @patch("apps.slack.commands.command.SlackConfig") + def test_configure_commands_when_app_is_none(self, mock_slack_config, mock_logger): + """Tests that a warning is logged if the Slack app is not configured.""" + mock_slack_config.app = None + CommandBase.configure_commands() + mock_logger.warning.assert_called_once() + + def test_command_name_property(self, command_instance): + """Tests that the command_name is derived correctly from the class name.""" + assert command_instance.command_name == "/command" + + def test_template_path_property(self, command_instance): + """Tests that the template_path is derived correctly.""" + assert command_instance.template_path == Path("commands/command.jinja") + + @patch("apps.slack.commands.command.env") + def test_template_property(self, mock_jinja_env, command_instance): + """Tests that the correct template is requested from the jinja environment.""" + _ = command_instance.template + + mock_jinja_env.get_template.assert_called_once_with("commands/command.jinja") + + def test_render_blocks(self, command_instance): + """Tests that the render_blocks method correctly parses rendered text into blocks.""" + test_string = f"Hello World{SECTION_BREAK}{DIVIDER}{SECTION_BREAK}Welcome to Nest" + + with patch.object(command_instance, "render_text", return_value=test_string): + blocks = command_instance.render_blocks(command={}) + + assert len(blocks) == 3 + assert blocks[0]["text"]["text"] == "Hello World" + assert blocks[1]["type"] == "divider" + assert blocks[2]["text"]["text"] == "Welcome to Nest" + + def test_handler_success(self, settings, command_instance, mock_command_payload): + """Tests the successful path of the command handler.""" + settings.SLACK_COMMANDS_ENABLED = True + ack = MagicMock() + mock_client = MagicMock() + mock_client.conversations_open.return_value = {"channel": {"id": "D123XYZ"}} + + with patch.object(command_instance, "render_blocks", return_value=[{"type": "section"}]): + command_instance.handler(ack=ack, command=mock_command_payload, client=mock_client) + + ack.assert_called_once() + mock_client.conversations_open.assert_called_once_with(users="U123ABC") + mock_client.chat_postMessage.assert_called_once() + assert mock_client.chat_postMessage.call_args[1]["channel"] == "D123XYZ" + + def test_handler_api_error(self, mocker, settings, command_instance, mock_command_payload): + """Tests that an exception during API calls is caught and logged.""" + settings.SLACK_COMMANDS_ENABLED = True + mock_logger = mocker.patch("apps.slack.commands.command.logger") + ack = MagicMock() + mock_client = MagicMock() + mock_client.chat_postMessage.side_effect = [Exception("API Error"), {"ok": True}] + mocker.patch.object(command_instance, "render_blocks", return_value=[{"type": "section"}]) + command_instance.handler(ack=ack, command=mock_command_payload, client=mock_client) + ack.assert_called_once() + mock_logger.exception.assert_called_once() + # Verify retry occurred and eventually succeeded + assert mock_client.chat_postMessage.call_count == 2 + mock_logger.exception.assert_called_with( + "Failed to handle command '%s'", command_instance.command_name + ) + + def test_handler_when_commands_disabled( + self, settings, command_instance, mock_command_payload + ): + """Tests that no message is sent when commands are disabled.""" + settings.SLACK_COMMANDS_ENABLED = False + ack = MagicMock() + mock_client = MagicMock() + command_instance.handler(ack=ack, command=mock_command_payload, client=mock_client) + ack.assert_called_once() + mock_client.chat_postMessage.assert_not_called() diff --git a/backend/tests/apps/slack/common/handlers/users_test.py b/backend/tests/apps/slack/common/handlers/users_test.py new file mode 100644 index 0000000000..dd7c72088b --- /dev/null +++ b/backend/tests/apps/slack/common/handlers/users_test.py @@ -0,0 +1,67 @@ +import pytest + +from apps.slack.common.handlers.users import get_blocks +from apps.slack.common.presentation import EntityPresentation + + +@pytest.fixture +def mock_users_data(): + return { + "hits": [ + { + "idx_name": "John Doe", + "idx_login": "johndoe", + "idx_url": "https://github.com/johndoe", + "idx_bio": "A passionate developer changing the world.", + "idx_location": "San Francisco", + "idx_company": "OWASP", + "idx_followers_count": 100, + "idx_following_count": 50, + "idx_public_repositories_count": 10, + } + ], + "nbPages": 3, + } + + +class TestGetUsersBlocks: + def test_get_blocks_no_results(self, mocker): + """Tests that a "No users found" message is returned when search results are empty.""" + mock_get_users = mocker.patch("apps.github.index.search.user.get_users") + mock_get_users.return_value = {"hits": [], "nbPages": 0} + blocks = get_blocks(search_query="nonexistent") + assert len(blocks) == 1 + assert "No users found for `nonexistent`" in blocks[0]["text"]["text"] + + def test_get_blocks_with_results(self, mocker, mock_users_data): + """Tests the happy path, ensuring user data is formatted correctly into blocks.""" + mocker.patch("apps.github.index.search.user.get_users", return_value=mock_users_data) + blocks = get_blocks(search_query="john") + assert len(blocks) > 1 + user_block_text = blocks[0]["text"]["text"] + assert "1. " in user_block_text + assert "Company: OWASP" in user_block_text + assert "Location: San Francisco" in user_block_text + assert "Followers: 100" in user_block_text + assert "A passionate developer" in user_block_text + + @pytest.mark.parametrize( + ("include_pagination", "should_call_pagination"), + [ + (True, True), + (False, False), + ], + ) + def test_get_blocks_pagination_logic( + self, mocker, mock_users_data, include_pagination, should_call_pagination + ): + mocker.patch("apps.github.index.search.user.get_users", return_value=mock_users_data) + mock_get_pagination = mocker.patch( + "apps.slack.common.handlers.users.get_pagination_buttons" + ) + presentation = EntityPresentation(include_pagination=include_pagination) + get_blocks(presentation=presentation) + if should_call_pagination: + mock_get_pagination.assert_called_once_with("users", 1, 2) + else: + mock_get_pagination.assert_not_called() diff --git a/backend/tests/apps/slack/events/event_test.py b/backend/tests/apps/slack/events/event_test.py new file mode 100644 index 0000000000..b9ced55985 --- /dev/null +++ b/backend/tests/apps/slack/events/event_test.py @@ -0,0 +1,170 @@ +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +from slack_sdk.errors import SlackApiError + +from apps.slack.events.event import EventBase + + +class MockEvent(EventBase): + event_type = "app_home_opened" + + +class TestEventBase: + """Tests for the EventBase class and its functionality.""" + + @pytest.fixture + def event_instance(self): + return MockEvent() + + @pytest.fixture + def mock_event_payload(self): + return {"user": "U123ABC", "channel": "C123XYZ"} + + @pytest.fixture + def mock_client(self): + client = MagicMock() + client.conversations_open.return_value = {"channel": {"id": "ABCDEF"}} + return client + + def test_direct_message_template_path(self, event_instance): + """Tests that the direct_message_template_path is derived correctly.""" + assert event_instance.direct_message_template_path == Path("events/mock_event.jinja") + + def test_ephemeral_message_template_path_is_none_by_default(self, event_instance): + """Tests that the ephemeral message path is None by default.""" + assert event_instance.ephemeral_message_template_path is None + + def test_handler_when_events_disabled( + self, settings, event_instance, mock_event_payload, mock_client + ): + """Tests that the handler exits early if events are disabled.""" + settings.SLACK_EVENTS_ENABLED = False + ack = MagicMock() + with patch.object(event_instance, "handle_event") as mock_handle_event: + event_instance.handler(event=mock_event_payload, client=mock_client, ack=ack) + ack.assert_called_once() + mock_handle_event.assert_not_called() + + def test_handler_catches_and_logs_generic_exception( + self, mocker, settings, event_instance, mock_event_payload, mock_client + ): + """Tests that a generic exception in handle_event is caught and logged.""" + settings.SLACK_EVENTS_ENABLED = True + ack = MagicMock() + mock_logger = mocker.patch("apps.slack.events.event.logger") + with patch.object( + event_instance, "handle_event", side_effect=Exception("Something went wrong") + ): + event_instance.handler(event=mock_event_payload, client=mock_client, ack=ack) + ack.assert_called_once() + mock_logger.exception.assert_called_once_with("Error handling %s", "app_home_opened") + + def test_handler_ignores_cannot_dm_bot_error( + self, mocker, settings, event_instance, mock_event_payload, mock_client + ): + """Tests that a SlackApiError for 'cannot_dm_bot' is caught and ignored.""" + settings.SLACK_EVENTS_ENABLED = True + ack = MagicMock() + mock_logger = mocker.patch("apps.slack.events.event.logger") + mock_slack_error = SlackApiError( + message="Cannot DM bot", response={"ok": False, "error": "cannot_dm_bot"} + ) + with patch.object(event_instance, "handle_event", side_effect=mock_slack_error): + event_instance.handler(event=mock_event_payload, client=mock_client, ack=ack) + ack.assert_called_once() + mock_logger.warning.assert_called_once() + mock_logger.exception.assert_not_called() + + def test_handle_event_sends_direct_message( + self, mocker, event_instance, mock_event_payload, mock_client + ): + """Tests that a direct message is correctly sent.""" + mock_template = MagicMock() + mock_template.render.return_value = "This is a direct message." + mocker.patch.object( + MockEvent, + "direct_message_template", + new_callable=mocker.PropertyMock, + return_value=mock_template, + ) + mocker.patch.object( + MockEvent, + "ephemeral_message_template", + new_callable=mocker.PropertyMock, + return_value=None, + ) + event_instance.handle_event(event=mock_event_payload, client=mock_client) + mock_client.chat_postMessage.assert_called_once() + assert mock_client.chat_postMessage.call_args[1]["channel"] == "ABCDEF" + mock_client.chat_postEphemeral.assert_not_called() + + def test_handler_fails_on_unknown_slack_error( + self, settings, event_instance, mock_event_payload, mock_client + ): + """Tests that SlackApiErrors other than 'cannot_dm_bot' are re-raised.""" + settings.SLACK_EVENTS_ENABLED = True + ack = MagicMock() + mock_slack_error = SlackApiError( + message="Other error", response={"ok": False, "error": "channel_not_found"} + ) + with ( + patch.object(event_instance, "handle_event", side_effect=mock_slack_error), + pytest.raises(SlackApiError), + ): + event_instance.handler(event=mock_event_payload, client=mock_client, ack=ack) + + def test_handle_event_sends_ephemeral_message( + self, mocker, event_instance, mock_event_payload, mock_client + ): + """Tests that an ephemeral message is correctly sent.""" + mock_template = MagicMock() + mock_template.render.return_value = "This is an ephemeral message." + mocker.patch.object( + MockEvent, + "direct_message_template", + new_callable=mocker.PropertyMock, + return_value=None, + ) + mocker.patch.object( + MockEvent, + "ephemeral_message_template", + new_callable=mocker.PropertyMock, + return_value=mock_template, + ) + event_instance.handle_event(event=mock_event_payload, client=mock_client) + mock_client.chat_postMessage.assert_not_called() + mock_client.chat_postEphemeral.assert_called_once() + assert mock_client.chat_postEphemeral.call_args[1]["user"] == "U123ABC" + + def test_render_blocks_no_template(self, event_instance): + """Tests that render_blocks returns an empty list if the template is None.""" + result = event_instance.render_blocks(template=None, context={}) + assert result == [] + + def test_open_conversation_no_user_id(self, event_instance, mock_client): + """Covers the edge case where user_id is missing.""" + result = event_instance.open_conversation(client=mock_client, user_id=None) + assert result is None + mock_client.conversations_open.assert_not_called() + + def test_configure_events_registers_events(self, mocker): + """Tests that subclasses are discovered and registered.""" + mock_slack_config = mocker.patch("apps.slack.events.event.SlackConfig") + mock_app = MagicMock() + mock_slack_config.app = mock_app + mock_event_class = MagicMock() + mocker.patch.object(EventBase, "get_events", return_value=[mock_event_class]) + EventBase.configure_events() + mock_event_class.return_value.register.assert_called_once() + + def test_register(self, mocker, event_instance): + """Tests that the register method correctly calls the app's event decorator.""" + mock_slack_config = mocker.patch("apps.slack.events.event.SlackConfig") + mock_app = MagicMock() + mock_slack_config.app = mock_app + event_instance.register() + mock_app.event.assert_called_once_with( + event_instance.event_type, matchers=event_instance.matchers + ) diff --git a/backend/tests/apps/slack/events/member_joined_channel/__init__.py b/backend/tests/apps/slack/events/member_joined_channel/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/tests/apps/slack/events/member_joined_channel/catch_all_test.py b/backend/tests/apps/slack/events/member_joined_channel/catch_all_test.py new file mode 100644 index 0000000000..446b13d4ec --- /dev/null +++ b/backend/tests/apps/slack/events/member_joined_channel/catch_all_test.py @@ -0,0 +1,13 @@ +from unittest.mock import MagicMock + +from apps.slack.events.member_joined_channel.catch_all import catch_all_handler + + +class TestMemberJoinedChannel: + """Tests for the catch_all_handler in member_joined_channel event.""" + + def test_catch_all_handler_acknowledges_event(self): + """Tests that the handler function correctly calls ack().""" + ack = MagicMock() + catch_all_handler(event={}, client=MagicMock(), ack=ack) + ack.assert_called_once() diff --git a/backend/tests/apps/slack/events/user_joined_channel/contribute_test.py b/backend/tests/apps/slack/events/member_joined_channel/contribute_test.py similarity index 100% rename from backend/tests/apps/slack/events/user_joined_channel/contribute_test.py rename to backend/tests/apps/slack/events/member_joined_channel/contribute_test.py diff --git a/backend/tests/apps/slack/events/user_joined_channel/gsoc_test.py b/backend/tests/apps/slack/events/member_joined_channel/gsoc_test.py similarity index 100% rename from backend/tests/apps/slack/events/user_joined_channel/gsoc_test.py rename to backend/tests/apps/slack/events/member_joined_channel/gsoc_test.py diff --git a/backend/tests/apps/slack/management/commands/__init__.py b/backend/tests/apps/slack/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/tests/apps/slack/management/commands/slack_set_conversation_sync_messages_flags_test.py b/backend/tests/apps/slack/management/commands/slack_set_conversation_sync_messages_flags_test.py new file mode 100644 index 0000000000..558f677b90 --- /dev/null +++ b/backend/tests/apps/slack/management/commands/slack_set_conversation_sync_messages_flags_test.py @@ -0,0 +1,29 @@ +from unittest.mock import MagicMock + +from apps.slack.management.commands.slack_set_conversation_sync_messages_flags import ( + SYNC_MESSAGES_CONVERSATIONS, + Command, +) + + +class TestSetSyncMessagesCommand: + def test_handle_method_calls_correct_orm_methods(self, mocker): + """Unit tests the handle method by mocking the ORM.""" + mock_conversation = mocker.patch( + "apps.slack.management.commands.slack_set_conversation_sync_messages_flags.Conversation" + ) + mock_workspace = mocker.patch( + "apps.slack.management.commands.slack_set_conversation_sync_messages_flags.Workspace" + ) + mock_default_workspace = MagicMock() + mock_workspace.get_default_workspace.return_value = mock_default_workspace + command = Command() + command.handle() + mock_conversation.objects.update.assert_called_once_with(sync_messages=False) + mock_conversation.objects.filter.assert_called_once_with( + is_private=False, + name__in=SYNC_MESSAGES_CONVERSATIONS, + workspace=mock_default_workspace, + ) + filtered_queryset_mock = mock_conversation.objects.filter.return_value + filtered_queryset_mock.update.assert_called_once_with(sync_messages=True) diff --git a/backend/tests/apps/slack/management/commands/slack_sync_messages_test.py b/backend/tests/apps/slack/management/commands/slack_sync_messages_test.py new file mode 100644 index 0000000000..f73c080a42 --- /dev/null +++ b/backend/tests/apps/slack/management/commands/slack_sync_messages_test.py @@ -0,0 +1,92 @@ +from unittest.mock import MagicMock + +from django.core.management import call_command + +from apps.slack.management.commands.slack_sync_messages import Command + + +class TestPopulateSlackMessages: + """Unit tests for the slack_sync_messages command.""" + + target_module = "apps.slack.management.commands.slack_sync_messages" + + def test_handle_command_flow(self, mocker): + """Tests the entire command flow by mocking the ORM and Slack client.""" + mocker.patch(f"{self.target_module}.Member") + mocker.patch(f"{self.target_module}.Message") + + mock_workspace = mocker.patch(f"{self.target_module}.Workspace") + mock_conversation = mocker.patch(f"{self.target_module}.Conversation") + mock_webclient = mocker.patch(f"{self.target_module}.WebClient") + + mock_workspace_instance = MagicMock() + mock_value = "test-token" + mock_workspace_instance.bot_token = mock_value + mock_queryset = MagicMock() + mock_queryset.exists.return_value = True + mock_queryset.__iter__.return_value = [mock_workspace_instance] + mock_workspace.objects.all.return_value = mock_queryset + mock_conversation.objects.filter.return_value = [MagicMock()] + + mock_client_instance = MagicMock() + mock_webclient.return_value = mock_client_instance + + mock_history_response = { + "ok": True, + "messages": [ + {"user": "U1", "reply_count": 1, "ts": "123.001"}, # Message WITH replies + {"user": "U2", "ts": "123.002"}, # Message WITHOUT replies + ], + "response_metadata": {"next_cursor": ""}, # No more pages + } + mock_client_instance.conversations_history.return_value = mock_history_response + mock_client_instance.conversations_replies.return_value = {"ok": True, "messages": []} + mock_client_instance.users_info.return_value = { + "ok": True, + "user": {"id": "U1", "name": "testuser"}, + } + + mock_message_with_replies = MagicMock() + mock_message_with_replies.has_replies = True + mock_message_without_replies = MagicMock() + mock_message_without_replies.has_replies = False + + mocker.patch.object( + Command, + "_create_message", + side_effect=[mock_message_with_replies, mock_message_without_replies], + ) + call_command("slack_sync_messages") + mock_client_instance.conversations_history.assert_called_once() + mock_client_instance.conversations_replies.assert_called_once() + + def test_handle_skips_workspace_without_token(self, mocker): + """Tests that a workspace is skipped if it has no bot token.""" + mock_workspace = mocker.patch(f"{self.target_module}.Workspace") + mock_webclient = mocker.patch(f"{self.target_module}.WebClient") + + mock_workspace_instance = MagicMock() + mock_workspace_instance.bot_token = None + + mock_queryset = MagicMock() + mock_queryset.exists.return_value = True + mock_queryset.__iter__.return_value = [mock_workspace_instance] + mock_workspace.objects.all.return_value = mock_queryset + + call_command("slack_sync_messages") + mock_webclient.assert_not_called() + + def test_handle_slack_response_on_failure(self, mocker): + """Tests that a non-OK Slack response is handled correctly.""" + mock_logger = mocker.patch(f"{self.target_module}.logger") + mock_stdout = MagicMock() + + failed_response = {"ok": False, "error": "some_error"} + + command = Command() + command.stdout = mock_stdout + + command._handle_slack_response(failed_response, "test_method") + + mock_logger.error.assert_called_once_with("test_method API call failed") + mock_stdout.write.assert_called_once() diff --git a/backend/tests/apps/slack/models/member_test.py b/backend/tests/apps/slack/models/member_test.py new file mode 100644 index 0000000000..f244c4cfa7 --- /dev/null +++ b/backend/tests/apps/slack/models/member_test.py @@ -0,0 +1,59 @@ +from unittest.mock import MagicMock, patch + +from apps.slack.models.member import Member +from apps.slack.models.workspace import Workspace + + +class TestMemberModel: + """Tests for the Member model.""" + + def test_str_method(self): + workspace = Workspace() + member = Member(username="testuser", slack_user_id="U123", workspace=workspace) + assert str(member) == "testuser (U123)" + + unnamed_member = Member(slack_user_id="U456", workspace=workspace) + assert str(unnamed_member) == "Unnamed (U456)" + + def test_from_slack_method(self): + member = Member() + workspace = Workspace() + slack_data = { + "id": "U123ABC", + "name": "testuser", + "real_name": "Test User", + "is_bot": False, + "profile": {"email": "test@example.com"}, + } + + member.from_slack(slack_data, workspace) + + assert member.slack_user_id == "U123ABC" + assert member.username == "testuser" + assert member.workspace == workspace + + def test_update_data_creates_new_member(self): + slack_data = {"id": "U123NEW", "name": "newuser", "is_bot": False} + workspace = Workspace() + + with ( + patch.object(Member, "save") as mock_save, + patch.object(Member.objects, "get", side_effect=Member.DoesNotExist) as mock_get, + ): + member = Member.update_data(slack_data, workspace, save=True) + + mock_get.assert_called_once_with(slack_user_id="U123NEW") + mock_save.assert_called_once() + assert member.username == "newuser" + + def test_update_data_updates_existing_member(self): + updated_data = {"id": "U123EXISTING", "name": "newname", "is_bot": False} + workspace = Workspace() + mock_existing_member = MagicMock() + + with patch.object(Member.objects, "get", return_value=mock_existing_member) as mock_get: + Member.update_data(updated_data, workspace, save=True) + + mock_get.assert_called_once_with(slack_user_id="U123EXISTING") + mock_existing_member.from_slack.assert_called_once_with(updated_data, workspace) + mock_existing_member.save.assert_called_once() diff --git a/backend/tests/apps/slack/models/message_test.py b/backend/tests/apps/slack/models/message_test.py new file mode 100644 index 0000000000..f41facac20 --- /dev/null +++ b/backend/tests/apps/slack/models/message_test.py @@ -0,0 +1,99 @@ +from datetime import UTC, datetime +from unittest.mock import MagicMock, patch + +from apps.slack.models.conversation import Conversation +from apps.slack.models.member import Member +from apps.slack.models.message import Message +from apps.slack.models.workspace import Workspace + + +class TestMessageModel: + """Tests for the Message model.""" + + def test_str_representation(self): + message = Message(raw_data={"text": "This is the message text"}) + assert str(message) == "This is the message text" + + huddle_message = Message(raw_data={"subtype": "huddle_thread", "channel": "C123"}) + assert str(huddle_message) == "C123 huddle" + + def test_cleaned_text_property(self): + message = Message() + message.raw_data = { + "text": "Hello <@U123> check this :smile: and this :thumbs_up:" + } + expected_text = "Hello check this and this" + assert message.cleaned_text == expected_text + + message.raw_data = {"text": " Extra whitespace "} + assert message.cleaned_text == "Extra whitespace" + + def test_url_property(self): + workspace = Workspace(name="MyWorkspace") + conversation = Conversation(workspace=workspace, slack_channel_id="C123") + message = Message(conversation=conversation, slack_message_id="12345.67890") + expected_url = "https://myworkspace.slack.com/archives/C123/p1234567890" + assert message.url == expected_url + + def test_latest_reply_property(self): + message = Message(conversation=Conversation()) + + with patch.object(Message.objects, "filter") as mock_filter: + _ = message.latest_reply + + mock_filter.assert_called_once_with( + conversation=message.conversation, parent_message=message + ) + mock_filter.return_value.order_by.assert_called_once_with("-created_at") + mock_filter.return_value.order_by.return_value.first.assert_called_once() + + def test_from_slack_method(self): + message = Message() + conversation = Conversation() + author = Member() + parent = Message() + + slack_data = { + "ts": "1672531200.000", # 2023-01-01 + "reply_count": 5, + "bot_id": "B123", + } + + message.from_slack(slack_data, conversation, author, parent_message=parent) + + assert message.created_at == datetime(2023, 1, 1, tzinfo=UTC) + assert message.has_replies is True + assert message.is_bot is True + assert message.raw_data == slack_data + assert message.slack_message_id == "1672531200.000" + assert message.author == author + assert message.conversation == conversation + assert message.parent_message == parent + + def test_update_data_creates_new_message(self): + slack_data = {"ts": "12345.001"} + conversation = Conversation() + + with ( + patch.object(Message, "save") as mock_save, + patch.object(Message.objects, "get", side_effect=Message.DoesNotExist) as mock_get, + ): + message = Message.update_data(slack_data, conversation, save=True) + + mock_get.assert_called_once_with(slack_message_id="12345.001", conversation=conversation) + mock_save.assert_called_once() + assert isinstance(message, Message) + assert message.slack_message_id == "12345.001" + + def test_update_data_updates_existing_message(self): + slack_data = {"ts": "12345.001"} + conversation = Conversation() + mock_existing_message = MagicMock(spec=Message) + + with patch.object(Message.objects, "get", return_value=mock_existing_message) as mock_get: + message = Message.update_data(slack_data, conversation, save=True) + + mock_get.assert_called_once() + mock_existing_message.from_slack.assert_called_once() + mock_existing_message.save.assert_called_once() + assert message == mock_existing_message diff --git a/backend/tests/apps/slack/views_test.py b/backend/tests/apps/slack/views_test.py new file mode 100644 index 0000000000..d88bf62385 --- /dev/null +++ b/backend/tests/apps/slack/views_test.py @@ -0,0 +1,20 @@ +from django.test import RequestFactory + + +class TestViews: + """Tests for the Slack views.""" + + def test_slack_request_handler_works(self, mocker): + """Tests that the view correctly calls the SlackRequestHandler.""" + mock_slack_request_handler = mocker.patch("slack_bolt.adapter.django.SlackRequestHandler") + + mock_handler_instance = mock_slack_request_handler.return_value + mock_handler_instance.handle.return_value = "mock_http_response" + + from apps.slack.views import slack_request_handler + + request = RequestFactory().post("/slack/events/") + response = slack_request_handler(request) + + mock_handler_instance.handle.assert_called_once_with(request) + assert response == "mock_http_response"