diff --git a/requirements.txt b/requirements.txt index 6aac4ed6..1b753fcc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ fedmsg fedora-messaging PyGithub pypandoc_binary +python-gitlab urllib3 jinja2 flask diff --git a/sync2jira/api/gitlab_client.py b/sync2jira/api/gitlab_client.py new file mode 100644 index 00000000..99abe3f6 --- /dev/null +++ b/sync2jira/api/gitlab_client.py @@ -0,0 +1,39 @@ +from gitlab import Gitlab + + +class GitlabClient: + + def __init__(self, url, token, project): + self.url = url + self.token = token + self.project = project + self._client = Gitlab(url=url, private_token=token) + self._project = self._client.get(self.project) + + def fetch_issue(self, iid): + return self._project.issues.get(iid) + + def fetch_notes_for_issue(self, iid): + issue = self.fetch_issue(iid) + return GitlabClient.map_notes_to_intermediary(issue.notes.list(all=True)) + + def fetch_mr(self, iid): + return self._project.mergerequests.get(iid) + + def fetch_notes_for_mr(self, iid): + mr = self.fetch_mr(iid) + return GitlabClient.map_notes_to_intermediary(mr.notes.list(all=True)) + + @staticmethod + def map_notes_to_intermediary(notes): + return [ + { + "author": note.author.username, + "name": note.author.name, + "body": note.body, + "id": note.id, + "date_created": note.created_at, + "changed": note.updated_at, + } + for note in notes + ] diff --git a/sync2jira/handler/base.py b/sync2jira/handler/base.py new file mode 100644 index 00000000..be142b3a --- /dev/null +++ b/sync2jira/handler/base.py @@ -0,0 +1,23 @@ +import logging + +# Local Modules +import sync2jira.handler.github as gh +import sync2jira.handler.gitlab as gl + +log = logging.getLogger("sync2jira") + + +def get_handler_for(suffix, topic, idx): + """ + Function to check if a handler for given suffix is configured + :param String suffix: Incoming suffix + :param String topic: Topic of incoming message + :param String idx: Id of incoming message + :returns: Handler function if configured for suffix. Otherwise None. + """ + if suffix.startswith("github"): + return gh.get_handler_for(suffix, topic, idx) + elif suffix.startswith("gitlab"): + return gl.get_handler_for(suffix, topic, idx) + log.info("Unsupported datasource %r", suffix) + return None diff --git a/sync2jira/handler/github.py b/sync2jira/handler/github.py new file mode 100644 index 00000000..9bf78a21 --- /dev/null +++ b/sync2jira/handler/github.py @@ -0,0 +1,110 @@ +import logging + +# Local Modules +import sync2jira.downstream_issue as d_issue +import sync2jira.downstream_pr as d_pr +import sync2jira.handler.github_upstream_issue as u_issue +import sync2jira.handler.github_upstream_pr as u_pr +from sync2jira.intermediary import matcher + +log = logging.getLogger("sync2jira") + + +def handle_issue_msg(body, headers, suffix, config): + """ + Function to handle incoming github issue message + :param Dict body: Incoming message body + :param Dict headers: Incoming message headers + :param String suffix: Incoming suffix + :param Dict config: Config dict + """ + # GitHub '.issue*' is used for both PR and Issue; check if this update + # is actually for a PR + if "pull_request" in body["issue"]: + if body["action"] == "deleted": + # I think this gets triggered when someone deletes a comment + # from a PR. Since we don't capture PR comments (only Issue + # comments), we don't need to react if one is deleted. + log.debug("Not handling PR 'action' == 'deleted'") + return + # Handle this PR update as though it were an Issue, if that's + # acceptable to the configuration. + if not (pr := u_issue.handle_github_message(body, config, is_pr=True)): + log.info("Not handling PR issue update -- not configured") + return + # PRs require additional handling (Issues do not have suffix, and + # reporter needs to be reformatted). + pr.suffix = suffix + pr.reporter = pr.reporter.get("fullname") + setattr(pr, "match", matcher(pr.content, pr.comments)) + d_pr.sync_with_jira(pr, config) + else: + if issue := u_issue.handle_github_message(body, config): + d_issue.sync_with_jira(issue, config) + else: + log.info("Not handling Issue update -- not configured") + + +def handle_pr_msg(body, headers, suffix, config): + """ + Function to handle incoming github PR message + :param Dict body: Incoming message body + :param Dict headers: Incoming message headers + :param String suffix: Incoming suffix + :param Dict config: Config dict + """ + if pr := u_pr.handle_github_message(body, config, suffix): + d_pr.sync_with_jira(pr, config) + else: + log.info("Not handling PR update -- not configured") + + +# Issue related handlers +issue_handlers = { + # GitHub + # New webhook-2fm topics + "github.issues": handle_issue_msg, + "github.issue_comment": handle_issue_msg, + # Old github2fedmsg topics + "github.issue.opened": handle_issue_msg, + "github.issue.reopened": handle_issue_msg, + "github.issue.labeled": handle_issue_msg, + "github.issue.assigned": handle_issue_msg, + "github.issue.unassigned": handle_issue_msg, + "github.issue.closed": handle_issue_msg, + "github.issue.comment": handle_issue_msg, + "github.issue.unlabeled": handle_issue_msg, + "github.issue.milestoned": handle_issue_msg, + "github.issue.demilestoned": handle_issue_msg, + "github.issue.edited": handle_issue_msg, +} + +# PR related handlers +pr_handlers = { + # GitHub + # New webhook-2fm topics + "github.pull_request": handle_pr_msg, + "github.issue_comment": handle_pr_msg, + # Old github2fedmsg topics + "github.pull_request.opened": handle_pr_msg, + "github.pull_request.edited": handle_pr_msg, + "github.issue.comment": handle_pr_msg, + "github.pull_request.reopened": handle_pr_msg, + "github.pull_request.closed": handle_pr_msg, +} + + +def get_handler_for(suffix, topic, idx): + """ + Function to check if a handler for given suffix is configured + :param String suffix: Incoming suffix + :param String topic: Topic of incoming message + :param String idx: Id of incoming message + :returns: Handler function if configured for suffix. Otherwise None. + """ + if suffix in issue_handlers: + return issue_handlers.get(suffix) + elif suffix in pr_handlers: + return pr_handlers.get(suffix) + log.info("No github handler for %r %r %r", suffix, topic, idx) + return None diff --git a/sync2jira/upstream_issue.py b/sync2jira/handler/github_upstream_issue.py similarity index 100% rename from sync2jira/upstream_issue.py rename to sync2jira/handler/github_upstream_issue.py diff --git a/sync2jira/upstream_pr.py b/sync2jira/handler/github_upstream_pr.py similarity index 98% rename from sync2jira/upstream_pr.py rename to sync2jira/handler/github_upstream_pr.py index 8b077bee..4ac98dda 100644 --- a/sync2jira/upstream_pr.py +++ b/sync2jira/handler/github_upstream_pr.py @@ -21,8 +21,8 @@ from github import Github, UnknownObjectException +import sync2jira.handler.github_upstream_issue as u_issue import sync2jira.intermediary as i -import sync2jira.upstream_issue as u_issue log = logging.getLogger("sync2jira") diff --git a/sync2jira/handler/gitlab.py b/sync2jira/handler/gitlab.py new file mode 100644 index 00000000..1b728f4d --- /dev/null +++ b/sync2jira/handler/gitlab.py @@ -0,0 +1,136 @@ +import logging + +from sync2jira.api.gitlab_client import GitlabClient +import sync2jira.downstream_issue as d_issue +import sync2jira.downstream_pr as d_pr + +# Local Modules +import sync2jira.intermediary as i + +log = logging.getLogger("sync2jira") + + +def should_sync(upstream, labels, config, event_type): + mapped_repos = config["sync2jira"]["map"]["gitlab"] + if upstream not in mapped_repos: + log.debug("%r not in Gitlab map: %r", upstream, mapped_repos.keys()) + return None + if event_type not in mapped_repos[upstream].get("sync", []): + log.debug( + "%r not in Gitlab sync map: %r", + event_type, + mapped_repos[upstream].get("sync", []), + ) + return None + + _filter = config["sync2jira"].get("filters", {}).get("gitlab", {}).get(upstream, {}) + for key, expected in _filter.items(): + if key == "labels": + if labels.isdisjoint(expected): + log.debug("Labels %s not found on issue: %s", expected, upstream) + return None + + +def handle_gitlab_issue(body, headers, config, suffix): + """ + Handle GitLab issue from FedMsg. + + :param Dict body: FedMsg Message body + :param Dict body: FedMsg Message headers + :param Dict config: Config File + :param Bool is_pr: msg refers to a pull request + """ + upstream = body["project"]["path_with_namespace"] + url = headers["x-gitlab-instance"] + token = config["sync2jira"].get("github_token") + labels = {label["title"] for label in body.get("labels", [])} + iid = body.get("object_attributes").get("iid") + + if should_sync(upstream, labels, config, "issue"): + sync_gitlab_issue(GitlabClient(url, token, upstream), iid, upstream, config) + + +def handle_gitlab_note(body, headers, config, suffix): + """ + Handle Gitlab note from FedMsg. + + :param Dict body: FedMsg Message body + :param Dict body: FedMsg Message headers + :param Dict config: Config File + :param String suffix: FedMsg suffix + """ + upstream = body["project"]["path_with_namespace"] + url = headers["x-gitlab-instance"] + token = config["sync2jira"].get("github_token") + + if "merge_request" in body: + labels = { + label["title"] for label in body.get("merge_request").get("labels", []) + } + iid = body.get("merge_request").get("iid") + + if should_sync(upstream, labels, config, "issue"): + sync_gitlab_mr(GitlabClient(url, token, upstream), iid, upstream) + if "issue" in body: + labels = {label["title"] for label in body.get("issue").get("labels", [])} + iid = body.get("issue").get("iid") + + if should_sync(upstream, labels, config, "pullrequest"): + sync_gitlab_issue(GitlabClient(url, token, upstream), iid, upstream) + log.info("Note was not added to an issue or merge request. Skipping note event.") + + +def handle_gitlab_mr(body, headers, config, suffix): + """ + Handle Gitlab merge request from FedMsg. + + :param Dict body: FedMsg Message body + :param Dict body: FedMsg Message headers + :param Dict config: Config File + :param String suffix: FedMsg suffix + """ + upstream = body["project"]["path_with_namespace"] + url = headers["x-gitlab-instance"] + token = config["sync2jira"].get("github_token") + labels = {label["title"] for label in body.get("labels", [])} + iid = body.get("object_attributes").get("iid") + + if should_sync(upstream, labels, config, "pullrequest"): + sync_gitlab_mr(GitlabClient(url, token, upstream), iid, upstream, config) + + +def sync_gitlab_issue(client, iid, upstream, config): + gitlab_issue = client.fetch_issue(iid) + comments = gitlab_issue.notes.list(all=True) + + issue = i.Issue.from_gitlab(gitlab_issue, comments, upstream, config) + d_issue.sync_with_jira(issue, config) + + +def sync_gitlab_mr(client, iid, upstream, config): + gitlab_mr = client.fetch_mr(iid) + comments = gitlab_mr.notes.list(all=True) + + mr = i.PR.from_gitlab(gitlab_mr, comments, upstream, config) + d_pr.sync_with_jira(mr, config) + + +handlers = { + "gitlab.issues": handle_gitlab_issue, + "gitlab.issue_comment": handle_gitlab_mr, + "gitlab.note": handle_gitlab_note, +} + + +def get_handler_for(suffix, topic, idx): + """ + Function to check if a handler for given suffix is configured + :param String suffix: Incoming suffix + :param String topic: Topic of incoming message + :param String idx: Id of incoming message + :returns: Handler function if configured for suffix. Otherwise None. + """ + if suffix in handlers: + return handlers.get(suffix) + log.info("No gitlab handler for %r %r %r", suffix, topic, idx) + return None diff --git a/sync2jira/intermediary.py b/sync2jira/intermediary.py index 9e0d3b86..bc79cf1e 100644 --- a/sync2jira/intermediary.py +++ b/sync2jira/intermediary.py @@ -86,7 +86,7 @@ def upstream_title(self): @classmethod def from_github(cls, upstream, issue, config): - """Helper function to create an intermediary Issue object.""" + """Helper function to create an intermediary Issue object from Github.""" upstream_source = "github" comments = reformat_github_comments(issue) @@ -131,6 +131,41 @@ def from_github(cls, upstream, issue, config): issue_type=issue_type, ) + @classmethod + def from_gitlab(cls, issue, comments, upstream, config): + """Helper function to create an intermediary Issue object from Gitlab.""" + upstream_source = "gitlab" + + # Reformat the state field + issue_status = issue.state + if issue_status == "open": + issue_status = "Open" + elif issue_status == "closed": + issue_status = "Closed" + + return cls( + source=upstream_source, + title=issue.title, + url=issue.web_url, + upstream=upstream, + config=config, + comments=reformat_gitlab_comments(comments), + tags=issue.labels, + fixVersion="" if not issue.milestone else issue.milestone.id, + priority="", + content=issue.description, + reporter={"login": issue.author.username, "fullname": issue.author.name}, + assignee=[ + {"login": entry.username, "fullname": entry.name} + for entry in issue.assignees + ], + status=issue_status, + id_=issue.id, + storypoints="", + upstream_id=issue.iid, + issue_type=issue.type, + ) + def __repr__(self): return f"" @@ -248,6 +283,54 @@ def from_github(cls, upstream, pr, suffix, config): match=match, ) + @classmethod + def from_gitlab(cls, pr, comments, upstream, config): + """Helper function to create an intermediary PR object from gitlab""" + upstream_source = "github" + reformatted_comments = reformat_gitlab_comments(comments) + + # Match to a JIRA + match = matcher(pr.description, reformatted_comments) + + # Return our PR object + return cls( + source=upstream_source, + jira_key=match, + title=pr.title, + url=pr.web_url, + upstream=upstream, + config=config, + comments=reformatted_comments, + # tags=issue['labels'], + # fixVersion=[issue['milestone']], + priority=None, + content=pr.description, + reporter=pr.author.name, + assignee={ + "login": pr.assignee.username + }, # used like this in jira sync code + # GitHub PRs do not have status + status=None, + id_=pr.iid, + # upstream_id=issue['number'], + suffix=pr.state, + match=match, + ) + + +def reformat_gitlab_comments(comments): + return [ + { + "author": comment.author.name, + "name": comment.author.username, + "body": trim_string(comment.body), + "id": comment.id, + "date_created": comment.created_at, + "changed": comment.updated_at, + } + for comment in comments + ] + def reformat_github_comments(issue): return [ diff --git a/sync2jira/main.py b/sync2jira/main.py index 86d7dd77..4f2185a9 100644 --- a/sync2jira/main.py +++ b/sync2jira/main.py @@ -36,10 +36,10 @@ # Local Modules import sync2jira.downstream_issue as d_issue import sync2jira.downstream_pr as d_pr -from sync2jira.intermediary import matcher +import sync2jira.handler.base as handlers +import sync2jira.handler.github_upstream_issue as u_issue +import sync2jira.handler.github_upstream_pr as u_pr from sync2jira.mailer import send_mail -import sync2jira.upstream_issue as u_issue -import sync2jira.upstream_pr as u_pr # Set up our logging FORMAT = "[%(asctime)s] %(levelname)s: %(message)s" @@ -55,39 +55,6 @@ remote_link_title = "Upstream issue" failure_email_subject = "Sync2Jira Has Failed!" -# Issue related handlers -issue_handlers = { - # GitHub - # New webhook-2fm topics - "github.issues": u_issue.handle_github_message, - "github.issue_comment": u_issue.handle_github_message, - # Old github2fedmsg topics - "github.issue.opened": u_issue.handle_github_message, - "github.issue.reopened": u_issue.handle_github_message, - "github.issue.labeled": u_issue.handle_github_message, - "github.issue.assigned": u_issue.handle_github_message, - "github.issue.unassigned": u_issue.handle_github_message, - "github.issue.closed": u_issue.handle_github_message, - "github.issue.comment": u_issue.handle_github_message, - "github.issue.unlabeled": u_issue.handle_github_message, - "github.issue.milestoned": u_issue.handle_github_message, - "github.issue.demilestoned": u_issue.handle_github_message, - "github.issue.edited": u_issue.handle_github_message, -} - -# PR related handlers -pr_handlers = { - # GitHub - # New webhook-2fm topics - "github.pull_request": u_pr.handle_github_message, - "github.issue_comment": u_pr.handle_github_message, - # Old github2fedmsg topics - "github.pull_request.opened": u_pr.handle_github_message, - "github.pull_request.edited": u_pr.handle_github_message, - "github.issue.comment": u_pr.handle_github_message, - "github.pull_request.reopened": u_pr.handle_github_message, - "github.pull_request.closed": u_pr.handle_github_message, -} INITIALIZE = os.getenv("INITIALIZE", "0") @@ -148,20 +115,19 @@ def callback(msg): idx = msg.id suffix = ".".join(topic.split(".")[3:]) - if suffix not in issue_handlers and suffix not in pr_handlers: - log.info("No handler for %r %r %r", suffix, topic, idx) - return - - config = load_config() - body = msg.body.get("body") or msg.body - try: - handle_msg(body, suffix, config) - except GithubException as e: - log.error("Unexpected GitHub error: %s", e) - except JIRAError as e: - log.error("Unexpected Jira error: %s", e) - except Exception as e: - log.exception("Unexpected error.", exc_info=e) + handler = handlers.get_handler_for(suffix, topic, idx) + if handler: + config = load_config() + body = msg.body.get("body") or msg.body + headers = msg.body.get("headers") or msg.headers + try: + handler(body, headers, suffix, config) + except GithubException as e: + log.error("Unexpected GitHub error: %s", e) + except JIRAError as e: + log.error("Unexpected Jira error: %s", e) + except Exception as e: + log.exception("Unexpected error.", exc_info=e) def listen(config): @@ -188,18 +154,27 @@ def listen(config): "arguments": {}, }, } + + # The topics that should be delivered to the queue + github_topics = [ + # New style + "org.fedoraproject.prod.github.issues", + "org.fedoraproject.prod.github.issue_comment", + "org.fedoraproject.prod.github.pull_request", + # Old style + "org.fedoraproject.prod.github.issue.#", + "org.fedoraproject.prod.github.pull_request.#", + ] + gitlab_topics = [ + "org.fedoraproject.prod.gitlab.issue" + "org.fedoraproject.prod.gitlab.merge_request", + "org.fedoraproject.prod.gitlab.note", + ] + bindings = { "exchange": "amq.topic", # The AMQP exchange to bind our queue to "queue": queue, - "routing_keys": [ # The topics that should be delivered to the queue - # New style - "org.fedoraproject.prod.github.issues", - "org.fedoraproject.prod.github.issue_comment", - "org.fedoraproject.prod.github.pull_request", - # Old style - "org.fedoraproject.prod.github.issue.#", - "org.fedoraproject.prod.github.pull_request.#", - ], + "routing_keys": github_topics + gitlab_topics, } log.info("Waiting for a relevant fedmsg message to arrive...") @@ -250,6 +225,16 @@ def initialize_issues(config, testing=False, repo_name=None): raise log.info("Done with GitHub issue initialization.") + for upstream in mapping.get("gitlab", {}).keys(): + if "issue" not in mapping.get("github", {}).get(upstream, {}).get("sync", []): + continue + if repo_name is not None and upstream != repo_name: + continue + + # TODO: Fetch all issues from the gitlab instance + + log.info("Done with Gitlab PR initialization.") + def initialize_pr(config, testing=False, repo_name=None): """ @@ -266,6 +251,7 @@ def initialize_pr(config, testing=False, repo_name=None): log.info("Running initialization to sync all PRs from upstream to jira") log.info("Testing flag is %r", config["sync2jira"]["testing"]) mapping = config["sync2jira"]["map"] + for upstream in mapping.get("github", {}).keys(): if "pullrequest" not in mapping.get("github", {}).get(upstream, {}).get( "sync", [] @@ -298,45 +284,17 @@ def initialize_pr(config, testing=False, repo_name=None): raise log.info("Done with GitHub PR initialization.") + for upstream in mapping.get("gitlab", {}).keys(): + if "pullrequest" not in mapping.get("gitlab", {}).get(upstream, {}).get( + "sync", [] + ): + continue + if repo_name is not None and upstream != repo_name: + continue -def handle_msg(body, suffix, config): - """ - Function to handle incoming message - :param Dict body: Incoming message body - :param String suffix: Incoming suffix - :param Dict config: Config dict - """ - if handler := issue_handlers.get(suffix): - # GitHub '.issue*' is used for both PR and Issue; check if this update - # is actually for a PR - if "pull_request" in body["issue"]: - if body["action"] == "deleted": - # I think this gets triggered when someone deletes a comment - # from a PR. Since we don't capture PR comments (only Issue - # comments), we don't need to react if one is deleted. - log.debug("Not handling PR 'action' == 'deleted'") - return - # Handle this PR update as though it were an Issue, if that's - # acceptable to the configuration. - if not (pr := handler(body, config, is_pr=True)): - log.info("Not handling PR issue update -- not configured") - return - # PRs require additional handling (Issues do not have suffix, and - # reporter needs to be reformatted). - pr.suffix = suffix - pr.reporter = pr.reporter.get("fullname") - setattr(pr, "match", matcher(pr.content, pr.comments)) - d_pr.sync_with_jira(pr, config) - else: - if issue := handler(body, config): - d_issue.sync_with_jira(issue, config) - else: - log.info("Not handling Issue update -- not configured") - elif handler := pr_handlers.get(suffix): - if pr := handler(body, config, suffix): - d_pr.sync_with_jira(pr, config) - else: - log.info("Not handling PR update -- not configured") + # TODO: Fetch all PRs from the gitlab instance + + log.info("Done with Gitlab PR initialization.") def main(runtime_test=False, runtime_config=None): diff --git a/tests/test_upstream_issue.py b/tests/test_github_upstream_issue.py similarity index 99% rename from tests/test_upstream_issue.py rename to tests/test_github_upstream_issue.py index 3be5fd35..25133cea 100644 --- a/tests/test_upstream_issue.py +++ b/tests/test_github_upstream_issue.py @@ -3,9 +3,9 @@ import unittest.mock as mock from unittest.mock import MagicMock -import sync2jira.upstream_issue as u +import sync2jira.handler.github_upstream_issue as u -PATH = "sync2jira.upstream_issue." +PATH = "sync2jira.handler.github_upstream_issue." class TestUpstreamIssue(unittest.TestCase): diff --git a/tests/test_upstream_pr.py b/tests/test_github_upstream_pr.py similarity index 99% rename from tests/test_upstream_pr.py rename to tests/test_github_upstream_pr.py index 6559836e..9a263fa9 100644 --- a/tests/test_upstream_pr.py +++ b/tests/test_github_upstream_pr.py @@ -3,9 +3,9 @@ import unittest.mock as mock from unittest.mock import MagicMock -import sync2jira.upstream_pr as u +import sync2jira.handler.github_upstream_pr as u -PATH = "sync2jira.upstream_pr." +PATH = "sync2jira.handler.github_upstream_pr." class TestUpstreamPR(unittest.TestCase): diff --git a/tests/test_main.py b/tests/test_main.py index 81ddc990..8136aefa 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,15 +2,18 @@ import unittest.mock as mock from unittest.mock import MagicMock +import sync2jira.handler.github as gh_handler import sync2jira.main as m PATH = "sync2jira.main." +HANDLER_PATH = "sync2jira.handler.github." class MockMessage(object): - def __init__(self, msg_id, body, topic): + def __init__(self, msg_id, body, headers, topic): self.id = msg_id self.body = body + self.headers = headers self.topic = topic @@ -41,11 +44,13 @@ def setUp(self): self.old_style_mock_message = MockMessage( msg_id="mock_id", body=self.mock_message_body, + headers={}, topic=None, ) self.new_style_mock_message = MockMessage( msg_id="mock_id", body={"body": self.mock_message_body}, + headers={}, topic=None, ) @@ -235,9 +240,12 @@ def test_initialize_github_error( mock_sleep.assert_not_called() mock_report_failure.assert_called_with(self.mock_config) - @mock.patch(PATH + "handle_msg") + @mock.patch(HANDLER_PATH + "handle_issue_msg") + @mock.patch(HANDLER_PATH + "handle_pr_msg") @mock.patch(PATH + "load_config") - def test_listen_no_handlers(self, mock_load_config, mock_handle_msg): + def test_listen_no_handlers( + self, mock_load_config, mock_handle_pr_msg, mock_handle_issue_msg + ): """ Test 'listen' function where suffix is not in handlers """ @@ -249,40 +257,47 @@ def test_listen_no_handlers(self, mock_load_config, mock_handle_msg): m.callback(self.old_style_mock_message) # Assert everything was called correctly - mock_handle_msg.assert_not_called() + mock_handle_pr_msg.assert_not_called() + mock_handle_issue_msg.assert_not_called() - @mock.patch.dict( - PATH + "issue_handlers", {"github.issue.comment": lambda msg, c: "dummy_issue"} - ) - @mock.patch(PATH + "handle_msg") @mock.patch(PATH + "load_config") - def test_listen(self, mock_load_config, mock_handle_msg): + def test_listen(self, mock_load_config): """ Test 'listen' function where everything goes smoothly """ # Set up return values mock_load_config.return_value = self.mock_config - # Call the function once with the old style - self.old_style_mock_message.topic = "d.d.d.github.issue.comment" - m.callback(self.old_style_mock_message) - - # ... and again with the new style - self.new_style_mock_message.topic = "d.d.d.github.issue.comment" - m.callback(self.new_style_mock_message) - - # Assert everything was called correctly - # It should be called twice, once for the old style message and once for the new. - mock_handle_msg.assert_has_calls( - [ - mock.call( - self.mock_message_body, "github.issue.comment", self.mock_config - ), - mock.call( - self.mock_message_body, "github.issue.comment", self.mock_config - ), - ] - ) + mock_handler = mock.Mock() + with mock.patch.dict( + HANDLER_PATH + "issue_handlers", {"github.issue.comment": mock_handler} + ): + # Call the function once with the old style + self.old_style_mock_message.topic = "d.d.d.github.issue.comment" + m.callback(self.old_style_mock_message) + + # ... and again with the new style + self.new_style_mock_message.topic = "d.d.d.github.issue.comment" + m.callback(self.new_style_mock_message) + + # Assert everything was called correctly + # It should be called twice, once for the old style message and once for the new. + mock_handler.assert_has_calls( + [ + mock.call( + self.mock_message_body, + {}, + "github.issue.comment", + self.mock_config, + ), + mock.call( + self.mock_message_body, + {}, + "github.issue.comment", + self.mock_config, + ), + ] + ) @mock.patch(PATH + "send_mail") @mock.patch(PATH + "jinja2") @@ -310,56 +325,90 @@ def test_report_failure(self, mock_jinja2, mock_send_mail): text="mock_html", ) - @mock.patch(PATH + "u_issue") - @mock.patch(PATH + "d_issue") - def test_handle_msg_no_handlers(self, mock_d, mock_u): + def test_get_handler_for(self): """ - Tests 'handle_msg' function where there are no handlers + Tests 'get_handler_for' function with finding a handler """ - # Call the function - m.handle_msg( - body=self.mock_message_body, suffix="no_handler", config=self.mock_config + handler = gh_handler.get_handler_for( + suffix="github.pull_request", topic="random topic", idx="1337" ) + assert ( + handler is not None + ), "Expected handler to be found for 'github.pull_request'" - # Assert everything was called correctly - mock_d.sync_with_jira.assert_not_called() - mock_u.handle_github_message.assert_not_called() + def test_get_handler_for_no_handler(self): + """ + Tests 'get_handler_for' function with finding a handler + """ + handler = gh_handler.get_handler_for( + suffix="does.not.exist", topic="random topic", idx="1337" + ) + assert handler is None, "Expected no handler to be found for 'does.not.exist'" @mock.patch.dict( - PATH + "issue_handlers", {"github.issue.comment": lambda msg, c: None} + HANDLER_PATH + "issue_handlers", {"github.issue.comment": lambda msg, c: None} ) - @mock.patch(PATH + "u_issue") - @mock.patch(PATH + "d_issue") + @mock.patch(HANDLER_PATH + "u_issue") + @mock.patch(HANDLER_PATH + "d_issue") def test_handle_msg_no_issue(self, mock_d, mock_u): """ Tests 'handle_msg' function where there is no issue """ + mock_u.handle_github_message.return_value = None + # Call the function - m.handle_msg( + gh_handler.handle_issue_msg( body=self.mock_message_body, + headers={}, suffix="github.issue.comment", config=self.mock_config, ) # Assert everything was called correctly mock_d.sync_with_jira.assert_not_called() - mock_u.handle_github_message.assert_not_called() + mock_u.handle_github_message.assert_called_once() @mock.patch.dict( - PATH + "issue_handlers", {"github.issue.comment": lambda msg, c: "dummy_issue"} + HANDLER_PATH + "issue_handlers", + {"github.issue.comment": lambda msg, c: "dummy_issue"}, ) - @mock.patch(PATH + "u_issue") - @mock.patch(PATH + "d_issue") - def test_handle_msg(self, mock_d, mock_u): + @mock.patch(HANDLER_PATH + "u_issue") + @mock.patch(HANDLER_PATH + "d_issue") + def test_handle_issue_msg(self, mock_d, mock_u): + """ + Tests 'handle_issue_msg' function + """ + # Set up return values + mock_u.handle_github_message.return_value = "dummy_issue" + + # Call the function + gh_handler.handle_issue_msg( + body=self.mock_message_body, + headers={}, + suffix="github.issue.comment", + config=self.mock_config, + ) + + # Assert everything was called correctly + mock_d.sync_with_jira.assert_called_with("dummy_issue", self.mock_config) + + @mock.patch.dict( + HANDLER_PATH + "issue_handlers", + {"github.issue.comment": lambda msg, c: "dummy_issue"}, + ) + @mock.patch(HANDLER_PATH + "u_pr") + @mock.patch(HANDLER_PATH + "d_pr") + def test_handle_pr_msg(self, mock_d, mock_u): """ - Tests 'handle_msg' function + Tests 'handle_issue_msg' function """ # Set up return values mock_u.handle_github_message.return_value = "dummy_issue" # Call the function - m.handle_msg( + gh_handler.handle_pr_msg( body=self.mock_message_body, + headers={}, suffix="github.issue.comment", config=self.mock_config, )