diff --git a/zulip_bots/zulip_bots/bots/jira/doc.md b/zulip_bots/zulip_bots/bots/jira/doc.md index 9916a27..bbb3fa6 100644 --- a/zulip_bots/zulip_bots/bots/jira/doc.md +++ b/zulip_bots/zulip_bots/bots/jira/doc.md @@ -2,11 +2,13 @@ ## Setup -To use Jira Bot, first set up `jira.conf`. `jira.conf` takes 3 options: +To use Jira Bot, first set up `jira.conf`. `jira.conf` requires 3 options: - username (an email or username that can access your Jira), - password (the password for that username), and - domain (a domain like `example.atlassian.net`) + - display_url ([optional] your front facing jira URL if different from domain. +E.g. `https://example-lb.atlassian.net`) ## Usage @@ -30,6 +32,23 @@ Jira Bot: > - Priority: *Medium* > - Status: *To Do* +### search + +`search` takes in a search term and returns issues with matching summaries. For example, + +you: + + > @**Jira Bot** search "XSS" + +Jira Bot: + + > **Search results for *"XSS"*:** + > + > - ***BOTS-5:*** Stored XSS **[Published]** + > - ***BOTS-6:*** Reflected XSS **[Draft]** + +--- + ### create `create` creates an issue using its diff --git a/zulip_bots/zulip_bots/bots/jira/fixtures/test_search.json b/zulip_bots/zulip_bots/bots/jira/fixtures/test_search.json new file mode 100644 index 0000000..4da2f09 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/jira/fixtures/test_search.json @@ -0,0 +1,47 @@ +{ + "request": { + "api_url": "https://example.atlassian.net/rest/api/2/search?jql=summary ~ TEST&fields=key,summary,status", + "method": "GET", + "headers": { + "Authorization": "Basic ZXhhbXBsZUBleGFtcGxlLmNvbTpxd2VydHkhMTIz" + } + }, + "response": { + "expand": "schema,names", + "startAt": 0, + "maxResults": 50, + "total": 2, + "issues": [ + { + "id": "1", + "key": "TEST-1", + "fields": { + "creator": {"name": "admin"}, + "description": "description", + "priority": {"name": "Medium"}, + "project": {"name": "Tests"}, + "issuetype": {"name": "Task"}, + "status": {"name": "To Do"}, + "summary": "summary test 1" + } + }, + { + "id": "2", + "key": "TEST-2", + "fields": { + "creator": {"name": "admin"}, + "description": "description", + "priority": {"name": "Medium"}, + "project": {"name": "Tests"}, + "issuetype": {"name": "Task"}, + "status": {"name": "To Do"}, + "summary": "summary test 2" + } + } + ] + }, + "response-headers": { + "status": 200, + "content-type": "application/json; charset=utf-8" + } +} diff --git a/zulip_bots/zulip_bots/bots/jira/fixtures/test_search_scheme.json b/zulip_bots/zulip_bots/bots/jira/fixtures/test_search_scheme.json new file mode 100644 index 0000000..762040a --- /dev/null +++ b/zulip_bots/zulip_bots/bots/jira/fixtures/test_search_scheme.json @@ -0,0 +1,47 @@ +{ + "request": { + "api_url": "http://example.atlassian.net/rest/api/2/search?jql=summary ~ TEST&fields=key,summary,status", + "method": "GET", + "headers": { + "Authorization": "Basic ZXhhbXBsZUBleGFtcGxlLmNvbTpxd2VydHkhMTIz" + } + }, + "response": { + "expand": "schema,names", + "startAt": 0, + "maxResults": 50, + "total": 2, + "issues": [ + { + "id": "1", + "key": "TEST-1", + "fields": { + "creator": {"name": "admin"}, + "description": "description", + "priority": {"name": "Medium"}, + "project": {"name": "Tests"}, + "issuetype": {"name": "Task"}, + "status": {"name": "To Do"}, + "summary": "summary test 1" + } + }, + { + "id": "2", + "key": "TEST-2", + "fields": { + "creator": {"name": "admin"}, + "description": "description", + "priority": {"name": "Medium"}, + "project": {"name": "Tests"}, + "issuetype": {"name": "Task"}, + "status": {"name": "To Do"}, + "summary": "summary test 2" + } + } + ] + }, + "response-headers": { + "status": 200, + "content-type": "application/json; charset=utf-8" + } +} diff --git a/zulip_bots/zulip_bots/bots/jira/jira.py b/zulip_bots/zulip_bots/bots/jira/jira.py index 4dd165b..b3e261e 100644 --- a/zulip_bots/zulip_bots/bots/jira/jira.py +++ b/zulip_bots/zulip_bots/bots/jira/jira.py @@ -27,6 +27,7 @@ EDIT_REGEX = re.compile( '( by making due "(?P.+?)")?' '$' ) +SEARCH_REGEX = re.compile('search "(?P.+)"$') HELP_REGEX = re.compile('help$') HELP_RESPONSE = ''' @@ -52,6 +53,23 @@ Jira Bot: --- +**search** + +`search` takes in a search term and returns issues with matching summaries. For example, + +you: + + > @**Jira Bot** search "XSS" + +Jira Bot: + + > **Search results for *"XSS"*:** + > + > - ***BOTS-5:*** Stored XSS **[Published]** + > - ***BOTS-6:*** Reflected XSS **[Draft]** + +--- + **create** `create` creates an issue using its @@ -139,6 +157,29 @@ class JiraHandler: if not self.display_url: self.display_url = self.domain_with_protocol + def jql_search(self, jql_query: str) -> str: + UNKNOWN_VAL = '*unknown*' + jira_response = requests.get( + self.domain_with_protocol + '/rest/api/2/search?jql={}&fields=key,summary,status'.format(jql_query), + headers={'Authorization': self.auth}, + ).json() + + url = self.display_url + '/browse/' + errors = jira_response.get('errorMessages', []) + results = jira_response.get('total', 0) + + if errors: + response = 'Oh no! Jira raised an error:\n > ' + ', '.join(errors) + else: + response = '*Found {} results*\n\n'.format(results) + for issue in jira_response.get('issues', []): + fields = issue.get('fields', {}) + summary = fields.get('summary', UNKNOWN_VAL) + status_name = fields.get('status', {}).get('name', UNKNOWN_VAL) + response += "\n - {}: [{}]({}) **[{}]**".format(issue['key'], summary, url + issue['key'], status_name) + + return response + def handle_message(self, message: Dict[str, str], bot_handler: Any) -> None: content = message.get('content') response = '' @@ -146,6 +187,7 @@ class JiraHandler: get_match = GET_REGEX.match(content) create_match = CREATE_REGEX.match(content) edit_match = EDIT_REGEX.match(content) + search_match = SEARCH_REGEX.match(content) help_match = HELP_REGEX.match(content) if get_match: @@ -231,6 +273,10 @@ class JiraHandler: response = 'Oh no! Jira raised an error:\n > ' + ', '.join(errors) else: response = 'Issue *' + key + '* was edited! ' + url + elif search_match: + search_term = search_match.group('search_term') + search_results = self.jql_search("summary ~ {}".format(search_term)) + response = '**Search results for "{}"**\n\n{}'.format(search_term, search_results) elif help_match: response = HELP_RESPONSE else: diff --git a/zulip_bots/zulip_bots/bots/jira/test_jira.py b/zulip_bots/zulip_bots/bots/jira/test_jira.py index bc0767f..b8975c4 100644 --- a/zulip_bots/zulip_bots/bots/jira/test_jira.py +++ b/zulip_bots/zulip_bots/bots/jira/test_jira.py @@ -9,6 +9,19 @@ class TestJiraBot(BotTestCase, DefaultTests): 'domain': 'example.atlassian.net' } + MOCK_SCHEME_CONFIG_INFO = { + 'username': 'example@example.com', + 'password': 'qwerty!123', + 'domain': 'http://example.atlassian.net' + } + + MOCK_DISPLAY_CONFIG_INFO = { + 'username': 'example@example.com', + 'password': 'qwerty!123', + 'domain': 'example.atlassian.net', + 'display_url': 'http://test.com' + } + MOCK_GET_RESPONSE = '''\ **Issue *[TEST-13](https://example.atlassian.net/browse/TEST-13)*: summary** @@ -50,6 +63,23 @@ Jira Bot: --- +**search** + +`search` takes in a search term and returns issues with matching summaries. For example, + +you: + + > @**Jira Bot** search "XSS" + +Jira Bot: + + > **Search results for *"XSS"*:** + > + > - ***BOTS-5:*** Stored XSS **[Published]** + > - ***BOTS-6:*** Reflected XSS **[Draft]** + +--- + **create** `create` creates an issue using its @@ -104,6 +134,10 @@ Jira Bot: > Issue *BOTS-16* was edited! https://example.atlassian.net/browse/BOTS-16 ''' + MOCK_SEARCH_RESPONSE = '**Search results for "TEST"**\n\n*Found 2 results*\n\n\n - TEST-1: [summary test 1](https://example.atlassian.net/browse/TEST-1) **[To Do]**\n - TEST-2: [summary test 2](https://example.atlassian.net/browse/TEST-2) **[To Do]**' + MOCK_SEARCH_RESPONSE_URL = '**Search results for "TEST"**\n\n*Found 2 results*\n\n\n - TEST-1: [summary test 1](http://test.com/browse/TEST-1) **[To Do]**\n - TEST-2: [summary test 2](http://test.com/browse/TEST-2) **[To Do]**' + MOCK_SEARCH_RESPONSE_SCHEME = '**Search results for "TEST"**\n\n*Found 2 results*\n\n\n - TEST-1: [summary test 1](http://example.atlassian.net/browse/TEST-1) **[To Do]**\n - TEST-2: [summary test 2](http://example.atlassian.net/browse/TEST-2) **[To Do]**' + def _test_invalid_config(self, invalid_config, error_message) -> None: with self.mock_config_info(invalid_config), \ self.assertRaisesRegex(KeyError, error_message): @@ -173,6 +207,21 @@ Jira Bot: 'by making due "2018-06-11"', 'Oh no! Jira raised an error:\n > error1') + def test_search(self) -> None: + with self.mock_config_info(self.MOCK_CONFIG_INFO), \ + self.mock_http_conversation('test_search'): + self.verify_reply('search "TEST"', self.MOCK_SEARCH_RESPONSE) + + def test_search_url(self) -> None: + with self.mock_config_info(self.MOCK_DISPLAY_CONFIG_INFO), \ + self.mock_http_conversation('test_search'): + self.verify_reply('search "TEST"', self.MOCK_SEARCH_RESPONSE_URL) + + def test_search_scheme(self) -> None: + with self.mock_config_info(self.MOCK_SCHEME_CONFIG_INFO), \ + self.mock_http_conversation('test_search_scheme'): + self.verify_reply('search "TEST"', self.MOCK_SEARCH_RESPONSE_SCHEME) + def test_help(self) -> None: with self.mock_config_info(self.MOCK_CONFIG_INFO): self.verify_reply('help', self.MOCK_HELP_RESPONSE)