interactive bots: Create Salesforce bot.
This commit is contained in:
parent
41b065eb76
commit
08bfe9d8c7
|
@ -78,7 +78,9 @@ force_include = [
|
||||||
"zulip_bots/zulip_bots/bots/mention/mention.py",
|
"zulip_bots/zulip_bots/bots/mention/mention.py",
|
||||||
"zulip_bots/zulip_bots/bots/mention/test_mention.py",
|
"zulip_bots/zulip_bots/bots/mention/test_mention.py",
|
||||||
"zulip_bots/zulip_bots/bots/baremetrics/baremetrics.py",
|
"zulip_bots/zulip_bots/bots/baremetrics/baremetrics.py",
|
||||||
"zulip_bots/zulip_bots/bots/baremetrics/test_baremetrics.py"
|
"zulip_bots/zulip_bots/bots/baremetrics/test_baremetrics.py",
|
||||||
|
"zulip_bots/zulip_bots/bots/salesforce/salesforce.py",
|
||||||
|
"zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py"
|
||||||
]
|
]
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.")
|
parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.")
|
||||||
|
|
|
@ -55,7 +55,8 @@ setuptools_info = dict(
|
||||||
'requests', # for bots/link_shortener and bots/jira
|
'requests', # for bots/link_shortener and bots/jira
|
||||||
'python-chess[engine,gaviota]', # for bots/chess
|
'python-chess[engine,gaviota]', # for bots/chess
|
||||||
'wit', # for bots/witai
|
'wit', # for bots/witai
|
||||||
'apiai' # for bots/dialogflow
|
'apiai', # for bots/dialogflow
|
||||||
|
'simple_salesforce' # for bots/salesforce
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
0
zulip_bots/zulip_bots/bots/salesforce/__init__.py
Normal file
0
zulip_bots/zulip_bots/bots/salesforce/__init__.py
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
zulip_bots/zulip_bots/bots/salesforce/assets/query_example.png
Normal file
BIN
zulip_bots/zulip_bots/bots/salesforce/assets/query_example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
80
zulip_bots/zulip_bots/bots/salesforce/doc.md
Normal file
80
zulip_bots/zulip_bots/bots/salesforce/doc.md
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
# Salesforce bot
|
||||||
|
|
||||||
|
The Salesforce bot can get records from your Salesforce database.
|
||||||
|
It can also show details about any Salesforce links that you post.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Create a user in Salesforce that the bot can use to access Salesforce.
|
||||||
|
Make sure it has the appropriate permissions to access records.
|
||||||
|
2. In `salesforce.conf` paste the Salesforce `username`, `password` and
|
||||||
|
`security_token`.
|
||||||
|
3. Run the bot as explained [here](https://zulipchat.com/api/running-bots#running-a-bot)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Standard query
|
||||||
|
![Standard query](assets/query_example.png)
|
||||||
|
|
||||||
|
### Custom query
|
||||||
|
![Custom query](assets/top_opportunities_example.png)
|
||||||
|
|
||||||
|
### Link details
|
||||||
|
![Link details](assets/link_details_example.png)
|
||||||
|
|
||||||
|
## Optional Configuration (Advanced)
|
||||||
|
|
||||||
|
The bot has been designed to be able to configure custom commands and objects.
|
||||||
|
|
||||||
|
If you wanted to find a custom object type, or an object type not included with the bot,
|
||||||
|
like `Event`, you can add these by adding to the Commands and Object Types in `utils.py`.
|
||||||
|
|
||||||
|
A Command is a phrase that the User asks the bot. For example `find contact bob`. To make a Command,
|
||||||
|
the corresponding object type must be made.
|
||||||
|
|
||||||
|
Object types are Salesforce objects, like `Event`, and are used to tell the bot which fields of the object the bot
|
||||||
|
should ask for and display.
|
||||||
|
|
||||||
|
To show details about a link posted, only the Object Type for the object needs to be present.
|
||||||
|
|
||||||
|
Please read the
|
||||||
|
[SOQL reference](https://goo.gl/6VwBV3)
|
||||||
|
to make custom queries, and the [simple_salesforce documentation](https://pypi.python.org/pypi/simple-salesforce)
|
||||||
|
to make custom callbacks.
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
For example: "find contact tim"
|
||||||
|
|
||||||
|
In `utils.py`, the commands are stored in the list `commands`.
|
||||||
|
|
||||||
|
Parameter | Required? | Type | Description | Default
|
||||||
|
--------- | --------- | ---- | ----------- | -------
|
||||||
|
commands | [x] | list[str] | What the user should start their command with | `None`
|
||||||
|
object | [x] | str | The Salesforce object type in `object_types` | `None`
|
||||||
|
query | [ ] | str | The SOQL query to access this object* | `'SELECT {} FROM {} WHERE Name LIKE %\'{}\'% LIMIT {}'`
|
||||||
|
description | [x] | str | What does the command do? | `None`
|
||||||
|
template | [x] | str | Example of the command | `None`
|
||||||
|
rank_output | [ ] | boolean | Should the output be ranked? (1., 2., 3. etc.) | `False`
|
||||||
|
force_keys | [ ] | list[str] | Values which should always be shown in the output | `[]`
|
||||||
|
callback | [ ] | callable** | Custom handling behaviour | `None`
|
||||||
|
|
||||||
|
**Note**: *`query` must have `LIMIT {}` at the end, and the 4 parameters are `fields`, `table` (from `object_types`),
|
||||||
|
`args` (the search term), `limit` (the maximum number of terms)
|
||||||
|
|
||||||
|
**`callback` must be a function which accepts `args: str`(arguments passed in by the user, including search term),
|
||||||
|
`sf: simple_salesforce.api.Salesforce` (the Salesforce handler object, `self.sf`), `command: Dict[str, Any]`
|
||||||
|
(the command used from `commands`)
|
||||||
|
|
||||||
|
### Object Types
|
||||||
|
In `utils.py` the object types are stored in the dictionary `object_types`.
|
||||||
|
|
||||||
|
The name of each object type corresponds to the `object` referenced in `commands`.
|
||||||
|
|
||||||
|
Parameter | Required? | Type | Description
|
||||||
|
--------- | --------- | ---- | -----------
|
||||||
|
fields* | [x] | str | The Salesforce fields to fetch from the database.
|
||||||
|
name | [x] | str | The API name of the object**.
|
||||||
|
|
||||||
|
**Note**: * This must contain Name and Id, however Id is not displayed.
|
||||||
|
** Found in the salesforce object manager.
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"totalSize": 2,
|
||||||
|
"done": true,
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"type": "Contact",
|
||||||
|
"url": ""
|
||||||
|
},
|
||||||
|
"Id": "foo_id",
|
||||||
|
"Name": "foo",
|
||||||
|
"Phone": "020 1234 5678"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"type": "Contact",
|
||||||
|
"url": ""
|
||||||
|
},
|
||||||
|
"Id": "bar_id",
|
||||||
|
"Name": "bar",
|
||||||
|
"Phone": "020 5678 1234"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"totalSize": 0,
|
||||||
|
"done": true,
|
||||||
|
"records": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"totalSize": 1,
|
||||||
|
"done": true,
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"type": "Contact",
|
||||||
|
"url": ""
|
||||||
|
},
|
||||||
|
"Id": "foo_id",
|
||||||
|
"Name": "foo",
|
||||||
|
"Phone": "020 1234 5678"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"totalSize": 2,
|
||||||
|
"done": true,
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"type": "Opportunity",
|
||||||
|
"url": ""
|
||||||
|
},
|
||||||
|
"Id": "foo_id",
|
||||||
|
"Name": "foo",
|
||||||
|
"Amount": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"type": "Opportunity",
|
||||||
|
"url": ""
|
||||||
|
},
|
||||||
|
"Id": "bar_id",
|
||||||
|
"Name": "bar",
|
||||||
|
"Amount": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
1
zulip_bots/zulip_bots/bots/salesforce/requirements.txt
Normal file
1
zulip_bots/zulip_bots/bots/salesforce/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
simple_salesforce
|
4
zulip_bots/zulip_bots/bots/salesforce/salesforce.conf
Normal file
4
zulip_bots/zulip_bots/bots/salesforce/salesforce.conf
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[salesforce]
|
||||||
|
username=foo
|
||||||
|
security_token=bar
|
||||||
|
password=baz
|
178
zulip_bots/zulip_bots/bots/salesforce/salesforce.py
Normal file
178
zulip_bots/zulip_bots/bots/salesforce/salesforce.py
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
# See readme.md for instructions on running this code.
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
import simple_salesforce
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
import getpass
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from zulip_bots.bots.salesforce.utils import *
|
||||||
|
|
||||||
|
base_help_text = '''Salesforce bot
|
||||||
|
This bot can do simple salesforce query requests
|
||||||
|
**All commands must be @-mentioned to the bot.**
|
||||||
|
Commands:
|
||||||
|
{}
|
||||||
|
Arguments:
|
||||||
|
**-limit <num>**: the maximum number of entries sent (default: 5)
|
||||||
|
**-show**: show all the properties of each entry (default: false)
|
||||||
|
|
||||||
|
This bot can also show details about any Salesforce links sent to it.
|
||||||
|
|
||||||
|
Supported Object types:
|
||||||
|
These are the types of Salesforce object supported by this bot.
|
||||||
|
The bot cannot show the details of any other object types.
|
||||||
|
{}'''
|
||||||
|
|
||||||
|
login_url = 'https://login.salesforce.com/'
|
||||||
|
|
||||||
|
|
||||||
|
def get_help_text() -> str:
|
||||||
|
command_text = ''
|
||||||
|
for command in commands:
|
||||||
|
if 'template' in command.keys() and 'description' in command.keys():
|
||||||
|
command_text += '**{}**: {}\n'.format('{} [arguments]'.format(
|
||||||
|
command['template']), command['description'])
|
||||||
|
object_type_text = ''
|
||||||
|
for object_type in object_types.values():
|
||||||
|
object_type_text += '{}\n'.format(object_type['table'])
|
||||||
|
return base_help_text.format(command_text, object_type_text)
|
||||||
|
|
||||||
|
|
||||||
|
def format_result(
|
||||||
|
result: Dict[str, Any],
|
||||||
|
exclude_keys: List[str]=[],
|
||||||
|
force_keys: List[str]=[],
|
||||||
|
rank_output: bool=False,
|
||||||
|
show_all_keys: bool=False
|
||||||
|
) -> str:
|
||||||
|
exclude_keys += ['Name', 'attributes', 'Id']
|
||||||
|
output = ''
|
||||||
|
if result['totalSize'] == 0:
|
||||||
|
return 'No records found.'
|
||||||
|
if result['totalSize'] == 1:
|
||||||
|
record = result['records'][0]
|
||||||
|
output += '**[{}]({}{})**\n'.format(record['Name'],
|
||||||
|
login_url, record['Id'])
|
||||||
|
for key, value in record.items():
|
||||||
|
if key not in exclude_keys:
|
||||||
|
output += '>**{}**: {}\n'.format(key, value)
|
||||||
|
else:
|
||||||
|
for i, record in enumerate(result['records']):
|
||||||
|
if rank_output:
|
||||||
|
output += '{}) '.format(i + 1)
|
||||||
|
output += '**[{}]({}{})**\n'.format(record['Name'],
|
||||||
|
login_url, record['Id'])
|
||||||
|
added_keys = False
|
||||||
|
for key, value in record.items():
|
||||||
|
if key in force_keys or (show_all_keys and key not in exclude_keys):
|
||||||
|
added_keys = True
|
||||||
|
output += '>**{}**: {}\n'.format(key, value)
|
||||||
|
if added_keys:
|
||||||
|
output += '\n'
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def query_salesforce(arg: str, sf: Any, command: Dict[str, Any]) -> str:
|
||||||
|
arg = arg.strip()
|
||||||
|
qarg = arg.split(' -', 1)[0]
|
||||||
|
split_args = [] # type: List[str]
|
||||||
|
raw_arg = ''
|
||||||
|
if len(arg.split(' -', 1)) > 1:
|
||||||
|
raw_arg = ' -' + arg.split(' -', 1)[1]
|
||||||
|
split_args = raw_arg.split(' -')
|
||||||
|
limit_num = 5
|
||||||
|
re_limit = re.compile('-limit \d+')
|
||||||
|
limit = re_limit.search(raw_arg)
|
||||||
|
if limit:
|
||||||
|
limit_num = int(limit.group().rsplit(' ', 1)[1])
|
||||||
|
logging.info('Searching with limit {}'.format(limit_num))
|
||||||
|
query = default_query
|
||||||
|
if 'query' in command.keys():
|
||||||
|
query = command['query']
|
||||||
|
object_type = object_types[command['object']]
|
||||||
|
res = sf.query(query.format(
|
||||||
|
object_type['fields'], object_type['table'], qarg, limit_num))
|
||||||
|
exclude_keys = [] # type: List[str]
|
||||||
|
if 'exclude_keys' in command.keys():
|
||||||
|
exclude_keys = command['exclude_keys']
|
||||||
|
force_keys = [] # type: List[str]
|
||||||
|
if 'force_keys' in command.keys():
|
||||||
|
force_keys = command['force_keys']
|
||||||
|
rank_output = False
|
||||||
|
if 'rank_output' in command.keys():
|
||||||
|
rank_output = command['rank_output']
|
||||||
|
show_all_keys = 'show' in split_args
|
||||||
|
if 'show_all_keys' in command.keys():
|
||||||
|
show_all_keys = command['show_all_keys'] or 'show' in split_args
|
||||||
|
return format_result(res, exclude_keys=exclude_keys, force_keys=force_keys, rank_output=rank_output, show_all_keys=show_all_keys)
|
||||||
|
|
||||||
|
|
||||||
|
def get_salesforce_link_details(link: str, sf: Any) -> str:
|
||||||
|
re_id = re.compile('/[A-Za-z0-9]{18}')
|
||||||
|
re_id_res = re_id.search(link)
|
||||||
|
if re_id_res is None:
|
||||||
|
return 'Invalid salesforce link'
|
||||||
|
id = re_id_res.group().strip('/')
|
||||||
|
for object_type in object_types.values():
|
||||||
|
res = sf.query(link_query.format(
|
||||||
|
object_type['fields'], object_type['table'], id))
|
||||||
|
if res['totalSize'] == 1:
|
||||||
|
return format_result(res)
|
||||||
|
return 'No object found. Make sure it is of the supported types. Type `help` for more info.'
|
||||||
|
|
||||||
|
|
||||||
|
class SalesforceHandler(object):
|
||||||
|
def usage(self) -> str:
|
||||||
|
return '''
|
||||||
|
This is a Salesforce bot, which can search for Contacts,
|
||||||
|
Accounts and Opportunities. And can be configured for any
|
||||||
|
other object types.
|
||||||
|
|
||||||
|
It will also show details of any Salesforce links posted.
|
||||||
|
|
||||||
|
@-mention the bot with 'help' to see available commands.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def get_salesforce_response(self, content: str) -> str:
|
||||||
|
content = content.strip()
|
||||||
|
if content is None or content == 'help':
|
||||||
|
return get_help_text()
|
||||||
|
if content.startswith('http') and 'force' in content:
|
||||||
|
return get_salesforce_link_details(content, self.sf)
|
||||||
|
for command in commands:
|
||||||
|
for command_keyword in command['commands']:
|
||||||
|
if content.startswith(command_keyword):
|
||||||
|
args = content.replace(command_keyword, '').strip()
|
||||||
|
if args is not None and args != '':
|
||||||
|
if 'callback' in command.keys():
|
||||||
|
return command['callback'](args, self.sf, command)
|
||||||
|
else:
|
||||||
|
return query_salesforce(args, self.sf, command)
|
||||||
|
else:
|
||||||
|
return 'Usage: {} [arguments]'.format(command['template'])
|
||||||
|
return get_help_text()
|
||||||
|
|
||||||
|
def initialize(self, bot_handler: Any) -> None:
|
||||||
|
self.config_info = bot_handler.get_config_info('salesforce')
|
||||||
|
try:
|
||||||
|
self.sf = simple_salesforce.Salesforce(
|
||||||
|
username=self.config_info['username'],
|
||||||
|
password=self.config_info['password'],
|
||||||
|
security_token=self.config_info['security_token']
|
||||||
|
)
|
||||||
|
except simple_salesforce.exceptions.SalesforceAuthenticationFailed as err:
|
||||||
|
logging.error(
|
||||||
|
'Failed to log in to Salesforce. {} {}'.format(err.code, err.message))
|
||||||
|
quit()
|
||||||
|
|
||||||
|
def handle_message(self, message: Any, bot_handler: Any) -> None:
|
||||||
|
try:
|
||||||
|
bot_response = self.get_salesforce_response(message['content'])
|
||||||
|
bot_handler.send_reply(message, bot_response)
|
||||||
|
except Exception as e:
|
||||||
|
bot_handler.send_reply('Error. {}.'.format(e), bot_response)
|
||||||
|
|
||||||
|
|
||||||
|
handler_class = SalesforceHandler
|
201
zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py
Normal file
201
zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
from zulip_bots.test_lib import BotTestCase, read_bot_fixture_data
|
||||||
|
import simple_salesforce
|
||||||
|
from simple_salesforce.exceptions import SalesforceAuthenticationFailed
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from unittest.mock import patch
|
||||||
|
from typing import Any, Dict
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mock_salesforce_query(test_name: str, bot_name: str) -> Any:
|
||||||
|
response_data = read_bot_fixture_data(bot_name, test_name)
|
||||||
|
sf_response = response_data.get('response')
|
||||||
|
|
||||||
|
with patch('simple_salesforce.api.Salesforce.query') as mock_query:
|
||||||
|
mock_query.return_value = sf_response
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mock_salesforce_auth(is_success: bool) -> Any:
|
||||||
|
if is_success:
|
||||||
|
with patch('simple_salesforce.api.Salesforce.__init__') as mock_sf_init:
|
||||||
|
mock_sf_init.return_value = None
|
||||||
|
yield
|
||||||
|
else:
|
||||||
|
with patch(
|
||||||
|
'simple_salesforce.api.Salesforce.__init__',
|
||||||
|
side_effect=SalesforceAuthenticationFailed(403, 'auth failed')
|
||||||
|
) as mock_sf_init:
|
||||||
|
mock_sf_init.return_value = None
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mock_salesforce_commands_types() -> Any:
|
||||||
|
with patch('zulip_bots.bots.salesforce.utils.commands', mock_commands), \
|
||||||
|
patch('zulip_bots.bots.salesforce.utils.object_types', mock_object_types):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
mock_config = {
|
||||||
|
'username': 'name@example.com',
|
||||||
|
'password': 'foo',
|
||||||
|
'security_token': 'abcdefg'
|
||||||
|
}
|
||||||
|
|
||||||
|
help_text = '''Salesforce bot
|
||||||
|
This bot can do simple salesforce query requests
|
||||||
|
**All commands must be @-mentioned to the bot.**
|
||||||
|
Commands:
|
||||||
|
**find contact <name> [arguments]**: finds contacts
|
||||||
|
**find top opportunities <amount> [arguments]**: finds opportunities
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
**-limit <num>**: the maximum number of entries sent (default: 5)
|
||||||
|
**-show**: show all the properties of each entry (default: false)
|
||||||
|
|
||||||
|
This bot can also show details about any Salesforce links sent to it.
|
||||||
|
|
||||||
|
Supported Object types:
|
||||||
|
These are the types of Salesforce object supported by this bot.
|
||||||
|
The bot cannot show the details of any other object types.
|
||||||
|
Table
|
||||||
|
Table
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def echo(arg: str, sf: Any, command: Dict[str, Any]) -> str:
|
||||||
|
return arg
|
||||||
|
|
||||||
|
|
||||||
|
mock_commands = [
|
||||||
|
{
|
||||||
|
'commands': ['find contact'],
|
||||||
|
'object': 'contact',
|
||||||
|
'description': 'finds contacts',
|
||||||
|
'template': 'find contact <name>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'commands': ['find top opportunities'],
|
||||||
|
'object': 'opportunity',
|
||||||
|
'query': 'SELECT {} FROM {} WHERE isClosed=false ORDER BY amount DESC LIMIT {}',
|
||||||
|
'description': 'finds opportunities',
|
||||||
|
'template': 'find top opportunities <amount>',
|
||||||
|
'rank_output': True,
|
||||||
|
'force_keys': ['Amount'],
|
||||||
|
'exclude_keys': ['Status'],
|
||||||
|
'show_all_keys': True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'commands': ['echo'],
|
||||||
|
'callback': echo
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
mock_object_types = {
|
||||||
|
'contact': {
|
||||||
|
'fields': 'Id, Name, Phone',
|
||||||
|
'table': 'Table'
|
||||||
|
},
|
||||||
|
'opportunity': {
|
||||||
|
'fields': 'Id, Name, Amount, Status',
|
||||||
|
'table': 'Table'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestSalesforceBot(BotTestCase):
|
||||||
|
bot_name = "salesforce" # type: str
|
||||||
|
|
||||||
|
def _test(self, test_name: str, message: str, response: str, auth_success: bool=True) -> None:
|
||||||
|
with self.mock_config_info(mock_config), \
|
||||||
|
mock_salesforce_auth(auth_success), \
|
||||||
|
mock_salesforce_query(test_name, 'salesforce'), \
|
||||||
|
mock_salesforce_commands_types():
|
||||||
|
self.verify_reply(message, response)
|
||||||
|
|
||||||
|
def _test_initialize(self, auth_success: bool=True) -> None:
|
||||||
|
with self.mock_config_info(mock_config), \
|
||||||
|
mock_salesforce_auth(auth_success), \
|
||||||
|
mock_salesforce_commands_types():
|
||||||
|
bot, bot_handler = self._get_handlers()
|
||||||
|
|
||||||
|
def test_bot_responds_to_empty_message(self) -> None:
|
||||||
|
self._test('test_one_result', '', help_text)
|
||||||
|
|
||||||
|
def test_one_result(self) -> None:
|
||||||
|
res = '''**[foo](https://login.salesforce.com/foo_id)**
|
||||||
|
>**Phone**: 020 1234 5678
|
||||||
|
'''
|
||||||
|
self._test('test_one_result', 'find contact foo', res)
|
||||||
|
|
||||||
|
def test_multiple_results(self) -> None:
|
||||||
|
res = '**[foo](https://login.salesforce.com/foo_id)**\n**[bar](https://login.salesforce.com/bar_id)**\n'
|
||||||
|
self._test('test_multiple_results', 'find contact foo', res)
|
||||||
|
|
||||||
|
def test_arg_show(self) -> None:
|
||||||
|
res = '''**[foo](https://login.salesforce.com/foo_id)**
|
||||||
|
>**Phone**: 020 1234 5678
|
||||||
|
|
||||||
|
**[bar](https://login.salesforce.com/bar_id)**
|
||||||
|
>**Phone**: 020 5678 1234
|
||||||
|
|
||||||
|
'''
|
||||||
|
self._test('test_multiple_results', 'find contact foo -show', res)
|
||||||
|
|
||||||
|
def test_no_results(self) -> None:
|
||||||
|
self._test('test_no_results', 'find contact foo', 'No records found.')
|
||||||
|
|
||||||
|
def test_rank_and_force_keys(self) -> None:
|
||||||
|
res = '''1) **[foo](https://login.salesforce.com/foo_id)**
|
||||||
|
>**Amount**: 2
|
||||||
|
|
||||||
|
2) **[bar](https://login.salesforce.com/bar_id)**
|
||||||
|
>**Amount**: 1
|
||||||
|
|
||||||
|
'''
|
||||||
|
self._test('test_top_opportunities', 'find top opportunities 2', res)
|
||||||
|
|
||||||
|
def test_limit_arg(self) -> None:
|
||||||
|
res = '''**[foo](https://login.salesforce.com/foo_id)**
|
||||||
|
>**Phone**: 020 1234 5678
|
||||||
|
'''
|
||||||
|
with self.assertLogs(level='INFO') as log:
|
||||||
|
self._test('test_one_result', 'find contact foo -limit 1', res)
|
||||||
|
self.assertIn('INFO:root:Searching with limit 1', log.output)
|
||||||
|
|
||||||
|
def test_help(self) -> None:
|
||||||
|
self._test('test_one_result', 'help', help_text)
|
||||||
|
self._test('test_one_result', 'foo bar baz', help_text)
|
||||||
|
self._test('test_one_result', 'find contact',
|
||||||
|
'Usage: find contact <name> [arguments]')
|
||||||
|
|
||||||
|
def test_bad_auth(self) -> None:
|
||||||
|
with self.assertLogs(level='ERROR') as log, \
|
||||||
|
patch('builtins.quit'):
|
||||||
|
self._test_initialize(auth_success=False)
|
||||||
|
self.assertIn(
|
||||||
|
'ERROR:root:Failed to log in to Salesforce. 403 auth failed', log.output)
|
||||||
|
|
||||||
|
def test_callback(self) -> None:
|
||||||
|
self._test('test_one_result', 'echo hello', 'hello')
|
||||||
|
|
||||||
|
def test_link_normal(self) -> None:
|
||||||
|
res = '''**[foo](https://login.salesforce.com/foo_id)**
|
||||||
|
>**Phone**: 020 1234 5678
|
||||||
|
'''
|
||||||
|
self._test('test_one_result',
|
||||||
|
'https://login.salesforce.com/1c3e5g7i9k1m3o5q7s', res)
|
||||||
|
|
||||||
|
def test_link_invalid(self) -> None:
|
||||||
|
self._test('test_one_result',
|
||||||
|
'https://login.salesforce.com/foo/bar/1c3e5g7$i9k1m3o5q7',
|
||||||
|
'Invalid salesforce link')
|
||||||
|
|
||||||
|
def test_link_no_results(self) -> None:
|
||||||
|
res = 'No object found. Make sure it is of the supported types. Type `help` for more info.'
|
||||||
|
self._test('test_no_results',
|
||||||
|
'https://login.salesforce.com/1c3e5g7i9k1m3o5q7s', res)
|
47
zulip_bots/zulip_bots/bots/salesforce/utils.py
Normal file
47
zulip_bots/zulip_bots/bots/salesforce/utils.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
link_query = 'SELECT {} FROM {} WHERE Id=\'{}\''
|
||||||
|
default_query = 'SELECT {} FROM {} WHERE Name LIKE \'%{}%\' LIMIT {}'
|
||||||
|
|
||||||
|
commands = [
|
||||||
|
{
|
||||||
|
'commands': ['search account', 'find account', 'search accounts', 'find accounts'],
|
||||||
|
'object': 'account',
|
||||||
|
'description': 'Returns a list of accounts of the name specified',
|
||||||
|
'template': 'search account <name>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'commands': ['search contact', 'find contact', 'search contacts', 'find contacts'],
|
||||||
|
'object': 'contact',
|
||||||
|
'description': 'Returns a list of contacts of the name specified',
|
||||||
|
'template': 'search contact <name>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'commands': ['search opportunity', 'find opportunity', 'search opportunities', 'find opportunities'],
|
||||||
|
'object': 'opportunity',
|
||||||
|
'description': 'Returns a list of opportunities of the name specified',
|
||||||
|
'template': 'search opportunity <name>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'commands': ['search top opportunity', 'find top opportunity', 'search top opportunities', 'find top opportunities'],
|
||||||
|
'object': 'opportunity',
|
||||||
|
'query': 'SELECT {} FROM {} WHERE isClosed=false ORDER BY amount DESC LIMIT {}',
|
||||||
|
'description': 'Returns a list of opportunities organised by amount',
|
||||||
|
'template': 'search top opportunities <amount>',
|
||||||
|
'rank_output': True,
|
||||||
|
'force_keys': ['Amount']
|
||||||
|
}
|
||||||
|
] # type: List[Dict[str, Any]]
|
||||||
|
|
||||||
|
object_types = {
|
||||||
|
'account': {
|
||||||
|
'fields': 'Id, Name, Phone, BillingStreet, BillingCity, BillingState',
|
||||||
|
'table': 'Account'
|
||||||
|
},
|
||||||
|
'contact': {
|
||||||
|
'fields': 'Id, Name, Phone, MobilePhone, Email',
|
||||||
|
'table': 'Contact'
|
||||||
|
},
|
||||||
|
'opportunity': {
|
||||||
|
'fields': 'Id, Name, Amount, Probability, StageName, CloseDate',
|
||||||
|
'table': 'Opportunity'
|
||||||
|
}
|
||||||
|
} # type: Dict[str, Dict[str, str]]
|
Loading…
Reference in a new issue