jira: Add search command.
MODIFY
zulip_bots/zulip_bots/bots/jira/doc.md
zulip_bots/zulip_bots/bots/jira/doc.md
## 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

ADD
zulip_bots/zulip_bots/bots/jira/fixtures/test_search.json
+{
+ "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"
+ }
+}

ADD
zulip_bots/zulip_bots/bots/jira/fixtures/test_search_scheme.json
+{
+ "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"
+ }
+}

MODIFY
zulip_bots/zulip_bots/bots/jira/jira.py
zulip_bots/zulip_bots/bots/jira/jira.py
@@ -27,6 +27,7 @@ EDIT_REGEX = re.compile(
'( by making due "(?P<due_date>.+?)")?'
'$'
)
+SEARCH_REGEX = re.compile('search "(?P<search_term>.+)"$')
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:

MODIFY
zulip_bots/zulip_bots/bots/jira/test_jira.py
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)

jira: Add search command.