jira: Add search command.
This commit is contained in:
parent
b0c2b1b9c8
commit
a46dae37f5
|
@ -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
|
||||
|
|
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>.+?)")?'
|
||||
'$'
|
||||
)
|
||||
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:
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue