interactive bots: Create monkeytest.it bot.
ADD
zulip_bots/zulip_bots/bots/monkeytestit/doc.md
+# 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.

ADD
zulip_bots/zulip_bots/bots/monkeytestit/fixtures/website_result_fail.json
+{
+ "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"
+ }
+}
+

ADD
zulip_bots/zulip_bots/bots/monkeytestit/fixtures/website_result_success.json
+{
+ "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"
+ }
+}
+

ADD
zulip_bots/zulip_bots/bots/monkeytestit/lib/__init__.py
zulip_bots/zulip_bots/bots/monkeytestit/lib/__init__.py
ADD
zulip_bots/zulip_bots/bots/monkeytestit/lib/extract.py
+"""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)

ADD
zulip_bots/zulip_bots/bots/monkeytestit/lib/parse.py
+"""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

ADD
zulip_bots/zulip_bots/bots/monkeytestit/lib/report.py
+"""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'])

ADD
zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.conf
+[monkeytestit]
+api_key = <api key here>
\ No newline at end of file

ADD
zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.py
+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

ADD
zulip_bots/zulip_bots/bots/monkeytestit/test_monkeytestit.py
+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'])

interactive bots: Create monkeytest.it bot.