Skip to content

Commit

Permalink
test(purrr): enable unit tests without AK (#149)
Browse files Browse the repository at this point in the history
Improve mocking, and expect dummy GitHub and Slack clients instead of
`ConnectionInitError` when the AutoKitteh Python SDK is running within
unit tests.

~This PR depends on autokitteh/autokitteh#964
being merged, and a new AutoKitteh Python SDK version released in PyPI -
which is why unit tests are currently failing.~

Also found and fixed a small bug in `slack_cmd.py`

Refs: INT-159
  • Loading branch information
daabr authored Jan 9, 2025
1 parent bdc9ee1 commit e9c8143
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 118 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
ruff format --check .
- name: Test with pytest
run: pytest -v --ignore=purrr .
run: pytest -v .

- name: Verify README.md is up to date
run: |
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ lint: deps
ruff check --fix --output-format full .

test: deps
pytest -v --ignore=purrr .
pytest -v .

.PHONY: all deps format lint test
98 changes: 46 additions & 52 deletions purrr/markdown_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@

import collections
import unittest
from unittest.mock import MagicMock
from unittest import mock

import markdown
import users
from autokitteh import github, slack


# This is needed before calling "import markdown" to avoid "ConnectionInitError"
# when initializing these clients in Purrr modules, due to the lack of AutoKitteh
# environment variables. This is also the reason for the "noqa" comments below.
github.github_client = mock.MagicMock()
slack.slack_client = mock.MagicMock()

import markdown # noqa: E402
import users # noqa: E402


class MarkdownGithubToSlackTest(unittest.TestCase):
Expand Down Expand Up @@ -93,9 +102,9 @@ def test_nested_lists(self):
" • 111\n ◦ 222\n ◦ 333\n • 444",
)

def test_user_mentions(self):
users.github_username_to_slack_user_id = MagicMock()
users.github_username_to_slack_user_id.side_effect = ["U123", None, None]
@mock.patch.object(users, "github_username_to_slack_user_id", autospec=True)
def test_user_mentions(self, mock_func):
mock_func.side_effect = ["U123", None, None]

# Slack user found.
self.assertEqual(
Expand Down Expand Up @@ -182,11 +191,11 @@ def test_links(self):
self.assertEqual(markdown.slack_to_github("<url|>"), "[](url)")
self.assertEqual(markdown.slack_to_github("<url>"), "<url>")

def test_channel(self):
markdown._slack_channel_name = MagicMock()
markdown._slack_channel_name.return_value = "channel"
markdown._slack_team_id = MagicMock()
markdown._slack_team_id.return_value = "TEAM_ID"
@mock.patch.object(markdown, "_slack_team_id", autospec=True)
@mock.patch.object(markdown, "_slack_channel_name", autospec=True)
def test_channel(self, mock_channel_name, mock_team_id):
mock_channel_name.return_value = "channel"
mock_team_id.return_value = "TEAM_ID"

self.assertEqual(
markdown.slack_to_github("<#C123>"),
Expand All @@ -205,72 +214,57 @@ def test_channel(self):
FakeGithubUser = collections.namedtuple("FakeGithubUser", ["name", "login"])


@mock.patch.object(users, "_slack_user_info", autospec=True)
@mock.patch.object(users, "_github_users", autospec=True)
@mock.patch.object(users, "_email_to_github_user_id", autospec=True)
class MarkdownSlackToGithubUserMentionsTest(unittest.TestCase):
"""Unit tests for user mentions in the "slack_to_github" function."""

def setUp(self):
super().setUp()

self._slack_user_info = users._slack_user_info
users._slack_user_info = MagicMock()

self.__email_to_github_user_id = users._email_to_github_user_id
users._email_to_github_user_id = MagicMock()

self._github_users = users._github_users
users._github_users = MagicMock()

def tearDown(self):
users._github_users = self._github_users
users._email_to_github_user_id = self.__email_to_github_user_id
users._slack_user_info = self._slack_user_info
super().tearDown()

def test_slack_user_info_error(self):
users._slack_user_info.return_value = {}
def test_slack_user_info_error(self, mock_email, mock_github, mock_slack):
mock_slack.return_value = {}
self.assertEqual(markdown.slack_to_github("<@U123>"), "Someone")

def test_email_and_name_not_found_in_slack_profile(self):
users._slack_user_info.return_value = {"profile": {"foo": "bar"}}
def test_email_and_name_not_found_in_slack_profile(
self, mock_email, mock_github, mock_slack
):
mock_slack.return_value = {"profile": {"foo": "bar"}}
self.assertEqual(markdown.slack_to_github("<@U123>"), "Someone")

def test_named_and_unnamed_slack_apps(self):
users._slack_user_info.side_effect = [
def test_named_and_unnamed_slack_apps(self, mock_email, mock_github, mock_slack):
mock_slack.side_effect = [
{"is_bot": True, "profile": {"real_name": "Mr. Robot"}},
{"is_bot": True},
]
self.assertEqual(markdown.slack_to_github("<@U123>"), "Mr. Robot")
self.assertEqual(markdown.slack_to_github("<@U123>"), "Some Slack app")

def test_match_by_email(self):
users._slack_user_info.return_value = {"profile": {"email": "[email protected]"}}
users._email_to_github_user_id.return_value = "username"
def test_match_by_email(self, mock_email, mock_github, mock_slack):
mock_slack.return_value = {"profile": {"email": "[email protected]"}}
mock_email.return_value = "username"
self.assertEqual(markdown.slack_to_github("<@U123>"), "@username")

def test_match_by_name(self):
users._slack_user_info.return_value = {
def test_match_by_name(self, mock_email, mock_github, mock_slack):
mock_slack.return_value = {
"profile": {"email": "[email protected]", "real_name": "John Doe"}
}
users._email_to_github_user_id.return_value = ""
users._github_users.return_value = [
FakeGithubUser("John Doe", "username"),
]
mock_email.return_value = ""
mock_github.return_value = [FakeGithubUser("John Doe", "username")]
self.assertEqual(markdown.slack_to_github("<@U123>"), "@username")

def test_no_matches_by_name(self):
users._slack_user_info.return_value = {
def test_no_matches_by_name(self, mock_email, mock_github, mock_slack):
mock_slack.return_value = {
"profile": {"email": "[email protected]", "real_name": "John Doe"}
}
users._email_to_github_user_id.return_value = ""
users._github_users.return_value = []
mock_email.return_value = ""
mock_github.return_value = []
self.assertEqual(markdown.slack_to_github("<@U123>"), "John Doe")

def test_too_many_matches_by_name(self):
users._slack_user_info.return_value = {
def test_too_many_matches_by_name(self, mock_email, mock_github, mock_slack):
mock_slack.return_value = {
"profile": {"email": "[email protected]", "real_name": "John Doe"}
}
users._email_to_github_user_id.return_value = ""
users._github_users.return_value = [
mock_email.return_value = ""
mock_github.return_value = [
FakeGithubUser("John Doe", "username1"),
FakeGithubUser("john doe", "username2"),
FakeGithubUser("JOHN DOE", "username3"),
Expand Down
1 change: 1 addition & 0 deletions purrr/slack_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def on_slack_slash_command(event):

if args[0] in _COMMANDS:
_COMMANDS[args[0]].handler(data, args)
return

error = f"Error: unrecognized Purrr command: `{args[0]}`"
slack.chat_postEphemeral(channel=data.channel_id, user=data.user_id, text=error)
Expand Down
114 changes: 51 additions & 63 deletions purrr/slack_cmd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,127 +2,115 @@

from datetime import datetime
import unittest
from unittest.mock import MagicMock
from unittest import mock

import autokitteh
from slack_sdk.web.client import WebClient

import slack_cmd


class SlackCmdTest(unittest.TestCase):
"""Unit tests for the "slack_cmd" module."""

def __init__(self, method_name="runTest"):
super().__init__(method_name)
self.data = {
"channel_id": "purr-debug",
"user_id": "user",
"command": "/purrr",
}
fake_data = {
"channel_id": "purr-debug",
"user_id": "user",
"command": "/purrr",
}

def setUp(self):
super().setUp()

self.slack = slack_cmd.slack
slack_cmd.slack = MagicMock()

self.data_helper = slack_cmd.data_helper
slack_cmd.data_helper = MagicMock()

def tearDown(self):
slack_cmd.data_helper = self.data_helper
slack_cmd.slack = self.slack
super().tearDown()
@mock.patch.object(slack_cmd, "slack", spec=WebClient)
@mock.patch.object(slack_cmd, "data_helper", autospec=True)
class SlackCmdTest(unittest.TestCase):
"""Unit tests for the "slack_cmd" module."""

def test_help_text(self):
data = autokitteh.AttrDict(self.data)
def test_help_text(self, *_):
data = autokitteh.AttrDict(fake_data)
text = slack_cmd._help_text(data)

for cmd in slack_cmd._COMMANDS.values():
self.assertIn(cmd.label, text)
self.assertIn(cmd.description, text)

def test_on_slack_slash_command_without_text(self):
event = autokitteh.AttrDict({"data": self.data | {"text": ""}})
def test_on_slack_slash_command_without_text(self, _, mock_slack):
event = autokitteh.AttrDict({"data": fake_data | {"text": ""}})
slack_cmd.on_slack_slash_command(event)

slack_cmd.slack.chat_postEphemeral.assert_called_once_with(
mock_slack.chat_postEphemeral.assert_called_once_with(
channel=event.data.channel_id,
user=event.data.user_id,
text=slack_cmd._help_text(event.data),
)

def test_on_slack_slash_command_with_help(self):
event = autokitteh.AttrDict({"data": self.data | {"text": "help"}})
def test_on_slack_slash_command_with_help(self, _, mock_slack):
event = autokitteh.AttrDict({"data": fake_data | {"text": "help"}})
slack_cmd.on_slack_slash_command(event)

slack_cmd.slack.chat_postEphemeral.assert_called_once_with(
mock_slack.chat_postEphemeral.assert_called_once_with(
channel=event.data.channel_id,
user=event.data.user_id,
text=slack_cmd._help_text(event.data),
)

def test_on_slack_slash_command_with_noop_opt_in(self):
slack_cmd.data_helper.slack_opted_out.return_value = ""
def test_on_slack_slash_command_with_noop_opt_in(
self, mock_data_helper, mock_slack
):
mock_data_helper.slack_opted_out.return_value = ""

event = autokitteh.AttrDict({"data": self.data | {"text": "opt-in"}})
event = autokitteh.AttrDict({"data": fake_data | {"text": "opt-in"}})
slack_cmd.on_slack_slash_command(event)

slack_cmd.data_helper.slack_opted_out.assert_called_once_with(
event.data.user_id
)
slack_cmd.data_helper.slack_opt_in.assert_not_called()
slack_cmd.slack.chat_postEphemeral.assert_called_once_with(
mock_data_helper.slack_opted_out.assert_called_once_with(event.data.user_id)
mock_data_helper.slack_opt_in.assert_not_called()
mock_slack.chat_postEphemeral.assert_called_once_with(
channel=event.data.channel_id,
user=event.data.user_id,
text=":bell: You're already opted into Purrr",
)

def test_on_slack_slash_command_with_actual_opt_in(self):
slack_cmd.data_helper.slack_opted_out.return_value = datetime.min
def test_on_slack_slash_command_with_actual_opt_in(
self, mock_data_helper, mock_slack
):
mock_data_helper.slack_opted_out.return_value = datetime.min

event = autokitteh.AttrDict({"data": self.data | {"text": "opt-in"}})
event = autokitteh.AttrDict({"data": fake_data | {"text": "opt-in"}})
slack_cmd.on_slack_slash_command(event)

slack_cmd.data_helper.slack_opted_out.assert_called_once_with(
event.data.user_id
)
slack_cmd.data_helper.slack_opt_in.assert_called_once_with(event.data.user_id)
slack_cmd.slack.chat_postEphemeral.assert_called_once_with(
mock_data_helper.slack_opted_out.assert_called_once_with(event.data.user_id)
mock_data_helper.slack_opt_in.assert_called_once_with(event.data.user_id)
mock_slack.chat_postEphemeral.assert_called_once_with(
channel=event.data.channel_id,
user=event.data.user_id,
text=":bell: You are now opted into Purrr",
)

def test_on_slack_slash_command_with_noop_opt_out(self):
slack_cmd.data_helper.slack_opted_out.return_value = datetime.min
def test_on_slack_slash_command_with_noop_opt_out(
self, mock_data_helper, mock_slack
):
mock_data_helper.slack_opted_out.return_value = datetime.min

event = autokitteh.AttrDict({"data": self.data | {"text": "opt-out"}})
event = autokitteh.AttrDict({"data": fake_data | {"text": "opt-out"}})
slack_cmd.on_slack_slash_command(event)

slack_cmd.data_helper.slack_opted_out.assert_called_once_with(
event.data.user_id
)
slack_cmd.data_helper.slack_opt_out.assert_not_called()
slack_cmd.slack.chat_postEphemeral.assert_called_once_with(
mock_data_helper.slack_opted_out.assert_called_once_with(event.data.user_id)
mock_data_helper.slack_opt_out.assert_not_called()
mock_slack.chat_postEphemeral.assert_called_once_with(
channel=event.data.channel_id,
user=event.data.user_id,
text=(
":no_bell: You're already opted out of Purrr since: 0001-01-01 00:00:00"
),
)

def test_on_slack_slash_command_with_second_opt_out(self):
slack_cmd.data_helper.slack_opted_out.return_value = ""
def test_on_slack_slash_command_with_second_opt_out(
self, mock_data_helper, mock_slack
):
mock_data_helper.slack_opted_out.return_value = ""

event = autokitteh.AttrDict({"data": self.data | {"text": "opt-out"}})
event = autokitteh.AttrDict({"data": fake_data | {"text": "opt-out"}})
slack_cmd.on_slack_slash_command(event)

slack_cmd.data_helper.slack_opted_out.assert_called_once_with(
event.data.user_id
)
slack_cmd.data_helper.slack_opt_out.assert_called_once_with(event.data.user_id)
slack_cmd.slack.chat_postEphemeral.assert_called_once_with(
mock_data_helper.slack_opted_out.assert_called_once_with(event.data.user_id)
mock_data_helper.slack_opt_out.assert_called_once_with(event.data.user_id)
mock_slack.chat_postEphemeral.assert_called_once_with(
channel=event.data.channel_id,
user=event.data.user_id,
text=":no_bell: You are now opted out of Purrr",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ order-by-type = false
known-third-party = ["discord", "github"]

force-single-line = true
single-line-exclusions = ["autokitteh.atlassian", "autokitteh.google", "datetime", "typing"]
single-line-exclusions = ["autokitteh", "autokitteh.atlassian", "autokitteh.google", "datetime", "typing"]

[tool.ruff.lint.pydocstyle]
convention = "google"
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
pytest
ruff

autokitteh
google-auth
PyGithub
slack-sdk

0 comments on commit e9c8143

Please sign in to comment.