From f7d9b5684e503fc0b28695f5dc745f4b24308308 Mon Sep 17 00:00:00 2001 From: mprahl Date: Dec 13 2017 22:35:08 +0000 Subject: Port fedrepo-req and fedrepo-req-branch to fedpkg Signed-off-by: mprahl --- diff --git a/conf/bash-completion/fedpkg.bash b/conf/bash-completion/fedpkg.bash index 57edb5c..03fa399 100644 --- a/conf/bash-completion/fedpkg.bash +++ b/conf/bash-completion/fedpkg.bash @@ -33,13 +33,14 @@ _fedpkg() # global options local options="--help -v -q" - local options_value="--dist --release --user --path" + local options_value="--dist --release --user --path --user-config" local commands="build chain-build ci clean clog clone co commit compile \ container-build diff gimmespec giturl help gitbuildhash import install lint \ local mockbuild mock-config module-build module-build-cancel \ module-build-local module-build-info module-build-watch module-overview \ - new new-sources patch prep pull push retire scratch-build sources srpm \ - switch-branch tag unused-patches update upload verify-files verrel" + new new-sources patch prep pull push retire request-branch request-repo \ + scratch-build sources srpm switch-branch tag unused-patches update upload \ + verify-files verrel" # parse main options and get command @@ -189,6 +190,13 @@ _fedpkg() retire) after_more=true ;; + request-branch) + options="--no-git-branch --all-releases" + options_string="--sl" + request-repo) + options="--exception" + options_string="--description --monitor --upstreamurl --summary" + ;; scratch-build) options="--nowait --background" options_target="--target" diff --git a/conf/etc/rpkg/fedpkg-stage.conf b/conf/etc/rpkg/fedpkg-stage.conf index 91cdb02..5a82141 100644 --- a/conf/etc/rpkg/fedpkg-stage.conf +++ b/conf/etc/rpkg/fedpkg-stage.conf @@ -29,3 +29,13 @@ oidc_id_provider = https://id.stg.fedoraproject.org/openidc/ oidc_client_id = mbs-authorizer oidc_client_secret = notsecret oidc_scopes = openid,https://id.fedoraproject.org/scope/groups,https://mbs.fedoraproject.org/oidc/submit-build + +[fedpkg-stage.bugzilla] +# Use production Bugzilla for read-only data +url = https://bugzilla.redhat.com/ + +[fedpkg-stage.pagure] +url = https://stg.pagure.io/ + +[fedpkg-stage.pdc] +url = https://pdc.stg.fedoraproject.org/ diff --git a/conf/etc/rpkg/fedpkg.conf b/conf/etc/rpkg/fedpkg.conf index 993ba03..4e45ebc 100644 --- a/conf/etc/rpkg/fedpkg.conf +++ b/conf/etc/rpkg/fedpkg.conf @@ -34,3 +34,12 @@ oidc_id_provider = https://id.fedoraproject.org/openidc/ oidc_client_id = mbs-authorizer oidc_client_secret = notsecret oidc_scopes = openid,https://id.fedoraproject.org/scope/groups,https://mbs.fedoraproject.org/oidc/submit-build + +[fedpkg.bugzilla] +url = https://bugzilla.redhat.com/ + +[fedpkg.pagure] +url = https://pagure.io/ + +[fedpkg.pdc] +url = https://pdc.fedoraproject.org/ diff --git a/conf/zsh-completion/_fedpkg b/conf/zsh-completion/_fedpkg index d16d6c1..7e3ec28 100644 --- a/conf/zsh-completion/_fedpkg +++ b/conf/zsh-completion/_fedpkg @@ -302,6 +302,28 @@ _fedpkg-push () { '(-h --help)'{-h,--help}'[show help message and exit]' } +(( $+functions[_fedpkg-request-branch] )) || +_fedpkg-request-branch () { + _arguments -C \ + '(-h --help)'{-h,--help}'[show help message and exit]' \ + '(--sl)'{--sl}'[The service levels (SLs) tied to the branch]' \ + '--no-git-branch[Do not create the branch in git but still create it in PDC]:optional' \ + '--all-releases[Make a new branch request for every active Fedora release]:optional' \ + ':branch' +} + +(( $+functions[_fedpkg-request-repo] )) || +_fedpkg-request-repo () { + _arguments -C \ + '(-h --help)'{-h,--help}'[show help message and exit]' \ + '(-d --description)'{-d,--description}'[The description in dist-git]' \ + '(-m --monitor)'{-m,--monitor}'[The Koshei monitoring type for the repo]' \ + '(-u --upstreamurl)'{-u,--upstreamurl}'[The upstream URL of the project]' \ + '(-s --summary)'{-s,--summary}'[Override the package summary from the Bugzilla bug]' \ + '--exception[The package is an exception to the regular package review process]:optional' \ + ':bug' +} + (( $+functions[_fedpkg-scratch-build] )) || _fedpkg-scratch-build () { _arguments -C \ @@ -426,6 +448,8 @@ _fedpkg_commands () { prep:'local test rpmbuild prep' pull:'pull changes from remote repository and update working copy' push:'push changes to remote repository' + request-branch:'request a new dist-git branch' + request-repo:'request a new dist-git repository' scratch-build:'request scratch build' sources:'download source files' srpm:'create a source rpm' @@ -462,6 +486,7 @@ _fedpkg() { _arguments -C \ '(- :)'{-h,--help}'[show help message and exit]' \ '(-C --config)'{-C,--config}'[specify a config file to use]:config file:_files' \ + '--user-config[specify a user config file to use]:user config file:_files' \ '--dist[override the discovered distribution]:distribution' \ '--release[override the discovered release]:release' \ '--user[override the discovered user name]:user' \ diff --git a/fedpkg/__main__.py b/fedpkg/__main__.py index be0ea28..31c2ade 100644 --- a/fedpkg/__main__.py +++ b/fedpkg/__main__.py @@ -15,6 +15,7 @@ import os import sys import fedpkg +import fedpkg.utils import pyrpkg import pyrpkg.utils @@ -29,10 +30,15 @@ cli_name = os.path.basename(sys.argv[0]) def main(): + default_user_config_path = os.path.join( + os.path.expanduser('~'), '.config', 'rpkg', '%s.conf' % cli_name) # Setup an argparser and parse the known commands to get the config file parser = argparse.ArgumentParser(add_help=False) parser.add_argument('-C', '--config', help='Specify a config file to use', default='/etc/rpkg/%s.conf' % cli_name) + parser.add_argument( + '--user-config', help='Specify a user config file to use', + default=default_user_config_path) (args, other) = parser.parse_known_args() @@ -45,6 +51,7 @@ def main(): # Setup a configuration object and read config file data config = ConfigParser() config.read(args.config) + config.read(args.user_config) client = fedpkg.cli.fedpkgClient(config, name=cli_name) client.do_imports(site='fedpkg') diff --git a/fedpkg/bugzilla.py b/fedpkg/bugzilla.py new file mode 100644 index 0000000..274fc29 --- /dev/null +++ b/fedpkg/bugzilla.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# cli.py - a cli client class module for fedpkg +# +# Copyright (C) 2017 Red Hat Inc. +# Author(s): Matt Prahl +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for +# the full text of the license. + +# So that we import the bugzilla package and not fedpkg.bugzilla +from __future__ import absolute_import +from datetime import datetime + +import bugzilla +from pyrpkg import rpkgError + + +class BugzillaClient(object): + """A Bugzilla helper class""" + api_url = None + _client = None + + def __init__(self, url): + self.api_url = '{0}/xmlrpc.cgi'.format(url.rstrip('/')) + + @property + def client(self): + """ + Only connect to Bugzilla when the client property is first used. This + will make the unit tests less complicated and shorten the connections + to the Bugzilla server. + """ + if not self._client: + # use_creds is only available in python-bugzilla 2.0+ + try: + self._client = bugzilla.Bugzilla(self.api_url, use_creds=False) + except TypeError: + self._client = bugzilla.Bugzilla(self.api_url) + + return self._client + + def get_review_bug(self, bug_id, namespace, pkg): + """ + Gets a Bugzilla bug representing a Fedora package review and does as + much validation as it can without authenticating to Bugzilla. This + function was inspired by: + https://github.com/fedora-infra/pkgdb2/blob/master/pkgdb2/api/extras.py + https://pagure.io/pkgdb-cli/blob/master/f/pkgdb2client/utils.py + :param bug_id: string or integer of the Bugzilla bug ID + :param namespace: string of the dist-git namespace + :param pkg: string of the package name + """ + try: + bug = self.client.getbug(bug_id) + except Exception as error: + raise rpkgError( + 'The Bugzilla bug could not be verified. The following ' + 'error was encountered: {0}'.format(str(error))) + + # Do some basic validation on the bug + pagure_namespace_to_component = { + 'rpms': 'Package Review', + 'container': 'Container Review', + 'modules': 'Module Review', + 'test-modules': 'Module Review' + } + pagure_namespace_to_product = { + 'rpms': ['Fedora', 'Fedora EPEL'], + 'container': ['Fedora Container Images'], + 'modules': ['Fedora Modules'], + 'test-modules': ['Fedora'] + } + bz_proper_component = pagure_namespace_to_component.get(namespace) + bz_proper_products = pagure_namespace_to_product.get(namespace) + if bz_proper_component is None or bug.component != bz_proper_component: + raise rpkgError('The Bugzilla bug provided is not the proper type') + elif bug.product not in bz_proper_products: + raise rpkgError('The Bugzilla bug provided is not for "{0}"' + .format('" or "'.join(bz_proper_products))) + elif bug.assigned_to in ['', None, 'nobody@fedoraproject.org']: + raise rpkgError( + 'The Bugzilla bug provided is not assigned to anyone') + # Check if the review was approved + flag_set = False + for flag in bug.flags: + name, status = flag.get('name'), flag.get('status') + if name == 'fedora-review' and status == '+': + flag_set = True + update_dt = flag.get('modification_date') + if update_dt: + dt = datetime.strptime( + update_dt.value, '%Y%m%dT%H:%M:%S') + delta = datetime.utcnow().date() - dt.date() + if delta.days > 60: + raise rpkgError('The Bugzilla bug\'s review was ' + 'approved over 60 days ago') + break + if not flag_set: + raise rpkgError('The Bugzilla bug is not approved yet') + # Check the format of the Bugzilla bug title + tmp_summary = bug.summary.partition(':')[2] + if not tmp_summary: + raise rpkgError( + 'Invalid title for this Bugzilla bug (no ":" present)') + if ' - ' not in tmp_summary: + raise rpkgError( + 'Invalid title for this Bugzilla bug (no "-" present)') + pkg_in_bug = tmp_summary.split(' - ', 1)[0].strip() + if pkg != pkg_in_bug: + error = ('The package in the Bugzilla bug "{0}" doesn\'t match ' + 'the one provided "{1}"'.format(pkg_in_bug, pkg)) + raise rpkgError(error) + return bug diff --git a/fedpkg/cli.py b/fedpkg/cli.py index a2d0aac..0e8fb6b 100644 --- a/fedpkg/cli.py +++ b/fedpkg/cli.py @@ -10,16 +10,22 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. +from __future__ import print_function from pyrpkg.cli import cliClient import hashlib import os import re +import json import six import textwrap from six.moves.configparser import NoSectionError from six.moves.configparser import NoOptionError from pyrpkg import rpkgError +from fedpkg.bugzilla import BugzillaClient +from fedpkg.utils import ( + get_release_branches, sl_list_to_dict, verify_sls, new_pagure_issue, + get_pagure_token) class fedpkgClient(cliClient): @@ -31,6 +37,10 @@ class fedpkgClient(cliClient): def setup_argparser(self): super(fedpkgClient, self).setup_argparser() + # This line is added here so that it shows up with the "--help" option, + # but it isn't used for anything else + self.parser.add_argument( + '--user-config', help='Specify a user config file to use') opt_release = self.parser._option_string_actions['--release'] opt_release.help = 'Override the discovered release, e.g. f25, which has to match ' \ 'the remote branch name created in package repository. ' \ @@ -41,6 +51,8 @@ class fedpkgClient(cliClient): self.register_retire() self.register_update() + self.register_request_repo() + self.register_request_branch() # Target registry goes here def register_retire(self): @@ -65,6 +77,55 @@ class fedpkgClient(cliClient): ) update_parser.set_defaults(command=self.update) + def register_request_repo(self): + help_msg = 'Request a new dist-git repository' + request_repo_parser = self.subparsers.add_parser( + 'request-repo', help=help_msg, description=help_msg) + request_repo_parser.add_argument( + 'bug', nargs='?', type=int, + help='Bugzilla bug ID of the package review request') + request_repo_parser.add_argument( + '--description', '-d', help='The repo\'s description in dist-git') + monitoring_choices = [ + 'no-monitoring', 'monitoring', 'monitoring-with-scratch'] + request_repo_parser.add_argument( + '--monitor', '-m', help='The Koshei monitoring type for the repo', + choices=monitoring_choices, default=monitoring_choices[1]) + request_repo_parser.add_argument( + '--upstreamurl', '-u', + help='The upstream URL of the project') + request_repo_parser.add_argument( + '--summary', '-s', + help='Override the package\'s summary from the Bugzilla bug') + request_repo_parser.add_argument( + '--exception', action='store_true', + help='The package is an exception to the regular package review ' + 'process (specifically, it does not require a Bugzilla bug)') + request_repo_parser.set_defaults(command=self.request_repo) + + def register_request_branch(self): + help_msg = 'Request a new dist-git branch' + request_branch_parser = self.subparsers.add_parser( + 'request-branch', help=help_msg, description=help_msg) + request_branch_parser.add_argument( + 'branch', nargs='?', help='The branch to request') + request_branch_parser.add_argument( + '--sl', nargs='*', + help=('The service levels (SLs) tied to the branch. This must be ' + 'in the format of "sl_name:2020-12-01". This is only for ' + 'non-release branches. You may provide more than one by ' + 'separating each SL with a space.') + ) + request_branch_parser.add_argument( + '--no-git-branch', default=False, action='store_true', + help='Don\'t create the branch in git but still create it in PDC' + ) + request_branch_parser.add_argument( + '--all-releases', default=False, action='store_true', + help='Make a new branch request for every active Fedora release' + ) + request_branch_parser.set_defaults(command=self.request_branch) + # Target functions go here def retire(self): # Skip if package is already retired... @@ -191,3 +252,125 @@ suggest_reboot=False # Clean up os.unlink('bodhi.template') os.unlink('clog') + + def request_repo(self): + # bug is not a required parameter in the event the packager has an + # exception, in which case, they may use the --exception flag + if not self.args.bug and not self.args.exception: + raise rpkgError( + 'A Bugzilla bug is required on new repository requests') + repo_regex = r'^[a-zA-Z0-9_][a-zA-Z0-9-_.+]*$' + if not bool(re.match(repo_regex, self.cmd.module_name)): + raise rpkgError( + 'The repository name "{0}" is invalid. It must be at least ' + 'two characters long with only letters, numbers, hyphens, ' + 'underscores, plus signs, and/or periods. Please note that ' + 'the project cannot start with a period or a plus sign.' + .format(self.cmd.module_name)) + + summary_from_bug = '' + if self.args.bug: + bz_url = self.config.get('{0}.bugzilla'.format(self.name), 'url') + bz_client = BugzillaClient(bz_url) + bug = bz_client.get_review_bug( + self.args.bug, self.cmd.ns, self.cmd.module_name) + summary_from_bug = bug.summary.split(' - ', 1)[1].strip() + + ticket_body = { + 'action': 'new_repo', + 'branch': 'master', + 'bug_id': self.args.bug or '', + 'description': self.args.description or '', + 'exception': self.args.exception, + 'monitor': self.args.monitor, + 'namespace': self.cmd.ns, + 'repo': self.cmd.module_name, + 'summary': self.args.summary or summary_from_bug, + 'upstreamurl': self.args.upstreamurl or '' + } + + ticket_body = json.dumps(ticket_body, indent=True) + ticket_body = '```\n{0}\n```'.format(ticket_body) + ticket_title = 'New Repo for "{0}/{1}"'.format( + self.cmd.ns, self.cmd.module_name) + + pagure_url = self.config.get('{0}.pagure'.format(self.name), 'url') + pagure_token = get_pagure_token(self.config, self.name) + print(new_pagure_issue( + pagure_url, pagure_token, ticket_title, ticket_body)) + + def request_branch(self): + service_levels = self.args.sl + branch = None + + if self.args.all_releases: + if self.args.branch: + raise rpkgError('You cannot specify a branch with the ' + '"--all-releases" option') + elif service_levels: + raise rpkgError('You cannot specify service levels with the ' + '"--all-releases" option') + elif not self.args.branch: + try: + branch = self.cmd.repo.active_branch.name + except rpkgError: + raise rpkgError('You must specify a branch if you are not in ' + 'a git repository') + else: + branch = self.args.branch + + bodhi_url = self.config.get('{0}.bodhi'.format(self.name), 'url') + if branch: + if self.cmd.ns in ['modules', 'test-modules']: + branch_valid = bool(re.match(r'^[a-zA-Z0-9.\-_+]+$', branch)) + if not branch_valid: + raise rpkgError( + 'Only characters, numbers, periods, dashes, ' + 'underscores, and pluses are allowed in module branch ' + 'names') + release_branches = get_release_branches(bodhi_url) + if branch in release_branches: + if service_levels: + raise rpkgError( + 'You can\'t provide SLs for release branches') + else: + if re.match(r'^(f\d+|el\d+|epel\d+)$', branch): + raise rpkgError('{0} is not a current release branch' + .format(branch)) + elif not service_levels: + raise rpkgError( + 'You must provide SLs for non-release branches') + + # If service levels were provided, verify them + if service_levels: + pdc_url = self.config.get('{0}.pdc'.format(self.name), 'url') + sl_dict = sl_list_to_dict(service_levels) + verify_sls(pdc_url, sl_dict) + + pagure_url = self.config.get('{0}.pagure'.format(self.name), 'url') + pagure_token = get_pagure_token(self.config, self.name) + if self.args.all_releases: + release_branches = get_release_branches(bodhi_url) + branches = [b for b in release_branches + if re.match(r'^(f\d+)$', b)] + else: + branches = [branch] + + for b in sorted(list(branches), reverse=True): + ticket_body = { + 'action': 'new_branch', + 'branch': b, + 'namespace': self.cmd.ns, + 'repo': self.cmd.module_name, + 'create_git_branch': not self.args.no_git_branch + } + if service_levels: + ticket_body['sls'] = sl_dict + + ticket_body = json.dumps(ticket_body, indent=True) + ticket_body = '```\n{0}\n```'.format(ticket_body) + ticket_title = 'New Branch "{0}" for "{1}/{2}"'.format( + b, self.cmd.ns, self.cmd.module_name) + + print(new_pagure_issue( + pagure_url, pagure_token, ticket_title, ticket_body)) diff --git a/fedpkg/utils.py b/fedpkg/utils.py new file mode 100644 index 0000000..a4bffc8 --- /dev/null +++ b/fedpkg/utils.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# cli.py - a cli client class module for fedpkg +# +# Copyright (C) 2017 Red Hat Inc. +# Author(s): Matt Prahl +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for +# the full text of the license. + +import re +import json +from datetime import datetime + +from six.moves.urllib.parse import urlencode +from six.moves.configparser import NoSectionError, NoOptionError +import requests +from requests.exceptions import ConnectionError +from fedora.client.bodhi import Bodhi2Client +from pyrpkg import rpkgError + + +def get_sl_type(url, sl_name): + """ + Gets the service level (SL) type from PDC + :param url: a string of the URL to PDC + :param sl_name: a string of the SL name + :return: a dictionary representing the SL type or None + """ + api_url = '{0}/rest_api/v1/component-sla-types/'.format(url.rstrip('/')) + api_url_w_args = '{0}?{1}'.format(api_url, urlencode({'name': sl_name})) + try: + rv = requests.get(api_url_w_args, timeout=60) + except ConnectionError as error: + error_msg = ('The connection to PDC failed while trying to validate ' + 'the passed in service level. The error was: {0}' + .format(str(error))) + raise rpkgError(error_msg) + + if not rv.ok: + base_error_msg = ('The following error occurred while validating the ' + 'passed in service level in PDC: {0}') + raise rpkgError(base_error_msg.format(rv.text)) + + rv_json = rv.json() + if rv_json['count'] == 1: + return rv_json['results'][0] + else: + return None + + +def new_pagure_issue(url, token, title, body): + """ + Posts a new Pagure issue + :param url: a string of the URL to Pagure + :param token: a string of the Pagure API token that has rights to create + a ticket + :param title: a string of the issue's title + :param body: a string pf the issue's body + :return: a string of the URL to the created issue in the UI + """ + api_url = '{0}/api/0'.format(url.rstrip('/')) + new_issue_url = '{0}/releng/fedora-scm-requests/new_issue'.format(api_url) + + headers = { + 'Authorization': 'token {0}'.format(token), + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + payload = json.dumps({ + 'title': title, + 'issue_content': body + }) + try: + rv = requests.post( + new_issue_url, headers=headers, data=payload, timeout=60) + except ConnectionError as error: + error_msg = ('The connection to Pagure failed while trying to ' + 'create a new issue. The error was: {0}'.format( + str(error))) + raise rpkgError(error_msg) + + base_error_msg = ('The following error occurred while creating a new ' + 'issue in Pagure: {0}') + if not rv.ok: + # Lets see if the API returned an error message in JSON that we can + # show the user + try: + rv_error = rv.json().get('error') + except ValueError: + rv_error = rv.text + raise rpkgError(base_error_msg.format(rv_error)) + + return '{0}/releng/fedora-scm-requests/issue/{1}'.format( + url.rstrip('/'), rv.json()['issue']['id']) + + +def get_release_branches(bodhi_url): + """ + Get the active Fedora release branches from Bodhi + :param bodhi_url: a string of the URL to Bodhi + :return: a set containing the active Fedora release branches + """ + bodhi = Bodhi2Client(bodhi_url) + branches = set() + page = 1 + while True: + rv = bodhi.send_request('releases', auth=False, params={'page': page}) + for release in rv['releases']: + if release['state'] == 'current': + branches.add(release['branch']) + if page < rv['pages']: + page += 1 + else: + break + + return branches + + +def sl_list_to_dict(sls): + """ + Takes a list of SLs and returns them in a dictionary format. Any errors in + the SLs will be raised as an rpkgError. + :param sls: list of SLs in the format of sl_name:2017-12-25 + :return: dictionary in the format of {'sl_name': '2017-12-25'} + """ + sl_dict = {} + # Ensures the SL is in the format "security_fixes:2020-01-01" + sl_regex = re.compile(r'^(.+)(?:\:)(\d{4}-\d{2}-\d{2})$') + for sl in sls: + sl_match = re.match(sl_regex, sl) + if sl_match: + sl_name = sl_match.groups()[0] + sl_date = sl_match.groups()[1] + sl_dict[sl_name] = sl_date + else: + raise rpkgError( + 'The SL "{0}" is in an invalid format'.format(sl)) + + return sl_dict + + +def verify_sls(pdc_url, sl_dict): + """ + Verifies that the service levels are properly formatted and exist in PDC + :param pdc_url: a string of the URL to PDC + :param sl_dict: a dictionary with the SLs of the request + :return: None or ValidationError + """ + # Make sure the EOL date is in the format of 2020-12-01 + eol_date_regex = re.compile(r'\d{4}-\d{2}-\d{2}') + for sl, eol in sl_dict.items(): + if re.match(eol_date_regex, eol): + eol_date = datetime.strptime(eol, '%Y-%m-%d').date() + today = datetime.utcnow().date() + if eol_date < today: + raise rpkgError( + 'The SL "{0}" is already expired'.format(eol)) + elif eol_date.month not in [6, 12] or eol_date.day != 1: + raise rpkgError( + 'The SL "{0}" must expire on June 1st or December 1st' + .format(eol)) + else: + raise rpkgError( + 'The EOL date "{0}" is in an invalid format'.format(eol)) + + sl_obj = get_sl_type(pdc_url, sl) + if not sl_obj: + raise rpkgError('The SL "{0}" is not in PDC'.format(sl)) + + +def get_pagure_token(config, cli_name): + """ + Gets the Pagure token configured in the user's configuration file + :param config: ConfigParser object + :param cli_name: string of the CLI's name (e.g. fedpkg) + :return: string of the Pagure token + """ + conf_section = '{0}.pagure'.format(cli_name) + try: + return config.get(conf_section, 'token') + except (NoSectionError, NoOptionError): + raise rpkgError('The "token" value must be set under the "{0}" ' + 'section in your "{1}" user configuration' + .format(conf_section, cli_name)) diff --git a/test/fedpkg-stage.conf b/test/fedpkg-stage.conf index cc3ee04..3c09ebc 100644 --- a/test/fedpkg-stage.conf +++ b/test/fedpkg-stage.conf @@ -12,4 +12,13 @@ kerberos_realms = STG.FEDORAPROJECT.ORG [fedpkg-stage.bodhi] url = https://bodhi.stg.example.com/ -staging = True \ No newline at end of file +staging = True + +[fedpkg-stage.bugzilla] +url = https://bugzilla.stg.example.com + +[fedpkg-stage.pagure] +url = https://pagure.stg.example.com/ + +[fedpkg-stage.pdc] +url = https://pdc.stg.example.com/ diff --git a/test/fedpkg-test.conf b/test/fedpkg-test.conf index 8917e85..2173f0e 100644 --- a/test/fedpkg-test.conf +++ b/test/fedpkg-test.conf @@ -12,4 +12,13 @@ kerberos_realms = FEDORAPROJECT.ORG [fedpkg.bodhi] url = https://bodhi.dummy.example.com/ -staging = False \ No newline at end of file +staging = False + +[fedpkg.bugzilla] +url = https://bugzilla.example.com + +[fedpkg.pagure] +url = https://pagure.example.com/ + +[fedpkg.pdc] +url = https://pdc.example.com/ diff --git a/test/fedpkg-user-stage.conf b/test/fedpkg-user-stage.conf new file mode 100644 index 0000000..9a3e5d0 --- /dev/null +++ b/test/fedpkg-user-stage.conf @@ -0,0 +1,2 @@ +[fedpkg-stage.pagure] +token = iamnotsecret diff --git a/test/fedpkg-user.conf b/test/fedpkg-user.conf new file mode 100644 index 0000000..4764f8b --- /dev/null +++ b/test/fedpkg-user.conf @@ -0,0 +1,2 @@ +[fedpkg.pagure] +token = iamnotsecret diff --git a/test/test_cli.py b/test/test_cli.py index 632367c..7d177ca 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -10,15 +10,22 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. -import six +import sys +import json +from datetime import datetime, timedelta +from tempfile import mkdtemp +from os import rmdir +import six from six.moves.configparser import NoOptionError from six.moves.configparser import NoSectionError +from six.moves import StringIO from pyrpkg.errors import rpkgError from utils import CliTestCase +from fedpkg.bugzilla import BugzillaClient -from mock import call, patch, mock_open, PropertyMock +from mock import call, patch, mock_open, PropertyMock, Mock class TestUpdate(CliTestCase): @@ -181,3 +188,769 @@ class TestUpdate(CliTestCase): '--user', 'someone', '--staging', self.mock_nvr.return_value], shell=True) ]) + + +@patch.object(BugzillaClient, 'client') +class TestRequestRepo(CliTestCase): + """Test the request-repo command""" + + def setUp(self): + self.mock_bug = Mock() + self.mock_bug.creator = 'Tom Hanks' + self.mock_bug.component = 'Package Review' + self.mock_bug.product = 'Fedora' + self.mock_bug.assigned_to = 'Tom Brady' + self.mock_bug.setter = 'Tom Brady' + mod_date = Mock() + mod_date.value = datetime.utcnow().strftime('%Y%m%dT%H:%M:%S') + self.mock_bug.flags = [{ + 'status': '+', + 'name': 'fedora-review', + 'type_id': 65, + 'is_active': 1, + 'id': 1441813, + 'setter': 'Tom Brady', + 'modification_date': mod_date + }] + self.mock_bug.summary = ('Review Request: nethack - A rogue-like ' + 'single player dungeon exploration game') + super(TestRequestRepo, self).setUp() + + def get_cli(self, cli_cmd, name='fedpkg-stage', cfg='fedpkg-stage.conf', + user_cfg='fedpkg-user-stage.conf'): + with patch('sys.argv', new=cli_cmd): + return self.new_cli(name=name, cfg=cfg, user_cfg=user_cfg) + + @patch('requests.post') + @patch('sys.stdout', new=StringIO()) + def test_request_repo(self, mock_request_post, mock_bz): + """Tests a standard request-repo call""" + self.mock_bug.summary = ('Review Request: testpkg - a description') + mock_bz.getbug.return_value = self.mock_bug + mock_rv = Mock() + mock_rv.ok = True + mock_rv.json.return_value = {'issue': {'id': 2}} + mock_request_post.return_value = mock_rv + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + 'request-repo', '1441813'] + cli = self.get_cli(cli_cmd) + cli.request_repo() + + expected_issue_content = { + 'action': 'new_repo', + 'branch': 'master', + 'bug_id': 1441813, + 'description': '', + 'exception': False, + 'monitor': 'monitoring', + 'namespace': 'rpms', + 'repo': 'testpkg', + 'summary': 'a description', + 'upstreamurl': '' + } + # Get the data that was submitted to Pagure + post_data = mock_request_post.call_args_list[0][1]['data'] + actual_issue_content = json.loads(json.loads( + post_data)['issue_content'].strip('```')) + self.assertEqual(expected_issue_content, actual_issue_content) + output = sys.stdout.getvalue().strip() + expected_output = ('https://pagure.stg.example.com/releng/' + 'fedora-scm-requests/issue/2') + self.assertEqual(output, expected_output) + + @patch('requests.post') + @patch('sys.stdout', new=StringIO()) + def test_request_repo_override(self, mock_request_post, mock_bz): + """Tests a request-repo call with an overridden repo name""" + mock_bz.getbug.return_value = self.mock_bug + mock_rv = Mock() + mock_rv.ok = True + mock_rv.json.return_value = {'issue': {'id': 2}} + mock_request_post.return_value = mock_rv + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-repo', '1441813'] + cli = self.get_cli(cli_cmd) + cli.request_repo() + + expected_issue_content = { + 'action': 'new_repo', + 'branch': 'master', + 'bug_id': 1441813, + 'description': '', + 'exception': False, + 'monitor': 'monitoring', + 'namespace': 'rpms', + 'repo': 'nethack', + 'summary': ('A rogue-like single player dungeon exploration ' + 'game'), + 'upstreamurl': '' + } + # Get the data that was submitted to Pagure + post_data = mock_request_post.call_args_list[0][1]['data'] + actual_issue_content = json.loads(json.loads( + post_data)['issue_content'].strip('```')) + self.assertEqual(expected_issue_content, actual_issue_content) + output = sys.stdout.getvalue().strip() + expected_output = ('https://pagure.stg.example.com/releng/' + 'fedora-scm-requests/issue/2') + self.assertEqual(output, expected_output) + + @patch('requests.post') + @patch('sys.stdout', new=StringIO()) + def test_request_repo_module(self, mock_request_post, mock_bz): + """Tests a request-repo call for a new module""" + self.mock_bug.product = 'Fedora Modules' + self.mock_bug.component = 'Module Review' + mock_bz.getbug.return_value = self.mock_bug + mock_rv = Mock() + mock_rv.ok = True + mock_rv.json.return_value = {'issue': {'id': 2}} + mock_request_post.return_value = mock_rv + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'modules/nethack', 'request-repo', + '1441813'] + cli = self.get_cli(cli_cmd) + cli.request_repo() + + expected_issue_content = { + 'action': 'new_repo', + 'branch': 'master', + 'bug_id': 1441813, + 'description': '', + 'exception': False, + 'monitor': 'monitoring', + 'namespace': 'modules', + 'repo': 'nethack', + 'summary': ('A rogue-like single player dungeon exploration ' + 'game'), + 'upstreamurl': '' + } + # Get the data that was submitted to Pagure + post_data = mock_request_post.call_args_list[0][1]['data'] + actual_issue_content = json.loads(json.loads( + post_data)['issue_content'].strip('```')) + self.assertEqual(expected_issue_content, actual_issue_content) + output = sys.stdout.getvalue().strip() + expected_output = ('https://pagure.stg.example.com/releng/' + 'fedora-scm-requests/issue/2') + self.assertEqual(output, expected_output) + + @patch('requests.post') + @patch('sys.stdout', new=StringIO()) + def test_request_repo_container(self, mock_request_post, mock_bz): + """Tests a request-repo call for a new container""" + self.mock_bug.product = 'Fedora Container Images' + self.mock_bug.component = 'Container Review' + mock_bz.getbug.return_value = self.mock_bug + mock_rv = Mock() + mock_rv.ok = True + mock_rv.json.return_value = {'issue': {'id': 2}} + mock_request_post.return_value = mock_rv + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'container/nethack', 'request-repo', + '1441813'] + cli = self.get_cli(cli_cmd) + cli.request_repo() + + expected_issue_content = { + 'action': 'new_repo', + 'branch': 'master', + 'bug_id': 1441813, + 'description': '', + 'exception': False, + 'monitor': 'monitoring', + 'namespace': 'container', + 'repo': 'nethack', + 'summary': ('A rogue-like single player dungeon exploration ' + 'game'), + 'upstreamurl': '' + } + # Get the data that was submitted to Pagure + post_data = mock_request_post.call_args_list[0][1]['data'] + actual_issue_content = json.loads(json.loads( + post_data)['issue_content'].strip('```')) + self.assertEqual(expected_issue_content, actual_issue_content) + output = sys.stdout.getvalue().strip() + expected_output = ('https://pagure.stg.example.com/releng/' + 'fedora-scm-requests/issue/2') + self.assertEqual(output, expected_output) + + @patch('requests.post') + @patch('sys.stdout', new=StringIO()) + def test_request_repo_with_optional_details( + self, mock_request_post, mock_bz): + """Tests a request-repo call with the optional details""" + mock_bz.getbug.return_value = self.mock_bug + mock_rv = Mock() + mock_rv.ok = True + mock_rv.json.return_value = {'issue': {'id': 2}} + mock_request_post.return_value = mock_rv + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-repo', '1441813', '-d', + 'a description', '-s', 'a summary', '-m', 'no-monitoring', + '-u', 'http://test.local'] + cli = self.get_cli(cli_cmd) + cli.request_repo() + + expected_issue_content = { + 'action': 'new_repo', + 'branch': 'master', + 'bug_id': 1441813, + 'description': 'a description', + 'exception': False, + 'monitor': 'no-monitoring', + 'namespace': 'rpms', + 'repo': 'nethack', + 'summary': 'a summary', + 'upstreamurl': 'http://test.local' + } + # Get the data that was submitted to Pagure + post_data = mock_request_post.call_args_list[0][1]['data'] + actual_issue_content = json.loads(json.loads( + post_data)['issue_content'].strip('```')) + self.assertEqual(expected_issue_content, actual_issue_content) + output = sys.stdout.getvalue().strip() + expected_output = ('https://pagure.stg.example.com/releng/' + 'fedora-scm-requests/issue/2') + self.assertEqual(output, expected_output) + + @patch('requests.post') + @patch('sys.stdout', new=StringIO()) + def test_request_repo_exception(self, mock_request_post, mock_bz): + """Tests a request-repo call with the exception flag""" + mock_rv = Mock() + mock_rv.ok = True + mock_rv.json.return_value = {'issue': {'id': 2}} + mock_request_post.return_value = mock_rv + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-repo', '--exception'] + cli = self.get_cli(cli_cmd) + cli.request_repo() + + expected_issue_content = { + 'action': 'new_repo', + 'branch': 'master', + 'bug_id': '', + 'description': '', + 'exception': True, + 'monitor': 'monitoring', + 'namespace': 'rpms', + 'repo': 'nethack', + 'summary': '', + 'upstreamurl': '' + } + # Get the data that was submitted to Pagure + post_data = mock_request_post.call_args_list[0][1]['data'] + actual_issue_content = json.loads(json.loads( + post_data)['issue_content'].strip('```')) + self.assertEqual(expected_issue_content, actual_issue_content) + output = sys.stdout.getvalue().strip() + expected_output = ('https://pagure.stg.example.com/releng/' + 'fedora-scm-requests/issue/2') + self.assertEqual(output, expected_output) + # Since it is an exception, Bugzilla will not have been queried + mock_bz.getbug.assert_not_called() + + def test_request_repo_wrong_package(self, mock_bz): + """Tests request-repo errors when the package is wrong""" + mock_bz.getbug.return_value = self.mock_bug + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'not-nethack', 'request-repo', '1441813'] + cli = self.get_cli(cli_cmd) + expected_error = ('The package in the Bugzilla bug "nethack" doesn\'t ' + 'match the one provided "not-nethack"') + try: + cli.request_repo() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + def test_request_repo_wrong_bug_product(self, mock_bz): + """Tests request-repo errors when the bug product is not Fedora""" + self.mock_bug.product = 'Red Hat' + mock_bz.getbug.return_value = self.mock_bug + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-repo', '1441813'] + cli = self.get_cli(cli_cmd) + expected_error = \ + 'The Bugzilla bug provided is not for "Fedora" or "Fedora EPEL"' + try: + cli.request_repo() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + def test_request_repo_invalid_summary(self, mock_bz): + """Tests request-repo errors when the bug summary has no colon""" + self.mock_bug.summary = 'I am so wrong' + mock_bz.getbug.return_value = self.mock_bug + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-repo', '1441813'] + cli = self.get_cli(cli_cmd) + expected_error = \ + 'Invalid title for this Bugzilla bug (no ":" present)' + try: + cli.request_repo() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + def test_request_repo_invalid_summary_two(self, mock_bz): + """Tests request-repo errors when the bug summary has no dash""" + self.mock_bug.summary = 'So:Wrong' + mock_bz.getbug.return_value = self.mock_bug + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-repo', '1441813'] + cli = self.get_cli(cli_cmd) + expected_error = \ + 'Invalid title for this Bugzilla bug (no "-" present)' + try: + cli.request_repo() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + def test_request_repo_wrong_summary(self, mock_bz): + """Tests request-repo errors when the bug summary is wrong""" + self.mock_bug.summary = ('Review Request: fedpkg - lorum ipsum') + mock_bz.getbug.return_value = self.mock_bug + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-repo', '1441813'] + cli = self.get_cli(cli_cmd) + expected_error = ('The package in the Bugzilla bug "fedpkg" doesn\'t ' + 'match the one provided "nethack"') + try: + cli.request_repo() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + def test_request_repo_expired_bug(self, mock_bz): + """Tests request-repo errors when the bug was approved over 60 days ago + """ + self.mock_bug.flags[0]['modification_date'].value = \ + (datetime.utcnow() - timedelta(days=75)).strftime( + '%Y%m%dT%H:%M:%S') + mock_bz.getbug.return_value = self.mock_bug + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-repo', '1441813'] + cli = self.get_cli(cli_cmd) + expected_error = \ + 'The Bugzilla bug\'s review was approved over 60 days ago' + try: + cli.request_repo() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + def test_request_repo_bug_not_approved(self, mock_bz): + """Tests request-repo errors when the bug is not approved""" + self.mock_bug.flags[0]['name'] = 'something else' + mock_bz.getbug.return_value = self.mock_bug + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-repo', '1441813'] + cli = self.get_cli(cli_cmd) + expected_error = 'The Bugzilla bug is not approved yet' + try: + cli.request_repo() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + def test_request_repo_bug_not_assigned(self, mock_bz): + """Tests request-repo errors when the bug is not assigned""" + self.mock_bug.assigned_to = None + mock_bz.getbug.return_value = self.mock_bug + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-repo', '1441813'] + cli = self.get_cli(cli_cmd) + expected_error = 'The Bugzilla bug provided is not assigned to anyone' + try: + cli.request_repo() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + def test_request_repo_invalid_name(self, mock_bz): + """Tests request-repo errors when the repo name is invalid""" + self.mock_bug.product = 'Red Hat' + mock_bz.getbug.return_value = self.mock_bug + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', '$nethack', 'request-repo', '1441813'] + cli = self.get_cli(cli_cmd) + expected_error = ( + 'The repository name "$nethack" is invalid. It must be at least ' + 'two characters long with only letters, numbers, hyphens, ' + 'underscores, plus signs, and/or periods. Please note that the ' + 'project cannot start with a period or a plus sign.') + try: + cli.request_repo() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + @patch('requests.post') + def test_request_repo_pagure_error(self, mock_request_post, mock_bz): + """Tests a standard request-repo call when the Pagure API call fails""" + mock_bz.getbug.return_value = self.mock_bug + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-repo', '1441813'] + cli = self.get_cli(cli_cmd) + mock_rv = Mock() + mock_rv.ok = False + mock_rv.json.return_value = {'error': 'some error'} + mock_request_post.return_value = mock_rv + try: + cli.request_repo() + assert False, 'rpkgError not raised' + except rpkgError as error: + expected_error = ('The following error occurred while creating a ' + 'new issue in Pagure: some error') + self.assertEqual(str(error), expected_error) + + def test_request_repo_no_bug(self, mock_bz): + """Tests request-repo errors when no bug or exception is provided""" + self.mock_bug.product = 'Red Hat' + mock_bz.getbug.return_value = self.mock_bug + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-repo'] + cli = self.get_cli(cli_cmd) + expected_error = \ + 'A Bugzilla bug is required on new repository requests' + try: + cli.request_repo() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + +class TestRequestBranch(CliTestCase): + """Test the request-branch command""" + + def setUp(self): + super(TestRequestBranch, self).setUp() + + def tearDown(self): + super(TestRequestBranch, self).tearDown() + + def get_cli(self, cli_cmd, name='fedpkg-stage', cfg='fedpkg-stage.conf', + user_cfg='fedpkg-user-stage.conf'): + with patch('sys.argv', new=cli_cmd): + return self.new_cli(name=name, cfg=cfg, user_cfg=user_cfg) + + @patch('requests.post') + @patch('fedpkg.cli.get_release_branches') + @patch('sys.stdout', new=StringIO()) + def test_request_branch(self, mock_grb, mock_request_post): + """Tests request-branch""" + mock_grb.return_value = set(['el6', 'epel7', 'f25', 'f26', 'f27']) + mock_rv = Mock() + mock_rv.ok = True + mock_rv.json.return_value = {'issue': {'id': 2}} + mock_request_post.return_value = mock_rv + # Checkout the f27 branch + self.run_cmd(['git', 'checkout', 'f27'], cwd=self.cloned_repo_path) + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + 'request-branch'] + cli = self.get_cli(cli_cmd) + cli.request_branch() + + expected_issue_content = { + 'action': 'new_branch', + 'repo': 'testpkg', + 'namespace': 'rpms', + 'branch': 'f27', + 'create_git_branch': True + } + # Get the data that was submitted to Pagure + post_data = mock_request_post.call_args_list[0][1]['data'] + actual_issue_content = json.loads(json.loads( + post_data)['issue_content'].strip('```')) + self.assertEqual(expected_issue_content, actual_issue_content) + output = sys.stdout.getvalue().strip() + expected_output = ('https://pagure.stg.example.com/releng/' + 'fedora-scm-requests/issue/2') + self.assertEqual(output, expected_output) + + @patch('requests.post') + @patch('fedpkg.cli.get_release_branches') + @patch('sys.stdout', new=StringIO()) + def test_request_branch_override(self, mock_grb, mock_request_post): + """Tests request-branch with an overriden package and branch name""" + mock_grb.return_value = set(['el6', 'epel7', 'f25', 'f26', 'f27']) + mock_rv = Mock() + mock_rv.ok = True + mock_rv.json.return_value = {'issue': {'id': 2}} + mock_request_post.return_value = mock_rv + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-branch', 'f27'] + cli = self.get_cli(cli_cmd) + cli.request_branch() + + expected_issue_content = { + 'action': 'new_branch', + 'repo': 'nethack', + 'namespace': 'rpms', + 'branch': 'f27', + 'create_git_branch': True + } + # Get the data that was submitted to Pagure + post_data = mock_request_post.call_args_list[0][1]['data'] + actual_issue_content = json.loads(json.loads( + post_data)['issue_content'].strip('```')) + self.assertEqual(expected_issue_content, actual_issue_content) + output = sys.stdout.getvalue().strip() + expected_output = ('https://pagure.stg.example.com/releng/' + 'fedora-scm-requests/issue/2') + self.assertEqual(output, expected_output) + + @patch('requests.post') + @patch('fedpkg.cli.get_release_branches') + @patch('sys.stdout', new=StringIO()) + def test_request_branch_module(self, mock_grb, mock_request_post): + """Tests request-branch for a new module branch""" + mock_grb.return_value = set(['el6', 'epel7', 'f25', 'f26', 'f27']) + mock_rv = Mock() + mock_rv.ok = True + mock_rv.json.return_value = {'issue': {'id': 2}} + mock_request_post.return_value = mock_rv + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'modules/nethack', 'request-branch', 'f27'] + cli = self.get_cli(cli_cmd) + cli.request_branch() + + expected_issue_content = { + 'action': 'new_branch', + 'repo': 'nethack', + 'namespace': 'modules', + 'branch': 'f27', + 'create_git_branch': True + } + # Get the data that was submitted to Pagure + post_data = mock_request_post.call_args_list[0][1]['data'] + actual_issue_content = json.loads(json.loads( + post_data)['issue_content'].strip('```')) + self.assertEqual(expected_issue_content, actual_issue_content) + output = sys.stdout.getvalue().strip() + expected_output = ('https://pagure.stg.example.com/releng/' + 'fedora-scm-requests/issue/2') + self.assertEqual(output, expected_output) + + @patch('requests.post') + @patch('fedpkg.cli.get_release_branches') + @patch('sys.stdout', new=StringIO()) + def test_request_branch_container(self, mock_grb, mock_request_post): + """Tests request-branch for a new container branch""" + mock_grb.return_value = set(['el6', 'epel7', 'f25', 'f26', 'f27']) + mock_rv = Mock() + mock_rv.ok = True + mock_rv.json.return_value = {'issue': {'id': 2}} + mock_request_post.return_value = mock_rv + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'container/nethack', 'request-branch', + 'f27'] + cli = self.get_cli(cli_cmd) + cli.request_branch() + + expected_issue_content = { + 'action': 'new_branch', + 'repo': 'nethack', + 'namespace': 'container', + 'branch': 'f27', + 'create_git_branch': True + } + # Get the data that was submitted to Pagure + post_data = mock_request_post.call_args_list[0][1]['data'] + actual_issue_content = json.loads(json.loads( + post_data)['issue_content'].strip('```')) + self.assertEqual(expected_issue_content, actual_issue_content) + output = sys.stdout.getvalue().strip() + expected_output = ('https://pagure.stg.example.com/releng/' + 'fedora-scm-requests/issue/2') + self.assertEqual(output, expected_output) + + @patch('requests.post') + @patch('fedpkg.cli.get_release_branches') + @patch('fedpkg.cli.verify_sls') + @patch('sys.stdout', new=StringIO()) + def test_request_branch_sls(self, mock_verify_sls, mock_grb, + mock_request_post): + """Tests request-branch with service levels""" + mock_grb.return_value = set(['el6', 'epel7', 'f25', 'f26', 'f27']) + mock_rv_post = Mock() + mock_rv_post.ok = True + mock_rv_post.json.return_value = {'issue': {'id': 2}} + mock_request_post.return_value = mock_rv_post + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-branch', '9', '--sl', + 'security_fixes:2030-12-01', 'bug_fixes:2030-12-01'] + cli = self.get_cli(cli_cmd) + cli.request_branch() + + expected_issue_content = { + 'action': 'new_branch', + 'repo': 'nethack', + 'namespace': 'rpms', + 'branch': '9', + 'create_git_branch': True, + 'sls': { + 'security_fixes': '2030-12-01', + 'bug_fixes': '2030-12-01' + } + } + # Get the data that was submitted to Pagure + post_data = mock_request_post.call_args_list[0][1]['data'] + actual_issue_content = json.loads(json.loads( + post_data)['issue_content'].strip('```')) + self.assertEqual(expected_issue_content, actual_issue_content) + output = sys.stdout.getvalue().strip() + expected_output = ('https://pagure.stg.example.com/releng/' + 'fedora-scm-requests/issue/2') + self.assertEqual(output, expected_output) + + @patch('requests.post') + @patch('fedpkg.cli.get_release_branches') + @patch('sys.stdout', new=StringIO()) + def test_request_branch_all_releases(self, mock_grb, mock_request_post): + """Tests request-branch with the '--all-releases' option """ + mock_grb.return_value = set(['el6', 'epel7', 'f25', 'f26', 'f27']) + post_side_effect = [] + for i in range(1, 4): + mock_rv = Mock() + mock_rv.ok = True + mock_rv.json.return_value = {'issue': {'id': i}} + post_side_effect.append(mock_rv) + mock_request_post.side_effect = post_side_effect + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-branch', + '--all-releases'] + cli = self.get_cli(cli_cmd) + cli.request_branch() + + for i in range(3): + expected_issue_content = { + 'action': 'new_branch', + 'repo': 'nethack', + 'namespace': 'rpms', + 'branch': 'f' + str(27 - i), + 'create_git_branch': True + } + post_data = mock_request_post.call_args_list[i][1]['data'] + actual_issue_content = json.loads(json.loads( + post_data)['issue_content'].strip('```')) + self.assertEqual(expected_issue_content, actual_issue_content) + + output = sys.stdout.getvalue().strip() + expected_output = """\ +https://pagure.stg.example.com/releng/fedora-scm-requests/issue/1 +https://pagure.stg.example.com/releng/fedora-scm-requests/issue/2 +https://pagure.stg.example.com/releng/fedora-scm-requests/issue/3""" + self.assertEqual(output, expected_output) + + def test_request_branch_invalid_use_of_all_releases(self): + """Tests request-branch with a branch and the '--all-releases' option + """ + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-branch', 'f27', + '--all-releases'] + cli = self.get_cli(cli_cmd) + expected_error = \ + 'You cannot specify a branch with the "--all-releases" option' + try: + cli.request_branch() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + def test_request_branch_invalid_use_of_all_releases_sl(self): + """Tests request-branch with an SL and the '--all-releases' option + """ + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-branch', + '--all-releases', '--sl', 'security_fixes:2020-01-01'] + cli = self.get_cli(cli_cmd) + expected_error = ('You cannot specify service levels with the ' + '"--all-releases" option') + try: + cli.request_branch() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + @patch('fedpkg.cli.get_release_branches') + def test_request_branch_invalid_sls(self, mock_grb): + """Tests request-branch with invalid service levels""" + mock_grb.return_value = set(['el6', 'epel7', 'f25', 'f26', 'f27']) + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-branch', '9', '--sl', + 'security_fixes-2030-12-01', 'bug_fixes:2030-12-01'] + cli = self.get_cli(cli_cmd) + expected_error = \ + 'The SL "security_fixes-2030-12-01" is in an invalid format' + try: + cli.request_branch() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + @patch('fedpkg.cli.get_release_branches') + def test_request_branch_sls_on_release_branch_error(self, mock_grb): + """Tests request-branch with a release branch and service levels""" + mock_grb.return_value = set(['el6', 'epel7', 'f25', 'f26', 'f27']) + + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'nethack', 'request-branch', 'f27', '--sl', + 'security_fixes-2030-12-01', 'bug_fixes:2030-12-01'] + cli = self.get_cli(cli_cmd) + expected_error = 'You can\'t provide SLs for release branches' + try: + cli.request_branch() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + def test_request_branch_invalid_module_branch_name(self): + """Test request-branch raises an exception when a invalid module branch + name is supplied""" + cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, + '--module-name', 'modules/nethack', 'request-branch', + 'some:branch'] + cli = self.get_cli(cli_cmd) + expected_error = ( + 'Only characters, numbers, periods, dashes, underscores, and ' + 'pluses are allowed in module branch names') + try: + cli.request_branch() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + + def test_request_branch_no_branch(self): + """Test request-branch raises an exception when a branch isn't supplied + """ + tempdir = mkdtemp() + cli_cmd = ['fedpkg-stage', '--path', tempdir, + '--module-name', 'nethack', 'request-branch'] + cli = self.get_cli(cli_cmd) + expected_error = ( + 'You must specify a branch if you are not in a git repository') + try: + cli.request_branch() + assert False, 'rpkgError not raised' + except rpkgError as error: + self.assertEqual(str(error), expected_error) + finally: + rmdir(tempdir) diff --git a/test/test_utils.py b/test/test_utils.py new file mode 100644 index 0000000..cff22bd --- /dev/null +++ b/test/test_utils.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# fedpkg - a Python library for RPM Packagers +# +# Copyright (C) 2017 Red Hat Inc. +# Author(s): Matt Prahl +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for +# the full text of the license. + +from mock import patch, Mock +from pyrpkg.errors import rpkgError + +from utils import CliTestCase +from fedpkg import utils + + +class TestUtils(CliTestCase): + """Test functions in fedpkg.utils""" + + @patch('requests.get') + def test_get_sl_type(self, mock_get): + """Test get_sl_type""" + sl_type = { + 'id': 1, + 'name': 'security_fixes', + 'description': 'security_fixes', + } + mock_rv = Mock() + mock_rv.ok = True + mock_rv.json.return_value = { + 'count': 1, + 'results': [sl_type] + } + mock_get.return_value = mock_rv + rv = utils.get_sl_type('http://pdc.local/', 'securty_fixes') + self.assertEqual(rv, sl_type) + + @patch('requests.get') + def test_get_sl_type_pdc_error(self, mock_request_get): + """Test get_sl_type when PDC errors""" + mock_rv = Mock() + mock_rv.ok = False + mock_rv.text = 'Some error' + mock_request_get.return_value = mock_rv + try: + utils.get_sl_type('http://pdc.local/', 'securty_fixes') + assert False, 'rpkgError not raised' + except rpkgError as error: + expected_error = ('The following error occurred while validating ' + 'the passed in service level in PDC: Some error') + self.assertEqual(str(error), expected_error) + + @patch('fedpkg.utils.get_sl_type') + def test_verify_sls(self, mock_get_sl_type): + """Test verify_sls""" + mock_get_sl_type.return_value = { + 'id': 1, + 'name': 'security_fixes', + 'description': 'security_fixes', + } + sls = {'security_fixes': '2222-12-01'} + # If it's invalid, an rpkgError will be raised + try: + utils.verify_sls('http://pdc.local/', sls) + except rpkgError: + assert False, 'An rpkgError exception was raised but not expected' + + @patch('fedpkg.utils.get_sl_type') + def test_verify_sls_eol_expired(self, mock_get_sl_type): + """Test verify_sls raises an exception when an EOL is expired""" + mock_get_sl_type.return_value = { + 'id': 1, + 'name': 'security_fixes', + 'description': 'security_fixes', + } + sls = {'security_fixes': '2001-12-01'} + + try: + utils.verify_sls('http://pdc.local/', sls) + assert False, 'An rpkgError exception was not raised' + except rpkgError as e: + self.assertEqual(str(e), 'The SL "2001-12-01" is already expired') + + def test_sl_list_dict(self): + """Test sl_list_to_dict""" + sls = ['security_fixes:2030-01-01', 'bug_fixes:2029-01-01'] + sls_dict = {'security_fixes': '2030-01-01', 'bug_fixes': '2029-01-01'} + self.assertEqual(utils.sl_list_to_dict(sls), sls_dict) + + def test_sl_list_to_dict_invalid_format(self): + """Tests sl_list_to_dict with an invalid SL format. An error is + expected. + """ + try: + sls = ['security_fixes:2030-12-01', 'bug_fixes/2030-12-01'] + utils.sl_list_to_dict(sls) + assert False, 'An rpkgError exception was not raised' + except rpkgError as e: + assert str(e) == \ + 'The SL "bug_fixes/2030-12-01" is in an invalid format' + + def test_verify_sls_invalid_date(self): + """Test verify_sls with an SL that is not June 1st or December 1st. An + error is expected. + """ + for eol in ['2030-01-01', '2030-12-25']: + try: + sls = {'security_fixes': eol, 'bug_fixes': '2030-12-01'} + utils.verify_sls('abc', sls) + assert False, 'An rpkgError exception was not raised' + except rpkgError as e: + assert str(e) == ('The SL "{0}" must expire on June 1st or ' + 'December 1st'.format(eol)) + + @patch('fedpkg.utils.Bodhi2Client') + def test_get_release_branches(self, mock_bodhi): + """Test that get_release_branches returns all the active Fedora release + branches. + """ + mock_bodhi_client = Mock() + mock_bodhi_client.send_request.return_value = { + u'page': 1, + u'pages': 1, + u'releases': [ + {'state': 'current', 'branch': 'el6'}, + {'state': 'archived', 'branch': 'f24'}, + {'state': 'current', 'branch': 'epel7'}, + {'state': 'current', 'branch': 'f25'}, + {'state': 'archived', 'branch': 'el5'}, + {'state': 'archived', 'branch': 'f23'}, + {'state': 'current', 'branch': 'f26'}, + {'state': 'pending', 'branch': 'f27m'}, + {'state': 'current', 'branch': 'f27'} + ], + u'rows_per_page': 20, + u'total': 11} + mock_bodhi.return_value = mock_bodhi_client + expected = set(['el6', 'epel7', 'f25', 'f26', 'f27']) + actual = utils.get_release_branches('http://bodhi.local') + self.assertEqual(expected, actual) diff --git a/test/utils.py b/test/utils.py index 04fb220..52e72f5 100644 --- a/test/utils.py +++ b/test/utils.py @@ -136,7 +136,10 @@ rm -rf $$RPM_BUILD_ROOT def setUp(self): # create a base repo - self.repo_path = tempfile.mkdtemp(prefix='fedpkg-commands-tests-') + self.repo_base = tempfile.mkdtemp(prefix='fedpkg-commands-tests-') + # Have a namespace of rpms and a repo name of testpkg + self.repo_path = os.path.join(self.repo_base, 'rpms', 'testpkg') + os.makedirs(self.repo_path) self.spec_filename = 'docpkg.spec' @@ -175,7 +178,7 @@ rm -rf $$RPM_BUILD_ROOT self.run_cmd(cmd, cwd=self.cloned_repo_path) def tearDown(self): - shutil.rmtree(self.repo_path) + shutil.rmtree(self.repo_base) shutil.rmtree(self.cloned_repo_path) def make_commands(self, path=None, user=None, dist=None, target=None, @@ -232,7 +235,7 @@ class CliTestCase(CommandTestCase): default_config_file = os.path.join(os.path.dirname(__file__), 'fedpkg-test.conf') - def new_cli(self, name='fedpkg', cfg=None): + def new_cli(self, name='fedpkg', cfg=None, user_cfg=None): config = configparser.SafeConfigParser() if cfg: config_file = os.path.join(os.path.dirname(__file__), cfg) @@ -240,6 +243,11 @@ class CliTestCase(CommandTestCase): config_file = self.default_config_file config.read(config_file) + if user_cfg: + user_config_file = os.path.join( + os.path.dirname(__file__), user_cfg) + config.read(user_config_file) + client = fedpkg.cli.fedpkgClient(config, name=name) client.setupLogging(pyrpkg.log) pyrpkg.log.setLevel(logging.CRITICAL)