Skip to content

Commit 94566f3

Browse files
Improve backend/apps/slack test coverage (#1956)
* Improve Slack app test coverage * improve test for slack admin * coderabbit suggestions * Update code --------- Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org> Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com>
1 parent 438b5a3 commit 94566f3

File tree

15 files changed

+717
-0
lines changed

15 files changed

+717
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from unittest.mock import MagicMock
2+
3+
import pytest
4+
from django.contrib import messages
5+
from django.contrib.admin.sites import AdminSite
6+
7+
from apps.slack.admin.member import MemberAdmin
8+
from apps.slack.models.member import Member
9+
10+
11+
@pytest.fixture
12+
def admin_instance():
13+
return MemberAdmin(model=Member, admin_site=AdminSite())
14+
15+
16+
class TestMemberAdmin:
17+
def test_approve_suggested_users_success(self, admin_instance):
18+
request = MagicMock()
19+
mock_suggested_user = MagicMock()
20+
mock_member = MagicMock()
21+
22+
mock_member.suggested_users.all.return_value.count.return_value = 1
23+
mock_member.suggested_users.all.return_value.first.return_value = mock_suggested_user
24+
25+
admin_instance.message_user = MagicMock()
26+
queryset = [mock_member]
27+
28+
admin_instance.approve_suggested_users(request, queryset)
29+
30+
assert mock_member.user == mock_suggested_user
31+
mock_member.save.assert_called_once()
32+
admin_instance.message_user.assert_called_with(
33+
request, pytest.approx(f" assigned user for {mock_member}."), messages.SUCCESS
34+
)
35+
36+
def test_approve_suggested_users_multiple_error(self, admin_instance):
37+
request = MagicMock()
38+
mock_member = MagicMock()
39+
40+
mock_member.suggested_users.all.return_value.count.return_value = 2
41+
42+
admin_instance.message_user = MagicMock()
43+
queryset = [mock_member]
44+
45+
admin_instance.approve_suggested_users(request, queryset)
46+
47+
mock_member.save.assert_not_called()
48+
expected_message = (
49+
f"Error: Multiple suggested users found for {mock_member}. "
50+
f"Only one user can be assigned due to the one-to-one constraint."
51+
)
52+
53+
admin_instance.message_user.assert_called_with(request, expected_message, messages.ERROR)
54+
55+
def test_approve_suggested_users_none_warning(self, admin_instance):
56+
request = MagicMock()
57+
mock_member = MagicMock()
58+
59+
mock_member.suggested_users.all.return_value.count.return_value = 0
60+
61+
admin_instance.message_user = MagicMock()
62+
queryset = [mock_member]
63+
64+
admin_instance.approve_suggested_users(request, queryset)
65+
66+
mock_member.save.assert_not_called()
67+
admin_instance.message_user.assert_called_with(
68+
request, f"No suggested users found for {mock_member}.", messages.WARNING
69+
)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from pathlib import Path
2+
from unittest.mock import MagicMock, patch
3+
4+
import pytest
5+
6+
from apps.slack.blocks import DIVIDER, SECTION_BREAK
7+
from apps.slack.commands.command import CommandBase
8+
9+
10+
class Command(CommandBase):
11+
pass
12+
13+
14+
class TestCommandBase:
15+
@pytest.fixture
16+
def command_instance(self):
17+
return Command()
18+
19+
@pytest.fixture
20+
def mock_command_payload(self):
21+
return {"user_id": "U123ABC"}
22+
23+
@patch("apps.slack.commands.command.logger")
24+
@patch("apps.slack.commands.command.SlackConfig")
25+
def test_configure_commands_when_app_is_none(self, mock_slack_config, mock_logger):
26+
"""Tests that a warning is logged if the Slack app is not configured."""
27+
mock_slack_config.app = None
28+
CommandBase.configure_commands()
29+
mock_logger.warning.assert_called_once()
30+
31+
def test_command_name_property(self, command_instance):
32+
"""Tests that the command_name is derived correctly from the class name."""
33+
assert command_instance.command_name == "/command"
34+
35+
def test_template_path_property(self, command_instance):
36+
"""Tests that the template_path is derived correctly."""
37+
assert command_instance.template_path == Path("commands/command.jinja")
38+
39+
@patch("apps.slack.commands.command.env")
40+
def test_template_property(self, mock_jinja_env, command_instance):
41+
"""Tests that the correct template is requested from the jinja environment."""
42+
_ = command_instance.template
43+
44+
mock_jinja_env.get_template.assert_called_once_with("commands/command.jinja")
45+
46+
def test_render_blocks(self, command_instance):
47+
"""Tests that the render_blocks method correctly parses rendered text into blocks."""
48+
test_string = f"Hello World{SECTION_BREAK}{DIVIDER}{SECTION_BREAK}Welcome to Nest"
49+
50+
with patch.object(command_instance, "render_text", return_value=test_string):
51+
blocks = command_instance.render_blocks(command={})
52+
53+
assert len(blocks) == 3
54+
assert blocks[0]["text"]["text"] == "Hello World"
55+
assert blocks[1]["type"] == "divider"
56+
assert blocks[2]["text"]["text"] == "Welcome to Nest"
57+
58+
def test_handler_success(self, settings, command_instance, mock_command_payload):
59+
"""Tests the successful path of the command handler."""
60+
settings.SLACK_COMMANDS_ENABLED = True
61+
ack = MagicMock()
62+
mock_client = MagicMock()
63+
mock_client.conversations_open.return_value = {"channel": {"id": "D123XYZ"}}
64+
65+
with patch.object(command_instance, "render_blocks", return_value=[{"type": "section"}]):
66+
command_instance.handler(ack=ack, command=mock_command_payload, client=mock_client)
67+
68+
ack.assert_called_once()
69+
mock_client.conversations_open.assert_called_once_with(users="U123ABC")
70+
mock_client.chat_postMessage.assert_called_once()
71+
assert mock_client.chat_postMessage.call_args[1]["channel"] == "D123XYZ"
72+
73+
def test_handler_api_error(self, mocker, settings, command_instance, mock_command_payload):
74+
"""Tests that an exception during API calls is caught and logged."""
75+
settings.SLACK_COMMANDS_ENABLED = True
76+
mock_logger = mocker.patch("apps.slack.commands.command.logger")
77+
ack = MagicMock()
78+
mock_client = MagicMock()
79+
mock_client.chat_postMessage.side_effect = [Exception("API Error"), {"ok": True}]
80+
mocker.patch.object(command_instance, "render_blocks", return_value=[{"type": "section"}])
81+
command_instance.handler(ack=ack, command=mock_command_payload, client=mock_client)
82+
ack.assert_called_once()
83+
mock_logger.exception.assert_called_once()
84+
# Verify retry occurred and eventually succeeded
85+
assert mock_client.chat_postMessage.call_count == 2
86+
mock_logger.exception.assert_called_with(
87+
"Failed to handle command '%s'", command_instance.command_name
88+
)
89+
90+
def test_handler_when_commands_disabled(
91+
self, settings, command_instance, mock_command_payload
92+
):
93+
"""Tests that no message is sent when commands are disabled."""
94+
settings.SLACK_COMMANDS_ENABLED = False
95+
ack = MagicMock()
96+
mock_client = MagicMock()
97+
command_instance.handler(ack=ack, command=mock_command_payload, client=mock_client)
98+
ack.assert_called_once()
99+
mock_client.chat_postMessage.assert_not_called()
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import pytest
2+
3+
from apps.slack.common.handlers.users import get_blocks
4+
from apps.slack.common.presentation import EntityPresentation
5+
6+
7+
@pytest.fixture
8+
def mock_users_data():
9+
return {
10+
"hits": [
11+
{
12+
"idx_name": "John Doe",
13+
"idx_login": "johndoe",
14+
"idx_url": "https://github.com/johndoe",
15+
"idx_bio": "A passionate developer changing the world.",
16+
"idx_location": "San Francisco",
17+
"idx_company": "OWASP",
18+
"idx_followers_count": 100,
19+
"idx_following_count": 50,
20+
"idx_public_repositories_count": 10,
21+
}
22+
],
23+
"nbPages": 3,
24+
}
25+
26+
27+
class TestGetUsersBlocks:
28+
def test_get_blocks_no_results(self, mocker):
29+
"""Tests that a "No users found" message is returned when search results are empty."""
30+
mock_get_users = mocker.patch("apps.github.index.search.user.get_users")
31+
mock_get_users.return_value = {"hits": [], "nbPages": 0}
32+
blocks = get_blocks(search_query="nonexistent")
33+
assert len(blocks) == 1
34+
assert "No users found for `nonexistent`" in blocks[0]["text"]["text"]
35+
36+
def test_get_blocks_with_results(self, mocker, mock_users_data):
37+
"""Tests the happy path, ensuring user data is formatted correctly into blocks."""
38+
mocker.patch("apps.github.index.search.user.get_users", return_value=mock_users_data)
39+
blocks = get_blocks(search_query="john")
40+
assert len(blocks) > 1
41+
user_block_text = blocks[0]["text"]["text"]
42+
assert "1. <https://github.com/johndoe|*John Doe*>" in user_block_text
43+
assert "Company: OWASP" in user_block_text
44+
assert "Location: San Francisco" in user_block_text
45+
assert "Followers: 100" in user_block_text
46+
assert "A passionate developer" in user_block_text
47+
48+
@pytest.mark.parametrize(
49+
("include_pagination", "should_call_pagination"),
50+
[
51+
(True, True),
52+
(False, False),
53+
],
54+
)
55+
def test_get_blocks_pagination_logic(
56+
self, mocker, mock_users_data, include_pagination, should_call_pagination
57+
):
58+
mocker.patch("apps.github.index.search.user.get_users", return_value=mock_users_data)
59+
mock_get_pagination = mocker.patch(
60+
"apps.slack.common.handlers.users.get_pagination_buttons"
61+
)
62+
presentation = EntityPresentation(include_pagination=include_pagination)
63+
get_blocks(presentation=presentation)
64+
if should_call_pagination:
65+
mock_get_pagination.assert_called_once_with("users", 1, 2)
66+
else:
67+
mock_get_pagination.assert_not_called()

0 commit comments

Comments
 (0)