jira: Add search command.

This commit is contained in:
pemontto 2020-04-27 15:21:09 +01:00 committed by Tim Abbott
parent b0c2b1b9c8
commit a46dae37f5
5 changed files with 209 additions and 1 deletions

View file

@ -2,11 +2,13 @@
## Setup ## 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), - username (an email or username that can access your Jira),
- password (the password for that username), and - password (the password for that username), and
- domain (a domain like `example.atlassian.net`) - 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 ## Usage
@ -30,6 +32,23 @@ Jira Bot:
> - Priority: *Medium* > - Priority: *Medium*
> - Status: *To Do* > - 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
`create` creates an issue using its `create` creates an issue using its

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -27,6 +27,7 @@ EDIT_REGEX = re.compile(
'( by making due "(?P<due_date>.+?)")?' '( by making due "(?P<due_date>.+?)")?'
'$' '$'
) )
SEARCH_REGEX = re.compile('search "(?P<search_term>.+)"$')
HELP_REGEX = re.compile('help$') HELP_REGEX = re.compile('help$')
HELP_RESPONSE = ''' 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**
`create` creates an issue using its `create` creates an issue using its
@ -139,6 +157,29 @@ class JiraHandler:
if not self.display_url: if not self.display_url:
self.display_url = self.domain_with_protocol 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: def handle_message(self, message: Dict[str, str], bot_handler: Any) -> None:
content = message.get('content') content = message.get('content')
response = '' response = ''
@ -146,6 +187,7 @@ class JiraHandler:
get_match = GET_REGEX.match(content) get_match = GET_REGEX.match(content)
create_match = CREATE_REGEX.match(content) create_match = CREATE_REGEX.match(content)
edit_match = EDIT_REGEX.match(content) edit_match = EDIT_REGEX.match(content)
search_match = SEARCH_REGEX.match(content)
help_match = HELP_REGEX.match(content) help_match = HELP_REGEX.match(content)
if get_match: if get_match:
@ -231,6 +273,10 @@ class JiraHandler:
response = 'Oh no! Jira raised an error:\n > ' + ', '.join(errors) response = 'Oh no! Jira raised an error:\n > ' + ', '.join(errors)
else: else:
response = 'Issue *' + key + '* was edited! ' + url 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: elif help_match:
response = HELP_RESPONSE response = HELP_RESPONSE
else: else:

View file

@ -9,6 +9,19 @@ class TestJiraBot(BotTestCase, DefaultTests):
'domain': 'example.atlassian.net' '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 = '''\ MOCK_GET_RESPONSE = '''\
**Issue *[TEST-13](https://example.atlassian.net/browse/TEST-13)*: summary** **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**
`create` creates an issue using its `create` creates an issue using its
@ -104,6 +134,10 @@ Jira Bot:
> Issue *BOTS-16* was edited! https://example.atlassian.net/browse/BOTS-16 > 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: def _test_invalid_config(self, invalid_config, error_message) -> None:
with self.mock_config_info(invalid_config), \ with self.mock_config_info(invalid_config), \
self.assertRaisesRegex(KeyError, error_message): self.assertRaisesRegex(KeyError, error_message):
@ -173,6 +207,21 @@ Jira Bot:
'by making due "2018-06-11"', 'by making due "2018-06-11"',
'Oh no! Jira raised an error:\n > error1') '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: def test_help(self) -> None:
with self.mock_config_info(self.MOCK_CONFIG_INFO): with self.mock_config_info(self.MOCK_CONFIG_INFO):
self.verify_reply('help', self.MOCK_HELP_RESPONSE) self.verify_reply('help', self.MOCK_HELP_RESPONSE)