interactive bots: Create monkeytest.it bot.
This commit is contained in:
parent
1fd4dfc86e
commit
08bd395658
65
zulip_bots/zulip_bots/bots/monkeytestit/doc.md
Normal file
65
zulip_bots/zulip_bots/bots/monkeytestit/doc.md
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# monkeytest.it bot
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This bot provides a quick way to check a site from a chat.
|
||||||
|
Using their monkeytestit's API key, the user can check a website
|
||||||
|
for certain failures that user wants to see.
|
||||||
|
|
||||||
|
## Setting up API key
|
||||||
|
|
||||||
|
1. Get your monkeytest.it API key, located in your
|
||||||
|
[dashboard](https://monkeytest.it/dashboard).
|
||||||
|
|
||||||
|
2. Create your own `monkeytestit.conf` file by copying the existing one in
|
||||||
|
`bots/monkeytest/monkeytestit.conf`.
|
||||||
|
|
||||||
|
3. Inside the config file, you will see this:
|
||||||
|
```
|
||||||
|
[monkeytestit]
|
||||||
|
api_key = <api key here>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Replace `<api key here>` with your API key
|
||||||
|
|
||||||
|
5. Save the configuration file.
|
||||||
|
|
||||||
|
## Running the bot
|
||||||
|
|
||||||
|
Let `<path_to_config>` be the path to the config file, and let
|
||||||
|
`<path_to_zuliprc>` be the path to the zuliprc file.
|
||||||
|
|
||||||
|
You can run the bot by running:
|
||||||
|
|
||||||
|
`zulip-run-bot -b <path_to_config> monkeytestit --config-file
|
||||||
|
<path_to_zuliprc>`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
**Note**: You **must** not forget to put `http://` or `https://`
|
||||||
|
before a website. Otherwise, the check will fail.
|
||||||
|
|
||||||
|
### Simple check with default settings
|
||||||
|
|
||||||
|
To check a website with all enabled checkers, run:
|
||||||
|
`check https://website`
|
||||||
|
|
||||||
|
### Check with options
|
||||||
|
|
||||||
|
To check a website with certain enabled checkers, run:
|
||||||
|
`check https://website <checker_options>`
|
||||||
|
|
||||||
|
The checker options are supplied to: `on_load`, `on_click`, `page_weight`,
|
||||||
|
`seo`, `broken_links`, `asset_count` **in order**.
|
||||||
|
|
||||||
|
Example 1: Disable `on_load`, enable the rest
|
||||||
|
command: `check https://website 0`
|
||||||
|
|
||||||
|
Example 2: Disable `asset_count`, enable the rest
|
||||||
|
command: `check https//website 1 1 1 1 1 0`
|
||||||
|
|
||||||
|
Example 3: Disable `on_load` and `page_weight`, enable the rest
|
||||||
|
command: `check https://website 0 1 0`
|
||||||
|
|
||||||
|
So for instance, if you wanted to disable `asset_count`, you have
|
||||||
|
to supply every params before it.
|
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"api_url": "https://monkeytest.it/test",
|
||||||
|
"params": {
|
||||||
|
"on_load": "true",
|
||||||
|
"seo": "true",
|
||||||
|
"secret": "None",
|
||||||
|
"broken_links": "true",
|
||||||
|
"url": "https://website.com",
|
||||||
|
"on_click": "true",
|
||||||
|
"page_weight": "true",
|
||||||
|
"asset_count": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"results_url": "https://monkeytest.it/test/33ffc164-cccd-4466-8008-3de00ba787e8",
|
||||||
|
"status": "tests_failed",
|
||||||
|
"enabled_checkers": {
|
||||||
|
"seo": true,
|
||||||
|
"asset_count": true,
|
||||||
|
"on_click": true,
|
||||||
|
"on_load": true,
|
||||||
|
"page_weight": true,
|
||||||
|
"broken_links": true
|
||||||
|
},
|
||||||
|
"failures": {
|
||||||
|
"seo": {
|
||||||
|
"Images missing alt": [
|
||||||
|
"div#site-header > header > div > div.logo.box > a > img",
|
||||||
|
"div#site-header > header > div > div.menu-button.box > a > img",
|
||||||
|
"div#homeTemplateCarousel > div > div > div.slick-slide.slick-cloned > img",
|
||||||
|
"div#homeTemplateCarousel > div > div > div.slick-slide.slick-cloned > img",
|
||||||
|
"div#homeTemplateCarousel > div > div > div.slick-slide.slick-current.slick-active.slick-center > img",
|
||||||
|
"div#homeTemplateCarousel > div > div > div:nth-child(4) > img",
|
||||||
|
"div#homeTemplateCarousel > div > div > div:nth-child(5) > img",
|
||||||
|
"div#homeTemplateCarousel > div > div > div.slick-slide.slick-cloned.slick-center > img",
|
||||||
|
"div#homeTemplateCarousel > div > div > div.slick-slide.slick-cloned > img"
|
||||||
|
],
|
||||||
|
"Too many H1 tags": [
|
||||||
|
"div#banner > div > h1",
|
||||||
|
"html > body > div.ui-loader.ui-corner-all.ui-body-a.ui-loader-default > h1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response-headers": {
|
||||||
|
"Content-Type":"application/json",
|
||||||
|
"Server":"cloudflare",
|
||||||
|
"Date":"Tue, 02 Jan 2018 14:29:18 GMT",
|
||||||
|
"Content-Length":"1280",
|
||||||
|
"CF-RAY":"3d6e6815a83817bc-SIN",
|
||||||
|
"Connection":"keep-alive",
|
||||||
|
"Set-Cookie":"__cfduid=dd7e2a746cf1abadd1348ba6411ad21391514903325; expires=Wed, 02-Jan-19 14:28:45 GMT; path=/; domain=.monkeytest.it; HttpOnly"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"api_url": "https://monkeytest.it/test",
|
||||||
|
"params": {
|
||||||
|
"on_load": "true",
|
||||||
|
"seo": "true",
|
||||||
|
"secret": "None",
|
||||||
|
"broken_links": "true",
|
||||||
|
"url": "https://website.com",
|
||||||
|
"on_click": "true",
|
||||||
|
"page_weight": "true",
|
||||||
|
"asset_count": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"enabled_checkers":{
|
||||||
|
"asset_count":false,
|
||||||
|
"broken_links":true,
|
||||||
|
"on_click":false,
|
||||||
|
"on_load":false,
|
||||||
|
"page_weight":false,
|
||||||
|
"seo":false
|
||||||
|
},
|
||||||
|
"status":"success",
|
||||||
|
"test_id":"a7c33bc6-35de-468c-81c1-7317162c02ab"
|
||||||
|
},
|
||||||
|
"response-headers": {
|
||||||
|
"Content-Type":"application/json",
|
||||||
|
"Server":"cloudflare",
|
||||||
|
"Date":"Tue, 02 Jan 2018 15:08:03 GMT",
|
||||||
|
"Transfer-Encoding":"chunked",
|
||||||
|
"CF-RAY":"3d6e9f90c8c5080b-SIN",
|
||||||
|
"Connection":"keep-alive",
|
||||||
|
"Content-Encoding":"gzip",
|
||||||
|
"Set-Cookie":"__cfduid=d709afa5ead1c50719f24ddf7266655791514905597; expires=Wed, 02-Jan-19 15:06:37 GMT; path=/; domain=.monkeytest.it; HttpOnly"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
28
zulip_bots/zulip_bots/bots/monkeytestit/lib/extract.py
Normal file
28
zulip_bots/zulip_bots/bots/monkeytestit/lib/extract.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
"""This module fetches the report from the website by doing a get request to
|
||||||
|
the predefined url. The result is then parsed to JSON for further management
|
||||||
|
in report.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(options: dict):
|
||||||
|
"""Makes a request then returns the dictionary version of the response
|
||||||
|
|
||||||
|
:param options: Options dictionary containing the payload for the request
|
||||||
|
:return: A dictionary containing keys and values to be managed by report.py
|
||||||
|
:raises JSONDecodeError: if the get is unsuccessful. This could mean
|
||||||
|
faulty link or any other causes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
res = requests.get("https://monkeytest.it/test", params=options)
|
||||||
|
|
||||||
|
if "server timed out" in res.text:
|
||||||
|
return {"error": "The server timed out before sending a response to "
|
||||||
|
"the request. Report is available at "
|
||||||
|
"[Test Report History]"
|
||||||
|
"(https://monkeytest.it/dashboard)."}
|
||||||
|
|
||||||
|
return json.loads(res.text)
|
72
zulip_bots/zulip_bots/bots/monkeytestit/lib/parse.py
Normal file
72
zulip_bots/zulip_bots/bots/monkeytestit/lib/parse.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
"""Used to parse message and return a dictionary containing a payload
|
||||||
|
for extract.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from json.decoder import JSONDecodeError
|
||||||
|
from typing import Text
|
||||||
|
|
||||||
|
from zulip_bots.bots.monkeytestit.lib import extract
|
||||||
|
from zulip_bots.bots.monkeytestit.lib import report
|
||||||
|
|
||||||
|
|
||||||
|
def execute(message: Text, apikey: Text) -> Text:
|
||||||
|
"""Parses message and returns a dictionary
|
||||||
|
|
||||||
|
:param message: The message
|
||||||
|
:param apikey: A MonkeyTestit api key, presumably in the config file
|
||||||
|
:return: A response string
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = message.split(" ")
|
||||||
|
command = params[0]
|
||||||
|
|
||||||
|
if "check" in command.lower():
|
||||||
|
len_params = len(params)
|
||||||
|
|
||||||
|
if len_params < 2:
|
||||||
|
return failed("You **must** provide at least an URL to perform a "
|
||||||
|
"check.")
|
||||||
|
|
||||||
|
options = {"secret": apikey, "url": params[1], "on_load": "true",
|
||||||
|
"on_click": "true", "page_weight": "true", "seo": "true",
|
||||||
|
"broken_links": "true", "asset_count": "true"}
|
||||||
|
|
||||||
|
# Set the options only if supplied
|
||||||
|
|
||||||
|
if len_params >= 3:
|
||||||
|
options["on_load"] = "true" if params[2] == "1" else "false"
|
||||||
|
if len_params >= 4:
|
||||||
|
options["on_click"] = "true" if params[3] == "1" else "false"
|
||||||
|
if len_params >= 5:
|
||||||
|
options["page_weight"] = "true" if params[4] == "1" else "false"
|
||||||
|
if len_params >= 6:
|
||||||
|
options["seo"] = "true" if params[5] == "1" else "false"
|
||||||
|
if len_params >= 7:
|
||||||
|
options["broken_links"] = "true" if params[6] == "1" else "false"
|
||||||
|
if len_params >= 8:
|
||||||
|
options["asset_count"] = "true" if params[7] == "1" else "false"
|
||||||
|
|
||||||
|
try:
|
||||||
|
fetch_result = extract.fetch(options)
|
||||||
|
except JSONDecodeError:
|
||||||
|
return failed("Cannot decode a JSON response. "
|
||||||
|
"Perhaps faulty link. Link must start "
|
||||||
|
"with `http://` or `https://`.")
|
||||||
|
|
||||||
|
return report.compose(fetch_result)
|
||||||
|
|
||||||
|
# The disadvantage here is that the user has to supply every params if
|
||||||
|
# the user needs to modify the asset_count. There are probably ways
|
||||||
|
# to counteract this, but I think this is more fast to run.
|
||||||
|
else:
|
||||||
|
return "Unknown command. Available commands: `check <website> " \
|
||||||
|
"[params]`"
|
||||||
|
|
||||||
|
|
||||||
|
def failed(message: Text) -> Text:
|
||||||
|
"""Simply attaches a failed marker to a message
|
||||||
|
|
||||||
|
:param message: The message
|
||||||
|
:return: String
|
||||||
|
"""
|
||||||
|
return "Failed: " + message
|
128
zulip_bots/zulip_bots/bots/monkeytestit/lib/report.py
Normal file
128
zulip_bots/zulip_bots/bots/monkeytestit/lib/report.py
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
"""Used to mainly compose a decorated report for the user
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Text, List
|
||||||
|
|
||||||
|
|
||||||
|
def compose(results: Dict) -> Text:
|
||||||
|
"""Composes a report based on test results
|
||||||
|
|
||||||
|
An example would be:
|
||||||
|
|
||||||
|
Status: tests_failed
|
||||||
|
Enabled checkers: seo
|
||||||
|
Failures from checkers: seo (3)
|
||||||
|
More info: https://monkeytest.it/...
|
||||||
|
|
||||||
|
This function assumes that the result dict is valid and does not contain
|
||||||
|
any "errors" like bad url
|
||||||
|
|
||||||
|
:param results: A dictionary containing the results of a check
|
||||||
|
:return: A response string containing the full report
|
||||||
|
"""
|
||||||
|
if "error" in results:
|
||||||
|
return "Error: {}".format(results['error'])
|
||||||
|
|
||||||
|
response = ""
|
||||||
|
|
||||||
|
response += "{}\n".format(print_status(results))
|
||||||
|
|
||||||
|
if "success" in response.lower():
|
||||||
|
response += "{}".format(print_test_id(results))
|
||||||
|
return response
|
||||||
|
|
||||||
|
response += "{}\n".format(print_enabled_checkers(results))
|
||||||
|
response += "{}\n".format(print_failures_checkers(results))
|
||||||
|
response += "{}".format(print_more_info_url(results))
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def print_more_info_url(results: Dict) -> Text:
|
||||||
|
"""Creates info for the test URL from monkeytest.it
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
More info: https://monkeytest.it/test/...
|
||||||
|
|
||||||
|
:param results: A dictionary containing the results of a check
|
||||||
|
:return: A response string containing the url info
|
||||||
|
"""
|
||||||
|
return "More info: {}".format(results['results_url'])
|
||||||
|
|
||||||
|
|
||||||
|
def print_test_id(results: Dict) -> Text:
|
||||||
|
"""Prints the test-id with attached to the url
|
||||||
|
|
||||||
|
:param results: A dictionary containing the results of a check
|
||||||
|
:return: A response string containing the test id
|
||||||
|
"""
|
||||||
|
return "Test: https://monkeytest.it/test/{}".format(results['test_id'])
|
||||||
|
|
||||||
|
|
||||||
|
def print_failures_checkers(results: Dict) -> Text:
|
||||||
|
"""Creates info for failures in enabled checkers
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
Failures from checkers: broken_links (3), seo (5)
|
||||||
|
|
||||||
|
This means that the check has 8 section failures, 3 sections in
|
||||||
|
broken_links and the other 5 are in seo.
|
||||||
|
|
||||||
|
:param results: A dictionary containing the results of a check
|
||||||
|
:return: A response string containing number of failures in each enabled
|
||||||
|
checkers
|
||||||
|
"""
|
||||||
|
failures_checkers = [(checker, len(results['failures'][checker]))
|
||||||
|
for checker in get_enabled_checkers(results)
|
||||||
|
if checker in results['failures']] # [('seo', 3), ..]
|
||||||
|
|
||||||
|
failures_checkers_messages = ["{} ({})".format(fail_checker[0],
|
||||||
|
fail_checker[1]) for fail_checker in
|
||||||
|
failures_checkers]
|
||||||
|
|
||||||
|
failures_checkers_message = ", ".join(failures_checkers_messages)
|
||||||
|
return "Failures from checkers: {}".format(failures_checkers_message)
|
||||||
|
|
||||||
|
|
||||||
|
def get_enabled_checkers(results: Dict) -> List:
|
||||||
|
"""Gets enabled checkers
|
||||||
|
|
||||||
|
For example, if enabled_checkers: {'seo' : True, 'broken_links' : False,
|
||||||
|
'page_weight' : true}, it will return ['seo'. 'page_weight']
|
||||||
|
|
||||||
|
:param results: A dictionary containing the results of a check
|
||||||
|
:return: A list containing enabled checkers
|
||||||
|
"""
|
||||||
|
checkers = results['enabled_checkers']
|
||||||
|
enabled_checkers = []
|
||||||
|
for checker in checkers.keys():
|
||||||
|
if checkers[checker]: # == True/False
|
||||||
|
enabled_checkers.append(checker)
|
||||||
|
return enabled_checkers
|
||||||
|
|
||||||
|
|
||||||
|
def print_enabled_checkers(results: Dict) -> Text:
|
||||||
|
"""Creates info for enabled checkers. This joins the list of enabled
|
||||||
|
checkers and format it with the current string response
|
||||||
|
|
||||||
|
For example, if get_enabled_checkers = ['seo', 'page_weight'] then it would
|
||||||
|
return "Enabled checkers: seo, page_weight"
|
||||||
|
|
||||||
|
:param results: A dictionary containing the results of a check
|
||||||
|
:return: A response string containing enabled checkers
|
||||||
|
"""
|
||||||
|
return "Enabled checkers: {}".format(", "
|
||||||
|
.join(get_enabled_checkers(results)))
|
||||||
|
|
||||||
|
|
||||||
|
def print_status(results: Dict) -> Text:
|
||||||
|
"""Creates info for the check status.
|
||||||
|
|
||||||
|
Example: Status: tests_failed
|
||||||
|
|
||||||
|
:param results: A dictionary containing the results of a check
|
||||||
|
:return: A response string containing check status
|
||||||
|
"""
|
||||||
|
return "Status: {}".format(results['status'])
|
|
@ -0,0 +1,2 @@
|
||||||
|
[monkeytestit]
|
||||||
|
api_key = <api key here>
|
52
zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.py
Normal file
52
zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from zulip_bots.bots.monkeytestit.lib import parse
|
||||||
|
from zulip_bots.lib import NoBotConfigException
|
||||||
|
|
||||||
|
|
||||||
|
class MonkeyTestitBot(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.api_key = "None"
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
def usage(self):
|
||||||
|
return "Remember to set your api_key first in the config. After " \
|
||||||
|
"that, to perform a check, mention me and add the website.\n\n" \
|
||||||
|
"Check doc.md for more options and setup instructions."
|
||||||
|
|
||||||
|
def initialize(self, bot_handler: Any) -> None:
|
||||||
|
try:
|
||||||
|
self.config = bot_handler.get_config_info('monkeytestit')
|
||||||
|
except NoBotConfigException:
|
||||||
|
bot_handler.quit("Quitting because there's no config file "
|
||||||
|
"supplied. See doc.md for a guide on setting up "
|
||||||
|
"one. If you already know the drill, just create "
|
||||||
|
"a .conf file with \"monkeytestit\" as the "
|
||||||
|
"section header and api_key = <your key> for "
|
||||||
|
"the api key.")
|
||||||
|
|
||||||
|
self.api_key = self.config.get('api_key')
|
||||||
|
|
||||||
|
if not self.api_key:
|
||||||
|
bot_handler.quit("Config file exists, but can't find api_key key "
|
||||||
|
"or value. Perhaps it is misconfigured. Check "
|
||||||
|
"doc.md for details on how to setup the config.")
|
||||||
|
|
||||||
|
logging.info("Checking validity of API key. This will take a while.")
|
||||||
|
|
||||||
|
if "wrong secret" in parse.execute("check https://website",
|
||||||
|
self.api_key).lower():
|
||||||
|
bot_handler.quit("API key exists, but it is not valid. Reconfigure"
|
||||||
|
" your api_key value and try again.")
|
||||||
|
|
||||||
|
def handle_message(self, message: Dict[str, str],
|
||||||
|
bot_handler: Any) -> None:
|
||||||
|
content = message['content']
|
||||||
|
|
||||||
|
response = parse.execute(content, self.api_key)
|
||||||
|
|
||||||
|
bot_handler.send_reply(message, response)
|
||||||
|
|
||||||
|
|
||||||
|
handler_class = MonkeyTestitBot
|
46
zulip_bots/zulip_bots/bots/monkeytestit/test_monkeytestit.py
Normal file
46
zulip_bots/zulip_bots/bots/monkeytestit/test_monkeytestit.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import zulip_bots.bots.monkeytestit.monkeytestit
|
||||||
|
from zulip_bots.test_lib import BotTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestMonkeyTestitBot(BotTestCase):
|
||||||
|
bot_name = "monkeytestit"
|
||||||
|
|
||||||
|
def test_bot_responds_to_empty_message(self):
|
||||||
|
message = dict(
|
||||||
|
content='',
|
||||||
|
type='stream',
|
||||||
|
)
|
||||||
|
with mock.patch.object(
|
||||||
|
zulip_bots.bots.monkeytestit.monkeytestit.MonkeyTestitBot,
|
||||||
|
'initialize', return_value=None):
|
||||||
|
with self.mock_config_info({'api_key': "magic"}):
|
||||||
|
res = self.get_response(message)
|
||||||
|
self.assertTrue("Unknown command" in res['content'])
|
||||||
|
|
||||||
|
def test_website_fail(self):
|
||||||
|
message = dict(
|
||||||
|
content='check https://website.com',
|
||||||
|
type='stream',
|
||||||
|
)
|
||||||
|
with mock.patch.object(
|
||||||
|
zulip_bots.bots.monkeytestit.monkeytestit.MonkeyTestitBot,
|
||||||
|
'initialize', return_value=None):
|
||||||
|
with self.mock_config_info({'api_key': "magic"}):
|
||||||
|
with self.mock_http_conversation('website_result_fail'):
|
||||||
|
res = self.get_response(message)
|
||||||
|
self.assertTrue("Status: tests_failed" in res['content'])
|
||||||
|
|
||||||
|
def test_website_success(self):
|
||||||
|
message = dict(
|
||||||
|
content='check https://website.com',
|
||||||
|
type='stream',
|
||||||
|
)
|
||||||
|
with mock.patch.object(
|
||||||
|
zulip_bots.bots.monkeytestit.monkeytestit.MonkeyTestitBot,
|
||||||
|
'initialize', return_value=None):
|
||||||
|
with self.mock_config_info({'api_key': "magic"}):
|
||||||
|
with self.mock_http_conversation('website_result_success'):
|
||||||
|
res = self.get_response(message)
|
||||||
|
self.assertTrue("success" in res['content'])
|
Loading…
Reference in a new issue