diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/doc.md b/zulip_bots/zulip_bots/bots/monkeytestit/doc.md new file mode 100644 index 0000000..3298978 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/monkeytestit/doc.md @@ -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 = + ``` + +4. Replace `` with your API key + +5. Save the configuration file. + +## Running the bot + +Let `` be the path to the config file, and let +`` be the path to the zuliprc file. + +You can run the bot by running: + +`zulip-run-bot -b monkeytestit --config-file +` + +## 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 ` + +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. diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/fixtures/website_result_fail.json b/zulip_bots/zulip_bots/bots/monkeytestit/fixtures/website_result_fail.json new file mode 100644 index 0000000..7551847 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/monkeytestit/fixtures/website_result_fail.json @@ -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" + } +} + diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/fixtures/website_result_success.json b/zulip_bots/zulip_bots/bots/monkeytestit/fixtures/website_result_success.json new file mode 100644 index 0000000..4c9894d --- /dev/null +++ b/zulip_bots/zulip_bots/bots/monkeytestit/fixtures/website_result_success.json @@ -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" + } +} + diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/lib/__init__.py b/zulip_bots/zulip_bots/bots/monkeytestit/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/lib/extract.py b/zulip_bots/zulip_bots/bots/monkeytestit/lib/extract.py new file mode 100644 index 0000000..90b795d --- /dev/null +++ b/zulip_bots/zulip_bots/bots/monkeytestit/lib/extract.py @@ -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) diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/lib/parse.py b/zulip_bots/zulip_bots/bots/monkeytestit/lib/parse.py new file mode 100644 index 0000000..b6487d6 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/monkeytestit/lib/parse.py @@ -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 " \ + "[params]`" + + +def failed(message: Text) -> Text: + """Simply attaches a failed marker to a message + + :param message: The message + :return: String + """ + return "Failed: " + message diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/lib/report.py b/zulip_bots/zulip_bots/bots/monkeytestit/lib/report.py new file mode 100644 index 0000000..4e03af3 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/monkeytestit/lib/report.py @@ -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']) diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.conf b/zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.conf new file mode 100644 index 0000000..7bc72a7 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.conf @@ -0,0 +1,2 @@ +[monkeytestit] +api_key = \ No newline at end of file diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.py b/zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.py new file mode 100644 index 0000000..9cc14c2 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.py @@ -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 = 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 diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/test_monkeytestit.py b/zulip_bots/zulip_bots/bots/monkeytestit/test_monkeytestit.py new file mode 100644 index 0000000..ba9f37c --- /dev/null +++ b/zulip_bots/zulip_bots/bots/monkeytestit/test_monkeytestit.py @@ -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'])