diff --git a/.arcconfig b/.arcconfig deleted file mode 100644 index 2ccf95b..0000000 --- a/.arcconfig +++ /dev/null @@ -1,27 +0,0 @@ -{ - "hosts": { - "http://phab.lubuntu.me/": { - "token": "" - } - }, - "config": { - "default": "http://phab.lubunutu.me/api/" - }, - "HMAC": i{ - "irc": "", - }, - "irc": { - "host": "irc.freenode.net", - "port": "6697", - "username": "", - "password": "", - "channel": "#lubuntu-devel" - }, - "launchpad": { - "application": "lugito", - "staging": "production", - "version": "devel", - "supported_versions": ["Cosmic", "Bionic", "Xenial", "Trusty"] - } -} - diff --git a/.gitignore b/.gitignore index 81d52e7..cdbd2a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ # Created by .ignore support plugin (hsz.mobi) # Config file .arcconfig +.lugitorc + +# launchpad files +devel/* + # ### Python template # Byte-compiled / optimized / DLL files diff --git a/AUTHORS.rst b/AUTHORS.rst index c7b9af5..5089bbd 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -5,9 +5,9 @@ Credits Development Lead ---------------- -* Ben Johnston +* Simon Quigley Contributors ------------- +----------- -None yet. Why not be the first? +* Ben Johnston (docEbrown) diff --git a/HISTORY.rst b/HISTORY.rst index 780647c..7e9be4a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,7 +2,28 @@ History ======= -0.1.0 (2018-11-07) ------------------- +0.1.0 (20th May 2018) +---------------------- -* First release on PyPI. +* Initial Release + + +0.2.0 (21st November 2018) +--------------------------- + +* added functionality for reporting creation, comments and edits on comments of + diffs +* Refactored into Python package +* irc and launchpad connections written into separate Python modules +* corrected issues with reporting using links with anchors via IRC +* added some unittests + +* **Still TODO** + + * Documentation - read the docs + * Add decorators to Lugito class to check that a request has been validated before other tasks can be completed + * Improve test coverage + * Pypi upload + * Add exceptions in the event that connector send method calls are not executed correctly + * CI and automated docs and Pypi udpate + * in-situ testing \ No newline at end of file diff --git a/README.md b/README.md index 22cd1c5..f09886b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,65 @@ -# Lugito +lugito +====== -This is Lubuntu's friendly IRC notifications bot, hooked up to our Phabricator instance at phab.lubuntu.me +[![image](https://img.shields.io/pypi/v/lugito.svg)](https://pypi.python.org/pypi/lugito) -The code is licensed under the 3-clause BSD license, and is copyrighted by the Lubuntu team. More info available in LICENSE. +[![image](https://img.shields.io/travis/doc-E-brown/lugito.svg)](https://travis-ci.org/doc-E-brown/lugito) + +[![Documentation Status](https://readthedocs.org/projects/lugito/badge/?version=latest)](https://lugito.readthedocs.io/en/latest/?badge=latest) + +Python Boilerplate contains all the boilerplate you need to create a +Python package. + +- Free software: 3 Clause BSD license +- Documentation: . + +Temp - Example .lugitorc +------------------------ + +``` +[phabricator] +host = http://127.0.0.1:9091/api/ +token = api-nojs2ip33hmp4zn6u6cf72w7d6yh + +[phabricator.hooks] +irc = cqg42zdcuqysff632kc6rnsu4m3hjg6c +commithook = znkyfflbcia5gviqx5ybad7s6uyfywxi + +[connector.irc] +host = irc.freenode.net +port = 6697 +username = someusername +password = somepassword +channel = #somechannel + +[connector.launchpad] +application = lugito +staging = production +version = devel +supported_versions = + Cosmic + Bionic + Xenial + Trusty + +[connector.launchpad.package_names] +rDEFAULTSETTINGS = lubuntu-default-settings +rART = lubuntu-artwork +rCALASETTINGS = calamares-settings-ubuntu +rQTERMINALPACKAGING = qterminal +rLXQTCONFIGPACKAGING = lxqt-config +rNMTRAYPACKAGING = nm-tray +``` + +Features +-------- + +- TODO + +Credits +------- + +This package was created with +[Cookiecutter](https://github.com/audreyr/cookiecutter) and the +[audreyr/cookiecutter-pypackage](https://github.com/audreyr/cookiecutter-pypackage) +project template. diff --git a/README.rst b/README.rst index 37d20f5..c438d6f 100644 --- a/README.rst +++ b/README.rst @@ -2,25 +2,55 @@ lugito ====== +lugito is a Python package that provides a webserver for connecting updates from Phabricator to communication tools such as irc and other services such as launchpad. + +Installation +------------- + +lugito can be installed via Pyp + + +* Free software: 3 Clause BSD license + + +Temp - Example .lugitorc +------------------------- + +.. code:: + + [phabricator] + host = http://127.0.0.1:9091/api/ + token = api-nojs2ip33hmp4zn6u6cf72w7d6yh + + [phabricator.hooks] + irc = cqg42zdcuqysff632kc6rnsu4m3hjg6c + commithook = znkyfflbcia5gviqx5ybad7s6uyfywxi + + [connector.irc] + host = irc.freenode.net + port = 6697 + username = someusername + password = somepassword + channel = #somechannel + + [connector.launchpad] + application = lugito + staging = production + version = devel + supported_versions = + Cosmic + Bionic + Xenial + Trusty + + [connector.launchpad.package_names] + rDEFAULTSETTINGS = lubuntu-default-settings + rART = lubuntu-artwork + rCALASETTINGS = calamares-settings-ubuntu + rQTERMINALPACKAGING = qterminal + rLXQTCONFIGPACKAGING = lxqt-config + rNMTRAYPACKAGING = nm-tray -.. image:: https://img.shields.io/pypi/v/lugito.svg - :target: https://pypi.python.org/pypi/lugito - -.. image:: https://img.shields.io/travis/doc-E-brown/lugito.svg - :target: https://travis-ci.org/doc-E-brown/lugito - -.. image:: https://readthedocs.org/projects/lugito/badge/?version=latest - :target: https://lugito.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status - - - - -Python Boilerplate contains all the boilerplate you need to create a Python package. - - -* Free software: BSD license -* Documentation: https://lugito.readthedocs.io. Features diff --git a/lugito/__init__.py b/lugito/__init__.py index d42007e..24e0c0a 100644 --- a/lugito/__init__.py +++ b/lugito/__init__.py @@ -2,14 +2,7 @@ from lugito.lugito import ( Lugito, ) -from lugito.connectors.irc import ( - IRCConnector, -) - -from lugito.connectors.launchpad import ( - LPConnectook, -) - +import lugito.config from ._version import get_versions __version__ = get_versions()['version'] diff --git a/lugito/cli.py b/lugito/cli.py index 03e261c..96ca34c 100644 --- a/lugito/cli.py +++ b/lugito/cli.py @@ -13,7 +13,6 @@ # Imports -from lugito.webhooks import run - if __name__ == "__main__": + from lugito.webhooks import run run() diff --git a/lugito/config.py b/lugito/config.py new file mode 100644 index 0000000..5efe0b6 --- /dev/null +++ b/lugito/config.py @@ -0,0 +1,135 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# S.D.G + +""" +:mod:`lugito.config` +====================================== + +Module to manage lugito configuration + +.. currentmodule:: lugito.config +""" + +# Imports +import os +import logging +import configparser + +DEFAULT_CONFIG_FILE = os.path.join( + os.getcwd(), '.lugitorc') + +logger = logging.getLogger('lugito.config') + +# Add log level +ch = logging.StreamHandler() + +formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + +ch.setFormatter(formatter) +logger.addHandler(ch) +logger.setLevel(logging.DEBUG) + +CONFIG = {} + + +def update_config(config_file=DEFAULT_CONFIG_FILE): + """ + Update the system config from a config file + + Parameters + ---------- + + config_file: str + The path of the lugito config file + + Returns + ------- + + config: dictionary + A dictionary of config parameters + + """ + config = configparser.ConfigParser() + config.read(config_file) + + # Make some basic assertions for minimum functionality + if 'phabricator' not in config: + raise ValueError('phabricator section missing from config file: %s' %\ + config_file) + + if 'host' not in config['phabricator']: + raise ValueError('host value missing from phabricator section in'\ + ' config file: %s' % config_file) + + if 'token' not in config['phabricator']: + raise ValueError('token value missing from phabricator section in'\ + ' config file: %s' % config_file) + + CONFIG['phabricator'] = {} + CONFIG['phabricator']['host'] = config['phabricator']['host'] + CONFIG['phabricator']['token'] = config['phabricator']['token'] + + CONFIG['phabricator']['hooks'] = {} + + # Iterate through hooks for HMAC keys + if 'phabricator.hooks' in config: + + for key, value in config['phabricator.hooks'].items(): + CONFIG['phabricator']['hooks'][key] = value + + CONFIG['connectors'] = {} + + # Iterate through available connectors + for key in config.keys(): + + # Is a connector section + if ('connector.' in key) and (key.count('.') == 1) : + connector = key.split('.')[1] + + if 'connectors' not in CONFIG: + CONFIG['connectors'] = {} + + if connector not in CONFIG['connectors']: + CONFIG['connectors'][connector] = {} + + for param, value in config[key].items(): + + # Check for multiple values for a parameter + if value.find('\n') >= 0: + value = value[1:].split('\n') + + CONFIG['connectors'][connector][param] = value + + # Is a connector sub-section + elif ('connector.' in key) and (key.count('.') > 1) : + sections = key.split('.') + connector = sections[1] + subsection = sections[-1] + + if 'connectors' not in CONFIG: + CONFIG['connectors'] = {} + + if connector not in CONFIG['connectors']: + CONFIG['connectors'][connector] = {} + + CONFIG['connectors'][connector][subsection] = {} + + for param, value in config[key].items(): + + # Check for multiple values for a parameter + if value.find('\n') >= 0: + value = value[1:].split('\n') + + # configparser reads the parameters as lower case + # convert all but first character to upper case + param = 'r{}'.format(param[1:].upper()) + CONFIG['connectors'][connector][subsection][param] = value + + +try: + update_config() +except ValueError: + # The config file is not present + logging.warning('Default config file: %s not found' % DEFAULT_CONFIG_FILE) diff --git a/lugito/connectors/__init__.py b/lugito/connectors/__init__.py index 12c9400..efe4ff4 100644 --- a/lugito/connectors/__init__.py +++ b/lugito/connectors/__init__.py @@ -1,3 +1,11 @@ #! /usr/bin/env python # -*- coding: utf-8 -*- # S.D.G + +from lugito.connectors.irc import ( + irc, +) + +from lugito.connectors.launchpad import ( + launchpad, +) diff --git a/lugito/connectors/irc.py b/lugito/connectors/irc.py index c0a0958..555da7d 100644 --- a/lugito/connectors/irc.py +++ b/lugito/connectors/irc.py @@ -18,25 +18,27 @@ import socket import logging import threading import phabricator +import lugito from time import sleep -class IRCConnector(object): +class irc(object): - def __init__(self, log_level=logging.DEBUG): + def __init__(self, log_level=logging.DEBUG, sleep_delay=5): # IRC info # Read the configuration out of the .arcconfig file - self.host = phabricator.ARCRC['irc']['host'] - self.port = int(phabricator.ARCRC['irc']['port']) - self.username = phabricator.ARCRC['irc']['username'] - self.password = phabricator.ARCRC['irc']['password'] - self.channel = phabricator.ARCRC['irc']['channel'] + self.host = lugito.config.CONFIG['connectors']['irc']['host'] + self.port = int(lugito.config.CONFIG['connectors']['irc']['port']) + self.username = lugito.config.CONFIG['connectors']['irc']['username'] + self.password = lugito.config.CONFIG['connectors']['irc']['password'] + self.channel = lugito.config.CONFIG['connectors']['irc']['channel'] # Phabricator info - self.phab = phabricator.Phabricator() - self.phab_host = phabricator.ARCRC['config']['default'].replace( - 'api/', '') + self.phab = phabricator.Phabricator( + host=lugito.config.CONFIG['phabricator']['host'], + token=lugito.config.CONFIG['phabricator']['token'],) + self.phab_host = self.phab.host.replace('api/', '') self.logger = logging.getLogger('lugito.connector.IRCConnector') @@ -50,14 +52,17 @@ class IRCConnector(object): self.logger.addHandler(ch) self.logger.setLevel(log_level) + self.sleep_delay = sleep_delay - def _send_raw(self, message): + + def _send_raw(self, message): # pragma: no cover """Low level send""" self.conn.send(message.encode('utf-8')) - def _socket_conn(self): + def _setup_connection(self): + """Setup connection""" self.conn = ssl.wrap_socket( socket.socket(socket.AF_INET, socket.SOCK_STREAM)) self.conn.connect((self.host, self.port)) @@ -66,11 +71,11 @@ class IRCConnector(object): def connect(self): """Connect""" - self._socket_conn() + self.logger.info("Connecting to IRC.") + self._setup_connection() setup = False usersuffix = 0 - self.logger.info("Connecting to IRC.") while not setup: response = self.conn.recv(512).decode("utf-8") @@ -84,11 +89,11 @@ class IRCConnector(object): self.username, self.password)) if "You are now identified" in response: - sleep(5) + sleep(self.sleep_delay) self._send_raw("JOIN {}\r\n".format(self.channel)) if "477" in response: - sleep(5) + sleep(self.sleep_delay) self._send_raw("JOIN {}\r\n".format(self.channel)) if "433" in response: @@ -106,20 +111,32 @@ class IRCConnector(object): self.logger.info("Successfully connected to the IRC server.") - def send_notice(self, message): + def send_notice(self, message): # pragma: no cover self._send_raw("NOTICE {} :{}\r\n".format(self.channel, message)) - def send(self, objectstr, who, body, link): + def send(self, *args, **kwargs): """Send a formatted message""" + if len(args) == 4: + objectstr, who, body, link = args + + elif len(kwargs) == 4: + objectstr = kwargs['objectstr'] + who = kwargs['who'] + body = kwargs['body'] + link = kwargs['link'] + + # else + # raise exception + # e.g. [T31: Better IRC integration] message = "\x033[\x03\x0313" + objectstr + "\x03\x033]\x03 " # e.g. tsimonq2 (Simon Quigley) - message = message + "\x0315" + who + "\x03 " + message += "\x0315" + who + "\x03 " # e.g. commented on the task: - message = message + body + ": " + message += body + ": " # e.g. https://phab.lubuntu.me/T40#779 - message = message + "\x032" + link + "\x03" + message += "\x032" + link + "\x03" # Make sure we can debug this if it goes haywire self.logger.debug(message) # Sleep for a fifth of a second, so when we have a bunch of messages we have a buffer @@ -127,7 +144,7 @@ class IRCConnector(object): # Aaaaand, send it off! self.send_notice(message) - def gettaskinfo(self, task): + def get_task_info(self, task): sendmessage = "" @@ -170,7 +187,7 @@ class IRCConnector(object): if color is not None: sendmessage += ", " - sendmessage += taskinfo["statusName"] + "\x03\x033]\x03 " + sendmessage += taskinfo["statusName"] + "\x03\x033]\x03 " # Put the title in there as well. sendmessage += taskinfo["title"].strip() + ": " @@ -189,8 +206,14 @@ class IRCConnector(object): # If someone wrote something like "Tblah", obviously that's not right. except ValueError: - self.send_notice("\x034Error: " + task.strip() + "is an invalid task reference.\x03") - return None + + if anchor is not None: + link = '{}#{}'.format(task.strip(), anchor) + else: + link = task.strip() + + self.send_notice("\x034Error: " + link +\ + " is an invalid task reference.\x03") def bot(self, message, msgtype): @@ -200,7 +223,7 @@ class IRCConnector(object): for item in message.split(): if item.startswith("T") or item.startwith("D"): - self.gettaskinfo(item.strip()) + self.get_task_info(item.strip()) elif msgtype == "link": @@ -208,7 +231,7 @@ class IRCConnector(object): if (item.split()[0].strip().startswith("T")) or \ (item.split()[0].strip().startswith("D")): - self.gettaskinfo(item.split()[0].strip()) + self.get_task_info(item.split()[0].strip()) else: self.sendnotice("\x034Error: unknown command.\x03") diff --git a/lugito/connectors/launchpad.py b/lugito/connectors/launchpad.py index fc1c4ff..b3465ac 100644 --- a/lugito/connectors/launchpad.py +++ b/lugito/connectors/launchpad.py @@ -11,29 +11,53 @@ Define a launchpad connector class """ # Imports +import re import logging import phabricator -from launchpadlib.launchpad import Launchpad +import lugito +from string import Template +from launchpadlib.launchpad import Launchpad as lp -class LPConnector(object): +BUG_MESSAGE = Template( + "This bug has been marked as fixed in the Git repository: $link\n" + "The commit message is the following: $commit_message\n\n" + "(Note: I am only a bot. If this message was received in error, " + "please contact my owners on the Lubuntu Team.)") + +RE_COMMIT_MSG = re.compile(r"lp:\s+\#\d+(?:,\s*\#\d+)*") + + +class launchpad(object): def __init__(self, log_level=logging.DEBUG): # Launchpad info - # Read the configuration out of the .arcconfig file - self.application = phabricator.ARCRC['launchpad']['application'] - self.staging = phabricator.ARCRC['launchpad']['staging'] - self.version = phabricator.ARCRC['launchpad']['version'] + # Read the configuration out of the .lugitorc file + self.application = lugito.config.CONFIG['connectors']\ + ['launchpad']['application'] + self.staging = lugito.config.CONFIG['connectors']\ + ['launchpad']['staging'] + self.version = lugito.config.CONFIG['connectors']\ + ['launchpad']['version'] self.supported_vers =\ - phabricator.ARCRC['launchpad']['supported_versions'] + lugito.config.CONFIG['connectors']\ + ['launchpad']['supported_versions'] + self.package_names =\ + lugito.config.CONFIG['connectors']\ + ['launchpad']['package_names'] + # Phabricator info - self.phab = phabricator.Phabricator() - self.phab_host = phabricator.ARCRC['config']['default'].replace( + self.phab = phabricator.Phabricator( + host=lugito.config.CONFIG['phabricator']['host'], + token=lugito.config.CONFIG['phabricator']['token'], + ) + + self.phab_host = lugito.config.CONFIG['phabricator']['host'].replace( 'api/', '') - self.logger = logging.getLogger('lugito.connector.LPConnector') + self.logger = logging.getLogger('lugito.connector.launchpad') # Add log level ch = logging.StreamHandler() @@ -50,17 +74,74 @@ class LPConnector(object): """Connect""" self.logger.info("Connecting to Launchpad") + self.lp = lp.login_with( + self.application, + self.staging, + self.version) + def get_package_name(self, name): + """Need to check""" - def send(self, objectstr, who, body, link): - pass + if name in self.package_names: + return self.package_names[name] + self.logger.debug('{} is an unsupported repository'.format( + name)) - def listen(self): - pass + return None + + def get_bugs_list(self, link): + """Get bugs list using a link""" + + regex_search = RE_COMMIT_MSG.search(link.lower()) + if not regex_search: + self.logger.debug('{} not a commit message'.format(link)) + return [] + + return regex_search.group(0).strip("lp: ").replace("#", "").split(", ") -if __name__ == "__main__": + def send(self, *args, **kwargs): + """Send the commit message""" - obj = LPConnector() + if len(args) == 2: + package_name, commit_msg = args + elif len(kwargs) == 2: + commit_msg = kwargs['commit_msg'] + package_name = kwargs['package_name'] + + # else + # raise exception + + package_name = self.get_package_name(package_name) + bug_list = self.get_bugs_list(commit_msg) + + if package_name and bug_list: + + for bug in bug_list: + goodtask = None + bug = self.lp.load("/bugs/" + str(bug).strip()) + + for task in bug.bug_tasks: + for rel in self.supported_vers: + if package_name + " (Ubuntu " + rel + ")" in task.bug_target_display_name: + goodtask = task + break + + if not goodtask: + if package_name + " (Ubuntu)" in task.bug_target_display_name: + goodtask = task + + if goodtask: + message = BUG_MESSAGE.substitute( + link=self.phab_host + package_name, + commit_message=commit_msg, + ) + bug.newMessage(content=message) + goodtask.status = "Fix Committed" + goodtask.lp_save() + + + def listen(self): + pass diff --git a/lugito/lugito.py b/lugito/lugito.py index b0c0a12..92412e8 100644 --- a/lugito/lugito.py +++ b/lugito/lugito.py @@ -15,6 +15,7 @@ import hmac import http import logging import phabricator +import lugito from hashlib import sha256 PHAB_WEBHOOK_SIG = "X-Phabricator-Webhook-Signature" @@ -31,9 +32,12 @@ class Lugito(object): Initialise """ - self.phab = phabricator.Phabricator() - self.HMAC = phabricator.ARCRC['HMAC'] - self.host = phabricator.ARCRC['config']['default'] + self.phab = phabricator.Phabricator( + host=lugito.config.CONFIG['phabricator']['host'], + token=lugito.config.CONFIG['phabricator']['token'], + ) + self.HMAC = lugito.config.CONFIG['phabricator']['hooks'] + self.host = lugito.config.CONFIG['phabricator']['host'] self.logger = logging.getLogger('lugito.lugito') @@ -51,15 +55,7 @@ class Lugito(object): self.HMAC[key] = bytes(u'%s' % val, 'utf-8') - def _transaction_search(self): - self.transaction = self.phab.transaction.search(objectIdentifier= - self.request_data["object"]["phid"])["data"] - - def _request_data(self, request): - self.request_data = json.loads(request.data) - - - def validate_HMAC(self, hmac_key, request): + def validate_request(self, hmac_key, request): """ Check a request originated from Phabricator. This method must be called first to validate a request is from Phabricator before any other method @@ -86,8 +82,13 @@ class Lugito(object): # check if from phabricator if hash_.hexdigest() == request.headers[PHAB_WEBHOOK_SIG]: - self._request_data(request) - self._transaction_search() + + # Store the request and transaction + self.request_data = json.loads(request.data) + self.transaction = self.phab.transaction.search(objectIdentifier= + self.request_data["object"]["phid"])["data"] + + self.logger.info('received phid: %s' %\ self.request_data["object"]["phid"]) return True diff --git a/lugito/webhooks.py b/lugito/webhooks.py index d34d232..c614a20 100644 --- a/lugito/webhooks.py +++ b/lugito/webhooks.py @@ -17,7 +17,7 @@ import logging import threading from flask import Flask, request from lugito import Lugito -from lugito.connectors.irc import IRCConnector +from lugito.connectors import irc, launchpad # Constants GLOBAL_LOG_LEVEL = logging.DEBUG @@ -26,7 +26,9 @@ GLOBAL_LOG_LEVEL = logging.DEBUG lugito = Lugito(GLOBAL_LOG_LEVEL) WEBSITE = lugito.host.replace('/api/', '') -irc_con = IRCConnector() +# Connectors +irc_con = irc() +launchpad_con = launchpad() # Logging logger = logging.getLogger('lugito.webhooks') @@ -45,11 +47,41 @@ logger.setLevel(GLOBAL_LOG_LEVEL) app = Flask('lugito') +@app.route("/commithook", methods=["POST"]) +def commithook(): + """Commit hook""" + + if lugito.validate_request('commithook', request): + + author = lugito.get_author_fullname() + + # Without the author we can't continue + if author is None: + return 'Ok' + + object_type = lugito.request_data["object"]["type"] + + if object_type == "CMIT": + logger.debug("Object is a commit.") + + commit_msg = lugito.get_object_string("fullName").replace( + lugito.get_object_string("name") + ": ", "") + pkg_name = lugito.get_object_string("name") + + + launchpad_con.send(pkg_name, commit_msg) + + + return 'Ok' + + + + @app.route("/irc", methods=["POST"]) def _main(): """Main route""" - if lugito.validate_HMAC('irc', request): + if lugito.validate_request('irc', request): author = lugito.get_author_fullname() @@ -136,6 +168,7 @@ def _main(): def run(): irc_con.connect() + launchpad_con.connect() t = threading.Thread(target=irc_con.listen) t.daemon = True t.start() diff --git a/pytest.ini b/pytest.ini index 4b3ee53..d4895e6 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,5 @@ [pytest] +timeout=100 python_files = tests.py test_*.py *_tests.py pytest_plugins = "pytest_cov", "pep8" addopts = --doctest-modules --cov-config=.coveragerc --cov=lugito --cov-report=term-missing diff --git a/requirements.txt b/requirements.txt index 567192f..299296c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,6 @@ keyring==16.0.2 launchpadlib==1.10.6 lazr.restfulclient==0.14.0 lazr.uri==1.0.3 --e git+ssh://git@phab.lubuntu.me:2222/source/lugito.git@165c866a5d5a184b0b36552334fa11aba95b5fb8#egg=lugito MarkupSafe==1.1.0 more-itertools==4.3.0 oauthlib==2.1.0 @@ -31,5 +30,7 @@ pytest-cov==2.6.0 SecretStorage==3.1.0 six==1.11.0 testresources==2.0.1 +versioneer==0.18 wadllib==1.3.3 Werkzeug==0.14.1 +twine==1.12.1 diff --git a/setup.py b/setup.py index 46499f7..a094aa0 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """The setup script.""" -# import versioneer +import versioneer from setuptools import setup, find_packages with open('README.rst') as readme_file: @@ -29,8 +29,8 @@ test_requirements = [ ] setup( - author="", - author_email='', + author="Ben Johnston (docEbrown)", + author_email='bjohnston@neomailbox.net', classifiers=[ 'Development Status :: 2 - Pre-Alpha', 'Intended Audience :: Developers', @@ -42,11 +42,11 @@ setup( 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], - description="Python Boilerplate contains all the boilerplate "\ - "you need to create a Python package.", + description="Python package to connect services such as irc and launchpad"\ + " to Phabricator and provide updates", entry_points={ 'console_scripts': [ - 'lugito=lugito.cli:run', + 'lugito=lugito.webhooks:run', ], }, install_requires=requirements, @@ -60,8 +60,7 @@ setup( test_suite='tests', tests_require=test_requirements, url='', - version='0.1.0', zip_safe=False, -# version=versioneer.get_version(), -# cmdclass=versioneer.get_cmdclass(), + version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), ) diff --git a/tests/.arcconfig b/tests/.arcconfig index 567dd51..adf61a1 100644 --- a/tests/.arcconfig +++ b/tests/.arcconfig @@ -4,24 +4,4 @@ "token": "api-nojs2ip33hmp4zn6u6cf72w7d6yh" } }, - "config": { - "default": "http://127.0.0.1:9091/api/" - }, - "HMAC": { - "diffhook": "vglzi6t4gsumnilv27r27no7rs3vgs75", - "commithook": "znkyfflbcia5gviqx5ybad7s6uyfywxi" - }, - "irc": { - "host": "irc.freenode.net", - "port": "6697", - "username": "someusername", - "password": "somepassword", - "channel": "#somechannel" - }, - "launchpad": { - "application": "lugito", - "staging": "production", - "version": "devel", - "supported_versions": ["Cosmic", "Bionic", "Xenial", "Trusty"] - } } diff --git a/tests/.lugitorc b/tests/.lugitorc new file mode 100644 index 0000000..0443782 --- /dev/null +++ b/tests/.lugitorc @@ -0,0 +1,32 @@ +[phabricator] +host = http://127.0.0.1:9091/api/ +token = api-nojs2ip33hmp4zn6u6cf72w7d6yh + +[phabricator.hooks] +diffhook = vglzi6t4gsumnilv27r27no7rs3vgs75 +commithook = znkyfflbcia5gviqx5ybad7s6uyfywxi + +[connector.irc] +host = irc.freenode.net +port = 6697 +username = someusername +password = somepassword +channel = #somechannel + +[connector.launchpad] +application = lugito +staging = production +version = devel +supported_versions = + Cosmic + Bionic + Xenial + Trusty + +[connector.launchpad.package_names] +rDEFAULTSETTINGS = lubuntu-default-settings +rART = lubuntu-artwork +rCALASETTINGS = calamares-settings-ubuntu +rQTERMINALPACKAGING = qterminal +rLXQTCONFIGPACKAGING = lxqt-config +rNMTRAYPACKAGING = nm-tray diff --git a/tests/.lugitorc_no_host b/tests/.lugitorc_no_host new file mode 100644 index 0000000..683068b --- /dev/null +++ b/tests/.lugitorc_no_host @@ -0,0 +1,23 @@ +[phabricator] +token = api-nojs2ip33hmp4zn6u6cf72w7d6yh + +[phabricator.hooks] +diffhook = vglzi6t4gsumnilv27r27no7rs3vgs75 +commithook = znkyfflbcia5gviqx5ybad7s6uyfywxi + +[connector.irc] +host = irc.freenode.net +port = 6697 +username = someusername +password = somepassword +channel = #somechannel + +[connector.launchpad] +application = lugito +staging = production +version = devel +supported_versions = + Cosmic + Bionic + Xenial + Trusty diff --git a/tests/.lugitorc_no_phab b/tests/.lugitorc_no_phab new file mode 100644 index 0000000..f15cccb --- /dev/null +++ b/tests/.lugitorc_no_phab @@ -0,0 +1,20 @@ +[phabricator.hooks] +diffhook = vglzi6t4gsumnilv27r27no7rs3vgs75 +commithook = znkyfflbcia5gviqx5ybad7s6uyfywxi + +[connector.irc] +host = irc.freenode.net +port = 6697 +username = someusername +password = somepassword +channel = #somechannel + +[connector.launchpad] +application = lugito +staging = production +version = devel +supported_versions = + Cosmic + Bionic + Xenial + Trusty diff --git a/tests/.lugitorc_no_token b/tests/.lugitorc_no_token new file mode 100644 index 0000000..b91d99e --- /dev/null +++ b/tests/.lugitorc_no_token @@ -0,0 +1,23 @@ +[phabricator] +host = http://127.0.0.1/api/ + +[phabricator.hooks] +diffhook = vglzi6t4gsumnilv27r27no7rs3vgs75 +commithook = znkyfflbcia5gviqx5ybad7s6uyfywxi + +[connector.irc] +host = irc.freenode.net +port = 6697 +username = someusername +password = somepassword +channel = #somechannel + +[connector.launchpad] +application = lugito +staging = production +version = devel +supported_versions = + Cosmic + Bionic + Xenial + Trusty diff --git a/tests/lugito.ini b/tests/lugito.ini new file mode 100644 index 0000000..b040d64 --- /dev/null +++ b/tests/lugito.ini @@ -0,0 +1 @@ +[lugito] diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..c6df178 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,114 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# S.D.G + +"""Test config values + + +:author: Ben Johnston + +""" + +# Imports +import os +import pytest +import lugito.config + +TEST_FILE = os.path.join( + os.path.dirname(__file__), + '.lugitorc') + + +TEST_FILE_NO_PHAB = os.path.join( + os.path.dirname(__file__), + '.lugitorc_no_phab') + + +TEST_FILE_NO_HOST = os.path.join( + os.path.dirname(__file__), + '.lugitorc_no_host') + + +TEST_FILE_NO_TOKEN = os.path.join( + os.path.dirname(__file__), + '.lugitorc_no_token') + + +def test_loading_config_hooks(): + """Test loading config""" + + lugito.config.update_config(TEST_FILE) + CONFIG = lugito.config.CONFIG + + assert(CONFIG['phabricator']['host'] == 'http://127.0.0.1:9091/api/') + assert(CONFIG['phabricator']['token'] == 'api-nojs2ip33hmp4zn6u6cf72w7d6yh') + + # Hooks + assert(CONFIG['phabricator']['hooks']['diffhook'] ==\ + 'vglzi6t4gsumnilv27r27no7rs3vgs75') + assert(CONFIG['phabricator']['hooks']['commithook'] ==\ + 'znkyfflbcia5gviqx5ybad7s6uyfywxi') + + +def test_loading_config_connectors(): + """Test loading config connectors""" + + + lugito.config.update_config(TEST_FILE) + CONFIG = lugito.config.CONFIG + + # Connectors + assert(CONFIG['connectors']['irc'] == { + 'host': 'irc.freenode.net', + 'port': '6697', + 'username': 'someusername', + 'password':'somepassword', + 'channel': '#somechannel', + }) + + if not (CONFIG['connectors']['launchpad'] == { + 'application': 'lugito', + 'staging': 'production', + 'version': 'devel', + 'supported_versions': ['Cosmic', 'Bionic', 'Xenial', 'Trusty'], + 'package_names': { + 'rDEFAULTSETTINGS': 'lubuntu-default-settings', + 'rART': 'lubuntu-artwork', + 'rCALASETTINGS': 'calamares-settings-ubuntu', + 'rQTERMINALPACKAGING': 'qterminal', + 'rLXQTCONFIGPACKAGING': 'lxqt-config', + 'rNMTRAYPACKAGING': 'nm-tray', + }, + }): + import pdb;pdb.set_trace() + + + + +def test_load_config_no_phab(): + """Test loading config to load phabricator""" + + with pytest.raises(ValueError) as err: + + lugito.config.update_config(TEST_FILE_NO_PHAB) + + assert('phabricator section missing from config file' in str(err)) + + +def test_load_config_no_host(): + """Test loading config to load phabricator""" + + with pytest.raises(ValueError) as err: + lugito.config.update_config(TEST_FILE_NO_HOST) + + assert('host value missing from phabricator section config file' in str(err)) + + +def test_load_config_no_token(): + """Test loading config to load phabricator""" + + with pytest.raises(ValueError) as err: + + lugito.config.update_config(TEST_FILE_NO_TOKEN) + + assert('host value missing from phabricator conffig file' in str(err)) diff --git a/tests/test_ircconnector.py b/tests/test_ircconnector.py index 726812b..b8db255 100644 --- a/tests/test_ircconnector.py +++ b/tests/test_ircconnector.py @@ -10,25 +10,49 @@ Test IRC connector import os import json import phabricator -from lugito import IRCConnector -from unittest.mock import MagicMock +import lugito +import pytest +from lugito.connectors import irc +# docEbrown - 20181120 +# There is a bug in inspect.unwrap preventing the import of call directly +import unittest.mock +from unittest.mock import MagicMock, patch # Setup ############################################################### -TEST_DIR = os.path.dirname(__file__) +lugito.config.CONFIG = { + 'phabricator': { + 'host': 'http://127.0.0.1:9091/api/', + 'token': 'api-nojs2ip33hmp4zn6u6cf72w7d6yh', + 'hooks': { + 'diffhook': 'vglzi6t4gsumnilv27r27no7rs3vgs75', + 'commithook': 'znkyfflbcia5gviqx5ybad7s6uyfywxi', + }, + }, + 'connectors': { + 'irc': { + 'host': 'irc.freenode.net', + 'port': '6697', + 'username': 'someusername', + 'password': 'somepassword', + 'channel': '#somechannel', + }, + 'launchpad': { + 'application': 'lugito', + 'staging': 'production', + 'version': 'devel', + 'supported_versions': ['Cosmic', 'Bionic', 'Xenial', 'Trusty'], + }, + }, +} -# Force phabricator to use the ./tests/.arcconfig file -TEST_CONFIG = os.path.join(TEST_DIR, '.arcconfig') - -with open(TEST_CONFIG, 'r') as f: - phabricator.ARCRC = json.load(f) # Tests ############################################################### def test_init(): """Test initialise irc connector""" - obj = IRCConnector() + obj = irc() assert('irc.freenode.net' == obj.host) assert(6697 == obj.port) @@ -38,12 +62,179 @@ def test_init(): assert('http://127.0.0.1:9091/' == obj.phab_host) -def test_connect(): +@patch('phabricator.Phabricator') +def test_connect(phab_mock): """Test initial connection""" - obj = IRCConnector() + obj = irc() + + assert(phab_mock.is_called()) + + +@patch('phabricator.Phabricator') +def test_send(phab_mock): + """Test sending a message""" + + obj = irc() + obj.send_notice = MagicMock() + + objectstr = "objectstr" + who = "who" + body = "body" + link = "link" + + obj.send(objectstr, who, body, link) + + obj.send_notice.assert_called_with( + '\x033[\x03\x0313objectstr\x03\x033]\x03 \x0315who\x03 body: \x032link\x03') + + +@patch('phabricator.Phabricator') +def test_send_kwargs(phab_mock): + """Test sending a message - kwargs""" + + obj = irc() + obj.send_notice = MagicMock() + + objectstr = "objectstr" + who = "who" + body = "body" + link = "link" + + obj.send(objectstr=objectstr, who=who, body=body, link=link) + + obj.send_notice.assert_called_with( + '\x033[\x03\x0313objectstr\x03\x033]\x03 \x0315who\x03 body: \x032link\x03') + + +def test_connect(): + """Test connect""" + + obj = irc(sleep_delay=0) + + obj._send_raw = MagicMock() + obj._setup_connection = MagicMock() + obj.conn = MagicMock() + + obj.conn.recv.side_effect = [ + b'No Ident response', + b'You are now identified', + b'477', + b'433', + b'PING: something', + b'366', + ] + + obj.connect() + + # No Ident response results + assert(unittest.mock.call('NICK someusername1\r\n') in\ + obj._send_raw.call_args_list) + assert(unittest.mock.call('USER someusername * * :someusername\r\n') in\ + obj._send_raw.call_args_list) + assert(unittest.mock.call('PRIVMSG nickserv :identify someusername'\ + ' somepassword\r\n') in obj._send_raw.call_args_list) + + # Now identified / 477 + assert(unittest.mock.call('JOIN #somechannel\r\n') in\ + obj._send_raw.call_args_list) + + # 433 + assert(unittest.mock.call('NICK someusername1\r\n') in\ + obj._send_raw.call_args_list) + assert(unittest.mock.call('USER someusername1 * * :someusername1\r\n') in\ + obj._send_raw.call_args_list) + + # Ping + assert(unittest.mock.call('PONG : something\r\n') in\ + obj._send_raw.call_args_list) + + +# docEbrown - 20181120 +# Address including anchors in reference +# https://phab.lubuntu.me/T88#3230 +def test_get_task_info_with_anchor(): + """Test getting task info with anchor""" + + obj = irc() + + obj.send_notice = MagicMock() + obj.phab = MagicMock() + obj.phab.maniphest.info = MagicMock( + return_value={ + 'priorityColor': 'pink', + 'statusName': 'Open', + 'title': 'Fix shortcuts related to Super key', + 'uri': 'https://phab.lubuntu.me/T154' + } + ) + + link_with_anchor = 'https://phab.lubuntu.me/T154#3228' + obj.get_task_info(link_with_anchor) + + assert(unittest.mock.call(task_id=154) in\ + obj.phab.maniphest.info.call_args_list) + assert(obj.send_notice.call_args == \ + unittest.mock.call('\x033[\x03\x035Unbreak Now!, Open\x03\x033]\x03 ' + 'Fix shortcuts related to Super key: '\ + '\x032https://phab.lubuntu.me/T154#3228\x03')) + + +def test_get_diff_info_with_anchor(): + """Test getting diff info with anchor""" + + obj = irc() + + obj.send_notice = MagicMock() + obj.phab = MagicMock() + obj.phab.differential.query = MagicMock( + return_value=[{ + 'statusName': 'Closed', + 'title': 'Some diff title', + 'uri': 'https://phab.lubuntu.me/D24' + },] + ) + + link_with_anchor = 'https://phab.lubuntu.me/D24#123' + obj.get_task_info(link_with_anchor) + + assert(unittest.mock.call(ids=[24]) in\ + obj.phab.differential.query.call_args_list) + assert(obj.send_notice.call_args == \ + unittest.mock.call('\x033[\x03Closed\x03\x033]\x03 ' + 'Some diff title: '\ + '\x032https://phab.lubuntu.me/D24#123\x03')) + +def test_get_task_info_with_error_anchor(): + """Test getting task info with anchor""" + + obj = irc() + + obj.send_notice = MagicMock() + obj.phab = MagicMock() + obj.phab.maniphest.info = MagicMock(side_effect=ValueError('')) + + link_with_anchor = 'https://phab.lubuntu.me/T154#3228' + + obj.get_task_info(link_with_anchor) + + assert(obj.send_notice.call_args == + unittest.mock.call('\x034Error: https://phab.lubuntu.me/T154#3228'\ + ' is an invalid task reference.\x03')) + +def test_get_task_info_with_error_no_anchor(): + """Test getting task info with no anchor""" + + obj = irc() + + obj.send_notice = MagicMock() + obj.phab = MagicMock() + obj.phab.maniphest.info = MagicMock(side_effect=ValueError('')) - obj._socket_conn = MagicMock() + link_with_anchor = 'https://phab.lubuntu.me/T154' -# obj.conn.recv = MagicMock(side_effect=[ + obj.get_task_info(link_with_anchor) + assert(obj.send_notice.call_args == + unittest.mock.call('\x034Error: https://phab.lubuntu.me/T154'\ + ' is an invalid task reference.\x03')) diff --git a/tests/test_launchpad.py b/tests/test_launchpad.py index a4be27b..5b271d0 100644 --- a/tests/test_launchpad.py +++ b/tests/test_launchpad.py @@ -9,19 +9,90 @@ Test launchpad connector # Imports import json import phabricator -from lugito.connectors.launchpad import LPConnector +import lugito +from lugito.connectors import launchpad # Setup ############################################################### -TEST_DIR = os.path.dirname(__file__) +lugito.config.CONFIG = { + 'phabricator': { + 'host': 'http://127.0.0.1:9091/api/', + 'token': 'api-nojs2ip33hmp4zn6u6cf72w7d6yh', + 'hooks': { + 'diffhook': 'vglzi6t4gsumnilv27r27no7rs3vgs75', + 'commithook': 'znkyfflbcia5gviqx5ybad7s6uyfywxi', + }, + }, + 'connectors': { + 'irc': { + 'host': 'irc.freenode.net', + 'port': '6697', + 'username': 'someusername', + 'password': 'somepassword', + 'channel': '#somechannel', + }, + 'launchpad': { + 'application': 'lugito', + 'staging': 'production', + 'version': 'devel', + 'supported_versions': ['Cosmic', 'Bionic', 'Xenial', 'Trusty'], + 'package_names': { + 'rDEFAULTSETTINGS': 'lubuntu-default-settings', + 'rART': 'lubuntu-artwork', + 'rCALASETTINGS': 'calamares-settings-ubuntu', + 'rQTERMINALPACKAGING': 'qterminal', + 'rLXQTCONFIGPACKAGING': 'lxqt-config', + 'rNMTRAYPACKAGING': 'nm-tray', + }, + }, + }, +} -# Force phabricator to use the ./tests/.arcconfig file -TEST_CONFIG = os.path.join(TEST_DIR, '.arcconfig') - -with open(TEST_CONFIG, 'r') as f: - phabricator.ARCRC = json.load(f) # Tests ############################################################### -def test_init() +def test_init(): + """Test initialising LPConnector""" + + obj = launchpad() + + assert(obj.application == "lugito") + assert(obj.staging == "production") + assert(obj.version == "devel") + assert(obj.supported_vers == ["Cosmic", "Bionic", "Xenial", "Trusty"]) + assert(obj.package_names == { + 'rDEFAULTSETTINGS': 'lubuntu-default-settings', + 'rART': 'lubuntu-artwork', + 'rCALASETTINGS': 'calamares-settings-ubuntu', + 'rQTERMINALPACKAGING': 'qterminal', + 'rLXQTCONFIGPACKAGING': 'lxqt-config', + 'rNMTRAYPACKAGING': 'nm-tray', + }) + + +def test_get_package_name(): + """Test get package name""" + + obj = launchpad() + + assert(obj.get_package_name('rART') == 'lubuntu-artwork') + assert(obj.get_package_name('rT') is None) + + +def test_get_package_name(): + """Test getting package name""" + + obj = launchpad() + + assert(obj.get_package_name('rNMTRAYPACKAGING') == 'nm-tray') + assert(obj.get_package_name('rNMTRKAGING') is None) + + +def test_get_bugs_list(): + """Test getting buglist""" + + obj = launchpad() + + assert(obj.get_bugs_list("lp: #1234") == ['1234']) + assert(obj.get_bugs_list("#1234") == []) diff --git a/tests/test_lugito.py b/tests/test_lugito.py index 48f48f1..6d533a0 100644 --- a/tests/test_lugito.py +++ b/tests/test_lugito.py @@ -12,6 +12,7 @@ import pytest import phabricator import json import http +import lugito from lugito import Lugito from unittest.mock import MagicMock @@ -19,11 +20,33 @@ from unittest.mock import MagicMock TEST_DIR = os.path.dirname(__file__) -# Force phabricator to use the ./tests/.arcconfig file -TEST_CONFIG = os.path.join(TEST_DIR, '.arcconfig') +# Apply default values +lugito.config.CONFIG = { + 'phabricator': { + 'host': 'http://127.0.0.1:9091/api/', + 'token': 'api-nojs2ip33hmp4zn6u6cf72w7d6yh', + 'hooks': { + 'diffhook': 'vglzi6t4gsumnilv27r27no7rs3vgs75', + 'commithook': 'znkyfflbcia5gviqx5ybad7s6uyfywxi', + }, + }, + 'connectors': { + 'irc': { + 'host': 'irc.freenode.net', + 'port': '6697', + 'username': 'someusername', + 'password': 'somepassword', + 'channel': '#somechannel', + }, + 'launchpad': { + 'application': 'lugito', + 'staging': 'production', + 'version': 'devel', + 'supported_versions': ['Cosmic', 'Bionic', 'Xenial', 'Trusty'], + }, + }, +} -with open(TEST_CONFIG, 'r') as f: - phabricator.ARCRC = json.load(f) # Pre-prepared request FAKE_REQUEST = os.path.join(TEST_DIR, 'request.json') @@ -56,10 +79,12 @@ def test_init(): 'utf-8')) -def test_validate_HMAC(): +def test_validate_request(): """Test validating HMAC""" obj = Lugito() + obj.phab = MagicMock() + obj.phab.transaction.search = MagicMock() request_mock = MagicMock() @@ -71,7 +96,8 @@ def test_validate_HMAC(): "a8f636f03ed4464ddb398ea873ffab409d941f87396f28fa9d22bb58cfbedc9f" } - assert(obj.validate_HMAC('diffhook', request_mock)) + assert(obj.validate_request('diffhook', request_mock)) + assert(obj.phab.transaction.search.is_called()) def test_invalid_HMAC(): @@ -89,7 +115,7 @@ def test_invalid_HMAC(): "a8f6364464ddb398ea873ffab409d941f87396f28fa9d22bb58cfbedc9f" } - assert(not obj.validate_HMAC('diffhook', request_mock)) + assert(not obj.validate_request('diffhook', request_mock)) def test_author_fullname(): @@ -141,8 +167,6 @@ def test_get_object_type(): with open(FAKE_REQ_DATA, 'r') as f: obj.request_data = json.load(f) - obj._transaction_search() - assert(obj.get_object_type() == 'DREV') def test_is_new_object_false():