jira: Add search command.
This commit is contained in:
parent
b0c2b1b9c8
commit
a46dae37f5
|
@ -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
|
||||||
|
|
47
zulip_bots/zulip_bots/bots/jira/fixtures/test_search.json
Normal file
47
zulip_bots/zulip_bots/bots/jira/fixtures/test_search.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue