diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 87536b59..00000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[bumpversion] -commit = True -tag = True -current_version = 2.2.0-dev -parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? -serialize = - {major}.{minor}.{patch}-{release} - {major}.{minor}.{patch} - -[bumpversion:file:bugzilla/apiversion.py] - -[bumpversion:file:sonar-project.properties] - -[bumpversion:part:release] -optional_value = gamma -values = - dev - gamma - diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..13e4e05b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + target-branch: "main" + commit-message: + prefix: "ci" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..d01e8bf9 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,147 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +# Derived from stock python-package.yml +name: CI + +on: [push, pull_request] + +jobs: + linter: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.x"] + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r test-requirements.txt setuptools + - name: Lint + run: | + pylint --output-format colorized --rcfile .pylintrc \ + bugzilla-cli setup.py bugzilla examples tests + test_3_6: + # python 3.6 is for rhel/centos8/sles15 compat + runs-on: ubuntu-latest + container: + image: python:3.6 + steps: + - uses: actions/checkout@v6 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + pip install -r requirements.txt -r test-requirements.txt + + - name: Test with pytest + run: | + pytest + + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest pytest-cov + pip install -r requirements.txt -r test-requirements.txt + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Test with pytest and generate coverage report + run: | + pytest --cov --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + # Run functional tests + integration: + runs-on: ubuntu-latest + services: + mariadb: + image: mariadb:latest + env: + MARIADB_USER: bugs + MARIADB_DATABASE: bugs + MARIADB_PASSWORD: secret + MARIADB_ROOT_PASSWORD: supersecret + ports: + - 3306:3306 + bugzilla: + image: ghcr.io/crazyscientist/bugzilla:test + ports: + - 80:80 + strategy: + matrix: + python-version: ["3.x"] + steps: + - uses: actions/checkout@v6 + - name: Install MariaDB utils + run: sudo apt install --no-install-recommends -q -y mariadb-client + - name: Restore DB dump + run: mariadb -h 127.0.0.1 -P 3306 --password=secret -u bugs bugs < tests/services/bugs.sql + - name: Store API key + run: | + mkdir -p ~/.config/python-bugzilla/ + cp tests/services/bugzillarc ~/.config/python-bugzilla/ + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest pytest-cov + pip install -r requirements.txt -r test-requirements.txt + - name: Test with pytest + run: pytest --ro-integration --rw-integration + env: + BUGZILLA_URL: http://localhost + + # Build and install on Windows + windows: + runs-on: windows-latest + strategy: + matrix: + python-version: ["3.x"] + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Build tarball & install + run: | + python setup.py sdist + + pip install --find-links dist python-bugzilla diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..3b537cb9 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,32 @@ +# This workflow will publish the package on PyPI +# For more information see: https://github.com/pypa/gh-action-pypi-publish + +name: Publish +on: + release: + types: [released] + +jobs: + publish: + name: Upload release to PyPI + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/python-bugzilla + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.x" + - name: Install pypa/build + run: pip install build + - name: Build a source tarball + run: python -m build --sdist + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: false + verbose: false diff --git a/.gitignore b/.gitignore index 17f44c0c..a3f16c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,7 @@ MANIFEST dist build +.cache .coverage .tox - -#python-bugzilla venvs -dev-env* +.pytest_cache diff --git a/.packit.yml b/.packit.yml new file mode 100644 index 00000000..aefa13b9 --- /dev/null +++ b/.packit.yml @@ -0,0 +1,16 @@ +--- +upstream_project_url: https://github.com/python-bugzilla/python-bugzilla + +jobs: + - job: copr_build + trigger: commit + metadata: + targets: + - fedora-all + - epel-8-x86_64 + - job: tests + trigger: commit + metadata: + targets: + - fedora-all + - epel-8-x86_64 diff --git a/tests/pylint.cfg b/.pylintrc similarity index 67% rename from tests/pylint.cfg rename to .pylintrc index 021cdf03..378199f1 100644 --- a/tests/pylint.cfg +++ b/.pylintrc @@ -7,7 +7,8 @@ persistent=no # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). -disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return +disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,fixme,global-statement,broad-except,bare-except,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension,import-outside-toplevel,use-a-generator,consider-using-with,consider-using-f-string,unspecified-encoding,use-implicit-booleaness-not-comparison +enable=fixme [REPORTS] @@ -18,7 +19,7 @@ score=no [FORMAT] # Maximum number of characters on a single line. -max-line-length=80 +max-line-length=100 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). @@ -33,7 +34,7 @@ notes=FIXME,XXX,TODO [VARIABLES] # A regular expression matching the beginning of the name of dummy variables # (i.e. not used). -dummy-variables-rgx=ignore.*|.*_ignore +dummy-variables-rgx=dummy.*|ignore.*|.*_ignore # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 01f2e480..00000000 --- a/.travis.yml +++ /dev/null @@ -1,50 +0,0 @@ -language: python - -python: - - 2.7 - - 3.3 - - 3.4 - - 3.5 - - 3.6 - - pypy-5.3.1 - -install: - - pip install tox - - pip install tox-travis - -script: - - tox -- --ro-functional - -jobs: - include: - - stage: sonar - dist: trusty - language: java - addons: - sonarcloud: - organization: "python-bugzilla" - token: - secure: "fk6YMhht7X5a60QWeeLWOrUu6zjS/1fhVfM7PicaDhkhwOaadzjuKgVj+xAmQL8cMh98hK8wk1aE9uDuwTbKMMydDqY9pH8agtyk3j/2ArbOl1hphYWo3FeoB6q8FTiKN1fiT+8hPy7MIjjFthe3e6dyUR2Le5QtH2B+BCYrDMdqbvN+GXLyBPTAvzbDerpnw1j5o2QWjaLu+juznaR+q/PL58796P6GU2WmpfkpOaMYJA2CA/w0Ehivp4zlbZpmuC/v6NopSjuT6RpO3ppPtTRmlwMZEGTAwXhxlU2CHlIyCq44Rsm0kxf1+xQK2aL/SWJzCDk6rHAd16B10r3AKwYW05Rclw49x9HV29zGeUTJD3qgRscf0v5yYyCISv1r9D4jJ+y9tIVHvMzuMvl6G0rZNiwxu6OwlzpyzQkgPbO183HGCyjYKaBn6jQmCWHzxnk+FRUt7Rvbd53Mz3TYbx7LeTnRDPNc69nIbybjzIRXEw/4SVsncue3whrj4s81NJzkvDaABzLdnd1X9G2aG8h1Vk1XpuUnP7wVyKr1L1wcSdvtIbZ7ekgOA8xQG9XHh4E+5FTZEKOYeb7ZeDKijvUnl1Qo3dXKMS2/Y+18Ogl+4M9w7c3PCjxhDAsuv42uNyxH66qxdzrzckjsyJVS6VTwTBf8fsjzgQ5hmHjKPYA=" - branches: - - master - script: - - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then sonar-scanner; fi' - branches: - only: - - master - - stage: deploy - python: 3.6 - script: ignore - deploy: - provider: pypi - user: python-bugzilla-admin - password: - secure: "hQKQ/EAD/zPndc+wL/wLiJXsP9lqXfSlousP5YuemaUenQNVrR5wXRDGr+Jm+S2rdaqVMq+1/lVcW3Mnd5MgqzFc8pL7Mg+6OpNZ8PAP3XLFZulc/Fyt0q98KVBQCQgHLXaQfMpitaC6/XYrOE22DfTyL2UbidWWcPMz+fLsM3jKcItv8jVLB8LlFVnDNpUt/HlTkatc9maNG+zR/aaWVsVPD5QEl+Ss01gnCj1yAoKSKxKF2A1PYh23Oue/yYb+n2dnaWqRGB3gUatFM+7cadEf7qfaRVwyghtso+CP4t9inldNugPuqBv3Ji7wZWA2+vLV18cWonPd7H7LJofRQmhDHDU+deGRtR0WYyZ2qjGi1b7ufP0ofU0NeqHlT3gTivzXXWviAjiYyYwN8Fa9AVsgietaLrxLo/HFYdr7fA6QS106Era1IlwQDgc32fOR+gpW03LuSFVEFEsfm2Vj8hvgxQZSLeL/Xb3YZDvAGd1ieLAvwotZgUd+QLlw1KMiZ141Kp63hz03Us6rftAhcOEYb/w2DnM/WsEwNNjuJSJZdKRyeV/rCMUH8MPx3bdooXPYNZcPOMEnbKYep4lvmDfhG8dO6A5N8IHXc4Yhu2TpRTafLd9DyvIZOBJuJdDN8Y0Z5NwBrjXlqLTCmrWpuCpFwJyat7zd+pWYFzb7gp0=" - distributions: "sdist bdist_wheel" - on: - tags: true - -notifications: - email: true - on_success: never - on_failure: always diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 638ebc97..c3943334 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,27 +9,33 @@ dependencies, running the command line from git is as simple as doing: # Running tests -Once you have already activated an environment, you can use the following. +Our test suite uses pytest. If your system has dependencies already, the +quick unit test suite is invoked simply with: -## Basic unit test suite - - python setup.py test + pytest ## Read-Only Functional tests -There are more comprehensive tests that are disabled by default. Readonly -functional tests that run against several public bugzilla instances. No -login account is required: - python setup.py test --ro-functional +There are more comprehensive, readonly functional tests that run against +several public bugzilla instances, but they are not run by default. No +login account is required. Run them with: + + pytest --ro-functional ## Read/Write Functional Tests. -Before running rw-functional tests, make sure you have logged into bugzilla -using. These currently run against the test bugzilla instance at -partner-bugzilla.redhat.com, and requires a valid login there: +Read/Write functional tests use bugzilla.stage.redhat.com, which is a +bugzilla instance specifically for this type of testing. Data is occasionally +hard synced with regular bugzilla.redhat.com, and all local edits are +removed. Login accounts are also synced. If you want access to +bugzilla.stage.redhat.com, sign up for a regular bugzilla.redhat.com login +and wait for the next sync period. + +Before running these tests, you'll need to cache login credentials. +Example: - bugzilla-cli --bugzilla=partner-bugzilla.redhat.com --username=$USER login - python setup.py test --rw-functional + ./bugzilla-cli --bugzilla=bugzilla.stage.redhat.com --username=$USER login + pytest --rw-functional ## Testing across python versions To test all supported python versions, run tox using any of the following. @@ -39,45 +45,21 @@ To test all supported python versions, run tox using any of the following. tox -- --rw-functional -# pylint and pep8 +# pylint and pycodestyle -To test for pylint or pep8 violations, you can run: +To test for pylint or pycodestyle violations, you can run: - python setup.py pylint + ./setup.py pylint -Note: This expects that you already have pylint and pep8 installed. +Note: This expects that you already have pylint and pycodestyle installed. # Patch Submission If you are submitting a patch, ensure the following: - [REQ] verify that no new pylint or pep8 violations + [REQ] verify that no new pylint or pycodestyle violations [REQ] run basic unit test suite across all python versions as described above. Running any of the functional tests is not a requirement for patch submission, but please give them a go if you are interested. - -Patches can be submitted via github pull-request, or via the mailing list -at python-bugzilla@lists.fedorahosted.org using 'git send-email'. - - -# Bug reports - -Bug reports should be submitted as github issues, or sent to the mailing list - -# Release and tag management - -This project uses [bumpversion](https://github.com/peritus/bumpversion) to manage releases. - -## Example release steps -```sh -# release the current version, eg: 2.2.0-dev -> 2.2.0 -bumpversion release - -# prepare the next patch (z-stream) version, eg: 2.2.0 -> 2.2.1-dev -bumpversion --no-tag patch - -# else, prepare the next minor (y-stream) version, eg: 2.2.0 -> 2.3.0-dev -bumpversion --no-tag minor -``` diff --git a/MANIFEST.in b/MANIFEST.in index 43b14525..b8f64d7b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,7 @@ include COPYING CONTRIBUTING.md MANIFEST.in README.md NEWS.md -include bugzilla.1 include xmlrpc-api-notes.txt include python-bugzilla.spec include *requirements.txt +include man/bugzilla.rst recursive-include examples *.py recursive-include tests *.py *.txt *.cfg diff --git a/NEWS.md b/NEWS.md index 012156ec..f3c91ab1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,68 @@ # python-bugzilla release news +## Release 3.3.0 (June, 2024) +- Expose error codes from the REST API (Stanislav Levin) +- Fixed broken link in documentation (Danilo C. L. de Paula) +- Set `Bug.weburl` that is compatible with the REST API +- Do not convert 'blocks' or 'depends' to int in `Bugzilla.build_update` (Adam Williamson) +- Use proper REST API route for getting a single bug +- Avoid duplicate entries when one id is 0 (Ricardo Branco) +- Removed unused argument from `Bugzilla.add_dict` +- Fixed API key leak (Ricardo Branco) +- Automatically include alias in include_fields in `Bugzilla._getbugs` +- Added method `Bugzilla.query_return_extra` +- cli: Support --field and --field-json for bugzilla attach + +## Release 3.2.0 (January 12, 2022) +- Use soon-to-be-required Authorization header for RH bugzilla +- Remove cookie auth support + +## Release 3.1.0 (July 27, 2021) +- Detect bugzilla.stage.redhat.com as RHBugzilla +- Add limit as option to build_query (Ivan Lausuch) + +## Release 3.0.2 (November 12, 2020) +- Fix API key leaking into requests exceptions + +## Release 3.0.1 (October 07, 2020) +- Skip man page generation to fix build on Windows (Alexander Todorov) + +## Release 3.0.0 (October 03, 2020) +- Drop python2 support +- New option `bugzilla modify --minor-update option` +- requests: use PYTHONBUGZILLA_REQUESTS_TIMEOUT env variable +- xmlrpc: Don't add api key to passed in user dictionary + +## Release 2.5.0 (July 04, 2020) +- cli: Add query --extrafield, --includefield, --excludefield +- Revive bugzilla.rhbugzilla.RHBugzilla import path + +## Release 2.4.0 (June 29, 2020) +- Bugzilla REST API support +- Add --json command line output option +- Add APIs for Bugzilla Groups (Pierre-Yves Chibon) +- Add `Bugzilla.get_requests_session()` API to access raw requests Session +- Add `Bugzilla.get_xmlrpc_proxy()` API to access raw ServerProxy +- Add `Bugzilla requests_session=` init parameter to pass in auth, etc. +- Add `bugzilla attach --ignore-obsolete` (Čestmír Kalina) +- Add `bugzilla login --api-key` for API key prompting (Danilo C. L. de + Paula) +- Add `bugzilla new --private` + +## Release 2.3.0 (August 26, 2019) +- restrict-login suppot (Viliam Krizan) +- cli: Add support for private attachments (Brian 'Redbeard' Harrington) +- Fix python3 deprecation warnings +- Drop python 3.3 support, minimum python3 is python 3.4 now + +## Release 2.2.0 (August 11, 2018) +- Port tests to pytest +- cli: --cert Client side certificate support (Tobias Wolter) +- cli: add ability to post comment while sending attachment (Jeff Mahoney) +- cli: Add --comment-tag option +- cli: Add info --active-components +- Add a raw Product.get wrapper API + ## Release 2.1.0 (March 30, 2017) - Support for bugzilla 5 API Keys (Dustin J. Mitchell) - bugzillarc can be used to set default URL for the cli tool diff --git a/README.md b/README.md index 4742be92..4c40be66 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ +[![CI](https://github.com/python-bugzilla/python-bugzilla/workflows/CI/badge.svg)](https://github.com/python-bugzilla/python-bugzilla/actions?query=workflow%3ACI) +[![codecov](https://codecov.io/gh/python-bugzilla/python-bugzilla/branch/main/graph/badge.svg?token=4R3FR4RVH4)](https://codecov.io/gh/python-bugzilla/python-bugzilla) +[![PyPI](https://img.shields.io/pypi/v/python-bugzilla)](https://pypi.org/project/python-bugzilla/) + # python-bugzilla This package provides two bits: -* 'bugzilla' python module for talking to a [Bugzilla](https://www.bugzilla.org/) instance over XMLRPC -* /usr/bin/bugzilla command line tool for performing actions from the command line: create or edit bugs, various queries, etc. +* `bugzilla` python module for talking to a [Bugzilla](https://www.bugzilla.org/) instance over XMLRPC or REST +* `/usr/bin/bugzilla` command line tool for performing actions from the command line: create or edit bugs, various queries, etc. -This was originally written specifically for Red Hat's Bugzilla instance -and is used heavily at Red Hat and in Fedora, but it should still be +This was originally written specifically for [Red Hat's Bugzilla instance](https://bugzilla.redhat.com) +and is used heavily at Red Hat and in Fedora, but it should be generically useful. -You can find some code examples in the [examples](examples) directory - -For questions about submitting bug reports or patches, see [CONTRIBUTING.md](CONTRIBUTING.md) +You can find some code examples in the [examples](examples) directory. -Questions, comments, and discussions should go to our mailing: http://lists.fedorahosted.org/mailman/listinfo/python-bugzilla +For questions about submitting patches, see [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/xmlrpc-api-notes.txt b/bugzilla-api.txt similarity index 74% rename from xmlrpc-api-notes.txt rename to bugzilla-api.txt index 4b4759fe..110ceffb 100644 --- a/xmlrpc-api-notes.txt +++ b/bugzilla-api.txt @@ -1,31 +1,9 @@ - -Fedora infrastructure depends on python-bugzilla in various ways: -http://lists.fedorahosted.org/pipermail/python-bugzilla/2012-June/000001.html - -Red Hat bugzilla originally had a totally custom API. Much of that is -being dropped in 2013, API conversions outlined here: -https://bugzilla.redhat.com/show_bug.cgi?id=822007 - -Externally facing RH bugzilla instance that doesn't send email and is -refreshed periodically. This is what is used in the functional test suite: -http://partner-bugzilla.redhat.com - -Some trackers in the wild to use for API testing: - bugzilla.redhat.com - bugzilla.mozilla.org - bugzilla.kernel.org - bugzilla.gnome.org - bugs.freedesktop.org - bugzilla.novell.com - bugzilla.zimbra.com - bugzilla.samba.org - bugs.gentoo.org - +This document tracks upstream bugzilla API changes, and related info. Upstream timeline ================= -Here's a timeline of the evolution of the upstream bugzilla XMLRPC API: +Here's a timeline of the evolution of the upstream bugzilla XMLRPC/REST API: Bugzilla 2.*: No XMLRPC API that I can tell @@ -97,7 +75,7 @@ Bugzilla 4.4: Util.params_to_objects Bugzilla 5.0: (July 2015) - https://www.bugzilla.org/docs/5.0/en/html/integrating/api/index.html + https://bugzilla.readthedocs.io/en/5.0/api/index.html Bug.update_attachment Bug.search/update_comment_tags Bug.search: @@ -110,13 +88,27 @@ Bugzilla 5.0: (July 2015) Component.create User.valid_login + Bugzilla latest/tip: https://bugzilla.readthedocs.io/en/latest/api/index.html -Redhat Bugzilla: 4.4 based with extensions. Bits on top of 4.4 + Misc info + ========= + +Bugzilla REST API code link + https://github.com/bugzilla/bugzilla/tree/5.2/Bugzilla/WebService/Server/REST/Resources + + +Redhat Bugzilla: 5.0 based with extensions https://bugzilla.redhat.com/docs/en/html/api/ Bug.search has --from-url extension Bug.update has more hashing support extra_fields for fetching comments, attachments, etc at Bug.get time ExternalBugs extension: https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html + + +Fedora infrastructure python-bugzilla consumers: + https://infrastructure.fedoraproject.org/cgit/ansible.git/tree/roles/distgit/pagure/templates/pagure-sync-bugzilla.py.j2 + https://github.com/fedora-infra/bodhi/blob/develop/bodhi/server/bugs.py + https://github.com/fedora-infra/fas/blob/develop/tools/export-bugzilla.py diff --git a/bugzilla-cli b/bugzilla-cli index 12e3ca12..8a5ddb76 100755 --- a/bugzilla-cli +++ b/bugzilla-cli @@ -1,3 +1,7 @@ -#!/bin/sh +#!/usr/bin/env python3 -PYTHONPATH=. exec bin/bugzilla "$@" +# This is a small wrapper script to simplify running the 'bugzilla' +# cli tool from a git checkout + +from bugzilla import _cli +_cli.main() diff --git a/bugzilla.1 b/bugzilla.1 deleted file mode 100644 index 88e54213..00000000 --- a/bugzilla.1 +++ /dev/null @@ -1,275 +0,0 @@ -.TH bugzilla 1 "Mar 30, 2017" "version 2.1.0" "User Commands" -.SH NAME -bugzilla \- command-line interface to Bugzilla over XML-RPC -.SH SYNOPSIS -.B bugzilla -[\fIoptions\fR] [\fIcommand\fR] [\fIcommand-options\fR] -.SH DESCRIPTION -.PP -.BR bugzilla -is a command-line utility that allows access to the XML-RPC interface provided -by Bugzilla. -.PP -\fIcommand\fP is one of: -.br -.I \fR * login - log into the given bugzilla instance -.br -.I \fR * new - create a new bug -.br -.I \fR * query - search for bugs matching given criteria -.br -.I \fR * modify - modify existing bugs -.br -.I \fR * attach - attach files to existing bugs, or get attachments -.br -.I \fR * info - get info about the given bugzilla instance -.SH GLOBAL OPTIONS -.IP "--version" -show program's version number and exit -.IP "--help, -h" -show this help message and exit -.IP "--bugzilla=BUGZILLA" -bugzilla XMLRPC URI. default: https://bugzilla.redhat.com/xmlrpc.cgi -.IP "--nosslverify" -Don't error on invalid bugzilla SSL certificate -.IP "--login" -Run interactive "login" before performing the specified command. -.IP "--username=USERNAME" -Log in with this username -.IP "--password=PASSWORD" -Log in with this password -.IP "--ensure-logged-in" -Raise an error if we aren't logged in to bugzilla. Consider using this if you are depending on cached credentials, to ensure that when they expire the tool errors, rather than subtly change output. -.IP "--no-cache-credentials" -Don't save any bugzilla cookies or tokens to disk, and don't use any pre-existing credentials. -.IP "--cookiefile=COOKIEFILE" -cookie file to use for bugzilla authentication -.IP "--tokenfile=TOKENFILE" -token file to use for bugzilla authentication -.IP "--verbose" -give more info about what's going on -.IP "--debug" -output bunches of debugging info -.IP "--version" -show program's version number and exit - -.SH Standard bugzilla options -.PP -These options are shared by some combination of the 'new', 'query', and 'modify' sub commands. Not every option works for each command though. - -.IP "--product=PRODUCT, -p PRODUCT" -Product name -.IP "--version=VERSION, -v VERSION" -Product version -.IP "--component=COMPONENT, -c COMPONENT" -Component name -.IP "--summary=SUMMARY, -s SUMMARY, --short_desc=SUMMARY" -Bug summary -.IP "--comment=DESCRIPTION, -l DESCRIPTION" -Set initial bug comment/description -.IP "--sub-component=SUB_COMPONENT" -RHBZ sub component name -.IP "--os=OS, -o OS" -Operating system -.IP "--arch=ARCH" -Arch this bug occurs on -.IP "--severity=SEVERITY, -x SEVERITY" -Bug severity -.IP "--priority=PRIORITY, -z PRIORITY" -Bug priority -.IP "--alias=ALIAS" -Bug alias (name) -.IP "--status=STATUS, -s STATUS, --bug_status=STATUS" -Bug status (NEW, ASSIGNED, etc.) -.IP "--url=URL, -u URL" -URL for further bug info -.IP "--target_milestone=TARGET_MILESTONE, -m TARGET_MILESTONE" -Target milestone -.IP "--target_release=TARGET_RELEASE" -RHBZ Target release -.IP "--blocked=BUGID[, BUGID, ...]" -Bug IDs that this bug blocks -.IP "--dependson=BUGID[, BUGID, ...]" -Bug IDs that this bug depends on -.IP "--keywords=KEYWORD[, KEYWORD, ...]" -Bug keywords -.IP "--groups=GROUP[, GROUP, ...]" -Which user groups can view this bug -.IP "--cc=CC[, CC, ...]" -CC list -.IP "--assigned_to=ASSIGNED_TO, -a ASSIGNED_TO, --assignee ASSIGNED_TO" -Bug assignee -.IP "--qa_contact=QA_CONTACT, -q QA_CONTACT" -QA contact -.IP "--whiteboard WHITEBOARD, -w WHITEBOARD, --status_whiteboard WHITEBOARD" -Whiteboard field -.IP "--devel_whiteboard DEVEL_WHITEBOARD" -RHBZ devel whiteboard field -.IP "--internal_whiteboard INTERNAL_WHITEBOARD" -RHBZ internal whiteboard field -.IP "--qa_whiteboard QA_WHITEBOARD" -RHBZ QA whiteboard field -.IP "--fixed_in FIXED_IN, -F FIXED_IN -RHBZ 'Fixed in version' field -.IP "--field=FIELD=VALUE" -Manually specify a bugzilla XMLRPC field. FIELD is the raw name used by the bugzilla instance. For example if your bugzilla instance has a custom field cf_my_field, do: --field cf_my_field=VALUE - - -.SH Output options -.PP -These options are shared by several commands, for tweaking the text output of the command results. -.IP "--full, -f" -output detailed bug info -.IP "--ids, -i" -output only bug IDs -.IP "--extra, -e" -output additional bug information (keywords, Whiteboards, etc.) -.IP "--oneline" -one line summary of the bug (useful for scripts) -.IP "--raw" -raw output of the bugzilla contents -.IP "--outputformat=OUTPUTFORMAT" -Print output in the form given. You can use RPM-style tags that match bug fields, e.g.: '%{id}: %{summary}'. - -The output of the bugzilla tool should NEVER BE PARSED unless you are using a -custom --outputformat. For everything else, just don't parse it, the formats -are not stable and are subject to change. - ---outputformat allows printing arbitrary bug data in a user preferred format. -For example, to print a returned bug ID, component, and product, separated -with ::, do: - ---outputformat "%{id}::%{component}::%{product}" - -The fields (like 'id', 'component', etc.) are the names of the values returned -by bugzilla's XMLRPC interface. To see a list of all fields, check the API -documentation in the 'SEE ALSO' section. Alternatively, run a 'bugzilla ---debug query ...' and look at the key names returned in the query results. -Also, in most cases, using the name of the associated command line switch -should work, like --bug_status becomes %{bug_status}, etc. - - -.SH \[oq]query\[cq] specific options -Certain options can accept a comma separated list to query multiple values, including --status, --component, --product, --version, --id. - -Note: querying via explicit command line options will only get you so far. See the --from-url option for a way to use powerful Web UI queries from the command line. -.IP "--id ID, -b ID, --bug_id ID" -specify individual bugs by IDs, separated with commas -.IP "--reporter REPORTER, -r REPORTER" -Email: search reporter email for given address -.IP "--quicksearch QUICKSEARCH" -Search using bugzilla's quicksearch functionality. -.IP "--savedsearch SAVEDSEARCH" -Name of a bugzilla saved search. If you don't own this saved search, you must passed --savedsearch_sharer_id. -.IP "--savedsearch-sharer-id SAVEDSEARCH_SHARER_ID" -Owner ID of the --savedsearch. You can get this ID from the URL bugzilla generates when running the saved search from the web UI. -.IP "--from-url WEB_QUERY_URL" -Make a working query via bugzilla's 'Advanced search' web UI, grab the url from your browser (the string with query.cgi or buglist.cgi in it), and --from-url will run it via the bugzilla API. Don't forget to quote the string! This only works for Bugzilla 5 and Red Hat bugzilla - - -.SH \[oq]modify\[cq] specific options -Fields that take multiple values have a special input format. - - Append: --cc=foo@example.com - Overwrite: --cc==foo@example.com - Remove: --cc=-foo@example.com - -Options that accept this format: --cc, --blocked, --dependson, --groups, --tags, whiteboard fields. -.IP "--close RESOLUTION, -k RESOLUTION" -Close with the given resolution (WONTFIX, NOTABUG, etc.) -.IP "--dupeid ORIGINAL, -d ORIGINAL" -ID of original bug. Implies --close DUPLICATE -.IP "--private" -Mark new comment as private -.IP "--reset-assignee" -Reset assignee to component default -.IP "--reset-qa-contact" -Reset QA contact to component default - - -.SH \[oq]attach\[cq] options -.IP "--file=FILENAME, -f FILENAME" -File to attach, or filename for data provided on stdin -.IP "--description=DESCRIPTION, -d DESCRIPTION" -A short description of the file being attached -.IP "--type=MIMETYPE, -t MIMETYPE" -Mime-type for the file being attached -.IP "--get=ATTACHID, -g ATTACHID" -Download the attachment with the given ID -.IP "--getall=BUGID, --get-all=BUGID" -Download all attachments on the given bug - - -.SH \[oq]info\[cq] options -.IP "--products, -p" -Get a list of products -.IP "--components=PRODUCT, -c PRODUCT" -List the components in the given product -.IP "--component_owners=PRODUCT, -o PRODUCT" -List components (and their owners) -.IP "--versions=VERSION, -v VERSION" -List the versions for the given product - - -.SH AUTHENTICATION COOKIES AND TOKENS - -Older bugzilla instances use cookie-based authentication, and -newer bugzilla instances (around 5.0) use a non-cookie token system. - -When you log into bugzilla with the "login" subcommand or the "--login" -argument, we cache the login credentials in ~/.cache/python-bugzilla/ -Previously we cached credentials in ~/.. If you want to see -which file the tool is using, check --debug output. - -To perform an authenticated bugzilla command on a new machine, run a one time -"bugzilla login" to cache credentials before running the desired command. You -can also run "bugzilla --login" and the login process will be initiated before -invoking the command. - -Additionally, the --no-cache-credentials option will tell the bugzilla tool to -_not_ save any credentials in $HOME, or use any previously cached credentials. - -.SH EXAMPLES -.PP -.RS 0 -bugzilla query --bug_id 62037 - -bugzilla query --version 15 --component python-bugzilla - -# All boolean options can be formatted like this -.br -bugzilla query --blocked "123456 | 224466" - -bugzilla login - -bugzilla new -p Fedora -v rawhide -c python-bugzilla \\ - --summary "python-bugzilla causes headaches" \\ - --comment "python-bugzilla made my brain hurt when I used it." - -bugzilla attach --file ~/Pictures/cam1.jpg --desc "me, in pain" $BUGID - -bugzilla attach --getall $BUGID - -bugzilla modify --close NOTABUG --comment "Actually, you're hungover." $BUGID - - -.SH EXIT STATUS -.BR bugzilla -normally returns 0 if the requested command was successful. -Otherwise, exit status is 1 if -.BR bugzilla -is interrupted by the user (or a login attempt fails), 2 if a -socket error occurs (e.g. TCP connection timeout), and 3 if the server returns -an XML-RPC fault. -.SH BUGS -Please report any bugs as github issues at -.br -https://github.com/python-bugzilla/python-bugzilla -.br -to the mailing list at -.br -https://fedorahosted.org/mailman/listinfo/python-bugzilla -.SH SEE ALSO -.nf -https://bugzilla.readthedocs.io/en/latest/api/index.html -https://bugzilla.redhat.com/docs/en/html/api/Bugzilla/WebService/Bug.html diff --git a/bugzilla/__init__.py b/bugzilla/__init__.py index 74f55148..95a52cda 100644 --- a/bugzilla/__init__.py +++ b/bugzilla/__init__.py @@ -3,19 +3,15 @@ # Copyright (C) 2007, 2008 Red Hat Inc. # Author: Will Woods # -# 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. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. from .apiversion import version, __version__ from .base import Bugzilla -from .transport import BugzillaError -from .rhbugzilla import RHBugzilla +from .exceptions import BugzillaError from .oldclasses import (Bugzilla3, Bugzilla32, Bugzilla34, Bugzilla36, Bugzilla4, Bugzilla42, Bugzilla44, - NovellBugzilla, RHBugzilla3, RHBugzilla4) + NovellBugzilla, RHBugzilla, RHBugzilla3, RHBugzilla4) # This is the public API. If you are explicitly instantiating any other diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py new file mode 100644 index 00000000..bdb977e7 --- /dev/null +++ b/bugzilla/_authfiles.py @@ -0,0 +1,179 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import configparser +import os +from logging import getLogger +import urllib.parse + +from ._util import listify + +log = getLogger(__name__) + + +def _parse_hostname(url): + # If http://example.com is passed, netloc=example.com path="" + # If just example.com is passed, netloc="" path=example.com + parsedbits = urllib.parse.urlparse(url) + return parsedbits.netloc or parsedbits.path + + +def _makedirs(path): + if os.path.exists(os.path.dirname(path)): + return + os.makedirs(os.path.dirname(path), 0o700) + + +def _default_cache_location(filename): + """ + Determine default location for passed xdg filename. + example: ~/.cache/python-bugzilla/bugzillarc + """ + return os.path.expanduser("~/.cache/python-bugzilla/%s" % filename) + + +class _BugzillaRCFile(object): + """ + Helper class for interacting with bugzillarc files + """ + @staticmethod + def get_default_configpaths(): + paths = [ + '/etc/bugzillarc', + '~/.bugzillarc', + '~/.config/python-bugzilla/bugzillarc', + ] + return paths + + def __init__(self): + self._cfg = None + self._configpaths = None + self.set_configpaths(None) + + def set_configpaths(self, configpaths): + configpaths = [os.path.expanduser(p) for p in + listify(configpaths or [])] + + cfg = configparser.ConfigParser() + read_files = cfg.read(configpaths) + if read_files: + log.info("Found bugzillarc files: %s", read_files) + + self._cfg = cfg + self._configpaths = configpaths or [] + + def get_configpaths(self): + return self._configpaths[:] + + def get_default_url(self): + """ + Grab a default URL from bugzillarc [DEFAULT] url=X + """ + cfgurl = self._cfg.defaults().get("url", None) + if cfgurl is not None: + log.debug("bugzillarc: found cli url=%s", cfgurl) + return cfgurl + + def parse(self, url): + """ + Find the section for the passed URL domain, and return all the fields + """ + section = "" + log.debug("bugzillarc: Searching for config section matching %s", url) + + urlhost = _parse_hostname(url) + for sectionhost in sorted(self._cfg.sections()): + # If the section is just a hostname, make it match + # If the section has a / in it, do a substring match + if "/" not in sectionhost: + if sectionhost == urlhost: + section = sectionhost + elif sectionhost in url: + section = sectionhost + if section: + log.debug("bugzillarc: Found matching section: %s", section) + break + + if not section: + log.debug("bugzillarc: No section found") + return {} + return dict(self._cfg.items(section)) + + + def save_api_key(self, url, api_key): + """ + Save the API_KEY in the config file. We use the last file + in the configpaths list, which is the one with the highest + precedence. + """ + configpaths = self.get_configpaths() + if not configpaths: + return None + + config_filename = configpaths[-1] + section = _parse_hostname(url) + cfg = configparser.ConfigParser() + cfg.read(config_filename) + + if section not in cfg.sections(): + cfg.add_section(section) + + cfg.set(section, 'api_key', api_key.strip()) + + _makedirs(config_filename) + with open(config_filename, 'w') as configfile: + cfg.write(configfile) + + return config_filename + + +class _BugzillaTokenCache(object): + """ + Class for interacting with a .bugzillatoken cache file + """ + @staticmethod + def get_default_path(): + return _default_cache_location("bugzillatoken") + + def __init__(self): + self._filename = None + self._cfg = None + + def _get_domain(self, url): + domain = urllib.parse.urlparse(url)[1] + if domain and domain not in self._cfg.sections(): + self._cfg.add_section(domain) + return domain + + def get_value(self, url): + domain = self._get_domain(url) + if domain and self._cfg.has_option(domain, 'token'): + return self._cfg.get(domain, 'token') + return None + + def set_value(self, url, value): + if self.get_value(url) == value: + return + + domain = self._get_domain(url) + if value is None: + self._cfg.remove_option(domain, 'token') + else: + self._cfg.set(domain, 'token', value) + + if self._filename: + _makedirs(self._filename) + with open(self._filename, 'w') as _cfg: + log.debug("Saving to _cfg") + self._cfg.write(_cfg) + + def get_filename(self): + return self._filename + + def set_filename(self, filename): + log.debug("Using tokenfile=%s", filename) + cfg = configparser.ConfigParser() + if filename: + cfg.read(filename) + self._filename = filename + self._cfg = cfg diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py new file mode 100644 index 00000000..8fd9a80e --- /dev/null +++ b/bugzilla/_backendbase.py @@ -0,0 +1,288 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +from logging import getLogger + +import requests + +log = getLogger(__name__) + + +class _BackendBase(object): + """ + Backends are thin wrappers around the different bugzilla API paradigms + (XMLRPC, REST). This base class defines the public API for the rest of + the code, but this is all internal to the library. + """ + def __init__(self, url, bugzillasession): + self._url = url + self._bugzillasession = bugzillasession + + + @staticmethod + def probe(url): + try: + requests.head(url, timeout=10).raise_for_status() + return True # pragma: no cover + except Exception as e: + log.debug("Failed to probe url=%s : %s", url, str(e)) + return False + + + ################# + # Internal APIs # + ################# + + def get_xmlrpc_proxy(self): + """ + Provides the raw XMLRPC proxy to API users of Bugzilla._proxy + """ + raise NotImplementedError() + + def is_rest(self): + """ + :returns: True if this is the REST backend + """ + return False + + def is_xmlrpc(self): + """ + :returns: True if this is the XMLRPC backend + """ + return False + + + ###################### + # Bugzilla info APIs # + ###################### + + def bugzilla_version(self): + """ + Fetch bugzilla version string + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bugzilla.html#version + """ + raise NotImplementedError() + + + ####################### + # Bug attachment APIs # + ####################### + + def bug_attachment_get(self, attachment_ids, paramdict): + """ + Fetch bug attachments IDs. One part of: + http://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#get-attachment + """ + raise NotImplementedError() + + def bug_attachment_get_all(self, bug_ids, paramdict): + """ + Fetch all bug attachments IDs. One part of + http://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#get-attachment + """ + raise NotImplementedError() + + def bug_attachment_create(self, bug_ids, data, paramdict): + """ + Create a bug attachment + http://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#create-attachment + + :param data: raw Bytes data of the attachment to attach. API will + encode this correctly if you pass it in and 'data' is not in + paramdict. + """ + raise NotImplementedError() + + def bug_attachment_update(self, attachment_ids, paramdict): + """ + Update a bug attachment + http://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#update-attachment + """ + raise NotImplementedError() + + + ############ + # bug APIs # + ############ + + def bug_comments(self, bug_ids, paramdict): + """ + Fetch bug comments + http://bugzilla.readthedocs.io/en/latest/api/core/v1/comment.html#get-comments + """ + raise NotImplementedError() + + def bug_create(self, paramdict): + """ + Create a new bug + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug + """ + raise NotImplementedError() + + def bug_fields(self, paramdict): + """ + Query available bug field values + http://bugzilla.readthedocs.io/en/latest/api/core/v1/field.html#fields + """ + raise NotImplementedError() + + def bug_get(self, bug_ids, aliases, paramdict): + """ + Lookup bug data by ID + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#get-bug + """ + raise NotImplementedError() + + def bug_history(self, bug_ids, paramdict): + """ + Lookup bug history + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#bug-history + """ + raise NotImplementedError() + + def bug_search(self, paramdict): + """ + Search/query bugs + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs + """ + raise NotImplementedError() + + def bug_update(self, bug_ids, paramdict): + """ + Update bugs + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#update-bug + """ + raise NotImplementedError() + + def bug_update_tags(self, bug_ids, paramdict): + """ + Update bug tags + https://www.bugzilla.org/docs/4.4/en/html/api/Bugzilla/WebService/Bug.html#update_tags + """ + raise NotImplementedError() + + + ################## + # Component APIs # + ################## + + def component_create(self, paramdict): + """ + Create component + https://bugzilla.readthedocs.io/en/latest/api/core/v1/component.html#create-component + """ + raise NotImplementedError() + + def component_update(self, paramdict): + """ + Update component + https://bugzilla.readthedocs.io/en/latest/api/core/v1/component.html#update-component + """ + raise NotImplementedError() + + + ############################### + # ExternalBugs extension APIs # + ############################### + + def externalbugs_add(self, paramdict): + """ + https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#add-external-bug + """ + raise NotImplementedError() + + def externalbugs_update(self, paramdict): + """ + https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#update-external-bug + """ + raise NotImplementedError() + + def externalbugs_remove(self, paramdict): + """ + https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#remove-external-bug + """ + raise NotImplementedError() + + + ############## + # Group APIs # + ############## + + def group_get(self, paramdict): + """ + https://bugzilla.readthedocs.io/en/latest/api/core/v1/group.html#get-group + """ + raise NotImplementedError() + + + ################ + # Product APIs # + ################ + + def product_get(self, paramdict): + """ + Fetch product details + http://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#get-product + """ + raise NotImplementedError() + + def product_get_accessible(self): + """ + List accessible products + http://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#list-products + """ + raise NotImplementedError() + + def product_get_enterable(self): + """ + List enterable products + http://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#list-products + """ + raise NotImplementedError() + + def product_get_selectable(self): + """ + List selectable products + http://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#list-products + """ + raise NotImplementedError() + + + ############# + # User APIs # + ############# + + def user_create(self, paramdict): + """ + Create user + http://bugzilla.readthedocs.io/en/latest/api/core/v1/user.html#create-user + """ + raise NotImplementedError() + + def user_get(self, paramdict): + """ + Get user info + http://bugzilla.readthedocs.io/en/latest/api/core/v1/user.html#get-user + """ + raise NotImplementedError() + + def user_login(self, paramdict): + """ + Log in to bugzilla + http://bugzilla.readthedocs.io/en/latest/api/core/v1/user.html#login + """ + raise NotImplementedError() + + def user_logout(self): + """ + Log out of bugzilla + http://bugzilla.readthedocs.io/en/latest/api/core/v1/user.html#logout + """ + raise NotImplementedError() + + def user_update(self, paramdict): + """ + Update user + http://bugzilla.readthedocs.io/en/latest/api/core/v1/user.html#update-user + """ + raise NotImplementedError() diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py new file mode 100644 index 00000000..45bc4999 --- /dev/null +++ b/bugzilla/_backendrest.py @@ -0,0 +1,229 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import base64 +import json +import logging +import os + +from ._backendbase import _BackendBase +from .exceptions import BugzillaError, BugzillaHTTPError +from ._util import listify + + +log = logging.getLogger(__name__) + + +def _update_key(indict, updict, key): + if key not in indict: + indict[key] = {} + indict[key].update(updict.get(key, {})) + + +class _BackendREST(_BackendBase): + """ + Internal interface for direct calls to bugzilla's REST API + """ + def __init__(self, url, bugzillasession): + _BackendBase.__init__(self, url, bugzillasession) + self._bugzillasession.set_rest_defaults() + + + ######################### + # Internal REST helpers # + ######################### + def _handle_error(self, e): + response = getattr(e, "response", None) + if response is None: + raise e # pragma: no cover + + if response.status_code in [400, 401, 404]: + self._handle_error_response(response.text) + raise e + + def _handle_error_response(self, text): + try: + result = json.loads(text) + except json.JSONDecodeError: + return + + if result.get("error"): + raise BugzillaError(result["message"], code=result["code"]) + + def _handle_response(self, text): + try: + ret = dict(json.loads(text)) + except Exception: # pragma: no cover + log.debug("Failed to parse REST response. Output is:\n%s", text) + raise + + if ret.get("error", False): # pragma: no cover + raise BugzillaError(ret["message"], code=ret["code"]) + return ret + + def _op(self, method, apiurl, paramdict=None): + fullurl = os.path.join(self._url, apiurl.lstrip("/")) + log.debug("Bugzilla REST %s %s params=%s", method, fullurl, paramdict) + + data = None + authparams = self._bugzillasession.get_auth_params() + if method == "GET": + authparams.update(paramdict or {}) + else: + data = json.dumps(paramdict or {}) + + try: + response = self._bugzillasession.request( + method, fullurl, data=data, params=authparams + ) + except BugzillaHTTPError as e: + self._handle_error(e) + + return self._handle_response(response.text) + + def _get(self, *args, **kwargs): + return self._op("GET", *args, **kwargs) + def _put(self, *args, **kwargs): + return self._op("PUT", *args, **kwargs) + def _post(self, *args, **kwargs): + return self._op("POST", *args, **kwargs) + + + ####################### + # API implementations # + ####################### + + def get_xmlrpc_proxy(self): + raise BugzillaError("You are using the bugzilla REST API, " + "so raw XMLRPC access is not provided.") + def is_rest(self): + return True + + def bugzilla_version(self): + return self._get("/version") + + def bug_create(self, paramdict): + return self._post("/bug", paramdict) + def bug_fields(self, paramdict): + return self._get("/field/bug", paramdict) + def bug_get(self, bug_ids, aliases, paramdict): + bug_list = listify(bug_ids) + alias_list = listify(aliases) + permissive = paramdict.pop("permissive", False) + data = paramdict.copy() + + # FYI: The high-level API expects the backends to raise an exception + # when retrieval of a single bug fails (default behavior of the XMLRPC + # API), but the REST API simply returns an empty search result set. + # To ensure compliant behavior, the REST backend needs to use the + # explicit URL to get a single bug. + if not permissive and len(bug_list or []) + len(alias_list or []) == 1: + for id_list in (bug_list, alias_list): + if id_list: + return self._get("/bug/%s" % id_list[0], data) + + data["id"] = bug_list + data["alias"] = alias_list + ret = self._get("/bug", data) + return ret + + def bug_attachment_get(self, attachment_ids, paramdict): + # XMLRPC supported mutiple fetch at once, but not REST + ret = {} + for attid in listify(attachment_ids): + out = self._get("/bug/attachment/%s" % attid, paramdict) + _update_key(ret, out, "attachments") + _update_key(ret, out, "bugs") + return ret + + def bug_attachment_get_all(self, bug_ids, paramdict): + # XMLRPC supported mutiple fetch at once, but not REST + ret = {} + for bugid in listify(bug_ids): + out = self._get("/bug/%s/attachment" % bugid, paramdict) + _update_key(ret, out, "attachments") + _update_key(ret, out, "bugs") + return ret + + def bug_attachment_create(self, bug_ids, data, paramdict): + if data is not None and "data" not in paramdict: + paramdict["data"] = base64.b64encode(data).decode("utf-8") + paramdict["ids"] = listify(bug_ids) + return self._post("/bug/%s/attachment" % paramdict["ids"][0], + paramdict) + + def bug_attachment_update(self, attachment_ids, paramdict): + paramdict["ids"] = listify(attachment_ids) + return self._put("/bug/attachment/%s" % paramdict["ids"][0], paramdict) + + def bug_comments(self, bug_ids, paramdict): + # XMLRPC supported mutiple fetch at once, but not REST + ret = {} + for bugid in bug_ids: + out = self._get("/bug/%s/comment" % bugid, paramdict) + _update_key(ret, out, "bugs") + return ret + def bug_history(self, bug_ids, paramdict): + # XMLRPC supported mutiple fetch at once, but not REST + ret = {"bugs": []} + for bugid in bug_ids: + out = self._get("/bug/%s/history" % bugid, paramdict) + ret["bugs"].extend(out.get("bugs", [])) + return ret + + def bug_search(self, paramdict): + return self._get("/bug", paramdict) + def bug_update(self, bug_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) + return self._put("/bug/%s" % data["ids"][0], data) + def bug_update_tags(self, bug_ids, paramdict): + raise BugzillaError("No REST API available for bug_update_tags") + + def component_create(self, paramdict): + return self._post("/component", paramdict) + def component_update(self, paramdict): + if "ids" in paramdict: + apiurl = str(listify(paramdict["ids"])[0]) # pragma: no cover + if "names" in paramdict: + apiurl = ("%(product)s/%(component)s" % + listify(paramdict["names"])[0]) + return self._put("/component/%s" % apiurl, paramdict) + + def externalbugs_add(self, paramdict): # pragma: no cover + raise BugzillaError( + "No REST API available yet for externalbugs_add") + def externalbugs_remove(self, paramdict): # pragma: no cover + raise BugzillaError( + "No REST API available yet for externalbugs_remove") + def externalbugs_update(self, paramdict): # pragma: no cover + raise BugzillaError( + "No REST API available yet for externalbugs_update") + + def group_get(self, paramdict): + return self._get("/group", paramdict) + + def product_get(self, paramdict): + return self._get("/product/get", paramdict) + def product_get_accessible(self): + return self._get("/product_accessible") + def product_get_enterable(self): + return self._get("/product_enterable") + def product_get_selectable(self): + return self._get("/product_selectable") + + def user_create(self, paramdict): + return self._post("/user", paramdict) + def user_get(self, paramdict): + return self._get("/user", paramdict) + def user_login(self, paramdict): + return self._get("/login", paramdict) + def user_logout(self): + return self._get("/logout") + def user_update(self, paramdict): + urlid = None + if "ids" in paramdict: + urlid = listify(paramdict["ids"])[0] # pragma: no cover + if "names" in paramdict: + urlid = listify(paramdict["names"])[0] + return self._put("/user/%s" % urlid, paramdict) diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py new file mode 100644 index 00000000..0558b350 --- /dev/null +++ b/bugzilla/_backendxmlrpc.py @@ -0,0 +1,228 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +from logging import getLogger +import sys +from xmlrpc.client import (Binary, Fault, ProtocolError, + ServerProxy, Transport) + +from requests import RequestException + +from ._backendbase import _BackendBase +from .exceptions import BugzillaError +from ._util import listify + + +log = getLogger(__name__) + + +class _BugzillaXMLRPCTransport(Transport): + def __init__(self, bugzillasession): + if hasattr(Transport, "__init__"): + Transport.__init__(self, use_datetime=False) + + self.__bugzillasession = bugzillasession + self.__bugzillasession.set_xmlrpc_defaults() + self.__seen_valid_xml = False + + # Override Transport.user_agent + self.user_agent = self.__bugzillasession.get_user_agent() + + + ############################ + # Bugzilla private helpers # + ############################ + + def __request_helper(self, url, request_body): + """ + A helper method to assist in making a request and parsing the response. + """ + response = None + # pylint: disable=try-except-raise + # pylint: disable=raise-missing-from + try: + response = self.__bugzillasession.request( + "POST", url, data=request_body) + + return self.parse_response(response) + except RequestException as e: + if not response: + raise + raise ProtocolError( # pragma: no cover + url, response.status_code, str(e), response.headers) + except Fault: + raise + except Exception: + msg = str(sys.exc_info()[1]) + if not self.__seen_valid_xml: + msg += "\nThe URL may not be an XMLRPC URL: %s" % url + e = BugzillaError(msg) + # pylint: disable=attribute-defined-outside-init + e.__traceback__ = sys.exc_info()[2] + # pylint: enable=attribute-defined-outside-init + raise e + + + ###################### + # Tranport overrides # + ###################### + + def parse_response(self, response): + """ + Override Transport.parse_response + """ + parser, unmarshaller = self.getparser() + msg = response.text.encode('utf-8') + try: + parser.feed(msg) + except Exception: # pragma: no cover + log.debug("Failed to parse this XMLRPC response:\n%s", msg) + raise + + self.__seen_valid_xml = True + parser.close() + return unmarshaller.close() + + def request(self, host, handler, request_body, verbose=0): + """ + Override Transport.request + """ + # Setting self.verbose here matches overrided request() behavior + # pylint: disable=attribute-defined-outside-init + self.verbose = verbose + + url = "%s://%s%s" % (self.__bugzillasession.get_scheme(), + host, handler) + + # xmlrpclib fails to escape \r + request_body = request_body.replace(b'\r', b' ') + + return self.__request_helper(url, request_body) + + +class _BugzillaXMLRPCProxy(ServerProxy, object): + """ + Override of xmlrpc ServerProxy, to insert bugzilla API auth + into the XMLRPC request data + """ + def __init__(self, uri, bugzillasession, *args, **kwargs): + self.__bugzillasession = bugzillasession + transport = _BugzillaXMLRPCTransport(self.__bugzillasession) + ServerProxy.__init__(self, uri, transport, *args, **kwargs) + + def _ServerProxy__request(self, methodname, params): + """ + Overrides ServerProxy _request method + """ + # params is a singleton tuple, enforced by xmlrpc.client.dumps + newparams = params and params[0].copy() or {} + + log.debug("XMLRPC call: %s(%s)", methodname, newparams) + authparams = self.__bugzillasession.get_auth_params() + authparams.update(newparams) + + # pylint: disable=no-member + ret = ServerProxy._ServerProxy__request( + self, methodname, (authparams,)) + # pylint: enable=no-member + + return ret + + +class _BackendXMLRPC(_BackendBase): + """ + Internal interface for direct calls to bugzilla's XMLRPC API + """ + def __init__(self, url, bugzillasession): + _BackendBase.__init__(self, url, bugzillasession) + self._xmlrpc_proxy = _BugzillaXMLRPCProxy(url, self._bugzillasession) + + def get_xmlrpc_proxy(self): + return self._xmlrpc_proxy + def is_xmlrpc(self): + return True + + def bugzilla_version(self): + return self._xmlrpc_proxy.Bugzilla.version() + + def bug_attachment_get(self, attachment_ids, paramdict): + data = paramdict.copy() + data["attachment_ids"] = listify(attachment_ids) + return self._xmlrpc_proxy.Bug.attachments(data) + def bug_attachment_get_all(self, bug_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) + return self._xmlrpc_proxy.Bug.attachments(data) + def bug_attachment_create(self, bug_ids, data, paramdict): + pdata = paramdict.copy() + pdata["ids"] = listify(bug_ids) + if data is not None and "data" not in paramdict: + pdata["data"] = Binary(data) + return self._xmlrpc_proxy.Bug.add_attachment(pdata) + def bug_attachment_update(self, attachment_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(attachment_ids) + return self._xmlrpc_proxy.Bug.update_attachment(data) + + def bug_comments(self, bug_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) + return self._xmlrpc_proxy.Bug.comments(data) + def bug_create(self, paramdict): + return self._xmlrpc_proxy.Bug.create(paramdict) + def bug_fields(self, paramdict): + return self._xmlrpc_proxy.Bug.fields(paramdict) + def bug_get(self, bug_ids, aliases, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) or [] + data["ids"] += listify(aliases) or [] + return self._xmlrpc_proxy.Bug.get(data) + def bug_history(self, bug_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) + return self._xmlrpc_proxy.Bug.history(data) + def bug_search(self, paramdict): + return self._xmlrpc_proxy.Bug.search(paramdict) + def bug_update(self, bug_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) + return self._xmlrpc_proxy.Bug.update(data) + def bug_update_tags(self, bug_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) + return self._xmlrpc_proxy.Bug.update_tags(data) + + def component_create(self, paramdict): + return self._xmlrpc_proxy.Component.create(paramdict) + def component_update(self, paramdict): + return self._xmlrpc_proxy.Component.update(paramdict) + + def externalbugs_add(self, paramdict): + return self._xmlrpc_proxy.ExternalBugs.add_external_bug(paramdict) + def externalbugs_update(self, paramdict): + return self._xmlrpc_proxy.ExternalBugs.update_external_bug(paramdict) + def externalbugs_remove(self, paramdict): + return self._xmlrpc_proxy.ExternalBugs.remove_external_bug(paramdict) + + def group_get(self, paramdict): + return self._xmlrpc_proxy.Group.get(paramdict) + + def product_get(self, paramdict): + return self._xmlrpc_proxy.Product.get(paramdict) + def product_get_accessible(self): + return self._xmlrpc_proxy.Product.get_accessible_products() + def product_get_enterable(self): + return self._xmlrpc_proxy.Product.get_enterable_products() + def product_get_selectable(self): + return self._xmlrpc_proxy.Product.get_selectable_products() + + def user_create(self, paramdict): + return self._xmlrpc_proxy.User.create(paramdict) + def user_get(self, paramdict): + return self._xmlrpc_proxy.User.get(paramdict) + def user_login(self, paramdict): + return self._xmlrpc_proxy.User.login(paramdict) + def user_logout(self): + return self._xmlrpc_proxy.User.logout() + def user_update(self, paramdict): + return self._xmlrpc_proxy.User.update(paramdict) diff --git a/bin/bugzilla b/bugzilla/_cli.py similarity index 52% rename from bin/bugzilla rename to bugzilla/_cli.py index 38588257..02d6a367 100755 --- a/bin/bugzilla +++ b/bugzilla/_cli.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # bugzilla - a commandline frontend for the python bugzilla module # @@ -6,104 +6,83 @@ # Author: Will Woods # Author: Cole Robinson # -# 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 __future__ import print_function +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +import argparse +import base64 +import datetime +import errno +import json import locale from logging import getLogger, DEBUG, INFO, WARN, StreamHandler, Formatter -import argparse import os import re import socket import sys import tempfile - -if sys.version_info[0] >= 3: - # pylint: disable=F0401,W0622,E0611 - from xmlrpc.client import Fault, ProtocolError - from urllib.parse import urlparse - basestring = (str, bytes) -else: - from xmlrpclib import Fault, ProtocolError - from urlparse import urlparse +import urllib.parse +import xmlrpc.client import requests.exceptions import bugzilla -DEFAULT_BZ = 'https://bugzilla.redhat.com/xmlrpc.cgi' -_is_unittest = bool(os.getenv("__BUGZILLA_UNITTEST")) -_is_unittest_debug = bool(os.getenv("__BUGZILLA_UNITTEST_DEBUG")) +DEFAULT_BZ = 'https://bugzilla.redhat.com' + format_field_re = re.compile("%{([a-z0-9_]+)(?::([^}]*))?}") log = getLogger(bugzilla.__name__) -handler = StreamHandler(sys.stderr) -handler.setFormatter(Formatter( - "[%(asctime)s] %(levelname)s (%(module)s:%(lineno)d) %(message)s", - "%H:%M:%S")) -log.addHandler(handler) ################ # Util helpers # ################ -def to_encoding(ustring): - string = '' - if isinstance(ustring, basestring): - string = ustring - elif ustring is not None: - string = str(ustring) - - if sys.version_info[0] >= 3: - return string - - preferred = locale.getpreferredencoding() - if _is_unittest: - preferred = "UTF-8" - return string.encode(preferred, 'replace') +def _is_unittest_debug(): + return bool(os.getenv("__BUGZILLA_UNITTEST_DEBUG")) def open_without_clobber(name, *args): - '''Try to open the given file with the given mode; if that filename exists, - try "name.1", "name.2", etc. until we find an unused filename.''' + """ + Try to open the given file with the given mode; if that filename exists, + try "name.1", "name.2", etc. until we find an unused filename. + """ fd = None count = 1 orig_name = name while fd is None: try: fd = os.open(name, os.O_CREAT | os.O_EXCL, 0o666) - except OSError: - err = sys.exc_info()[1] - if err.errno == os.errno.EEXIST: + except OSError as err: + if err.errno == errno.EEXIST: name = "%s.%i" % (orig_name, count) count += 1 - else: - raise IOError(err.errno, err.strerror, err.filename) + else: # pragma: no cover + raise IOError(err.errno, err.strerror, err.filename) from None fobj = open(name, *args) if fd != fobj.fileno(): os.close(fd) return fobj -def get_default_url(): - """ - Grab a default URL from bugzillarc [DEFAULT] url=X - """ - from bugzilla.base import _open_bugzillarc - cfg = _open_bugzillarc() - if cfg: - cfgurl = cfg.defaults().get("url", None) - if cfgurl is not None: - log.debug("bugzillarc: found cli url=%s", cfgurl) - return cfgurl - return DEFAULT_BZ +def setup_logging(debug, verbose): + handler = StreamHandler(sys.stderr) + handler.setFormatter(Formatter( + "[%(asctime)s] %(levelname)s (%(module)s:%(lineno)d) %(message)s", + "%H:%M:%S")) + log.addHandler(handler) + + if debug: + log.setLevel(DEBUG) + elif verbose: + log.setLevel(INFO) + else: + log.setLevel(WARN) + + if _is_unittest_debug(): + log.setLevel(DEBUG) # pragma: no cover ################## @@ -114,20 +93,27 @@ def _setup_root_parser(): epilog = 'Try "bugzilla COMMAND --help" for command-specific help.' p = argparse.ArgumentParser(epilog=epilog) - default_url = get_default_url() + default_url = bugzilla.Bugzilla.get_rcfile_default_url() + if not default_url: + default_url = DEFAULT_BZ # General bugzilla connection options p.add_argument('--bugzilla', default=default_url, - help="bugzilla XMLRPC URI. default: %s" % default_url) + help="bugzilla URI. default: %s" % default_url) p.add_argument("--nosslverify", dest="sslverify", action="store_false", default=True, help="Don't error on invalid bugzilla SSL certificate") + p.add_argument('--cert', + help="client side certificate file needed by the webserver") p.add_argument('--login', action="store_true", help='Run interactive "login" before performing the ' 'specified command.') p.add_argument('--username', help="Log in with this username") p.add_argument('--password', help="Log in with this password") + p.add_argument('--restrict-login', action="store_true", + help="The session (login token) will be restricted to " + "the current IP address.") p.add_argument('--ensure-logged-in', action="store_true", help="Raise an error if we aren't logged in to bugzilla. " @@ -139,8 +125,7 @@ def _setup_root_parser(): help="Don't save any bugzilla cookies or tokens to disk, and " "don't use any pre-existing credentials.") - p.add_argument('--cookiefile', default=None, - help="cookie file to use for bugzilla authentication") + p.add_argument('--cookiefile', default=None, help=argparse.SUPPRESS) p.add_argument('--tokenfile', default=None, help="token file to use for bugzilla authentication") @@ -173,8 +158,26 @@ def _parser_add_output_options(p): outg.add_argument('--oneline', action='store_const', dest='output', const='oneline', help="one line summary of the bug (useful for scripts)") + outg.add_argument('--json', action='store_const', dest='output', + const='json', help="output contents in json format") + outg.add_argument("--includefield", action="append", + help="Pass the field name to bugzilla include_fields list. " + "Only the fields passed to include_fields are returned " + "by the bugzilla server. " + "This can be specified multiple times.") + outg.add_argument("--extrafield", action="append", + help="Pass the field name to bugzilla extra_fields list. " + "When used with --json this can be used to request " + "bugzilla to return values for non-default fields. " + "This can be specified multiple times.") + outg.add_argument("--excludefield", action="append", + help="Pass the field name to bugzilla exclude_fields list. " + "When used with --json this can be used to request " + "bugzilla to not return values for a field. " + "This can be specified multiple times.") outg.add_argument('--raw', action='store_const', dest='output', - const='raw', help="raw output of the bugzilla contents") + const='raw', help="raw output of the bugzilla contents. This " + "format is unstable and difficult to parse. Use --json instead.") outg.add_argument('--outputformat', help="Print output in the form given. " "You can use RPM-style tags that match bug " @@ -182,6 +185,19 @@ def _parser_add_output_options(p): "section 'Output options' for more details.") +def _parser_add_field_passthrough_opts(p): + p.add_argument('--field', + metavar="FIELD=VALUE", action="append", dest="fields", + help="Manually specify a bugzilla API field. FIELD is " + "the raw name used by the bugzilla instance. For example, if your " + "bugzilla instance has a custom field cf_my_field, do:\n" + " --field cf_my_field=VALUE") + p.add_argument('--field-json', + metavar="JSONSTRING", action="append", dest="field_jsons", + help="Specify --field data as a JSON string. Example: --field-json " + '\'{"cf_my_field": "VALUE", "cf_array_field": [1, 2]}\'') + + def _parser_add_bz_fields(rootp, command): cmd_new = (command == "new") cmd_query = (command == "query") @@ -200,6 +216,9 @@ def _parser_add_bz_fields(rootp, command): p.add_argument('-c', '--component', help="Component name") p.add_argument('-t', '--summary', '--short_desc', help="Bug summary") p.add_argument('-l', '--comment', '--long_desc', help=comment_help) + if not cmd_query: + p.add_argument("--comment-tag", action="append", + help="Comment tag for the new comment") p.add_argument("--sub-component", action="append", help="RHBZ sub component field") p.add_argument('-o', '--os', help="Operating system") @@ -225,13 +244,19 @@ def _parser_add_bz_fields(rootp, command): p.add_argument('--cc', action="append", help="CC list") p.add_argument('-a', '--assigned_to', '--assignee', help="Bug assignee") p.add_argument('-q', '--qa_contact', help='QA contact') + if cmd_modify: + p.add_argument("--minor-update", action="store_true", + help="Request bugzilla to not send any " + "email about this change") if not cmd_new: p.add_argument('-f', '--flag', action='append', help="Bug flags state. Ex:\n" " --flag needinfo?\n" - " --flag dev_ack+") - p.add_argument("--tags", action="append", help="Tags field.") + " --flag dev_ack+ \n" + " clear with --flag needinfoX") + p.add_argument("--tags", action="append", + help="Tags/Personal Tags field.") p.add_argument('-w', "--whiteboard", '--status_whiteboard', action="append", help='Whiteboard field') @@ -244,17 +269,7 @@ def _parser_add_bz_fields(rootp, command): p.add_argument('-F', '--fixed_in', help="RHBZ 'Fixed in version' field") - # Put this at the end, so it sticks out more - p.add_argument('--field', - metavar="FIELD=VALUE", action="append", dest="fields", - help="Manually specify a bugzilla XMLRPC field. FIELD is " - "the raw name used by the bugzilla instance. For example if your " - "bugzilla instance has a custom field cf_my_field, do:\n" - " --field cf_my_field=VALUE") - - # Used by unit tests, not for end user consumption - p.add_argument('--test-return-result', action="store_true", - help=argparse.SUPPRESS) + _parser_add_field_passthrough_opts(p) if not cmd_modify: _parser_add_output_options(rootp) @@ -269,6 +284,9 @@ def _setup_action_new_parser(subparsers): p = subparsers.add_parser("new", description=description) _parser_add_bz_fields(p, "new") + g = p.add_argument_group("'new' specific options") + g.add_argument('--private', action='store_true', default=False, + help='Mark new comment as private') def _setup_action_query_parser(subparsers): @@ -315,10 +333,6 @@ def _setup_action_query_parser(subparsers): help=argparse.SUPPRESS) p.add_argument('-W', '--status_whiteboard_type', help=argparse.SUPPRESS) - p.add_argument('-B', '--booleantype', - help=argparse.SUPPRESS) - p.add_argument('--boolean_query', action="append", - help=argparse.SUPPRESS) p.add_argument('--fixed_in_type', help=argparse.SUPPRESS) @@ -327,14 +341,18 @@ def _setup_action_info_parser(subparsers): "bugzilla server.") p = subparsers.add_parser("info", description=description) - p.add_argument('-p', '--products', action='store_true', + x = p.add_mutually_exclusive_group(required=True) + x.add_argument('-p', '--products', action='store_true', help='Get a list of products') - p.add_argument('-c', '--components', metavar="PRODUCT", + x.add_argument('-c', '--components', metavar="PRODUCT", help='List the components in the given product') - p.add_argument('-o', '--component_owners', metavar="PRODUCT", + x.add_argument('-o', '--component_owners', metavar="PRODUCT", help='List components (and their owners)') - p.add_argument('-v', '--versions', metavar="VERSION", + x.add_argument('-v', '--versions', metavar="PRODUCT", help='List the versions for the given product') + p.add_argument('--active-components', action="store_true", + help='Only show active components. Combine with --components*') + def _setup_action_modify_parser(subparsers): @@ -350,6 +368,7 @@ def _setup_action_modify_parser(subparsers): _parser_add_bz_fields(p, "modify") g = p.add_argument_group("'modify' specific options") + g.add_argument("ids", nargs="+", help="Bug IDs to modify") g.add_argument('-k', '--close', metavar="RESOLUTION", help='Close with the given resolution (WONTFIX, NOTABUG, etc.)') g.add_argument('-d', '--dupeid', metavar="ORIGINAL", @@ -365,11 +384,12 @@ def _setup_action_modify_parser(subparsers): def _setup_action_attach_parser(subparsers): usage = """ bugzilla attach --file=FILE --desc=DESC [--type=TYPE] BUGID [BUGID...] -bugzilla attach --get=ATTACHID --getall=BUGID [...] +bugzilla attach --get=ATTACHID --getall=BUGID [--ignore-obsolete] [...] bugzilla attach --type=TYPE BUGID [BUGID...]""" description = "Attach files or download attachments." p = subparsers.add_parser("attach", description=description, usage=usage) + p.add_argument("ids", nargs="*", help="BUGID references") p.add_argument('-f', '--file', metavar="FILENAME", help='File to attach, or filename for data provided on stdin') p.add_argument('-d', '--description', '--summary', @@ -381,17 +401,37 @@ def _setup_action_attach_parser(subparsers): default=[], help="Download the attachment with the given ID") p.add_argument("--getall", "--get-all", metavar="BUGID", action="append", default=[], help="Download all attachments on the given bug") + p.add_argument('--ignore-obsolete', action="store_true", + help='Do not download attachments marked as obsolete.') + p.add_argument('-l', '--comment', '--long_desc', + help="Add comment with attachment") + p.add_argument('--private', action='store_true', default=False, + help='Mark new comment as private') + + _parser_add_field_passthrough_opts(p) def _setup_action_login_parser(subparsers): - usage = 'bugzilla login [username [password]]' - description = "Log into bugzilla and save a login cookie or token." - subparsers.add_parser("login", description=description, usage=usage) + usage = 'bugzilla login [--api-key] [username [password]]' + description = """Log into bugzilla and save a login cookie or token. +Note: These tokens are short-lived, and future Bugzilla versions will no +longer support token authentication at all. Please use a +~/.config/python-bugzilla/bugzillarc file with an API key instead, or +use 'bugzilla login --api-key' and we will save it for you.""" + p = subparsers.add_parser("login", description=description, usage=usage) + p.add_argument('--api-key', action='store_true', default=False, + help='Prompt for and save an API key into bugzillarc, ' + 'rather than prompt for username and password.') + p.add_argument("pos_username", nargs="?", help="Optional username ", + metavar="username") + p.add_argument("pos_password", nargs="?", help="Optional password ", + metavar="password") def setup_parser(): rootparser = _setup_root_parser() - subparsers = rootparser.add_subparsers(dest="command_name") + subparsers = rootparser.add_subparsers(dest="command") + subparsers.required = True _setup_action_new_parser(subparsers) _setup_action_query_parser(subparsers) _setup_action_info_parser(subparsers) @@ -405,18 +445,28 @@ def setup_parser(): # Command routines # #################### -def _merge_field_opts(query, opt, parser): - # Add any custom fields if specified - if opt.fields is None: - return +def _merge_field_opts(query, fields, field_jsons, parser): + values = {} - for f in opt.fields: + # Add any custom fields if specified + for f in (fields or []): try: f, v = f.split('=', 1) - query[f] = v - except: + values[f] = v + except Exception: parser.error("Invalid field argument provided: %s" % (f)) + for j in (field_jsons or []): + try: + jvalues = json.loads(j) + values.update(jvalues) + except Exception as e: + parser.error("Invalid field-json value=%s: %s" % (j, e)) + + if values: + log.debug("parsed --field* values: %s", values) + query.update(values) + def _do_query(bz, opt, parser): q = {} @@ -471,7 +521,7 @@ def _do_query(bz, opt, parser): setattr(opt, optname, val.split(",")) include_fields = None - if opt.output == 'raw': + if opt.output in ['raw', 'json']: # 'raw' always does a getbug() call anyways, so just ask for ID back include_fields = ['id'] @@ -496,55 +546,88 @@ def _do_query(bz, opt, parser): if include_fields is not None: include_fields.sort() - built_query = bz.build_query( - product=opt.product or None, - component=opt.component or None, - sub_component=opt.sub_component or None, - version=opt.version or None, - reporter=opt.reporter or None, - bug_id=opt.id or None, - short_desc=opt.summary or None, - long_desc=opt.comment or None, - cc=opt.cc or None, - assigned_to=opt.assigned_to or None, - qa_contact=opt.qa_contact or None, - status=opt.status or None, - blocked=opt.blocked or None, - dependson=opt.dependson or None, - keywords=opt.keywords or None, - keywords_type=opt.keywords_type or None, - url=opt.url or None, - url_type=opt.url_type or None, - status_whiteboard=opt.whiteboard or None, - status_whiteboard_type=opt.status_whiteboard_type or None, - fixed_in=opt.fixed_in or None, - fixed_in_type=opt.fixed_in_type or None, - flag=opt.flag or None, - alias=opt.alias or None, - qa_whiteboard=opt.qa_whiteboard or None, - devel_whiteboard=opt.devel_whiteboard or None, - boolean_query=opt.boolean_query or None, - bug_severity=opt.severity or None, - priority=opt.priority or None, - target_release=opt.target_release or None, - target_milestone=opt.target_milestone or None, - emailtype=opt.emailtype or None, - booleantype=opt.booleantype or None, - include_fields=include_fields, - quicksearch=opt.quicksearch or None, - savedsearch=opt.savedsearch or None, - savedsearch_sharer_id=opt.savedsearch_sharer_id or None, - tags=opt.tags or None) - - _merge_field_opts(built_query, opt, parser) + kwopts = {} + if opt.product: + kwopts["product"] = opt.product + if opt.component: + kwopts["component"] = opt.component + if opt.sub_component: + kwopts["sub_component"] = opt.sub_component + if opt.version: + kwopts["version"] = opt.version + if opt.reporter: + kwopts["reporter"] = opt.reporter + if opt.id: + kwopts["bug_id"] = opt.id + if opt.summary: + kwopts["short_desc"] = opt.summary + if opt.comment: + kwopts["long_desc"] = opt.comment + if opt.cc: + kwopts["cc"] = opt.cc + if opt.assigned_to: + kwopts["assigned_to"] = opt.assigned_to + if opt.qa_contact: + kwopts["qa_contact"] = opt.qa_contact + if opt.status: + kwopts["status"] = opt.status + if opt.blocked: + kwopts["blocked"] = opt.blocked + if opt.dependson: + kwopts["dependson"] = opt.dependson + if opt.keywords: + kwopts["keywords"] = opt.keywords + if opt.keywords_type: + kwopts["keywords_type"] = opt.keywords_type + if opt.url: + kwopts["url"] = opt.url + if opt.url_type: + kwopts["url_type"] = opt.url_type + if opt.whiteboard: + kwopts["status_whiteboard"] = opt.whiteboard + if opt.status_whiteboard_type: + kwopts["status_whiteboard_type"] = opt.status_whiteboard_type + if opt.fixed_in: + kwopts["fixed_in"] = opt.fixed_in + if opt.fixed_in_type: + kwopts["fixed_in_type"] = opt.fixed_in_type + if opt.flag: + kwopts["flag"] = opt.flag + if opt.alias: + kwopts["alias"] = opt.alias + if opt.qa_whiteboard: + kwopts["qa_whiteboard"] = opt.qa_whiteboard + if opt.devel_whiteboard: + kwopts["devel_whiteboard"] = opt.devel_whiteboard + if opt.severity: + kwopts["bug_severity"] = opt.severity + if opt.priority: + kwopts["priority"] = opt.priority + if opt.target_release: + kwopts["target_release"] = opt.target_release + if opt.target_milestone: + kwopts["target_milestone"] = opt.target_milestone + if opt.emailtype: + kwopts["emailtype"] = opt.emailtype + if include_fields: + kwopts["include_fields"] = include_fields + if opt.quicksearch: + kwopts["quicksearch"] = opt.quicksearch + if opt.savedsearch: + kwopts["savedsearch"] = opt.savedsearch + if opt.savedsearch_sharer_id: + kwopts["savedsearch_sharer_id"] = opt.savedsearch_sharer_id + if opt.tags: + kwopts["tags"] = opt.tags + + built_query = bz.build_query(**kwopts) + _merge_field_opts(built_query, opt.fields, opt.field_jsons, parser) built_query.update(q) q = built_query - if not q: + if not q: # pragma: no cover parser.error("'query' command requires additional arguments") - if opt.test_return_result: - return q return bz.query(q) @@ -554,43 +637,52 @@ def _do_info(bz, opt): """ # All these commands call getproducts internally, so do it up front # with minimal include_fields for speed + def _filter_components(compdetails): + ret = {} + for k, v in compdetails.items(): + if v.get("is_active", True): + ret[k] = v + return ret + + productname = (opt.components or opt.component_owners or opt.versions) + fastcomponents = (opt.components and not opt.active_components) + include_fields = ["name", "id"] + if opt.components or opt.component_owners: + include_fields += ["components.name"] + if opt.component_owners: + include_fields += ["components.default_assigned_to"] + if opt.active_components: + include_fields += ["components.is_active"] + if opt.versions: - include_fields.append("versions") - products = bz.getproducts(include_fields=include_fields) + include_fields += ["versions"] + + bz.refresh_products(names=productname and [productname] or None, + include_fields=include_fields) if opt.products: - for name in sorted([p["name"] for p in products]): + for name in sorted([p["name"] for p in bz.getproducts()]): + print(name) + + elif fastcomponents: + for name in sorted(bz.getcomponents(productname)): print(name) - if opt.components: - for name in sorted(bz.getcomponents(opt.components)): + elif opt.components: + details = bz.getcomponentsdetails(productname) + for name in sorted(_filter_components(details)): print(name) - if opt.component_owners: - # Looking up this info for rhbz 'Fedora' product is sloooow - # since there are so many components. So delay getting this - # info until as late as possible - bz.refresh_products(names=[opt.component_owners], - include_fields=include_fields + [ - "components.default_assigned_to", - "components.default_qa_contact", - "components.name", - "components.description"]) - - component_details = bz.getcomponentsdetails(opt.component_owners) - for c in sorted(component_details): - print(to_encoding(u"%s: %s" % - (c, component_details[c]['initialowner']))) + elif opt.versions: + proddict = bz.getproducts()[0] + for v in proddict['versions']: + print(str(v["name"] or '')) - if opt.versions: - for p in products: - if p['name'] != opt.versions: - continue - if "versions" in p: - for v in p['versions']: - print(to_encoding(v["name"])) - break + elif opt.component_owners: + details = bz.getcomponentsdetails(productname) + for c in sorted(_filter_components(details)): + print("%s: %s" % (c, details[c]['default_assigned_to'])) def _convert_to_outputformat(output): @@ -621,84 +713,139 @@ def _convert_to_outputformat(output): fmt += "#%{bug_id} %{status} %{assigned_to} %{component}\t" fmt += "[%{target_milestone}] %{flags} %{cve}" - else: + else: # pragma: no cover raise RuntimeError("Unknown output type '%s'" % output) return fmt -def _format_output(bz, opt, buglist): - if opt.output == 'raw': - buglist = bz.getbugs([b.bug_id for b in buglist]) - for b in buglist: - print("Bugzilla %s: " % b.bug_id) - for attrname in sorted(b.__dict__): - print(to_encoding(u"ATTRIBUTE[%s]: %s" % - (attrname, b.__dict__[attrname]))) - print("\n\n") - return +def _xmlrpc_converter(obj): + if "DateTime" in str(obj.__class__): + # xmlrpc DateTime object. Convert to date format that + # bugzilla REST API outputs + dobj = datetime.datetime.strptime(str(obj), '%Y%m%dT%H:%M:%S') + return dobj.isoformat() + "Z" + if "Binary" in str(obj.__class__): + # xmlrpc Binary object. Convert to base64 + return base64.b64encode(obj.data).decode("utf-8") + raise RuntimeError( + "Unexpected JSON conversion class=%s" % obj.__class__) + - def bug_field(matchobj): - # whiteboard and flag allow doing - # %{whiteboard:devel} and %{flag:needinfo} - # That's what 'rest' matches - (fieldname, rest) = matchobj.groups() - - if fieldname == "whiteboard" and rest: - fieldname = rest + "_" + fieldname - - if fieldname == "flag" and rest: - val = b.get_flag_status(rest) - - elif fieldname == "flags" or fieldname == "flags_requestee": - tmpstr = [] - for f in getattr(b, "flags", []): - requestee = f.get('requestee', "") - if fieldname == "flags": - requestee = "" - if fieldname == "flags_requestee": - if requestee == "": - continue - tmpstr.append("%s" % requestee) - else: - tmpstr.append("%s%s%s" % - (f['name'], f['status'], requestee)) - - val = ",".join(tmpstr) - - elif fieldname == "cve": - cves = [] - for key in getattr(b, "keywords", []): - # grab CVE from keywords and blockers - if key.find("Security") == -1: +def _format_output_json(buglist): + out = {"bugs": [b.get_raw_data() for b in buglist]} + s = json.dumps(out, default=_xmlrpc_converter, indent=2, sort_keys=True) + print(s) + + +def _format_output_raw(buglist): + for b in buglist: + print("Bugzilla %s: " % b.bug_id) + SKIP_NAMES = ["bugzilla"] + for attrname in sorted(b.__dict__): + if attrname in SKIP_NAMES: + continue + if attrname.startswith("_"): + continue + print("ATTRIBUTE[%s]: %s" % (attrname, b.__dict__[attrname])) + print("\n\n") + + +def _bug_field_repl_cb(bz, b, matchobj): + # whiteboard and flag allow doing + # %{whiteboard:devel} and %{flag:needinfo} + # That's what 'rest' matches + (fieldname, rest) = matchobj.groups() + + if fieldname == "whiteboard" and rest: + fieldname = rest + "_" + fieldname + + if fieldname == "flag" and rest: + val = b.get_flag_status(rest) + + elif fieldname in ["flags", "flags_requestee"]: + tmpstr = [] + for f in getattr(b, "flags", []): + requestee = f.get('requestee', "") + if fieldname == "flags": + requestee = "" + if fieldname == "flags_requestee": + if requestee == "": continue - for bl in b.blocks: - cvebug = bz.getbug(bl) - for cb in cvebug.alias: - if cb.find("CVE") == -1: - continue - if cb.strip() not in cves: - cves.append(cb) - val = ",".join(cves) - - elif fieldname == "comments": - val = "" - for c in getattr(b, "comments", []): - val += ("\n* %s - %s:\n%s\n" % - (c['time'], c.get("creator", ""), c['text'])) - - elif fieldname == "__unicode__": - val = b.__unicode__() - else: - val = getattr(b, fieldname, "") + tmpstr.append("%s" % requestee) + else: + tmpstr.append("%s%s%s" % + (f['name'], f['status'], requestee)) + + val = ",".join(tmpstr) + + elif fieldname == "cve": + cves = [] + for key in getattr(b, "keywords", []): + # grab CVE from keywords and blockers + if key.find("Security") == -1: + continue + for bl in b.blocks: + cvebug = bz.getbug(bl) + for cb in cvebug.alias: + if (cb.find("CVE") != -1 and + cb.strip() not in cves): + cves.append(cb) + val = ",".join(cves) + + elif fieldname == "comments": + val = "" + for c in getattr(b, "comments", []): + val += ("\n* %s - %s:\n%s\n" % (c['time'], + c.get("creator", c.get("author", "")), c['text'])) + + elif fieldname == "external_bugs": + val = "" + for e in getattr(b, "external_bugs", []): + url = e["type"]["full_url"].replace("%id%", e["ext_bz_bug_id"]) + if not val: + val += "\n" + val += "External bug: %s\n" % url + + elif fieldname == "__unicode__": + val = b.__unicode__() + else: + val = getattr(b, fieldname, "") - vallist = isinstance(val, list) and val or [val] - val = ','.join([to_encoding(v) for v in vallist]) + vallist = isinstance(val, list) and val or [val] + val = ','.join([str(v or '') for v in vallist]) - return val + return val + + +def _format_output(bz, opt, buglist): + if opt.output in ['raw', 'json']: + include_fields = None + exclude_fields = None + extra_fields = None + + if opt.includefield: + include_fields = opt.includefield + if opt.excludefield: + exclude_fields = opt.excludefield + if opt.extrafield: + extra_fields = opt.extrafield + + buglist = bz.getbugs([b.bug_id for b in buglist], + include_fields=include_fields, + exclude_fields=exclude_fields, + extra_fields=extra_fields) + if opt.output == 'json': + _format_output_json(buglist) + if opt.output == 'raw': + _format_output_raw(buglist) + return for b in buglist: - print(format_field_re.sub(bug_field, opt.outputformat)) + # pylint: disable=cell-var-from-loop + def cb(matchobj): + return _bug_field_repl_cb(bz, b, matchobj) + print(format_field_re.sub(cb, opt.outputformat)) def _parse_triset(vallist, checkplus=True, checkminus=True, checkequal=True, @@ -736,40 +883,60 @@ def parse_multi(val): return _parse_triset(val, checkplus=False, checkminus=False, checkequal=False, splitcomma=True)[0] - ret = bz.build_createbug( - blocks=parse_multi(opt.blocked) or None, - cc=parse_multi(opt.cc) or None, - component=opt.component or None, - depends_on=parse_multi(opt.dependson) or None, - description=opt.comment or None, - groups=parse_multi(opt.groups) or None, - keywords=parse_multi(opt.keywords) or None, - op_sys=opt.os or None, - platform=opt.arch or None, - priority=opt.priority or None, - product=opt.product or None, - severity=opt.severity or None, - summary=opt.summary or None, - url=opt.url or None, - version=opt.version or None, - assigned_to=opt.assigned_to or None, - qa_contact=opt.qa_contact or None, - sub_component=opt.sub_component or None, - alias=opt.alias or None, - ) - - _merge_field_opts(ret, opt, parser) - - if opt.test_return_result: - return ret + kwopts = {} + if opt.blocked: + kwopts["blocks"] = parse_multi(opt.blocked) + if opt.cc: + kwopts["cc"] = parse_multi(opt.cc) + if opt.component: + kwopts["component"] = opt.component + if opt.dependson: + kwopts["depends_on"] = parse_multi(opt.dependson) + if opt.comment: + kwopts["description"] = opt.comment + if opt.groups: + kwopts["groups"] = parse_multi(opt.groups) + if opt.keywords: + kwopts["keywords"] = parse_multi(opt.keywords) + if opt.os: + kwopts["op_sys"] = opt.os + if opt.arch: + kwopts["platform"] = opt.arch + if opt.priority: + kwopts["priority"] = opt.priority + if opt.product: + kwopts["product"] = opt.product + if opt.severity: + kwopts["severity"] = opt.severity + if opt.summary: + kwopts["summary"] = opt.summary + if opt.url: + kwopts["url"] = opt.url + if opt.version: + kwopts["version"] = opt.version + if opt.assigned_to: + kwopts["assigned_to"] = opt.assigned_to + if opt.qa_contact: + kwopts["qa_contact"] = opt.qa_contact + if opt.sub_component: + kwopts["sub_component"] = opt.sub_component + if opt.alias: + kwopts["alias"] = opt.alias + if opt.comment_tag: + kwopts["comment_tags"] = opt.comment_tag + if opt.private: + kwopts["comment_private"] = opt.private + + ret = bz.build_createbug(**kwopts) + _merge_field_opts(ret, opt.fields, opt.field_jsons, parser) b = bz.createbug(ret) b.refresh() return [b] -def _do_modify(bz, parser, opt, args): - bugid_list = [bugid for a in args for bugid in a.split(',')] +def _do_modify(bz, parser, opt): + bugid_list = [bugid for a in opt.ids for bugid in a.split(',')] add_wb, rm_wb, set_wb = _parse_triset(opt.whiteboard) add_devwb, rm_devwb, set_devwb = _parse_triset(opt.devel_whiteboard) @@ -799,49 +966,96 @@ def _do_modify(bz, parser, opt, args): for f in opt.flag: flags.append({"name": f[:-1], "status": f[-1]}) - update = bz.build_update( - assigned_to=opt.assigned_to or None, - comment=opt.comment or None, - comment_private=opt.private or None, - component=opt.component or None, - product=opt.product or None, - blocks_add=add_blk or None, - blocks_remove=rm_blk or None, - blocks_set=set_blk, - url=opt.url or None, - cc_add=add_cc or None, - cc_remove=rm_cc or None, - depends_on_add=add_deps or None, - depends_on_remove=rm_deps or None, - depends_on_set=set_deps, - groups_add=add_groups or None, - groups_remove=rm_groups or None, - keywords_add=add_key or None, - keywords_remove=rm_key or None, - keywords_set=set_key, - op_sys=opt.os or None, - platform=opt.arch or None, - priority=opt.priority or None, - qa_contact=opt.qa_contact or None, - severity=opt.severity or None, - status=status, - summary=opt.summary or None, - version=opt.version or None, - reset_assigned_to=opt.reset_assignee or None, - reset_qa_contact=opt.reset_qa_contact or None, - resolution=opt.close or None, - target_release=opt.target_release or None, - target_milestone=opt.target_milestone or None, - dupe_of=opt.dupeid or None, - fixed_in=opt.fixed_in or None, - whiteboard=set_wb and set_wb[0] or None, - devel_whiteboard=set_devwb and set_devwb[0] or None, - internal_whiteboard=set_intwb and set_intwb[0] or None, - qa_whiteboard=set_qawb and set_qawb[0] or None, - sub_component=opt.sub_component or None, - alias=opt.alias or None, - flags=flags or None, - ) + update_opts = {} + + if opt.assigned_to: + update_opts["assigned_to"] = opt.assigned_to + if opt.comment: + update_opts["comment"] = opt.comment + if opt.private: + update_opts["comment_private"] = opt.private + if opt.component: + update_opts["component"] = opt.component + if opt.product: + update_opts["product"] = opt.product + if add_blk: + update_opts["blocks_add"] = add_blk + if rm_blk: + update_opts["blocks_remove"] = rm_blk + if set_blk is not None: + update_opts["blocks_set"] = set_blk + if opt.url: + update_opts["url"] = opt.url + if add_cc: + update_opts["cc_add"] = add_cc + if rm_cc: + update_opts["cc_remove"] = rm_cc + if add_deps: + update_opts["depends_on_add"] = add_deps + if rm_deps: + update_opts["depends_on_remove"] = rm_deps + if set_deps is not None: + update_opts["depends_on_set"] = set_deps + if add_groups: + update_opts["groups_add"] = add_groups + if rm_groups: + update_opts["groups_remove"] = rm_groups + if add_key: + update_opts["keywords_add"] = add_key + if rm_key: + update_opts["keywords_remove"] = rm_key + if set_key is not None: + update_opts["keywords_set"] = set_key + if opt.os: + update_opts["op_sys"] = opt.os + if opt.arch: + update_opts["platform"] = opt.arch + if opt.priority: + update_opts["priority"] = opt.priority + if opt.qa_contact: + update_opts["qa_contact"] = opt.qa_contact + if opt.severity: + update_opts["severity"] = opt.severity + if status: + update_opts["status"] = status + if opt.summary: + update_opts["summary"] = opt.summary + if opt.version: + update_opts["version"] = opt.version + if opt.reset_assignee: + update_opts["reset_assigned_to"] = opt.reset_assignee + if opt.reset_qa_contact: + update_opts["reset_qa_contact"] = opt.reset_qa_contact + if opt.close: + update_opts["resolution"] = opt.close + if opt.target_release: + update_opts["target_release"] = opt.target_release + if opt.target_milestone: + update_opts["target_milestone"] = opt.target_milestone + if opt.dupeid: + update_opts["dupe_of"] = opt.dupeid + if opt.fixed_in: + update_opts["fixed_in"] = opt.fixed_in + if set_wb and set_wb[0]: + update_opts["whiteboard"] = set_wb and set_wb[0] + if set_devwb and set_devwb[0]: + update_opts["devel_whiteboard"] = set_devwb and set_devwb[0] + if set_intwb and set_intwb[0]: + update_opts["internal_whiteboard"] = set_intwb and set_intwb[0] + if set_qawb and set_qawb[0]: + update_opts["qa_whiteboard"] = set_qawb and set_qawb[0] + if opt.sub_component: + update_opts["sub_component"] = opt.sub_component + if opt.alias: + update_opts["alias"] = opt.alias + if flags: + update_opts["flags"] = flags + if opt.comment_tag: + update_opts["comment_tags"] = opt.comment_tag + if opt.minor_update: + update_opts["minor_update"] = opt.minor_update + + update = bz.build_update(**update_opts) # We make this a little convoluted to facilitate unit testing wbmap = { @@ -853,9 +1067,9 @@ def _do_modify(bz, parser, opt, args): for k, v in wbmap.copy().items(): if not v[0] and not v[1]: - del(wbmap[k]) + del wbmap[k] - _merge_field_opts(update, opt, parser) + _merge_field_opts(update, opt.fields, opt.field_jsons, parser) log.debug("update bug dict=%s", update) log.debug("update whiteboard dict=%s", wbmap) @@ -863,9 +1077,6 @@ def _do_modify(bz, parser, opt, args): if not any([update, wbmap, add_tags, rm_tags]): parser.error("'modify' command requires additional arguments") - if opt.test_return_result: - return (update, wbmap, add_tags, rm_tags) - if add_tags or rm_tags: ret = bz.update_tags(bugid_list, tags_add=add_tags, tags_remove=rm_tags) @@ -880,37 +1091,49 @@ def _do_modify(bz, parser, opt, args): # Now for the things we can't blindly batch. # Being able to prepend/append to whiteboards, which are just # plain string values, is an old rhbz semantic that we try to maintain - # here. This is a bit weird for traditional bugzilla XMLRPC + # here. This is a bit weird for traditional bugzilla API log.debug("Adjusting whiteboard fields one by one") for bug in bz.getbugs(bugid_list): - for wb, (add_list, rm_list) in wbmap.items(): + update_kwargs = {} + for wbkey, (add_list, rm_list) in wbmap.items(): + bugval = getattr(bug, wbkey) or "" for tag in add_list: - newval = getattr(bug, wb) or "" - if newval: - newval += " " - newval += tag - bz.update_bugs([bug.id], - bz.build_update(**{wb: newval})) + if bugval: + bugval += " " + bugval += tag for tag in rm_list: - newval = (getattr(bug, wb) or "").split() - for t in newval[:]: + bugsplit = bugval.split() + for t in bugsplit[:]: if t == tag: - newval.remove(t) - bz.update_bugs([bug.id], - bz.build_update(**{wb: " ".join(newval)})) + bugsplit.remove(t) + bugval = " ".join(bugsplit) + + update_kwargs[wbkey] = bugval + + bz.update_bugs([bug.id], bz.build_update(**update_kwargs)) + + +def _do_get_attach(bz, opt): + data = {} + def _process_attachment_data(_attlist): + for _att in _attlist: + data[_att["id"]] = _att -def _do_get_attach(bz, opt, parser, args): - if args: - parser.error("Extra args '%s' not used for getting attachments" % - args) + if opt.getall: + for attlist in bz.get_attachments(opt.getall, None)["bugs"].values(): + _process_attachment_data(attlist) + if opt.get: + _process_attachment_data( + bz.get_attachments(None, opt.get)["attachments"].values()) - for bug in bz.getbugs(opt.getall): - opt.get += bug.get_attachment_ids() + for attdata in data.values(): + is_obsolete = attdata.get("is_obsolete", None) == 1 + if opt.ignore_obsolete and is_obsolete: + continue - for attid in set(opt.get): - att = bz.openattachment(attid) + att = bz.openattachment_data(attdata) outfile = open_without_clobber(att.name, "wb") data = att.read(4096) while data: @@ -918,17 +1141,15 @@ def _do_get_attach(bz, opt, parser, args): data = att.read(4096) print("Wrote %s" % outfile.name) - return - -def _do_set_attach(bz, opt, parser, args): - if not args: +def _do_set_attach(bz, opt, parser): + if not opt.ids: parser.error("Bug ID must be specified for setting attachments") if sys.stdin.isatty(): if not opt.file: parser.error("--file must be specified") - fileobj = open(opt.file) + fileobj = open(opt.file, "rb") else: # piped input on stdin if not opt.desc: @@ -950,10 +1171,16 @@ def _do_set_attach(bz, opt, parser, args): kwargs["contenttype"] = opt.type if opt.type in ["text/x-patch"]: kwargs["ispatch"] = True + if opt.comment: + kwargs["comment"] = opt.comment + if opt.private: + kwargs["is_private"] = True desc = opt.desc or os.path.basename(fileobj.name) + _merge_field_opts(kwargs, opt.fields, opt.field_jsons, parser) + # Upload attachments - for bugid in args: + for bugid in opt.ids: attid = bz.attachfile(bugid, fileobj, desc, **kwargs) print("Created attachment %i on bug %s" % (attid, bugid)) @@ -971,19 +1198,22 @@ def _make_bz_instance(opt): cookiefile = None tokenfile = None + use_creds = False if opt.cache_credentials: cookiefile = opt.cookiefile or -1 tokenfile = opt.tokenfile or -1 + use_creds = True - bz = bugzilla.Bugzilla( + return bugzilla.Bugzilla( url=opt.bugzilla, cookiefile=cookiefile, tokenfile=tokenfile, - sslverify=opt.sslverify) - return bz + sslverify=opt.sslverify, + use_creds=use_creds, + cert=opt.cert) -def _handle_login(opt, parser, args, action, bz): +def _handle_login(opt, action, bz): """ Handle all login related bits """ @@ -991,23 +1221,23 @@ def _handle_login(opt, parser, args, action, bz): do_interactive_login = (is_login_command or opt.login or opt.username or opt.password) - - if is_login_command: - if len(args) == 2: - (opt.username, opt.password) = args - elif len(args) == 1: - (opt.username, ) = args - elif len(args) > 2: - parser.error("Too many arguments for login") + username = getattr(opt, "pos_username", None) or opt.username + password = getattr(opt, "pos_password", None) or opt.password + use_key = getattr(opt, "api_key", False) try: - if do_interactive_login: - if bz.url: - print("Logging into %s" % urlparse(bz.url)[1]) - bz.interactive_login( - opt.username, opt.password) - except bugzilla.BugzillaError: - print(str(sys.exc_info()[1])) + if use_key: + bz.interactive_save_api_key() + elif do_interactive_login: + if bz.api_key: + print("You already have an API key configured for %s" % bz.url) + print("There is no need to cache a login token. Exiting.") + sys.exit(0) + print("Logging into %s" % urllib.parse.urlparse(bz.url)[1]) + bz.interactive_login(username, password, + restrict_login=opt.restrict_login) + except bugzilla.BugzillaError as e: + print(str(e)) sys.exit(1) if opt.ensure_logged_in and not bz.logged_in: @@ -1016,33 +1246,17 @@ def _handle_login(opt, parser, args, action, bz): sys.exit(1) if is_login_command: - msg = "Login successful." - if bz.cookiefile or bz.tokenfile: - msg = "Login successful, token cache updated." - - print(msg) sys.exit(0) -def main(unittest_bz_instance=None): +def _main(unittest_bz_instance): parser = setup_parser() - opt, args = parser.parse_known_args() - action = opt.command_name - - if opt.debug: - log.setLevel(DEBUG) - elif opt.verbose: - log.setLevel(INFO) - else: - log.setLevel(WARN) - - if _is_unittest_debug: - log.setLevel(DEBUG) + opt = parser.parse_args() + action = opt.command + setup_logging(opt.debug, opt.verbose) log.debug("Launched with command line: %s", " ".join(sys.argv)) - - # Connect to bugzilla - log.info('Connecting to %s', opt.bugzilla) + log.debug("Bugzilla module: %s", bugzilla) if unittest_bz_instance: bz = unittest_bz_instance @@ -1050,7 +1264,7 @@ def main(unittest_bz_instance=None): bz = _make_bz_instance(opt) # Handle login options - _handle_login(opt, parser, args, action, bz) + _handle_login(opt, action, bz) ########################### @@ -1058,52 +1272,31 @@ def main(unittest_bz_instance=None): ########################### if hasattr(opt, "outputformat"): - if not opt.outputformat and opt.output not in ['raw', None]: + if not opt.outputformat and opt.output not in ['raw', 'json', None]: opt.outputformat = _convert_to_outputformat(opt.output) buglist = [] if action == 'info': - if args: - parser.error("Extra arguments '%s'" % args) - - if not (opt.products or - opt.components or - opt.component_owners or - opt.versions): - parser.error("'info' command requires additional arguments") - _do_info(bz, opt) elif action == 'query': - if args: - parser.error("Extra arguments '%s'" % args) - buglist = _do_query(bz, opt, parser) - if opt.test_return_result: - return buglist elif action == 'new': - if args: - parser.error("Extra arguments '%s'" % args) buglist = _do_new(bz, opt, parser) - if opt.test_return_result: - return buglist elif action == 'attach': if opt.get or opt.getall: - _do_get_attach(bz, opt, parser, args) + if opt.ids: + parser.error("Bug IDs '%s' not used for " + "getting attachments" % opt.ids) + _do_get_attach(bz, opt) else: - _do_set_attach(bz, opt, parser, args) + _do_set_attach(bz, opt, parser) elif action == 'modify': - if not args: - parser.error('No bug IDs given ' - '(maybe you forgot an argument somewhere?)') - - modout = _do_modify(bz, parser, opt, args) - if opt.test_return_result: - return modout - else: + _do_modify(bz, parser, opt) + else: # pragma: no cover raise RuntimeError("Unexpected action '%s'" % action) # If we're doing new/query/modify, output our results @@ -1111,32 +1304,20 @@ def main(unittest_bz_instance=None): _format_output(bz, opt, buglist) -if __name__ == '__main__': +def main(unittest_bz_instance=None): try: - main() + try: + return _main(unittest_bz_instance) + except (Exception, KeyboardInterrupt): + log.debug("", exc_info=True) + raise except KeyboardInterrupt: - log.debug("", exc_info=True) print("\nExited at user request.") sys.exit(1) - except (Fault, bugzilla.BugzillaError): - e = sys.exc_info()[1] - log.debug("", exc_info=True) + except (xmlrpc.client.Fault, bugzilla.BugzillaError) as e: print("\nServer error: %s" % str(e)) sys.exit(3) - except ProtocolError: - e = sys.exc_info()[1] - log.debug("", exc_info=True) - print("\nInvalid server response: %d %s" % (e.errcode, e.errmsg)) - # Detect redirect - redir = (e.headers and 'location' in e.headers) - if redir: - print("\nServer was attempting a redirect. Try: " - " bugzilla --bugzilla %s ..." % redir) - sys.exit(4) - except requests.exceptions.SSLError: - e = sys.exc_info()[1] - log.debug("", exc_info=True) - + except requests.exceptions.SSLError as e: # Give SSL recommendations print("SSL error: %s" % e) print("\nIf you trust the remote server, you can work " @@ -1145,8 +1326,12 @@ def main(unittest_bz_instance=None): sys.exit(4) except (socket.error, requests.exceptions.HTTPError, - requests.exceptions.ConnectionError): - e = sys.exc_info()[1] - log.debug("", exc_info=True) + requests.exceptions.ConnectionError, + requests.exceptions.InvalidURL, + xmlrpc.client.ProtocolError) as e: print("\nConnection lost/failed: %s" % str(e)) sys.exit(2) + + +def cli(): + main() diff --git a/bugzilla/_rhconverters.py b/bugzilla/_rhconverters.py new file mode 100644 index 00000000..fb371cda --- /dev/null +++ b/bugzilla/_rhconverters.py @@ -0,0 +1,128 @@ +# rhbugzilla.py - a Python interface to Red Hat Bugzilla using xmlrpclib. +# +# Copyright (C) 2008-2012 Red Hat Inc. +# Author: Will Woods +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +from logging import getLogger + +from ._util import listify + +log = getLogger(__name__) + + +class _RHBugzillaConverters(object): + """ + Static class that holds functional Red Hat back compat converters. + Called inline in Bugzilla + """ + @staticmethod + def convert_build_update( + component=None, + fixed_in=None, + qa_whiteboard=None, + devel_whiteboard=None, + internal_whiteboard=None, + sub_component=None): + adddict = {} + + def get_alias(): + # RHBZ has a custom extension to allow a bug to have multiple + # aliases, so the format of aliases is + # {"add": [...], "remove": [...]} + # But that means in order to approximate upstream, behavior + # which just overwrites the existing alias, we need to read + # the bug's state first to know what string to remove. Which + # we can't do, since we don't know the bug numbers at this point. + # So fail for now. + # + # The API should provide {"set": [...]} + # https://bugzilla.redhat.com/show_bug.cgi?id=1173114 + # + # Implementation will go here when it's available + pass + + if fixed_in is not None: + adddict["cf_fixed_in"] = fixed_in + if qa_whiteboard is not None: + adddict["cf_qa_whiteboard"] = qa_whiteboard + if devel_whiteboard is not None: + adddict["cf_devel_whiteboard"] = devel_whiteboard + if internal_whiteboard is not None: + adddict["cf_internal_whiteboard"] = internal_whiteboard + + if sub_component: + if not isinstance(sub_component, dict): + component = listify(component) + if not component: + raise ValueError("component must be specified if " + "specifying sub_component") + sub_component = {component[0]: sub_component} + adddict["sub_components"] = sub_component + + get_alias() + + return adddict + + + ################# + # Query methods # + ################# + + @staticmethod + def pre_translation(query): + """ + Translates the query for possible aliases + """ + old = query.copy() + + def split_comma(_v): + if isinstance(_v, list): + return _v + return _v.split(",") + + if 'bug_id' in query: + query['id'] = split_comma(query.pop('bug_id')) + + if 'component' in query: + query['component'] = split_comma(query['component']) + + if 'include_fields' not in query and 'column_list' in query: + query['include_fields'] = query.pop('column_list') + + if old != query: + log.debug("RHBugzilla pretranslated query to: %s", query) + + @staticmethod + def post_translation(query, bug): + """ + Convert the results of getbug back to the ancient RHBZ value + formats + """ + ignore = query + + # RHBZ _still_ returns component and version as lists, which + # deviates from upstream. Copy the list values to components + # and versions respectively. + if 'component' in bug and "components" not in bug: + val = bug['component'] + bug['components'] = isinstance(val, list) and val or [val] + bug['component'] = bug['components'][0] + + if 'version' in bug and "versions" not in bug: + val = bug['version'] + bug['versions'] = isinstance(val, list) and val or [val] + bug['version'] = bug['versions'][0] + + # sub_components isn't too friendly of a format, add a simpler + # sub_component value + if 'sub_components' in bug and 'sub_component' not in bug: + val = bug['sub_components'] + bug['sub_component'] = "" + if isinstance(val, dict): + values = [] + for vallist in val.values(): + values += vallist + bug['sub_component'] = " ".join(values) diff --git a/bugzilla/_session.py b/bugzilla/_session.py new file mode 100644 index 00000000..ff00f3f5 --- /dev/null +++ b/bugzilla/_session.py @@ -0,0 +1,121 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +from logging import getLogger + +import os +import sys +import urllib.parse + +import requests + +from .exceptions import BugzillaHTTPError + +log = getLogger(__name__) + + +class _BugzillaSession(object): + """ + Class to handle the backend agnostic 'requests' setup + """ + def __init__(self, url, user_agent, + sslverify, cert, tokencache, api_key, + is_redhat_bugzilla, + requests_session=None): + self._url = url + self._user_agent = user_agent + self._scheme = urllib.parse.urlparse(url)[0] + self._tokencache = tokencache + self._api_key = api_key + self._is_xmlrpc = False + self._use_auth_bearer = False + + if self._scheme not in ["http", "https"]: + raise ValueError("Invalid URL scheme: %s (%s)" % ( + self._scheme, url)) + + self._session = requests_session + if not self._session: + self._session = requests.Session() + + if cert: + self._session.cert = cert + if sslverify is False: + self._session.verify = False + self._session.headers["User-Agent"] = self._user_agent + + if is_redhat_bugzilla and self._api_key: + self._use_auth_bearer = True + self._session.headers["Authorization"] = ( + "Bearer %s" % self._api_key) + + def _get_timeout(self): + # Default to 5 minutes. This is longer than bugzilla.redhat.com's + # apparent 3 minute timeout so shouldn't affect legitimate usage, + # but saves us from indefinite hangs + DEFAULT_TIMEOUT = 300 + envtimeout = os.environ.get("PYTHONBUGZILLA_REQUESTS_TIMEOUT") + return float(envtimeout or DEFAULT_TIMEOUT) + + def set_rest_defaults(self): + self._session.headers["Content-Type"] = "application/json" + def set_xmlrpc_defaults(self): + self._is_xmlrpc = True + self._session.headers["Content-Type"] = "text/xml" + + def get_user_agent(self): + return self._user_agent + def get_scheme(self): + return self._scheme + + def get_auth_params(self): + # bugzilla.redhat.com will error if there's auth bits in params + # when Authorization header is used + if self._use_auth_bearer: + return {} + + # Don't add a token to the params list if an API key is set. + # Keeping API key solo means bugzilla will definitely fail + # if the key expires. Passing in a token could hide that + # fact, which could make it confusing to pinpoint the issue. + if self._api_key: + # Bugzilla 5.0 only supports api_key as a query parameter. + # Bugzilla 5.1+ takes it as a X-BUGZILLA-API-KEY header as well, + # with query param taking preference. + return {"Bugzilla_api_key": self._api_key} + + token = self._tokencache.get_value(self._url) + if token: + return {"Bugzilla_token": token} + + return {} + + def get_requests_session(self): + return self._session + + def request(self, *args, **kwargs): + timeout = self._get_timeout() + if "timeout" not in kwargs: + kwargs["timeout"] = timeout + + try: + response = self._session.request(*args, **kwargs) + + if self._is_xmlrpc: + # This still appears to matter for properly decoding unicode + # code points in bugzilla.redhat.com content + response.encoding = "UTF-8" + + response.raise_for_status() + except Exception as e: + # Scrape the api key out of the returned exception string + message = str(e).replace(self._api_key or "", "") + if isinstance(e, requests.HTTPError): + response = getattr(e, "response", None) + raise BugzillaHTTPError( + message, response=response).with_traceback( + sys.exc_info()[2]) + raise type(e)(message).with_traceback(sys.exc_info()[2]) + + + return response diff --git a/bugzilla/_util.py b/bugzilla/_util.py new file mode 100644 index 00000000..04555779 --- /dev/null +++ b/bugzilla/_util.py @@ -0,0 +1,12 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + + +def listify(val): + """Ensure that value is either None or a list, converting single values + into 1-element lists""" + if val is None: + return val + if isinstance(val, list): + return val + return [val] diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index 9c5cf823..c4c11d14 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -1,11 +1,8 @@ # # Copyright (C) 2014 Red Hat Inc. # -# 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. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. -version = "2.2.0-dev" +version = "3.3.0" __version__ = version diff --git a/bugzilla/base.py b/bugzilla/base.py index b7f14f40..ddda9137 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -3,134 +3,42 @@ # Copyright (C) 2007, 2008, 2009, 2010 Red Hat Inc. # Author: Will Woods # -# 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. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +import collections import getpass import locale from logging import getLogger +import mimetypes import os import sys +import urllib.parse from io import BytesIO -# pylint: disable=ungrouped-imports -if sys.version_info[0] >= 3: - # pylint: disable=F0401,E0611 - from configparser import SafeConfigParser - from http.cookiejar import LoadError, MozillaCookieJar - from urllib.parse import urlparse, parse_qsl - from xmlrpc.client import Binary, Fault -else: - from ConfigParser import SafeConfigParser - from cookielib import LoadError, MozillaCookieJar - from urlparse import urlparse, parse_qsl - from xmlrpclib import Binary, Fault - - +from ._authfiles import _BugzillaRCFile, _BugzillaTokenCache from .apiversion import __version__ -from .bug import Bug, User -from .transport import BugzillaError, _BugzillaServerProxy, _RequestsTransport +from ._backendrest import _BackendREST +from ._backendxmlrpc import _BackendXMLRPC +from .bug import Bug, Group, User +from .exceptions import BugzillaError +from ._rhconverters import _RHBugzillaConverters +from ._session import _BugzillaSession +from ._util import listify log = getLogger(__name__) -mimemagic = None - - -def _detect_filetype(fname): - # pylint: disable=E1103 - # E1103: Instance of 'bool' has no '%s' member - # pylint confuses mimemagic to be of type 'bool' - global mimemagic - - if mimemagic is None: - try: - # pylint: disable=F0401 - # F0401: Unable to import 'magic' (import-error) - import magic - mimemagic = magic.open(getattr(magic, "MAGIC_MIME_TYPE", 16)) - mimemagic.load() - except ImportError: - e = sys.exc_info()[1] - log.debug("Could not load python-magic: %s", e) - mimemagic = None - if not mimemagic: - return None - - if not os.path.isabs(fname): - return None - - try: - return mimemagic.file(fname) - except Exception: - e = sys.exc_info()[1] - log.debug("Could not detect content_type: %s", e) - return None - -def _default_auth_location(filename): - """ - Determine auth location for filename, like 'bugzillacookies'. If - old style ~/.bugzillacookies exists, we use that, otherwise we - use ~/.cache/python-bugzilla/bugzillacookies. Same for bugzillatoken - """ - homepath = os.path.expanduser("~/.%s" % filename) - xdgpath = os.path.expanduser("~/.cache/python-bugzilla/%s" % filename) - if os.path.exists(xdgpath): - return xdgpath - if os.path.exists(homepath): - return homepath - - if not os.path.exists(os.path.dirname(xdgpath)): - os.makedirs(os.path.dirname(xdgpath), 0o700) - return xdgpath - - -def _build_cookiejar(cookiefile): - cj = MozillaCookieJar(cookiefile) - if cookiefile is None: - return cj - if not os.path.exists(cookiefile): - # Make sure a new file has correct permissions - open(cookiefile, 'a').close() - os.chmod(cookiefile, 0o600) - cj.save() - return cj - - try: - cj.load() - return cj - except LoadError: - raise BugzillaError("cookiefile=%s not in Mozilla format" % - cookiefile) - - -_default_configpaths = [ - '/etc/bugzillarc', - '~/.bugzillarc', - '~/.config/python-bugzilla/bugzillarc', -] - - -def _open_bugzillarc(configpaths=-1): - if configpaths == -1: - configpaths = _default_configpaths[:] - - # pylint: disable=protected-access - configpaths = [os.path.expanduser(p) for p in - Bugzilla._listify(configpaths)] - # pylint: enable=protected-access - cfg = SafeConfigParser() - read_files = cfg.read(configpaths) - if not read_files: - return - - log.info("Found bugzillarc files: %s", read_files) - return cfg +def _nested_update(d, u): + # Helper for nested dict update() + for k, v in list(u.items()): + if isinstance(v, collections.abc.Mapping): + d[k] = _nested_update(d.get(k, {}), v) + else: + d[k] = v + return d class _FieldAlias(object): @@ -162,9 +70,10 @@ class _BugzillaAPICache(object): """ def __init__(self): self.products = [] + self.component_names = {} self.bugfields = [] - self.components = {} - self.components_details = {} + self.version_raw = None + self.version_parsed = (0, 0) class Bugzilla(object): @@ -177,11 +86,11 @@ class Bugzilla(object): bzapi = Bugzilla("http://bugzilla.example.com") If you have previously logged into that URL, and have cached login - cookies/tokens, you will automatically be logged in. Otherwise to + tokens, you will automatically be logged in. Otherwise to log in, you can either pass auth options to __init__, or call a login helper like interactive_login(). - If you are not logged in, you won be able to access restricted data like + If you are not logged in, you won't be able to access restricted data like user email, or perform write actions like bug create/update. But simple querys will work correctly. @@ -191,29 +100,23 @@ class Bugzilla(object): Another way to specify auth credentials is via a 'bugzillarc' file. See readconfig() documentation for details. """ - - # bugzilla version that the class is targeting. filled in by - # subclasses - bz_ver_major = 0 - bz_ver_minor = 0 - @staticmethod def url_to_query(url): - ''' + """ Given a big huge bugzilla query URL, returns a query dict that can be passed along to the Bugzilla.query() method. - ''' + """ q = {} # pylint: disable=unpacking-non-sequence - (ignore, ignore, path, - ignore, query, ignore) = urlparse(url) + (ignore1, ignore2, path, + ignore, query, ignore3) = urllib.parse.urlparse(url) base = os.path.basename(path) if base not in ('buglist.cgi', 'query.cgi'): return {} - for (k, v) in parse_qsl(query): + for (k, v) in urllib.parse.parse_qsl(query): if k not in q: q[k] = v elif isinstance(q[k], list): @@ -232,29 +135,49 @@ def url_to_query(url): return q @staticmethod - def fix_url(url): + def fix_url(url, force_rest=False): """ Turn passed url into a bugzilla XMLRPC web url + + :param force_rest: If True, generate a REST API url """ - if '://' not in url: - log.debug('No scheme given for url, assuming https') - url = 'https://' + url - if url.count('/') < 3: - log.debug('No path given for url, assuming /xmlrpc.cgi') - url = url + '/xmlrpc.cgi' - return url + (scheme, netloc, path, + params, query, fragment) = urllib.parse.urlparse(url) + if not scheme: + scheme = 'https' + + if path and not netloc: + netloc = path.split("/", 1)[0] + path = "/".join(path.split("/")[1:]) or None + + if not path: + path = 'xmlrpc.cgi' + if force_rest: + path = "rest/" + + if not path.startswith("/"): + path = "/" + path + + newurl = urllib.parse.urlunparse( + (scheme, netloc, path, params, query, fragment)) + return newurl @staticmethod - def _listify(val): - if val is None: - return val - if isinstance(val, list): - return val - return [val] + def get_rcfile_default_url(): + """ + Helper to check all the default bugzillarc file paths for + a [DEFAULT] url=X section, and if found, return it. + """ + configpaths = _BugzillaRCFile.get_default_configpaths() + rcfile = _BugzillaRCFile() + rcfile.set_configpaths(configpaths) + return rcfile.get_default_url() def __init__(self, url=-1, user=None, password=None, cookiefile=-1, - sslverify=True, tokenfile=-1, use_creds=True, api_key=None): + sslverify=True, tokenfile=-1, use_creds=True, api_key=None, + cert=None, configpaths=-1, + force_rest=False, force_xmlrpc=False, requests_session=None): """ :param url: The bugzilla instance URL, which we will connect to immediately. Most users will want to specify this at @@ -262,24 +185,31 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, url=None and calling connect(URL) manually :param user: optional username to connect with :param password: optional password for the connecting user - :param cookiefile: Location to cache the login session cookies so you - don't have to keep specifying username/password. Bugzilla 5+ will - use tokens instead of cookies. - If -1, use the default path. If None, don't use or save - any cookiefile. + :param cert: optional certificate file for client side certificate + authentication + :param cookiefile: Deprecated, raises an error if not -1 or None :param sslverify: Set this to False to skip SSL hostname and CA validation checks, like out of date certificate :param tokenfile: Location to cache the API login token so youi don't have to keep specifying username/password. If -1, use the default path. If None, don't use or save any tokenfile. - :param use_creds: If False, this disables cookiefile, tokenfile, - and any bugzillarc reading. This overwrites any tokenfile - or cookiefile settings + :param use_creds: If False, this disables tokenfile + and configpaths by default. This is a convenience option to + unset those values at init time. If those values are later + changed, they may be used for future operations. :param sslverify: Maps to 'requests' sslverify parameter. Set to False to disable SSL verification, but it can also be a path to file or directory for custom certs. - :param api_key: A bugzilla + :param api_key: A bugzilla5+ API key + :param configpaths: A list of possible bugzillarc locations. + :param force_rest: Force use of the REST API + :param force_xmlrpc: Force use of the XMLRPC API. If neither force_X + parameter are specified, heuristics will be used to determine + which API to use, with XMLRPC preferred for back compatability. + :param requests_session: An optional requests.Session object the + API will use to contact the remote bugzilla instance. This + way the API user can set up whatever auth bits they may need. """ if url == -1: raise TypeError("Specify a valid bugzilla url, or pass url=None") @@ -288,170 +218,177 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self.user = user or '' self.password = password or '' self.api_key = api_key + self.cert = cert or None self.url = '' - self._proxy = None - self._transport = None - self._cookiejar = None + self._backend = None + self._session = None + self._user_requests_session = requests_session self._sslverify = sslverify self._cache = _BugzillaAPICache() self._bug_autorefresh = False + self._is_redhat_bugzilla = False + + self._rcfile = _BugzillaRCFile() + self._tokencache = _BugzillaTokenCache() + + self._force_rest = force_rest + self._force_xmlrpc = force_xmlrpc - self._field_aliases = [] - self._init_field_aliases() + if cookiefile not in [None, -1]: + raise TypeError("cookiefile is deprecated, don't pass any value.") - self.configpath = _default_configpaths[:] if not use_creds: - cookiefile = None tokenfile = None - self.configpath = [] + configpaths = [] - if cookiefile == -1: - cookiefile = _default_auth_location("bugzillacookies") if tokenfile == -1: - tokenfile = _default_auth_location("bugzillatoken") - log.debug("Using tokenfile=%s", tokenfile) - self.cookiefile = cookiefile - self.tokenfile = tokenfile + tokenfile = self._tokencache.get_default_path() + if configpaths == -1: + configpaths = _BugzillaRCFile.get_default_configpaths() + + self._settokenfile(tokenfile) + self._setconfigpath(configpaths) if url: self.connect(url) - self._init_class_from_url() - self._init_class_state() + + def _detect_is_redhat_bugzilla(self): + if self._is_redhat_bugzilla: + return True + + match = ".redhat.com" + if match in self.url: + log.info("Using RHBugzilla for URL containing %s", match) + return True + + return False def _init_class_from_url(self): """ Detect if we should use RHBugzilla class, and if so, set it """ - from bugzilla import RHBugzilla - if isinstance(self, RHBugzilla): - return + from .oldclasses import RHBugzilla # pylint: disable=cyclic-import - c = None - if "bugzilla.redhat.com" in self.url: - log.info("Using RHBugzilla for URL containing bugzilla.redhat.com") - c = RHBugzilla - else: - try: - extensions = self._proxy.Bugzilla.extensions() - if "RedHat" in extensions.get('extensions', {}): - log.info("Found RedHat bugzilla extension, " - "using RHBugzilla") - c = RHBugzilla - except Fault: - log.debug("Failed to fetch bugzilla extensions", exc_info=True) - - if not c: + if not self._detect_is_redhat_bugzilla(): return - self.__class__ = c - - def _init_class_state(self): - """ - Hook for subclasses to do any __init__ time setup - """ - pass + self._is_redhat_bugzilla = True + if self.__class__ == Bugzilla: + # Overriding the class doesn't have any functional effect, + # but we continue to do it for API back compat incase anyone + # is doing any class comparison. We should drop this in the future + self.__class__ = RHBugzilla - def _init_field_aliases(self): + def _get_field_aliases(self): # List of field aliases. Maps old style RHBZ parameter # names to actual upstream values. Used for createbug() and # query include_fields at least. - self._add_field_alias('summary', 'short_desc') - self._add_field_alias('description', 'comment') - self._add_field_alias('platform', 'rep_platform') - self._add_field_alias('severity', 'bug_severity') - self._add_field_alias('status', 'bug_status') - self._add_field_alias('id', 'bug_id') - self._add_field_alias('blocks', 'blockedby') - self._add_field_alias('blocks', 'blocked') - self._add_field_alias('depends_on', 'dependson') - self._add_field_alias('creator', 'reporter') - self._add_field_alias('url', 'bug_file_loc') - self._add_field_alias('dupe_of', 'dupe_id') - self._add_field_alias('dupe_of', 'dup_id') - self._add_field_alias('comments', 'longdescs') - self._add_field_alias('creation_time', 'opendate') - self._add_field_alias('creation_time', 'creation_ts') - self._add_field_alias('whiteboard', 'status_whiteboard') - self._add_field_alias('last_change_time', 'delta_ts') + ret = [] + + def _add(*args, **kwargs): + ret.append(_FieldAlias(*args, **kwargs)) + + def _add_both(newname, origname): + _add(newname, origname, is_api=False) + _add(origname, newname, is_bug=False) + + _add('summary', 'short_desc') + _add('description', 'comment') + _add('platform', 'rep_platform') + _add('severity', 'bug_severity') + _add('status', 'bug_status') + _add('id', 'bug_id') + _add('blocks', 'blockedby') + _add('blocks', 'blocked') + _add('depends_on', 'dependson') + _add('creator', 'reporter') + _add('url', 'bug_file_loc') + _add('dupe_of', 'dupe_id') + _add('dupe_of', 'dup_id') + _add('comments', 'longdescs') + _add('creation_time', 'opendate') + _add('creation_time', 'creation_ts') + _add('whiteboard', 'status_whiteboard') + _add('last_change_time', 'delta_ts') + + if self._is_redhat_bugzilla: + _add_both('fixed_in', 'cf_fixed_in') + _add_both('qa_whiteboard', 'cf_qa_whiteboard') + _add_both('devel_whiteboard', 'cf_devel_whiteboard') + _add_both('internal_whiteboard', 'cf_internal_whiteboard') + + _add('component', 'components', is_bug=False) + _add('version', 'versions', is_bug=False) + # Yes, sub_components is the field name the API expects + _add('sub_components', 'sub_component', is_bug=False) + # flags format isn't exactly the same but it's the closest approx + _add('flags', 'flag_types') + + return ret def _get_user_agent(self): return 'python-bugzilla/%s' % __version__ user_agent = property(_get_user_agent) + @property + def bz_ver_major(self): + return self._cache.version_parsed[0] + + @property + def bz_ver_minor(self): + return self._cache.version_parsed[1] + ################### # Private helpers # ################### - def _check_version(self, major, minor): + def _get_version(self): """ - Check if the detected bugzilla version is >= passed major/minor pair. + Return version number as a float """ - if major < self.bz_ver_major: - return True - if (major == self.bz_ver_major and minor <= self.bz_ver_minor): - return True - return False - - def _product_id_to_name(self, productid): - '''Convert a product ID (int) to a product name (str).''' - for p in self.products: - if p['id'] == productid: - return p['name'] - raise ValueError('No product with id #%i' % productid) - - def _product_name_to_id(self, product): - '''Convert a product name (str) to a product ID (int).''' - for p in self.products: - if p['name'] == product: - return p['id'] - raise ValueError('No product named "%s"' % product) - - def _add_field_alias(self, *args, **kwargs): - self._field_aliases.append(_FieldAlias(*args, **kwargs)) + return float("%d.%d" % (self.bz_ver_major, self.bz_ver_minor)) def _get_bug_aliases(self): return [(f.newname, f.oldname) - for f in self._field_aliases if f.is_bug] + for f in self._get_field_aliases() if f.is_bug] def _get_api_aliases(self): return [(f.newname, f.oldname) - for f in self._field_aliases if f.is_api] + for f in self._get_field_aliases() if f.is_api] - ################### - # Cookie handling # - ################### + ################# + # Auth handling # + ################# def _getcookiefile(self): - '''cookiefile is the file that bugzilla session cookies are loaded - and saved from. - ''' - return self._cookiejar.filename - - def _delcookiefile(self): - self._cookiejar = None - - def _setcookiefile(self, cookiefile): - if (self._cookiejar and cookiefile == self._cookiejar.filename): - return - - if self._proxy is not None: - raise RuntimeError("Can't set cookies with an open connection, " - "disconnect() first.") + return None + cookiefile = property(_getcookiefile) - log.debug("Using cookiefile=%s", cookiefile) - self._cookiejar = _build_cookiejar(cookiefile) + def _gettokenfile(self): + return self._tokencache.get_filename() + def _settokenfile(self, filename): + self._tokencache.set_filename(filename) + def _deltokenfile(self): + self._settokenfile(None) + tokenfile = property(_gettokenfile, _settokenfile, _deltokenfile) - cookiefile = property(_getcookiefile, _setcookiefile, _delcookiefile) + def _getconfigpath(self): + return self._rcfile.get_configpaths() + def _setconfigpath(self, configpaths): + return self._rcfile.set_configpaths(configpaths) + def _delconfigpath(self): + return self._rcfile.set_configpaths(None) + configpath = property(_getconfigpath, _setconfigpath, _delconfigpath) ############################# # Login/connection handling # ############################# - def readconfig(self, configpath=None): + def readconfig(self, configpath=None, overwrite=True): """ :param configpath: Optional bugzillarc path to read, instead of the default list. @@ -480,49 +417,73 @@ def readconfig(self, configpath=None): Be sure to set appropriate permissions on bugzillarc if you choose to store your password in it! - """ - cfg = _open_bugzillarc(configpath or self.configpath) - if not cfg: - return - section = "" - log.debug("bugzillarc: Searching for config section matching %s", - self.url) - for s in sorted(cfg.sections()): - # Substring match - prefer the longest match found - if s in self.url: - log.debug("bugzillarc: Found matching section: %s", s) - section = s - - if not section: - log.debug("bugzillarc: No section found") - return + :param overwrite: If True, bugzillarc will clobber any already + set self.user/password/api_key/cert value. + """ + if configpath: + self._setconfigpath(configpath) + data = self._rcfile.parse(self.url) - for key, val in cfg.items(section): - if key == "api_key": + for key, val in data.items(): + if key == "api_key" and (overwrite or not self.api_key): log.debug("bugzillarc: setting api_key") self.api_key = val - elif key == "user": + elif key == "user" and (overwrite or not self.user): log.debug("bugzillarc: setting user=%s", val) self.user = val - elif key == "password": + elif key == "password" and (overwrite or not self.password): log.debug("bugzillarc: setting password") self.password = val + elif key == "cert" and (overwrite or not self.cert): + log.debug("bugzillarc: setting cert") + self.cert = val else: log.debug("bugzillarc: unknown key=%s", key) def _set_bz_version(self, version): + self._cache.version_raw = version try: - self.bz_ver_major, self.bz_ver_minor = [ - int(i) for i in version.split(".")[0:2]] - except: + major, minor = [int(i) for i in version.split(".")[0:2]] + except Exception: log.debug("version doesn't match expected format X.Y.Z, " "assuming 5.0", exc_info=True) - self.bz_ver_major = 5 - self.bz_ver_minor = 0 + major = 5 + minor = 0 + self._cache.version_parsed = (major, minor) + + def _get_backend_class(self, url): # pragma: no cover + # This is a hook for the test suite to do some mock hackery + if self._force_rest and self._force_xmlrpc: + raise BugzillaError( + "Cannot specify both force_rest and force_xmlrpc") + + xmlurl = self.fix_url(url) + if self._force_xmlrpc: + return _BackendXMLRPC, xmlurl + + resturl = self.fix_url(url, force_rest=self._force_rest) + if self._force_rest: + return _BackendREST, resturl + + # Simple heuristic if the original url has a path in it + if "/xmlrpc" in url: + return _BackendXMLRPC, xmlurl + if "/rest" in url: + return _BackendREST, resturl + + # We were passed something like bugzilla.example.com but we + # aren't sure which method to use, try probing + if _BackendXMLRPC.probe(xmlurl): + return _BackendXMLRPC, xmlurl + if _BackendREST.probe(resturl): + return _BackendREST, resturl + + # Otherwise fallback to XMLRPC default and let it fail + return _BackendXMLRPC, xmlurl def connect(self, url=None): - ''' + """ Connect to the bugzilla instance with the given url. This is called by __init__ if a URL is passed. Or it can be called manually at any time with a passed URL. @@ -532,23 +493,31 @@ def connect(self, url=None): If 'user' and 'password' are both set, we'll run login(). Otherwise you'll have to login() yourself before some methods will work. - ''' - if self._transport: + """ + if self._session: self.disconnect() - if url is None and self.url: - url = self.url - url = self.fix_url(url) - - self._transport = _RequestsTransport( - url, self._cookiejar, sslverify=self._sslverify) - self._transport.user_agent = self.user_agent - self._proxy = _BugzillaServerProxy(url, self.tokenfile, - self._transport) + url = url or self.url + backendclass, newurl = self._get_backend_class(url) + if url != newurl: + log.debug("Converted url=%s to fixed url=%s", url, newurl) + self.url = newurl + log.debug("Connecting with URL %s", self.url) - self.url = url # we've changed URLs - reload config - self.readconfig() + self.readconfig(overwrite=False) + + # Detect if connecting to redhat bugzilla + self._init_class_from_url() + + self._session = _BugzillaSession(self.url, self.user_agent, + sslverify=self._sslverify, + cert=self.cert, + tokencache=self._tokencache, + api_key=self.api_key, + is_redhat_bugzilla=self._is_redhat_bugzilla, + requests_session=self._user_requests_session) + self._backend = backendclass(self.url, self._session) if (self.user and self.password): log.info("user and password present - doing login()") @@ -556,31 +525,55 @@ def connect(self, url=None): if self.api_key: log.debug("using API key") - self._proxy.use_api_key(self.api_key) - version = self._proxy.Bugzilla.version()["version"] + version = self._backend.bugzilla_version()["version"] log.debug("Bugzilla version string: %s", version) self._set_bz_version(version) - def disconnect(self): - ''' - Disconnect from the given bugzilla instance. - ''' - self._proxy = None - self._transport = None - self._cache = _BugzillaAPICache() + @property + def _proxy(self): + """ + Return an xmlrpc ServerProxy instance that will work seamlessly + with bugzilla + + Some apps have historically accessed _proxy directly, like + fedora infrastrucutre pieces. So we consider it part of the API + """ + return self._backend.get_xmlrpc_proxy() + + def is_xmlrpc(self): + """ + :returns: True if using the XMLRPC API + """ + return self._backend.is_xmlrpc() - def _login(self, user, password): - '''Backend login method for Bugzilla3''' - return self._proxy.User.login({'login': user, 'password': password}) + def is_rest(self): + """ + :returns: True if using the REST API + """ + return self._backend.is_rest() - def _logout(self): - '''Backend login method for Bugzilla3''' - return self._proxy.User.logout() + def get_requests_session(self): + """ + Give API users access to the Requests.session object we use for + talking to the remote bugzilla instance. - def login(self, user=None, password=None): - '''Attempt to log in using the given username and password. Subsequent + :returns: The Requests.session object backing the open connection. + """ + return self._session.get_requests_session() + + def disconnect(self): + """ + Disconnect from the given bugzilla instance. + """ + self._backend = None + self._session = None + self._cache = _BugzillaAPICache() + + def login(self, user=None, password=None, restrict_login=None): + """ + Attempt to log in using the given username and password. Subsequent method calls will use this username and password. Returns False if login fails, otherwise returns some kind of login info - typically either a numeric userid, or a dict of user info. @@ -589,10 +582,13 @@ def login(self, user=None, password=None): is not set, ValueError will be raised. If login fails, BugzillaError will be raised. + The login session can be restricted to current user IP address + with restrict_login argument. (Bugzilla 4.4+) + This method will be called implicitly at the end of connect() if user and password are both set. So under most circumstances you won't need to call this yourself. - ''' + """ if self.api_key: raise ValueError("cannot login when using an API key") @@ -606,22 +602,61 @@ def login(self, user=None, password=None): if not self.password: raise ValueError("missing password") + payload = {"login": self.user} + if restrict_login: + payload['restrict_login'] = True + log.debug("logging in with options %s", str(payload)) + payload['password'] = self.password + try: - ret = self._login(self.user, self.password) + ret = self._backend.user_login(payload) self.password = '' - log.info("login successful for user=%s", self.user) + log.info("login succeeded for user=%s", self.user) + if "token" in ret: + self._tokencache.set_value(self.url, ret["token"]) return ret - except Fault: - e = sys.exc_info()[1] - raise BugzillaError("Login failed: %s" % str(e.faultString)) + except Exception as e: + log.debug("Login exception: %s", str(e), exc_info=True) + raise BugzillaError("Login failed: %s" % + BugzillaError.get_bugzilla_error_string(e)) from None + + def interactive_save_api_key(self): + """ + Helper method to interactively ask for an API key, verify it + is valid, and save it to a bugzillarc file referenced via + self.configpaths + """ + sys.stdout.write('API Key: ') + sys.stdout.flush() + api_key = sys.stdin.readline().strip() + + self.disconnect() + self.api_key = api_key + + log.info('Checking API key... ') + self.connect() + + if not self.logged_in: # pragma: no cover + raise BugzillaError("Login with API_KEY failed") + log.info('API Key accepted') + + wrote_filename = self._rcfile.save_api_key(self.url, self.api_key) + log.info("API key written to filename=%s", wrote_filename) - def interactive_login(self, user=None, password=None, force=False): + msg = "Login successful." + if wrote_filename: + msg += " API key written to %s" % wrote_filename + print(msg) + + def interactive_login(self, user=None, password=None, force=False, + restrict_login=None): """ Helper method to handle login for this bugzilla instance. :param user: bugzilla username. If not specified, prompt for it. :param password: bugzilla password. If not specified, prompt for it. :param force: Unused + :param restrict_login: restricts session to IP address """ ignore = force log.debug('Calling interactive_login') @@ -634,13 +669,27 @@ def interactive_login(self, user=None, password=None, force=False): password = getpass.getpass('Bugzilla Password: ') log.info('Logging in... ') - self.login(user, password) - log.info('Authorization cookie received.') + out = self.login(user, password, restrict_login) + msg = "Login successful." + if "token" not in out: + msg += " However no token was returned." + else: + if not self.tokenfile: + msg += " Token not saved to disk." + else: + msg += " Token cache saved to %s" % self.tokenfile + if self._get_version() >= 5.0: + msg += "\nToken usage is deprecated. " + msg += "Consider using bugzilla API keys instead. " + msg += "See `man bugzilla` for more details." + print(msg) def logout(self): - '''Log out of bugzilla. Drops server connection and user info, and - destroys authentication cookies.''' - self._logout() + """ + Log out of bugzilla. Drops server connection and user info, and + destroys authentication cache + """ + self._backend.user_logout() self.disconnect() self.user = '' self.password = '' @@ -665,35 +714,39 @@ def logged_in(self): http://bugzilla.readthedocs.org/en/latest/api/core/v1/user.html#valid-login """ try: - self._proxy.User.get({'ids': []}) + self._backend.user_get({"ids": [1]}) return True - except Fault: - e = sys.exc_info()[1] - if e.faultCode == 505 or e.faultCode == 32000: + except Exception as e: + code = BugzillaError.get_bugzilla_error_code(e) + if code in [505, 32000]: return False raise e - ############################################# - # Fetching info about the bugzilla instance # - ############################################# - - def _getbugfields(self): - ''' - Get the list of valid fields for Bug objects - ''' - r = self._proxy.Bug.fields({'include_fields': ['name']}) - return [f['name'] for f in r['fields']] + ###################### + # Bugfields querying # + ###################### - def getbugfields(self, force_refresh=False): - ''' + def getbugfields(self, force_refresh=False, names=None): + """ Calls getBugFields, which returns a list of fields in each bug for this bugzilla instance. This can be used to set the list of attrs on the Bug object. - ''' + + :param force_refresh: If True, overwrite the bugfield cache + with these newly checked values. + :param names: Only check for the passed bug field names + """ + def _fieldnames(): + data = {"include_fields": ["name"]} + if names: + data["names"] = names + r = self._backend.bug_fields(data) + return [f['name'] for f in r['fields']] + if force_refresh or not self._cache.bugfields: log.debug("Refreshing bugfields") - self._cache.bugfields = self._getbugfields() + self._cache.bugfields = _fieldnames() self._cache.bugfields.sort() log.debug("bugfields = %s", self._cache.bugfields) @@ -702,74 +755,191 @@ def getbugfields(self, force_refresh=False): fdel=lambda self: setattr(self, '_bugfields', None)) + #################### + # Product querying # + #################### + + def product_get(self, ids=None, names=None, + include_fields=None, exclude_fields=None, + ptype=None): + """ + Raw wrapper around Product.get + https://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#get-product + + This does not perform any caching like other product API calls. + If ids, names, or ptype is not specified, we default to + ptype=accessible for historical reasons + + @ids: List of product IDs to lookup + @names: List of product names to lookup + @ptype: Either 'accessible', 'selectable', or 'enterable'. If + specified, we return data for all those + @include_fields: Only include these fields in the output + @exclude_fields: Do not include these fields in the output + """ + if ids is None and names is None and ptype is None: + ptype = "accessible" + + if ptype: + raw = None + if ptype == "accessible": + raw = self._backend.product_get_accessible() + elif ptype == "enterable": + raw = self._backend.product_get_enterable() + elif ptype == "selectable": + raw = self._backend.product_get_selectable() + + if raw is None: + raise RuntimeError("Unknown ptype=%s" % ptype) + ids = raw['ids'] + log.debug("For ptype=%s found ids=%s", ptype, ids) + + kwargs = {} + if ids: + kwargs["ids"] = listify(ids) + if names: + kwargs["names"] = listify(names) + if include_fields: + kwargs["include_fields"] = include_fields + if exclude_fields: + kwargs["exclude_fields"] = exclude_fields + + ret = self._backend.product_get(kwargs) + return ret['products'] + def refresh_products(self, **kwargs): """ - Refresh a product's cached info - Takes same arguments as _getproductinfo + Refresh a product's cached info. Basically calls product_get + with the passed arguments, and tries to intelligently update + our product cache. + + For example, if we already have cached info for product=foo, + and you pass in names=["bar", "baz"], the new cache will have + info for products foo, bar, baz. Individual product fields are + also updated. """ - for product in self._getproductinfo(**kwargs): - added = False + for product in self.product_get(**kwargs): + updated = False for current in self._cache.products[:]: if (current.get("id", -1) != product.get("id", -2) and current.get("name", -1) != product.get("name", -2)): continue - self._cache.products.remove(current) - self._cache.products.append(product) - added = True + _nested_update(current, product) + updated = True break - if not added: + if not updated: self._cache.products.append(product) def getproducts(self, force_refresh=False, **kwargs): - '''Get product data: names, descriptions, etc. - The data varies between Bugzilla versions but the basic format is a - list of dicts, where the dicts will have at least the following keys: - {'id':1, 'name':"Some Product", 'description':"This is a product"} + """ + Query all products and return the raw dict info. Takes all the + same arguments as product_get. - Any method that requires a 'product' can be given either the - id or the name.''' + On first invocation this will contact bugzilla and internally + cache the results. Subsequent getproducts calls or accesses to + self.products will return this cached data only. + + :param force_refresh: force refreshing via refresh_products() + """ if force_refresh or not self._cache.products: - self._cache.products = self._getproducts(**kwargs) + self.refresh_products(**kwargs) return self._cache.products - products = property(fget=lambda self: self.getproducts(), - fdel=lambda self: setattr(self, '_products', None)) + products = property( + fget=lambda self: self.getproducts(), + fdel=lambda self: setattr(self, '_products', None), + doc="Helper for accessing the products cache. If nothing " + "has been cached yet, this calls getproducts()") + ####################### + # components querying # + ####################### + + def _lookup_product_in_cache(self, productname): + prodstr = isinstance(productname, str) and productname or None + prodint = isinstance(productname, int) and productname or None + for proddict in self._cache.products: + if prodstr == proddict.get("name", -1): + return proddict + if prodint == proddict.get("id", "nope"): + return proddict + return {} + def getcomponentsdetails(self, product, force_refresh=False): - '''Returns a dict of dicts, containing detailed component information - for the given product. The keys of the dict are component names. For - each component, the value is a dict with the following keys: - description, initialowner, initialqacontact''' - if force_refresh or product not in self._cache.components_details: - clist = self._getcomponentsdetails(product) - cdict = {} - for item in clist: - name = item['component'] - del item['component'] - cdict[name] = item - self._cache.components_details[product] = cdict - - return self._cache.components_details[product] + """ + Wrapper around Product.get(include_fields=["components"]), + returning only the "components" data for the requested product, + slightly reworked to a dict mapping of components.name: components, + for historical reasons. + + This uses the product cache, but will update it if the product + isn't found or "components" isn't cached for the product. + + In cases like bugzilla.redhat.com where there are tons of + components for some products, this API will time out. You + should use product_get instead. + """ + proddict = self._lookup_product_in_cache(product) + + if (force_refresh or not proddict or "components" not in proddict): + self.refresh_products(names=[product], + include_fields=["name", "id", "components"]) + proddict = self._lookup_product_in_cache(product) + + ret = {} + for compdict in proddict["components"]: + ret[compdict["name"]] = compdict + return ret def getcomponentdetails(self, product, component, force_refresh=False): - '''Get details for a single component. See bugzilla documentation - for a list of returned keys.''' + """ + Helper for accessing a single component's info. This is a wrapper + around getcomponentsdetails, see that for explanation + """ d = self.getcomponentsdetails(product, force_refresh) return d[component] def getcomponents(self, product, force_refresh=False): - '''Return a dict of components:descriptions for the given product.''' - if force_refresh or product not in self._cache.components: - self._cache.components[product] = self._getcomponents(product) - return self._cache.components[product] + """ + Return a list of component names for the passed product. - def _component_data_convert(self, data, update=False): - if isinstance(data['product'], int): - data['product'] = self._product_id_to_name(data['product']) + On first invocation the value is cached, and subsequent calls + will return the cached data. + :param force_refresh: Force refreshing the cache, and return + the new data + """ + proddict = self._lookup_product_in_cache(product) + product_id = proddict.get("id", None) + + if (force_refresh or product_id is None or + "components" not in proddict): + self.refresh_products( + names=[product], + include_fields=["name", "id", "components.name"]) + proddict = self._lookup_product_in_cache(product) + if "id" not in proddict: + raise BugzillaError("Product '%s' not found" % product) + product_id = proddict["id"] + + if product_id not in self._cache.component_names: + names = [] + for comp in proddict.get("components", []): + name = comp.get("name") + if name: + names.append(name) + self._cache.component_names[product_id] = names + + return self._cache.component_names[product_id] + + + ############################ + # component adding/editing # + ############################ + def _component_data_convert(self, data, update=False): # Back compat for the old RH interface convert_fields = [ ("initialowner", "default_assignee"), @@ -784,7 +954,7 @@ def _component_data_convert(self, data, update=False): names = {"product": data.pop("product"), "component": data.pop("component")} updates = {} - for k in data.keys(): + for k in list(data.keys()): updates[k] = data.pop(k) data["names"] = [names] @@ -792,13 +962,13 @@ def _component_data_convert(self, data, update=False): def addcomponent(self, data): - ''' + """ A method to create a component in Bugzilla. Takes a dict, with the following elements: product: The product to create the component in component: The name of the component to create - desription: A one sentence summary of the component + description: A one sentence summary of the component default_assignee: The bugzilla login (email address) of the initial owner of the component default_qa_contact (optional): The bugzilla login of the @@ -807,100 +977,21 @@ def addcomponent(self, data): new bugs for the component. is_active: (optional) If False, the component is hidden from the component list when filing new bugs. - ''' + """ data = data.copy() self._component_data_convert(data) - log.debug("Calling Component.create with: %s", data) - return self._proxy.Component.create(data) + return self._backend.component_create(data) def editcomponent(self, data): - ''' + """ A method to edit a component in Bugzilla. Takes a dict, with mandatory elements of product. component, and initialowner. All other elements are optional and use the same names as the addcomponent() method. - ''' + """ data = data.copy() self._component_data_convert(data, update=True) - log.debug("Calling Component.update with: %s", data) - return self._proxy.Component.update(data) - - - def _getproductinfo(self, ids=None, names=None, - include_fields=None, exclude_fields=None): - ''' - Get all info for the requested products. - - @ids: List of product IDs to lookup - @names: List of product names to lookup (since bz 4.2, - though we emulate it for older versions) - @include_fields: Only include these fields in the output (since bz 4.2) - @exclude_fields: Do not include these fields in the output (since - bz 4.2) - ''' - if ids is None and names is None: - raise RuntimeError("Products must be specified") - - kwargs = {} - if not self._check_version(4, 2): - if names: - ids = [self._product_name_to_id(name) for name in names] - names = None - include_fields = None - exclude_fields = None - - if ids: - kwargs["ids"] = self._listify(ids) - if names: - kwargs["names"] = self._listify(names) - if include_fields: - kwargs["include_fields"] = include_fields - if exclude_fields: - kwargs["exclude_fields"] = exclude_fields - - log.debug("Calling Product.get with: %s", kwargs) - ret = self._proxy.Product.get(kwargs) - return ret['products'] - - def _getproducts(self, **kwargs): - product_ids = self._proxy.Product.get_accessible_products() - r = self._getproductinfo(product_ids['ids'], **kwargs) - return r - - def _getcomponents(self, product): - if isinstance(product, str): - product = self._product_name_to_id(product) - r = self._proxy.Bug.legal_values({'product_id': product, - 'field': 'component'}) - return r['values'] - - def _getcomponentsdetails(self, product): - def _find_comps(): - for p in self._cache.products: - if p["name"] != product: - continue - return p.get("components", None) - - comps = _find_comps() - if comps is None: - self.refresh_products(names=[product], - include_fields=["name", "id", "components"]) - comps = _find_comps() - - if comps is None: - raise ValueError("Unknown product '%s'" % product) - - # Convert to old style dictionary to maintain back compat - # with original RH bugzilla call - ret = [] - for comp in comps: - row = {} - row["component"] = comp["name"] - row["initialqacontact"] = comp["default_qa_contact"] - row["initialowner"] = comp["default_assigned_to"] - row["description"] = comp["description"] - ret.append(row) - return ret + return self._backend.component_update(data) ################### @@ -913,9 +1004,6 @@ def _process_include_fields(self, include_fields, exclude_fields, Internal helper to process include_fields lists """ def _convert_fields(_in): - if not _in: - return _in - for newname, oldname in self._get_api_aliases(): if oldname in _in: _in.remove(oldname) @@ -924,16 +1012,15 @@ def _convert_fields(_in): return _in ret = {} - if self._check_version(4, 0): - if include_fields: - include_fields = _convert_fields(include_fields) - if "id" not in include_fields: - include_fields.append("id") - ret["include_fields"] = include_fields - if exclude_fields: - exclude_fields = _convert_fields(exclude_fields) - ret["exclude_fields"] = exclude_fields - if self._supports_getbug_extra_fields: + if include_fields: + include_fields = _convert_fields(include_fields) + if "id" not in include_fields: + include_fields.append("id") + ret["include_fields"] = include_fields + if exclude_fields: + exclude_fields = _convert_fields(exclude_fields) + ret["exclude_fields"] = exclude_fields + if self._supports_getbug_extra_fields(): if extra_fields: ret["extra_fields"] = _convert_fields(extra_fields) return ret @@ -951,61 +1038,83 @@ def _set_bug_autorefresh(self, val): bug_autorefresh = property(_get_bug_autorefresh, _set_bug_autorefresh) - # getbug_extra_fields: Extra fields that need to be explicitly - # requested from Bug.get in order for the data to be returned. - # - # As of Dec 2012 it seems like only RH bugzilla actually has behavior - # like this, for upstream bz it returns all info for every Bug.get() - _getbug_extra_fields = [] - _supports_getbug_extra_fields = False + def _getbug_extra_fields(self): + """ + Extra fields that need to be explicitly + requested from Bug.get in order for the data to be returned. + """ + rhbz_extra_fields = [ + "comments", "description", + "external_bugs", "flags", "sub_components", + "tags", + ] + if self._is_redhat_bugzilla: + return rhbz_extra_fields + return [] + + def _supports_getbug_extra_fields(self): + """ + Return True if the bugzilla instance supports passing + extra_fields to getbug + + As of Dec 2012 it seems like only RH bugzilla actually has behavior + like this, for upstream bz it returns all info for every Bug.get() + """ + return self._is_redhat_bugzilla + def _getbugs(self, idlist, permissive, include_fields=None, exclude_fields=None, extra_fields=None): - ''' + """ Return a list of dicts of full bug info for each given bug id. bug ids that couldn't be found will return None instead of a dict. - ''' - oldidlist = idlist - idlist = [] - for i in oldidlist: - try: - idlist.append(int(i)) - except ValueError: - # String aliases can be passed as well - idlist.append(i) - - extra_fields = self._listify(extra_fields or []) - extra_fields += self._getbug_extra_fields - - getbugdata = {"ids": idlist} + """ + ids = [] + aliases = [] + + def _alias_or_int(_v): + if str(_v).isdigit(): + return int(_v), None + return None, str(_v) + + for idstr in idlist: + idint, alias = _alias_or_int(idstr) + if alias: + aliases.append(alias) + else: + ids.append(idstr) + + if (include_fields is not None and aliases + and "alias" not in include_fields): + # Extra field to prevent sorting (see below) from causing an error + include_fields.append("alias") + + extra_fields = listify(extra_fields or []) + extra_fields += self._getbug_extra_fields() + + getbugdata = {} if permissive: getbugdata["permissive"] = 1 getbugdata.update(self._process_include_fields( include_fields, exclude_fields, extra_fields)) - log.debug("Calling Bug.get with: %s", getbugdata) - r = self._proxy.Bug.get(getbugdata) - - if self._check_version(4, 0): - bugdict = dict([(b['id'], b) for b in r['bugs']]) - else: - bugdict = dict([(b['id'], b['internals']) for b in r['bugs']]) + r = self._backend.bug_get(ids, aliases, getbugdata) + # Do some wrangling to ensure we return bugs in the same order + # the were passed in, for historical reasons ret = [] - for i in idlist: - found = None - if i in bugdict: - found = bugdict[i] - else: - # Need to map an alias - for valdict in bugdict.values(): - if i in self._listify(valdict.get("alias", None)): - found = valdict - break - - ret.append(found) + for idval in idlist: + idint, alias = _alias_or_int(idval) + for bugdict in r["bugs"]: + if idint is not None and idint != bugdict.get("id", None): + continue + aliaslist = listify(bugdict.get("alias", None) or []) + if alias and alias not in aliaslist: + continue + ret.append(bugdict) + break return ret def _getbug(self, objid, **kwargs): @@ -1021,8 +1130,10 @@ def _getbug(self, objid, **kwargs): def getbug(self, objid, include_fields=None, exclude_fields=None, extra_fields=None): - '''Return a Bug object with the full complement of bug data - already loaded.''' + """ + Return a Bug object with the full complement of bug data + already loaded. + """ data = self._getbug(objid, include_fields=include_fields, exclude_fields=exclude_fields, extra_fields=extra_fields) @@ -1031,9 +1142,11 @@ def getbug(self, objid, def getbugs(self, idlist, include_fields=None, exclude_fields=None, extra_fields=None, permissive=True): - '''Return a list of Bug objects with the full complement of bug data + """ + Return a list of Bug objects with the full complement of bug data already loaded. If there's a problem getting the data for a given id, - the corresponding item in the returned list will be None.''' + the corresponding item in the returned list will be None. + """ data = self._getbugs(idlist, include_fields=include_fields, exclude_fields=exclude_fields, extra_fields=extra_fields, permissive=permissive) @@ -1042,9 +1155,11 @@ def getbugs(self, idlist, for b in data] def get_comments(self, idlist): - '''Returns a dictionary of bugs and comments. The comments key will - be empty. See bugzilla docs for details''' - return self._proxy.Bug.comments({'ids': idlist}) + """ + Returns a dictionary of bugs and comments. The comments key will + be empty. See bugzilla docs for details + """ + return self._backend.bug_comments(idlist, {}) ################# @@ -1077,13 +1192,11 @@ def build_query(self, alias=None, qa_whiteboard=None, devel_whiteboard=None, - boolean_query=None, bug_severity=None, priority=None, target_release=None, target_milestone=None, emailtype=None, - booleantype=None, include_fields=None, quicksearch=None, savedsearch=None, @@ -1091,12 +1204,14 @@ def build_query(self, sub_component=None, tags=None, exclude_fields=None, - extra_fields=None): + extra_fields=None, + limit=None, + resolution=None): """ Build a query string from passed arguments. Will handle query parameter differences between various bugzilla versions. - Most of the parameters should be self explanatory. However + Most of the parameters should be self-explanatory. However, if you want to perform a complex query, and easy way is to create it with the bugzilla web UI, copy the entire URL it generates, and pass it to the static method @@ -1108,15 +1223,10 @@ def build_query(self, For details about the specific argument formats, see the bugzilla docs: https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs """ - if boolean_query or booleantype: - raise RuntimeError("boolean_query format is no longer supported. " - "If you need complicated URL queries, look into " - "query --from-url/url_to_query().") - query = { "alias": alias, - "product": self._listify(product), - "component": self._listify(component), + "product": listify(product), + "component": listify(component), "version": version, "id": bug_id, "short_desc": short_desc, @@ -1125,17 +1235,19 @@ def build_query(self, "priority": priority, "target_release": target_release, "target_milestone": target_milestone, - "tag": self._listify(tags), + "tag": listify(tags), "quicksearch": quicksearch, "savedsearch": savedsearch, "sharer_id": savedsearch_sharer_id, + "limit": limit, + "resolution": resolution, # RH extensions... don't add any more. See comment below - "sub_components": self._listify(sub_component), + "sub_components": listify(sub_component), } def add_bool(bzkey, value, bool_id, booltype=None): - value = self._listify(value) + value = listify(value) if value is None: return bool_id @@ -1200,56 +1312,76 @@ def add_email(key, value, count): # Strip out None elements in the dict for k, v in query.copy().items(): if v is None: - del(query[k]) + del query[k] self.pre_translation(query) return query - def query(self, query): - '''Query bugzilla and return a list of matching bugs. - query must be a dict with fields like those in in querydata['fields']. - Returns a list of Bug objects. - Also see the _query() method for details about the underlying - implementation. - ''' - log.debug("Calling Bug.search with: %s", query) - try: - r = self._proxy.Bug.search(query) - except Fault: - e = sys.exc_info()[1] + def query_return_extra(self, query): + """ + Same as `query()`, but the return value is altered to be + (buglist, values), where `values` is raw dictionary output from + the API call, excluding the bug content. For example this may + include a `limit` value if the bugzilla instance puts an implied + limit on returned result numbers. + """ + try: + r = self._backend.bug_search(query) + log.debug("bug_search returned:\n%s", str(r)) + except Exception as e: # Try to give a hint in the error message if url_to_query # isn't supported by this bugzilla instance if ("query_format" not in str(e) or - "RHBugzilla" in str(e.__class__) or - self._check_version(5, 0)): + not BugzillaError.get_bugzilla_error_code(e) or + self._get_version() >= 5.0): raise raise BugzillaError("%s\nYour bugzilla instance does not " "appear to support API queries derived from bugzilla " - "web URL queries." % e) + "web URL queries." % e) from None + + rawbugs = r.pop("bugs") + log.debug("Query returned %s bugs", len(rawbugs)) + bugs = [Bug(self, dict=b, + autorefresh=self.bug_autorefresh) for b in rawbugs] + + return bugs, r + + def query(self, query): + """ + Pass search terms to bugzilla and and return a list of matching + Bug objects. - log.debug("Query returned %s bugs", len(r['bugs'])) - return [Bug(self, dict=b, - autorefresh=self.bug_autorefresh) for b in r['bugs']] + See `build_query` for more details about constructing the + `query` dict parameter. + """ + bugs, dummy = self.query_return_extra(query) + return bugs def pre_translation(self, query): - '''In order to keep the API the same, Bugzilla4 needs to process the + """ + In order to keep the API the same, Bugzilla4 needs to process the query and the result. This also applies to the refresh() function - ''' - pass + """ + if self._is_redhat_bugzilla: + _RHBugzillaConverters.pre_translation(query) + query.update(self._process_include_fields( + query.get("include_fields", []), None, None)) def post_translation(self, query, bug): - '''In order to keep the API the same, Bugzilla4 needs to process the + """ + In order to keep the API the same, Bugzilla4 needs to process the query and the result. This also applies to the refresh() function - ''' - pass + """ + if self._is_redhat_bugzilla: + _RHBugzillaConverters.post_translation(query, bug) def bugs_history_raw(self, bug_ids): - ''' + """ Experimental. Gets the history of changes for particular bugs in the database. - ''' - return self._proxy.Bug.history({'ids': bug_ids}) + """ + return self._backend.bug_history(bug_ids, {}) ####################################### @@ -1267,28 +1399,23 @@ def update_bugs(self, ids, updates): build_update(), otherwise we cannot guarantee back compatibility. """ tmp = updates.copy() - tmp["ids"] = self._listify(ids) - - log.debug("Calling Bug.update with: %s", tmp) - return self._proxy.Bug.update(tmp) + return self._backend.bug_update(listify(ids), tmp) def update_tags(self, idlist, tags_add=None, tags_remove=None): - ''' + """ Updates the 'tags' field for a bug. - ''' + """ tags = {} if tags_add: - tags["add"] = self._listify(tags_add) + tags["add"] = listify(tags_add) if tags_remove: - tags["remove"] = self._listify(tags_remove) + tags["remove"] = listify(tags_remove) d = { - "ids": self._listify(idlist), "tags": tags, } - log.debug("Calling Bug.update_tags with: %s", d) - return self._proxy.Bug.update_tags(d) + return self._backend.bug_update_tags(listify(idlist), d) def update_flags(self, idlist, flags): """ @@ -1346,7 +1473,9 @@ def build_update(self, devel_whiteboard=None, internal_whiteboard=None, sub_component=None, - flags=None): + flags=None, + comment_tags=None, + minor_update=None): """ Returns a python dict() with properly formatted parameters to pass to update_bugs(). See bugzilla documentation for the format @@ -1354,23 +1483,29 @@ def build_update(self, https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug """ - # pylint: disable=W0221 - # Argument number differs from overridden method - # Base defines it with *args, **kwargs, so we don't have to maintain - # the master argument list in 2 places ret = {} + rhbzret = {} # These are only supported for rhbugzilla - for key, val in [ - ("fixed_in", fixed_in), - ("devel_whiteboard", devel_whiteboard), - ("qa_whiteboard", qa_whiteboard), - ("internal_whiteboard", internal_whiteboard), - ("sub_component", sub_component), - ]: - if val is not None: - raise ValueError("bugzilla instance does not support " - "updating '%s'" % key) + # + # This should not be extended any more. + # If people want to handle custom fields, manually extend the + # returned dictionary. + rhbzargs = { + "fixed_in": fixed_in, + "devel_whiteboard": devel_whiteboard, + "qa_whiteboard": qa_whiteboard, + "internal_whiteboard": internal_whiteboard, + "sub_component": sub_component, + } + if self._is_redhat_bugzilla: + rhbzret = _RHBugzillaConverters.convert_build_update( + component=component, **rhbzargs) + else: + for key, val in rhbzargs.items(): + if val is not None: + raise ValueError("bugzilla instance does not support " + "updating '%s'" % key) def s(key, val, convert=None): if val is None: @@ -1379,23 +1514,17 @@ def s(key, val, convert=None): val = convert(val) ret[key] = val - def add_dict(key, add, remove, _set=None, convert=None): + def add_dict(key, add, remove, _set=None): if add is remove is _set is None: return - def c(val): - val = self._listify(val) - if convert: - val = [convert(v) for v in val] - return val - newdict = {} if add is not None: - newdict["add"] = c(add) + newdict["add"] = listify(add) if remove is not None: - newdict["remove"] = c(remove) + newdict["remove"] = listify(remove) if _set is not None: - newdict["set"] = c(_set) + newdict["set"] = listify(_set) ret[key] = newdict @@ -1426,11 +1555,11 @@ def c(val): s("whiteboard", whiteboard) s("work_time", work_time, float) s("flags", flags) + s("comment_tags", comment_tags, listify) + s("minor_update", minor_update, bool) - add_dict("blocks", blocks_add, blocks_remove, blocks_set, - convert=int) - add_dict("depends_on", depends_on_add, depends_on_remove, - depends_on_set, convert=int) + add_dict("blocks", blocks_add, blocks_remove, blocks_set) + add_dict("depends_on", depends_on_add, depends_on_remove, depends_on_set) add_dict("cc", cc_add, cc_remove) add_dict("groups", groups_add, groups_remove) add_dict("keywords", keywords_add, keywords_remove, keywords_set) @@ -1441,6 +1570,7 @@ def c(val): if comment_private: ret["comment"]["is_private"] = comment_private + ret.update(rhbzret) return ret @@ -1448,14 +1578,8 @@ def c(val): # Methods for working with attachments # ######################################## - def _attachment_uri(self, attachid): - '''Returns the URI for the given attachment ID.''' - att_uri = self.url.replace('xmlrpc.cgi', 'attachment.cgi') - att_uri = att_uri + '?id=%s' % attachid - return att_uri - def attachfile(self, idlist, attachfile, description, **kwargs): - ''' + """ Attach a file to the given bug IDs. Returns the ID of the attachment or raises XMLRPC Fault if something goes wrong. @@ -1479,9 +1603,9 @@ def attachfile(self, idlist, attachfile, description, **kwargs): Returns the list of attachment ids that were added. If only one attachment was added, we return the single int ID for back compat - ''' + """ if isinstance(attachfile, str): - f = open(attachfile) + f = open(attachfile, "rb") elif hasattr(attachfile, 'read'): f = attachfile else: @@ -1500,21 +1624,20 @@ def attachfile(self, idlist, attachfile, description, **kwargs): kwargs['summary'] = description data = f.read() - if not isinstance(data, bytes): + if not isinstance(data, bytes): # pragma: no cover data = data.encode(locale.getpreferredencoding()) - kwargs['data'] = Binary(data) - - kwargs['ids'] = self._listify(idlist) if 'file_name' not in kwargs and hasattr(f, "name"): kwargs['file_name'] = os.path.basename(f.name) if 'content_type' not in kwargs: - ctype = _detect_filetype(getattr(f, "name", None)) - if not ctype: - ctype = 'application/octet-stream' - kwargs['content_type'] = ctype + ctype = None + if kwargs['file_name']: + ctype = mimetypes.guess_type( + kwargs['file_name'], strict=False)[0] + kwargs['content_type'] = ctype or 'application/octet-stream' - ret = self._proxy.Bug.add_attachment(kwargs) + ret = self._backend.bug_attachment_create( + listify(idlist), data, kwargs) if "attachments" in ret: # Up to BZ 4.2 @@ -1527,58 +1650,52 @@ def attachfile(self, idlist, attachfile, description, **kwargs): ret = ret[0] return ret - - def openattachment(self, attachid): - '''Get the contents of the attachment with the given attachment ID. - Returns a file-like object.''' - - def get_filename(headers): - import re - - match = re.search( - r'^.*filename="?(.*)"$', - headers.get('content-disposition', '') - ) - - # default to attchid if no match was found - return match.group(1) if match else attachid - - att_uri = self._attachment_uri(attachid) - - defaults = self._transport.request_defaults.copy() - defaults["headers"] = defaults["headers"].copy() - del(defaults["headers"]["Content-Type"]) - - response = self._transport.session.get( - att_uri, stream=True, **defaults) - + def openattachment_data(self, attachment_dict): + """ + Helper for turning passed API attachment dictionary into a + filelike object + """ ret = BytesIO() - for chunk in response.iter_content(chunk_size=1024): - if chunk: - ret.write(chunk) - ret.name = get_filename(response.headers) + data = attachment_dict["data"] - # Hooray, now we have a file-like object with .read() and .name + if hasattr(data, "data"): + # This is for xmlrpc Binary + content = data.data # pragma: no cover + else: + import base64 + content = base64.b64decode(data) + + ret.write(content) + ret.name = attachment_dict["file_name"] ret.seek(0) return ret + def openattachment(self, attachid): + """ + Get the contents of the attachment with the given attachment ID. + Returns a file-like object. + """ + attachments = self.get_attachments(None, attachid) + data = attachments["attachments"][str(attachid)] + return self.openattachment_data(data) + def updateattachmentflags(self, bugid, attachid, flagname, **kwargs): - ''' + """ Updates a flag for the given attachment ID. Optional keyword args are: status: new status for the flag ('-', '+', '?', 'X') requestee: new requestee for the flag - ''' + """ # Bug ID was used for the original custom redhat API, no longer # needed though ignore = bugid flags = {"name": flagname} flags.update(kwargs) - update = {'ids': [int(attachid)], 'flags': [flags]} + attachment_ids = [int(attachid)] + update = {'flags': [flags]} - log.debug("Calling Bug.update_attachment(%s)", update) - return self._proxy.Bug.update_attachment(update) + return self._backend.bug_attachment_update(attachment_ids, update) def get_attachments(self, ids, attachment_ids, include_fields=None, exclude_fields=None): @@ -1590,17 +1707,15 @@ def get_attachments(self, ids, attachment_ids, https://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#get-attachment """ - params = { - "ids": self._listify(ids) or [], - "attachment_ids": self._listify(attachment_ids) or [], - } + params = {} if include_fields: - params["include_fields"] = self._listify(include_fields) + params["include_fields"] = listify(include_fields) if exclude_fields: - params["exclude_fields"] = self._listify(exclude_fields) + params["exclude_fields"] = listify(exclude_fields) - log.debug("Calling Bug.attachments(%s)", params) - return self._proxy.Bug.attachments(params) + if attachment_ids: + return self._backend.bug_attachment_get(attachment_ids, params) + return self._backend.bug_attachment_get_all(ids, params) ##################### @@ -1634,8 +1749,9 @@ def build_createbug(self, target_release=None, url=None, sub_component=None, - alias=None): - """" + alias=None, + comment_tags=None): + """ Returns a python dict() with properly formatted parameters to pass to createbug(). See bugzilla documentation for the format of the individual fields: @@ -1645,15 +1761,15 @@ def build_createbug(self, localdict = {} if blocks: - localdict["blocks"] = self._listify(blocks) + localdict["blocks"] = listify(blocks) if cc: - localdict["cc"] = self._listify(cc) + localdict["cc"] = listify(cc) if depends_on: - localdict["depends_on"] = self._listify(depends_on) - if groups: - localdict["groups"] = self._listify(groups) + localdict["depends_on"] = listify(depends_on) + if groups is not None: + localdict["groups"] = listify(groups) if keywords: - localdict["keywords"] = self._listify(keywords) + localdict["keywords"] = listify(keywords) if description: localdict["description"] = description if comment_private: @@ -1668,7 +1784,7 @@ def build_createbug(self, target_milestone=target_milestone, target_release=target_release, url=url, assigned_to=assigned_to, sub_component=sub_component, - alias=alias) + alias=alias, comment_tags=comment_tags) ret.update(localdict) return ret @@ -1677,14 +1793,15 @@ def _validate_createbug(self, *args, **kwargs): # Previous API required users specifying keyword args that mapped # to the XMLRPC arg names. Maintain that bad compat, but also allow # receiving a single dictionary like query() does - if kwargs and args: + if kwargs and args: # pragma: no cover raise BugzillaError("createbug: cannot specify positional " "args=%s with kwargs=%s, must be one or the " "other." % (args, kwargs)) if args: if len(args) > 1 or not isinstance(args[0], dict): - raise BugzillaError("createbug: positional arguments only " - "accept a single dictionary.") + raise BugzillaError( # pragma: no cover + "createbug: positional arguments only " + "accept a single dictionary.") data = args[0] else: data = kwargs @@ -1699,20 +1816,19 @@ def _validate_createbug(self, *args, **kwargs): # Back compat handling for check_args if "check_args" in data: - del(data["check_args"]) + del data["check_args"] return data def createbug(self, *args, **kwargs): - ''' + """ Create a bug with the given info. Returns a new Bug object. Check bugzilla API documentation for valid values, at least product, component, summary, version, and description need to be passed. - ''' + """ data = self._validate_createbug(*args, **kwargs) - log.debug("Calling Bug.create with: %s", data) - rawbug = self._proxy.Bug.create(data) + rawbug = self._backend.bug_create(data) return Bug(self, bug_id=rawbug["id"], autorefresh=self.bug_autorefresh) @@ -1721,54 +1837,28 @@ def createbug(self, *args, **kwargs): # Methods for handling Users # ############################## - def _getusers(self, ids=None, names=None, match=None): - '''Return a list of users that match criteria. - - :kwarg ids: list of user ids to return data on - :kwarg names: list of user names to return data on - :kwarg match: list of patterns. Returns users whose real name or - login name match the pattern. - :raises XMLRPC Fault: Code 51: if a Bad Login Name was sent to the - names array. - Code 304: if the user was not authorized to see user they - requested. - Code 505: user is logged out and can't use the match or ids - parameter. - - Available in Bugzilla-3.4+ - ''' - params = {} - if ids: - params['ids'] = self._listify(ids) - if names: - params['names'] = self._listify(names) - if match: - params['match'] = self._listify(match) - if not params: - raise BugzillaError('_get() needs one of ids, ' - ' names, or match kwarg.') - - log.debug("Calling User.get with: %s", params) - return self._proxy.User.get(params) - def getuser(self, username): - '''Return a bugzilla User for the given username + """ + Return a bugzilla User for the given username :arg username: The username used in bugzilla. :raises XMLRPC Fault: Code 51 if the username does not exist :returns: User record for the username - ''' + """ ret = self.getusers(username) return ret and ret[0] def getusers(self, userlist): - '''Return a list of Users from . + """ + Return a list of Users from . :userlist: List of usernames to lookup :returns: List of User records - ''' + """ + userlist = listify(userlist) + rawusers = self._backend.user_get({"names": userlist}) userobjs = [User(self, **rawuser) for rawuser in - self._getusers(names=userlist).get('users', [])] + rawusers.get('users', [])] # Return users in same order they were passed in ret = [] @@ -1783,16 +1873,19 @@ def getusers(self, userlist): def searchusers(self, pattern): - '''Return a bugzilla User for the given list of patterns + """ + Return a bugzilla User for the given list of patterns :arg pattern: List of patterns to match against. :returns: List of User records - ''' + """ + rawusers = self._backend.user_get({"match": listify(pattern)}) return [User(self, **rawuser) for rawuser in - self._getusers(match=pattern).get('users', [])] + rawusers.get('users', [])] def createuser(self, email, name='', password=''): - '''Return a bugzilla User for the given username + """ + Return a bugzilla User for the given username :arg email: The email address to use in bugzilla :kwarg name: Real name to associate with the account @@ -1802,12 +1895,17 @@ def createuser(self, email, name='', password=''): Code 502 if the password is too short Code 503 if the password is too long :return: User record for the username - ''' - self._proxy.User.create(email, name, password) + """ + args = {"email": email} + if name: + args["name"] = name + if password: + args["password"] = password + self._backend.user_create(args) return self.getuser(email) def updateperms(self, user, action, groups): - ''' + """ A method to update the permissions (group membership) of a bugzilla user. @@ -1815,19 +1913,211 @@ def updateperms(self, user, action, groups): also be a list of emails. :arg action: add, remove, or set :arg groups: list of groups to be added to (i.e. ['fedora_contrib']) - ''' - groups = self._listify(groups) + """ + groups = listify(groups) if action == "rem": action = "remove" if action not in ["add", "remove", "set"]: raise BugzillaError("Unknown user permission action '%s'" % action) update = { - "names": self._listify(user), + "names": listify(user), "groups": { action: groups, } } - log.debug("Call User.update with: %s", update) - return self._proxy.User.update(update) + return self._backend.user_update(update) + + + ############################### + # Methods for handling Groups # + ############################### + + def _getgroups(self, names, membership=False): + """ + Return a list of groups that match criteria. + + :kwarg ids: list of group ids to return data on + :kwarg membership: boolean specifying wether to query the members + of the group or not. + :raises XMLRPC Fault: Code 51: if a Bad Login Name was sent to the + names array. + Code 304: if the user was not authorized to see user they + requested. + Code 505: user is logged out and can't use the match or ids + parameter. + Code 805: logged in user do not have enough priviledges to view + groups. + """ + params = {"membership": membership} + params['names'] = listify(names) + return self._backend.group_get(params) + + def getgroup(self, name, membership=False): + """ + Return a bugzilla Group for the given name + + :arg name: The group name used in bugzilla. + :raises XMLRPC Fault: Code 51 if the name does not exist + :raises XMLRPC Fault: Code 805 if the user does not have enough + permissions to view groups + :returns: Group record for the name + """ + ret = self.getgroups(name, membership=membership) + return ret and ret[0] + + def getgroups(self, grouplist, membership=False): + """ + Return a list of Groups from . + + :userlist: List of group names to lookup + :returns: List of Group records + """ + grouplist = listify(grouplist) + groupobjs = [ + Group(self, **rawgroup) + for rawgroup in self._getgroups( + names=grouplist, membership=membership).get('groups', []) + ] + + # Return in same order they were passed in + ret = [] + for g in grouplist: + for gobj in groupobjs[:]: + if gobj.name == g: + groupobjs.remove(gobj) + ret.append(gobj) + break + ret += groupobjs + return ret + + + ############################# + # ExternalBugs API wrappers # + ############################# + + def add_external_tracker(self, bug_ids, ext_bz_bug_id, ext_type_id=None, + ext_type_description=None, ext_type_url=None, + ext_status=None, ext_description=None, + ext_priority=None): + """ + Wrapper method to allow adding of external tracking bugs using the + ExternalBugs::WebService::add_external_bug method. + + This is documented at + https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#add-external-bug + + bug_ids: A single bug id or list of bug ids to have external trackers + added. + ext_bz_bug_id: The external bug id (ie: the bug number in the + external tracker). + ext_type_id: The external tracker id as used by Bugzilla. + ext_type_description: The external tracker description as used by + Bugzilla. + ext_type_url: The external tracker url as used by Bugzilla. + ext_status: The status of the external bug. + ext_description: The description of the external bug. + ext_priority: The priority of the external bug. + """ + param_dict = {'ext_bz_bug_id': ext_bz_bug_id} + if ext_type_id is not None: + param_dict['ext_type_id'] = ext_type_id + if ext_type_description is not None: + param_dict['ext_type_description'] = ext_type_description + if ext_type_url is not None: + param_dict['ext_type_url'] = ext_type_url + if ext_status is not None: + param_dict['ext_status'] = ext_status + if ext_description is not None: + param_dict['ext_description'] = ext_description + if ext_priority is not None: + param_dict['ext_priority'] = ext_priority + params = { + 'bug_ids': listify(bug_ids), + 'external_bugs': [param_dict], + } + return self._backend.externalbugs_add(params) + + def update_external_tracker(self, ids=None, ext_type_id=None, + ext_type_description=None, ext_type_url=None, + ext_bz_bug_id=None, bug_ids=None, + ext_status=None, ext_description=None, + ext_priority=None): + """ + Wrapper method to allow adding of external tracking bugs using the + ExternalBugs::WebService::update_external_bug method. + + This is documented at + https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#update-external-bug + + ids: A single external tracker bug id or list of external tracker bug + ids. + ext_type_id: The external tracker id as used by Bugzilla. + ext_type_description: The external tracker description as used by + Bugzilla. + ext_type_url: The external tracker url as used by Bugzilla. + ext_bz_bug_id: A single external bug id or list of external bug ids + (ie: the bug number in the external tracker). + bug_ids: A single bug id or list of bug ids to have external tracker + info updated. + ext_status: The status of the external bug. + ext_description: The description of the external bug. + ext_priority: The priority of the external bug. + """ + params = {} + if ids is not None: + params['ids'] = listify(ids) + if ext_type_id is not None: + params['ext_type_id'] = ext_type_id + if ext_type_description is not None: + params['ext_type_description'] = ext_type_description + if ext_type_url is not None: + params['ext_type_url'] = ext_type_url + if ext_bz_bug_id is not None: + params['ext_bz_bug_id'] = listify(ext_bz_bug_id) + if bug_ids is not None: + params['bug_ids'] = listify(bug_ids) + if ext_status is not None: + params['ext_status'] = ext_status + if ext_description is not None: + params['ext_description'] = ext_description + if ext_priority is not None: + params['ext_priority'] = ext_priority + return self._backend.externalbugs_update(params) + + def remove_external_tracker(self, ids=None, ext_type_id=None, + ext_type_description=None, ext_type_url=None, + ext_bz_bug_id=None, bug_ids=None): + """ + Wrapper method to allow removal of external tracking bugs using the + ExternalBugs::WebService::remove_external_bug method. + + This is documented at + https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#remove-external-bug + + ids: A single external tracker bug id or list of external tracker bug + ids. + ext_type_id: The external tracker id as used by Bugzilla. + ext_type_description: The external tracker description as used by + Bugzilla. + ext_type_url: The external tracker url as used by Bugzilla. + ext_bz_bug_id: A single external bug id or list of external bug ids + (ie: the bug number in the external tracker). + bug_ids: A single bug id or list of bug ids to have external tracker + info updated. + """ + params = {} + if ids is not None: + params['ids'] = listify(ids) + if ext_type_id is not None: + params['ext_type_id'] = ext_type_id + if ext_type_description is not None: + params['ext_type_description'] = ext_type_description + if ext_type_url is not None: + params['ext_type_url'] = ext_type_url + if ext_bz_bug_id is not None: + params['ext_bz_bug_id'] = listify(ext_bz_bug_id) + if bug_ids is not None: + params['bug_ids'] = listify(bug_ids) + return self._backend.externalbugs_remove(params) diff --git a/bugzilla/bug.py b/bugzilla/bug.py index 366a8c12..15cea004 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -1,71 +1,74 @@ -# base.py - the base classes etc. for a Python interface to bugzilla -# # Copyright (C) 2007, 2008, 2009, 2010 Red Hat Inc. # Author: Will Woods # -# 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 __future__ import unicode_literals -import locale +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import copy from logging import getLogger -import sys +from urllib.parse import urlparse, urlunparse + log = getLogger(__name__) class Bug(object): - '''A container object for a bug report. Requires a Bugzilla instance - + """ + A container object for a bug report. Requires a Bugzilla instance - every Bug is on a Bugzilla, obviously. Optional keyword args: dict=DICT - populate attributes with the result of a getBug() call bug_id=ID - if dict does not contain bug_id, this is required before you can read any attributes or make modifications to this bug. - ''' + """ def __init__(self, bugzilla, bug_id=None, dict=None, autorefresh=False): # pylint: disable=redefined-builtin # API had pre-existing issue that we can't change ('dict' usage) self.bugzilla = bugzilla - self._bug_fields = [] + self._rawdata = {} self.autorefresh = autorefresh + # pylint: disable=protected-access + self._aliases = self.bugzilla._get_bug_aliases() + # pylint: enable=protected-access + if not dict: dict = {} if bug_id: dict["id"] = bug_id - log.debug("Bug(%s)", sorted(dict.keys())) self._update_dict(dict) + self.weburl = self._generate_weburl() - self.weburl = bugzilla.url.replace('xmlrpc.cgi', - 'show_bug.cgi?id=%i' % self.bug_id) + def _generate_weburl(self): + """ + Generate the URL to the bug in the web UI + """ + parsed = urlparse(self.bugzilla.url) + return urlunparse((parsed.scheme, parsed.netloc, + '/show_bug.cgi', '', 'id=%s' % self.bug_id, + '')) def __str__(self): - '''Return a simple string representation of this bug - - This is available only for compatibility. Using 'str(bug)' and - 'print(bug)' is not recommended because of potential encoding issues. - Please use unicode(bug) where possible. - ''' - if sys.version_info[0] >= 3: - return self.__unicode__() - else: - return self.__unicode__().encode( - locale.getpreferredencoding(), 'replace') + """ + Return a simple string representation of this bug + """ + return self.__unicode__() def __unicode__(self): - '''Return a simple unicode string representation of this bug''' + """ + Return a simple unicode string representation of this bug + """ return "#%-6s %-10s - %s - %s" % (self.bug_id, self.bug_status, self.assigned_to, self.summary) def __repr__(self): - return '' % (self.bug_id, self.bugzilla.url, - id(self)) + url = "" + if self.bugzilla: + url = self.bugzilla.url + return '' % (self.bug_id, url, id(self)) def __getattr__(self, name): refreshed = False @@ -75,11 +78,7 @@ def __getattr__(self, name): # have never been called. return self.__dict__[name] - # pylint: disable=protected-access - aliases = self.bugzilla._get_bug_aliases() - # pylint: enable=protected-access - - for newname, oldname in aliases: + for newname, oldname in self._aliases: if name == oldname and newname in self.__dict__: return self.__dict__[newname] @@ -110,47 +109,52 @@ def __getattr__(self, name): "to adjust your include_fields for getbug/query." % name) raise AttributeError(msg) + def get_raw_data(self): + """ + Return the raw API dictionary data that has been used to + populate this bug + """ + return copy.deepcopy(self._rawdata) + def refresh(self, include_fields=None, exclude_fields=None, extra_fields=None): - ''' + """ Refresh the bug with the latest data from bugzilla - ''' + """ # pylint: disable=protected-access + extra_fields = list(self._rawdata.keys()) + (extra_fields or []) r = self.bugzilla._getbug(self.bug_id, include_fields=include_fields, exclude_fields=exclude_fields, - extra_fields=self._bug_fields + (extra_fields or [])) + extra_fields=extra_fields) # pylint: enable=protected-access self._update_dict(r) reload = refresh - def _update_dict(self, newdict): - ''' - Update internal dictionary, in a way that ensures no duplicate - entries are stored WRT field aliases - ''' + def _translate_dict(self, newdict): if self.bugzilla: self.bugzilla.post_translation({}, newdict) - # pylint: disable=protected-access - aliases = self.bugzilla._get_bug_aliases() - # pylint: enable=protected-access - - for newname, oldname in aliases: - if oldname not in newdict: - continue - - if newname not in newdict: - newdict[newname] = newdict[oldname] - elif newdict[newname] != newdict[oldname]: - log.debug("Update dict contained differing alias values " - "d[%s]=%s and d[%s]=%s , dropping the value " - "d[%s]", newname, newdict[newname], oldname, - newdict[oldname], oldname) - del(newdict[oldname]) - - for key in newdict.keys(): - if key not in self._bug_fields: - self._bug_fields.append(key) + for newname, oldname in self._aliases: + if oldname not in newdict: + continue + + if newname not in newdict: + newdict[newname] = newdict[oldname] + elif newdict[newname] != newdict[oldname]: + log.debug("Update dict contained differing alias values " + "d[%s]=%s and d[%s]=%s , dropping the value " + "d[%s]", newname, newdict[newname], oldname, + newdict[oldname], oldname) + del newdict[oldname] + + + def _update_dict(self, newdict): + """ + Update internal dictionary, in a way that ensures no duplicate + entries are stored WRT field aliases + """ + self._translate_dict(newdict) + self._rawdata.update(newdict) self.__dict__.update(newdict) if 'id' not in self.__dict__ and 'bug_id' not in self.__dict__: @@ -162,14 +166,15 @@ def _update_dict(self, newdict): ################## def __getstate__(self): - ret = {} - for key in self._bug_fields: - ret[key] = self.__dict__[key] + ret = self._rawdata.copy() + ret["_aliases"] = self._aliases return ret def __setstate__(self, vals): - self._bug_fields = [] + self._rawdata = {} self.bugzilla = None + self._aliases = vals.get("_aliases", []) + self.autorefresh = False self._update_dict(vals) @@ -178,12 +183,12 @@ def __setstate__(self, vals): ##################### def setstatus(self, status, comment=None, private=False): - ''' + """ Update the status for this bug report. Commonly-used values are ASSIGNED, MODIFIED, and NEEDINFO. To change bugs to CLOSED, use .close() instead. - ''' + """ # Note: fedora bodhi uses this function vals = self.bugzilla.build_update(status=status, comment=comment, @@ -194,7 +199,8 @@ def setstatus(self, status, comment=None, private=False): def close(self, resolution, dupeid=None, fixedin=None, comment=None, isprivate=False): - '''Close this bug. + """ + Close this bug. Valid values for resolution are in bz.querydefaults['resolution_list'] For bugzilla.redhat.com that's: ['NOTABUG', 'WONTFIX', 'DEFERRED', 'WORKSFORME', 'CURRENTRELEASE', @@ -206,14 +212,14 @@ def close(self, resolution, dupeid=None, fixedin=None, version that fixes the bug. You can optionally add a comment while closing the bug. Set 'isprivate' to True if you want that comment to be private. - ''' + """ # Note: fedora bodhi uses this function vals = self.bugzilla.build_update(comment=comment, comment_private=isprivate, resolution=resolution, dupe_of=dupeid, fixed_in=fixedin, - status="CLOSED") + status=str("CLOSED")) log.debug("close: update=%s", vals) return self.bugzilla.update_bugs(self.bug_id, vals) @@ -225,7 +231,7 @@ def close(self, resolution, dupeid=None, fixedin=None, def setassignee(self, assigned_to=None, qa_contact=None, comment=None): - ''' + """ Set any of the assigned_to or qa_contact fields to a new bugzilla account, with an optional comment, e.g. setassignee(assigned_to='wwoods@redhat.com') @@ -235,7 +241,7 @@ def setassignee(self, assigned_to=None, will throw a ValueError. Returns [bug_id, mailresults]. - ''' + """ if not (assigned_to or qa_contact): raise ValueError("You must set one of assigned_to " " or qa_contact") @@ -248,11 +254,11 @@ def setassignee(self, assigned_to=None, return self.bugzilla.update_bugs(self.bug_id, vals) def addcc(self, cclist, comment=None): - ''' + """ Adds the given email addresses to the CC list for this bug. cclist: list of email addresses (strings) comment: optional comment to add to the bug - ''' + """ vals = self.bugzilla.build_update(comment=comment, cc_add=cclist) log.debug("addcc: update=%s", vals) @@ -260,9 +266,9 @@ def addcc(self, cclist, comment=None): return self.bugzilla.update_bugs(self.bug_id, vals) def deletecc(self, cclist, comment=None): - ''' + """ Removes the given email addresses from the CC list for this bug. - ''' + """ vals = self.bugzilla.build_update(comment=comment, cc_remove=cclist) log.debug("deletecc: update=%s", vals) @@ -275,10 +281,10 @@ def deletecc(self, cclist, comment=None): #################### def addcomment(self, comment, private=False): - ''' + """ Add the given comment to this bug. Set private to True to mark this comment as private. - ''' + """ # Note: fedora bodhi uses this function vals = self.bugzilla.build_update(comment=comment, comment_private=private) @@ -286,14 +292,14 @@ def addcomment(self, comment, private=False): return self.bugzilla.update_bugs(self.bug_id, vals) - def getcomments(self): - ''' + def get_comments(self): + """ Returns an array of comment dictionaries for this bug - ''' + """ comment_list = self.bugzilla.get_comments([self.bug_id]) return comment_list['bugs'][str(self.bug_id)]['comments'] - + getcomments = get_comments ##################### # Get/Set bug flags # ##################### @@ -354,6 +360,22 @@ def updateflags(self, flags): self.bugzilla.build_update(flags=flaglist)) + ####################### + # Bug fields handling # + ####################### + + def setsummary(self, summary): + """ + Set the summary of bug to the given summary string + """ + # Create update object + vals = self.bugzilla.build_update(summary=summary) + + log.debug("setsummary: update=%s", vals) + + # Send update to bugzilla and return + return self.bugzilla.update_bugs(self.bug_id, vals) + ######################## # Experimental methods # ######################## @@ -377,18 +399,19 @@ def get_attachment_ids(self): return [a["id"] for a in self.get_attachments(exclude_fields=["data"])] def get_history_raw(self): - ''' + """ Experimental. Get the history of changes for this bug. - ''' + """ return self.bugzilla.bugs_history_raw([self.bug_id]) class User(object): - '''Container object for a bugzilla User. + """ + Container object for a bugzilla User. :arg bugzilla: Bugzilla instance that this User belongs to. Rest of the params come straight from User.get() - ''' + """ def __init__(self, bugzilla, **kwargs): self.bugzilla = bugzilla self.__userid = kwargs.get('id') @@ -440,11 +463,74 @@ def refresh(self): self.__dict__.update(newuser.__dict__) def updateperms(self, action, groups): - ''' + """ A method to update the permissions (group membership) of a bugzilla user. :arg action: add, remove, or set :arg groups: list of groups to be added to (i.e. ['fedora_contrib']) - ''' + """ self.bugzilla.updateperms(self.name, action, groups) + + +class Group(object): + """ + Container object for a bugzilla Group. + + :arg bugzilla: Bugzilla instance that this Group belongs to. + Rest of the params come straight from Group.get() + """ + def __init__(self, bugzilla, **kwargs): + self.bugzilla = bugzilla + self.__groupid = kwargs.get('id') + + self.name = kwargs.get('name') + self.description = kwargs.get('description', self.name) + self.is_active = kwargs.get('is_active', False) + self.icon_url = kwargs.get('icon_url', None) + self.is_active_bug_group = kwargs.get('is_active_bug_group', None) + + self.membership = kwargs.get('membership', []) + self.__member_emails = set() + self._refresh_member_emails_list() + + ######################## + # Read-only attributes # + ######################## + + # We make these properties so that the user cannot set them. They are + # unaffected by the update() method so it would be misleading to let them + # be changed. + @property + def groupid(self): + return self.__groupid + + @property + def member_emails(self): + return sorted(self.__member_emails) + + def _refresh_member_emails_list(self): + """ + Refresh the list of emails of the members of the group. + """ + if self.membership: + for m in self.membership: + if "email" in m: + self.__member_emails.add(m["email"]) + + def refresh(self, membership=False): + """ + Update Group object with latest info from bugzilla + """ + newgroup = self.bugzilla.getgroup( + self.name, membership=membership) + self.__dict__.update(newgroup.__dict__) + self._refresh_member_emails_list() + + def members(self): + """ + Retrieve the members of this Group from bugzilla + """ + if not self.membership: + self.refresh(membership=True) + return self.membership diff --git a/bugzilla/exceptions.py b/bugzilla/exceptions.py new file mode 100644 index 00000000..2562dc45 --- /dev/null +++ b/bugzilla/exceptions.py @@ -0,0 +1,43 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +from requests import HTTPError + + +class BugzillaError(Exception): + """ + Error raised in the Bugzilla client code. + """ + @staticmethod + def get_bugzilla_error_string(exc): + """ + Helper to return the bugzilla instance error message from an + XMLRPC Fault, or any other exception type that's raised from bugzilla + interaction + """ + return getattr(exc, "faultString", str(exc)) + + @staticmethod + def get_bugzilla_error_code(exc): + """ + Helper to return the bugzilla instance error code from an + XMLRPC Fault, or any other exception type that's raised from bugzilla + interaction + """ + for propname in ["faultCode", "code"]: + if hasattr(exc, propname): + return getattr(exc, propname) + return None + + def __init__(self, message, code=None): + """ + :param code: The error code from the remote bugzilla instance. Only + set if the error came directly from the remove bugzilla + """ + self.code = code + if self.code: + message += " (code=%s)" % self.code + Exception.__init__(self, message) + + +class BugzillaHTTPError(HTTPError): + """Error raised in the Bugzilla session""" diff --git a/bugzilla/oldclasses.py b/bugzilla/oldclasses.py index 18169e78..e579fb9f 100644 --- a/bugzilla/oldclasses.py +++ b/bugzilla/oldclasses.py @@ -1,23 +1,64 @@ -# 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. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. from .base import Bugzilla -from .rhbugzilla import RHBugzilla - # These are old compat classes. Nothing new should be added here, # and these should not be altered -class Bugzilla3(Bugzilla): pass -class Bugzilla32(Bugzilla): pass -class Bugzilla34(Bugzilla): pass -class Bugzilla36(Bugzilla): pass -class Bugzilla4(Bugzilla): pass -class Bugzilla42(Bugzilla): pass -class Bugzilla44(Bugzilla): pass -class NovellBugzilla(Bugzilla): pass -class RHBugzilla3(RHBugzilla): pass -class RHBugzilla4(RHBugzilla): pass + +class Bugzilla3(Bugzilla): + pass + + +class Bugzilla32(Bugzilla): + pass + + +class Bugzilla34(Bugzilla): + pass + + +class Bugzilla36(Bugzilla): + pass + + +class Bugzilla4(Bugzilla): + pass + + +class Bugzilla42(Bugzilla): + pass + + +class Bugzilla44(Bugzilla): + pass + + +class NovellBugzilla(Bugzilla): + pass + + +class RHBugzilla(Bugzilla): + """ + Helper class for historical bugzilla.redhat.com back compat + + Historically this class used many more non-upstream methods, but + in 2012 RH started dropping most of its custom bits. By that time, + upstream BZ had most of the important functionality. + + Much of the remaining code here is just trying to keep things operating + in python-bugzilla back compatible manner. + + This class was written using bugzilla.redhat.com's API docs: + https://bugzilla.redhat.com/docs/en/html/api/ + """ + _is_redhat_bugzilla = True + + +class RHBugzilla3(RHBugzilla): + pass + + +class RHBugzilla4(RHBugzilla): + pass diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 04910c2e..10b45946 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -1,351 +1,7 @@ -# rhbugzilla.py - a Python interface to Red Hat Bugzilla using xmlrpclib. -# -# Copyright (C) 2008-2012 Red Hat Inc. -# Author: Will Woods -# -# 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. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. -from logging import getLogger +# This class needs to live in rhbugzilla.py to preserve historical +# 'bugzilla.rhbugzilla' import compat -from .base import Bugzilla - -log = getLogger(__name__) - - -class RHBugzilla(Bugzilla): - ''' - Bugzilla class for connecting Red Hat's forked bugzilla instance, - bugzilla.redhat.com - - Historically this class used many more non-upstream methods, but - in 2012 RH started dropping most of its custom bits. By that time, - upstream BZ had most of the important functionality. - - Much of the remaining code here is just trying to keep things operating - in python-bugzilla back compatible manner. - - This class was written using bugzilla.redhat.com's API docs: - https://bugzilla.redhat.com/docs/en/html/api/ - ''' - def _init_class_state(self): - def _add_both_alias(newname, origname): - self._add_field_alias(newname, origname, is_api=False) - self._add_field_alias(origname, newname, is_bug=False) - - _add_both_alias('fixed_in', 'cf_fixed_in') - _add_both_alias('qa_whiteboard', 'cf_qa_whiteboard') - _add_both_alias('devel_whiteboard', 'cf_devel_whiteboard') - _add_both_alias('internal_whiteboard', 'cf_internal_whiteboard') - - self._add_field_alias('component', 'components', is_bug=False) - self._add_field_alias('version', 'versions', is_bug=False) - self._add_field_alias('sub_component', 'sub_components', is_bug=False) - - # flags format isn't exactly the same but it's the closest approx - self._add_field_alias('flags', 'flag_types') - - self._getbug_extra_fields = self._getbug_extra_fields + [ - "comments", "description", - "external_bugs", "flags", "sub_components", - "tags", - ] - self._supports_getbug_extra_fields = True - - - ###################### - # Bug update methods # - ###################### - - def build_update(self, **kwargs): - # pylint: disable=arguments-differ - adddict = {} - - def pop(key, destkey): - val = kwargs.pop(key, None) - if val is None: - return - adddict[destkey] = val - - def get_sub_component(): - val = kwargs.pop("sub_component", None) - if val is None: - return - - if not isinstance(val, dict): - component = self._listify(kwargs.get("component")) - if not component: - raise ValueError("component must be specified if " - "specifying sub_component") - val = {component[0]: val} - adddict["sub_components"] = val - - def get_alias(): - # RHBZ has a custom extension to allow a bug to have multiple - # aliases, so the format of aliases is - # {"add": [...], "remove": [...]} - # But that means in order to approximate upstream, behavior - # which just overwrites the existing alias, we need to read - # the bug's state first to know what string to remove. Which - # we can't do, since we don't know the bug numbers at this point. - # So fail for now. - # - # The API should provide {"set": [...]} - # https://bugzilla.redhat.com/show_bug.cgi?id=1173114 - # - # Implementation will go here when it's available - pass - - pop("fixed_in", "cf_fixed_in") - pop("qa_whiteboard", "cf_qa_whiteboard") - pop("devel_whiteboard", "cf_devel_whiteboard") - pop("internal_whiteboard", "cf_internal_whiteboard") - - get_sub_component() - get_alias() - - vals = Bugzilla.build_update(self, **kwargs) - vals.update(adddict) - - return vals - - def add_external_tracker(self, bug_ids, ext_bz_bug_id, ext_type_id=None, - ext_type_description=None, ext_type_url=None, - ext_status=None, ext_description=None, - ext_priority=None): - """ - Wrapper method to allow adding of external tracking bugs using the - ExternalBugs::WebService::add_external_bug method. - - This is documented at - https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#add_external_bug - - bug_ids: A single bug id or list of bug ids to have external trackers - added. - ext_bz_bug_id: The external bug id (ie: the bug number in the - external tracker). - ext_type_id: The external tracker id as used by Bugzilla. - ext_type_description: The external tracker description as used by - Bugzilla. - ext_type_url: The external tracker url as used by Bugzilla. - ext_status: The status of the external bug. - ext_description: The description of the external bug. - ext_priority: The priority of the external bug. - """ - param_dict = {'ext_bz_bug_id': ext_bz_bug_id} - if ext_type_id is not None: - param_dict['ext_type_id'] = ext_type_id - if ext_type_description is not None: - param_dict['ext_type_description'] = ext_type_description - if ext_type_url is not None: - param_dict['ext_type_url'] = ext_type_url - if ext_status is not None: - param_dict['ext_status'] = ext_status - if ext_description is not None: - param_dict['ext_description'] = ext_description - if ext_priority is not None: - param_dict['ext_priority'] = ext_priority - params = { - 'bug_ids': self._listify(bug_ids), - 'external_bugs': [param_dict], - } - - log.debug("Calling ExternalBugs.add_external_bug(%s)", params) - return self._proxy.ExternalBugs.add_external_bug(params) - - def update_external_tracker(self, ids=None, ext_type_id=None, - ext_type_description=None, ext_type_url=None, - ext_bz_bug_id=None, bug_ids=None, - ext_status=None, ext_description=None, - ext_priority=None): - """ - Wrapper method to allow adding of external tracking bugs using the - ExternalBugs::WebService::update_external_bug method. - - This is documented at - https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#update_external_bug - - ids: A single external tracker bug id or list of external tracker bug - ids. - ext_type_id: The external tracker id as used by Bugzilla. - ext_type_description: The external tracker description as used by - Bugzilla. - ext_type_url: The external tracker url as used by Bugzilla. - ext_bz_bug_id: A single external bug id or list of external bug ids - (ie: the bug number in the external tracker). - bug_ids: A single bug id or list of bug ids to have external tracker - info updated. - ext_status: The status of the external bug. - ext_description: The description of the external bug. - ext_priority: The priority of the external bug. - """ - params = {} - if ids is not None: - params['ids'] = self._listify(ids) - if ext_type_id is not None: - params['ext_type_id'] = ext_type_id - if ext_type_description is not None: - params['ext_type_description'] = ext_type_description - if ext_type_url is not None: - params['ext_type_url'] = ext_type_url - if ext_bz_bug_id is not None: - params['ext_bz_bug_id'] = self._listify(ext_bz_bug_id) - if bug_ids is not None: - params['bug_ids'] = self._listify(bug_ids) - if ext_status is not None: - params['ext_status'] = ext_status - if ext_description is not None: - params['ext_description'] = ext_description - if ext_priority is not None: - params['ext_priority'] = ext_priority - - log.debug("Calling ExternalBugs.update_external_bug(%s)", params) - return self._proxy.ExternalBugs.update_external_bug(params) - - def remove_external_tracker(self, ids=None, ext_type_id=None, - ext_type_description=None, ext_type_url=None, - ext_bz_bug_id=None, bug_ids=None): - """ - Wrapper method to allow removal of external tracking bugs using the - ExternalBugs::WebService::remove_external_bug method. - - This is documented at - https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#remove_external_bug - - ids: A single external tracker bug id or list of external tracker bug - ids. - ext_type_id: The external tracker id as used by Bugzilla. - ext_type_description: The external tracker description as used by - Bugzilla. - ext_type_url: The external tracker url as used by Bugzilla. - ext_bz_bug_id: A single external bug id or list of external bug ids - (ie: the bug number in the external tracker). - bug_ids: A single bug id or list of bug ids to have external tracker - info updated. - """ - params = {} - if ids is not None: - params['ids'] = self._listify(ids) - if ext_type_id is not None: - params['ext_type_id'] = ext_type_id - if ext_type_description is not None: - params['ext_type_description'] = ext_type_description - if ext_type_url is not None: - params['ext_type_url'] = ext_type_url - if ext_bz_bug_id is not None: - params['ext_bz_bug_id'] = self._listify(ext_bz_bug_id) - if bug_ids is not None: - params['bug_ids'] = self._listify(bug_ids) - - log.debug("Calling ExternalBugs.remove_external_bug(%s)", params) - return self._proxy.ExternalBugs.remove_external_bug(params) - - - ################# - # Query methods # - ################# - - def pre_translation(self, query): - '''Translates the query for possible aliases''' - old = query.copy() - - if 'bug_id' in query: - if not isinstance(query['bug_id'], list): - query['id'] = query['bug_id'].split(',') - else: - query['id'] = query['bug_id'] - del query['bug_id'] - - if 'component' in query: - if not isinstance(query['component'], list): - query['component'] = query['component'].split(',') - - if 'include_fields' not in query and 'column_list' not in query: - return - - if 'include_fields' not in query: - query['include_fields'] = [] - if 'column_list' in query: - query['include_fields'] = query['column_list'] - del query['column_list'] - - # We need to do this for users here for users that - # don't call build_query - query.update(self._process_include_fields(query["include_fields"], - None, None)) - - if old != query: - log.debug("RHBugzilla pretranslated query to: %s", query) - - def post_translation(self, query, bug): - ''' - Convert the results of getbug back to the ancient RHBZ value - formats - ''' - ignore = query - - # RHBZ _still_ returns component and version as lists, which - # deviates from upstream. Copy the list values to components - # and versions respectively. - if 'component' in bug and "components" not in bug: - val = bug['component'] - bug['components'] = isinstance(val, list) and val or [val] - bug['component'] = bug['components'][0] - - if 'version' in bug and "versions" not in bug: - val = bug['version'] - bug['versions'] = isinstance(val, list) and val or [val] - bug['version'] = bug['versions'][0] - - # sub_components isn't too friendly of a format, add a simpler - # sub_component value - if 'sub_components' in bug and 'sub_component' not in bug: - val = bug['sub_components'] - bug['sub_component'] = "" - if isinstance(val, dict): - values = [] - for vallist in val.values(): - values += vallist - bug['sub_component'] = " ".join(values) - - def build_external_tracker_boolean_query(self, *args, **kwargs): - ignore1 = args - ignore2 = kwargs - raise RuntimeError("Building external boolean queries is " - "no longer supported. Please build a URL query " - "via the bugzilla web UI and pass it to 'query --from-url' " - "or url_to_query()") - - - def build_query(self, **kwargs): - # pylint: disable=arguments-differ - - # We previously accepted a text format to approximate boolean - # queries, and only for RHBugzilla. Upstream bz has --from-url - # support now, so point people to that instead so we don't have - # to document and maintain this logic anymore - def _warn_bool(kwkey): - vallist = self._listify(kwargs.get(kwkey, None)) - for value in vallist or []: - for s in value.split(" "): - if s not in ["|", "&", "!"]: - continue - log.warning("%s value '%s' appears to use the now " - "unsupported boolean formatting, your query may " - "be incorrect. If you need complicated URL queries, " - "look into bugzilla --from-url/url_to_query().", - kwkey, value) - return - - _warn_bool("fixed_in") - _warn_bool("blocked") - _warn_bool("dependson") - _warn_bool("flag") - _warn_bool("qa_whiteboard") - _warn_bool("devel_whiteboard") - _warn_bool("alias") - - return Bugzilla.build_query(self, **kwargs) +from .oldclasses import RHBugzilla # pylint: disable=unused-import diff --git a/bugzilla/transport.py b/bugzilla/transport.py deleted file mode 100644 index 92cc3fa6..00000000 --- a/bugzilla/transport.py +++ /dev/null @@ -1,191 +0,0 @@ -# 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 logging import getLogger -import sys - -if sys.version_info[0] >= 3: - # pylint: disable=import-error,no-name-in-module - from configparser import SafeConfigParser - from urllib.parse import urlparse - from xmlrpc.client import Fault, ProtocolError, ServerProxy, Transport -else: - from ConfigParser import SafeConfigParser - from urlparse import urlparse # pylint: disable=ungrouped-imports - from xmlrpclib import Fault, ProtocolError, ServerProxy, Transport - -import requests - - -log = getLogger(__name__) - - -class BugzillaError(Exception): - '''Error raised in the Bugzilla client code.''' - pass - - -class _BugzillaTokenCache(object): - """ - Cache for tokens, including, with apologies for the duplicative - terminology, both Bugzilla Tokens and API Keys. - """ - - def __init__(self, uri, tokenfilename): - self.tokenfilename = tokenfilename - self.tokenfile = SafeConfigParser() - self.domain = urlparse(uri)[1] - - if self.tokenfilename: - self.tokenfile.read(self.tokenfilename) - - if self.domain not in self.tokenfile.sections(): - self.tokenfile.add_section(self.domain) - - @property - def value(self): - if self.tokenfile.has_option(self.domain, 'token'): - return self.tokenfile.get(self.domain, 'token') - else: - return None - - @value.setter - def value(self, value): - if self.value == value: - return - - if value is None: - self.tokenfile.remove_option(self.domain, 'token') - else: - self.tokenfile.set(self.domain, 'token', value) - - if self.tokenfilename: - with open(self.tokenfilename, 'w') as tokenfile: - log.debug("Saving to tokenfile") - self.tokenfile.write(tokenfile) - - def __repr__(self): - return '' % self.value - - -class _BugzillaServerProxy(ServerProxy, object): - def __init__(self, uri, tokenfile, *args, **kwargs): - super(_BugzillaServerProxy, self).__init__(uri, *args, **kwargs) - self.token_cache = _BugzillaTokenCache(uri, tokenfile) - self.api_key = None - - def use_api_key(self, api_key): - self.api_key = api_key - - def clear_token(self): - self.token_cache.value = None - - def __request(self, methodname, params): - if len(params) == 0: - params = ({}, ) - - if self.api_key is not None: - if 'Bugzilla_api_key' not in params[0]: - params[0]['Bugzilla_api_key'] = self.api_key - elif self.token_cache.value is not None: - if 'Bugzilla_token' not in params[0]: - params[0]['Bugzilla_token'] = self.token_cache.value - - ret = super(_BugzillaServerProxy, self).__request(methodname, params) - - if isinstance(ret, dict) and 'token' in ret.keys(): - self.token_cache.value = ret.get('token') - return ret - - -class _RequestsTransport(Transport): - user_agent = 'Python/Bugzilla' - - def __init__(self, url, cookiejar=None, - sslverify=True, sslcafile=None, debug=0): - # pylint: disable=W0231 - # pylint does not handle multiple import of Transport well - if hasattr(Transport, "__init__"): - Transport.__init__(self, use_datetime=False) - - self.verbose = debug - self._cookiejar = cookiejar - - # transport constructor needs full url too, as xmlrpc does not pass - # scheme to request - self.scheme = urlparse(url)[0] - if self.scheme not in ["http", "https"]: - raise Exception("Invalid URL scheme: %s (%s)" % (self.scheme, url)) - - self.use_https = self.scheme == 'https' - - self.request_defaults = { - 'cert': sslcafile if self.use_https else None, - 'cookies': cookiejar, - 'verify': sslverify, - 'headers': { - 'Content-Type': 'text/xml', - 'User-Agent': self.user_agent, - } - } - - # Using an explicit Session, rather than requests.get, will use - # HTTP KeepAlive if the server supports it. - self.session = requests.Session() - - def parse_response(self, response): - """ Parse XMLRPC response """ - parser, unmarshaller = self.getparser() - parser.feed(response.text.encode('utf-8')) - parser.close() - return unmarshaller.close() - - def _request_helper(self, url, request_body): - """ - A helper method to assist in making a request and provide a parsed - response. - """ - response = None - try: - response = self.session.post( - url, data=request_body, **self.request_defaults) - - # We expect utf-8 from the server - response.encoding = 'UTF-8' - - # update/set any cookies - if self._cookiejar is not None: - for cookie in response.cookies: - self._cookiejar.set_cookie(cookie) - - if self._cookiejar.filename is not None: - # Save is required only if we have a filename - self._cookiejar.save() - - response.raise_for_status() - return self.parse_response(response) - except requests.RequestException: - e = sys.exc_info()[1] - if not response: - raise - raise ProtocolError( - url, response.status_code, str(e), response.headers) - except Fault: - raise sys.exc_info()[1] - except Exception: - # pylint: disable=W0201 - e = BugzillaError(str(sys.exc_info()[1])) - e.__traceback__ = sys.exc_info()[2] - raise e - - def request(self, host, handler, request_body, verbose=0): - self.verbose = verbose - url = "%s://%s%s" % (self.scheme, host, handler) - - # xmlrpclib fails to escape \r - request_body = request_body.replace(b'\r', b' ') - - return self._request_helper(url, request_body) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..4aa11c0f --- /dev/null +++ b/codecov.yml @@ -0,0 +1,6 @@ +# These files will only get full coverage from running the functional +# tests, but those aren't run from CI +ignore: + - "bugzilla/_backendrest.py" + - "bugzilla/_backendxmlrpc.py" + - "bugzilla/_session.py" diff --git a/examples/apikey.py b/examples/apikey.py index 95ed3412..021d70fb 100644 --- a/examples/apikey.py +++ b/examples/apikey.py @@ -1,15 +1,10 @@ #!/usr/bin/env python # -# 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. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. -# create.py: Create a new bug report - -from __future__ import print_function -import sys +# apikey.py: Demostrate prompting for API key and passing it to Bugzilla +# pylint: disable=undefined-variable import bugzilla @@ -21,13 +16,10 @@ " https://landfill.bugzilla.org/bugzilla-5.0-branch/userprefs.cgi") print("This is a test site, so no harm will come!\n") -if sys.version_info[0] >= 3: - api_key = input("Enter Bugzilla API Key: ") -else: - api_key = raw_input("Enter Bugzilla API Key: ") +api_key = input("Enter Bugzilla API Key: ") # API key usage assumes the API caller is storing the API key; if you would # like to use one of the login options that stores credentials on-disk for -# command-line usage, use tokens or cookies. +# command-line usage, use login tokens. bzapi = bugzilla.Bugzilla(URL, api_key=api_key) assert bzapi.logged_in diff --git a/examples/bug_autorefresh.py b/examples/bug_autorefresh.py index 66984d01..a8aa6728 100644 --- a/examples/bug_autorefresh.py +++ b/examples/bug_autorefresh.py @@ -1,20 +1,15 @@ #!/usr/bin/env python # -# 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. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. # bug_autorefresh.py: Show what bug_autorefresh is all about, and explain # how to handle the default change via python-bugzilla in 2016 -from __future__ import print_function - import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes -URL = "partner-bugzilla.redhat.com" +URL = "bugzilla.stage.redhat.com" bzapi = bugzilla.Bugzilla(URL) # The Bugzilla.bug_autorefresh setting controls whether bugs will @@ -54,6 +49,6 @@ # Why does this matter? Some scripts are implicitly depending on this # auto-refresh behavior, because their include_fields specification doesn't # cover all attributes they actually use. Your script will work, sure, but -# it's likely doing many more XML-RPC calls than needed, possibly 1 per bug. +# it's likely doing many more API calls than needed, possibly 1 per bug. # So if after upgrading python-bugzilla you start hitting issues, the # recommendation is to fix your include_fields. diff --git a/examples/create.py b/examples/create.py index 727600f4..124a93b0 100644 --- a/examples/create.py +++ b/examples/create.py @@ -1,15 +1,10 @@ #!/usr/bin/env python # -# 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. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. # create.py: Create a new bug report -from __future__ import print_function - import time import bugzilla @@ -17,8 +12,8 @@ # public test instance of bugzilla.redhat.com. # # Don't worry, changing things here is fine, and won't send any email to -# users or anything. It's what partner-bugzilla.redhat.com is for! -URL = "partner-bugzilla.redhat.com" +# users or anything. It's what bugzilla.stage.redhat.com is for! +URL = "bugzilla.stage.redhat.com" bzapi = bugzilla.Bugzilla(URL) if not bzapi.logged_in: print("This example requires cached login credentials for %s" % URL) @@ -28,7 +23,7 @@ # Similar to build_query, build_createbug is a helper function that handles # some bugzilla version incompatibility issues. All it does is return a # properly formatted dict(), and provide friendly parameter names. -# The argument names map to those accepted by XMLRPC Bug.create: +# The argument names map to those accepted by Bugzilla Bug.create: # https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug # # The arguments specified here are mandatory, but there are many other diff --git a/examples/getbug.py b/examples/getbug.py index b84a43a0..f164c0a1 100644 --- a/examples/getbug.py +++ b/examples/getbug.py @@ -1,29 +1,24 @@ #!/usr/bin/env python # -# 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. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. # getbug.py: Simple demonstration of connecting to bugzilla, fetching # a bug, and printing some details. -from __future__ import print_function - import pprint import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes -URL = "partner-bugzilla.redhat.com" +URL = "bugzilla.stage.redhat.com" bzapi = bugzilla.Bugzilla(URL) # getbug() is just a simple wrapper around getbugs(), which takes a list # IDs, if you need to fetch multiple # -# Example bug: https://partner-bugzilla.redhat.com/show_bug.cgi?id=427301 +# Example bug: https://bugzilla.stage.redhat.com/show_bug.cgi?id=427301 bug = bzapi.getbug(427301) print("Fetched bug #%s:" % bug.id) print(" Product = %s" % bug.product) @@ -38,8 +33,8 @@ # comments must be fetched separately on stock bugzilla. this just returns # a raw dict with all the info. -comments = bug.getcomments() +comments = bug.get_comments() print("\nLast comment data:\n%s" % pprint.pformat(comments[-1])) -# getcomments is just a wrapper around bzapi.get_comments(), which can be +# get_comments is just a wrapper around bzapi.get_comments(), which can be # used for bulk comments fetching diff --git a/examples/getbug_restapi.py b/examples/getbug_restapi.py new file mode 100644 index 00000000..1cb4e797 --- /dev/null +++ b/examples/getbug_restapi.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +# getbug_restapi.py: +# Simple demonstration of connecting to bugzilla over the REST +# API and printing some bug details. + +import bugzilla + +# public test instance of bugzilla.redhat.com. It's okay to make changes +URL = "bugzilla.stage.redhat.com" + +# By default, if plain Bugzilla(URL) is invoked, the Bugzilla class will +# attempt to determine if XMLRPC or REST API is available, with a preference +# for XMLRPC for back compatability. But you can use the REST API +# with force_rest=True +bzapi = bugzilla.Bugzilla(URL, force_rest=True) + +# After that, the bugzilla API can be used as normal. See getbug.py for +# some more info here. +bug = bzapi.getbug(427301) +print("Fetched bug #%s:" % bug.id) +print(" Product = %s" % bug.product) +print(" Component = %s" % bug.component) +print(" Status = %s" % bug.status) +print(" Resolution= %s" % bug.resolution) +print(" Summary = %s" % bug.summary) diff --git a/examples/query.py b/examples/query.py index 2da9893b..05ad7026 100644 --- a/examples/query.py +++ b/examples/query.py @@ -1,21 +1,16 @@ #!/usr/bin/env python # -# 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. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. # query.py: Perform a few varieties of queries -from __future__ import print_function - import time import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes -URL = "partner-bugzilla.redhat.com" +URL = "bugzilla.stage.redhat.com" bzapi = bugzilla.Bugzilla(URL) @@ -23,7 +18,7 @@ # build_query is a helper function that handles some bugzilla version # incompatibility issues. All it does is return a properly formatted # dict(), and provide friendly parameter names. The param names map -# to those accepted by XMLRPC Bug.search: +# to those accepted by Bugzilla Bug.search: # https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs query = bzapi.build_query( product="Fedora", @@ -60,7 +55,7 @@ # bugzilla.redhat.com, and bugzilla >= 5.0 support queries using the same # format as is used for 'advanced' search URLs via the Web UI. For example, -# I go to partner-bugzilla.redhat.com -> Search -> Advanced Search, select +# I go to bugzilla.stage.redhat.com -> Search -> Advanced Search, select # Classification=Fedora # Product=Fedora # Component=python-bugzilla @@ -70,7 +65,7 @@ # # Run that, copy the URL and bring it here, pass it to url_to_query to # convert it to a dict(), and query as usual -query = bzapi.url_to_query("https://partner-bugzilla.redhat.com/" +query = bzapi.url_to_query("https://bugzilla.stage.redhat.com/" "buglist.cgi?classification=Fedora&component=python-bugzilla&" "f1=creation_ts&o1=lessthaneq&order=Importance&product=Fedora&" "query_format=advanced&v1=2010-01-01") diff --git a/examples/redhat_query_all.py b/examples/redhat_query_all.py new file mode 100644 index 00000000..9ec5f26f --- /dev/null +++ b/examples/redhat_query_all.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +# redhat_query_all.py: Perform a few varieties of queries + +import bugzilla + +# public test instance of bugzilla.redhat.com. It's okay to make changes +URL = "bugzilla.stage.redhat.com" + +bzapi = bugzilla.Bugzilla(URL) + + +# In late 2021, bugzilla.redhat.com changed query() results to default to +# returning only 20 bugs. If the user passes in limit=0, that number changes +# to 1000, but is still capped if the query would return more than that. +# +# There's a discussion here with multiple proposed ways to work around it: +# https://github.com/python-bugzilla/python-bugzilla/issues/149 +# +# This method uses ids_only=True, which is a custom bugzilla.redhat.com +# query feature to bypass the query limit by only returning matching bug IDs. +# rhbz feature bug: https://bugzilla.redhat.com/show_bug.cgi?id=2005153 + + +# As of Feb 2024 this 1300+ bugs, which would have hit the query limit of 1000 +query = bzapi.build_query( + product="Fedora", + component="virt-manager") +# Request the bugzilla.redhat.com extension ids_only=True to bypass limit +query["ids_only"] = True + +queried_bugs = bzapi.query(query) +ids = [bug.id for bug in queried_bugs] +print(f"Queried {len(ids)} ids") + + +# Use getbugs to fetch the full list. getbugs is not affected by +# default RHBZ limits. However, requesting too much data via getbugs +# will timeout. This paginates the lookup to query 1000 bugs at a time. +# +# We also limit the returned data to just give us the `summary`. +# You should always limit your queries with include_fields` to only return +# the data you need. +count = 0 +pagesize = 1000 +include_fields = ["summary"] +while count < len(ids): + idslice = ids[count:(count + pagesize)] + print(f"Fetching data for bugs {count}-{count+len(idslice)-1}") + bugs = bzapi.getbugs(idslice, include_fields=include_fields) + print(f"Fetched {len(bugs)} bugs") + count += pagesize diff --git a/examples/update.py b/examples/update.py index 7e07d266..86b1967c 100644 --- a/examples/update.py +++ b/examples/update.py @@ -1,21 +1,16 @@ #!/usr/bin/env python # -# 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. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. # update.py: Make changes to an existing bug -from __future__ import print_function - import time import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes -URL = "partner-bugzilla.redhat.com" +URL = "bugzilla.stage.redhat.com" bzapi = bugzilla.Bugzilla(URL) if not bzapi.logged_in: print("This example requires cached login credentials for %s" % URL) @@ -25,12 +20,12 @@ # Similar to build_query, build_update is a helper function that handles # some bugzilla version incompatibility issues. All it does is return a # properly formatted dict(), and provide friendly parameter names. -# The param names map to those accepted by XMLRPC Bug.update: +# The param names map to those accepted by Bugzilla Bug.update: # https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#update-bug # -# Example bug: https://partner-bugzilla.redhat.com/show_bug.cgi?id=427301 +# Example bug: https://bugzilla.stage.redhat.com/show_bug.cgi?id=427301 # Don't worry, changing things here is fine, and won't send any email to -# users or anything. It's what partner-bugzilla.redhat.com is for! +# users or anything. It's what bugzilla.stage.redhat.com is for! bug = bzapi.getbug(427301) print("Bug id=%s original summary=%s" % (bug.id, bug.summary)) @@ -43,7 +38,7 @@ # Now let's add a comment -comments = bug.getcomments() +comments = bug.get_comments() print("Bug originally has %d comments" % len(comments)) update = bzapi.build_update(comment="new example comment %s" % time.time()) @@ -51,7 +46,7 @@ # refresh() actually isn't required here because comments are fetched # on demand -comments = bug.getcomments() +comments = bug.get_comments() print("Bug now has %d comments. Last comment=%s" % (len(comments), comments[-1]["text"])) diff --git a/man/bugzilla.1 b/man/bugzilla.1 new file mode 100644 index 00000000..fba29568 --- /dev/null +++ b/man/bugzilla.1 @@ -0,0 +1,657 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "BUGZILLA" 1 "" "" "User Commands" +.SH NAME +bugzilla \- command line tool for interacting with Bugzilla +.SH SYNOPSIS +.sp +\fBbugzilla\fP [\fIoptions\fP] [\fIcommand\fP] [\fIcommand\-options\fP] +.SH DESCRIPTION +.sp +\fBbugzilla\fP is a command line tool for interacting with a Bugzilla +instance over REST or XMLRPC. +.nf + +\fIcommand\fP is one of: +* login \- log into the given bugzilla instance +* new \- create a new bug +* query \- search for bugs matching given criteria +* modify \- modify existing bugs +* attach \- attach files to existing bugs, or get attachments +* info \- get info about the given bugzilla instance +.fi +.sp +.SH GLOBAL OPTIONS +.SS \fB\-\-help, \-h\fP +.sp +\fBSyntax:\fP \fB\-h\fP +.sp +show this help message and exit +.SS \fB\-\-bugzilla\fP +.sp +\fBSyntax:\fP \fB\-\-bugzilla\fP BUGZILLA +.sp +The bugzilla URL. Full API URLs are typically like: +.nf + +* \fI\%https://bugzilla.example.com/xmlrpc.cgi\fP # XMLRPC API +* \fI\%https://bugzilla.example.com/rest/\fP # REST API + +.fi +.sp +.sp +If a non\-specific URL is passed, like \(aqbugzilla.redhat.com\(aq, \fBbugzilla\fP +will try to probe whether the expected XMLRPC or REST path is available, +preferring XMLRPC for backwards compatibility. +.sp +The default URL \fI\%https://bugzilla.redhat.com\fP +.SS \fB\-\-nosslverify\fP +.sp +\fBSyntax:\fP \fB\-\-nosslverify\fP +.sp +Don\(aqt error on invalid bugzilla SSL certificate +.SS \fB\-\-cert\fP +.sp +\fBSyntax:\fP \fB\-\-cert\fP CERTFILE +.sp +client side certificate file needed by the webserver. +.SS \fB\-\-login\fP +.sp +\fBSyntax:\fP \fB\-\-login\fP +.sp +Run interactive \(dqlogin\(dq before performing the specified command. +.SS \fB\-\-username\fP +.sp +\fBSyntax:\fP \fB\-\-username\fP USERNAME +.sp +Log in with this username +.SS \fB\-\-password\fP +.sp +\fBSyntax:\fP \fB\-\-password\fP PASSWORD +.sp +Log in with this password +.SS \fB\-\-restrict\-login\fP +.sp +\fBSyntax:\fP \fB\-\-restrict\-login\fP +.sp +The session (login token) will be restricted to the current IP +address. +.SS \fB\-\-ensure\-logged\-in\fP +.sp +\fBSyntax:\fP \fB\-\-ensure\-logged\-in\fP +.sp +Raise an error if we aren\(aqt logged in to bugzilla. Consider using +this if you are depending on cached credentials, to ensure that when +they expire the tool errors, rather than subtly change output. +.SS \fB\-\-no\-cache\-credentials\fP +.sp +\fBSyntax:\fP \fB\-\-no\-cache\-credentials\fP +.sp +Don\(aqt save any bugzilla tokens to disk, and don\(aqt use any +pre\-existing credentials. +.SS \fB\-\-tokenfile\fP +.sp +\fBSyntax:\fP \fB\-\-tokenfile\fP TOKENFILE +.sp +token file to use for bugzilla authentication +.SS \fB\-\-verbose\fP +.sp +\fBSyntax:\fP \fB\-\-verbose\fP +.sp +give more info about what\(aqs going on +.SS \fB\-\-debug\fP +.sp +\fBSyntax:\fP \fB\-\-debug\fP +.sp +output bunches of debugging info +.SS \fB\-\-version\fP +.sp +\fBSyntax:\fP \fB\-\-version\fP +.sp +show program\(aqs version number and exit +.SH STANDARD BUGZILLA OPTIONS +.sp +These options are shared by some combination of the \(aqnew\(aq, \(aqquery\(aq, and +\(aqmodify\(aq sub commands. Not every option works for each command though. +.SS \fB\-p, \-\-product\fP +.sp +\fBSyntax:\fP \fB\-\-product\fP PRODUCT +.sp +Product name +.SS \fB\-v, \-\-version\fP +.sp +\fBSyntax:\fP \fB\-\-version\fP VERSION +.sp +Product version +.SS \fB\-c, \-\-component\fP +.sp +\fBSyntax:\fP \fB\-\-component\fP COMPONENT +.sp +Component name +.SS \fB\-s, \-\-summary\fP +.sp +\fBSyntax:\fP \fB\-\-summary\fP SUMMARY +.sp +Bug summary +.SS \fB\-l, \-\-comment\fP +.sp +\fBSyntax:\fP \fB\-\-comment\fP DESCRIPTION +.sp +Set initial bug comment/description +.SS \fB\-\-comment\-tag\fP +.sp +\fBSyntax:\fP \fB\-\-comment\-tag\fP TAG +.sp +Comment tag for the new comment +.SS \fB\-\-sub\-component\fP +.sp +\fBSyntax:\fP \fB\-\-sub\-component\fP SUB_COMPONENT +.sp +RHBZ sub component name +.SS \fB\-o, \-\-os\fP +.sp +\fBSyntax:\fP \fB\-\-os\fP OS +.sp +Operating system +.SS \fB\-\-arch\fP +.sp +\fBSyntax:\fP \fB\-\-arch\fP ARCH +.sp +Arch this bug occurs on +.SS \fB\-x, \-\-severity\fP +.sp +\fBSyntax:\fP \fB\-\-severity\fP SEVERITY +.sp +Bug severity +.SS \fB\-z, \-\-priority\fP +.sp +\fBSyntax:\fP \fB\-\-priority\fP PRIORITY +.sp +Bug priority +.SS \fB\-\-alias\fP +.sp +\fBSyntax:\fP \fB\-\-alias\fP ALIAS +.sp +Bug alias (name) +.SS \fB\-s, \-\-status\fP +.sp +\fBSyntax:\fP \fB\-\-status\fP STATUS +.sp +Bug status (NEW, ASSIGNED, etc.) +.SS \fB\-u, \-\-url\fP +.sp +\fBSyntax:\fP \fB\-\-url\fP URL +.sp +URL for further bug info +.SS \fB\-m \-\-target_milestone\fP +.sp +\fBSyntax:\fP \fB\-\-target_milestone\fP TARGET_MILESTONE +.sp +Target milestone +.SS \fB\-\-target_release\fP +.sp +\fBSyntax:\fP \fB\-\-target_release\fP TARGET_RELEASE +.sp +RHBZ Target release +.SS \fB\-\-blocked\fP +.sp +\fBSyntax:\fP \fB\&...]\fP +.sp +Bug IDs that this bug blocks +.SS \fB\-\-dependson\fP +.sp +\fBSyntax:\fP \fB\&...]\fP +.sp +Bug IDs that this bug depends on +.SS \fB\-\-keywords\fP +.sp +\fBSyntax:\fP \fB\&...]\fP +.sp +Bug keywords +.SS \fB\-\-groups\fP +.sp +\fBSyntax:\fP \fB\&...]\fP +.sp +Which user groups can view this bug +.SS \fB\-\-cc\fP +.sp +\fBSyntax:\fP \fB\&...]\fP +.sp +CC list +.SS \fB\-a, \-\-assignee, \-\-assigned_to\fP +.sp +\fBSyntax:\fP \fB\-\-assigned_to\fP ASSIGNED_TO +.sp +Bug assignee +.SS \fB\-q, \-\-qa_contact\fP +.sp +\fBSyntax:\fP \fB\-\-qa_contact\fP QA_CONTACT +.sp +QA contact +.SS \fB\-f, \-\-flag\fP +.sp +\fBSyntax:\fP \fB\-\-flag\fP FLAG +.sp +Set or unset a flag. For example, to set a flag named devel_ack, do +\-\-flag devel_ack+ Unset a flag with the \(aqX\(aq value, like \-\-flag +needinfoX +.SS \fB\-\-tags\fP +.sp +\fBSyntax:\fP \fB\-\-tags\fP TAG +.sp +Set (personal) tags field +.SS \fB\-w, \-\-whiteboard\fP +.sp +\fBSyntax:\fP \fB\-\-whiteboard\fP WHITEBOARD +.sp +Whiteboard field +.SS \fB\-\-devel_whiteboard\fP +.sp +\fBSyntax:\fP \fB\-\-devel_whiteboard\fP DEVEL_WHITEBOARD +.sp +RHBZ devel whiteboard field +.SS \fB\-\-internal_whiteboard\fP +.sp +\fBSyntax:\fP \fB\-\-internal_whiteboard\fP INTERNAL_WHITEBOARD +.sp +RHBZ internal whiteboard field +.SS \fB\-\-qa_whiteboard\fP +.sp +\fBSyntax:\fP \fB\-\-qa_whiteboard\fP QA_WHITEBOARD +.sp +RHBZ QA whiteboard field +.SS \fB\-F, \-\-fixed_in\fP +.sp +\fBSyntax:\fP \fB\-\-fixed_in\fP FIXED_IN +.sp +RHBZ \(aqFixed in version\(aq field +.SS \fB\-\-field\fP +.sp +\fBSyntax:\fP \fB\-\-field\fP FIELD=VALUE +.sp +Manually specify a bugzilla API field. FIELD is the raw name used +by the bugzilla instance. For example if your bugzilla instance has a +custom field cf_my_field, do: \-\-field cf_my_field=VALUE +.SS \fB\-\-field\-json\fP +.sp +\fBSyntax:\fP \fB\-\-field\-json\fP JSONSTRING +.sp +Specify \-\-field data as a JSON string. Example: +\-\-field\-json \(aq{\(dqcf_my_field\(dq: \(dqVALUE\(dq, \(dqcf_array_field\(dq: [1, 2]}\(aq +.SH OUTPUT OPTIONS +.sp +These options are shared by several commands, for tweaking the text +output of the command results. +.SS \fB\-\-full\fP +.sp +\fBSyntax:\fP \fB\-\-full\fP +.sp +output detailed bug info +.SS \fB\-i, \-\-ids\fP +.sp +\fBSyntax:\fP \fB\-\-ids\fP +.sp +output only bug IDs +.SS \fB\-e, \-\-extra\fP +.sp +\fBSyntax:\fP \fB\-\-extra\fP +.sp +output additional bug information (keywords, Whiteboards, etc.) +.SS \fB\-\-oneline\fP +.sp +\fBSyntax:\fP \fB\-\-oneline\fP +.sp +one line summary of the bug (useful for scripts) +.SS \fB\-\-json\fP +.sp +\fBSyntax:\fP \fB\-\-json\fP +.sp +output bug contents in JSON format +.SS \fB\-\-includefield\fP +.sp +\fBSyntax:\fP \fB\-\-includefield\fP +.sp +Pass the field name to bugzilla include_fields list. +Only the fields passed to include_fields are returned +by the bugzilla server. +This can be specified multiple times. +.SS \fB\-\-extrafield\fP +.sp +\fBSyntax:\fP \fB\-\-extrafield\fP +.sp +Pass the field name to bugzilla extra_fields list. +When used with \-\-json this can be used to request +bugzilla to return values for non\-default fields. +This can be specified multiple times. +.SS \fB\-\-excludefield\fP +.sp +\fBSyntax:\fP \fB\-\-excludefield\fP +.sp +Pass the field name to bugzilla exclude_fields list. +When used with \-\-json this can be used to request +bugzilla to not return values for a field. +This can be specified multiple times. +.SS \fB\-\-raw\fP +.sp +\fBSyntax:\fP \fB\-\-raw\fP +.sp +raw output of the bugzilla contents. This format is unstable and +difficult to parse. Please use the \fB\-\-json\fP instead if you want +maximum output from the \fIbugzilla\fP +.SS \fB\-\-outputformat\fP +.sp +\fBSyntax:\fP \fB\-\-outputformat\fP OUTPUTFORMAT +.sp +Print output in the form given. You can use RPM\-style tags that match +bug fields, e.g.: \(aq%{id}: %{summary}\(aq. +.sp +The output of the bugzilla tool should NEVER BE PARSED unless you are +using a custom \-\-outputformat. For everything else, just don\(aqt parse it, +the formats are not stable and are subject to change. +.sp +\-\-outputformat allows printing arbitrary bug data in a user preferred +format. For example, to print a returned bug ID, component, and product, +separated with ::, do: +.sp +\-\-outputformat \(dq%{id}::%{component}::%{product}\(dq +.sp +The fields (like \(aqid\(aq, \(aqcomponent\(aq, etc.) are the names of the values +returned by bugzilla\(aqs API. To see a list of all fields, +check the API documentation in the \(aqSEE ALSO\(aq section. Alternatively, +run a \(aqbugzilla \-\-debug query ...\(aq and look at the key names returned in +the query results. Also, in most cases, using the name of the associated +command line switch should work, like \-\-bug_status becomes +%{bug_status}, etc. +.SH ‘QUERY’ SPECIFIC OPTIONS +.sp +Certain options can accept a comma separated list to query multiple +values, including \-\-status, \-\-component, \-\-product, \-\-version, \-\-id. +.sp +Note: querying via explicit command line options will only get you so +far. See the \-\-from\-url option for a way to use powerful Web UI queries +from the command line. +.SS \fB\-b, \-\-bug_id, \-\-id\fP +.sp +\fBSyntax:\fP \fB\-\-id\fP ID +.sp +specify individual bugs by IDs, separated with commas +.SS \fB\-r, \-\-reporter\fP +.sp +\fBSyntax:\fP \fB\-\-reporter\fP REPORTER +.sp +Email: search reporter email for given address +.SS \fB\-\-quicksearch\fP +.sp +\fBSyntax:\fP \fB\-\-quicksearch\fP QUICKSEARCH +.sp +Search using bugzilla\(aqs quicksearch functionality. +.SS \fB\-\-savedsearch\fP +.sp +\fBSyntax:\fP \fB\-\-savedsearch\fP SAVEDSEARCH +.sp +Name of a bugzilla saved search. If you don\(aqt own this saved search, +you must passed \-\-savedsearch_sharer_id. +.SS \fB\-\-savedsearch\-sharer\-id\fP +.sp +\fBSyntax:\fP \fB\-\-savedsearch\-sharer\-id\fP SAVEDSEARCH_SHARER_ID +.sp +Owner ID of the \-\-savedsearch. You can get this ID from the URL +bugzilla generates when running the saved search from the web UI. +.SS \fB\-\-from\-url\fP +.sp +\fBSyntax:\fP \fB\-\-from\-url\fP WEB_QUERY_URL +.sp +Make a working query via bugzilla\(aqs \(aqAdvanced search\(aq web UI, grab +the url from your browser (the string with query.cgi or buglist.cgi +in it), and \-\-from\-url will run it via the bugzilla API. Don\(aqt forget +to quote the string! This only works for Bugzilla 5 and Red Hat +bugzilla +.SH ‘MODIFY’ SPECIFIC OPTIONS +.sp +Fields that take multiple values have a special input format. +.nf +Append: \fI\%\-\-cc=foo@example.com\fP +Overwrite: \fI\%\-\-cc==foo@example.com\fP +Remove: \fI\%\-\-cc=\-foo@example.com\fP +.fi +.sp +.sp +Options that accept this format: \-\-cc, \-\-blocked, \-\-dependson, \-\-groups, +\-\-tags, whiteboard fields. +.SS \fB\-k, \-\-close RESOLUTION\fP +.sp +\fBSyntax:\fP \fBRESOLUTION\fP +.sp +Close with the given resolution (WONTFIX, NOTABUG, etc.) +.SS \fB\-d, \-\-dupeid\fP +.sp +\fBSyntax:\fP \fB\-\-dupeid\fP ORIGINAL +.sp +ID of original bug. Implies \-\-close DUPLICATE +.SS \fB\-\-private\fP +.sp +\fBSyntax:\fP \fB\-\-private\fP +.sp +Mark new comment as private +.SS \fB\-\-reset\-assignee\fP +.sp +\fBSyntax:\fP \fB\-\-reset\-assignee\fP +.sp +Reset assignee to component default +.SS \fB\-\-reset\-qa\-contact\fP +.sp +\fBSyntax:\fP \fB\-\-reset\-qa\-contact\fP +.sp +Reset QA contact to component default +.SS \fB\-\-minor\-update\fP +.sp +\fBSyntax:\fP \fB\-\-minor\-update\fP +.sp +Request bugzilla to not send any email about this change +.SH ‘NEW’ SPECIFIC OPTIONS +.SS \fB\-\-private\fP +.sp +\fBSyntax:\fP \fB\-\-private\fP +.sp +Mark new comment as private +.SH ‘ATTACH’ OPTIONS +.SS \fB\-f, \-\-file\fP +.sp +\fBSyntax:\fP \fB\-\-file\fP FILENAME +.sp +File to attach, or filename for data provided on stdin +.SS \fB\-d, \-\-description\fP +.sp +\fBSyntax:\fP \fB\-\-description\fP DESCRIPTION +.sp +A short description of the file being attached +.SS \fB\-t, \-\-type\fP +.sp +\fBSyntax:\fP \fB\-\-type\fP MIMETYPE +.sp +Mime\-type for the file being attached +.SS \fB\-g, \-\-get\fP +.sp +\fBSyntax:\fP \fB\-\-get\fP ATTACHID +.sp +Download the attachment with the given ID +.SS \fB\-\-getall\fP +.sp +\fBSyntax:\fP \fB\-\-getall\fP BUGID +.sp +Download all attachments on the given bug +.SS \fB\-\-ignore\-obsolete\fP +.sp +\fBSyntax:\fP \fB\-\-ignore\-obsolete\fP +.sp +Do not download attachments marked as obsolete. +.SS \fB\-l, \-\-comment\fP +.sp +\fBSyntax:\fP \fB\-\-comment\fP COMMENT +.sp +Add comment with attachment +.SH ‘INFO’ OPTIONS +.SS \fB\-p, \-\-products\fP +.sp +\fBSyntax:\fP \fB\-\-products\fP +.sp +Get a list of products +.SS \fB\-c, \-\-components\fP +.sp +\fBSyntax:\fP \fB\-\-components\fP PRODUCT +.sp +List the components in the given product +.SS \fB\-o, \-\-component_owners\fP +.sp +\fBSyntax:\fP \fB\-\-component_owners\fP PRODUCT +.sp +List components (and their owners) +.SS \fB\-v, \-\-versions\fP +.sp +\fBSyntax:\fP \fB\-\-versions\fP PRODUCT +.sp +List the versions for the given product +.SS \fB\-\-active\-components\fP +.sp +\fBSyntax:\fP \fB\-\-active\-components\fP +.sp +Only show active components. Combine with \-\-components* +.SH BUGZILLARC CONFIG FILE +.sp +Both \fBbugzilla\fP and the python\-bugzilla library will read +a \fBbugzillarc\fP config file if it is present in the following +locations: +.INDENT 0.0 +.IP \(bu 2 +/etc/bugzillarc +.IP \(bu 2 +~/.bugzillarc +.IP \(bu 2 +~/.config/python\-bugzilla/bugzillarc +.UNINDENT +.sp +The contents of the files are processed and merged together +in the order they are listed above. +.sp +The main usage for \fBbugzillarc\fP is to store API keys for your +bugzilla URLs: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +[bugzilla.example.com] +api_key=INSERT\-YOUR\-API\-KEY\-HERE + +[bugzilla.redhat.com] +api_key=MY\-REDHAT\-API\-KEY\-BLAH +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +The sections must be hostnames. Other values that can be +set per hostname section are +.INDENT 0.0 +.IP \(bu 2 +\fBuser\fP: default auth username +.IP \(bu 2 +\fBpassword\fP: default auth password +.IP \(bu 2 +\fBcert\fP: default client side certificate +.UNINDENT +.sp +A \fB[DEFAULTS]\fP section is also accepted, which takes the following +values: +.INDENT 0.0 +.IP \(bu 2 +\fBurl\fP: default bugzilla URL +.UNINDENT +.SH AUTHENTICATION CACHE AND API KEYS +.sp +Some command usage will require an active login to the bugzilla +instance. For example, if the bugzilla instance has some private bugs, +those bugs will be missing from \(aqquery\(aq output if you do not have an +active login. +.sp +If you are connecting to a bugzilla 5.0 or later instance, the best +option is to use bugzilla API keys. From the bugzilla web UI, log in, +navigate to Preferences\->API Keys, and generate a key (it will be a long +string of characters and numbers). +.sp +Then use \(aqbugzilla \-\-bugzilla URL login \-\-api\-key\(aq, which will ask +for the API key, and save it to \fBbugzillarc\fP for you. +.sp +For older bugzilla instances, you will need to cache a login token +with the \(dqlogin\(dq subcommand or the \(dq\-\-login\(dq argument. +.sp +Additionally, the \-\-no\-cache\-credentials option will tell the bugzilla +tool to \fInot\fP save or use any authentication cache, including the +\fBbugzillarc\fP file. +.SH EXAMPLES +.nf +bugzilla query \-\-bug_id 62037 + +bugzilla query \-\-version 15 \-\-component python\-bugzilla + +bugzilla login + +bugzilla new \-p Fedora \-v rawhide \-c python\-bugzilla \e +.in +2 +\-\-summary \(dqpython\-bugzilla causes headaches\(dq \e +\-\-comment \(dqpython\-bugzilla made my brain hurt when I used it.\(dq + +.in -2 +bugzilla attach \-\-file ~/Pictures/cam1.jpg \-\-desc \(dqme, in pain\(dq +$BUGID + +bugzilla attach \-\-getall $BUGID + +bugzilla modify \-\-close NOTABUG \-\-comment \(dqActually, you\(aqre +hungover.\(dq $BUGID +.fi +.sp +.SH EXIT STATUS +.sp +\fBbugzilla\fP normally returns 0 if the requested command was successful. +Otherwise, exit status is 1 if \fBbugzilla\fP is interrupted by the user +(or a login attempt fails), 2 if a socket error occurs (e.g. TCP +connection timeout), and 3 if the Bugzilla server throws an error. +.SH BUGS +.sp +Please report any bugs as github issues at +\fI\%https://github.com/python\-bugzilla/python\-bugzilla\fP +.SH SEE ALSO +.sp +\fI\%https://bugzilla.readthedocs.io/en/latest/api/index.html\fP +.sp +\fI\%https://bugzilla.redhat.com/docs/en/html/api/core/v1/bug.html\fP +.\" Generated by docutils manpage writer. +. diff --git a/man/bugzilla.rst b/man/bugzilla.rst new file mode 100644 index 00000000..09ff4132 --- /dev/null +++ b/man/bugzilla.rst @@ -0,0 +1,901 @@ +======== +bugzilla +======== + +----------------------------------------------- +command line tool for interacting with Bugzilla +----------------------------------------------- + +:Manual section: 1 +:Manual group: User Commands + + +SYNOPSIS +======== + +**bugzilla** [*options*] [*command*] [*command-options*] + + +DESCRIPTION +=========== + +**bugzilla** is a command line tool for interacting with a Bugzilla +instance over REST or XMLRPC. + +| +| *command* is one of: +| * login - log into the given bugzilla instance +| * new - create a new bug +| * query - search for bugs matching given criteria +| * modify - modify existing bugs +| * attach - attach files to existing bugs, or get attachments +| * info - get info about the given bugzilla instance + + + +GLOBAL OPTIONS +============== + +``--help, -h`` +^^^^^^^^^^^^^^ + +**Syntax:** ``-h`` + +show this help message and exit + + +``--bugzilla`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--bugzilla`` BUGZILLA + +The bugzilla URL. Full API URLs are typically like: + +| +| * https://bugzilla.example.com/xmlrpc.cgi # XMLRPC API +| * https://bugzilla.example.com/rest/ # REST API +| + +If a non-specific URL is passed, like 'bugzilla.redhat.com', **bugzilla** +will try to probe whether the expected XMLRPC or REST path is available, +preferring XMLRPC for backwards compatibility. + +The default URL https://bugzilla.redhat.com + + +``--nosslverify`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--nosslverify`` + +Don't error on invalid bugzilla SSL certificate + + +``--cert`` +^^^^^^^^^^ + +**Syntax:** ``--cert`` CERTFILE + +client side certificate file needed by the webserver. + + +``--login`` +^^^^^^^^^^^ + +**Syntax:** ``--login`` + +Run interactive "login" before performing the specified command. + + +``--username`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--username`` USERNAME + +Log in with this username + + +``--password`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--password`` PASSWORD + +Log in with this password + + +``--restrict-login`` +^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--restrict-login`` + +The session (login token) will be restricted to the current IP +address. + + +``--ensure-logged-in`` +^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--ensure-logged-in`` + +Raise an error if we aren't logged in to bugzilla. Consider using +this if you are depending on cached credentials, to ensure that when +they expire the tool errors, rather than subtly change output. + + +``--no-cache-credentials`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--no-cache-credentials`` + +Don't save any bugzilla tokens to disk, and don't use any +pre-existing credentials. + + +``--tokenfile`` +^^^^^^^^^^^^^^^ + +**Syntax:** ``--tokenfile`` TOKENFILE + +token file to use for bugzilla authentication + + +``--verbose`` +^^^^^^^^^^^^^ + +**Syntax:** ``--verbose`` + +give more info about what's going on + + +``--debug`` +^^^^^^^^^^^ + +**Syntax:** ``--debug`` + +output bunches of debugging info + + +``--version`` +^^^^^^^^^^^^^ + +**Syntax:** ``--version`` + +show program's version number and exit + + + +Standard bugzilla options +========================= + +These options are shared by some combination of the 'new', 'query', and +'modify' sub commands. Not every option works for each command though. + + +``-p, --product`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--product`` PRODUCT + +Product name + + +``-v, --version`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--version`` VERSION + +Product version + + +``-c, --component`` +^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--component`` COMPONENT + +Component name + + +``-s, --summary`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--summary`` SUMMARY + +Bug summary + + +``-l, --comment`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--comment`` DESCRIPTION + +Set initial bug comment/description + + +``--comment-tag`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--comment-tag`` TAG + +Comment tag for the new comment + + +``--sub-component`` +^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--sub-component`` SUB_COMPONENT + +RHBZ sub component name + + +``-o, --os`` +^^^^^^^^^^^^ + +**Syntax:** ``--os`` OS + +Operating system + + +``--arch`` +^^^^^^^^^^ + +**Syntax:** ``--arch`` ARCH + +Arch this bug occurs on + + +``-x, --severity`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--severity`` SEVERITY + +Bug severity + + +``-z, --priority`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--priority`` PRIORITY + +Bug priority + + +``--alias`` +^^^^^^^^^^^ + +**Syntax:** ``--alias`` ALIAS + +Bug alias (name) + + +``-s, --status`` +^^^^^^^^^^^^^^^^ + +**Syntax:** ``--status`` STATUS + +Bug status (NEW, ASSIGNED, etc.) + + +``-u, --url`` +^^^^^^^^^^^^^ + +**Syntax:** ``--url`` URL + +URL for further bug info + + +``-m --target_milestone`` +^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--target_milestone`` TARGET_MILESTONE + +Target milestone + + +``--target_release`` +^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--target_release`` TARGET_RELEASE + +RHBZ Target release + + +``--blocked`` +^^^^^^^^^^^^^ + +**Syntax:** ``...]`` + +Bug IDs that this bug blocks + + +``--dependson`` +^^^^^^^^^^^^^^^ + +**Syntax:** ``...]`` + +Bug IDs that this bug depends on + + +``--keywords`` +^^^^^^^^^^^^^^ + +**Syntax:** ``...]`` + +Bug keywords + + +``--groups`` +^^^^^^^^^^^^ + +**Syntax:** ``...]`` + +Which user groups can view this bug + + +``--cc`` +^^^^^^^^ + +**Syntax:** ``...]`` + +CC list + + +``-a, --assignee, --assigned_to`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--assigned_to`` ASSIGNED_TO + +Bug assignee + + +``-q, --qa_contact`` +^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--qa_contact`` QA_CONTACT + +QA contact + + +``-f, --flag`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--flag`` FLAG + +Set or unset a flag. For example, to set a flag named devel_ack, do +--flag devel_ack+ Unset a flag with the 'X' value, like --flag +needinfoX + + +``--tags`` +^^^^^^^^^^ + +**Syntax:** ``--tags`` TAG + +Set (personal) tags field + + +``-w, --whiteboard`` +^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--whiteboard`` WHITEBOARD + +Whiteboard field + + +``--devel_whiteboard`` +^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--devel_whiteboard`` DEVEL_WHITEBOARD + +RHBZ devel whiteboard field + + +``--internal_whiteboard`` +^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--internal_whiteboard`` INTERNAL_WHITEBOARD + +RHBZ internal whiteboard field + + +``--qa_whiteboard`` +^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--qa_whiteboard`` QA_WHITEBOARD + +RHBZ QA whiteboard field + + +``-F, --fixed_in`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--fixed_in`` FIXED_IN + +RHBZ 'Fixed in version' field + + +``--field`` +^^^^^^^^^^^ + +**Syntax:** ``--field`` FIELD=VALUE + +Manually specify a bugzilla API field. FIELD is the raw name used +by the bugzilla instance. For example if your bugzilla instance has a +custom field cf_my_field, do: --field cf_my_field=VALUE + + +``--field-json`` +^^^^^^^^^^^^^^^^ + +**Syntax:** ``--field-json`` JSONSTRING + +Specify --field data as a JSON string. Example: +--field-json '{"cf_my_field": "VALUE", "cf_array_field": [1, 2]}' + + + +Output options +============== + +These options are shared by several commands, for tweaking the text +output of the command results. + + +``--full`` +^^^^^^^^^^ + +**Syntax:** ``--full`` + +output detailed bug info + + +``-i, --ids`` +^^^^^^^^^^^^^ + +**Syntax:** ``--ids`` + +output only bug IDs + + +``-e, --extra`` +^^^^^^^^^^^^^^^ + +**Syntax:** ``--extra`` + +output additional bug information (keywords, Whiteboards, etc.) + + +``--oneline`` +^^^^^^^^^^^^^ + +**Syntax:** ``--oneline`` + +one line summary of the bug (useful for scripts) + + +``--json`` +^^^^^^^^^^ + +**Syntax:** ``--json`` + +output bug contents in JSON format + + +``--includefield`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--includefield`` + +Pass the field name to bugzilla include_fields list. +Only the fields passed to include_fields are returned +by the bugzilla server. +This can be specified multiple times. + + +``--extrafield`` +^^^^^^^^^^^^^^^^ + +**Syntax:** ``--extrafield`` + +Pass the field name to bugzilla extra_fields list. +When used with --json this can be used to request +bugzilla to return values for non-default fields. +This can be specified multiple times. + + +``--excludefield`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--excludefield`` + +Pass the field name to bugzilla exclude_fields list. +When used with --json this can be used to request +bugzilla to not return values for a field. +This can be specified multiple times. + + +``--raw`` +^^^^^^^^^ + +**Syntax:** ``--raw`` + +raw output of the bugzilla contents. This format is unstable and +difficult to parse. Please use the ``--json`` instead if you want +maximum output from the `bugzilla` + + +``--outputformat`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--outputformat`` OUTPUTFORMAT + +Print output in the form given. You can use RPM-style tags that match +bug fields, e.g.: '%{id}: %{summary}'. + +The output of the bugzilla tool should NEVER BE PARSED unless you are +using a custom --outputformat. For everything else, just don't parse it, +the formats are not stable and are subject to change. + +--outputformat allows printing arbitrary bug data in a user preferred +format. For example, to print a returned bug ID, component, and product, +separated with ::, do: + +--outputformat "%{id}::%{component}::%{product}" + +The fields (like 'id', 'component', etc.) are the names of the values +returned by bugzilla's API. To see a list of all fields, +check the API documentation in the 'SEE ALSO' section. Alternatively, +run a 'bugzilla --debug query ...' and look at the key names returned in +the query results. Also, in most cases, using the name of the associated +command line switch should work, like --bug_status becomes +%{bug_status}, etc. + + +‘query’ specific options +======================== + +Certain options can accept a comma separated list to query multiple +values, including --status, --component, --product, --version, --id. + +Note: querying via explicit command line options will only get you so +far. See the --from-url option for a way to use powerful Web UI queries +from the command line. + + +``-b, --bug_id, --id`` +^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--id`` ID + +specify individual bugs by IDs, separated with commas + + +``-r, --reporter`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--reporter`` REPORTER + +Email: search reporter email for given address + + +``--quicksearch`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--quicksearch`` QUICKSEARCH + +Search using bugzilla's quicksearch functionality. + + +``--savedsearch`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--savedsearch`` SAVEDSEARCH + +Name of a bugzilla saved search. If you don't own this saved search, +you must passed --savedsearch_sharer_id. + + +``--savedsearch-sharer-id`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--savedsearch-sharer-id`` SAVEDSEARCH_SHARER_ID + +Owner ID of the --savedsearch. You can get this ID from the URL +bugzilla generates when running the saved search from the web UI. + + +``--from-url`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--from-url`` WEB_QUERY_URL + +Make a working query via bugzilla's 'Advanced search' web UI, grab +the url from your browser (the string with query.cgi or buglist.cgi +in it), and --from-url will run it via the bugzilla API. Don't forget +to quote the string! This only works for Bugzilla 5 and Red Hat +bugzilla + + +‘modify’ specific options +========================= + +Fields that take multiple values have a special input format. + +| Append: --cc=foo@example.com +| Overwrite: --cc==foo@example.com +| Remove: --cc=-foo@example.com + +Options that accept this format: --cc, --blocked, --dependson, --groups, +--tags, whiteboard fields. + + +``-k, --close RESOLUTION`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``RESOLUTION`` + +Close with the given resolution (WONTFIX, NOTABUG, etc.) + + +``-d, --dupeid`` +^^^^^^^^^^^^^^^^ + +**Syntax:** ``--dupeid`` ORIGINAL + +ID of original bug. Implies --close DUPLICATE + + +``--private`` +^^^^^^^^^^^^^ + +**Syntax:** ``--private`` + +Mark new comment as private + + +``--reset-assignee`` +^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--reset-assignee`` + +Reset assignee to component default + + +``--reset-qa-contact`` +^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--reset-qa-contact`` + +Reset QA contact to component default + + +``--minor-update`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--minor-update`` + +Request bugzilla to not send any email about this change + + + +‘new’ specific options +====================== + +``--private`` +^^^^^^^^^^^^^ + +**Syntax:** ``--private`` + +Mark new comment as private + + + +‘attach’ options +================ + +``-f, --file`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--file`` FILENAME + +File to attach, or filename for data provided on stdin + + +``-d, --description`` +^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--description`` DESCRIPTION + +A short description of the file being attached + + +``-t, --type`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--type`` MIMETYPE + +Mime-type for the file being attached + + +``-g, --get`` +^^^^^^^^^^^^^ + +**Syntax:** ``--get`` ATTACHID + +Download the attachment with the given ID + + +``--getall`` +^^^^^^^^^^^^ + +**Syntax:** ``--getall`` BUGID + +Download all attachments on the given bug + + +``--ignore-obsolete`` +^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--ignore-obsolete`` + +Do not download attachments marked as obsolete. + + +``-l, --comment`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--comment`` COMMENT + +Add comment with attachment + + +‘info’ options +============== + +``-p, --products`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--products`` + +Get a list of products + + +``-c, --components`` +^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--components`` PRODUCT + +List the components in the given product + + +``-o, --component_owners`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--component_owners`` PRODUCT + +List components (and their owners) + + +``-v, --versions`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--versions`` PRODUCT + +List the versions for the given product + + +``--active-components`` +^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--active-components`` + +Only show active components. Combine with --components* + + +``bugzillarc`` CONFIG FILE +========================== + +Both ``bugzilla`` and the python-bugzilla library will read +a ``bugzillarc`` config file if it is present in the following +locations: + +- /etc/bugzillarc +- ~/.bugzillarc +- ~/.config/python-bugzilla/bugzillarc + +The contents of the files are processed and merged together +in the order they are listed above. + +The main usage for ``bugzillarc`` is to store API keys for your +bugzilla URLs: + +:: + + [bugzilla.example.com] + api_key=INSERT-YOUR-API-KEY-HERE + + [bugzilla.redhat.com] + api_key=MY-REDHAT-API-KEY-BLAH + + +The sections must be hostnames. Other values that can be +set per hostname section are + +- ``user``: default auth username +- ``password``: default auth password +- ``cert``: default client side certificate + + +A ``[DEFAULTS]`` section is also accepted, which takes the following +values: + +- ``url``: default bugzilla URL + + +AUTHENTICATION CACHE AND API KEYS +================================= + +Some command usage will require an active login to the bugzilla +instance. For example, if the bugzilla instance has some private bugs, +those bugs will be missing from 'query' output if you do not have an +active login. + +If you are connecting to a bugzilla 5.0 or later instance, the best +option is to use bugzilla API keys. From the bugzilla web UI, log in, +navigate to Preferences->API Keys, and generate a key (it will be a long +string of characters and numbers). + +Then use 'bugzilla --bugzilla URL login --api-key', which will ask +for the API key, and save it to ``bugzillarc`` for you. + +For older bugzilla instances, you will need to cache a login token +with the "login" subcommand or the "--login" argument. + +Additionally, the --no-cache-credentials option will tell the bugzilla +tool to *not* save or use any authentication cache, including the +``bugzillarc`` file. + + +EXAMPLES +======== + +| bugzilla query --bug_id 62037 +| +| bugzilla query --version 15 --component python-bugzilla +| +| bugzilla login +| +| bugzilla new -p Fedora -v rawhide -c python-bugzilla \\ +| --summary "python-bugzilla causes headaches" \\ +| --comment "python-bugzilla made my brain hurt when I used it." +| +| bugzilla attach --file ~/Pictures/cam1.jpg --desc "me, in pain" +| $BUGID +| +| bugzilla attach --getall $BUGID +| +| bugzilla modify --close NOTABUG --comment "Actually, you're +| hungover." $BUGID + + +EXIT STATUS +=========== + +**bugzilla** normally returns 0 if the requested command was successful. +Otherwise, exit status is 1 if **bugzilla** is interrupted by the user +(or a login attempt fails), 2 if a socket error occurs (e.g. TCP +connection timeout), and 3 if the Bugzilla server throws an error. + + +BUGS +==== + +Please report any bugs as github issues at +https://github.com/python-bugzilla/python-bugzilla + + +SEE ALSO +======== + +https://bugzilla.readthedocs.io/en/latest/api/index.html + +https://bugzilla.redhat.com/docs/en/html/api/core/v1/bug.html diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 1418eb7b..716fc933 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -1,67 +1,39 @@ -%if 0%{?fedora} || 0%{?rhel} >= 8 -%global with_python3 1 -%else -%{!?__python2: %global __python2 /usr/bin/python2} -%{!?python2_sitelib2: %global python2_sitelib %(%{__python2} -c "from distutils.sysconfig import get_python_lib; print (get_python_lib())")} -%endif - Name: python-bugzilla -Version: 2.1.0 +Version: 3.3.0 Release: 1%{?dist} -Summary: python2 library for interacting with Bugzilla +Summary: Python library for interacting with Bugzilla License: GPLv2+ URL: https://github.com/python-bugzilla/python-bugzilla -Source0: https://github.com/python-bugzilla/python-bugzilla/archive/v%{version}.tar.gz#/%{name}-%{version}.tar.gz +Source0: https://github.com/python-bugzilla/python-bugzilla/archive/v%{version}/%{name}-%{version}.tar.gz BuildArch: noarch -BuildRequires: python2-devel -BuildRequires: python-requests -BuildRequires: python-setuptools -%if 0%{?el6} -BuildRequires: python-argparse -%endif - -%if 0%{?with_python3} BuildRequires: python3-devel BuildRequires: python3-requests BuildRequires: python3-setuptools -%endif # if with_python3 +BuildRequires: python3-pytest -Requires: python-requests -Requires: python-magic -%if 0%{?el6} -Requires: python-argparse -%endif -# This dep is for back compat, so that installing python-bugzilla continues -# to give the cli tool -Requires: python-bugzilla-cli +%global _description\ +python-bugzilla is a python library for interacting with bugzilla instances\ +over XMLRPC or REST.\ +%description %_description -%description -python-bugzilla is a python 2 library for interacting with bugzilla instances -over XML-RPC. - -%if 0%{?with_python3} %package -n python3-bugzilla -Summary: python 3 library for interacting with Bugzilla +Summary: %summary Requires: python3-requests -Requires: python3-magic +%{?python_provide:%python_provide python3-bugzilla} + +Obsoletes: python-bugzilla < %{version}-%{release} +Obsoletes: python2-bugzilla < %{version}-%{release} -%description -n python3-bugzilla -python3-bugzilla is a python 3 library for interacting with bugzilla instances -over XML-RPC. -%endif # if with_python3 +%description -n python3-bugzilla %_description %package cli Summary: Command line tool for interacting with Bugzilla -%if 0%{?with_python3} Requires: python3-bugzilla = %{version}-%{release} -%else -Requires: python-bugzilla = %{version}-%{release} -%endif %description cli This package includes the 'bugzilla' command-line tool for interacting with bugzilla. Uses the python-bugzilla API @@ -71,62 +43,21 @@ This package includes the 'bugzilla' command-line tool for interacting with bugz %prep %setup -q -%if 0%{?with_python3} -rm -rf %{py3dir} -cp -a . %{py3dir} -%endif # with_python3 - - - -%build -%if 0%{?with_python3} -pushd %{py3dir} -%{__python3} setup.py build -popd -%endif # with_python3 - -%{__python2} setup.py build - %install -%if 0%{?with_python3} -pushd %{py3dir} -%{__python3} setup.py install -O1 --skip-build --root %{buildroot} -rm %{buildroot}/usr/bin/bugzilla -popd -%endif # with_python3 - -%{__python2} setup.py install -O1 --skip-build --root %{buildroot} - -# Replace '#!/usr/bin/env python' with '#!/usr/bin/python2' -# The format is ideal for upstream, but not a distro. See: -# https://fedoraproject.org/wiki/Features/SystemPythonExecutablesUseSystemPython -%if 0%{?with_python3} -%global python_env_path %{__python3} -%else -%global python_env_path %{__python2} -%endif -for f in $(find %{buildroot} -type f -executable -print); do - sed -i "1 s|^#!/usr/bin/.*|#!%{python_env_path}|" $f || : -done +%{__python3} setup.py install -O1 --root %{buildroot} %check -%{__python2} setup.py test - +pytest-3 -%files -%doc COPYING README.md NEWS.md -%{python2_sitelib}/* -%if 0%{?with_python3} %files -n python3-bugzilla %doc COPYING README.md NEWS.md %{python3_sitelib}/* -%endif # with_python3 %files cli %{_bindir}/bugzilla diff --git a/setup.py b/setup.py index 456962ee..b9db594d 100755 --- a/setup.py +++ b/setup.py @@ -1,141 +1,59 @@ -#!/usr/bin/env python - -from __future__ import print_function +#!/usr/bin/env python3 import glob import os +import shutil +import subprocess import sys -import unittest - -from distutils.core import Command -from setuptools import setup - - -def unsupported_python_version(): - return sys.version_info < (2, 7) \ - or (sys.version_info > (3,) and sys.version_info < (3, 3)) - -if unsupported_python_version(): - raise ImportError("python-bugzilla does not support this python version") +import setuptools def get_version(): f = open("bugzilla/apiversion.py") for line in f: if line.startswith('version = '): - return eval(line.split('=')[-1]) - - -class TestCommand(Command): - user_options = [ - ("ro-functional", None, - "Run readonly functional tests against actual bugzilla instances. " - "This will be very slow."), - ("rw-functional", None, - "Run read/write functional tests against actual bugzilla instances. " - "As of now this only runs against partner-bugzilla.redhat.com, " - "which requires an RH bugzilla account with cached cookies. " - "This will also be very slow."), - ("only=", None, - "Run only tests whose name contains the passed string"), - ("redhat-url=", None, - "Redhat bugzilla URL to use for ro/rw_functional tests"), - ("debug", None, - "Enable python-bugzilla debug output. This may break output " - "comparison tests."), - ] + return eval(line.split('=')[-1]) # pylint: disable=eval-used - def initialize_options(self): - self.ro_functional = False - self.rw_functional = False - self.only = None - self.redhat_url = None - self.debug = False +class PylintCommand(setuptools.Command): + user_options = [] + + def initialize_options(self): + pass def finalize_options(self): pass def run(self): - os.environ["__BUGZILLA_UNITTEST"] = "1" - - try: - import coverage - usecov = int(coverage.__version__.split(".")[0]) >= 3 - except: - usecov = False - - if usecov: - cov = coverage.coverage(omit=[ - "/*/tests/*", "/usr/*", "*dev-env*", "*.tox/*"]) - cov.erase() - cov.start() - - testfiles = [] - for t in glob.glob(os.path.join(os.getcwd(), 'tests', '*.py')): - if t.endswith("__init__.py"): - continue - - base = os.path.basename(t) - if (base == "ro_functional.py" and not self.ro_functional): - continue - - if (base == "rw_functional.py" and not self.rw_functional): - continue - - testfiles.append('.'.join(['tests', os.path.splitext(base)[0]])) - - - if hasattr(unittest, "installHandler"): - try: - unittest.installHandler() - except: - print("installHandler hack failed") - - import tests as testsmodule - testsmodule.REDHAT_URL = self.redhat_url - if self.debug: - import logging - import bugzilla - logging.getLogger(bugzilla.__name__).setLevel(logging.DEBUG) - os.environ["__BUGZILLA_UNITTEST_DEBUG"] = "1" - - tests = unittest.TestLoader().loadTestsFromNames(testfiles) - if self.only: - newtests = [] - for suite1 in tests: - for suite2 in suite1: - for testcase in suite2: - if self.only in str(testcase): - newtests.append(testcase) - - if not newtests: - print("--only didn't find any tests") - sys.exit(1) - - tests = unittest.TestSuite(newtests) - print("Running only:") - for test in newtests: - print("%s" % test) - print() - - - t = unittest.TextTestRunner(verbosity=1) - - result = t.run(tests) - - if usecov: - cov.stop() - cov.save() - - err = int(bool(len(result.failures) > 0 or - len(result.errors) > 0)) - if not err and usecov: - cov.report(show_missing=False) - sys.exit(err) + import pylint.lint + import pycodestyle + files = (["bugzilla-cli", "bugzilla", "setup.py"] + + glob.glob("examples/*.py") + + glob.glob("tests/*.py")) + output_format = sys.stdout.isatty() and "colorized" or "text" -class PylintCommand(Command): + print("running pycodestyle") + style_guide = pycodestyle.StyleGuide( + config_file='tox.ini', + format="pylint", + paths=files, + ) + report = style_guide.check_files() + if style_guide.options.count: + sys.stderr.write(str(report.total_errors) + '\n') + + print("running pylint") + pylint_opts = [ + "--rcfile", ".pylintrc", + "--output-format=%s" % output_format, + ] + pylint.lint.Run(files + pylint_opts) + + +class RPMCommand(setuptools.Command): + description = ("Build src and binary rpms and output them " + "in the source directory") user_options = [] def initialize_options(self): @@ -143,36 +61,21 @@ def initialize_options(self): def finalize_options(self): pass - def _run(self): - files = ["bugzilla/", "bin-bugzilla", "examples/*.py", "tests/*.py"] - output_format = sys.stdout.isatty() and "colorized" or "text" - - if os.path.exists("/usr/bin/pylint-2"): - cmd = "pylint-2 " - else: - cmd = "pylint " - cmd += "--output-format=%s " % output_format - cmd += " ".join(files) - os.system(cmd + " --rcfile tests/pylint.cfg") - - print("running pep8") - cmd = "pep8 " - cmd += " ".join(files) - os.system(cmd + " --config tests/pep8.cfg --exclude oldclasses.py") - def run(self): - os.link("bin/bugzilla", "bin-bugzilla") - try: - self._run() - finally: - try: - os.unlink("bin-bugzilla") - except: - pass - - -class RPMCommand(Command): - description = "Build src and binary rpms." + self.run_command('sdist') + srcdir = os.path.dirname(__file__) + cmd = [ + "rpmbuild", "-ta", + "--define", "_rpmdir %s" % srcdir, + "--define", "_srcrpmdir %s" % srcdir, + "--define", "_specdir /tmp", + "dist/python-bugzilla-%s.tar.gz" % get_version(), + ] + subprocess.check_call(cmd) + + +class ManCommand(setuptools.Command): + description = ("Regenerate manpages from rst") user_options = [] def initialize_options(self): @@ -180,18 +83,28 @@ def initialize_options(self): def finalize_options(self): pass + def _make_man_pages(self): + rstbin = shutil.which("rst2man") + if not rstbin: + rstbin = shutil.which("rst2man.py") + if not rstbin: + raise RuntimeError("Didn't find rst2man or rst2man.py") + + for path in glob.glob("man/*.rst"): + base = os.path.basename(path) + appname = os.path.splitext(base)[0] + newpath = os.path.join(os.path.dirname(path), + appname + ".1") + + print("Generating %s" % newpath) + out = subprocess.check_output([rstbin, path]) + open(newpath, "wb").write(out) + + self.distribution.data_files.append( + ('share/man/man1', (newpath,))) + def run(self): - """ - Run sdist, then 'rpmbuild' the tar.gz - """ - os.system("cp python-bugzilla.spec /tmp") - try: - os.system("rm -rf python-bugzilla-%s" % get_version()) - self.run_command('sdist') - os.system('rpmbuild -ta --clean dist/python-bugzilla-%s.tar.gz' % - get_version()) - finally: - os.system("mv /tmp/python-bugzilla.spec .") + self._make_man_pages() def _parse_requirements(fname): @@ -202,37 +115,38 @@ def _parse_requirements(fname): ret.append(line) return ret -setup(name='python-bugzilla', - version=get_version(), - description='Bugzilla XMLRPC access module', - author='Cole Robinson', - author_email='python-bugzilla@lists.fedorahosted.org', - license="GPLv2", - url='https://github.com/python-bugzilla/python-bugzilla', - classifiers=[ - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - ], - packages = ['bugzilla'], - scripts=['bin/bugzilla'], - data_files=[('share/man/man1', ['bugzilla.1'])], - - install_requires=_parse_requirements("requirements.txt"), - tests_require=_parse_requirements("test-requirements.txt"), - - cmdclass={ - "pylint" : PylintCommand, - "rpm" : RPMCommand, - "test" : TestCommand, - }, + +setuptools.setup( + name='python-bugzilla', + version=get_version(), + description='Library and command line tool for interacting with Bugzilla', + license="GPLv2", + url='https://github.com/python-bugzilla/python-bugzilla', + classifiers=[ + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: ' + 'GNU General Public License v2 or later (GPLv2+)', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + ], + packages=['bugzilla'], + data_files=[('share/man/man1', ['man/bugzilla.1'])], + entry_points={'console_scripts': ['bugzilla = bugzilla._cli:cli']}, + + install_requires=_parse_requirements("requirements.txt"), + tests_require=_parse_requirements("test-requirements.txt"), + + cmdclass={ + "regenerate_manpages": ManCommand, + "pylint": PylintCommand, + "rpm": RPMCommand, + }, ) diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index 4fc2efb3..00000000 --- a/sonar-project.properties +++ /dev/null @@ -1,13 +0,0 @@ -# Required metadata -sonar.projectKey=com.github:python-bugzilla:python-bugzilla -sonar.projectName=Python Bugzilla -sonar.projectVersion=2.2.0-dev - -# Comma-separated paths to directories with sources (required) -sonar.sources=bugzilla - -# Language -sonar.language=py - -# Encoding of the source files -sonar.sourceEncoding=UTF-8 diff --git a/test-requirements.txt b/test-requirements.txt index f45fca24..6e80f2c7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,5 @@ # additional packages needed for testing -coverage -pep8 +pytest +pylint<3.1 +pycodestyle<2.12 +responses diff --git a/tests/__init__.py b/tests/__init__.py index a7442ead..df9650b9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,128 +1,16 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. -from __future__ import print_function - -import atexit -import difflib -import imp import os -import shlex -import sys - -if sys.version_info[0] >= 3: - from io import StringIO -else: - from StringIO import StringIO - -from bugzilla import Bugzilla, RHBugzilla - - -_cleanup = [] - - -def _import(name, path): - _cleanup.append(path + "c") - return imp.load_source(name, path) - - -def _cleanup_cb(): - for f in _cleanup: - if os.path.exists(f): - os.unlink(f) - - -atexit.register(_cleanup_cb) -bugzillascript = _import("bugzillascript", "bin/bugzilla") - -# This is overwritten by python setup.py test --redhat-url, and then -# used in ro/rw tests -REDHAT_URL = None - - -def make_bz(version, *args, **kwargs): - cls = Bugzilla - if kwargs.pop("rhbz", False): - cls = RHBugzilla - if "cookiefile" not in kwargs and "tokenfile" not in kwargs: - kwargs["use_creds"] = False - if "url" not in kwargs: - kwargs["url"] = None - bz = cls(*args, **kwargs) - bz._set_bz_version(version) # pylint: disable=protected-access - return bz - - -def diff(orig, new): - """ - Return a unified diff string between the passed strings - """ - return "".join(difflib.unified_diff(orig.splitlines(1), - new.splitlines(1), - fromfile="Orig", - tofile="New")) - - -def difffile(expect, filename): - expect += '\n' - if not os.path.exists(filename) or os.getenv("__BUGZILLA_UNITTEST_REGEN"): - open(filename, "w").write(expect) - ret = diff(open(filename).read(), expect) - if ret: - raise AssertionError("Output was different:\n%s" % ret) - - -def clicomm(argv, bzinstance, returnmain=False, printcliout=False, - stdin=None, stdinstr=None, expectfail=False): - """ - Run bin/bugzilla.main() directly with passed argv - """ - - argv = shlex.split(argv) - - oldstdout = sys.stdout - oldstderr = sys.stderr - oldstdin = sys.stdin - oldargv = sys.argv - try: - if not printcliout: - out = StringIO() - sys.stdout = out - sys.stderr = out - if stdin: - sys.stdin = stdin - elif stdinstr: - sys.stdin = StringIO(stdinstr) - - sys.argv = argv - - ret = 0 - mainout = None - try: - print(" ".join(argv)) - print() - mainout = bugzillascript.main(unittest_bz_instance=bzinstance) - except SystemExit: - sys_e = sys.exc_info()[1] - ret = sys_e.code - outt = "" - if not printcliout: - outt = out.getvalue() - if outt.endswith("\n"): - outt = outt[:-1] +class _CLICONFIG(object): + def __init__(self): + self.REDHAT_URL = None + self.REGENERATE_OUTPUT = False + self.ONLY_REST = False + self.ONLY_XMLRPC = False - if ret != 0 and not expectfail: - raise RuntimeError("Command failed with %d\ncmd=%s\nout=%s" % - (ret, argv, outt)) - elif ret == 0 and expectfail: - raise RuntimeError("Command succeeded but we expected success\n" - "ret=%d\ncmd=%s\nout=%s" % (ret, argv, outt)) - if returnmain: - return mainout - return outt - finally: - sys.stdout = oldstdout - sys.stderr = oldstderr - sys.stdin = oldstdin - sys.argv = oldargv +CLICONFIG = _CLICONFIG() +os.environ["__BUGZILLA_UNITTEST"] = "1" diff --git a/tests/bug.py b/tests/bug.py deleted file mode 100644 index 5f9f79bb..00000000 --- a/tests/bug.py +++ /dev/null @@ -1,84 +0,0 @@ -# -# Copyright Red Hat, Inc. 2014 -# -# This work is licensed under the terms of the GNU GPL, version 2 or later. -# See the COPYING file in the top-level directory. -# - -''' -Unit tests for testing some bug.py magic -''' - -import pickle -import sys -import unittest - -import tests -from tests import StringIO - -from bugzilla.bug import Bug - - -rhbz = tests.make_bz("4.4.0", rhbz=True) - - -class BugTest(unittest.TestCase): - maxDiff = None - bz = rhbz - - def testBasic(self): - data = { - "bug_id": 123456, - "status": "NEW", - "assigned_to": "foo@bar.com", - "component": "foo", - "product": "bar", - "short_desc": "some short desc", - "cf_fixed_in": "nope", - "fixed_in": "1.2.3.4", - "devel_whiteboard": "some status value", - } - - bug = Bug(bugzilla=self.bz, dict=data) - - def _assert_bug(): - self.assertEqual(hasattr(bug, "component"), True) - self.assertEqual(getattr(bug, "components"), ["foo"]) - self.assertEqual(getattr(bug, "product"), "bar") - self.assertEqual(hasattr(bug, "short_desc"), True) - self.assertEqual(getattr(bug, "summary"), "some short desc") - self.assertEqual(bool(getattr(bug, "cf_fixed_in")), True) - self.assertEqual(getattr(bug, "fixed_in"), "1.2.3.4") - self.assertEqual(bool(getattr(bug, "cf_devel_whiteboard")), True) - self.assertEqual(getattr(bug, "devel_whiteboard"), - "some status value") - - _assert_bug() - - self.assertEqual(str(bug), - "#123456 NEW - foo@bar.com - some short desc") - self.assertTrue(repr(bug).startswith("= 3: - from io import BytesIO - fd = BytesIO() - else: - fd = StringIO() - - pickle.dump(bug, fd) - fd.seek(0) - bug = pickle.load(fd) - self.assertEqual(getattr(bug, "bugzilla"), None) - bug.bugzilla = self.bz - _assert_bug() - - def testBugNoID(self): - try: - Bug(bugzilla=self.bz, dict={"component": "foo"}) - raise AssertionError("Expected lack of ID failure.") - except TypeError: - pass diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..d398437d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,169 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import locale +import logging +import os +import re + +import pytest +import responses + +import tests +import tests.utils + +import bugzilla + + +# pytest plugin adding custom options. Hooks are documented here: +# https://docs.pytest.org/en/latest/writing_plugins.html + +def pytest_addoption(parser): + parser.addoption("--ro-integration", action="store_true", default=False, + help="Run readonly tests against local Bugzilla instance.") + parser.addoption("--rw-integration", action="store_true", default=False, + help="Run read-write tests against local Bugzilla instance.") + parser.addoption("--ro-functional", action="store_true", default=False, + help=("Run readonly functional tests against actual " + "bugzilla instances. This will be very slow.")) + parser.addoption("--rw-functional", action="store_true", default=False, + help=("Run read/write functional tests against actual bugzilla " + "instances. As of now this only runs against " + "bugzilla.stage.redhat.com, which requires an RH " + "bugzilla account with cached login creds. This will " + "also be very slow.")) + parser.addoption("--redhat-url", + help="Redhat bugzilla URL to use for ro/rw_functional tests") + parser.addoption("--pybz-debug", action="store_true", default=False, + help=("Enable python-bugzilla debug output. This may break " + "output comparison tests.")) + parser.addoption("--regenerate-output", + action="store_true", default=False, + help=("Force regeneration of generated test output")) + parser.addoption("--only-rest", action="store_true", default=False) + parser.addoption("--only-xmlrpc", action="store_true", default=False) + + +def pytest_ignore_collect(collection_path, config): + has_ro = config.getoption("--ro-functional") + has_ro_i = config.getoption("--ro-integration") + has_rw = config.getoption("--rw-functional") + has_rw_i = config.getoption("--rw-integration") + + base = os.path.basename(str(collection_path)) + is_ro = base == "test_ro_functional.py" + is_ro_i = "tests/integration/ro" in str(collection_path) + is_rw = base == "test_rw_functional.py" + is_rw_i = "tests/integration/rw" in str(collection_path) + + if is_ro_i and not has_ro_i: + return True + if is_rw_i and not has_rw_i: + return True + + if is_ro and not has_ro: + return True + if is_rw and not has_rw: + return True + + +def pytest_configure(config): + # Needed for test reproducibility on any system not using a UTF-8 locale + locale.setlocale(locale.LC_ALL, "C") + for loc in ["C.UTF-8", "C.utf8", "UTF-8", "en_US.UTF-8"]: + try: + locale.setlocale(locale.LC_CTYPE, loc) + break + except locale.Error: + pass + else: + raise locale.Error("No UTF-8 locale found") + + if config.getoption("--redhat-url"): + tests.CLICONFIG.REDHAT_URL = config.getoption("--redhat-url") + if config.getoption("--pybz-debug"): + logging.getLogger(bugzilla.__name__).setLevel(logging.DEBUG) + os.environ["__BUGZILLA_UNITTEST_DEBUG"] = "1" + if config.getoption("--regenerate-output"): + tests.CLICONFIG.REGENERATE_OUTPUT = config.getoption( + "--regenerate-output") + if config.getoption("--only-rest"): + tests.CLICONFIG.ONLY_REST = True + if config.getoption("--only-xmlrpc"): + tests.CLICONFIG.ONLY_XMLRPC = True + + if (config.getoption("--ro-functional") or + config.getoption("--rw-functional")): + config.option.verbose = 2 + + +def pytest_generate_tests(metafunc): + """ + If a test requests the 'backends' fixture, run that test with both + force_rest=True and force_xmlrpc=True Bugzilla options + """ + if 'backends' in metafunc.fixturenames: + values = [] + ids = [] + if not tests.CLICONFIG.ONLY_REST: + values.append({"force_xmlrpc": True}) + ids.append("XMLRPC") + if not tests.CLICONFIG.ONLY_XMLRPC: + values.append({"force_rest": True}) + ids.append("REST") + metafunc.parametrize("backends", values, ids=ids, scope="session") + + +@pytest.fixture +def run_cli(capsys, monkeypatch): + """ + Custom pytest fixture to pass a function for testing + a bugzilla cli command. + """ + def _do_run(*args, **kwargs): + return tests.utils.do_run_cli(capsys, monkeypatch, *args, **kwargs) + return _do_run + + +@pytest.fixture +def mocked_responses(): + """ + Mock responses + + * Quickly return error responses + * Pass through requests to live instances + * Provide an incorrect XMLRPC response + """ + passthrough = () + status_pattern = re.compile(r"https://httpstat.us/(?P\d+).*") + + def status_callback(request): + match = status_pattern.match(request.url) + status_code = 400 + if match: + status_code = int(match.group("status")) + + return status_code, {}, "

Lorem ipsum

" + + test_url = os.getenv("BUGZILLA_URL") + if test_url: + passthrough += (test_url, test_url.replace("http://", "https://")) + with responses.RequestsMock(passthru_prefixes=passthrough, + assert_all_requests_are_fired=False) as mock: + mock.add_callback( + method=responses.GET, + url=status_pattern, + callback=status_callback + ) + mock.add_callback( + method=responses.POST, + url=status_pattern, + callback=status_callback + ) + mock.add( + method=responses.POST, + url="https://example.com/#xmlrpc", + status=200, + body="This is no XML" + ) + yield mock diff --git a/tests/createbug.py b/tests/createbug.py deleted file mode 100644 index f9c7b1bc..00000000 --- a/tests/createbug.py +++ /dev/null @@ -1,91 +0,0 @@ -# -# Copyright Red Hat, Inc. 2013 -# -# This work is licensed under the terms of the GNU GPL, version 2 or later. -# See the COPYING file in the top-level directory. -# - -''' -Unit tests for building createbug dictionaries with bin/bugzilla -''' - -import unittest - -import tests - - -bz4 = tests.make_bz("4.0.0") - - -class CreatebugTest(unittest.TestCase): - maxDiff = None - bz = bz4 - - def assertDictEqual(self, *args, **kwargs): - # pylint: disable=arguments-differ - # EPEL5 back compat - if hasattr(unittest.TestCase, "assertDictEqual"): - return unittest.TestCase.assertDictEqual(self, *args, **kwargs) - return self.assertEqual(*args, **kwargs) - - def clicomm(self, argstr, out): - comm = "bugzilla new --test-return-result " + argstr - - if out is None: - self.assertRaises(RuntimeError, tests.clicomm, comm, self.bz) - else: - q = tests.clicomm(comm, self.bz, returnmain=True) - self.assertDictEqual(out, q) - - def testBasic(self): - self.clicomm( - "--product foo --component bar --summary baz --version 12", - {'component': 'bar', 'product': 'foo', - 'summary': 'baz', 'version': '12'} - ) - - def testOpSys(self): - self.clicomm( - "--os windowsNT --arch ia64 --comment 'youze a foo' --cc me", - {'description': 'youze a foo', 'op_sys': 'windowsNT', - 'platform': 'ia64', 'cc': ["me"]} - ) - - def testSeverity(self): - self.clicomm( - "--severity HIGH --priority Low --url http://example.com", - {'url': 'http://example.com', 'priority': 'Low', - 'severity': 'HIGH'} - ) - - def testMisc(self): - self.clicomm( - "--alias some-alias", - {"alias": "some-alias"} - ) - - def testMultiOpts(self): - # Test all opts that can take lists - out = {'blocks': ['3', '4'], 'cc': ['1', '2'], - 'depends_on': ['5', 'foo', 'wib'], 'groups': ['bar', '8'], - 'keywords': ['TestOnly', 'ZStream']} - self.clicomm( - "--cc 1,2 --blocked 3,4 --dependson 5,foo,wib --groups bar,8 " - "--keywords TestOnly,ZStream", - out - ) - self.clicomm( - "--cc 1 --cc 2 --blocked 3 --blocked 4 " - "--dependson 5,foo --dependson wib --groups bar --groups 8 " - "--keywords TestOnly --keywords ZStream", - out - ) - - def testFieldConversion(self): - vc = self.bz._validate_createbug # pylint: disable=protected-access - out = vc(product="foo", component="bar", - version="12", description="foo", short_desc="bar", - check_args=False) - self.assertDictEqual(out, - {'component': 'bar', 'description': 'foo', 'product': 'foo', - 'summary': 'bar', 'version': '12'}) diff --git a/tests/data/authfiles/output-bugzillarc.txt b/tests/data/authfiles/output-bugzillarc.txt new file mode 100644 index 00000000..5083b9b6 --- /dev/null +++ b/tests/data/authfiles/output-bugzillarc.txt @@ -0,0 +1,3 @@ +[example.com] +api_key = TEST-API-KEY + diff --git a/tests/data/authfiles/output-token.txt b/tests/data/authfiles/output-token.txt new file mode 100644 index 00000000..9fd7d445 --- /dev/null +++ b/tests/data/authfiles/output-token.txt @@ -0,0 +1,3 @@ +[example.com] +token = MY-FAKE-TOKEN + diff --git a/tests/data/clioutput/test_info_components-active.txt b/tests/data/clioutput/test_info_components-active.txt new file mode 100644 index 00000000..265e86d7 --- /dev/null +++ b/tests/data/clioutput/test_info_components-active.txt @@ -0,0 +1,2 @@ +backend/kernel +client-interfaces diff --git a/tests/data/clioutput/test_info_components-owners.txt b/tests/data/clioutput/test_info_components-owners.txt new file mode 100644 index 00000000..68c2aa89 --- /dev/null +++ b/tests/data/clioutput/test_info_components-owners.txt @@ -0,0 +1,2 @@ +client-interfaces: Fake Guy +configuration: ANother fake dude! diff --git a/tests/data/clioutput/test_info_components.txt b/tests/data/clioutput/test_info_components.txt new file mode 100644 index 00000000..265e86d7 --- /dev/null +++ b/tests/data/clioutput/test_info_components.txt @@ -0,0 +1,2 @@ +backend/kernel +client-interfaces diff --git a/tests/data/clioutput/test_info_products.txt b/tests/data/clioutput/test_info_products.txt new file mode 100644 index 00000000..e3612b11 --- /dev/null +++ b/tests/data/clioutput/test_info_products.txt @@ -0,0 +1,2 @@ +Prod 1 Test +test-fake-product diff --git a/tests/data/clioutput/test_info_versions.txt b/tests/data/clioutput/test_info_versions.txt new file mode 100644 index 00000000..5bbac933 --- /dev/null +++ b/tests/data/clioutput/test_info_versions.txt @@ -0,0 +1,2 @@ +7.1 +fooversion! diff --git a/tests/data/clioutput/test_interactive_login_apikey_rcfile.txt b/tests/data/clioutput/test_interactive_login_apikey_rcfile.txt new file mode 100644 index 00000000..3d266dd5 --- /dev/null +++ b/tests/data/clioutput/test_interactive_login_apikey_rcfile.txt @@ -0,0 +1,3 @@ +[example.com] +api_key = MY-FAKE-KEY + diff --git a/tests/data/clioutput/test_json_xmlrpc.txt b/tests/data/clioutput/test_json_xmlrpc.txt new file mode 100644 index 00000000..262b9487 --- /dev/null +++ b/tests/data/clioutput/test_json_xmlrpc.txt @@ -0,0 +1,9 @@ +{ + "bugs": [ + { + "binarytest": "LS0tIGJhc2UucHkub2xkCTIwMTAtMTItMTYgMTI6MTU6MDkuOTMyMDEwNjU5ICswMTAwCisrKyBiYXNlLnB5CTIwMTAtMTItMTYgMTY6MDQ6MTguOTk1MTg1OTMzICswMTAwCkBAIC0xOSw2ICsxOSw4IEBACiBpbXBvcnQgdGVtcGZpbGUKIGltcG9ydCBsb2dnaW5nCiBpbXBvcnQgbG9jYWxlCitpbXBvcnQgZW1haWwuaGVhZGVyCitpbXBvcnQgcmUKIAogbG9nID0gbG9nZ2luZy5nZXRMb2dnZXIoJ2J1Z3ppbGxhJykKIApAQCAtNjc3LDEwICs2NzksMTcgQEAKICAgICAgICAgIyBSRkMgMjE4MyBkZWZpbmVzIHRoZSBjb250ZW50LWRpc3Bvc2l0aW9uIGhlYWRlciwgaWYgeW91J3JlIGN1cmlvdXMKICAgICAgICAgZGlzcCA9IGF0dC5oZWFkZXJzWydjb250ZW50LWRpc3Bvc2l0aW9uJ10uc3BsaXQoJzsnKQogICAgICAgICBbZmlsZW5hbWVfcGFybV0gPSBbaSBmb3IgaSBpbiBkaXNwIGlmIGkuc3RyaXAoKS5zdGFydHN3aXRoKCdmaWxlbmFtZT0nKV0KLSAgICAgICAgKGR1bW15LGZpbGVuYW1lKSA9IGZpbGVuYW1lX3Bhcm0uc3BsaXQoJz0nKQotICAgICAgICAjIFJGQyAyMDQ1LzgyMiBkZWZpbmVzIHRoZSBncmFtbWFyIGZvciB0aGUgZmlsZW5hbWUgdmFsdWUsIGJ1dAotICAgICAgICAjIEkgdGhpbmsgd2UganVzdCBuZWVkIHRvIHJlbW92ZSB0aGUgcXVvdGluZy4gSSBob3BlLgotICAgICAgICBhdHQubmFtZSA9IGZpbGVuYW1lLnN0cmlwKCciJykKKyAgICAgICAgKGR1bW15LGZpbGVuYW1lKSA9IGZpbGVuYW1lX3Bhcm0uc3BsaXQoJz0nLDEpCisgICAgICAgICMgUkZDIDIwNDUvODIyIGRlZmluZXMgdGhlIGdyYW1tYXIgZm9yIHRoZSBmaWxlbmFtZSB2YWx1ZQorICAgICAgICBmaWxlbmFtZSA9IGZpbGVuYW1lLnN0cmlwKCciJykKKyAgICAgICAgIyBlbWFpbC5oZWFkZXIuZGVjb2RlX2hlYWRlciBjYW5ub3QgaGFuZGxlIHN0cmluZ3Mgbm90IGVuZGluZyB3aXRoICc/PScsCisgICAgICAgICMgc28gbGV0J3MgdHJhbnNmb3JtIG9uZSA9Py4uLj89IHBhcnQgYXQgYSB0aW1lCisgICAgICAgIHdoaWxlIFRydWU6CisgICAgICAgICAgICBtYXRjaCA9IHJlLnNlYXJjaCgiPVw/Lio/XD89IiwgZmlsZW5hbWUpCisgICAgICAgICAgICBpZiBtYXRjaCBpcyBOb25lOgorICAgICAgICAgICAgICAgIGJyZWFrCisgICAgICAgICAgICBmaWxlbmFtZSA9IGZpbGVuYW1lWzptYXRjaC5zdGFydCgpXSArIGVtYWlsLmhlYWRlci5kZWNvZGVfaGVhZGVyKG1hdGNoLmdyb3VwKDApKVswXVswXSArIGZpbGVuYW1lW21hdGNoLmVuZCgpOl0KKyAgICAgICAgYXR0Lm5hbWUgPSBmaWxlbmFtZQogICAgICAgICAjIEhvb3JheSwgbm93IHdlIGhhdmUgYSBmaWxlLWxpa2Ugb2JqZWN0IHdpdGggLnJlYWQoKSBhbmQgLm5hbWUKICAgICAgICAgcmV0dXJuIGF0dAogCg==", + "id": 1165434, + "timetest": "2018-12-09T19:12:12Z" + } + ] +} diff --git a/tests/data/clioutput/test_new1.txt b/tests/data/clioutput/test_new1.txt new file mode 100644 index 00000000..0e1e2bde --- /dev/null +++ b/tests/data/clioutput/test_new1.txt @@ -0,0 +1 @@ +#1694158 CLOSED - crobinso@redhat.com - python-bugzilla test bug for API minor_update diff --git a/tests/data/clioutput/test_new2.txt b/tests/data/clioutput/test_new2.txt new file mode 100644 index 00000000..0e1e2bde --- /dev/null +++ b/tests/data/clioutput/test_new2.txt @@ -0,0 +1 @@ +#1694158 CLOSED - crobinso@redhat.com - python-bugzilla test bug for API minor_update diff --git a/tests/data/clioutput/test_query1-ids.txt b/tests/data/clioutput/test_query1-ids.txt new file mode 100644 index 00000000..71aca18a --- /dev/null +++ b/tests/data/clioutput/test_query1-ids.txt @@ -0,0 +1,2 @@ +508645 +668543 diff --git a/tests/data/clioutput/test_query1-rhbz.txt b/tests/data/clioutput/test_query1-rhbz.txt new file mode 100644 index 00000000..5d93087c --- /dev/null +++ b/tests/data/clioutput/test_query1-rhbz.txt @@ -0,0 +1,2 @@ +#508645 NEW - Libvirt Maintainers - RFE: qemu: Support a managed autoconnect mode for host USB devices +#668543 NEW - Cole Robinson - RFE: warn users at guest start if networks/storage pools are inactive diff --git a/tests/data/clioutput/test_query1.txt b/tests/data/clioutput/test_query1.txt new file mode 100644 index 00000000..5d93087c --- /dev/null +++ b/tests/data/clioutput/test_query1.txt @@ -0,0 +1,2 @@ +#508645 NEW - Libvirt Maintainers - RFE: qemu: Support a managed autoconnect mode for host USB devices +#668543 NEW - Cole Robinson - RFE: warn users at guest start if networks/storage pools are inactive diff --git a/tests/data/clioutput/test_query10.txt b/tests/data/clioutput/test_query10.txt new file mode 100644 index 00000000..76cb0117 --- /dev/null +++ b/tests/data/clioutput/test_query10.txt @@ -0,0 +1 @@ +#1165434 CLOSED - lvm-team@redhat.com - LVM mirrored root can deadlock dmeventd if a mirror leg is lost diff --git a/tests/data/clioutput/test_query2-rhbz.txt b/tests/data/clioutput/test_query2-rhbz.txt new file mode 100644 index 00000000..5d93087c --- /dev/null +++ b/tests/data/clioutput/test_query2-rhbz.txt @@ -0,0 +1,2 @@ +#508645 NEW - Libvirt Maintainers - RFE: qemu: Support a managed autoconnect mode for host USB devices +#668543 NEW - Cole Robinson - RFE: warn users at guest start if networks/storage pools are inactive diff --git a/tests/data/clioutput/test_query2.txt b/tests/data/clioutput/test_query2.txt new file mode 100644 index 00000000..d959145f --- /dev/null +++ b/tests/data/clioutput/test_query2.txt @@ -0,0 +1,64 @@ +Bugzilla 1165434: +ATTRIBUTE[actual_time]: 0.0 +ATTRIBUTE[alias]: [] +ATTRIBUTE[assigned_to]: lvm-team@redhat.com +ATTRIBUTE[assigned_to_detail]: 'DICT SCRUBBED' +ATTRIBUTE[autorefresh]: False +ATTRIBUTE[blocks]: [123456] +ATTRIBUTE[cc]: ['example@redhat.com', 'example2@redhat.com'] +ATTRIBUTE[cc_detail]: ['DICT SCRUBBED'] +ATTRIBUTE[cf_build_id]: +ATTRIBUTE[cf_conditional_nak]: [] +ATTRIBUTE[cf_cust_facing]: --- +ATTRIBUTE[cf_devel_whiteboard]: somedeveltag,someothertag +ATTRIBUTE[cf_doc_type]: Bug Fix +ATTRIBUTE[cf_environment]: +ATTRIBUTE[cf_fixed_in]: +ATTRIBUTE[cf_internal_whiteboard]: someinternal TAG +ATTRIBUTE[cf_last_closed]: 2016-03-03T22:15:07 +ATTRIBUTE[cf_partner]: [] +ATTRIBUTE[cf_pgm_internal]: +ATTRIBUTE[cf_pm_score]: 0 +ATTRIBUTE[cf_qa_whiteboard]: foo bar baz +ATTRIBUTE[cf_qe_conditional_nak]: [] +ATTRIBUTE[cf_release_notes]: +ATTRIBUTE[cf_target_upstream_version]: +ATTRIBUTE[cf_verified]: [] +ATTRIBUTE[classification]: Red Hat +ATTRIBUTE[comments]: ['DICT SCRUBBED'] +ATTRIBUTE[depends_on]: [112233] +ATTRIBUTE[docs_contact]: +ATTRIBUTE[estimated_time]: 0.0 +ATTRIBUTE[external_bugs]: ['DICT SCRUBBED'] +ATTRIBUTE[flags]: ['DICT SCRUBBED'] +ATTRIBUTE[groups]: ['somegroup'] +ATTRIBUTE[id]: 1165434 +ATTRIBUTE[is_cc_accessible]: True +ATTRIBUTE[is_confirmed]: True +ATTRIBUTE[is_creator_accessible]: True +ATTRIBUTE[is_open]: False +ATTRIBUTE[keywords]: ['key1', 'keyword2', 'Security'] +ATTRIBUTE[last_change_time]: 2018-12-09T19:12:12 +ATTRIBUTE[op_sys]: Linux +ATTRIBUTE[platform]: All +ATTRIBUTE[priority]: medium +ATTRIBUTE[product]: Red Hat Enterprise Linux 5 +ATTRIBUTE[qa_contact]: mspqa-list@redhat.com +ATTRIBUTE[qa_contact_detail]: 'DICT SCRUBBED' +ATTRIBUTE[remaining_time]: 0.0 +ATTRIBUTE[resolution]: WONTFIX +ATTRIBUTE[see_also]: [] +ATTRIBUTE[severity]: medium +ATTRIBUTE[status]: CLOSED +ATTRIBUTE[sub_components]: 'DICT SCRUBBED' +ATTRIBUTE[summary]: LVM mirrored root can deadlock dmeventd if a mirror leg is lost +ATTRIBUTE[tags]: [] +ATTRIBUTE[target_milestone]: rc +ATTRIBUTE[target_release]: ['---'] +ATTRIBUTE[url]: +ATTRIBUTE[version]: ['5.8'] +ATTRIBUTE[weburl]: https:///show_bug.cgi?id=1165434 +ATTRIBUTE[whiteboard]: genericwhiteboard + + + diff --git a/tests/data/clioutput/test_query3.txt b/tests/data/clioutput/test_query3.txt new file mode 100644 index 00000000..0e223c4c --- /dev/null +++ b/tests/data/clioutput/test_query3.txt @@ -0,0 +1,23 @@ +:::genericwhiteboard:qe_test_coverage?,release?,pm_ack?,devel_ack?,qa_ack+,needinfo?:hello@example.com::?:: +* 2014-11-19T00:26:50 - example@redhat.com: +Description of problem: +Version-Release number of selected component (if applicable): +kernel-2.6.18-308.el5 +device-mapper-multipath-0.4.7-48.el5 +device-mapper-1.02.67-2.el5 +device-mapper-1.02.67-2.el5 +device-mapper-event-1.02.67-2.el5 + + +* 2014-11-19T00:47:57 - example@redhat.com: +We can see that there is a dmeventd task that has sent data over a socket and is waiting for the peer to respond: + +crash> bt +any interaction with the filesystem until it has issued the suspend command to convert the mirror device to a linear device. + +* 2014-11-19T01:53:53 - example@redhat.com: +Test text +:: +External bug: https://bugzilla.gnome.org/show_bug.cgi?id=703421 +External bug: https://bugs.launchpad.net/bugs/1203576 + diff --git a/tests/data/clioutput/test_query4.txt b/tests/data/clioutput/test_query4.txt new file mode 100644 index 00000000..7b1accb3 --- /dev/null +++ b/tests/data/clioutput/test_query4.txt @@ -0,0 +1,26 @@ +#1165434 CLOSED - lvm-team@redhat.com - LVM mirrored root can deadlock dmeventd if a mirror leg is lost +Component: +CC: example@redhat.com,example2@redhat.com +Blocked: 123456 +Depends: 112233 + +* 2014-11-19T00:26:50 - example@redhat.com: +Description of problem: +Version-Release number of selected component (if applicable): +kernel-2.6.18-308.el5 +device-mapper-multipath-0.4.7-48.el5 +device-mapper-1.02.67-2.el5 +device-mapper-1.02.67-2.el5 +device-mapper-event-1.02.67-2.el5 + + +* 2014-11-19T00:47:57 - example@redhat.com: +We can see that there is a dmeventd task that has sent data over a socket and is waiting for the peer to respond: + +crash> bt +any interaction with the filesystem until it has issued the suspend command to convert the mirror device to a linear device. + +* 2014-11-19T01:53:53 - example@redhat.com: +Test text + + diff --git a/tests/data/clioutput/test_query5.txt b/tests/data/clioutput/test_query5.txt new file mode 100644 index 00000000..b82e66d2 --- /dev/null +++ b/tests/data/clioutput/test_query5.txt @@ -0,0 +1,6 @@ +#1165434 CLOSED - lvm-team@redhat.com - LVM mirrored root can deadlock dmeventd if a mirror leg is lost + +Keywords: key1,keyword2,Security + +QA Whiteboard: + +Status Whiteboard: genericwhiteboard + +Devel Whiteboard: + diff --git a/tests/data/clioutput/test_query6.txt b/tests/data/clioutput/test_query6.txt new file mode 100644 index 00000000..58990a65 --- /dev/null +++ b/tests/data/clioutput/test_query6.txt @@ -0,0 +1 @@ +#1165434 CLOSED lvm-team@redhat.com [rc] qe_test_coverage?,release?,pm_ack?,devel_ack?,qa_ack+,needinfo? CVE-1234-5678 diff --git a/tests/data/clioutput/test_query7.txt b/tests/data/clioutput/test_query7.txt new file mode 100644 index 00000000..76cb0117 --- /dev/null +++ b/tests/data/clioutput/test_query7.txt @@ -0,0 +1 @@ +#1165434 CLOSED - lvm-team@redhat.com - LVM mirrored root can deadlock dmeventd if a mirror leg is lost diff --git a/tests/data/clioutput/test_query8.txt b/tests/data/clioutput/test_query8.txt new file mode 100644 index 00000000..b8d29c56 --- /dev/null +++ b/tests/data/clioutput/test_query8.txt @@ -0,0 +1,248 @@ +{ + "bugs": [ + { + "actual_time": 0.0, + "alias": [], + "assigned_to": "lvm-team@redhat.com", + "assigned_to_detail": { + "email": "lvm-team@redhat.com", + "id": 206817, + "name": "lvm-team@redhat.com", + "real_name": "LVM and device-mapper development team" + }, + "blocks": [ + 123456 + ], + "cc": [ + "example@redhat.com", + "example2@redhat.com" + ], + "cc_detail": [ + { + "email": "example@redhat.com", + "id": 123456, + "name": "example@redhat.com", + "real_name": "Example user" + }, + { + "email": "example2@redhat.com", + "id": 123457, + "name": "heinzm@redhat.com", + "real_name": "Example2 user" + } + ], + "cf_build_id": "", + "cf_conditional_nak": [], + "cf_cust_facing": "---", + "cf_devel_whiteboard": "somedeveltag,someothertag", + "cf_doc_type": "Bug Fix", + "cf_environment": "", + "cf_fixed_in": "", + "cf_internal_whiteboard": "someinternal TAG", + "cf_last_closed": "2016-03-03T22:15:07", + "cf_partner": [], + "cf_pgm_internal": "", + "cf_pm_score": "0", + "cf_qa_whiteboard": "foo bar baz", + "cf_qe_conditional_nak": [], + "cf_release_notes": "", + "cf_target_upstream_version": "", + "cf_verified": [], + "classification": "Red Hat", + "comments": [ + { + "bug_id": 1165434, + "count": 0, + "creation_time": "2014-11-19T00:26:50", + "creator": "example@redhat.com", + "creator_id": 276776, + "id": 7685441, + "is_private": false, + "tags": [], + "text": "Description of problem:\nVersion-Release number of selected component (if applicable):\nkernel-2.6.18-308.el5\ndevice-mapper-multipath-0.4.7-48.el5\ndevice-mapper-1.02.67-2.el5\ndevice-mapper-1.02.67-2.el5\ndevice-mapper-event-1.02.67-2.el5\n", + "time": "2014-11-19T00:26:50" + }, + { + "bug_id": 1165434, + "count": 1, + "creation_time": "2014-11-19T00:47:57", + "creator": "example@redhat.com", + "creator_id": 276776, + "id": 7685467, + "is_private": false, + "tags": [], + "text": "We can see that there is a dmeventd task that has sent data over a socket and is waiting for the peer to respond:\n\ncrash> bt\nany interaction with the filesystem until it has issued the suspend command to convert the mirror device to a linear device.", + "time": "2014-11-19T00:47:57" + }, + { + "bug_id": 1165434, + "count": 2, + "creation_time": "2014-11-19T01:53:53", + "creator": "example@redhat.com", + "creator_id": 156796, + "id": 7685595, + "is_private": false, + "tags": [], + "text": "Test text", + "time": "2014-11-19T01:53:53" + } + ], + "depends_on": [ + 112233 + ], + "docs_contact": "", + "estimated_time": 0.0, + "external_bugs": [ + { + "bug_id": 989253, + "ext_bz_bug_id": "703421", + "ext_bz_id": 3, + "ext_description": "None", + "ext_priority": "None", + "ext_status": "None", + "id": 115528, + "type": { + "can_get": true, + "can_send": false, + "description": "GNOME Bugzilla", + "full_url": "https://bugzilla.gnome.org/show_bug.cgi?id=%id%", + "id": 3, + "must_send": false, + "send_once": false, + "type": "Bugzilla", + "url": "https://bugzilla.gnome.org" + } + }, + { + "bug_id": 989253, + "ext_bz_bug_id": "1203576", + "ext_bz_id": 29, + "ext_description": "None", + "ext_priority": "None", + "ext_status": "None", + "id": 115527, + "type": { + "can_get": false, + "can_send": false, + "description": "Launchpad", + "full_url": "https://bugs.launchpad.net/bugs/%id%", + "id": 29, + "must_send": false, + "send_once": false, + "type": "None", + "url": "https://bugs.launchpad.net/bugs" + } + } + ], + "flags": [ + { + "creation_date": "2019-11-15T21:57:21Z", + "id": 4302313, + "is_active": 1, + "modification_date": "2019-11-15T21:57:21Z", + "name": "qe_test_coverage", + "setter": "pm-rhel@redhat.com", + "status": "?", + "type_id": 318 + }, + { + "creation_date": "2018-12-25T16:47:43Z", + "id": 3883137, + "is_active": 1, + "modification_date": "2018-12-25T16:47:43Z", + "name": "release", + "setter": "rule-engine@redhat.com", + "status": "?", + "type_id": 1197 + }, + { + "creation_date": "2018-12-25T16:47:38Z", + "id": 3883134, + "is_active": 1, + "modification_date": "2018-12-25T16:47:38Z", + "name": "pm_ack", + "setter": "example3@redhat.com", + "status": "?", + "type_id": 11 + }, + { + "creation_date": "2018-12-25T16:47:38Z", + "id": 3883135, + "is_active": 1, + "modification_date": "2018-12-25T16:47:38Z", + "name": "devel_ack", + "setter": "example2@redhat.com", + "status": "?", + "type_id": 10 + }, + { + "creation_date": "2018-12-25T16:47:38Z", + "id": 3883136, + "is_active": 1, + "modification_date": "2019-04-28T02:07:03Z", + "name": "qa_ack", + "setter": "example@redhat.com", + "status": "+", + "type_id": 9 + }, + { + "creation_date": "2019-03-29T06:50:01Z", + "id": 3999302, + "is_active": 1, + "modification_date": "2019-03-29T06:50:01Z", + "name": "needinfo", + "requestee": "hello@example.com", + "setter": "example@redhat.com", + "status": "?", + "type_id": 1164 + } + ], + "groups": [ + "somegroup" + ], + "id": 1165434, + "is_cc_accessible": true, + "is_confirmed": true, + "is_creator_accessible": true, + "is_open": false, + "keywords": [ + "key1", + "keyword2", + "Security" + ], + "last_change_time": "2018-12-09T19:12:12", + "op_sys": "Linux", + "platform": "All", + "priority": "medium", + "product": "Red Hat Enterprise Linux 5", + "qa_contact": "mspqa-list@redhat.com", + "qa_contact_detail": { + "email": "mspqa-list@redhat.com", + "id": 164197, + "name": "mspqa-list@redhat.com", + "real_name": "Cluster QE" + }, + "remaining_time": 0.0, + "resolution": "WONTFIX", + "see_also": [], + "severity": "medium", + "status": "CLOSED", + "sub_components": { + "lvm2": [ + "dmeventd (RHEL5)" + ] + }, + "summary": "LVM mirrored root can deadlock dmeventd if a mirror leg is lost", + "tags": [], + "target_milestone": "rc", + "target_release": [ + "---" + ], + "url": "", + "version": [ + "5.8" + ], + "whiteboard": "genericwhiteboard" + } + ] +} diff --git a/tests/data/clioutput/test_query9.txt b/tests/data/clioutput/test_query9.txt new file mode 100644 index 00000000..2e698205 --- /dev/null +++ b/tests/data/clioutput/test_query9.txt @@ -0,0 +1,250 @@ +{ + "bugs": [ + { + "actual_time": 0.0, + "alias": [], + "assigned_to": "lvm-team@redhat.com", + "assigned_to_detail": { + "email": "lvm-team@redhat.com", + "id": 206817, + "name": "lvm-team@redhat.com", + "real_name": "LVM and device-mapper development team" + }, + "blocks": [ + 123456 + ], + "cc": [ + "example@redhat.com", + "example2@redhat.com" + ], + "cc_detail": [ + { + "email": "example@redhat.com", + "id": 123456, + "name": "example@redhat.com", + "real_name": "Example user" + }, + { + "email": "example2@redhat.com", + "id": 123457, + "name": "heinzm@redhat.com", + "real_name": "Example2 user" + } + ], + "cf_build_id": "", + "cf_conditional_nak": [], + "cf_cust_facing": "---", + "cf_doc_type": "Bug Fix", + "cf_environment": "", + "cf_last_closed": "2016-03-03T22:15:07", + "cf_partner": [], + "cf_pgm_internal": "", + "cf_pm_score": "0", + "cf_qe_conditional_nak": [], + "cf_release_notes": "", + "cf_target_upstream_version": "", + "cf_verified": [], + "classification": "Red Hat", + "comments": [ + { + "bug_id": 1165434, + "count": 0, + "creation_time": "2014-11-19T00:26:50", + "creator": "example@redhat.com", + "creator_id": 276776, + "id": 7685441, + "is_private": false, + "tags": [], + "text": "Description of problem:\nVersion-Release number of selected component (if applicable):\nkernel-2.6.18-308.el5\ndevice-mapper-multipath-0.4.7-48.el5\ndevice-mapper-1.02.67-2.el5\ndevice-mapper-1.02.67-2.el5\ndevice-mapper-event-1.02.67-2.el5\n", + "time": "2014-11-19T00:26:50" + }, + { + "bug_id": 1165434, + "count": 1, + "creation_time": "2014-11-19T00:47:57", + "creator": "example@redhat.com", + "creator_id": 276776, + "id": 7685467, + "is_private": false, + "tags": [], + "text": "We can see that there is a dmeventd task that has sent data over a socket and is waiting for the peer to respond:\n\ncrash> bt\nany interaction with the filesystem until it has issued the suspend command to convert the mirror device to a linear device.", + "time": "2014-11-19T00:47:57" + }, + { + "bug_id": 1165434, + "count": 2, + "creation_time": "2014-11-19T01:53:53", + "creator": "example@redhat.com", + "creator_id": 156796, + "id": 7685595, + "is_private": false, + "tags": [], + "text": "Test text", + "time": "2014-11-19T01:53:53" + } + ], + "depends_on": [ + 112233 + ], + "devel_whiteboard": "somedeveltag,someothertag", + "docs_contact": "", + "estimated_time": 0.0, + "external_bugs": [ + { + "bug_id": 989253, + "ext_bz_bug_id": "703421", + "ext_bz_id": 3, + "ext_description": "None", + "ext_priority": "None", + "ext_status": "None", + "id": 115528, + "type": { + "can_get": true, + "can_send": false, + "description": "GNOME Bugzilla", + "full_url": "https://bugzilla.gnome.org/show_bug.cgi?id=%id%", + "id": 3, + "must_send": false, + "send_once": false, + "type": "Bugzilla", + "url": "https://bugzilla.gnome.org" + } + }, + { + "bug_id": 989253, + "ext_bz_bug_id": "1203576", + "ext_bz_id": 29, + "ext_description": "None", + "ext_priority": "None", + "ext_status": "None", + "id": 115527, + "type": { + "can_get": false, + "can_send": false, + "description": "Launchpad", + "full_url": "https://bugs.launchpad.net/bugs/%id%", + "id": 29, + "must_send": false, + "send_once": false, + "type": "None", + "url": "https://bugs.launchpad.net/bugs" + } + } + ], + "fixed_in": "", + "flags": [ + { + "creation_date": "2019-11-15T21:57:21Z", + "id": 4302313, + "is_active": 1, + "modification_date": "2019-11-15T21:57:21Z", + "name": "qe_test_coverage", + "setter": "pm-rhel@redhat.com", + "status": "?", + "type_id": 318 + }, + { + "creation_date": "2018-12-25T16:47:43Z", + "id": 3883137, + "is_active": 1, + "modification_date": "2018-12-25T16:47:43Z", + "name": "release", + "setter": "rule-engine@redhat.com", + "status": "?", + "type_id": 1197 + }, + { + "creation_date": "2018-12-25T16:47:38Z", + "id": 3883134, + "is_active": 1, + "modification_date": "2018-12-25T16:47:38Z", + "name": "pm_ack", + "setter": "example3@redhat.com", + "status": "?", + "type_id": 11 + }, + { + "creation_date": "2018-12-25T16:47:38Z", + "id": 3883135, + "is_active": 1, + "modification_date": "2018-12-25T16:47:38Z", + "name": "devel_ack", + "setter": "example2@redhat.com", + "status": "?", + "type_id": 10 + }, + { + "creation_date": "2018-12-25T16:47:38Z", + "id": 3883136, + "is_active": 1, + "modification_date": "2019-04-28T02:07:03Z", + "name": "qa_ack", + "setter": "example@redhat.com", + "status": "+", + "type_id": 9 + }, + { + "creation_date": "2019-03-29T06:50:01Z", + "id": 3999302, + "is_active": 1, + "modification_date": "2019-03-29T06:50:01Z", + "name": "needinfo", + "requestee": "hello@example.com", + "setter": "example@redhat.com", + "status": "?", + "type_id": 1164 + } + ], + "groups": [ + "somegroup" + ], + "id": 1165434, + "internal_whiteboard": "someinternal TAG", + "is_cc_accessible": true, + "is_confirmed": true, + "is_creator_accessible": true, + "is_open": false, + "keywords": [ + "key1", + "keyword2", + "Security" + ], + "last_change_time": "2018-12-09T19:12:12", + "op_sys": "Linux", + "platform": "All", + "priority": "medium", + "product": "Red Hat Enterprise Linux 5", + "qa_contact": "mspqa-list@redhat.com", + "qa_contact_detail": { + "email": "mspqa-list@redhat.com", + "id": 164197, + "name": "mspqa-list@redhat.com", + "real_name": "Cluster QE" + }, + "qa_whiteboard": "foo bar baz", + "remaining_time": 0.0, + "resolution": "WONTFIX", + "see_also": [], + "severity": "medium", + "status": "CLOSED", + "sub_component": "dmeventd (RHEL5)", + "sub_components": { + "lvm2": [ + "dmeventd (RHEL5)" + ] + }, + "summary": "LVM mirrored root can deadlock dmeventd if a mirror leg is lost", + "tags": [], + "target_milestone": "rc", + "target_release": [ + "---" + ], + "url": "", + "version": "5.8", + "versions": [ + "5.8" + ], + "whiteboard": "genericwhiteboard" + } + ] +} diff --git a/tests/data/clioutput/tokenfile.txt b/tests/data/clioutput/tokenfile.txt new file mode 100644 index 00000000..3f4ff578 --- /dev/null +++ b/tests/data/clioutput/tokenfile.txt @@ -0,0 +1,3 @@ +[example.com] +token = my-fake-token + diff --git a/tests/data/cookies-bad.txt b/tests/data/cookies-bad.txt deleted file mode 100644 index 0928f036..00000000 --- a/tests/data/cookies-bad.txt +++ /dev/null @@ -1 +0,0 @@ -foo this is invalid cookies diff --git a/tests/data/cookies-lwp.txt b/tests/data/cookies-lwp.txt deleted file mode 100644 index b8818ef3..00000000 --- a/tests/data/cookies-lwp.txt +++ /dev/null @@ -1,3 +0,0 @@ -#LWP-Cookies-2.0 -Set-Cookie3: Bugzilla_login=notacookie; path="/"; domain=".partner-bugzilla.redhat.com"; domain_dot; expires="2038-01-01 00:00:00Z"; version=0 -Set-Cookie3: Bugzilla_logincookie=notacookie; path="/"; domain=".partner-bugzilla.redhat.com"; domain_dot; expires="2038-01-01 00:00:00Z"; version=0 diff --git a/tests/data/cookies-moz.txt b/tests/data/cookies-moz.txt deleted file mode 100644 index 6a16c9db..00000000 --- a/tests/data/cookies-moz.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Netscape HTTP Cookie File -# http://www.netscape.com/newsref/std/cookie_spec.html -# This is a generated file! Do not edit. - -.partner-bugzilla.redhat.com TRUE / FALSE 2145916800 Bugzilla_login notacookie -.partner-bugzilla.redhat.com TRUE / FALSE 2145916800 Bugzilla_logincookie notacookie diff --git a/tests/data/mockargs/test_api_attachments_create1.txt b/tests/data/mockargs/test_api_attachments_create1.txt new file mode 100644 index 00000000..49fa401b --- /dev/null +++ b/tests/data/mockargs/test_api_attachments_create1.txt @@ -0,0 +1,6 @@ +([123456], + 'STRIPPED-BY-TESTSUITE', + {'content_type': 'text/plain', + 'file_name': 'bz-attach-get1.txt', + 'is_private': True, + 'summary': 'some desc'}) diff --git a/tests/data/mockargs/test_api_component_create1.txt b/tests/data/mockargs/test_api_component_create1.txt new file mode 100644 index 00000000..a59238a0 --- /dev/null +++ b/tests/data/mockargs/test_api_component_create1.txt @@ -0,0 +1,5 @@ +{'default_assignee': 'foo@example.com', + 'default_cc': 'foo3@example.com', + 'default_qa_contact': 'foo2@example.com', + 'is_active': 0, + 'product': 'fooproduct'} diff --git a/tests/data/mockargs/test_api_component_update1.txt b/tests/data/mockargs/test_api_component_update1.txt new file mode 100644 index 00000000..8234405e --- /dev/null +++ b/tests/data/mockargs/test_api_component_update1.txt @@ -0,0 +1,4 @@ +{'names': [{'component': 'foocomponent', 'product': 'fooproduct'}], + 'updates': {'blaharg': 'blahval', + 'default_assignee': 'foo@example.com', + 'is_active': 0}} diff --git a/tests/data/mockargs/test_api_getbugs1.txt b/tests/data/mockargs/test_api_getbugs1.txt new file mode 100644 index 00000000..f31bf44e --- /dev/null +++ b/tests/data/mockargs/test_api_getbugs1.txt @@ -0,0 +1 @@ +([], ['CVE-1234-5678'], {'exclude_fields': 'foo'}) diff --git a/tests/data/mockargs/test_api_getbugs2.txt b/tests/data/mockargs/test_api_getbugs2.txt new file mode 100644 index 00000000..80d8cb73 --- /dev/null +++ b/tests/data/mockargs/test_api_getbugs2.txt @@ -0,0 +1 @@ +(['123456'], ['CVE-1234-FAKE'], {'permissive': 1}) diff --git a/tests/data/mockargs/test_api_groups_get1.txt b/tests/data/mockargs/test_api_groups_get1.txt new file mode 100644 index 00000000..b750e91b --- /dev/null +++ b/tests/data/mockargs/test_api_groups_get1.txt @@ -0,0 +1 @@ +{'membership': False, 'names': ['TestGroups']} diff --git a/tests/data/mockargs/test_api_groups_get2.txt b/tests/data/mockargs/test_api_groups_get2.txt new file mode 100644 index 00000000..70138e42 --- /dev/null +++ b/tests/data/mockargs/test_api_groups_get2.txt @@ -0,0 +1 @@ +{'membership': True, 'names': ['TestGroup']} diff --git a/tests/data/mockargs/test_api_login1.txt b/tests/data/mockargs/test_api_login1.txt new file mode 100644 index 00000000..36ee16ac --- /dev/null +++ b/tests/data/mockargs/test_api_login1.txt @@ -0,0 +1 @@ +{'login': None, 'password': None} diff --git a/tests/data/mockargs/test_api_login2.txt b/tests/data/mockargs/test_api_login2.txt new file mode 100644 index 00000000..a1334c12 --- /dev/null +++ b/tests/data/mockargs/test_api_login2.txt @@ -0,0 +1 @@ +{'login': 'FOO', 'password': 'BAR'} diff --git a/tests/data/mockargs/test_api_products_get1.txt b/tests/data/mockargs/test_api_products_get1.txt new file mode 100644 index 00000000..8275b104 --- /dev/null +++ b/tests/data/mockargs/test_api_products_get1.txt @@ -0,0 +1 @@ +{'ids': [1, 7]} diff --git a/tests/data/mockargs/test_api_products_get2.txt b/tests/data/mockargs/test_api_products_get2.txt new file mode 100644 index 00000000..e90b8640 --- /dev/null +++ b/tests/data/mockargs/test_api_products_get2.txt @@ -0,0 +1,2 @@ +{'include_fields': ['name', 'id', 'components.name'], + 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_api_products_get3.txt b/tests/data/mockargs/test_api_products_get3.txt new file mode 100644 index 00000000..9220969e --- /dev/null +++ b/tests/data/mockargs/test_api_products_get3.txt @@ -0,0 +1 @@ +{'include_fields': ['name', 'id', 'components'], 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_api_products_get4.txt b/tests/data/mockargs/test_api_products_get4.txt new file mode 100644 index 00000000..f193b73a --- /dev/null +++ b/tests/data/mockargs/test_api_products_get4.txt @@ -0,0 +1 @@ +{'exclude_fields': ['product.foo'], 'ids': ['7']} diff --git a/tests/data/mockargs/test_api_products_get5.txt b/tests/data/mockargs/test_api_products_get5.txt new file mode 100644 index 00000000..995f6507 --- /dev/null +++ b/tests/data/mockargs/test_api_products_get5.txt @@ -0,0 +1 @@ +{'include_fields': ['name', 'id', 'components.name'], 'names': [0]} diff --git a/tests/data/mockargs/test_api_users_create.txt b/tests/data/mockargs/test_api_users_create.txt new file mode 100644 index 00000000..5daeae42 --- /dev/null +++ b/tests/data/mockargs/test_api_users_create.txt @@ -0,0 +1 @@ +{'email': 'example1@example.com', 'name': 'fooname', 'password': 'foopass'} diff --git a/tests/data/mockargs/test_api_users_get1.txt b/tests/data/mockargs/test_api_users_get1.txt new file mode 100644 index 00000000..fa10ab9f --- /dev/null +++ b/tests/data/mockargs/test_api_users_get1.txt @@ -0,0 +1 @@ +{'names': ['example2@example.com']} diff --git a/tests/data/mockargs/test_api_users_get2.txt b/tests/data/mockargs/test_api_users_get2.txt new file mode 100644 index 00000000..26109bc5 --- /dev/null +++ b/tests/data/mockargs/test_api_users_get2.txt @@ -0,0 +1 @@ +{'names': ['example1@example.com']} diff --git a/tests/data/mockargs/test_api_users_get3.txt b/tests/data/mockargs/test_api_users_get3.txt new file mode 100644 index 00000000..688392fc --- /dev/null +++ b/tests/data/mockargs/test_api_users_get3.txt @@ -0,0 +1 @@ +{'match': ['example1@example.com']} diff --git a/tests/data/mockargs/test_api_users_update1.txt b/tests/data/mockargs/test_api_users_update1.txt new file mode 100644 index 00000000..cb313590 --- /dev/null +++ b/tests/data/mockargs/test_api_users_update1.txt @@ -0,0 +1 @@ +{'groups': {'remove': ['fedora_contrib']}, 'names': ['example name']} diff --git a/tests/data/mockargs/test_attach1.txt b/tests/data/mockargs/test_attach1.txt new file mode 100644 index 00000000..34292bbd --- /dev/null +++ b/tests/data/mockargs/test_attach1.txt @@ -0,0 +1,8 @@ +(['123456'], + 'STRIPPED-BY-TESTSUITE', + {'comment': 'some comment to go with it', + 'content_type': 'text/x-patch', + 'file_name': 'bz-attach-get1.txt', + 'is_patch': True, + 'is_private': True, + 'summary': 'bz-attach-get1.txt'}) diff --git a/tests/data/mockargs/test_attach2.txt b/tests/data/mockargs/test_attach2.txt new file mode 100644 index 00000000..002998b7 --- /dev/null +++ b/tests/data/mockargs/test_attach2.txt @@ -0,0 +1,5 @@ +(['123456'], + 'STRIPPED-BY-TESTSUITE', + {'content_type': 'text/plain', + 'file_name': 'fake-file-name.txt', + 'summary': 'Some attachment description'}) diff --git a/tests/data/mockargs/test_attach3.txt b/tests/data/mockargs/test_attach3.txt new file mode 100644 index 00000000..48da7d7a --- /dev/null +++ b/tests/data/mockargs/test_attach3.txt @@ -0,0 +1,9 @@ +(['123456'], + 'STRIPPED-BY-TESTSUITE', + {'content_type': 'text/plain', + 'file_name': 'bz-attach-get1.txt', + 'flags': [{'name': 'review', + 'requestee': 'crobinso@redhat.com', + 'status': '-'}], + 'is_obsolete': '1', + 'summary': 'bz-attach-get1.txt'}) diff --git a/tests/data/mockargs/test_attach_get1.txt b/tests/data/mockargs/test_attach_get1.txt new file mode 100644 index 00000000..25381b70 --- /dev/null +++ b/tests/data/mockargs/test_attach_get1.txt @@ -0,0 +1 @@ +(['112233'], {}) diff --git a/tests/data/mockargs/test_attach_get2.txt b/tests/data/mockargs/test_attach_get2.txt new file mode 100644 index 00000000..558c8feb --- /dev/null +++ b/tests/data/mockargs/test_attach_get2.txt @@ -0,0 +1 @@ +(['663674'], {}) diff --git a/tests/data/mockargs/test_attachments_get1.txt b/tests/data/mockargs/test_attachments_get1.txt new file mode 100644 index 00000000..22469fd3 --- /dev/null +++ b/tests/data/mockargs/test_attachments_get1.txt @@ -0,0 +1 @@ +(502352, {}) diff --git a/tests/data/mockargs/test_attachments_getall1.txt b/tests/data/mockargs/test_attachments_getall1.txt new file mode 100644 index 00000000..0614705e --- /dev/null +++ b/tests/data/mockargs/test_attachments_getall1.txt @@ -0,0 +1 @@ +([123456], {'exclude_fields': ['bar'], 'include_fields': ['foo']}) diff --git a/tests/data/mockargs/test_attachments_update1.txt b/tests/data/mockargs/test_attachments_update1.txt new file mode 100644 index 00000000..726b5e76 --- /dev/null +++ b/tests/data/mockargs/test_attachments_update1.txt @@ -0,0 +1,2 @@ +([112233], + {'flags': [{'is_patch': True, 'name': 'needinfo', 'value': 'foobar'}]}) diff --git a/tests/data/mockargs/test_bug_api_comments.txt b/tests/data/mockargs/test_bug_api_comments.txt new file mode 100644 index 00000000..8c0dc078 --- /dev/null +++ b/tests/data/mockargs/test_bug_api_comments.txt @@ -0,0 +1 @@ +([1165434], {}) diff --git a/tests/data/mockargs/test_bug_api_get_attachments.txt b/tests/data/mockargs/test_bug_api_get_attachments.txt new file mode 100644 index 00000000..20c730e9 --- /dev/null +++ b/tests/data/mockargs/test_bug_api_get_attachments.txt @@ -0,0 +1 @@ +([663674], {}) diff --git a/tests/data/mockargs/test_bug_api_history.txt b/tests/data/mockargs/test_bug_api_history.txt new file mode 100644 index 00000000..8c0dc078 --- /dev/null +++ b/tests/data/mockargs/test_bug_api_history.txt @@ -0,0 +1 @@ +([1165434], {}) diff --git a/tests/data/mockargs/test_bug_apis_addcc_update.txt b/tests/data/mockargs/test_bug_apis_addcc_update.txt new file mode 100644 index 00000000..3eea2c90 --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_addcc_update.txt @@ -0,0 +1,2 @@ +([1165434], + {'cc': {'add': ['foo2@example.com']}, 'comment': {'comment': 'foocomment'}}) diff --git a/tests/data/mockargs/test_bug_apis_addcomment_update.txt b/tests/data/mockargs/test_bug_apis_addcomment_update.txt new file mode 100644 index 00000000..3396f824 --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_addcomment_update.txt @@ -0,0 +1 @@ +([1165434], {'comment': {'comment': 'test comment', 'is_private': True}}) diff --git a/tests/data/mockargs/test_bug_apis_close_update.txt b/tests/data/mockargs/test_bug_apis_close_update.txt new file mode 100644 index 00000000..1931f05a --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_close_update.txt @@ -0,0 +1,6 @@ +([1165434], + {'cf_fixed_in': '1.2.3.4.5', + 'comment': {'comment': 'foocomment2'}, + 'dupe_of': 123456, + 'resolution': 'UPSTREAM', + 'status': 'CLOSED'}) diff --git a/tests/data/mockargs/test_bug_apis_deletecc_update.txt b/tests/data/mockargs/test_bug_apis_deletecc_update.txt new file mode 100644 index 00000000..25281146 --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_deletecc_update.txt @@ -0,0 +1,2 @@ +([1165434], + {'cc': {'remove': ['foo2@example.com']}, 'comment': {'comment': 'foocomment'}}) diff --git a/tests/data/mockargs/test_bug_apis_setassignee_update.txt b/tests/data/mockargs/test_bug_apis_setassignee_update.txt new file mode 100644 index 00000000..f07c6672 --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_setassignee_update.txt @@ -0,0 +1,4 @@ +([1165434], + {'assigned_to': 'foo@example.com', + 'comment': {'comment': 'foocomment'}, + 'qa_contact': 'bar@example.com'}) diff --git a/tests/data/mockargs/test_bug_apis_setstatus_update.txt b/tests/data/mockargs/test_bug_apis_setstatus_update.txt new file mode 100644 index 00000000..6e2abf92 --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_setstatus_update.txt @@ -0,0 +1,2 @@ +([1165434], + {'comment': {'comment': 'foocomment', 'is_private': True}, 'status': 'POST'}) diff --git a/tests/data/mockargs/test_bug_apis_updateflags_update.txt b/tests/data/mockargs/test_bug_apis_updateflags_update.txt new file mode 100644 index 00000000..04a79656 --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_updateflags_update.txt @@ -0,0 +1 @@ +([1165434], {'flags': [{'name': 'someflag', 'status': 'someval'}]}) diff --git a/tests/data/mockargs/test_bug_fields.txt b/tests/data/mockargs/test_bug_fields.txt new file mode 100644 index 00000000..4d770533 --- /dev/null +++ b/tests/data/mockargs/test_bug_fields.txt @@ -0,0 +1 @@ +{'include_fields': ['name'], 'names': ['bug_status']} diff --git a/tests/data/mockargs/test_externalbugs_add.txt b/tests/data/mockargs/test_externalbugs_add.txt new file mode 100644 index 00000000..fbf1cd3a --- /dev/null +++ b/tests/data/mockargs/test_externalbugs_add.txt @@ -0,0 +1,8 @@ +{'bug_ids': [1234, 5678], + 'external_bugs': [{'ext_bz_bug_id': 'externalid', + 'ext_description': 'link to launchpad', + 'ext_priority': 'bigly', + 'ext_status': 'CLOSED', + 'ext_type_description': 'some-bug-add-description', + 'ext_type_id': 'launchpad', + 'ext_type_url': 'https://example.com/launchpad/1234'}]} diff --git a/tests/data/mockargs/test_externalbugs_remove.txt b/tests/data/mockargs/test_externalbugs_remove.txt new file mode 100644 index 00000000..02930c2f --- /dev/null +++ b/tests/data/mockargs/test_externalbugs_remove.txt @@ -0,0 +1,6 @@ +{'bug_ids': ['blah'], + 'ext_bz_bug_id': ['99999'], + 'ext_type_description': 'foo-desc', + 'ext_type_id': 'footype', + 'ext_type_url': 'foo-url', + 'ids': ['remove1']} diff --git a/tests/data/mockargs/test_externalbugs_update.txt b/tests/data/mockargs/test_externalbugs_update.txt new file mode 100644 index 00000000..dbc266e2 --- /dev/null +++ b/tests/data/mockargs/test_externalbugs_update.txt @@ -0,0 +1,9 @@ +{'bug_ids': ['some', 'bug', 'id'], + 'ext_bz_bug_id': ['externalid-update'], + 'ext_description': 'link to mozilla', + 'ext_priority': 'like, really bigly', + 'ext_status': 'OPEN', + 'ext_type_description': 'some-bug-update', + 'ext_type_id': 'mozilla', + 'ext_type_url': 'https://mozilla.foo/bar/5678', + 'ids': ['external1', 'external2']} diff --git a/tests/data/mockargs/test_getbug_query9.txt b/tests/data/mockargs/test_getbug_query9.txt new file mode 100644 index 00000000..1905d96e --- /dev/null +++ b/tests/data/mockargs/test_getbug_query9.txt @@ -0,0 +1,13 @@ +([1165434], + [], + {'exclude_fields': ['excludeme'], + 'extra_fields': ['extrame1', + 'extrame2', + 'comments', + 'description', + 'external_bugs', + 'flags', + 'sub_components', + 'tags'], + 'include_fields': ['foo', 'bar', 'id'], + 'permissive': 1}) diff --git a/tests/data/mockargs/test_info_components-active.txt b/tests/data/mockargs/test_info_components-active.txt new file mode 100644 index 00000000..c526a897 --- /dev/null +++ b/tests/data/mockargs/test_info_components-active.txt @@ -0,0 +1,2 @@ +{'include_fields': ['name', 'id', 'components.name', 'components.is_active'], + 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_info_components-owners.txt b/tests/data/mockargs/test_info_components-owners.txt new file mode 100644 index 00000000..4523e133 --- /dev/null +++ b/tests/data/mockargs/test_info_components-owners.txt @@ -0,0 +1,5 @@ +{'include_fields': ['name', + 'id', + 'components.name', + 'components.default_assigned_to'], + 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_info_components.txt b/tests/data/mockargs/test_info_components.txt new file mode 100644 index 00000000..e90b8640 --- /dev/null +++ b/tests/data/mockargs/test_info_components.txt @@ -0,0 +1,2 @@ +{'include_fields': ['name', 'id', 'components.name'], + 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_info_products.txt b/tests/data/mockargs/test_info_products.txt new file mode 100644 index 00000000..fd9d9c8b --- /dev/null +++ b/tests/data/mockargs/test_info_products.txt @@ -0,0 +1 @@ +{'ids': [1, 7], 'include_fields': ['name', 'id']} diff --git a/tests/data/mockargs/test_info_versions.txt b/tests/data/mockargs/test_info_versions.txt new file mode 100644 index 00000000..50dc6e00 --- /dev/null +++ b/tests/data/mockargs/test_info_versions.txt @@ -0,0 +1 @@ +{'include_fields': ['name', 'id', 'versions'], 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_interactive_login.txt b/tests/data/mockargs/test_interactive_login.txt new file mode 100644 index 00000000..24933de8 --- /dev/null +++ b/tests/data/mockargs/test_interactive_login.txt @@ -0,0 +1 @@ +{'login': 'fakeuser', 'password': 'fakepass'} diff --git a/tests/data/mockargs/test_login-restrict.txt b/tests/data/mockargs/test_login-restrict.txt new file mode 100644 index 00000000..a6d82c48 --- /dev/null +++ b/tests/data/mockargs/test_login-restrict.txt @@ -0,0 +1 @@ +{'login': 'FOO', 'password': 'BAR', 'restrict_login': True} diff --git a/tests/data/mockargs/test_login.txt b/tests/data/mockargs/test_login.txt new file mode 100644 index 00000000..a1334c12 --- /dev/null +++ b/tests/data/mockargs/test_login.txt @@ -0,0 +1 @@ +{'login': 'FOO', 'password': 'BAR'} diff --git a/tests/data/mockargs/test_modify1.txt b/tests/data/mockargs/test_modify1.txt new file mode 100644 index 00000000..8d28eb0d --- /dev/null +++ b/tests/data/mockargs/test_modify1.txt @@ -0,0 +1 @@ +(['123456', '1234567'], {'component': 'NEWCOMP', 'status': 'ASSIGNED'}) diff --git a/tests/data/mockargs/test_modify2.txt b/tests/data/mockargs/test_modify2.txt new file mode 100644 index 00000000..3c0315af --- /dev/null +++ b/tests/data/mockargs/test_modify2.txt @@ -0,0 +1,11 @@ +(['123456'], + {'blocks': {'set': ['123456', '445566']}, + 'comment': {'comment': 'some example comment', 'is_private': True}, + 'component': 'NEWCOMP', + 'dupe_of': 555666, + 'flags': [{'name': '-needinfo,+somethingels', 'status': 'e'}], + 'groups': {'remove': ['BAR']}, + 'keywords': {'add': ['FOO']}, + 'resolution': 'DUPLICATE', + 'status': 'CLOSED', + 'whiteboard': 'thisone'}) diff --git a/tests/data/mockargs/test_modify3-tags.txt b/tests/data/mockargs/test_modify3-tags.txt new file mode 100644 index 00000000..e0b3ffdc --- /dev/null +++ b/tests/data/mockargs/test_modify3-tags.txt @@ -0,0 +1 @@ +(['1165434'], {'tags': {'add': ['addtag'], 'remove': ['rmtag']}}) diff --git a/tests/data/mockargs/test_modify3.txt b/tests/data/mockargs/test_modify3.txt new file mode 100644 index 00000000..d3cee364 --- /dev/null +++ b/tests/data/mockargs/test_modify3.txt @@ -0,0 +1,4 @@ +([1165434], + {'cf_devel_whiteboard': 'somedeveltag,someothertag devel-duh', + 'cf_internal_whiteboard': 'someinternal TAG internal-hey bar', + 'cf_qa_whiteboard': 'bar baz yo-qa'}) diff --git a/tests/data/mockargs/test_modify4.txt b/tests/data/mockargs/test_modify4.txt new file mode 100644 index 00000000..7bf1e1c3 --- /dev/null +++ b/tests/data/mockargs/test_modify4.txt @@ -0,0 +1,4 @@ +(['1165434'], + {'cf_fixed_in': 'foofixedin', + 'component': 'lvm2', + 'sub_components': {'lvm2': ['some-sub-component']}}) diff --git a/tests/data/mockargs/test_modify5.txt b/tests/data/mockargs/test_modify5.txt new file mode 100644 index 00000000..b21a6ee2 --- /dev/null +++ b/tests/data/mockargs/test_modify5.txt @@ -0,0 +1,29 @@ +(['1165434'], + {'alias': 'fooalias', + 'assigned_to': 'foo@example.com', + 'bar': 'foo', + 'blocks': {'add': ['1234'], 'remove': ['1235'], 'set': []}, + 'cc': {'add': ['+bar@example.com'], 'remove': ['steve@example.com']}, + 'cf_blah': {'1': 2}, + 'cf_devel_whiteboard': 'DEVBOARD', + 'cf_internal_whiteboard': 'INTBOARD', + 'cf_qa_whiteboard': 'QABOARD', + 'cf_verified': ['Tested'], + 'comment_tags': ['FOOTAG'], + 'depends_on': {'add': ['2234'], 'remove': ['2235'], 'set': []}, + 'groups': {'add': ['foogroup']}, + 'keywords': {'add': ['newkeyword'], 'remove': ['byekeyword'], 'set': []}, + 'minor_update': True, + 'op_sys': 'windows', + 'platform': 'mips', + 'priority': 'high', + 'product': 'newproduct', + 'qa_contact': 'qa@example.com', + 'reset_assigned_to': True, + 'reset_qa_contact': True, + 'severity': 'low', + 'summary': 'newsummary', + 'target_milestone': 'beta', + 'target_release': '1.2.4', + 'url': 'https://example.com', + 'version': '1.2.3'}) diff --git a/tests/data/mockargs/test_new1.txt b/tests/data/mockargs/test_new1.txt new file mode 100644 index 00000000..78f518b7 --- /dev/null +++ b/tests/data/mockargs/test_new1.txt @@ -0,0 +1,10 @@ +{'blocks': ['12345', '6789'], + 'cc': ['foo@example.com', 'bar@example.com'], + 'comment_is_private': True, + 'component': 'FOOCOMP', + 'depends_on': ['dependme'], + 'description': 'This is the first comment!\nWith newline & stuff.', + 'groups': ['FOOGROUP', 'BARGROUP'], + 'keywords': ['ADDKEY'], + 'product': 'FOOPROD', + 'summary': 'Hey this is the title!'} diff --git a/tests/data/mockargs/test_new2.txt b/tests/data/mockargs/test_new2.txt new file mode 100644 index 00000000..a3636ead --- /dev/null +++ b/tests/data/mockargs/test_new2.txt @@ -0,0 +1,24 @@ +{'alias': 'somealias', + 'assigned_to': 'foo@example.com', + 'blocks': ['12345', '6789'], + 'cc': ['foo@example.com', 'bar@example.com'], + 'cf_blah': {'1': 2}, + 'cf_verified': ['Tested'], + 'comment_is_private': True, + 'comment_tags': ['FOO'], + 'component': 'FOOCOMP', + 'depends_on': ['dependme'], + 'description': 'This is the first comment!\nWith newline & stuff.', + 'foo': 'bar', + 'groups': ['FOOGROUP', 'BARGROUP'], + 'keywords': ['ADDKEY'], + 'op_sys': 'linux', + 'platform': 'mips', + 'priority': 'low', + 'product': 'FOOPROD', + 'qa_contact': 'qa@example.com', + 'severity': 'high', + 'sub_components': {'FOOCOMP': ['FOOCOMP']}, + 'summary': 'Hey this is the title!', + 'url': 'https://some.example.com', + 'version': '5.6.7'} diff --git a/tests/data/mockargs/test_query1-ids.txt b/tests/data/mockargs/test_query1-ids.txt new file mode 100644 index 00000000..1f2bb4b3 --- /dev/null +++ b/tests/data/mockargs/test_query1-ids.txt @@ -0,0 +1,4 @@ +{'component': ['foo', 'bar'], + 'id': ['1234', '2480'], + 'include_fields': ['id'], + 'product': ['foo']} diff --git a/tests/data/mockargs/test_query1-rhbz.txt b/tests/data/mockargs/test_query1-rhbz.txt new file mode 100644 index 00000000..76d342e0 --- /dev/null +++ b/tests/data/mockargs/test_query1-rhbz.txt @@ -0,0 +1,18 @@ +{'cc': ['foo@example.com'], + 'component': ['foo', 'bar'], + 'field0-0-0': 'keywords', + 'field1-0-0': 'cf_fixed_in', + 'field2-0-0': 'cf_qa_whiteboard', + 'id': ['1234', '2480'], + 'include_fields': ['assigned_to', 'id', 'status', 'summary'], + 'longdesc': 'some comment string', + 'longdesc_type': 'allwordssubstr', + 'product': ['foo'], + 'qa_contact': 'qa@example.com', + 'query_format': 'advanced', + 'type0-0-0': 'substring', + 'type1-0-0': 'substring', + 'type2-0-0': 'substring', + 'value0-0-0': 'fribkeyword', + 'value1-0-0': 'amifixed', + 'value2-0-0': 'some-example-whiteboard'} diff --git a/tests/data/mockargs/test_query1.txt b/tests/data/mockargs/test_query1.txt new file mode 100644 index 00000000..7b37ed0f --- /dev/null +++ b/tests/data/mockargs/test_query1.txt @@ -0,0 +1,4 @@ +{'component': ['foo', 'bar'], + 'id': ['1234', '2480'], + 'include_fields': ['assigned_to', 'id', 'status', 'summary'], + 'product': ['foo']} diff --git a/tests/data/mockargs/test_query10.txt b/tests/data/mockargs/test_query10.txt new file mode 100644 index 00000000..42ae8b14 --- /dev/null +++ b/tests/data/mockargs/test_query10.txt @@ -0,0 +1,39 @@ +{'alias': 'somealias', + 'assigned_to': 'bar@example.com', + 'field0-0-0': 'keywords', + 'field1-0-0': 'blocked', + 'field2-0-0': 'dependson', + 'field3-0-0': 'bug_file_loc', + 'field4-0-0': 'cf_fixed_in', + 'field5-0-0': 'flagtypes.name', + 'field6-0-0': 'status_whiteboard', + 'field7-0-0': 'cf_devel_whiteboard', + 'include_fields': ['assigned_to', 'id', 'status', 'summary'], + 'priority': ['wibble'], + 'query_format': 'advanced', + 'quicksearch': '1', + 'reporter': 'me@example.com', + 'savedsearch': '2', + 'sharer_id': '3', + 'short_desc': 'search summary', + 'sub_components': ['FOOCOMP'], + 'tag': ['+foo'], + 'target_milestone': 'bar', + 'target_release': 'foo', + 'type0-0-0': 'substring', + 'type1-0-0': 'substring', + 'type2-0-0': 'substring', + 'type3-0-0': 'sometype', + 'type4-0-0': 'substring', + 'type5-0-0': 'substring', + 'type6-0-0': 'substring', + 'type7-0-0': 'substring', + 'value0-0-0': 'FOO', + 'value1-0-0': '12345', + 'value2-0-0': '23456', + 'value3-0-0': 'https://example.com', + 'value4-0-0': '5.5.5', + 'value5-0-0': 'needinfo', + 'value6-0-0': 'FOO', + 'value7-0-0': 'DEVBOARD', + 'version': ['5.6.7']} diff --git a/tests/data/mockargs/test_query2-rhbz.txt b/tests/data/mockargs/test_query2-rhbz.txt new file mode 100644 index 00000000..8889d7f7 --- /dev/null +++ b/tests/data/mockargs/test_query2-rhbz.txt @@ -0,0 +1,5 @@ +{'email1': ['foo@example.com'], + 'emailcc1': True, + 'emailtype1': 'BAR', + 'include_fields': ['assigned_to', 'id', 'status', 'summary'], + 'query_format': 'advanced'} diff --git a/tests/data/mockargs/test_query2.txt b/tests/data/mockargs/test_query2.txt new file mode 100644 index 00000000..d499186e --- /dev/null +++ b/tests/data/mockargs/test_query2.txt @@ -0,0 +1 @@ +{'id': ['1165434'], 'include_fields': ['id']} diff --git a/tests/data/mockargs/test_query3.txt b/tests/data/mockargs/test_query3.txt new file mode 100644 index 00000000..a1e7c651 --- /dev/null +++ b/tests/data/mockargs/test_query3.txt @@ -0,0 +1,10 @@ +{'bug_severity': ['sev1', 'sev2'], + 'include_fields': ['bar', + 'comments', + 'devel_whiteboard', + 'external_bugs', + 'flags', + 'flags_requestee', + 'foo', + 'whiteboard', + 'id']} diff --git a/tests/data/mockargs/test_query4.txt b/tests/data/mockargs/test_query4.txt new file mode 100644 index 00000000..6bcd62e6 --- /dev/null +++ b/tests/data/mockargs/test_query4.txt @@ -0,0 +1,16 @@ +{'bug_status': ['NEW', + 'ASSIGNED', + 'NEEDINFO', + 'ON_DEV', + 'MODIFIED', + 'POST', + 'REOPENED'], + 'include_fields': ['assigned_to', + 'blocks', + 'cc', + 'comments', + 'component', + 'depends_on', + 'id', + 'status', + 'summary']} diff --git a/tests/data/mockargs/test_query5.txt b/tests/data/mockargs/test_query5.txt new file mode 100644 index 00000000..8551539f --- /dev/null +++ b/tests/data/mockargs/test_query5.txt @@ -0,0 +1,10 @@ +{'bug_status': ['ASSIGNED', 'ON_QA', 'FAILS_QA', 'PASSES_QA'], + 'component': ['foo', 'bar', 'baz'], + 'include_fields': ['assigned_to', + 'devel_whiteboard', + 'id', + 'keywords', + 'qa_whiteboard', + 'status', + 'summary', + 'whiteboard']} diff --git a/tests/data/mockargs/test_query6.txt b/tests/data/mockargs/test_query6.txt new file mode 100644 index 00000000..b3a21252 --- /dev/null +++ b/tests/data/mockargs/test_query6.txt @@ -0,0 +1,13 @@ +{'BAR': 'WIBBLE', + 'FOO': '1', + 'bug_status': ['VERIFIED', 'RELEASE_PENDING', 'CLOSED'], + 'cf_blah': {'1': 2}, + 'cf_verified': ['Tested'], + 'include_fields': ['assigned_to', + 'blocks', + 'component', + 'flags', + 'keywords', + 'status', + 'target_milestone', + 'id']} diff --git a/tests/data/mockargs/test_query7.txt b/tests/data/mockargs/test_query7.txt new file mode 100644 index 00000000..56c5fe3e --- /dev/null +++ b/tests/data/mockargs/test_query7.txt @@ -0,0 +1,15 @@ +{'bug_status': ['NEW', + 'ASSIGNED', + 'MODIFIED', + 'ON_DEV', + 'ON_QA', + 'VERIFIED', + 'FAILS_QA', + 'RELEASE_PENDING', + 'POST'], + 'classification': 'Fedora', + 'component': 'virt-manager', + 'include_fields': ['assigned_to', 'id', 'status', 'summary'], + 'order': 'bug_status,bug_id', + 'product': 'Fedora', + 'query_format': 'advanced'} diff --git a/tests/data/mockargs/test_query8.txt b/tests/data/mockargs/test_query8.txt new file mode 100644 index 00000000..d499186e --- /dev/null +++ b/tests/data/mockargs/test_query8.txt @@ -0,0 +1 @@ +{'id': ['1165434'], 'include_fields': ['id']} diff --git a/tests/data/mockargs/test_query9.txt b/tests/data/mockargs/test_query9.txt new file mode 100644 index 00000000..d499186e --- /dev/null +++ b/tests/data/mockargs/test_query9.txt @@ -0,0 +1 @@ +{'id': ['1165434'], 'include_fields': ['id']} diff --git a/tests/data/mockargs/test_query_cve_getbug.txt b/tests/data/mockargs/test_query_cve_getbug.txt new file mode 100644 index 00000000..9f6ec7d2 --- /dev/null +++ b/tests/data/mockargs/test_query_cve_getbug.txt @@ -0,0 +1 @@ +([123456], [], {}) diff --git a/tests/data/mockargs/test_update_flags.txt b/tests/data/mockargs/test_update_flags.txt new file mode 100644 index 00000000..64d864ab --- /dev/null +++ b/tests/data/mockargs/test_update_flags.txt @@ -0,0 +1 @@ +([12345, 6789], {'flags': {'name': 'needinfo', 'status': '?'}}) diff --git a/tests/data/mockreturn/test_attach_get1.txt b/tests/data/mockreturn/test_attach_get1.txt new file mode 100644 index 00000000..5a3a355a --- /dev/null +++ b/tests/data/mockreturn/test_attach_get1.txt @@ -0,0 +1,16 @@ +{'attachments': {'502352': {'bug_id': 663674, + 'content_type': 'text/plain', + 'creation_time': '2011-06-01T18:57:50Z', + 'creator': 'example', + 'data': 'SG9vcmF5IGZvciBNZXRlb3JvbG9naWNrw6kgenByw6F2eSA1XzA0LnBkZiEK', + 'file_name': 'Klíč memorial test file.txt', + 'flags': [], + 'id': 502352, + 'is_obsolete': 0, + 'is_patch': 0, + 'is_private': 0, + 'last_change_time': '2011-06-01T18:57:50Z', + 'size': 45, + 'summary': 'An empty test file with a utf-8 ' + 'filename'}}, + 'bugs': {}} diff --git a/tests/data/mockreturn/test_attach_get2.txt b/tests/data/mockreturn/test_attach_get2.txt new file mode 100644 index 00000000..e744a7aa --- /dev/null +++ b/tests/data/mockreturn/test_attach_get2.txt @@ -0,0 +1,43 @@ +{'attachments': {}, + 'bugs': {'663674': [{'bug_id': 663674, + 'content_type': 'application/octet-stream', + 'creation_time': '2010-12-16T15:28:01Z', + 'creator': 'example', + 'data': 'LS0tIGJhc2UucHkub2xkCTIwMTAtMTItMTYgMTI6MTU6MDkuOTMyMDEwNjU5ICswMTAwCisrKyBiYXNlLnB5CTIwMTAtMTItMTYgMTY6MDQ6MTguOTk1MTg1OTMzICswMTAwCkBAIC0xOSw2ICsxOSw4IEBACiBpbXBvcnQgdGVtcGZpbGUKIGltcG9ydCBsb2dnaW5nCiBpbXBvcnQgbG9jYWxlCitpbXBvcnQgZW1haWwuaGVhZGVyCitpbXBvcnQgcmUKIAogbG9nID0gbG9nZ2luZy5nZXRMb2dnZXIoJ2J1Z3ppbGxhJykKIApAQCAtNjc3LDEwICs2NzksMTcgQEAKICAgICAgICAgIyBSRkMgMjE4MyBkZWZpbmVzIHRoZSBjb250ZW50LWRpc3Bvc2l0aW9uIGhlYWRlciwgaWYgeW91J3JlIGN1cmlvdXMKICAgICAgICAgZGlzcCA9IGF0dC5oZWFkZXJzWydjb250ZW50LWRpc3Bvc2l0aW9uJ10uc3BsaXQoJzsnKQogICAgICAgICBbZmlsZW5hbWVfcGFybV0gPSBbaSBmb3IgaSBpbiBkaXNwIGlmIGkuc3RyaXAoKS5zdGFydHN3aXRoKCdmaWxlbmFtZT0nKV0KLSAgICAgICAgKGR1bW15LGZpbGVuYW1lKSA9IGZpbGVuYW1lX3Bhcm0uc3BsaXQoJz0nKQotICAgICAgICAjIFJGQyAyMDQ1LzgyMiBkZWZpbmVzIHRoZSBncmFtbWFyIGZvciB0aGUgZmlsZW5hbWUgdmFsdWUsIGJ1dAotICAgICAgICAjIEkgdGhpbmsgd2UganVzdCBuZWVkIHRvIHJlbW92ZSB0aGUgcXVvdGluZy4gSSBob3BlLgotICAgICAgICBhdHQubmFtZSA9IGZpbGVuYW1lLnN0cmlwKCciJykKKyAgICAgICAgKGR1bW15LGZpbGVuYW1lKSA9IGZpbGVuYW1lX3Bhcm0uc3BsaXQoJz0nLDEpCisgICAgICAgICMgUkZDIDIwNDUvODIyIGRlZmluZXMgdGhlIGdyYW1tYXIgZm9yIHRoZSBmaWxlbmFtZSB2YWx1ZQorICAgICAgICBmaWxlbmFtZSA9IGZpbGVuYW1lLnN0cmlwKCciJykKKyAgICAgICAgIyBlbWFpbC5oZWFkZXIuZGVjb2RlX2hlYWRlciBjYW5ub3QgaGFuZGxlIHN0cmluZ3Mgbm90IGVuZGluZyB3aXRoICc/PScsCisgICAgICAgICMgc28gbGV0J3MgdHJhbnNmb3JtIG9uZSA9Py4uLj89IHBhcnQgYXQgYSB0aW1lCisgICAgICAgIHdoaWxlIFRydWU6CisgICAgICAgICAgICBtYXRjaCA9IHJlLnNlYXJjaCgiPVw/Lio/XD89IiwgZmlsZW5hbWUpCisgICAgICAgICAgICBpZiBtYXRjaCBpcyBOb25lOgorICAgICAgICAgICAgICAgIGJyZWFrCisgICAgICAgICAgICBmaWxlbmFtZSA9IGZpbGVuYW1lWzptYXRjaC5zdGFydCgpXSArIGVtYWlsLmhlYWRlci5kZWNvZGVfaGVhZGVyKG1hdGNoLmdyb3VwKDApKVswXVswXSArIGZpbGVuYW1lW21hdGNoLmVuZCgpOl0KKyAgICAgICAgYXR0Lm5hbWUgPSBmaWxlbmFtZQogICAgICAgICAjIEhvb3JheSwgbm93IHdlIGhhdmUgYSBmaWxlLWxpa2Ugb2JqZWN0IHdpdGggLnJlYWQoKSBhbmQgLm5hbWUKICAgICAgICAgcmV0dXJuIGF0dAogCg==', + 'file_name': 'bugzilla-filename.patch', + 'flags': [], + 'id': 469147, + 'is_obsolete': 1, + 'is_patch': 0, + 'is_private': 0, + 'last_change_time': '2010-12-21T17:57:36Z', + 'size': 1390, + 'summary': 'Proposed patch'}, + {'bug_id': 663674, + 'content_type': 'text/plain', + 'creation_time': '2010-12-21T17:57:36Z', + 'creator': 'example', + 'data': 'LS0tIC91c3IvbGliL3B5dGhvbjIuNy9zaXRlLXBhY2thZ2VzL2J1Z3ppbGxhL2Jhc2UucHkub3JpZwkyMDEwLTEyLTIxIDEzOjA1OjI5LjcyNzE4OTE0MSArMDEwMAorKysgL3Vzci9saWIvcHl0aG9uMi43L3NpdGUtcGFja2FnZXMvYnVnemlsbGEvYmFzZS5weQkyMDEwLTEyLTIxIDE4OjQ4OjMxLjU5NDgwMjMwNSArMDEwMApAQCAtMTksNiArMTksOCBAQAogaW1wb3J0IHRlbXBmaWxlCiBpbXBvcnQgbG9nZ2luZwogaW1wb3J0IGxvY2FsZQoraW1wb3J0IHJlCitpbXBvcnQgZW1haWwuaGVhZGVyCiAKIGxvZyA9IGxvZ2dpbmcuZ2V0TG9nZ2VyKCdidWd6aWxsYScpCiAKQEAgLTY3NywxMCArNjc5LDEzIEBACiAgICAgICAgICMgUkZDIDIxODMgZGVmaW5lcyB0aGUgY29udGVudC1kaXNwb3NpdGlvbiBoZWFkZXIsIGlmIHlvdSdyZSBjdXJpb3VzCiAgICAgICAgIGRpc3AgPSBhdHQuaGVhZGVyc1snY29udGVudC1kaXNwb3NpdGlvbiddLnNwbGl0KCc7JykKICAgICAgICAgW2ZpbGVuYW1lX3Bhcm1dID0gW2kgZm9yIGkgaW4gZGlzcCBpZiBpLnN0cmlwKCkuc3RhcnRzd2l0aCgnZmlsZW5hbWU9JyldCi0gICAgICAgIChkdW1teSxmaWxlbmFtZSkgPSBmaWxlbmFtZV9wYXJtLnNwbGl0KCc9JykKLSAgICAgICAgIyBSRkMgMjA0NS84MjIgZGVmaW5lcyB0aGUgZ3JhbW1hciBmb3IgdGhlIGZpbGVuYW1lIHZhbHVlLCBidXQKLSAgICAgICAgIyBJIHRoaW5rIHdlIGp1c3QgbmVlZCB0byByZW1vdmUgdGhlIHF1b3RpbmcuIEkgaG9wZS4KLSAgICAgICAgYXR0Lm5hbWUgPSBmaWxlbmFtZS5zdHJpcCgnIicpCisgICAgICAgIChkdW1teSxmaWxlbmFtZSkgPSBmaWxlbmFtZV9wYXJtLnNwbGl0KCc9JywgMSkKKyAgICAgICAgIyBSRkMgMjA0NS84MjIgZGVmaW5lcyB0aGUgZ3JhbW1hciBmb3IgdGhlIGZpbGVuYW1lIHZhbHVlCisgICAgICAgIGZpbGVuYW1lID0gZmlsZW5hbWUuc3RyaXAoJyInKQorICAgICAgICAjIEluIGNhc2UgdGhlIGZpbGVuYW1lIGlzIG5vdCBjb21wbGlhbnQgd2l0aCB0aGUgc3RhbmRhcmQsIGxldCdzIG1ha2UKKyAgICAgICAgIyBpdCBjb3JyZWN0LgorICAgICAgICBmaWVsZHMgPSBlbWFpbC5oZWFkZXIuZGVjb2RlX2hlYWRlcihyZS5zdWIoJyg9XD8oW15cP10qXD8pezN9PSknLCAnIFxcMSAnLCBmaWxlbmFtZSkpCisgICAgICAgIGF0dC5uYW1lID0gJycuam9pbihmaWVsZFsxXSBhbmQgZmllbGRbMF0uZGVjb2RlKGZpZWxkWzFdKSBvciBmaWVsZFswXSBmb3IgZmllbGQgaW4gZmllbGRzKQogICAgICAgICAjIEhvb3JheSwgbm93IHdlIGhhdmUgYSBmaWxlLWxpa2Ugb2JqZWN0IHdpdGggLnJlYWQoKSBhbmQgLm5hbWUKICAgICAgICAgcmV0dXJuIGF0dAogCg==', + 'file_name': 'bugzilla-filename-2.patch', + 'flags': [], + 'id': 470041, + 'is_obsolete': 0, + 'is_patch': 1, + 'is_private': 0, + 'last_change_time': '2010-12-21T17:57:36Z', + 'size': 1351, + 'summary': 'Better proposed patch'}, + {'bug_id': 663674, + 'content_type': 'text/plain', + 'creation_time': '2011-06-01T18:57:50Z', + 'creator': 'example', + 'data': 'SG9vcmF5IGZvciBNZXRlb3JvbG9naWNrw6kgenByw6F2eSA1XzA0LnBkZiEK', + 'file_name': 'Klíč memorial test file.txt', + 'flags': [], + 'id': 502352, + 'is_obsolete': 0, + 'is_patch': 0, + 'is_private': 0, + 'last_change_time': '2011-06-01T18:57:50Z', + 'size': 45, + 'summary': 'An empty test file with a utf-8 filename'}]}} diff --git a/tests/data/mockreturn/test_bug_fields.txt b/tests/data/mockreturn/test_bug_fields.txt new file mode 100644 index 00000000..72c675fa --- /dev/null +++ b/tests/data/mockreturn/test_bug_fields.txt @@ -0,0 +1,391 @@ +# bugzilla.redhat.com 2020-01-10 with {"names": "bug_status"} + +{ + "fields": [ + { + "display_name": "Status", + "id": 2, + "is_custom": False, + "is_mandatory": False, + "is_on_bug_entry": False, + "name": "bug_status", + "type": 2, + "values": [ + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + } + ], + "is_open": True, + "sort_key": 0, + "sortkey": 0, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "NEW", + "sort_key": 10, + "sortkey": 10, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "ASSIGNED", + "sort_key": 20, + "sortkey": 20, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "POST", + "sort_key": 30, + "sortkey": 30, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "MODIFIED", + "sort_key": 40, + "sortkey": 40, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "ON_DEV", + "sort_key": 50, + "sortkey": 50, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "ON_QA", + "sort_key": 60, + "sortkey": 60, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "VERIFIED", + "sort_key": 70, + "sortkey": 70, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "RELEASE_PENDING", + "sort_key": 80, + "sortkey": 80, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_QA" + } + ], + "is_open": False, + "name": "CLOSED", + "sort_key": 90, + "sortkey": 90, + "visibility_values": [] + } + ], + "visibility_values": [] + } + ] +} diff --git a/tests/data/mockreturn/test_getbug.txt b/tests/data/mockreturn/test_getbug.txt new file mode 100644 index 00000000..7824dfa9 --- /dev/null +++ b/tests/data/mockreturn/test_getbug.txt @@ -0,0 +1,72 @@ +{'bugs': [{ + 'actual_time': 0.0, + 'alias': [], + 'assigned_to': 'crobinso@redhat.com', + 'assigned_to_detail': {'email': 'crobinso@redhat.com', + 'id': 199727, + 'name': 'crobinso@redhat.com', + 'real_name': 'Cole Robinson'}, + 'blocks': [], + 'cc': ['crobinso@redhat.com'], + 'cc_detail': [{'email': 'crobinso@redhat.com', + 'id': 199727, + 'name': 'crobinso@redhat.com', + 'real_name': 'Cole Robinson'}], + 'cf_build_id': '', + 'cf_conditional_nak': [], + 'cf_cust_facing': '---', + 'cf_devel_whiteboard': '', + 'cf_doc_type': 'If docs needed, set a value', + 'cf_environment': '', + 'cf_fixed_in': '', + 'cf_internal_whiteboard': '', + 'cf_last_closed': '2019-03-29T16:39:27', + 'cf_partner': [], + 'cf_pgm_internal': '', + 'cf_pm_score': '0', + 'cf_qa_whiteboard': '', + 'cf_qe_conditional_nak': [], + 'cf_release_notes': '', + 'cf_verified': [], + 'classification': 'Fedora', + 'component': ['python-bugzilla'], + 'creation_time': '2019-03-29T16:39:01', + 'creator': 'crobinso@redhat.com', + 'creator_detail': {'email': 'crobinso@redhat.com', + 'id': 199727, + 'name': 'crobinso@redhat.com', + 'real_name': 'Cole Robinson'}, + 'depends_on': [], + 'docs_contact': '', + 'estimated_time': 0.0, + 'groups': [], + 'id': 1694158, + 'is_cc_accessible': True, + 'is_confirmed': True, + 'is_creator_accessible': True, + 'is_open': False, + 'keywords': [], + 'last_change_time': '2019-03-29T16:57:48', + 'op_sys': 'Unspecified', + 'platform': 'Unspecified', + 'priority': 'unspecified', + 'product': 'Fedora', + 'qa_contact': 'extras-qa@fedoraproject.org', + 'qa_contact_detail': {'email': 'extras-qa@fedoraproject.org', + 'id': 171387, + 'name': 'extras-qa@fedoraproject.org', + 'real_name': 'Fedora Extras Quality ' + 'Assurance'}, + 'remaining_time': 0.0, + 'resolution': 'NOTABUG', + 'see_also': [], + 'severity': 'unspecified', + 'status': 'CLOSED', + 'summary': 'python-bugzilla test bug for API minor_update', + 'target_milestone': '---', + 'target_release': ['---'], + 'update_token': '1578493259-h3_XQLcFwQkxxRzj5fTivx_wB8OizN7dPUyU_iJ59Bc', + 'url': '', + 'version': ['30'], + 'whiteboard': ''}], +'faults': []} diff --git a/tests/data/mockreturn/test_getbug_rhel.txt b/tests/data/mockreturn/test_getbug_rhel.txt new file mode 100644 index 00000000..1e0632a2 --- /dev/null +++ b/tests/data/mockreturn/test_getbug_rhel.txt @@ -0,0 +1,196 @@ +{'faults': [], + "bugs" : [{ + 'actual_time': 0.0, + 'alias': [], + 'assigned_to': 'lvm-team@redhat.com', + 'assigned_to_detail': {'email': 'lvm-team@redhat.com', + 'id': 206817, + 'name': 'lvm-team@redhat.com', + 'real_name': 'LVM and device-mapper development team'}, + 'blocks': [123456], + 'cc': ['example@redhat.com', + 'example2@redhat.com'], + 'cc_detail': [{'email': 'example@redhat.com', + 'id': 123456, + 'name': 'example@redhat.com', + 'real_name': 'Example user'}, + {'email': 'example2@redhat.com', + 'id': 123457, + 'name': 'heinzm@redhat.com', + 'real_name': 'Example2 user'}], + 'cf_build_id': '', + 'cf_conditional_nak': [], + 'cf_cust_facing': '---', + 'cf_devel_whiteboard': 'somedeveltag,someothertag', + 'cf_doc_type': 'Bug Fix', + 'cf_environment': '', + 'cf_fixed_in': '', + 'cf_internal_whiteboard': 'someinternal TAG', + 'cf_last_closed': '2016-03-03T22:15:07', + 'cf_partner': [], + 'cf_pgm_internal': '', + 'cf_pm_score': '0', + 'cf_qa_whiteboard': 'foo bar baz', + 'cf_qe_conditional_nak': [], + 'cf_release_notes': '', + 'cf_target_upstream_version': '', + 'cf_verified': [], + 'classification': 'Red Hat', + 'comments': [{'bug_id': 1165434, + 'count': 0, + 'creation_time': '2014-11-19T00:26:50', + 'creator': 'example@redhat.com', + 'creator_id': 276776, + 'id': 7685441, + 'is_private': False, + 'tags': [], + 'text': 'Description of problem:\n' + 'Version-Release number of selected component (if ' + 'applicable):\n' + 'kernel-2.6.18-308.el5\n' + 'device-mapper-multipath-0.4.7-48.el5\n' + 'device-mapper-1.02.67-2.el5\n' + 'device-mapper-1.02.67-2.el5\n' + 'device-mapper-event-1.02.67-2.el5\n', + 'time': '2014-11-19T00:26:50'}, + {'bug_id': 1165434, + 'count': 1, + 'creation_time': '2014-11-19T00:47:57', + 'creator': 'example@redhat.com', + 'creator_id': 276776, + 'id': 7685467, + 'is_private': False, + 'tags': [], + 'text': 'We can see that there is a dmeventd task that has sent ' + 'data over a socket and is waiting for the peer to ' + 'respond:\n' + '\n' + 'crash> bt\n' + 'any interaction with the filesystem until it has ' + 'issued the suspend command to convert the mirror ' + 'device to a linear device.', + 'time': '2014-11-19T00:47:57'}, + {'bug_id': 1165434, + 'count': 2, + 'creation_time': '2014-11-19T01:53:53', + 'creator': 'example@redhat.com', + 'creator_id': 156796, + 'id': 7685595, + 'is_private': False, + 'tags': [], + 'text': 'Test text', + 'time': '2014-11-19T01:53:53'}], + 'depends_on': [112233], + 'docs_contact': '', + 'estimated_time': 0.0, + 'external_bugs': [{'bug_id': 989253, + 'ext_bz_bug_id': '703421', + 'ext_bz_id': 3, + 'ext_description': 'None', + 'ext_priority': 'None', + 'ext_status': 'None', + 'id': 115528, + 'type': {'can_get': True, + 'can_send': False, + 'description': 'GNOME Bugzilla', + 'full_url': 'https://bugzilla.gnome.org/show_bug.cgi?id=%id%', + 'id': 3, + 'must_send': False, + 'send_once': False, + 'type': 'Bugzilla', + 'url': 'https://bugzilla.gnome.org'}}, + {'bug_id': 989253, + 'ext_bz_bug_id': '1203576', + 'ext_bz_id': 29, + 'ext_description': 'None', + 'ext_priority': 'None', + 'ext_status': 'None', + 'id': 115527, + 'type': {'can_get': False, + 'can_send': False, + 'description': 'Launchpad', + 'full_url': 'https://bugs.launchpad.net/bugs/%id%', + 'id': 29, + 'must_send': False, + 'send_once': False, + 'type': 'None', + 'url': 'https://bugs.launchpad.net/bugs'}}], + 'flags': [{'creation_date': '2019-11-15T21:57:21Z', + 'id': 4302313, + 'is_active': 1, + 'modification_date': '2019-11-15T21:57:21Z', + 'name': 'qe_test_coverage', + 'setter': 'pm-rhel@redhat.com', + 'status': '?', + 'type_id': 318}, + {'creation_date': '2018-12-25T16:47:43Z', + 'id': 3883137, + 'is_active': 1, + 'modification_date': '2018-12-25T16:47:43Z', + 'name': 'release', + 'setter': 'rule-engine@redhat.com', + 'status': '?', + 'type_id': 1197}, + {'creation_date': '2018-12-25T16:47:38Z', + 'id': 3883134, + 'is_active': 1, + 'modification_date': '2018-12-25T16:47:38Z', + 'name': 'pm_ack', + 'setter': 'example3@redhat.com', + 'status': '?', + 'type_id': 11}, + {'creation_date': '2018-12-25T16:47:38Z', + 'id': 3883135, + 'is_active': 1, + 'modification_date': '2018-12-25T16:47:38Z', + 'name': 'devel_ack', + 'setter': 'example2@redhat.com', + 'status': '?', + 'type_id': 10}, + {'creation_date': '2018-12-25T16:47:38Z', + 'id': 3883136, + 'is_active': 1, + 'modification_date': '2019-04-28T02:07:03Z', + 'name': 'qa_ack', + 'setter': 'example@redhat.com', + 'status': '+', + 'type_id': 9}, + {'creation_date': '2019-03-29T06:50:01Z', + 'id': 3999302, + 'is_active': 1, + 'modification_date': '2019-03-29T06:50:01Z', + 'name': 'needinfo', + 'setter': 'example@redhat.com', + 'requestee': 'hello@example.com', + 'status': '?', + 'type_id': 1164}], + 'groups': ["somegroup"], + 'id': 1165434, + 'is_cc_accessible': True, + 'is_confirmed': True, + 'is_creator_accessible': True, + 'is_open': False, + 'keywords': ["key1", "keyword2", "Security"], + 'last_change_time': '2018-12-09T19:12:12', + 'op_sys': 'Linux', + 'platform': 'All', + 'priority': 'medium', + 'product': 'Red Hat Enterprise Linux 5', + 'qa_contact': 'mspqa-list@redhat.com', + 'qa_contact_detail': {'email': 'mspqa-list@redhat.com', + 'id': 164197, + 'name': 'mspqa-list@redhat.com', + 'real_name': 'Cluster QE'}, + 'remaining_time': 0.0, + 'resolution': 'WONTFIX', + 'see_also': [], + 'severity': 'medium', + 'status': 'CLOSED', + 'sub_components': {'lvm2': ['dmeventd (RHEL5)']}, + 'summary': 'LVM mirrored root can deadlock dmeventd if a mirror leg is lost', + 'tags': [], + 'target_milestone': 'rc', + 'target_release': ['---'], + 'url': '', + 'version': ['5.8'], + 'whiteboard': 'genericwhiteboard'}]} diff --git a/tests/data/mockreturn/test_query1.txt b/tests/data/mockreturn/test_query1.txt new file mode 100644 index 00000000..4f7019b2 --- /dev/null +++ b/tests/data/mockreturn/test_query1.txt @@ -0,0 +1 @@ +{'bugs': [{'assigned_to_detail': {'real_name': 'Libvirt Maintainers', 'email': 'libvirt-maint', 'name': 'libvirt-maint', 'id': 311982}, 'summary': 'RFE: qemu: Support a managed autoconnect mode for host USB devices', 'status': 'NEW', 'assigned_to': 'Libvirt Maintainers', 'id': 508645}, {'assigned_to_detail': {'real_name': 'Cole Robinson', 'email': 'crobinso', 'name': 'crobinso', 'id': 199727}, 'summary': 'RFE: warn users at guest start if networks/storage pools are inactive', 'status': 'NEW', 'assigned_to': 'Cole Robinson', 'id': 668543}], 'limit': 0, 'FOOFAKEVALUE': 'hello'} diff --git a/tests/data/mockreturn/test_query_cve_getbug.txt b/tests/data/mockreturn/test_query_cve_getbug.txt new file mode 100644 index 00000000..a3c84e95 --- /dev/null +++ b/tests/data/mockreturn/test_query_cve_getbug.txt @@ -0,0 +1,71 @@ +{'bugs': [{'actual_time': 0.0, + 'alias': ["CVE-1234-5678"], + 'assigned_to': 'crobinso@redhat.com', + 'assigned_to_detail': {'email': 'crobinso@redhat.com', + 'id': 199727, + 'name': 'crobinso@redhat.com', + 'real_name': 'Cole Robinson'}, + 'blocks': [], + 'cc': ['crobinso@redhat.com'], + 'cc_detail': [{'email': 'crobinso@redhat.com', + 'id': 199727, + 'name': 'crobinso@redhat.com', + 'real_name': 'Cole Robinson'}], + 'cf_build_id': '', + 'cf_conditional_nak': [], + 'cf_cust_facing': '---', + 'cf_devel_whiteboard': '', + 'cf_doc_type': 'If docs needed, set a value', + 'cf_environment': '', + 'cf_fixed_in': '', + 'cf_internal_whiteboard': '', + 'cf_last_closed': '2019-03-29T16:39:27', + 'cf_partner': [], + 'cf_pgm_internal': '', + 'cf_pm_score': '0', + 'cf_qa_whiteboard': '', + 'cf_qe_conditional_nak': [], + 'cf_release_notes': '', + 'cf_verified': [], + 'classification': 'Fedora', + 'component': ['python-bugzilla'], + 'creation_time': '2019-03-29T16:39:01', + 'creator': 'crobinso@redhat.com', + 'creator_detail': {'email': 'crobinso@redhat.com', + 'id': 199727, + 'name': 'crobinso@redhat.com', + 'real_name': 'Cole Robinson'}, + 'depends_on': [], + 'docs_contact': '', + 'estimated_time': 0.0, + 'groups': [], + 'id': 123456, + 'is_cc_accessible': True, + 'is_confirmed': True, + 'is_creator_accessible': True, + 'is_open': False, + 'keywords': [], + 'last_change_time': '2019-03-29T16:57:48', + 'op_sys': 'Unspecified', + 'platform': 'Unspecified', + 'priority': 'unspecified', + 'product': 'Fedora', + 'qa_contact': 'extras-qa@fedoraproject.org', + 'qa_contact_detail': {'email': 'extras-qa@fedoraproject.org', + 'id': 171387, + 'name': 'extras-qa@fedoraproject.org', + 'real_name': 'Fedora Extras Quality ' + 'Assurance'}, + 'remaining_time': 0.0, + 'resolution': 'NOTABUG', + 'see_also': [], + 'severity': 'unspecified', + 'status': 'CLOSED', + 'summary': 'python-bugzilla test bug for API minor_update', + 'target_milestone': '---', + 'target_release': ['---'], + 'update_token': '1578493259-h3_XQLcFwQkxxRzj5fTivx_wB8OizN7dPUyU_iJ59Bc', + 'url': '', + 'version': ['30'], + 'whiteboard': ''}], + 'faults': []} diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..908a1f54 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,9 @@ +import os + + +TEST_URL = os.getenv("BUGZILLA_URL", "http://localhost") +TEST_OWNER = "andreas@hasenkopf.xyz" +TEST_PRODUCTS = {"Red Hat Enterprise Linux 9", + "SUSE Linux Enterprise Server 15 SP6", + "TestProduct"} +TEST_SUSE_COMPONENTS = {"Containers", "Kernel"} diff --git a/tests/integration/ro_api_test.py b/tests/integration/ro_api_test.py new file mode 100644 index 00000000..d3d6b526 --- /dev/null +++ b/tests/integration/ro_api_test.py @@ -0,0 +1,238 @@ +# Ignoring pytest-related warnings: +# pylint: disable=redefined-outer-name,unused-argument +from urllib.parse import urljoin +from xmlrpc.client import Fault + +import pytest + +from bugzilla import BugzillaError + +from ..utils import open_bz +from . import TEST_URL, TEST_PRODUCTS, TEST_SUSE_COMPONENTS, TEST_OWNER + + +def test_rest_xmlrpc_detection(mocked_responses): + # The default: use XMLRPC + bz = open_bz(url=TEST_URL) + assert bz.is_xmlrpc() + assert "/xmlrpc.cgi" in bz.url + + # See /rest in the URL, so use REST + bz = open_bz(url=TEST_URL + "/rest") + assert bz.is_rest() + with pytest.raises(BugzillaError) as e: + dummy = bz._proxy # pylint: disable=protected-access + assert "raw XMLRPC access is not provided" in str(e) + + # See /xmlrpc.cgi in the URL, so use XMLRPC + bz = open_bz(url=TEST_URL + "/xmlrpc.cgi") + assert "/xmlrpc.cgi" in bz.url + assert bz.is_xmlrpc() + assert bz._proxy # pylint: disable=protected-access + + +def test_apikey_error_scraping(mocked_responses): + # Ensure the API key does not leak into any requests exceptions + fakekey = "FOOBARMYKEY" + with pytest.raises(Exception) as e: + open_bz("https://httpstat.us/400&foo", + force_xmlrpc=True, api_key=fakekey) + assert "Client Error" in str(e.value) + assert fakekey not in str(e.value) + + with pytest.raises(Exception) as e: + open_bz("https://httpstat.us/400&foo", + force_rest=True, api_key=fakekey) + assert "Client Error" in str(e.value) + assert fakekey not in str(e.value) + + +def test_xmlrpc_bad_url(mocked_responses): + with pytest.raises(BugzillaError) as e: + open_bz(url="https://example.com/#xmlrpc", force_xmlrpc=True) + assert "URL may not be an XMLRPC URL" in str(e) + + +def test_get_products(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + + assert len(bz.products) == 3 + assert {p["name"] for p in bz.products} == TEST_PRODUCTS + + rhel = next(p for p in bz.products if p["id"] == 2) + assert {v["name"] for v in rhel["versions"]} == {"9.0", "9.1", "unspecified"} + + +def test_get_product(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + + product_ids = {product["id"] for product in bz.product_get(ptype="enterable", + include_fields=["id"])} + product_names = {product["name"] for product in bz.product_get(ptype="selectable", + include_fields=["name"])} + assert product_ids == {1, 2, 3} + assert product_names == {'Red Hat Enterprise Linux 9', 'SUSE Linux Enterprise Server 15 SP6', + 'TestProduct'} + + +def test_get_components(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + components = bz.getcomponents(product="SUSE Linux Enterprise Server 15 SP6") + assert len(components) == 2 + assert set(components) == TEST_SUSE_COMPONENTS + + +def test_get_component_detail(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + component = bz.getcomponentdetails(product="Red Hat Enterprise Linux 9", + component="python-bugzilla") + assert component["id"] == 2 + assert component["default_assigned_to"] == TEST_OWNER + + +def test_query(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + query = bz.build_query(product="Red Hat Enterprise Linux 9", component="python-bugzilla") + bugs = bz.query(query=query) + + assert len(bugs) == 1 + assert bugs[0].id == 2 + assert bugs[0].summary == "Expect the Spanish inquisition" + + bz = open_bz(url=TEST_URL, **backends) + query = bz.build_query(product="SUSE Linux Enterprise Server 15 SP6", component="Containers") + bugs = bz.query(query=query) + + assert len(bugs) == 1 + assert bugs[0].id == 1 + assert bugs[0].whiteboard == "AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:L" + + +def test_get_bug_alias(mocked_responses, backends): + bug_id, alias = 1, "FOO-1" + bz = open_bz(url=TEST_URL, **backends) + bug = bz.getbug(alias) + + assert bug.id == bug_id + assert bug.bug_id == bug_id + assert bug.alias == [alias] + assert bug.summary == "ZeroDivisionError in function foo_bar()" + + +def test_bug_url(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + bug_id = 2 + + # Ensure weburl is generated consistently whether + # we are using XMLRPC or REST + bug = bz.getbug(bug_id) + assert bug.weburl == urljoin(TEST_URL, f"/show_bug.cgi?id={bug_id}") + + +def test_get_bug_alias_included_field(mocked_responses, backends): + bug_id, alias = 1, "FOO-1" + bz = open_bz(url=TEST_URL, **backends) + bug = bz.getbug(alias, include_fields=["id"]) + + assert bug.id == bug_id + assert bug.bug_id == bug_id + assert bug.alias == [alias] + assert not hasattr(bug, "summary") + + +def test_get_bug_exclude_fields(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + + # Check default extra_fields will pull in comments + bug = bz.getbug(2, exclude_fields=["product"]) + assert not hasattr(bug, "product") + + # Ensure that include_fields overrides default extra_fields + bug = bz.getbug(2) + assert hasattr(bug, "product") + + +def test_get_bug_404(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + try: + bz.getbug(666) + except Fault as error: # XMLRPC API + assert error.faultCode == 101 + except BugzillaError as error: # REST API + assert error.code == 101 + else: + raise AssertionError("No exception raised") + + +def test_get_bug_alias_404(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + try: + bz.getbug("CVE-1234-4321") + except Fault as error: # XMLRPC API + assert error.faultCode == 100 + except BugzillaError as error: # REST API + assert error.code == 100 + else: + raise AssertionError("No exception raised") + + +def test_get_bug_fields(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + fields = bz.getbugfields(names=["product"]) + assert fields == ["product"] + bz.getbugfields(names=["product", "bug_status"], force_refresh=True) + assert set(bz.bugfields) == {"product", "bug_status"} + + +def test_query_autorefresh(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + + bz.bug_autorefresh = True + bug = bz.query(bz.build_query(bug_id=1, include_fields=["summary"]))[0] + assert hasattr(bug, "component") + assert bool(bug.component) + + bz.bug_autorefresh = False + bug = bz.query(bz.build_query(bug_id=1, include_fields=["summary"]))[0] + assert not hasattr(bug, "component") + try: + assert bool(bug.component) + except Exception as e: + assert "adjust your include_fields" in str(e) + + +def test_logged_in_no_creds(mocked_responses, backends): + bz = open_bz(url=TEST_URL, use_creds=False, **backends) + assert not bz.logged_in + + +def test_login_stubs(mocked_responses, backends): + # Explicitly set configpaths to avoid interference with an API key set by another test + bz = open_bz(url=TEST_URL, configpaths="/dev/null", **backends) + bz_apikey = open_bz(url=TEST_URL, api_key="random-and-secure-api-key", **backends) + + # Failed login, verifies our backends are calling the correct API + with pytest.raises(BugzillaError) as e: + bz.login("foo", "bar") + assert "Login failed" in str(e) + + # Login is prohibited, when an API key is defined + with pytest.raises(ValueError) as e: + bz_apikey.login("foo", "bar") + assert "cannot login when using an API key" in str(e) + + # Works fine when not logged in + bz.logout() + + +def test_query_resolution(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + + bugs = bz.query(bz.build_query(short_desc="ZeroDivisionError", resolution=None)) + assert len(bugs) == 1 + + bugs = bz.query(bz.build_query(short_desc="ZeroDivisionError", resolution="---")) + assert len(bugs) == 1 + + bugs = bz.query(bz.build_query(short_desc="ZeroDivisionError", resolution="DUPLICATE")) + assert len(bugs) == 0 diff --git a/tests/integration/ro_cli_test.py b/tests/integration/ro_cli_test.py new file mode 100644 index 00000000..9cd5b3dd --- /dev/null +++ b/tests/integration/ro_cli_test.py @@ -0,0 +1,134 @@ +# Ignoring pytest-related warnings: +# pylint: disable=unused-argument +import re +from urllib.parse import urljoin, urlparse, urlunparse + +from ..utils import open_bz +from . import TEST_URL, TEST_PRODUCTS, TEST_SUSE_COMPONENTS, TEST_OWNER + + +def test_fails(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --field=IDONTEXIST=FOO", bzinstance=bz, expectfail=True) + assert "Server error:" in out + + out = run_cli("bugzilla --bugzilla https://example.com/xmlrpc.cgi query --field=IDONTEXIST=FOO", + bzinstance=None, expectfail=True) + assert "Connection lost/failed" in out + + parsed = urlparse(TEST_URL) + netloc = parsed.netloc + if not re.search(r":\d+$", netloc): + netloc += ":80" + + https_test_url = urlunparse(("https", netloc, parsed.path, parsed.params, parsed.query, + parsed.fragment)) + out = run_cli(f"bugzilla --bugzilla {https_test_url} query --bug_id 1234", + bzinstance=None, expectfail=True) + assert "trust the remote server" in out + assert "--nosslverify" in out + + +def test_get_products(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla info --products", bzinstance=bz) + assert len(out.strip().split("\n")) == 3 + + for product in TEST_PRODUCTS: + assert product in out + + +def test_get_components(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla info --components 'SUSE Linux Enterprise Server 15 SP6'", bzinstance=bz) + assert len(out.strip().split("\n")) == 2 + for comp in TEST_SUSE_COMPONENTS: + assert comp in out + + +def test_get_active_components(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla info --components 'SUSE Linux Enterprise Server 15 SP6' " + "--active-components", bzinstance=bz) + assert "Containers" in out + assert "Kernel" in out + + +def test_get_component_owners(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla info --component_owners 'SUSE Linux Enterprise Server 15 SP6'", + bzinstance=bz) + assert TEST_OWNER in out + + +def test_get_versions(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla info --versions 'Red Hat Enterprise Linux 9'", bzinstance=bz) + versions = set(out.strip().split("\n")) + + assert versions == {"unspecified", "9.0", "9.1"} + + +def test_query(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --product 'Red Hat Enterprise Linux 9' " + "--component 'python-bugzilla'", bzinstance=bz) + lines = out.strip().splitlines() + + assert len(lines) == 1 + assert lines[0].startswith("#2") + assert "Expect the Spanish inquisition" in lines[0] + + +def test_query_full(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --full --bug_id 2", bzinstance=bz) + lines = out.strip().splitlines() + assert len(lines) == 5 + + for name in ('Component', 'CC', 'Blocked', 'Depends'): + assert name in out + + assert "Status Whiteboard" not in out + + +def test_query_raw(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --raw --bug_id 2", bzinstance=bz) + + assert "ATTRIBUTE[whiteboard]: lorem ipsum" in out + assert "ATTRIBUTE[id]: 2" in out + + +def test_query_oneline(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --oneline --bug_id 2", bzinstance=bz) + lines = out.strip().splitlines() + assert len(lines) == 1 + assert "python-bugzilla" in lines[0] + + +def test_query_extra(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --extra --bug_id 2", bzinstance=bz) + lines = out.strip().splitlines() + assert len(lines) == 5 + assert "Keywords: FooBar" in out + assert "Status Whiteboard: lorem ipsum" in out + + +def test_query_format(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --outputformat=\"id=%{bug_id} " + "sw=%{whiteboard:status} needinfo=%{flag:needinfo} " + "sum=%{summary}\" --bug_id 2", bzinstance=bz) + lines = out.strip().splitlines() + assert len(lines) == 1 + assert out.strip() == "id=2 sw=lorem ipsum needinfo=? sum=Expect the Spanish inquisition" + + +def test_query_url(mocked_responses, run_cli, backends): + url = urljoin(TEST_URL, "/buglist.cgi?version=9.1") + bz = open_bz(url=TEST_URL, **backends) + out = run_cli(f"bugzilla query --from-url \"{url}\"", bzinstance=bz) + assert re.search(r"#2\s+CONFIRMED", out) diff --git a/tests/integration/rw_api_test.py b/tests/integration/rw_api_test.py new file mode 100644 index 00000000..8fba7dfb --- /dev/null +++ b/tests/integration/rw_api_test.py @@ -0,0 +1,120 @@ +# pylint: disable=unused-argument +from uuid import uuid4 +from xmlrpc.client import Fault + +from pytest import raises +from pytest import mark + +from bugzilla import Bugzilla, BugzillaError +from bugzilla.bug import Bug + +from ..utils import open_bz +from . import TEST_URL + +# NOTE: The tests in this file assume that an API key is defined in the bugzillarc! + + +DEFAULT_PARAMS = {"product": "TestProduct", + "component": "TestComponent", + "version": "unspecified", + "summary": "A new bug", + "description": "Details on how to reproduce", + "cc": "nemo@example.com", + "op_sys": "Linux", + "platform": "PC"} + + +def _create_bug(bz: Bugzilla, **kwargs) -> Bug: + """ + Create a new bug with overwrite-able defaults + """ + params = DEFAULT_PARAMS.copy() + params.update(kwargs) + + return bz.createbug(**bz.build_createbug(**params)) + + +def test_create_bug(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + bug = _create_bug(bz) + + assert isinstance(bug, Bug) + assert bug.id + + bug = bz.getbug(bug.id) + for field in ("product", "component", "version", "summary"): + assert getattr(bug, field) == DEFAULT_PARAMS[field] + + +def test_create_bug_anonymous(mocked_responses, backends): + bz = open_bz(url=TEST_URL, configpaths="/dev/null", **backends) + with raises((Fault, BugzillaError)): + _create_bug(bz) + + +def test_create_bug_alias(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + alias = uuid4().hex + bug = _create_bug(bz, alias=alias) + + bug = bz.getbug(bug.id) + assert alias in bug.alias + + with raises((Fault, BugzillaError)): + _create_bug(bz, alias=alias) + + +def test_update_bug(mocked_responses, backends): + email = "nemo@example.com" + bz = open_bz(url=TEST_URL, **backends) + bug = _create_bug(bz) + params = bz.build_update(resolution="WONTFIX", status="RESOLVED", cc_remove=email) + bz.update_bugs(bug.id, params) + bug.refresh() + + assert bug.resolution == "WONTFIX" + assert bug.status == "RESOLVED" + assert bug.cc == [] + + params = bz.build_update(cc_add=email) + bz.update_bugs(bug.id, params) + bug.refresh() + + assert bug.cc == [email] + + +# Bugzilla instance has no CLOSED status +@mark.xfail +def test_close_bug(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + bug = _create_bug(bz) + bug.close(resolution="WORKSFORME", comment="Bla bla", isprivate=True) + bug.refresh() + + assert bug.resolution == "WORKSFORME" + assert bug.status == "CLOSED" + + +def test_add_comment(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + bug = bz.getbug(1) + + comment_count = len(bug.get_comments()) + bug.addcomment("Bla Bla bla", private=True) + bug.refresh() + + assert len(bug.get_comments()) == comment_count + 1 + + +def test_update_flags(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + bug = _create_bug(bz) + flag = {"requestee": "nemo@example.com", "name": "needinfo", "status": "?"} + params = bz.build_update(flags=[flag]) + bz.update_bugs([bug.id], params) + bug.refresh() + + assert len(bug.flags) == 1 + + for key, value in flag.items(): + assert bug.flags[0][key] == value diff --git a/tests/misc.py b/tests/misc.py deleted file mode 100644 index 78ee5ab7..00000000 --- a/tests/misc.py +++ /dev/null @@ -1,160 +0,0 @@ -# -# Copyright Red Hat, Inc. 2012 -# -# This work is licensed under the terms of the GNU GPL, version 2 or later. -# See the COPYING file in the top-level directory. -# - -''' -Unit tests for building query strings with bin/bugzilla -''' - -from __future__ import print_function - -import os -import tempfile -import unittest - -import bugzilla - -import tests - - -class MiscCLI(unittest.TestCase): - """ - Test miscellaneous CLI bits to get build out our code coverage - """ - maxDiff = None - - def testHelp(self): - out = tests.clicomm("bugzilla --help", None) - self.assertTrue(len(out.splitlines()) > 18) - - def testCmdHelp(self): - out = tests.clicomm("bugzilla query --help", None) - self.assertTrue(len(out.splitlines()) > 40) - - def testVersion(self): - out = tests.clicomm("bugzilla --version", None) - self.assertTrue(len(out.splitlines()) >= 2) - - -class MiscAPI(unittest.TestCase): - """ - Test miscellaneous API bits - """ - def testUserAgent(self): - b3 = tests.make_bz("3.0.0") - self.assertTrue("python-bugzilla" in b3.user_agent) - - def testCookies(self): - cookiesbad = os.path.join(os.getcwd(), "tests/data/cookies-bad.txt") - cookieslwp = os.path.join(os.getcwd(), "tests/data/cookies-lwp.txt") - cookiesmoz = os.path.join(os.getcwd(), "tests/data/cookies-moz.txt") - - # We used to convert LWP cookies, but it shouldn't matter anymore, - # so verify they fail at least - try: - tests.make_bz("3.0.0", cookiefile=cookieslwp) - raise AssertionError("Expected BugzillaError from parsing %s" % - os.path.basename(cookieslwp)) - except bugzilla.BugzillaError: - # Expected result - pass - - # Make sure bad cookies raise an error - try: - tests.make_bz("3.0.0", cookiefile=cookiesbad) - raise AssertionError("Expected BugzillaError from parsing %s" % - os.path.basename(cookiesbad)) - except bugzilla.BugzillaError: - # Expected result - pass - - # Mozilla should 'just work' - tests.make_bz("3.0.0", cookiefile=cookiesmoz) - - def test_readconfig(self): - # Testing for bugzillarc handling - bzapi = tests.make_bz("4.4.0", rhbz=True) - bzapi.url = "foo.example.com" - temp = tempfile.NamedTemporaryFile(mode="w") - - content = """ -[example.com] -foo=1 -user=test1 -password=test2""" - temp.write(content) - temp.flush() - bzapi.readconfig(temp.name) - self.assertEqual(bzapi.user, "test1") - self.assertEqual(bzapi.password, "test2") - self.assertEqual(bzapi.api_key, None) - - content = """ -[foo.example.com] -user=test3 -password=test4 -api_key=123abc -""" - temp.write(content) - temp.flush() - bzapi.readconfig(temp.name) - self.assertEqual(bzapi.user, "test3") - self.assertEqual(bzapi.password, "test4") - self.assertEqual(bzapi.api_key, "123abc") - - bzapi.url = "bugzilla.redhat.com" - bzapi.user = None - bzapi.password = None - bzapi.api_key = None - bzapi.readconfig(temp.name) - self.assertEqual(bzapi.user, None) - self.assertEqual(bzapi.password, None) - self.assertEqual(bzapi.api_key, None) - - - def testPostTranslation(self): - def _testPostCompare(bz, indict, outexpect): - outdict = indict.copy() - bz.post_translation({}, outdict) - self.assertTrue(outdict == outexpect) - - # Make sure multiple calls don't change anything - bz.post_translation({}, outdict) - self.assertTrue(outdict == outexpect) - - bug3 = tests.make_bz("3.4.0") - rhbz = tests.make_bz("4.4.0", rhbz=True) - - test1 = { - "component": ["comp1"], - "version": ["ver1", "ver2"], - - 'flags': [{ - 'is_active': 1, - 'name': 'qe_test_coverage', - 'setter': 'pm-rhel@redhat.com', - 'status': '?', - }, { - 'is_active': 1, - 'name': 'rhel-6.4.0', - 'setter': 'pm-rhel@redhat.com', - 'status': '+', - }], - - 'alias': ["FOO", "BAR"], - 'blocks': [782183, 840699, 923128], - 'keywords': ['Security'], - 'groups': ['redhat'], - } - - out_simple = test1.copy() - out_simple["components"] = out_simple["component"] - out_simple["component"] = out_simple["components"][0] - out_simple["versions"] = out_simple["version"] - out_simple["version"] = out_simple["versions"][0] - - _testPostCompare(bug3, test1, test1) - _testPostCompare(rhbz, test1, out_simple) diff --git a/tests/mockbackend.py b/tests/mockbackend.py new file mode 100644 index 00000000..3dcfc014 --- /dev/null +++ b/tests/mockbackend.py @@ -0,0 +1,152 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import inspect + +import bugzilla +from bugzilla._backendbase import _BackendBase + +import tests.utils + + +# pylint: disable=abstract-method,arguments-differ + + +class BackendMock(_BackendBase): + _version = None + + def bugzilla_version(self): + return {"version": self._version} + + def __helper(self, args): + # Grab the calling function name and use it to generate + # input and output variable names for the class. So if this + # is called from bug_get, we look for: + # self._bug_get_args + # self._bug_get_return + prevfuncname = inspect.stack()[1][3] + func_args = getattr(self, "_%s_args" % prevfuncname) + func_return = getattr(self, "_%s_return" % prevfuncname) + if isinstance(func_return, BaseException): + raise func_return + + filename = None + expect_out = func_args + if isinstance(func_args, str): + filename = func_args + expect_out = None + + # Hack to strip out attachment content from the generated + # test output, because it doesn't play well with the test + # suite running on python2 + if "content-disposition" in str(args): + largs = list(args) + largs[1] = "STRIPPED-BY-TESTSUITE" + args = tuple(largs) + + if filename or expect_out: + tests.utils.diff_compare(args, filename, expect_out) + + if isinstance(func_return, dict): + return func_return + + returnstr = open(tests.utils.tests_path(func_return)).read() + return eval(returnstr) # pylint: disable=eval-used + + def bug_attachment_create(self, *args): + return self.__helper(args) + def bug_attachment_get(self, *args): + return self.__helper(args) + def bug_attachment_get_all(self, *args): + return self.__helper(args) + def bug_attachment_update(self, *args): + return self.__helper(args) + + def bug_comments(self, *args): + return self.__helper(args) + def bug_create(self, *args): + return self.__helper(args) + def bug_history(self, *args): + return self.__helper(args) + def bug_get(self, *args): + return self.__helper(args) + def bug_fields(self, *args): + return self.__helper(args) + def bug_search(self, *args): + return self.__helper(args) + def bug_update(self, *args): + return self.__helper(args) + def bug_update_tags(self, *args): + return self.__helper(args) + + def component_create(self, *args): + return self.__helper(args) + def component_get(self, *args): + return self.__helper(args) + def component_update(self, *args): + return self.__helper(args) + + def group_get(self, *args): + return self.__helper(args) + + def externalbugs_add(self, *args): + return self.__helper(args) + def externalbugs_update(self, *args): + return self.__helper(args) + def externalbugs_remove(self, *args): + return self.__helper(args) + + def product_get(self, *args): + return self.__helper(args) + def product_get_accessible(self, *args): + return self.__helper(args) + def product_get_enterable(self, *args): + return self.__helper(args) + def product_get_selectable(self, *args): + return self.__helper(args) + + def user_create(self, *args): + return self.__helper(args) + def user_get(self, *args): + return self.__helper(args) + def user_login(self, *args): + return self.__helper(args) + def user_logout(self, *args): + return self.__helper(args) + def user_update(self, *args): + return self.__helper(args) + + +def _make_backend_class(version="6.0.0", **kwargs): + + class TmpBackendClass(BackendMock): + _version = version + + for key, val in kwargs.items(): + setattr(TmpBackendClass, "_%s" % key, val) + + return TmpBackendClass + + +def make_bz(bz_kwargs=None, rhbz=False, **kwargs): + bz_kwargs = (bz_kwargs or {}).copy() + if "url" in bz_kwargs: + raise RuntimeError("Can't set 'url' in mock make_bz, use connect()") + + if "use_creds" not in bz_kwargs: + bz_kwargs["use_creds"] = False + + bz = bugzilla.Bugzilla(url=None, **bz_kwargs) + backendclass = _make_backend_class(**kwargs) + + def _get_backend_class(url): + return backendclass, bugzilla.Bugzilla.fix_url(url) + + # pylint: disable=protected-access + bz._get_backend_class = _get_backend_class + + url = "https:///TESTSUITEMOCK" + if rhbz: + url += "?fakeredhat=bugzilla.redhat.com" + bz.connect(url) + return bz diff --git a/tests/modify.py b/tests/modify.py deleted file mode 100644 index 99527e14..00000000 --- a/tests/modify.py +++ /dev/null @@ -1,206 +0,0 @@ -# -# Copyright Red Hat, Inc. 2013 -# -# This work is licensed under the terms of the GNU GPL, version 2 or later. -# See the COPYING file in the top-level directory. -# - -''' -Unit tests for building update dictionaries with 'bugzilla modify' -''' - -import unittest - -import tests - - -rhbz = tests.make_bz("4.4.0", rhbz=True) - - -class ModifyTest(unittest.TestCase): - maxDiff = None - bz = rhbz - - def assertDictEqual(self, *args, **kwargs): - # pylint: disable=arguments-differ - # EPEL5 back compat - if hasattr(unittest.TestCase, "assertDictEqual"): - return unittest.TestCase.assertDictEqual(self, *args, **kwargs) - return self.assertEqual(*args, **kwargs) - - def clicomm(self, argstr, out, wbout=None, tags_add=None, tags_rm=None): - comm = "bugzilla modify --test-return-result 123456 224466 " + argstr - # pylint: disable=unpacking-non-sequence - - if out is None: - self.assertRaises(RuntimeError, tests.clicomm, comm, self.bz) - else: - (mdict, wdict, tagsa, tagsr) = tests.clicomm( - comm, self.bz, returnmain=True) - - if wbout: - self.assertDictEqual(wbout, wdict) - if out: - self.assertDictEqual(out, mdict) - if tags_add: - self.assertEqual(tags_add, tagsa) - if tags_rm: - self.assertEqual(tags_rm, tagsr) - - def testBasic(self): - self.clicomm( - "--component foocomp --product barprod --status ASSIGNED " - "--assignee foo@example.com --qa_contact bar@example.com " - "--comment 'hey some comment'", - {'assigned_to': 'foo@example.com', - 'comment': {'comment': 'hey some comment'}, - 'component': 'foocomp', - 'product': 'barprod', - 'qa_contact': 'bar@example.com', - 'status': 'ASSIGNED'} - ) - - def testPrivateComment(self): - self.clicomm( - "--comment 'hey private comment' --private", - {'comment': {'comment': 'hey private comment', 'is_private': True}} - ) - - def testClose(self): - self.clicomm( - "--close CANTFIX", - {'resolution': 'CANTFIX', 'status': 'CLOSED'} - ) - self.clicomm( - "--dupeid 111333", - {'dupe_of': 111333, 'resolution': 'DUPLICATE', 'status': 'CLOSED'} - ) - - def testFlags(self): - self.clicomm( - "--flag needinfoX --flag dev_ack+ --flag qa_ack-", - {"flags": [ - {'status': 'X', 'name': 'needinfo'}, - {'status': '+', 'name': 'dev_ack'}, - {'status': '-', 'name': 'qa_ack'} - ]} - ) - - def testWhiteboard(self): - self.clicomm( - "--whiteboard tagfoo --whiteboard=-tagbar", - {}, wbout={"whiteboard": (["tagfoo"], ["tagbar"])} - ) - self.clicomm( - "--whiteboard =foo --whiteboard =thisone", - {'whiteboard': 'thisone'} - ) - - self.clicomm( - "--qa_whiteboard =yo-qa --qa_whiteboard=-foo " - "--internal_whiteboard =internal-hey --internal_whiteboard +bar " - "--devel_whiteboard =devel-duh --devel_whiteboard=-yay " - "--tags foo1 --tags=-remove2", - {'cf_devel_whiteboard': 'devel-duh', - 'cf_internal_whiteboard': 'internal-hey', - 'cf_qa_whiteboard': 'yo-qa'}, wbout={ - "qa_whiteboard": ([], ["foo"]), - "internal_whiteboard": (["bar"], []), - "devel_whiteboard": ([], ["yay"]) - }, tags_add=["foo1"], tags_rm=["remove2"], - ) - - def testMisc(self): - self.clicomm( - "--fixed_in foo-bar-1.2.3 --reset-qa-contact --reset-assignee", - {"cf_fixed_in": "foo-bar-1.2.3", - 'reset_assigned_to': True, - 'reset_qa_contact': True} - ) - self.clicomm( - "--groups +foo --groups=-bar,baz --groups fribby", - {'groups': {'add': ['foo', 'fribby'], 'remove': ['bar', 'baz']}} - ) - self.clicomm( - "--target_milestone foomile --target_release relfoo", - {"target_milestone": "foomile", "target_release": "relfoo"}, - ) - self.clicomm( - "--priority medium --severity high", - {"priority": "medium", "severity": "high"}, - ) - self.clicomm( - "--os Windows --arch ia64 --version 1000 --url http://example.com " - "--summary 'foo summary'", - {"op_sys": "Windows", "platform": "ia64", "version": "1000", - "url": "http://example.com", "summary": 'foo summary'}, - ) - self.clicomm( - "--alias some-alias", - {"alias": "some-alias"} - ) - - - def testField(self): - self.clicomm( - "--field cf_fixed_in=foo-bar-1.2.4", - {"cf_fixed_in": "foo-bar-1.2.4"} - ) - - self.clicomm( - "--field cf_fixed_in=foo-bar-1.2.5 --field=cf_release_notes=blah", - {"cf_fixed_in": "foo-bar-1.2.5", - "cf_release_notes": "blah"} - ) - - - def testDepends(self): - self.clicomm( - "--dependson 100,200", - {'depends_on': {'add': [100, 200]}} - ) - self.clicomm( - "--dependson +100,200", - {'depends_on': {'add': [100, 200]}} - ) - self.clicomm( - "--dependson=-100,200", - {'depends_on': {'remove': [100, 200]}} - ) - self.clicomm( - "--dependson =100,200", - {'depends_on': {'set': [100, 200]}} - ) - - self.clicomm( - "--dependson 1 --dependson=-2 --dependson +3 --dependson =4", - {'depends_on': {'add': [1, 3], 'remove': [2], 'set': [4]}} - ) - self.clicomm( - "--blocked 5 --blocked -6 --blocked +7 --blocked =8,9", - {'blocks': {'add': [5, 7], 'remove': [6], 'set': [8, 9]}} - ) - self.clicomm( - "--keywords foo --keywords=-bar --keywords +baz --keywords =yay", - {'keywords': {'add': ["foo", "baz"], - 'remove': ["bar"], 'set': ["yay"]}} - ) - self.clicomm("--keywords =", {'keywords': {'set': []}}) - - - def testCC(self): - self.clicomm( - "--cc foo@example.com --cc=-minus@example.com " - "--cc =foo@example.com --cc +foo@example.com", - {'cc': {'add': ['foo@example.com', "=foo@example.com", - "+foo@example.com"], - 'remove': ["minus@example.com"]}}, - ) - - def testSubComponents(self): - self.clicomm("--component foo --sub-component 'bar baz'", - {"component": "foo", "sub_components": {"foo": ["bar baz"]}}) - - def testSubComponentFail(self): - self.assertRaises(ValueError, self.bz.build_update, - sub_component="some sub component") diff --git a/tests/pep8.cfg b/tests/pep8.cfg deleted file mode 100644 index e6179eac..00000000 --- a/tests/pep8.cfg +++ /dev/null @@ -1,12 +0,0 @@ -[pep8] - -format = pylint - -# [E125] Continuation indent isn't different from next block -# [E128] Not indented for visual style -# [E129] visually indented line with same indent as next logical line -# [E303] Too many blank lines -# [E402] module level import not at top of file -# [E731] do not assign a lambda expression, use a def - -ignore=E125,E128,E129,E303,E402,E731 diff --git a/tests/pycodestyle.cfg b/tests/pycodestyle.cfg new file mode 100644 index 00000000..e69de29b diff --git a/tests/query.py b/tests/query.py deleted file mode 100644 index 32b3702c..00000000 --- a/tests/query.py +++ /dev/null @@ -1,321 +0,0 @@ -# -# Copyright Red Hat, Inc. 2012 -# -# This work is licensed under the terms of the GNU GPL, version 2 or later. -# See the COPYING file in the top-level directory. -# - -''' -Unit tests for building query strings with bin/bugzilla -''' - -import copy -import os -import unittest - -import tests - -bz34 = tests.make_bz("3.4.0") -bz4 = tests.make_bz("4.0.0") -rhbz4 = tests.make_bz("4.4.0", rhbz=True) - - -class BZ34Test(unittest.TestCase): - """ - This is the base query class, but it's also functional on its - own. - """ - maxDiff = None - - def assertDictEqual(self, *args, **kwargs): - # pylint: disable=arguments-differ - # EPEL5 back compat - if hasattr(unittest.TestCase, "assertDictEqual"): - return unittest.TestCase.assertDictEqual(self, *args, **kwargs) - return self.assertEqual(*args, **kwargs) - - def clicomm(self, argstr, out): - comm = "bugzilla query --test-return-result " + argstr - - if out is None: - self.assertRaises(RuntimeError, tests.clicomm, comm, self.bz) - else: - q = tests.clicomm(comm, self.bz, returnmain=True) - self.assertDictEqual(out, q) - - def testBasicQuery(self): - self.clicomm("--product foo --component foo,bar --bug_id 1234,2480", - self._basic_query_out) - - def testOneline(self): - self.clicomm("--product foo --oneline", self._oneline_out) - - def testOutputFormat(self): - self.clicomm("--product foo --outputformat " - "%{bug_id}:%{blockedby}:%{bug_status}:%{short_desc}:" - "%{status_whiteboard}:%{product}:%{rep_platform}", - self._output_format_out) - - def testBugStatusALL(self): - self.clicomm("--product foo --bug_status ALL", self._status_all_out) - - def testBugStatusDEV(self): - self.clicomm("--bug_status DEV", self._status_dev_out) - - def testBugStatusQE(self): - self.clicomm("--bug_status QE", self._status_qe_out) - - def testBugStatusEOL(self): - self.clicomm("--bug_status EOL", self._status_eol_out) - - def testBugStatusOPEN(self): - self.clicomm("--bug_status OPEN", self._status_open_out) - - def testBugStatusRegular(self): - self.clicomm("--bug_status POST", self._status_post_out) - - def testEmailOptions(self): - cmd = ("--cc foo1@example.com " - "--assigned_to foo2@example.com " - "--reporter foo3@example.com " - "--qa_contact foo7@example.com") - self.clicomm(cmd, self._email_out) - self.clicomm(cmd + " --emailtype notsubstring", self._email_type_out) - - def testComponentsFile(self): - self.clicomm("--components_file " + - os.getcwd() + "/tests/data/components_file.txt", - self._components_file_out) - - def testKeywords(self): - self.clicomm("--keywords Triaged " - "--url http://example.com --url_type foo", - self._keywords_out) - - def testBooleanChart(self): - self.clicomm("--boolean_query 'keywords-substring-Partner & " - "keywords-notsubstring-OtherQA' " - "--boolean_query 'foo-bar-baz | foo-bar-wee' " - "--boolean_query '! foo-bar-yargh'", None) - - def testLongDesc(self): - self.clicomm("--long_desc 'foobar'", self._longdesc_out) - - def testQuicksearch(self): - self.clicomm("--quicksearch 'foo bar baz'", self._quicksearch_out) - - def testSavedsearch(self): - self.clicomm("--savedsearch 'my saved search' " - "--savedsearch-sharer-id 123456", self._savedsearch_out) - - def testSubComponent(self): - self.clicomm("--component lvm2,kernel " - "--sub-component 'Command-line tools (RHEL5)'", - self._sub_component_out) - - # Test data. This is what subclasses need to fill in - bz = bz34 - - _basic_query_out = {'product': ['foo'], 'component': ['foo', 'bar'], - 'id': ["1234", "2480"]} - _oneline_out = {'product': ['foo']} - _output_format_out = {'product': ['foo']} - output_format_out = _output_format_out - - _status_all_out = {'product': ['foo']} - _status_dev_out = {'bug_status': ['NEW', 'ASSIGNED', 'NEEDINFO', - 'ON_DEV', 'MODIFIED', 'POST', 'REOPENED']} - _status_qe_out = {'bug_status': ['ASSIGNED', 'ON_QA', - 'FAILS_QA', 'PASSES_QA']} - _status_eol_out = {'bug_status': ['VERIFIED', 'RELEASE_PENDING', - 'CLOSED']} - _status_open_out = {'bug_status': ['NEW', 'ASSIGNED', 'MODIFIED', - 'ON_DEV', 'ON_QA', 'VERIFIED', 'RELEASE_PENDING', 'POST']} - _status_post_out = {'bug_status': ['POST']} - _email_out = {'assigned_to': 'foo2@example.com', - 'cc': ["foo1@example.com"], - 'reporter': "foo3@example.com", "qa_contact": "foo7@example.com"} - _email_type_out = { - 'email1': ['foo1@example.com'], 'email2': "foo2@example.com", - 'email3': 'foo3@example.com', 'email4': 'foo7@example.com', - 'emailtype1': 'notsubstring', 'emailtype2': 'notsubstring', - 'emailtype3': 'notsubstring', 'emailtype4': 'notsubstring', - 'emailcc1': True, 'emailassigned_to2': True, - 'emailreporter3': True, 'emailqa_contact4': True, - 'query_format': 'advanced'} - _components_file_out = {'component': ["foo", "bar", "baz"]} - _keywords_out = {'query_format': 'advanced', - 'field0-0-0': 'keywords', 'value0-0-0': 'Triaged', - 'field1-0-0': 'bug_file_loc', 'value1-0-0': 'http://example.com', - 'type0-0-0': 'substring', 'type1-0-0': 'foo'} - _longdesc_out = {'longdesc': 'foobar', 'longdesc_type': 'allwordssubstr', - 'query_format': 'advanced'} - _quicksearch_out = {'quicksearch': 'foo bar baz'} - _savedsearch_out = {'savedsearch': "my saved search", - 'sharer_id': "123456"} - _sub_component_out = {'component': ["lvm2", "kernel"], - 'sub_components': ["Command-line tools (RHEL5)"]} - - -class BZ4Test(BZ34Test): - bz = bz4 - - _default_includes = ['assigned_to', 'id', 'status', 'summary'] - - _basic_query_out = BZ34Test._basic_query_out.copy() - _basic_query_out["include_fields"] = _default_includes - - _oneline_out = BZ34Test._oneline_out.copy() - _oneline_out["include_fields"] = ['assigned_to', 'blocks', 'component', - 'flags', 'keywords', 'status', 'target_milestone', 'id'] - - _output_format_out = BZ34Test._output_format_out.copy() - _output_format_out["include_fields"] = ['product', 'summary', - 'platform', 'status', 'id', 'blocks', 'whiteboard'] - - _status_all_out = BZ34Test._status_all_out.copy() - _status_all_out["include_fields"] = _default_includes - - _status_dev_out = BZ34Test._status_dev_out.copy() - _status_dev_out["include_fields"] = _default_includes - - _status_qe_out = BZ34Test._status_qe_out.copy() - _status_qe_out["include_fields"] = _default_includes - - _status_eol_out = BZ34Test._status_eol_out.copy() - _status_eol_out["include_fields"] = _default_includes - - _status_open_out = BZ34Test._status_open_out.copy() - _status_open_out["include_fields"] = _default_includes - - _status_post_out = BZ34Test._status_post_out.copy() - _status_post_out["include_fields"] = _default_includes - - _email_out = BZ34Test._email_out.copy() - _email_out["include_fields"] = _default_includes - - _email_type_out = BZ34Test._email_type_out.copy() - _email_type_out["include_fields"] = _default_includes - - _components_file_out = BZ34Test._components_file_out.copy() - _components_file_out["include_fields"] = _default_includes - - _keywords_out = BZ34Test._keywords_out.copy() - _keywords_out["include_fields"] = _default_includes - - _longdesc_out = BZ34Test._longdesc_out.copy() - _longdesc_out["include_fields"] = _default_includes - - _quicksearch_out = BZ34Test._quicksearch_out.copy() - _quicksearch_out["include_fields"] = _default_includes - _savedsearch_out = BZ34Test._savedsearch_out.copy() - _savedsearch_out["include_fields"] = _default_includes - _sub_component_out = BZ34Test._sub_component_out.copy() - _sub_component_out["include_fields"] = _default_includes - - -class RHBZTest(BZ4Test): - bz = rhbz4 - - _output_format_out = BZ34Test.output_format_out.copy() - _output_format_out["include_fields"] = ['product', 'summary', - 'platform', 'status', 'id', 'blocks', 'whiteboard'] - _booleans_out = {} - - def testTranslation(self): - def translate(_in): - _out = copy.deepcopy(_in) - self.bz.pre_translation(_out) - return _out - - in_query = { - "fixed_in": "foo.bar", - "product": "some-product", - "cf_devel_whiteboard": "some_devel_whiteboard", - "include_fields": ["fixed_in", - "components", "cf_devel_whiteboard"], - } - out_query = translate(in_query) - - in_query["include_fields"] = [ - "cf_devel_whiteboard", "cf_fixed_in", "component", "id"] - self.assertDictEqual(in_query, out_query) - - in_query = {"bug_id": "123,456", "component": "foo,bar"} - out_query = translate(in_query) - self.assertEqual(out_query["id"], ["123", "456"]) - self.assertEqual(out_query["component"], ["foo", "bar"]) - - in_query = {"bug_id": [123, 124], "column_list": ["id"]} - out_query = translate(in_query) - self.assertEqual(out_query["id"], [123, 124]) - self.assertEqual(out_query["include_fields"], in_query["column_list"]) - - def testInvalidBoolean(self): - self.assertRaises(RuntimeError, self.bz.build_query, - boolean_query="foobar") - - def testBooleans(self): - out = { - 'query_format': 'advanced', - 'type0-0-0': 'substring', - 'type1-0-0': 'substring', - 'type2-0-0': 'substring', - 'type3-0-0': 'substring', - 'value0-0-0': '123456', - 'value1-0-0': 'needinfo & devel_ack', - 'value2-0-0': '! baz foo', - 'value3-0-0': 'foobar | baz', - 'field0-0-0': 'blocked', - 'field1-0-0': 'flagtypes.name', - 'field2-0-0': 'cf_qa_whiteboard', - 'field3-0-0': 'cf_devel_whiteboard', - 'include_fields': ['assigned_to', 'id', 'status', 'summary'], - } - - import bugzilla - import logging - log = logging.getLogger(bugzilla.__name__) - handlers = log.handlers - try: - log.handlers = [] - self.clicomm("--blocked 123456 " - "--devel_whiteboard 'foobar | baz' " - "--qa_whiteboard '! baz foo' " - "--flag 'needinfo & devel_ack'", out) - finally: - log.handlers = handlers - - -class TestURLToQuery(BZ34Test): - def _check(self, url, query): - self.assertDictEqual(bz4.url_to_query(url), query) - - def testSavedSearch(self): - url = ("https://bugzilla.redhat.com/buglist.cgi?" - "cmdtype=dorem&list_id=2342312&namedcmd=" - "RHEL7%20new%20assigned%20virt-maint&remaction=run&" - "sharer_id=321167") - query = { - 'sharer_id': '321167', - 'savedsearch': 'RHEL7 new assigned virt-maint' - } - self._check(url, query) - - def testStandardQuery(self): - url = ("https://bugzilla.redhat.com/buglist.cgi?" - "component=virt-manager&query_format=advanced&classification=" - "Fedora&product=Fedora&bug_status=NEW&bug_status=ASSIGNED&" - "bug_status=MODIFIED&bug_status=ON_DEV&bug_status=ON_QA&" - "bug_status=VERIFIED&bug_status=FAILS_QA&bug_status=" - "RELEASE_PENDING&bug_status=POST&order=bug_status%2Cbug_id") - query = { - 'product': 'Fedora', - 'query_format': 'advanced', - 'bug_status': ['NEW', 'ASSIGNED', 'MODIFIED', 'ON_DEV', - 'ON_QA', 'VERIFIED', 'FAILS_QA', 'RELEASE_PENDING', 'POST'], - 'classification': 'Fedora', - 'component': 'virt-manager', - 'order': 'bug_status,bug_id' - } - self._check(url, query) diff --git a/tests/ro_functional.py b/tests/ro_functional.py deleted file mode 100644 index 6aa883f4..00000000 --- a/tests/ro_functional.py +++ /dev/null @@ -1,335 +0,0 @@ -# -*- encoding: utf-8 -*- - -# -# Copyright Red Hat, Inc. 2012 -# -# This work is licensed under the terms of the GNU GPL, version 2 or later. -# See the COPYING file in the top-level directory. -# - -''' -Unit tests that do readonly functional tests against real bugzilla instances. -''' - -import sys -import unittest - -from bugzilla import Bugzilla, BugzillaError, RHBugzilla - -import tests - - -class BaseTest(unittest.TestCase): - url = None - bzclass = Bugzilla - bzversion = (0, 0) - closestatus = "CLOSED" - - def clicomm(self, argstr, expectexc=False, bz=None): - comm = "bugzilla " + argstr - - if not bz: - bz = Bugzilla(url=self.url, use_creds=False) - if expectexc: - self.assertRaises(Exception, tests.clicomm, comm, bz) - else: - return tests.clicomm(comm, bz) - - def _testBZVersion(self): - bz = Bugzilla(self.url, use_creds=False) - self.assertEqual(bz.__class__, self.bzclass) - if tests.REDHAT_URL: - print("BZ version=%s.%s" % (bz.bz_ver_major, bz.bz_ver_minor)) - else: - self.assertEqual(bz.bz_ver_major, self.bzversion[0]) - self.assertEqual(bz.bz_ver_minor, self.bzversion[1]) - - # Since we are running these tests against bugzilla instances in - # the wild, we can't depend on certain data like product lists - # remaining static. Use lax sanity checks in this case - - def _testInfoProducts(self, mincount, expectstr): - out = self.clicomm("info --products").splitlines() - self.assertTrue(len(out) >= mincount) - self.assertTrue(expectstr in out) - - def _testInfoComps(self, comp, mincount, expectstr): - out = self.clicomm("info --components \"%s\"" % comp).splitlines() - self.assertTrue(len(out) >= mincount) - self.assertTrue(expectstr in out) - - def _testInfoVers(self, comp, mincount, expectstr): - out = self.clicomm("info --versions \"%s\"" % comp).splitlines() - self.assertTrue(len(out) >= mincount) - if expectstr: - self.assertTrue(expectstr in out) - - def _testInfoCompOwners(self, comp, expectstr): - expectexc = (expectstr == "FAIL") - out = self.clicomm("info --component_owners \"%s\"" % - comp, expectexc=expectexc) - if expectexc: - return - - self.assertTrue(expectstr in out.splitlines()) - - def _testQuery(self, args, mincount, expectbug): - expectexc = (expectbug == "FAIL") - cli = "query %s --bug_status %s" % (args, self.closestatus) - out = self.clicomm(cli, expectexc=expectexc) - if expectexc: - return - - self.assertTrue(len(out.splitlines()) >= mincount) - self.assertTrue(bool([l for l in out.splitlines() if - l.startswith("#" + expectbug)])) - - # Check --ids output option - out2 = self.clicomm(cli + " --ids") - self.assertTrue(len(out.splitlines()) == len(out2.splitlines())) - self.assertTrue(bool([l for l in out2.splitlines() if l == expectbug])) - - - def _testQueryFull(self, bugid, mincount, expectstr): - out = self.clicomm("query --full --bug_id %s" % bugid) - self.assertTrue(len(out.splitlines()) >= mincount) - self.assertTrue(expectstr in out) - - def _testQueryRaw(self, bugid, mincount, expectstr): - out = self.clicomm("query --raw --bug_id %s" % bugid) - self.assertTrue(len(out.splitlines()) >= mincount) - self.assertTrue(expectstr in out) - - def _testQueryOneline(self, bugid, expectstr): - out = self.clicomm("query --oneline --bug_id %s" % bugid) - self.assertTrue(len(out.splitlines()) == 3) - self.assertTrue(out.splitlines()[2].startswith("#%s" % bugid)) - self.assertTrue(expectstr in out) - - def _testQueryExtra(self, bugid, expectstr): - out = self.clicomm("query --extra --bug_id %s" % bugid) - self.assertTrue(("#%s" % bugid) in out) - self.assertTrue(expectstr in out) - - def _testQueryFormat(self, args, expectstr): - out = self.clicomm("query %s" % args) - self.assertTrue(expectstr in out) - - def _testQueryURL(self, querystr, count, expectstr): - url = self.url - if "/xmlrpc.cgi" in self.url: - url = url.replace("/xmlrpc.cgi", querystr) - else: - url += querystr - out = self.clicomm("query --from-url \"%s\"" % url) - self.assertEqual(len(out.splitlines()), count) - self.assertTrue(expectstr in out) - - -class BZMozilla(BaseTest): - def testVersion(self): - # bugzilla.mozilla.org returns version values in YYYY-MM-DD - # format, so just try to confirm that - bz = Bugzilla("bugzilla.mozilla.org", use_creds=False) - self.assertEqual(bz.__class__, Bugzilla) - self.assertTrue(bz.bz_ver_major >= 2016) - self.assertTrue(bz.bz_ver_minor in range(1, 13)) - - -class BZGentoo(BaseTest): - url = "bugs.gentoo.org" - bzversion = (5, 0) - test0 = BaseTest._testBZVersion - - def testURLQuery(self): - # This is a bugzilla 5.0 instance, which supports URL queries now - query_url = ("https://bugs.gentoo.org/buglist.cgi?" - "component=[CS]&product=Doc%20Translations" - "&query_format=advanced&resolution=FIXED") - bz = Bugzilla(url=self.url, use_creds=False) - ret = bz.query(bz.url_to_query(query_url)) - self.assertTrue(len(ret) > 0) - - -class BZGnome(BaseTest): - url = "https://bugzilla.gnome.org/xmlrpc.cgi" - bzversion = (4, 4) - closestatus = "RESOLVED" - - test0 = BaseTest._testBZVersion - test1 = lambda s: BaseTest._testQuery(s, - "--product dogtail --component sniff", - 9, "321654") - # BZ < 4 doesn't report values for --full - test2 = lambda s: BaseTest._testQueryRaw(s, "321654", 30, - "ATTRIBUTE[version]: CVS HEAD") - test3 = lambda s: BaseTest._testQueryOneline(s, "321654", "Sniff") - - def testURLQuery(self): - # This instance is old and doesn't support URL queries, we are - # just verifying our extra error message report - query_url = ("https://bugzilla.gnome.org/buglist.cgi?" - "bug_status=RESOLVED&order=Importance&product=accerciser" - "&query_format=advanced&resolution=NOTABUG") - bz = Bugzilla(url=self.url, use_creds=False) - try: - bz.query(bz.url_to_query(query_url)) - except BugzillaError: - e = sys.exc_info()[1] - self.assertTrue("derived from bugzilla" in str(e)) - - -class BZFDO(BaseTest): - url = "https://bugs.freedesktop.org/xmlrpc.cgi" - bzversion = (5, 0) - closestatus = "CLOSED,RESOLVED" - - test0 = BaseTest._testBZVersion - - test1 = lambda s: BaseTest._testQuery(s, "--product avahi", 10, "3450") - test2 = lambda s: BaseTest._testQueryFull(s, "3450", 10, "Blocked: \n") - test2 = lambda s: BaseTest._testQueryRaw(s, "3450", 30, - "ATTRIBUTE[creator]: daniel@fooishbar.org") - test3 = lambda s: BaseTest._testQueryOneline(s, "3450", - "daniel@fooishbar.org libavahi") - test4 = lambda s: BaseTest._testQueryExtra(s, "3450", "Error") - test5 = lambda s: BaseTest._testQueryFormat(s, - "--bug_id 3450 --outputformat " - "\"%{bug_id} %{assigned_to} %{summary}\"", - "3450 daniel@fooishbar.org Error") - - -class RHTest(BaseTest): - url = tests.REDHAT_URL or "https://bugzilla.redhat.com/xmlrpc.cgi" - bzclass = RHBugzilla - bzversion = (4, 4) - - test0 = BaseTest._testBZVersion - test01 = lambda s: BaseTest._testInfoProducts(s, 125, - "Virtualization Tools") - test02 = lambda s: BaseTest._testInfoComps(s, "Virtualization Tools", - 10, "virt-manager") - test03 = lambda s: BaseTest._testInfoVers(s, "Fedora", 19, "rawhide") - test04 = lambda s: BaseTest._testInfoCompOwners(s, "Virtualization Tools", - "libvirt: Libvirt Maintainers") - - test05 = lambda s: BaseTest._testQuery(s, - "--product Fedora --component python-bugzilla --version 14", - 6, "621030") - test06 = lambda s: BaseTest._testQueryFull(s, "621601", 60, - "end-of-life (EOL)") - test07 = lambda s: BaseTest._testQueryRaw(s, "307471", 70, - "ATTRIBUTE[whiteboard]: bzcl34nup") - test08 = lambda s: BaseTest._testQueryOneline(s, "785016", - "[---] fedora-review+,fedora-cvs+") - test09 = lambda s: BaseTest._testQueryExtra(s, "307471", - " +Status Whiteboard: bzcl34nup") - test10 = lambda s: BaseTest._testQueryFormat(s, - "--bug_id 307471 --outputformat=\"id=%{bug_id} " - "sw=%{whiteboard:status} needinfo=%{flag:needinfo} " - "sum=%{summary}\"", - "id=307471 sw= bzcl34nup needinfo= ") - test11 = lambda s: BaseTest._testQueryURL(s, - "/buglist.cgi?f1=creation_ts" - "&list_id=973582&o1=greaterthaneq&classification=Fedora&" - "o2=lessthaneq&query_format=advanced&f2=creation_ts" - "&v1=2010-01-01&component=python-bugzilla&v2=2011-01-01" - "&product=Fedora", 26, "#553878 CLOSED") - test12 = lambda s: BaseTest._testQueryFormat(s, - "--bug_id 785016 --outputformat=\"id=%{bug_id} " - "sw=%{whiteboard:status} flag=%{flag:fedora-review} " - "sum=%{summary}\"", - "id=785016 sw= flag=+") - # Unicode in this bug's summary - test13 = lambda s: BaseTest._testQueryFormat(s, - "--bug_id 522796 --outputformat \"%{summary}\"", - "V34 — system") - # CVE bug output - test14 = lambda s: BaseTest._testQueryOneline(s, "720784", - " CVE-2011-2527") - - def testDoubleConnect(self): - bz = self.bzclass(url=self.url) - bz.connect(self.url) - - def testQueryFlags(self): - bz = self.bzclass(url=self.url) - if not bz.logged_in: - print("not logged in, skipping testQueryFlags") - return - - out = self.clicomm("query --product 'Red Hat Enterprise Linux 5' " - "--component virt-manager --bug_status CLOSED " - "--flag rhel-5.4.0+", bz=bz) - self.assertTrue(len(out.splitlines()) > 15) - self.assertTrue(len(out.splitlines()) < 28) - self.assertTrue("223805" in out) - - def testQueryFixedIn(self): - out = self.clicomm("query --fixed_in anaconda-15.29-1") - self.assertEqual(len(out.splitlines()), 6) - self.assertTrue("#629311 CLOSED" in out) - - def testComponentsDetails(self): - """ - Fresh call to getcomponentsdetails should properly refresh - """ - bz = self.bzclass(url=self.url, use_creds=False) - self.assertTrue( - bool(bz.getcomponentsdetails("Red Hat Developer Toolset"))) - - def testGetBugAlias(self): - """ - getbug() works if passed an alias - """ - bz = self.bzclass(url=self.url, use_creds=False) - bug = bz.getbug("CVE-2011-2527") - self.assertTrue(bug.bug_id == 720773) - - def testQuerySubComponent(self): - out = self.clicomm("query --product 'Red Hat Enterprise Linux 7' " - "--component lvm2 --sub-component 'Thin Provisioning'") - self.assertTrue(len(out.splitlines()) >= 5) - self.assertTrue("#1060931 " in out) - - def testBugFields(self): - bz = self.bzclass(url=self.url, use_creds=False) - fields1 = bz.getbugfields()[:] - fields2 = bz.getbugfields(force_refresh=True)[:] - self.assertTrue(bool([f for f in fields1 if - f.startswith("attachments")])) - self.assertEqual(fields1, fields2) - - def testBugAutoRefresh(self): - bz = self.bzclass(self.url, use_creds=False) - - bz.bug_autorefresh = True - - bug = bz.query(bz.build_query(bug_id=720773, - include_fields=["summary"]))[0] - self.assertTrue(hasattr(bug, "component")) - self.assertTrue(bool(bug.component)) - - bz.bug_autorefresh = False - - bug = bz.query(bz.build_query(bug_id=720773, - include_fields=["summary"]))[0] - self.assertFalse(hasattr(bug, "component")) - try: - self.assertFalse(bool(bug.component)) - except: - e = sys.exc_info()[1] - self.assertTrue("adjust your include_fields" in str(e)) - - def testExtraFields(self): - bz = self.bzclass(self.url, cookiefile=None, tokenfile=None) - - # Check default extra_fields will pull in comments - bug = bz.getbug(720773, exclude_fields=["product"]) - self.assertTrue("comments" in dir(bug)) - self.assertTrue("product" not in dir(bug)) - - # Ensure that include_fields overrides default extra_fields - bug = bz.getbug(720773, include_fields=["summary"]) - self.assertTrue("summary" in dir(bug)) - self.assertTrue("comments" not in dir(bug)) diff --git a/tests/rw_functional.py b/tests/rw_functional.py deleted file mode 100644 index 050ab9d4..00000000 --- a/tests/rw_functional.py +++ /dev/null @@ -1,961 +0,0 @@ -# -# Copyright Red Hat, Inc. 2012 -# -# This work is licensed under the terms of the GNU GPL, version 2 or later. -# See the COPYING file in the top-level directory. -# - -''' -Unit tests that do permanent functional against a real bugzilla instances. -''' - -from __future__ import print_function - -import datetime -import os -import random -import sys -import unittest - -if sys.version_info[0] >= 3: - # pylint: disable=F0401,E0611 - from urllib.parse import urlparse -else: - from urlparse import urlparse - -import bugzilla -from bugzilla import Bugzilla -from bugzilla.transport import _BugzillaTokenCache - -import tests - -cf = os.path.expanduser("~/.bugzillacookies") -tf = os.path.expanduser("~/.bugzillatoken") - - -def _split_int(s): - return [int(i) for i in s.split(",")] - - -class BaseTest(unittest.TestCase): - url = None - bzclass = None - - def _testBZClass(self): - bz = Bugzilla(url=self.url, use_creds=False) - self.assertTrue(bz.__class__ is self.bzclass) - - def _testCookieOrToken(self): - domain = urlparse(self.url)[1] - if os.path.exists(cf): - out = open(cf).read(1024) - if domain in out: - return - - if os.path.exists(tf): - token = _BugzillaTokenCache(self.url, tokenfilename=tf) - if token.value is not None: - return - - raise RuntimeError("%s or %s must exist and contain domain '%s'" % - (cf, tf, domain)) - - -class RHPartnerTest(BaseTest): - # Despite its name, this instance is simply for bugzilla testing, - # doesn't send out emails and is blown away occasionally. The front - # page has some info. - url = tests.REDHAT_URL or "partner-bugzilla.redhat.com" - bzclass = bugzilla.RHBugzilla - - - def _check_have_admin(self, bz, funcname): - # groupnames is empty for any user if our logged in user does not - # have admin privs. - # Check a known account that likely won't ever go away - ret = bool(bz.getuser("anaconda-maint-list@redhat.com").groupnames) - if not ret: - print("\nNo admin privs, reduced testing of %s" % funcname) - return ret - - test2 = BaseTest._testBZClass - - - def test00LoginState(self): - bz = self.bzclass(url=self.url) - self.assertTrue(bz.logged_in, - "R/W tests require cached login credentials for url=%s" % self.url) - - bz = self.bzclass(url=self.url, use_creds=False) - self.assertFalse(bz.logged_in, - "Login state check failed for logged out user.") - - - def test03NewBugBasic(self): - """ - Create a bug with minimal amount of fields, then close it - """ - bz = self.bzclass(url=self.url) - component = "python-bugzilla" - version = "rawhide" - summary = ("python-bugzilla test basic bug %s" % - datetime.datetime.today()) - newout = tests.clicomm("bugzilla new " - "--product Fedora --component %s --version %s " - "--summary \"%s\" " - "--comment \"Test bug from the python-bugzilla test suite\" " - "--outputformat \"%%{bug_id}\"" % - (component, version, summary), bz) - - self.assertTrue(len(newout.splitlines()) == 3) - - bugid = int(newout.splitlines()[2]) - bug = bz.getbug(bugid) - print("\nCreated bugid: %s" % bugid) - - # Verify hasattr works - self.assertTrue(hasattr(bug, "id")) - self.assertTrue(hasattr(bug, "bug_id")) - - self.assertEqual(bug.component, component) - self.assertEqual(bug.version, version) - self.assertEqual(bug.summary, summary) - - # Close the bug - tests.clicomm("bugzilla modify --close NOTABUG %s" % bugid, - bz) - bug.refresh() - self.assertEqual(bug.status, "CLOSED") - self.assertEqual(bug.resolution, "NOTABUG") - - - def test04NewBugAllFields(self): - """ - Create a bug using all 'new' fields, check some values, close it - """ - bz = self.bzclass(url=self.url) - - summary = ("python-bugzilla test manyfields bug %s" % - datetime.datetime.today()) - url = "http://example.com" - osval = "Windows" - cc = "triage@lists.fedoraproject.org" - blocked = "461686,461687" - dependson = "427301" - comment = "Test bug from python-bugzilla test suite" - sub_component = "Command-line tools (RHEL6)" - alias = "pybz-%s" % datetime.datetime.today().strftime("%s") - newout = tests.clicomm("bugzilla new " - "--product 'Red Hat Enterprise Linux 6' --version 6.0 " - "--component lvm2 --sub-component '%s' " - "--summary \"%s\" " - "--comment \"%s\" " - "--url %s --severity Urgent --priority Low --os %s " - "--arch ppc --cc %s --blocked %s --dependson %s " - "--alias %s " - "--outputformat \"%%{bug_id}\"" % - (sub_component, summary, comment, url, - osval, cc, blocked, dependson, alias), bz) - - self.assertTrue(len(newout.splitlines()) == 3) - - bugid = int(newout.splitlines()[2]) - bug = bz.getbug(bugid, extra_fields=["sub_components"]) - print("\nCreated bugid: %s" % bugid) - - self.assertEqual(bug.summary, summary) - self.assertEqual(bug.bug_file_loc, url) - self.assertEqual(bug.op_sys, osval) - self.assertEqual(bug.blocks, _split_int(blocked)) - self.assertEqual(bug.depends_on, _split_int(dependson)) - self.assertTrue(all([e in bug.cc for e in cc.split(",")])) - self.assertEqual(bug.longdescs[0]["text"], comment) - self.assertEqual(bug.sub_components, {"lvm2": [sub_component]}) - self.assertEqual(bug.alias, [alias]) - - # Close the bug - - # RHBZ makes it difficult to provide consistent semantics for - # 'alias' update: - # https://bugzilla.redhat.com/show_bug.cgi?id=1173114 - # alias += "-closed" - tests.clicomm("bugzilla modify " - "--close WONTFIX %s " % - bugid, bz) - bug.refresh() - self.assertEqual(bug.status, "CLOSED") - self.assertEqual(bug.resolution, "WONTFIX") - self.assertEqual(bug.alias, [alias]) - - # Check bug's minimal history - ret = bug.get_history_raw() - self.assertTrue(len(ret["bugs"]) == 1) - self.assertTrue(len(ret["bugs"][0]["history"]) == 1) - - - def test05ModifyStatus(self): - """ - Modify status and comment fields for an existing bug - """ - bz = self.bzclass(url=self.url) - bugid = "663674" - cmd = "bugzilla modify %s " % bugid - - bug = bz.getbug(bugid) - - # We want to start with an open bug, so fix things - if bug.status == "CLOSED": - tests.clicomm(cmd + "--status ASSIGNED", bz) - bug.refresh() - self.assertEqual(bug.status, "ASSIGNED") - - origstatus = bug.status - - # Set to ON_QA with a private comment - status = "ON_QA" - comment = ("changing status to %s at %s" % - (status, datetime.datetime.today())) - tests.clicomm(cmd + - "--status %s --comment \"%s\" --private" % (status, comment), bz) - - bug.refresh() - self.assertEqual(bug.status, status) - self.assertEqual(bug.longdescs[-1]["is_private"], 1) - self.assertEqual(bug.longdescs[-1]["text"], comment) - - # Close bug as DEFERRED with a private comment - resolution = "DEFERRED" - comment = ("changing status to CLOSED=%s at %s" % - (resolution, datetime.datetime.today())) - tests.clicomm(cmd + - "--close %s --comment \"%s\" --private" % - (resolution, comment), bz) - - bug.refresh() - self.assertEqual(bug.status, "CLOSED") - self.assertEqual(bug.resolution, resolution) - self.assertEqual(bug.comments[-1]["is_private"], 1) - self.assertEqual(bug.comments[-1]["text"], comment) - - # Close bug as dup with no comment - dupeid = "461686" - desclen = len(bug.longdescs) - tests.clicomm(cmd + - "--close DUPLICATE --dupeid %s" % dupeid, bz) - - bug.refresh() - self.assertEqual(bug.dupe_of, int(dupeid)) - self.assertEqual(len(bug.longdescs), desclen + 1) - self.assertTrue("marked as a duplicate" in bug.longdescs[-1]["text"]) - - # bz.setstatus test - comment = ("adding lone comment at %s" % datetime.datetime.today()) - bug.setstatus("POST", comment=comment, private=True) - bug.refresh() - self.assertEqual(bug.longdescs[-1]["is_private"], 1) - self.assertEqual(bug.longdescs[-1]["text"], comment) - self.assertEqual(bug.status, "POST") - - # bz.close test - fixed_in = str(datetime.datetime.today()) - bug.close("ERRATA", fixedin=fixed_in) - bug.refresh() - self.assertEqual(bug.status, "CLOSED") - self.assertEqual(bug.resolution, "ERRATA") - self.assertEqual(bug.fixed_in, fixed_in) - - # bz.addcomment test - comment = ("yet another test comment %s" % datetime.datetime.today()) - bug.addcomment(comment, private=False) - bug.refresh() - self.assertEqual(bug.longdescs[-1]["text"], comment) - self.assertEqual(bug.longdescs[-1]["is_private"], 0) - - # Confirm comments is same as getcomments - self.assertEqual(bug.comments, bug.getcomments()) - - # Reset state - tests.clicomm(cmd + "--status %s" % origstatus, bz) - bug.refresh() - self.assertEqual(bug.status, origstatus) - - - def test06ModifyEmails(self): - """ - Modify cc, assignee, qa_contact for existing bug - """ - bz = self.bzclass(url=self.url) - bugid = "663674" - cmd = "bugzilla modify %s " % bugid - - bug = bz.getbug(bugid) - - origcc = bug.cc - - # Test CC list and reset it - email1 = "triage@lists.fedoraproject.org" - email2 = "crobinso@redhat.com" - bug.deletecc(origcc) - tests.clicomm(cmd + "--cc %s --cc %s" % (email1, email2), bz) - bug.addcc(email1) - - bug.refresh() - self.assertTrue(email1 in bug.cc) - self.assertTrue(email2 in bug.cc) - self.assertEqual(len(bug.cc), 2) - - tests.clicomm(cmd + "--cc=-%s" % email1, bz) - bug.refresh() - self.assertTrue(email1 not in bug.cc) - - # Test assigned target - tests.clicomm(cmd + "--assignee %s" % email1, bz) - bug.refresh() - self.assertEqual(bug.assigned_to, email1) - - # Test QA target - tests.clicomm(cmd + "--qa_contact %s" % email1, bz) - bug.refresh() - self.assertEqual(bug.qa_contact, email1) - - # Reset values - bug.deletecc(bug.cc) - tests.clicomm(cmd + "--reset-qa-contact --reset-assignee", bz) - - bug.refresh() - self.assertEqual(bug.cc, []) - self.assertEqual(bug.assigned_to, "crobinso@redhat.com") - self.assertEqual(bug.qa_contact, "extras-qa@fedoraproject.org") - - - def test07ModifyMultiFlags(self): - """ - Modify flags and fixed_in for 2 bugs - """ - bz = self.bzclass(url=self.url) - bugid1 = "461686" - bugid2 = "461687" - cmd = "bugzilla modify %s %s " % (bugid1, bugid2) - - def flagstr(b): - ret = [] - for flag in b.flags: - ret.append(flag["name"] + flag["status"]) - return " ".join(sorted(ret)) - - def cleardict_old(b): - """ - Clear flag dictionary, for format meant for bug.updateflags - """ - clearflags = {} - for flag in b.flags: - clearflags[flag["name"]] = "X" - return clearflags - - def cleardict_new(b): - """ - Clear flag dictionary, for format meant for update_bugs - """ - clearflags = [] - for flag in b.flags: - clearflags.append({"name": flag["name"], "status": "X"}) - return clearflags - - bug1 = bz.getbug(bugid1) - if cleardict_old(bug1): - bug1.updateflags(cleardict_old(bug1)) - bug2 = bz.getbug(bugid2) - if cleardict_old(bug2): - bug2.updateflags(cleardict_old(bug2)) - - - # Set flags and confirm - setflags = "needinfo? requires_doc_text-" - tests.clicomm(cmd + - " ".join([(" --flag " + f) for f in setflags.split()]), bz) - - bug1.refresh() - bug2.refresh() - - self.assertEqual(flagstr(bug1), setflags) - self.assertEqual(flagstr(bug2), setflags) - self.assertEqual(bug1.get_flags("needinfo")[0]["status"], "?") - self.assertEqual(bug1.get_flag_status("requires_doc_text"), "-") - - # Clear flags - if cleardict_new(bug1): - bz.update_flags(bug1.id, cleardict_new(bug1)) - bug1.refresh() - if cleardict_new(bug2): - bz.update_flags(bug2.id, cleardict_new(bug2)) - bug2.refresh() - - self.assertEqual(cleardict_old(bug1), {}) - self.assertEqual(cleardict_old(bug2), {}) - - # Set "Fixed In" field - origfix1 = bug1.fixed_in - origfix2 = bug2.fixed_in - - newfix = origfix1 and (origfix1 + "-new1") or "blippy1" - if newfix == origfix2: - newfix = origfix2 + "-2" - - tests.clicomm(cmd + "--fixed_in=%s" % newfix, bz) - - bug1.refresh() - bug2.refresh() - self.assertEqual(bug1.fixed_in, newfix) - self.assertEqual(bug2.fixed_in, newfix) - - # Reset fixed_in - tests.clicomm(cmd + "--fixed_in=\"-\"", bz) - - bug1.refresh() - bug2.refresh() - self.assertEqual(bug1.fixed_in, "-") - self.assertEqual(bug2.fixed_in, "-") - - - def test07ModifyMisc(self): - bugid = "461686" - cmd = "bugzilla modify %s " % bugid - bz = self.bzclass(url=self.url) - bug = bz.getbug(bugid) - - # modify --dependson - tests.clicomm(cmd + "--dependson 123456", bz) - bug.refresh() - self.assertTrue(123456 in bug.depends_on) - tests.clicomm(cmd + "--dependson =111222", bz) - bug.refresh() - self.assertEqual([111222], bug.depends_on) - tests.clicomm(cmd + "--dependson=-111222", bz) - bug.refresh() - self.assertEqual([], bug.depends_on) - - # modify --blocked - tests.clicomm(cmd + "--blocked 123,456", bz) - bug.refresh() - self.assertEqual([123, 456], bug.blocks) - tests.clicomm(cmd + "--blocked =", bz) - bug.refresh() - self.assertEqual([], bug.blocks) - - # modify --keywords - tests.clicomm(cmd + "--keywords +Documentation --keywords EasyFix", bz) - bug.refresh() - self.assertEqual(["Documentation", "EasyFix"], bug.keywords) - tests.clicomm(cmd + "--keywords=-EasyFix --keywords=-Documentation", - bz) - bug.refresh() - self.assertEqual([], bug.keywords) - - # modify --target_release - # modify --target_milestone - targetbugid = 492463 - targetbug = bz.getbug(targetbugid) - targetcmd = "bugzilla modify %s " % targetbugid - tests.clicomm(targetcmd + - "--target_milestone beta --target_release 6.2", bz) - targetbug.refresh() - self.assertEqual(targetbug.target_milestone, "beta") - self.assertEqual(targetbug.target_release, ["6.2"]) - tests.clicomm(targetcmd + - "--target_milestone rc --target_release 6.0", bz) - targetbug.refresh() - self.assertEqual(targetbug.target_milestone, "rc") - self.assertEqual(targetbug.target_release, ["6.0"]) - - # modify --priority - # modify --severity - tests.clicomm(cmd + "--priority low --severity high", bz) - bug.refresh() - self.assertEqual(bug.priority, "low") - self.assertEqual(bug.severity, "high") - tests.clicomm(cmd + "--priority medium --severity medium", bz) - bug.refresh() - self.assertEqual(bug.priority, "medium") - self.assertEqual(bug.severity, "medium") - - # modify --os - # modify --platform - # modify --version - tests.clicomm(cmd + "--version rawhide --os Windows --arch ppc " - "--url http://example.com", bz) - bug.refresh() - self.assertEqual(bug.version, "rawhide") - self.assertEqual(bug.op_sys, "Windows") - self.assertEqual(bug.platform, "ppc") - self.assertEqual(bug.url, "http://example.com") - tests.clicomm(cmd + "--version rawhide --os Linux --arch s390 " - "--url http://example.com/fribby", bz) - bug.refresh() - self.assertEqual(bug.version, "rawhide") - self.assertEqual(bug.op_sys, "Linux") - self.assertEqual(bug.platform, "s390") - self.assertEqual(bug.url, "http://example.com/fribby") - - # modify --field - tests.clicomm(cmd + "--field cf_fixed_in=foo-bar-1.2.3 \ - --field=cf_release_notes=baz", bz) - - bug.refresh() - self.assertEqual(bug.fixed_in, "foo-bar-1.2.3") - self.assertEqual(bug.cf_release_notes, "baz") - - - def test08Attachments(self): - tmpdir = "__test_attach_output" - if tmpdir in os.listdir("."): - os.system("rm -r %s" % tmpdir) - os.mkdir(tmpdir) - os.chdir(tmpdir) - - try: - self._test8Attachments() - finally: - os.chdir("..") - os.system("rm -r %s" % tmpdir) - - def _test8Attachments(self): - """ - Get and set attachments for a bug - """ - bz = self.bzclass(url=self.url) - getallbugid = "663674" - setbugid = "461686" - cmd = "bugzilla attach " - testfile = "../tests/data/bz-attach-get1.txt" - - # Add attachment as CLI option - setbug = bz.getbug(setbugid, extra_fields=["attachments"]) - orignumattach = len(setbug.attachments) - - # Add attachment from CLI with mime guessing - desc1 = "python-bugzilla cli upload %s" % datetime.datetime.today() - out1 = tests.clicomm(cmd + "%s --description \"%s\" --file %s" % - (setbugid, desc1, testfile), bz) - - desc2 = "python-bugzilla cli upload %s" % datetime.datetime.today() - out2 = tests.clicomm(cmd + "%s --file test --summary \"%s\"" % - (setbugid, desc2), bz, stdin=open(testfile)) - - # Expected output format: - # Created attachment on bug - - setbug.refresh() - self.assertEqual(len(setbug.attachments), orignumattach + 2) - self.assertEqual(setbug.attachments[-2]["summary"], desc1) - self.assertEqual(setbug.attachments[-2]["id"], - int(out1.splitlines()[2].split()[2])) - self.assertEqual(setbug.attachments[-1]["summary"], desc2) - self.assertEqual(setbug.attachments[-1]["id"], - int(out2.splitlines()[2].split()[2])) - attachid = setbug.attachments[-2]["id"] - - # Set attachment flags - self.assertEqual(setbug.attachments[-1]["flags"], []) - bz.updateattachmentflags(setbug.id, setbug.attachments[-1]["id"], - "review", status="+") - setbug.refresh() - - self.assertEqual(len(setbug.attachments[-1]["flags"]), 1) - self.assertEqual(setbug.attachments[-1]["flags"][0]["name"], "review") - self.assertEqual(setbug.attachments[-1]["flags"][0]["status"], "+") - - bz.updateattachmentflags(setbug.id, setbug.attachments[-1]["id"], - "review", status="X") - setbug.refresh() - self.assertEqual(setbug.attachments[-1]["flags"], []) - - - # Get attachment, verify content - out = tests.clicomm(cmd + "--get %s" % attachid, bz).splitlines() - - # Expect format: - # Wrote - fname = out[2].split()[1].strip() - - self.assertEqual(len(out), 3) - self.assertEqual(fname, "bz-attach-get1.txt") - self.assertEqual(open(fname).read(), - open(testfile).read()) - os.unlink(fname) - - # Get all attachments - getbug = bz.getbug(getallbugid) - getbug.autorefresh = True - numattach = len(getbug.attachments) - out = tests.clicomm(cmd + "--getall %s" % getallbugid, bz).splitlines() - - self.assertEqual(len(out), numattach + 2) - fnames = [l.split(" ", 1)[1].strip() for l in out[2:]] - self.assertEqual(len(fnames), numattach) - for f in fnames: - if not os.path.exists(f): - raise AssertionError("filename '%s' not found" % f) - os.unlink(f) - - - def test09Whiteboards(self): - bz = self.bzclass(url=self.url) - bug_id = "663674" - cmd = "bugzilla modify %s " % bug_id - bug = bz.getbug(bug_id) - - # Set all whiteboards - initval = str(random.randint(1, 1024)) - tests.clicomm(cmd + - "--whiteboard =%sstatus " - "--devel_whiteboard =%sdevel " - "--internal_whiteboard '=%sinternal, security, foo security1' " - "--qa_whiteboard =%sqa " % - (initval, initval, initval, initval), bz) - - bug.refresh() - self.assertEqual(bug.whiteboard, initval + "status") - self.assertEqual(bug.qa_whiteboard, initval + "qa") - self.assertEqual(bug.devel_whiteboard, initval + "devel") - self.assertEqual(bug.internal_whiteboard, - initval + "internal, security, foo security1") - - # Modify whiteboards - tests.clicomm(cmd + - "--whiteboard =foobar " - "--qa_whiteboard _app " - "--devel_whiteboard =pre-%s" % bug.devel_whiteboard, bz) - - bug.refresh() - self.assertEqual(bug.qa_whiteboard, initval + "qa" + " _app") - self.assertEqual(bug.devel_whiteboard, "pre-" + initval + "devel") - self.assertEqual(bug.status_whiteboard, "foobar") - - # Verify that tag manipulation is smart about separator - tests.clicomm(cmd + - "--qa_whiteboard=-_app " - "--internal_whiteboard=-security,", bz) - bug.refresh() - - self.assertEqual(bug.qa_whiteboard, initval + "qa") - self.assertEqual(bug.internal_whiteboard, - initval + "internal, foo security1") - - # Clear whiteboards - update = bz.build_update( - whiteboard="", devel_whiteboard="", - internal_whiteboard="", qa_whiteboard="") - bz.update_bugs(bug.id, update) - - bug.refresh() - self.assertEqual(bug.whiteboard, "") - self.assertEqual(bug.qa_whiteboard, "") - self.assertEqual(bug.devel_whiteboard, "") - self.assertEqual(bug.internal_whiteboard, "") - - - def test10Login(self): - """ - Failed login test, gives us a bit more coverage - """ - # We overwrite getpass for testing - import getpass - - def fakegetpass(prompt): - sys.stdout.write(prompt) - sys.stdout.flush() - return sys.stdin.readline() - oldgetpass = getpass.getpass - getpass.getpass = fakegetpass - - try: - # Implied login with --username and --password - ret = tests.clicomm("bugzilla --bugzilla %s " - "--user foobar@example.com " - "--password foobar query -b 123456" % self.url, - None, expectfail=True) - self.assertTrue("Login failed: " in ret) - - # 'login' with explicit options - ret = tests.clicomm("bugzilla --bugzilla %s " - "--user foobar@example.com " - "--password foobar login" % self.url, - None, expectfail=True) - self.assertTrue("Login failed: " in ret) - - # 'login' with positional options - ret = tests.clicomm("bugzilla --bugzilla %s " - "login foobar@example.com foobar" % self.url, - None, expectfail=True) - self.assertTrue("Login failed: " in ret) - - - # bare 'login' - stdinstr = "foobar@example.com\n\rfoobar\n\r" - ret = tests.clicomm("bugzilla --bugzilla %s login" % self.url, - None, expectfail=True, stdinstr=stdinstr) - self.assertTrue("Bugzilla Username:" in ret) - self.assertTrue("Bugzilla Password:" in ret) - self.assertTrue("Login failed: " in ret) - finally: - getpass.getpass = oldgetpass - - - def test11UserUpdate(self): - # This won't work if run by the same user we are using - bz = self.bzclass(url=self.url) - email = "anaconda-maint-list@redhat.com" - group = "fedora_contrib" - - fn = sys._getframe().f_code.co_name # pylint: disable=protected-access - have_admin = self._check_have_admin(bz, fn) - - user = bz.getuser(email) - if have_admin: - self.assertTrue(group in user.groupnames) - origgroups = user.groupnames - - # Remove the group - try: - bz.updateperms(email, "remove", [group]) - user.refresh() - self.assertTrue(group not in user.groupnames) - except: - e = sys.exc_info()[1] - if have_admin: - raise - self.assertTrue("Sorry, you aren't a member" in str(e)) - - # Re add it - try: - bz.updateperms(email, "add", group) - user.refresh() - self.assertTrue(group in user.groupnames) - except: - e = sys.exc_info()[1] - if have_admin: - raise - self.assertTrue("Sorry, you aren't a member" in str(e)) - - # Set groups - try: - newgroups = user.groupnames[:] - if have_admin: - newgroups.remove(group) - bz.updateperms(email, "set", newgroups) - user.refresh() - self.assertTrue(group not in user.groupnames) - except: - e = sys.exc_info()[1] - if have_admin: - raise - self.assertTrue("Sorry, you aren't a member" in str(e)) - - # Reset everything - try: - bz.updateperms(email, "set", origgroups) - except: - e = sys.exc_info()[1] - if have_admin: - raise - self.assertTrue("Sorry, you aren't a member" in str(e)) - - user.refresh() - self.assertEqual(user.groupnames, origgroups) - - - def test11ComponentEditing(self): - bz = self.bzclass(url=self.url) - component = ("python-bugzilla-testcomponent-%s" % - str(random.randint(1, 1024 * 1024 * 1024))) - basedata = { - "product": "Fedora Documentation", - "component": component, - } - - fn = sys._getframe().f_code.co_name # pylint: disable=protected-access - have_admin = self._check_have_admin(bz, fn) - - def compare(data, newid): - proxy = bz._proxy # pylint: disable=protected-access - products = proxy.Product.get({"names": [basedata["product"]]}) - compdata = None - for c in products["products"][0]["components"]: - if int(c["id"]) == int(newid): - compdata = c - break - - self.assertTrue(bool(compdata)) - self.assertEqual(data["component"], compdata["name"]) - self.assertEqual(data["description"], compdata["description"]) - self.assertEqual(data["initialowner"], - compdata["default_assigned_to"]) - self.assertEqual(data["initialqacontact"], - compdata["default_qa_contact"]) - self.assertEqual(data["is_active"], compdata["is_active"]) - - - # Create component - data = basedata.copy() - data.update({ - "description": "foo test bar", - "initialowner": "crobinso@redhat.com", - "initialqacontact": "extras-qa@fedoraproject.org", - "initialcclist": ["wwoods@redhat.com", "toshio@fedoraproject.org"], - "is_active": True, - }) - try: - newid = bz.addcomponent(data)['id'] - print("Created product=%s component=%s" % ( - basedata["product"], basedata["component"])) - compare(data, newid) - except: - e = sys.exc_info()[1] - if have_admin: - raise - self.assertTrue("Sorry, you aren't a member" in str(e)) - - - # Edit component - data = basedata.copy() - data.update({ - "description": "hey new desc!", - "initialowner": "extras-qa@fedoraproject.org", - "initialqacontact": "virt-mgr-maint@redhat.com", - "initialcclist": ["libvirt-maint@redhat.com", - "virt-maint@lists.fedoraproject.org"], - "is_active": False, - }) - try: - bz.editcomponent(data) - compare(data, newid) - except: - e = sys.exc_info()[1] - if have_admin: - raise - self.assertTrue("Sorry, you aren't a member" in str(e)) - - def test12SetCookie(self): - bz = self.bzclass(self.url, - cookiefile=-1, tokenfile=None) - - try: - bz.cookiefile = None - raise AssertionError("Setting cookiefile for active connection " - "should fail.") - except RuntimeError: - e = sys.exc_info()[1] - self.assertTrue("disconnect()" in str(e)) - - bz.disconnect() - bz.cookiefile = None - bz.connect() - self.assertFalse(bz.logged_in) - - def test13SubComponents(self): - bz = self.bzclass(url=self.url) - # Long closed RHEL5 lvm2 bug. This component has sub_components - bug = bz.getbug("185526") - bug.autorefresh = True - self.assertEqual(bug.component, "lvm2") - - bz.update_bugs(bug.id, bz.build_update( - component="lvm2", sub_component="Command-line tools (RHEL5)")) - bug.refresh() - self.assertEqual(bug.sub_components, - {"lvm2": ["Command-line tools (RHEL5)"]}) - - bz.update_bugs(bug.id, bz.build_update(sub_component={})) - bug.refresh() - self.assertEqual(bug.sub_components, {}) - - def test13ExternalTrackerQuery(self): - bz = self.bzclass(url=self.url) - self.assertRaises(RuntimeError, - bz.build_external_tracker_boolean_query) - - def _deleteAllExistingExternalTrackers(self, bugid): - bz = self.bzclass(url=self.url) - ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] - if ids != []: - bz.remove_external_tracker(ids=ids) - - def test14ExternalTrackersAddUpdateRemoveQuery(self): - bz = self.bzclass(url=self.url) - bugid = 461686 - ext_bug_id = 380489 - - # Delete any existing external trackers to get to a known state - self._deleteAllExistingExternalTrackers(bugid) - - url = "https://bugzilla.mozilla.org" - if bz.bz_ver_major < 5: - url = "http://bugzilla.mozilla.org" - - # test adding tracker - kwargs = { - 'ext_type_id': 6, - 'ext_type_url': url, - 'ext_type_description': 'Mozilla Foundation', - 'ext_status': 'Original Status', - 'ext_description': 'the description', - 'ext_priority': 'the priority' - } - bz.add_external_tracker(bugid, ext_bug_id, **kwargs) - added_bug = bz.getbug(bugid).external_bugs[0] - assert added_bug['type']['id'] == kwargs['ext_type_id'] - assert added_bug['type']['url'] == kwargs['ext_type_url'] - assert (added_bug['type']['description'] == - kwargs['ext_type_description']) - assert added_bug['ext_status'] == kwargs['ext_status'] - assert added_bug['ext_description'] == kwargs['ext_description'] - assert added_bug['ext_priority'] == kwargs['ext_priority'] - - # test updating status, description, and priority by id - kwargs = { - 'ids': bz.getbug(bugid).external_bugs[0]['id'], - 'ext_status': 'New Status', - 'ext_description': 'New Description', - 'ext_priority': 'New Priority' - } - bz.update_external_tracker(**kwargs) - updated_bug = bz.getbug(bugid).external_bugs[0] - assert updated_bug['ext_bz_bug_id'] == str(ext_bug_id) - assert updated_bug['ext_status'] == kwargs['ext_status'] - assert updated_bug['ext_description'] == kwargs['ext_description'] - assert updated_bug['ext_priority'] == kwargs['ext_priority'] - - # test removing tracker - ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] - assert len(ids) == 1 - bz.remove_external_tracker(ids=ids) - ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] - assert len(ids) == 0 - - def test15EnsureLoggedIn(self): - bz = self.bzclass(url=self.url) - comm = "bugzilla --ensure-logged-in query --bug_id 979546" - tests.clicomm(comm, bz) - - def test16ModifyTags(self): - bugid = "461686" - cmd = "bugzilla modify %s " % bugid - bz = self.bzclass(url=self.url) - bug = bz.getbug(bugid) - - if bug.tags: - bz.update_tags(bug.id, tags_remove=bug.tags) - bug.refresh() - self.assertEqual(bug.tags, []) - - tests.clicomm(cmd + "--tags foo --tags +bar --tags baz", bz) - bug.refresh() - self.assertEqual(bug.tags, ["foo", "bar", "baz"]) - - tests.clicomm(cmd + "--tags=-bar", bz) - bug.refresh() - self.assertEqual(bug.tags, ["foo", "baz"]) - - bz.update_tags(bug.id, tags_remove=bug.tags) - bug.refresh() - self.assertEqual(bug.tags, []) diff --git a/tests/services/Dockerfile b/tests/services/Dockerfile new file mode 100644 index 00000000..83c1e209 --- /dev/null +++ b/tests/services/Dockerfile @@ -0,0 +1,28 @@ +FROM ubuntu:22.04 +LABEL description="Bugzilla image for testing purposes" +ARG DEBIAN_FRONTEND=noninteractive +ENV TZ="Etc/UTC" +RUN apt update && \ + apt install --no-install-recommends -q -y \ + tzdata wget apache2 libcgi-pm-perl libdatetime-perl libdatetime-timezone-perl libdbi-perl \ + libdbix-connector-perl libdigest-sha-perl libemail-address-perl libemail-mime-perl \ + libemail-sender-perl libjson-xs-perl liblist-moreutils-perl libmath-random-isaac-perl \ + libtemplate-perl libtimedate-perl liburi-perl libmariadb-dev-compat libdbd-mysql-perl \ + libxmlrpc-lite-perl libsoap-lite-perl libapache2-mod-perl2 libtest-taint-perl \ + libjson-rpc-perl && \ + apt clean +RUN mkdir -p /var/www/webapps && \ + wget https://ftp.mozilla.org/pub/mozilla.org/webtools/bugzilla-5.0.6.tar.gz \ + -O /tmp/bugzilla-5.0.6.tar.gz&& \ + tar xvzf /tmp/bugzilla-5.0.6.tar.gz && \ + rm /tmp/bugzilla-5.0.6.tar.gz && \ + mv /bugzilla-5.0.6/ /var/www/webapps/bugzilla/ && \ + mkdir /var/www/webapps/bugzilla/data/ +COPY bugzilla.conf /etc/apache2/sites-available/ +COPY localconfig /var/www/webapps/bugzilla/ +COPY params.json /var/www/webapps/bugzilla/data/ +RUN a2dissite 000-default && \ + a2ensite bugzilla && \ + a2enmod cgi headers expires rewrite perl && \ + /var/www/webapps/bugzilla/checksetup.pl +CMD apachectl -D FOREGROUND diff --git a/tests/services/README.md b/tests/services/README.md new file mode 100644 index 00000000..c59174f3 --- /dev/null +++ b/tests/services/README.md @@ -0,0 +1,58 @@ +# Working with the containerized Bugzilla instance + +This document describes the steps for building a Bugzilla container image that can be used in the +GitHub Actions as a service and generating a database dump. + +In the following examples, the use of `docker` is assumed. Commands for `podman` should be +identical. + +## Build + +```shell +$ docker network create --driver bridge local-bridge +$ docker run --rm -itd \ + --env MARIADB_USER=bugs \ + --env MARIADB_DATABASE=bugs \ + --env MARIADB_PASSWORD=secret \ + --env MARIADB_ROOT_PASSWORD=supersecret \ + -p 3306:3306 \ + --network local-bridge \ + --name mariadb \ + mariadb:latest +$ mariadb -u bugs -h 127.0.0.1 -P 3306 --password=secret bugs < bugs.sql +$ docker build --network local-bridge . -t ghcr.io/crazyscientist/bugzilla:test +``` + +For those, who can spot the _chicken and egg problem_: The first version of `bugs.sql` was +created after running the Bugzilla installer inside the container. + +## Usage + +Once built, you can follow the above instructions; instead of building +the image, you can run it: + +```shell +docker run --rm -itd \ + -p 8000:80 \ + --network local-bridge \ + ghcr.io/crazyscientist/bugzilla:test +``` + +## Test data + +The test data used by the Bugzilla service in the integration test suite is stored in `bugs.sql`. + +One can edit this file manually or follow the above instructions to start both a MariaDB and +Bugzilla container and edit the data in Bugzilla. Once done, one needs to dump the changed data into +the file again: + +```shell +$ mariadb-dump -u bugs -h 127.0.0.1 -P 3306 --password=secret bugs > bugs.sql +``` + +## Testing +And now, you can run the integration tests against this instance: + +```shell +BUGZILLA_URL=http://localhost:8000 pytest --ro-integration +``` diff --git a/tests/services/bugs.sql b/tests/services/bugs.sql new file mode 100644 index 00000000..15c78d34 --- /dev/null +++ b/tests/services/bugs.sql @@ -0,0 +1,2277 @@ +/*!999999\- enable the sandbox mode */ +-- MariaDB dump 10.19 Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64) +-- +-- Host: 127.0.0.1 Database: bugs +-- ------------------------------------------------------ +-- Server version 11.1.2-MariaDB-1:11.1.2+maria~ubu2204 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `attach_data` +-- + +DROP TABLE IF EXISTS `attach_data`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `attach_data` ( + `id` mediumint(9) NOT NULL, + `thedata` longblob NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_attach_data_id_attachments_attach_id` FOREIGN KEY (`id`) REFERENCES `attachments` (`attach_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci MAX_ROWS=100000 AVG_ROW_LENGTH=1000000; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `attach_data` +-- + +LOCK TABLES `attach_data` WRITE; +/*!40000 ALTER TABLE `attach_data` DISABLE KEYS */; +/*!40000 ALTER TABLE `attach_data` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `attachments` +-- + +DROP TABLE IF EXISTS `attachments`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `attachments` ( + `attach_id` mediumint(9) NOT NULL AUTO_INCREMENT, + `bug_id` mediumint(9) NOT NULL, + `creation_ts` datetime NOT NULL, + `modification_time` datetime NOT NULL, + `description` tinytext NOT NULL, + `mimetype` tinytext NOT NULL, + `ispatch` tinyint(4) NOT NULL DEFAULT 0, + `filename` varchar(255) NOT NULL, + `submitter_id` mediumint(9) NOT NULL, + `isobsolete` tinyint(4) NOT NULL DEFAULT 0, + `isprivate` tinyint(4) NOT NULL DEFAULT 0, + PRIMARY KEY (`attach_id`), + KEY `attachments_bug_id_idx` (`bug_id`), + KEY `attachments_creation_ts_idx` (`creation_ts`), + KEY `attachments_modification_time_idx` (`modification_time`), + KEY `attachments_submitter_id_idx` (`submitter_id`,`bug_id`), + CONSTRAINT `fk_attachments_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_attachments_submitter_id_profiles_userid` FOREIGN KEY (`submitter_id`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `attachments` +-- + +LOCK TABLES `attachments` WRITE; +/*!40000 ALTER TABLE `attachments` DISABLE KEYS */; +/*!40000 ALTER TABLE `attachments` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `audit_log` +-- + +DROP TABLE IF EXISTS `audit_log`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `audit_log` ( + `user_id` mediumint(9) DEFAULT NULL, + `class` varchar(255) NOT NULL, + `object_id` int(11) NOT NULL, + `field` varchar(64) NOT NULL, + `removed` mediumtext DEFAULT NULL, + `added` mediumtext DEFAULT NULL, + `at_time` datetime NOT NULL, + KEY `audit_log_class_idx` (`class`,`at_time`), + KEY `fk_audit_log_user_id_profiles_userid` (`user_id`), + CONSTRAINT `fk_audit_log_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `audit_log` +-- + +LOCK TABLES `audit_log` WRITE; +/*!40000 ALTER TABLE `audit_log` DISABLE KEYS */; +INSERT INTO `audit_log` VALUES (NULL,'Bugzilla::Field',1,'__create__',NULL,'bug_id','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',2,'__create__',NULL,'short_desc','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',3,'__create__',NULL,'classification','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',4,'__create__',NULL,'product','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',5,'__create__',NULL,'version','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',6,'__create__',NULL,'rep_platform','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',7,'__create__',NULL,'bug_file_loc','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',8,'__create__',NULL,'op_sys','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',9,'__create__',NULL,'bug_status','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',10,'__create__',NULL,'status_whiteboard','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',11,'__create__',NULL,'keywords','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',12,'__create__',NULL,'resolution','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',13,'__create__',NULL,'bug_severity','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',14,'__create__',NULL,'priority','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',15,'__create__',NULL,'component','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',16,'__create__',NULL,'assigned_to','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',17,'__create__',NULL,'reporter','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',18,'__create__',NULL,'qa_contact','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',19,'__create__',NULL,'assigned_to_realname','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',20,'__create__',NULL,'reporter_realname','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',21,'__create__',NULL,'qa_contact_realname','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',22,'__create__',NULL,'cc','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',23,'__create__',NULL,'dependson','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',24,'__create__',NULL,'blocked','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',25,'__create__',NULL,'attachments.description','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',26,'__create__',NULL,'attachments.filename','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',27,'__create__',NULL,'attachments.mimetype','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',28,'__create__',NULL,'attachments.ispatch','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',29,'__create__',NULL,'attachments.isobsolete','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',30,'__create__',NULL,'attachments.isprivate','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',31,'__create__',NULL,'attachments.submitter','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',32,'__create__',NULL,'target_milestone','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',33,'__create__',NULL,'creation_ts','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',34,'__create__',NULL,'delta_ts','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',35,'__create__',NULL,'longdesc','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',36,'__create__',NULL,'longdescs.isprivate','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',37,'__create__',NULL,'longdescs.count','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',38,'__create__',NULL,'alias','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',39,'__create__',NULL,'everconfirmed','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',40,'__create__',NULL,'reporter_accessible','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',41,'__create__',NULL,'cclist_accessible','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',42,'__create__',NULL,'bug_group','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',43,'__create__',NULL,'estimated_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',44,'__create__',NULL,'remaining_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',45,'__create__',NULL,'deadline','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',46,'__create__',NULL,'commenter','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',47,'__create__',NULL,'flagtypes.name','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',48,'__create__',NULL,'requestees.login_name','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',49,'__create__',NULL,'setters.login_name','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',50,'__create__',NULL,'work_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',51,'__create__',NULL,'percentage_complete','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',52,'__create__',NULL,'content','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',53,'__create__',NULL,'attach_data.thedata','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',54,'__create__',NULL,'owner_idle_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',55,'__create__',NULL,'see_also','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',56,'__create__',NULL,'tag','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',57,'__create__',NULL,'last_visit_ts','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',58,'__create__',NULL,'comment_tag','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',59,'__create__',NULL,'days_elapsed','2023-09-20 13:12:35'),(NULL,'Bugzilla::Classification',1,'__create__',NULL,'Unclassified','2023-09-20 13:12:35'),(NULL,'Bugzilla::Group',1,'__create__',NULL,'admin','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',2,'__create__',NULL,'tweakparams','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',3,'__create__',NULL,'editusers','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',4,'__create__',NULL,'creategroups','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',5,'__create__',NULL,'editclassifications','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',6,'__create__',NULL,'editcomponents','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',7,'__create__',NULL,'editkeywords','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',8,'__create__',NULL,'editbugs','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',9,'__create__',NULL,'canconfirm','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',10,'__create__',NULL,'bz_canusewhineatothers','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',11,'__create__',NULL,'bz_canusewhines','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',12,'__create__',NULL,'bz_sudoers','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',13,'__create__',NULL,'bz_sudo_protect','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',14,'__create__',NULL,'bz_quip_moderators','2023-09-20 13:12:40'),(NULL,'Bugzilla::User',1,'__create__',NULL,'andreas@hasenkopf.xyz','2023-09-20 13:12:55'),(NULL,'Bugzilla::Product',1,'__create__',NULL,'TestProduct','2023-09-20 13:12:55'),(NULL,'Bugzilla::Version',1,'__create__',NULL,'unspecified','2023-09-20 13:12:55'),(NULL,'Bugzilla::Milestone',1,'__create__',NULL,'---','2023-09-20 13:12:55'),(NULL,'Bugzilla::Component',1,'__create__',NULL,'TestComponent','2023-09-20 13:12:55'),(1,'Bugzilla::Product',2,'__create__',NULL,'Red Hat Enterprise Linux 9','2023-11-27 12:25:54'),(1,'Bugzilla::Version',2,'__create__',NULL,'unspecified','2023-11-27 12:25:54'),(1,'Bugzilla::Milestone',2,'__create__',NULL,'---','2023-11-27 12:25:54'),(1,'Bugzilla::Component',2,'__create__',NULL,'python-bugzilla','2023-11-27 12:25:54'),(1,'Bugzilla::Version',3,'__create__',NULL,'9.0','2023-11-27 12:26:06'),(1,'Bugzilla::Version',4,'__create__',NULL,'9.1','2023-11-27 12:26:14'),(1,'Bugzilla::Product',3,'__create__',NULL,'SUSE Linux Enterprise Server 15 SP6','2023-11-27 12:29:18'),(1,'Bugzilla::Version',5,'__create__',NULL,'unspecified','2023-11-27 12:29:18'),(1,'Bugzilla::Milestone',3,'__create__',NULL,'---','2023-11-27 12:29:18'),(1,'Bugzilla::Component',3,'__create__',NULL,'Kernel','2023-11-27 12:29:18'),(1,'Bugzilla::Component',4,'__create__',NULL,'Containers','2023-11-27 12:29:46'),(1,'Bugzilla::Keyword',1,'__create__',NULL,'FooBar','2024-10-15 13:05:27'),(1,'Bugzilla::Keyword',2,'__create__',NULL,'LoremIpsum','2024-10-15 13:05:52'),(1,'Bugzilla::FlagType',1,'__create__',NULL,'needinfo','2024-10-15 13:26:28'),(1,'Bugzilla::User',2,'__create__',NULL,'nemo@example.com','2024-10-15 13:28:58'); +/*!40000 ALTER TABLE `audit_log` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bug_group_map` +-- + +DROP TABLE IF EXISTS `bug_group_map`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bug_group_map` ( + `bug_id` mediumint(9) NOT NULL, + `group_id` mediumint(9) NOT NULL, + UNIQUE KEY `bug_group_map_bug_id_idx` (`bug_id`,`group_id`), + KEY `bug_group_map_group_id_idx` (`group_id`), + CONSTRAINT `fk_bug_group_map_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_bug_group_map_group_id_groups_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bug_group_map` +-- + +LOCK TABLES `bug_group_map` WRITE; +/*!40000 ALTER TABLE `bug_group_map` DISABLE KEYS */; +/*!40000 ALTER TABLE `bug_group_map` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bug_see_also` +-- + +DROP TABLE IF EXISTS `bug_see_also`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bug_see_also` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `bug_id` mediumint(9) NOT NULL, + `value` varchar(255) NOT NULL, + `class` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + UNIQUE KEY `bug_see_also_bug_id_idx` (`bug_id`,`value`), + CONSTRAINT `fk_bug_see_also_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bug_see_also` +-- + +LOCK TABLES `bug_see_also` WRITE; +/*!40000 ALTER TABLE `bug_see_also` DISABLE KEYS */; +/*!40000 ALTER TABLE `bug_see_also` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bug_severity` +-- + +DROP TABLE IF EXISTS `bug_severity`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bug_severity` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `visibility_value_id` smallint(6) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `bug_severity_value_idx` (`value`), + KEY `bug_severity_sortkey_idx` (`sortkey`,`value`), + KEY `bug_severity_visibility_value_id_idx` (`visibility_value_id`) +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bug_severity` +-- + +LOCK TABLES `bug_severity` WRITE; +/*!40000 ALTER TABLE `bug_severity` DISABLE KEYS */; +INSERT INTO `bug_severity` VALUES (1,'blocker',100,1,NULL),(2,'critical',200,1,NULL),(3,'major',300,1,NULL),(4,'normal',400,1,NULL),(5,'minor',500,1,NULL),(6,'trivial',600,1,NULL),(7,'enhancement',700,1,NULL); +/*!40000 ALTER TABLE `bug_severity` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bug_status` +-- + +DROP TABLE IF EXISTS `bug_status`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bug_status` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `visibility_value_id` smallint(6) DEFAULT NULL, + `is_open` tinyint(4) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `bug_status_value_idx` (`value`), + KEY `bug_status_sortkey_idx` (`sortkey`,`value`), + KEY `bug_status_visibility_value_id_idx` (`visibility_value_id`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bug_status` +-- + +LOCK TABLES `bug_status` WRITE; +/*!40000 ALTER TABLE `bug_status` DISABLE KEYS */; +INSERT INTO `bug_status` VALUES (1,'UNCONFIRMED',100,1,NULL,1),(2,'CONFIRMED',200,1,NULL,1),(3,'IN_PROGRESS',300,1,NULL,1),(4,'RESOLVED',400,1,NULL,0),(5,'VERIFIED',500,1,NULL,0); +/*!40000 ALTER TABLE `bug_status` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bug_tag` +-- + +DROP TABLE IF EXISTS `bug_tag`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bug_tag` ( + `bug_id` mediumint(9) NOT NULL, + `tag_id` mediumint(9) NOT NULL, + UNIQUE KEY `bug_tag_bug_id_idx` (`bug_id`,`tag_id`), + KEY `fk_bug_tag_tag_id_tag_id` (`tag_id`), + CONSTRAINT `fk_bug_tag_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_bug_tag_tag_id_tag_id` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bug_tag` +-- + +LOCK TABLES `bug_tag` WRITE; +/*!40000 ALTER TABLE `bug_tag` DISABLE KEYS */; +/*!40000 ALTER TABLE `bug_tag` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bug_user_last_visit` +-- + +DROP TABLE IF EXISTS `bug_user_last_visit`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bug_user_last_visit` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` mediumint(9) NOT NULL, + `bug_id` mediumint(9) NOT NULL, + `last_visit_ts` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `bug_user_last_visit_idx` (`user_id`,`bug_id`), + KEY `bug_user_last_visit_last_visit_ts_idx` (`last_visit_ts`), + KEY `fk_bug_user_last_visit_bug_id_bugs_bug_id` (`bug_id`), + CONSTRAINT `fk_bug_user_last_visit_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_bug_user_last_visit_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bug_user_last_visit` +-- + +LOCK TABLES `bug_user_last_visit` WRITE; +/*!40000 ALTER TABLE `bug_user_last_visit` DISABLE KEYS */; +INSERT INTO `bug_user_last_visit` VALUES (1,1,1,'2024-10-15 14:00:54'),(2,1,2,'2024-10-15 14:00:49'),(3,1,3,'2024-10-15 13:45:42'); +/*!40000 ALTER TABLE `bug_user_last_visit` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bugs` +-- + +DROP TABLE IF EXISTS `bugs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bugs` ( + `bug_id` mediumint(9) NOT NULL AUTO_INCREMENT, + `assigned_to` mediumint(9) NOT NULL, + `bug_file_loc` mediumtext NOT NULL DEFAULT '', + `bug_severity` varchar(64) NOT NULL, + `bug_status` varchar(64) NOT NULL, + `creation_ts` datetime DEFAULT NULL, + `delta_ts` datetime NOT NULL, + `short_desc` varchar(255) NOT NULL, + `op_sys` varchar(64) NOT NULL, + `priority` varchar(64) NOT NULL, + `product_id` smallint(6) NOT NULL, + `rep_platform` varchar(64) NOT NULL, + `reporter` mediumint(9) NOT NULL, + `version` varchar(64) NOT NULL, + `component_id` mediumint(9) NOT NULL, + `resolution` varchar(64) NOT NULL DEFAULT '', + `target_milestone` varchar(64) NOT NULL DEFAULT '---', + `qa_contact` mediumint(9) DEFAULT NULL, + `status_whiteboard` mediumtext NOT NULL DEFAULT '', + `lastdiffed` datetime DEFAULT NULL, + `everconfirmed` tinyint(4) NOT NULL, + `reporter_accessible` tinyint(4) NOT NULL DEFAULT 1, + `cclist_accessible` tinyint(4) NOT NULL DEFAULT 1, + `estimated_time` decimal(7,2) NOT NULL DEFAULT 0.00, + `remaining_time` decimal(7,2) NOT NULL DEFAULT 0.00, + `deadline` datetime DEFAULT NULL, + PRIMARY KEY (`bug_id`), + KEY `bugs_assigned_to_idx` (`assigned_to`), + KEY `bugs_creation_ts_idx` (`creation_ts`), + KEY `bugs_delta_ts_idx` (`delta_ts`), + KEY `bugs_bug_severity_idx` (`bug_severity`), + KEY `bugs_bug_status_idx` (`bug_status`), + KEY `bugs_op_sys_idx` (`op_sys`), + KEY `bugs_priority_idx` (`priority`), + KEY `bugs_product_id_idx` (`product_id`), + KEY `bugs_reporter_idx` (`reporter`), + KEY `bugs_version_idx` (`version`), + KEY `bugs_component_id_idx` (`component_id`), + KEY `bugs_resolution_idx` (`resolution`), + KEY `bugs_target_milestone_idx` (`target_milestone`), + KEY `bugs_qa_contact_idx` (`qa_contact`), + CONSTRAINT `fk_bugs_assigned_to_profiles_userid` FOREIGN KEY (`assigned_to`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_component_id_components_id` FOREIGN KEY (`component_id`) REFERENCES `components` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_qa_contact_profiles_userid` FOREIGN KEY (`qa_contact`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_reporter_profiles_userid` FOREIGN KEY (`reporter`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bugs` +-- + +LOCK TABLES `bugs` WRITE; +/*!40000 ALTER TABLE `bugs` DISABLE KEYS */; +INSERT INTO `bugs` VALUES (1,1,'','major','IN_PROGRESS','2023-11-27 15:35:33','2023-11-27 15:53:04','ZeroDivisionError in function foo_bar()','Linux','---',3,'PC',1,'unspecified',4,'','---',NULL,'AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:L','2023-11-27 15:53:04',1,1,1,0.00,0.00,NULL),(2,1,'','enhancement','CONFIRMED','2023-11-27 15:38:45','2024-10-15 13:29:13','Expect the Spanish inquisition','Linux','---',2,'PC',1,'9.1',2,'','---',NULL,'lorem ipsum','2024-10-15 13:29:13',1,1,1,0.00,0.00,NULL),(3,1,'','enhancement','CONFIRMED','2024-10-15 13:45:40','2024-10-15 13:45:40','Kernel Panic in the Discothek','Linux','---',3,'PC',1,'unspecified',3,'','---',NULL,'','2024-10-15 13:45:40',1,1,1,0.00,0.00,NULL); +/*!40000 ALTER TABLE `bugs` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bugs_activity` +-- + +DROP TABLE IF EXISTS `bugs_activity`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bugs_activity` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `bug_id` mediumint(9) NOT NULL, + `attach_id` mediumint(9) DEFAULT NULL, + `who` mediumint(9) NOT NULL, + `bug_when` datetime NOT NULL, + `fieldid` mediumint(9) NOT NULL, + `added` varchar(255) DEFAULT NULL, + `removed` varchar(255) DEFAULT NULL, + `comment_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `bugs_activity_bug_id_idx` (`bug_id`), + KEY `bugs_activity_who_idx` (`who`), + KEY `bugs_activity_bug_when_idx` (`bug_when`), + KEY `bugs_activity_fieldid_idx` (`fieldid`), + KEY `bugs_activity_added_idx` (`added`), + KEY `bugs_activity_removed_idx` (`removed`), + KEY `fk_bugs_activity_attach_id_attachments_attach_id` (`attach_id`), + KEY `fk_bugs_activity_comment_id_longdescs_comment_id` (`comment_id`), + CONSTRAINT `fk_bugs_activity_attach_id_attachments_attach_id` FOREIGN KEY (`attach_id`) REFERENCES `attachments` (`attach_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_activity_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_activity_comment_id_longdescs_comment_id` FOREIGN KEY (`comment_id`) REFERENCES `longdescs` (`comment_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_activity_fieldid_fielddefs_id` FOREIGN KEY (`fieldid`) REFERENCES `fielddefs` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_activity_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bugs_activity` +-- + +LOCK TABLES `bugs_activity` WRITE; +/*!40000 ALTER TABLE `bugs_activity` DISABLE KEYS */; +INSERT INTO `bugs_activity` VALUES (1,1,NULL,1,'2023-11-27 15:45:09',9,'IN_PROGRESS','CONFIRMED',NULL),(2,1,NULL,1,'2023-11-27 15:47:58',10,'AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:L','',NULL),(3,1,NULL,1,'2023-11-27 15:53:04',38,'FOO-1','',NULL),(4,2,NULL,1,'2024-10-15 13:08:14',10,'lorem ipsum','',NULL),(5,2,NULL,1,'2024-10-15 13:08:14',11,'FooBar','',NULL),(6,2,NULL,1,'2024-10-15 13:29:13',47,'needinfo?(nemo@example.com)','',NULL),(7,2,NULL,1,'2024-10-15 13:29:13',22,'nemo@example.com','',NULL); +/*!40000 ALTER TABLE `bugs_activity` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bugs_aliases` +-- + +DROP TABLE IF EXISTS `bugs_aliases`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bugs_aliases` ( + `alias` varchar(40) NOT NULL, + `bug_id` mediumint(9) DEFAULT NULL, + UNIQUE KEY `bugs_aliases_alias_idx` (`alias`), + KEY `bugs_aliases_bug_id_idx` (`bug_id`), + CONSTRAINT `fk_bugs_aliases_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bugs_aliases` +-- + +LOCK TABLES `bugs_aliases` WRITE; +/*!40000 ALTER TABLE `bugs_aliases` DISABLE KEYS */; +INSERT INTO `bugs_aliases` VALUES ('FOO-1',1); +/*!40000 ALTER TABLE `bugs_aliases` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bugs_fulltext` +-- + +DROP TABLE IF EXISTS `bugs_fulltext`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bugs_fulltext` ( + `bug_id` mediumint(9) NOT NULL, + `short_desc` varchar(255) NOT NULL, + `comments` mediumtext DEFAULT NULL, + `comments_noprivate` mediumtext DEFAULT NULL, + PRIMARY KEY (`bug_id`), + FULLTEXT KEY `bugs_fulltext_short_desc_idx` (`short_desc`), + FULLTEXT KEY `bugs_fulltext_comments_idx` (`comments`), + FULLTEXT KEY `bugs_fulltext_comments_noprivate_idx` (`comments_noprivate`), + CONSTRAINT `fk_bugs_fulltext_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bugs_fulltext` +-- + +LOCK TABLES `bugs_fulltext` WRITE; +/*!40000 ALTER TABLE `bugs_fulltext` DISABLE KEYS */; +INSERT INTO `bugs_fulltext` VALUES (1,'ZeroDivisionError in function foo_bar()','Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\nAt vero eos et accusam et justo duo dolores et ea rebum.\nStet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.','Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\nAt vero eos et accusam et justo duo dolores et ea rebum.\nStet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.'),(2,'Expect the Spanish inquisition','Nobody expects the Spanish Inquisition! \n\nOur chief weapon is surprise, surprise and fear, fear and surprise. \n\nOur two weapons are fear and surprise, and ruthless efficiency. \n\nOur three weapons are fear and surprise and ruthless efficiency and an almost fanatical dedication to the pope.','Nobody expects the Spanish Inquisition! \n\nOur chief weapon is surprise, surprise and fear, fear and surprise. \n\nOur two weapons are fear and surprise, and ruthless efficiency. \n\nOur three weapons are fear and surprise and ruthless efficiency and an almost fanatical dedication to the pope.'),(3,'Kernel Panic in the Discothek','lorem ipsum dolor sit amet','lorem ipsum dolor sit amet'); +/*!40000 ALTER TABLE `bugs_fulltext` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bz_schema` +-- + +DROP TABLE IF EXISTS `bz_schema`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bz_schema` ( + `schema_data` longblob NOT NULL, + `version` decimal(3,2) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bz_schema` +-- + +LOCK TABLES `bz_schema` WRITE; +/*!40000 ALTER TABLE `bz_schema` DISABLE KEYS */; +INSERT INTO `bz_schema` VALUES ('$VAR1 = {\n \'attach_data\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'attach_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'attachments\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'thedata\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'LONGBLOB\'\n }\n ]\n },\n \'attachments\' => {\n \'FIELDS\' => [\n \'attach_id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'creation_ts\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'modification_time\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'TINYTEXT\'\n },\n \'mimetype\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'TINYTEXT\'\n },\n \'ispatch\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'filename\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'submitter_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'isobsolete\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'isprivate\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'attachments_bug_id_idx\',\n [\n \'bug_id\'\n ],\n \'attachments_creation_ts_idx\',\n [\n \'creation_ts\'\n ],\n \'attachments_modification_time_idx\',\n [\n \'modification_time\'\n ],\n \'attachments_submitter_id_idx\',\n [\n \'submitter_id\',\n \'bug_id\'\n ]\n ]\n },\n \'audit_log\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'SET NULL\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'class\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'object_id\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'field\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'removed\',\n {\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'added\',\n {\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'at_time\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n }\n ],\n \'INDEXES\' => [\n \'audit_log_class_idx\',\n [\n \'class\',\n \'at_time\'\n ]\n ]\n },\n \'bug_group_map\' => {\n \'FIELDS\' => [\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'group_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'bug_group_map_bug_id_idx\',\n {\n \'FIELDS\' => [\n \'bug_id\',\n \'group_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'bug_group_map_group_id_idx\',\n [\n \'group_id\'\n ]\n ]\n },\n \'bug_see_also\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'class\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n }\n ],\n \'INDEXES\' => [\n \'bug_see_also_bug_id_idx\',\n {\n \'FIELDS\' => [\n \'bug_id\',\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'bug_severity\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_value_id\',\n {\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'bug_severity_value_idx\',\n {\n \'FIELDS\' => [\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'bug_severity_sortkey_idx\',\n [\n \'sortkey\',\n \'value\'\n ],\n \'bug_severity_visibility_value_id_idx\',\n [\n \'visibility_value_id\'\n ]\n ]\n },\n \'bug_status\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_value_id\',\n {\n \'TYPE\' => \'INT2\'\n },\n \'is_open\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'bug_status_value_idx\',\n {\n \'FIELDS\' => [\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'bug_status_sortkey_idx\',\n [\n \'sortkey\',\n \'value\'\n ],\n \'bug_status_visibility_value_id_idx\',\n [\n \'visibility_value_id\'\n ]\n ]\n },\n \'bug_tag\' => {\n \'FIELDS\' => [\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'tag_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'tag\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'bug_tag_bug_id_idx\',\n {\n \'FIELDS\' => [\n \'bug_id\',\n \'tag_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'bug_user_last_visit\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'last_visit_ts\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n }\n ],\n \'INDEXES\' => [\n \'bug_user_last_visit_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'bug_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'bug_user_last_visit_last_visit_ts_idx\',\n [\n \'last_visit_ts\'\n ]\n ]\n },\n \'bugs\' => {\n \'FIELDS\' => [\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'assigned_to\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_file_loc\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'bug_severity\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'bug_status\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'creation_ts\',\n {\n \'TYPE\' => \'DATETIME\'\n },\n \'delta_ts\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'short_desc\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'op_sys\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'priority\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'product_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'rep_platform\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'reporter\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'version\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'component_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'TABLE\' => \'components\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'resolution\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'target_milestone\',\n {\n \'DEFAULT\' => \'\\\'---\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'qa_contact\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'status_whiteboard\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'lastdiffed\',\n {\n \'TYPE\' => \'DATETIME\'\n },\n \'everconfirmed\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'reporter_accessible\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'cclist_accessible\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'estimated_time\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'decimal(7,2)\'\n },\n \'remaining_time\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'decimal(7,2)\'\n },\n \'deadline\',\n {\n \'TYPE\' => \'DATETIME\'\n }\n ],\n \'INDEXES\' => [\n \'bugs_assigned_to_idx\',\n [\n \'assigned_to\'\n ],\n \'bugs_creation_ts_idx\',\n [\n \'creation_ts\'\n ],\n \'bugs_delta_ts_idx\',\n [\n \'delta_ts\'\n ],\n \'bugs_bug_severity_idx\',\n [\n \'bug_severity\'\n ],\n \'bugs_bug_status_idx\',\n [\n \'bug_status\'\n ],\n \'bugs_op_sys_idx\',\n [\n \'op_sys\'\n ],\n \'bugs_priority_idx\',\n [\n \'priority\'\n ],\n \'bugs_product_id_idx\',\n [\n \'product_id\'\n ],\n \'bugs_reporter_idx\',\n [\n \'reporter\'\n ],\n \'bugs_version_idx\',\n [\n \'version\'\n ],\n \'bugs_component_id_idx\',\n [\n \'component_id\'\n ],\n \'bugs_resolution_idx\',\n [\n \'resolution\'\n ],\n \'bugs_target_milestone_idx\',\n [\n \'target_milestone\'\n ],\n \'bugs_qa_contact_idx\',\n [\n \'qa_contact\'\n ]\n ]\n },\n \'bugs_activity\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'attach_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'attach_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'attachments\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'who\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_when\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'fieldid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'TABLE\' => \'fielddefs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'added\',\n {\n \'TYPE\' => \'varchar(255)\'\n },\n \'removed\',\n {\n \'TYPE\' => \'varchar(255)\'\n },\n \'comment_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'comment_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'longdescs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT4\'\n }\n ],\n \'INDEXES\' => [\n \'bugs_activity_bug_id_idx\',\n [\n \'bug_id\'\n ],\n \'bugs_activity_who_idx\',\n [\n \'who\'\n ],\n \'bugs_activity_bug_when_idx\',\n [\n \'bug_when\'\n ],\n \'bugs_activity_fieldid_idx\',\n [\n \'fieldid\'\n ],\n \'bugs_activity_added_idx\',\n [\n \'added\'\n ],\n \'bugs_activity_removed_idx\',\n [\n \'removed\'\n ]\n ]\n },\n \'bugs_aliases\' => {\n \'FIELDS\' => [\n \'alias\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(40)\'\n },\n \'bug_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'bugs_aliases_bug_id_idx\',\n [\n \'bug_id\'\n ],\n \'bugs_aliases_alias_idx\',\n {\n \'FIELDS\' => [\n \'alias\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'bugs_fulltext\' => {\n \'FIELDS\' => [\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'short_desc\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'comments\',\n {\n \'TYPE\' => \'LONGTEXT\'\n },\n \'comments_noprivate\',\n {\n \'TYPE\' => \'LONGTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'bugs_fulltext_short_desc_idx\',\n {\n \'FIELDS\' => [\n \'short_desc\'\n ],\n \'TYPE\' => \'FULLTEXT\'\n },\n \'bugs_fulltext_comments_idx\',\n {\n \'FIELDS\' => [\n \'comments\'\n ],\n \'TYPE\' => \'FULLTEXT\'\n },\n \'bugs_fulltext_comments_noprivate_idx\',\n {\n \'FIELDS\' => [\n \'comments_noprivate\'\n ],\n \'TYPE\' => \'FULLTEXT\'\n }\n ]\n },\n \'bz_schema\' => {\n \'FIELDS\' => [\n \'schema_data\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'LONGBLOB\'\n },\n \'version\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'decimal(3,2)\'\n }\n ]\n },\n \'category_group_map\' => {\n \'FIELDS\' => [\n \'category_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'series_categories\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'group_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'category_group_map_category_id_idx\',\n {\n \'FIELDS\' => [\n \'category_id\',\n \'group_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'cc\' => {\n \'FIELDS\' => [\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'who\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'cc_bug_id_idx\',\n {\n \'FIELDS\' => [\n \'bug_id\',\n \'who\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'cc_who_idx\',\n [\n \'who\'\n ]\n ]\n },\n \'classifications\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'description\',\n {\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'classifications_name_idx\',\n {\n \'FIELDS\' => [\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'component_cc\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'component_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'components\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'component_cc_user_id_idx\',\n {\n \'FIELDS\' => [\n \'component_id\',\n \'user_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'components\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'product_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'initialowner\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'initialqacontact\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'SET NULL\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'components_product_id_idx\',\n {\n \'FIELDS\' => [\n \'product_id\',\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'components_name_idx\',\n [\n \'name\'\n ]\n ]\n },\n \'dependencies\' => {\n \'FIELDS\' => [\n \'blocked\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'dependson\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'dependencies_blocked_idx\',\n {\n \'FIELDS\' => [\n \'blocked\',\n \'dependson\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'dependencies_dependson_idx\',\n [\n \'dependson\'\n ]\n ]\n },\n \'duplicates\' => {\n \'FIELDS\' => [\n \'dupe_of\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'dupe\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ]\n },\n \'email_bug_ignore\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'email_bug_ignore_user_id_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'bug_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'email_setting\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'relationship\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n },\n \'event\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n }\n ],\n \'INDEXES\' => [\n \'email_setting_user_id_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'relationship\',\n \'event\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'field_visibility\' => {\n \'FIELDS\' => [\n \'field_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'fielddefs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'value_id\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'field_visibility_field_id_idx\',\n {\n \'FIELDS\' => [\n \'field_id\',\n \'value_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'fielddefs\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'type\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'custom\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'TINYTEXT\'\n },\n \'long_desc\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'mailhead\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'sortkey\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'obsolete\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'enter_bug\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'buglist\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_field_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'TABLE\' => \'fielddefs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'value_field_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'TABLE\' => \'fielddefs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'reverse_desc\',\n {\n \'TYPE\' => \'TINYTEXT\'\n },\n \'is_mandatory\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'is_numeric\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'fielddefs_name_idx\',\n {\n \'FIELDS\' => [\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'fielddefs_sortkey_idx\',\n [\n \'sortkey\'\n ],\n \'fielddefs_value_field_id_idx\',\n [\n \'value_field_id\'\n ],\n \'fielddefs_is_mandatory_idx\',\n [\n \'is_mandatory\'\n ]\n ]\n },\n \'flagexclusions\' => {\n \'FIELDS\' => [\n \'type_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'flagtypes\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'product_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'component_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'components\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'flagexclusions_type_id_idx\',\n {\n \'FIELDS\' => [\n \'type_id\',\n \'product_id\',\n \'component_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'flaginclusions\' => {\n \'FIELDS\' => [\n \'type_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'flagtypes\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'product_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'component_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'components\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'flaginclusions_type_id_idx\',\n {\n \'FIELDS\' => [\n \'type_id\',\n \'product_id\',\n \'component_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'flags\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'type_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'flagtypes\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'status\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'char(1)\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'attach_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'attach_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'attachments\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'creation_date\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'modification_date\',\n {\n \'TYPE\' => \'DATETIME\'\n },\n \'setter_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'requestee_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'flags_bug_id_idx\',\n [\n \'bug_id\',\n \'attach_id\'\n ],\n \'flags_setter_id_idx\',\n [\n \'setter_id\'\n ],\n \'flags_requestee_id_idx\',\n [\n \'requestee_id\'\n ],\n \'flags_type_id_idx\',\n [\n \'type_id\'\n ]\n ]\n },\n \'flagtypes\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(50)\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'cc_list\',\n {\n \'TYPE\' => \'varchar(200)\'\n },\n \'target_type\',\n {\n \'DEFAULT\' => \'\\\'b\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'char(1)\'\n },\n \'is_active\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'is_requestable\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'is_requesteeble\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'is_multiplicable\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'grant_group_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'SET NULL\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'request_group_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'SET NULL\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ]\n },\n \'group_control_map\' => {\n \'FIELDS\' => [\n \'group_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'product_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'entry\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'membercontrol\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n },\n \'othercontrol\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n },\n \'canedit\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'editcomponents\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'editbugs\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'canconfirm\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'group_control_map_product_id_idx\',\n {\n \'FIELDS\' => [\n \'product_id\',\n \'group_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'group_control_map_group_id_idx\',\n [\n \'group_id\'\n ]\n ]\n },\n \'group_group_map\' => {\n \'FIELDS\' => [\n \'member_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'grantor_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'grant_type\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n }\n ],\n \'INDEXES\' => [\n \'group_group_map_member_id_idx\',\n {\n \'FIELDS\' => [\n \'member_id\',\n \'grantor_id\',\n \'grant_type\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'groups\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'isbuggroup\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'userregexp\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'TINYTEXT\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'icon_url\',\n {\n \'TYPE\' => \'TINYTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'groups_name_idx\',\n {\n \'FIELDS\' => [\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'keyworddefs\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'keyworddefs_name_idx\',\n {\n \'FIELDS\' => [\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'keywords\' => {\n \'FIELDS\' => [\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'keywordid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'keyworddefs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'keywords_bug_id_idx\',\n {\n \'FIELDS\' => [\n \'bug_id\',\n \'keywordid\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'keywords_keywordid_idx\',\n [\n \'keywordid\'\n ]\n ]\n },\n \'login_failure\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'login_time\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'ip_addr\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(40)\'\n }\n ],\n \'INDEXES\' => [\n \'login_failure_user_id_idx\',\n [\n \'user_id\'\n ]\n ]\n },\n \'logincookies\' => {\n \'FIELDS\' => [\n \'cookie\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'varchar(16)\'\n },\n \'userid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'ipaddr\',\n {\n \'TYPE\' => \'varchar(40)\'\n },\n \'lastused\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n }\n ],\n \'INDEXES\' => [\n \'logincookies_lastused_idx\',\n [\n \'lastused\'\n ]\n ]\n },\n \'longdescs\' => {\n \'FIELDS\' => [\n \'comment_id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'who\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_when\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'work_time\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'decimal(7,2)\'\n },\n \'thetext\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'LONGTEXT\'\n },\n \'isprivate\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'already_wrapped\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'type\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'extra_data\',\n {\n \'TYPE\' => \'varchar(255)\'\n }\n ],\n \'INDEXES\' => [\n \'longdescs_bug_id_idx\',\n [\n \'bug_id\',\n \'work_time\'\n ],\n \'longdescs_who_idx\',\n [\n \'who\',\n \'bug_id\'\n ],\n \'longdescs_bug_when_idx\',\n [\n \'bug_when\'\n ]\n ]\n },\n \'longdescs_tags\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'comment_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'comment_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'longdescs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT4\'\n },\n \'tag\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(24)\'\n }\n ],\n \'INDEXES\' => [\n \'longdescs_tags_idx\',\n {\n \'FIELDS\' => [\n \'comment_id\',\n \'tag\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'longdescs_tags_activity\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'comment_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'comment_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'longdescs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT4\'\n },\n \'who\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_when\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'added\',\n {\n \'TYPE\' => \'varchar(24)\'\n },\n \'removed\',\n {\n \'TYPE\' => \'varchar(24)\'\n }\n ],\n \'INDEXES\' => [\n \'longdescs_tags_activity_bug_id_idx\',\n [\n \'bug_id\'\n ]\n ]\n },\n \'longdescs_tags_weights\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'tag\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(24)\'\n },\n \'weight\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'longdescs_tags_weights_tag_idx\',\n {\n \'FIELDS\' => [\n \'tag\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'mail_staging\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'message\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'LONGBLOB\'\n }\n ]\n },\n \'milestones\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'product_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'milestones_product_id_idx\',\n {\n \'FIELDS\' => [\n \'product_id\',\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'namedqueries\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'userid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'query\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'LONGTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'namedqueries_userid_idx\',\n {\n \'FIELDS\' => [\n \'userid\',\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'namedqueries_link_in_footer\' => {\n \'FIELDS\' => [\n \'namedquery_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'namedqueries\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'namedqueries_link_in_footer_id_idx\',\n {\n \'FIELDS\' => [\n \'namedquery_id\',\n \'user_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'namedqueries_link_in_footer_userid_idx\',\n [\n \'user_id\'\n ]\n ]\n },\n \'namedquery_group_map\' => {\n \'FIELDS\' => [\n \'namedquery_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'namedqueries\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'group_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'namedquery_group_map_namedquery_id_idx\',\n {\n \'FIELDS\' => [\n \'namedquery_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'namedquery_group_map_group_id_idx\',\n [\n \'group_id\'\n ]\n ]\n },\n \'op_sys\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_value_id\',\n {\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'op_sys_value_idx\',\n {\n \'FIELDS\' => [\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'op_sys_sortkey_idx\',\n [\n \'sortkey\',\n \'value\'\n ],\n \'op_sys_visibility_value_id_idx\',\n [\n \'visibility_value_id\'\n ]\n ]\n },\n \'priority\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_value_id\',\n {\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'priority_value_idx\',\n {\n \'FIELDS\' => [\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'priority_sortkey_idx\',\n [\n \'sortkey\',\n \'value\'\n ],\n \'priority_visibility_value_id_idx\',\n [\n \'visibility_value_id\'\n ]\n ]\n },\n \'products\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'classification_id\',\n {\n \'DEFAULT\' => \'1\',\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'classifications\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => 1,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'defaultmilestone\',\n {\n \'DEFAULT\' => \'\\\'---\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'allows_unconfirmed\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'products_name_idx\',\n {\n \'FIELDS\' => [\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'profile_search\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_list\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'list_order\',\n {\n \'TYPE\' => \'MEDIUMTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'profile_search_user_id_idx\',\n [\n \'user_id\'\n ]\n ]\n },\n \'profile_setting\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'setting_name\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'name\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'setting\',\n \'created\' => 1\n },\n \'TYPE\' => \'varchar(32)\'\n },\n \'setting_value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(32)\'\n }\n ],\n \'INDEXES\' => [\n \'profile_setting_value_unique_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'setting_name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'profiles\' => {\n \'FIELDS\' => [\n \'userid\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'login_name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'cryptpassword\',\n {\n \'TYPE\' => \'varchar(128)\'\n },\n \'realname\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'disabledtext\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'disable_mail\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'mybugslink\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'extern_id\',\n {\n \'TYPE\' => \'varchar(64)\'\n },\n \'is_enabled\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'last_seen_date\',\n {\n \'TYPE\' => \'DATETIME\'\n }\n ],\n \'INDEXES\' => [\n \'profiles_login_name_idx\',\n {\n \'FIELDS\' => [\n \'login_name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'profiles_extern_id_idx\',\n {\n \'FIELDS\' => [\n \'extern_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'profiles_activity\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'userid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'who\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'profiles_when\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'fieldid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'TABLE\' => \'fielddefs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'oldvalue\',\n {\n \'TYPE\' => \'TINYTEXT\'\n },\n \'newvalue\',\n {\n \'TYPE\' => \'TINYTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'profiles_activity_userid_idx\',\n [\n \'userid\'\n ],\n \'profiles_activity_profiles_when_idx\',\n [\n \'profiles_when\'\n ],\n \'profiles_activity_fieldid_idx\',\n [\n \'fieldid\'\n ]\n ]\n },\n \'quips\' => {\n \'FIELDS\' => [\n \'quipid\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'userid\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'SET NULL\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'quip\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(512)\'\n },\n \'approved\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ]\n },\n \'rep_platform\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_value_id\',\n {\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'rep_platform_value_idx\',\n {\n \'FIELDS\' => [\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'rep_platform_sortkey_idx\',\n [\n \'sortkey\',\n \'value\'\n ],\n \'rep_platform_visibility_value_id_idx\',\n [\n \'visibility_value_id\'\n ]\n ]\n },\n \'reports\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'query\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'LONGTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'reports_user_id_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'resolution\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_value_id\',\n {\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'resolution_value_idx\',\n {\n \'FIELDS\' => [\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'resolution_sortkey_idx\',\n [\n \'sortkey\',\n \'value\'\n ],\n \'resolution_visibility_value_id_idx\',\n [\n \'visibility_value_id\'\n ]\n ]\n },\n \'series\' => {\n \'FIELDS\' => [\n \'series_id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'creator\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'category\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'series_categories\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'subcategory\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'series_categories\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'frequency\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'query\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'is_public\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'series_creator_idx\',\n [\n \'creator\'\n ],\n \'series_category_idx\',\n {\n \'FIELDS\' => [\n \'category\',\n \'subcategory\',\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'series_categories\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n }\n ],\n \'INDEXES\' => [\n \'series_categories_name_idx\',\n {\n \'FIELDS\' => [\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'series_data\' => {\n \'FIELDS\' => [\n \'series_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'series_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'series\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'series_date\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'series_value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'series_data_series_id_idx\',\n {\n \'FIELDS\' => [\n \'series_id\',\n \'series_date\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'setting\' => {\n \'FIELDS\' => [\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'varchar(32)\'\n },\n \'default_value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(32)\'\n },\n \'is_enabled\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'subclass\',\n {\n \'TYPE\' => \'varchar(32)\'\n }\n ]\n },\n \'setting_value\' => {\n \'FIELDS\' => [\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'name\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'setting\',\n \'created\' => 1\n },\n \'TYPE\' => \'varchar(32)\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(32)\'\n },\n \'sortindex\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'setting_value_nv_unique_idx\',\n {\n \'FIELDS\' => [\n \'name\',\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'setting_value_ns_unique_idx\',\n {\n \'FIELDS\' => [\n \'name\',\n \'sortindex\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'status_workflow\' => {\n \'FIELDS\' => [\n \'old_status\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bug_status\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'new_status\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bug_status\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'require_comment\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n }\n ],\n \'INDEXES\' => [\n \'status_workflow_idx\',\n {\n \'FIELDS\' => [\n \'old_status\',\n \'new_status\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'tag\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'tag_user_id_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'tokens\' => {\n \'FIELDS\' => [\n \'userid\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'issuedate\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'token\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'varchar(16)\'\n },\n \'tokentype\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(16)\'\n },\n \'eventdata\',\n {\n \'TYPE\' => \'TINYTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'tokens_userid_idx\',\n [\n \'userid\'\n ]\n ]\n },\n \'ts_error\' => {\n \'FIELDS\' => [\n \'error_time\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'jobid\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'message\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'funcid\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n }\n ],\n \'INDEXES\' => [\n \'ts_error_funcid_idx\',\n [\n \'funcid\',\n \'error_time\'\n ],\n \'ts_error_error_time_idx\',\n [\n \'error_time\'\n ],\n \'ts_error_jobid_idx\',\n [\n \'jobid\'\n ]\n ]\n },\n \'ts_exitstatus\' => {\n \'FIELDS\' => [\n \'jobid\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'funcid\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'status\',\n {\n \'TYPE\' => \'INT2\'\n },\n \'completion_time\',\n {\n \'TYPE\' => \'INT4\'\n },\n \'delete_after\',\n {\n \'TYPE\' => \'INT4\'\n }\n ],\n \'INDEXES\' => [\n \'ts_exitstatus_funcid_idx\',\n [\n \'funcid\'\n ],\n \'ts_exitstatus_delete_after_idx\',\n [\n \'delete_after\'\n ]\n ]\n },\n \'ts_funcmap\' => {\n \'FIELDS\' => [\n \'funcid\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'funcname\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n }\n ],\n \'INDEXES\' => [\n \'ts_funcmap_funcname_idx\',\n {\n \'FIELDS\' => [\n \'funcname\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'ts_job\' => {\n \'FIELDS\' => [\n \'jobid\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'funcid\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'arg\',\n {\n \'TYPE\' => \'LONGBLOB\'\n },\n \'uniqkey\',\n {\n \'TYPE\' => \'varchar(255)\'\n },\n \'insert_time\',\n {\n \'TYPE\' => \'INT4\'\n },\n \'run_after\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'grabbed_until\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'priority\',\n {\n \'TYPE\' => \'INT2\'\n },\n \'coalesce\',\n {\n \'TYPE\' => \'varchar(255)\'\n }\n ],\n \'INDEXES\' => [\n \'ts_job_funcid_idx\',\n {\n \'FIELDS\' => [\n \'funcid\',\n \'uniqkey\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'ts_job_run_after_idx\',\n [\n \'run_after\',\n \'funcid\'\n ],\n \'ts_job_coalesce_idx\',\n [\n \'coalesce\',\n \'funcid\'\n ]\n ]\n },\n \'ts_note\' => {\n \'FIELDS\' => [\n \'jobid\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'notekey\',\n {\n \'TYPE\' => \'varchar(255)\'\n },\n \'value\',\n {\n \'TYPE\' => \'LONGBLOB\'\n }\n ],\n \'INDEXES\' => [\n \'ts_note_jobid_idx\',\n {\n \'FIELDS\' => [\n \'jobid\',\n \'notekey\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'user_api_keys\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'api_key\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'VARCHAR(40)\'\n },\n \'description\',\n {\n \'TYPE\' => \'VARCHAR(255)\'\n },\n \'revoked\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'last_used\',\n {\n \'TYPE\' => \'DATETIME\'\n }\n ],\n \'INDEXES\' => [\n \'user_api_keys_api_key_idx\',\n {\n \'FIELDS\' => [\n \'api_key\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'user_api_keys_user_id_idx\',\n [\n \'user_id\'\n ]\n ]\n },\n \'user_group_map\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'group_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'isbless\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'grant_type\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n }\n ],\n \'INDEXES\' => [\n \'user_group_map_user_id_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'group_id\',\n \'grant_type\',\n \'isbless\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'versions\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'product_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'versions_product_id_idx\',\n {\n \'FIELDS\' => [\n \'product_id\',\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'watch\' => {\n \'FIELDS\' => [\n \'watcher\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'watched\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'watch_watcher_idx\',\n {\n \'FIELDS\' => [\n \'watcher\',\n \'watched\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'watch_watched_idx\',\n [\n \'watched\'\n ]\n ]\n },\n \'whine_events\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'owner_userid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'subject\',\n {\n \'TYPE\' => \'varchar(128)\'\n },\n \'body\',\n {\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'mailifnobugs\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ]\n },\n \'whine_queries\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'eventid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'whine_events\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'query_name\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'onemailperbug\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'title\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(128)\'\n }\n ],\n \'INDEXES\' => [\n \'whine_queries_eventid_idx\',\n [\n \'eventid\'\n ]\n ]\n },\n \'whine_schedules\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'eventid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'whine_events\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'run_day\',\n {\n \'TYPE\' => \'varchar(32)\'\n },\n \'run_time\',\n {\n \'TYPE\' => \'varchar(32)\'\n },\n \'run_next\',\n {\n \'TYPE\' => \'DATETIME\'\n },\n \'mailto\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT3\'\n },\n \'mailto_type\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'whine_schedules_run_next_idx\',\n [\n \'run_next\'\n ],\n \'whine_schedules_eventid_idx\',\n [\n \'eventid\'\n ]\n ]\n }\n };\n',3.00); +/*!40000 ALTER TABLE `bz_schema` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `category_group_map` +-- + +DROP TABLE IF EXISTS `category_group_map`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `category_group_map` ( + `category_id` smallint(6) NOT NULL, + `group_id` mediumint(9) NOT NULL, + UNIQUE KEY `category_group_map_category_id_idx` (`category_id`,`group_id`), + KEY `fk_category_group_map_group_id_groups_id` (`group_id`), + CONSTRAINT `fk_category_group_map_category_id_series_categories_id` FOREIGN KEY (`category_id`) REFERENCES `series_categories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_category_group_map_group_id_groups_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `category_group_map` +-- + +LOCK TABLES `category_group_map` WRITE; +/*!40000 ALTER TABLE `category_group_map` DISABLE KEYS */; +/*!40000 ALTER TABLE `category_group_map` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `cc` +-- + +DROP TABLE IF EXISTS `cc`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `cc` ( + `bug_id` mediumint(9) NOT NULL, + `who` mediumint(9) NOT NULL, + UNIQUE KEY `cc_bug_id_idx` (`bug_id`,`who`), + KEY `cc_who_idx` (`who`), + CONSTRAINT `fk_cc_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_cc_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `cc` +-- + +LOCK TABLES `cc` WRITE; +/*!40000 ALTER TABLE `cc` DISABLE KEYS */; +INSERT INTO `cc` VALUES (2,2); +/*!40000 ALTER TABLE `cc` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `classifications` +-- + +DROP TABLE IF EXISTS `classifications`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `classifications` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `description` mediumtext DEFAULT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `classifications_name_idx` (`name`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `classifications` +-- + +LOCK TABLES `classifications` WRITE; +/*!40000 ALTER TABLE `classifications` DISABLE KEYS */; +INSERT INTO `classifications` VALUES (1,'Unclassified','Not assigned to any classification',0); +/*!40000 ALTER TABLE `classifications` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `component_cc` +-- + +DROP TABLE IF EXISTS `component_cc`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `component_cc` ( + `user_id` mediumint(9) NOT NULL, + `component_id` mediumint(9) NOT NULL, + UNIQUE KEY `component_cc_user_id_idx` (`component_id`,`user_id`), + KEY `fk_component_cc_user_id_profiles_userid` (`user_id`), + CONSTRAINT `fk_component_cc_component_id_components_id` FOREIGN KEY (`component_id`) REFERENCES `components` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_component_cc_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `component_cc` +-- + +LOCK TABLES `component_cc` WRITE; +/*!40000 ALTER TABLE `component_cc` DISABLE KEYS */; +/*!40000 ALTER TABLE `component_cc` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `components` +-- + +DROP TABLE IF EXISTS `components`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `components` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `product_id` smallint(6) NOT NULL, + `initialowner` mediumint(9) NOT NULL, + `initialqacontact` mediumint(9) DEFAULT NULL, + `description` mediumtext NOT NULL, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `components_product_id_idx` (`product_id`,`name`), + KEY `components_name_idx` (`name`), + KEY `fk_components_initialqacontact_profiles_userid` (`initialqacontact`), + KEY `fk_components_initialowner_profiles_userid` (`initialowner`), + CONSTRAINT `fk_components_initialowner_profiles_userid` FOREIGN KEY (`initialowner`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, + CONSTRAINT `fk_components_initialqacontact_profiles_userid` FOREIGN KEY (`initialqacontact`) REFERENCES `profiles` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `fk_components_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `components` +-- + +LOCK TABLES `components` WRITE; +/*!40000 ALTER TABLE `components` DISABLE KEYS */; +INSERT INTO `components` VALUES (1,'TestComponent',1,1,NULL,'This is a test component in the test product database. This ought to be blown away and replaced with real stuff in a finished installation of Bugzilla.',1),(2,'python-bugzilla',2,1,NULL,'Lorem ipsum dolor sit amet',1),(3,'Kernel',3,1,NULL,'Lorem ipsum',1),(4,'Containers',3,1,NULL,'Lorem ipsum',1); +/*!40000 ALTER TABLE `components` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `dependencies` +-- + +DROP TABLE IF EXISTS `dependencies`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `dependencies` ( + `blocked` mediumint(9) NOT NULL, + `dependson` mediumint(9) NOT NULL, + UNIQUE KEY `dependencies_blocked_idx` (`blocked`,`dependson`), + KEY `dependencies_dependson_idx` (`dependson`), + CONSTRAINT `fk_dependencies_blocked_bugs_bug_id` FOREIGN KEY (`blocked`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_dependencies_dependson_bugs_bug_id` FOREIGN KEY (`dependson`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `dependencies` +-- + +LOCK TABLES `dependencies` WRITE; +/*!40000 ALTER TABLE `dependencies` DISABLE KEYS */; +/*!40000 ALTER TABLE `dependencies` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `duplicates` +-- + +DROP TABLE IF EXISTS `duplicates`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `duplicates` ( + `dupe_of` mediumint(9) NOT NULL, + `dupe` mediumint(9) NOT NULL, + PRIMARY KEY (`dupe`), + KEY `fk_duplicates_dupe_of_bugs_bug_id` (`dupe_of`), + CONSTRAINT `fk_duplicates_dupe_bugs_bug_id` FOREIGN KEY (`dupe`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_duplicates_dupe_of_bugs_bug_id` FOREIGN KEY (`dupe_of`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `duplicates` +-- + +LOCK TABLES `duplicates` WRITE; +/*!40000 ALTER TABLE `duplicates` DISABLE KEYS */; +/*!40000 ALTER TABLE `duplicates` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `email_bug_ignore` +-- + +DROP TABLE IF EXISTS `email_bug_ignore`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `email_bug_ignore` ( + `user_id` mediumint(9) NOT NULL, + `bug_id` mediumint(9) NOT NULL, + UNIQUE KEY `email_bug_ignore_user_id_idx` (`user_id`,`bug_id`), + KEY `fk_email_bug_ignore_bug_id_bugs_bug_id` (`bug_id`), + CONSTRAINT `fk_email_bug_ignore_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_email_bug_ignore_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `email_bug_ignore` +-- + +LOCK TABLES `email_bug_ignore` WRITE; +/*!40000 ALTER TABLE `email_bug_ignore` DISABLE KEYS */; +/*!40000 ALTER TABLE `email_bug_ignore` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `email_setting` +-- + +DROP TABLE IF EXISTS `email_setting`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `email_setting` ( + `user_id` mediumint(9) NOT NULL, + `relationship` tinyint(4) NOT NULL, + `event` tinyint(4) NOT NULL, + UNIQUE KEY `email_setting_user_id_idx` (`user_id`,`relationship`,`event`), + CONSTRAINT `fk_email_setting_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `email_setting` +-- + +LOCK TABLES `email_setting` WRITE; +/*!40000 ALTER TABLE `email_setting` DISABLE KEYS */; +INSERT INTO `email_setting` VALUES (1,0,0),(1,0,1),(1,0,2),(1,0,3),(1,0,4),(1,0,5),(1,0,6),(1,0,7),(1,0,9),(1,0,10),(1,0,11),(1,0,50),(1,1,0),(1,1,1),(1,1,2),(1,1,3),(1,1,4),(1,1,5),(1,1,6),(1,1,7),(1,1,9),(1,1,10),(1,1,11),(1,1,50),(1,2,0),(1,2,1),(1,2,2),(1,2,3),(1,2,4),(1,2,5),(1,2,6),(1,2,7),(1,2,8),(1,2,9),(1,2,10),(1,2,11),(1,2,50),(1,3,0),(1,3,1),(1,3,2),(1,3,3),(1,3,4),(1,3,5),(1,3,6),(1,3,7),(1,3,9),(1,3,10),(1,3,11),(1,3,50),(1,5,0),(1,5,1),(1,5,2),(1,5,3),(1,5,4),(1,5,5),(1,5,6),(1,5,7),(1,5,9),(1,5,10),(1,5,11),(1,5,50),(1,100,100),(1,100,101),(2,0,0),(2,0,1),(2,0,2),(2,0,3),(2,0,4),(2,0,5),(2,0,6),(2,0,7),(2,0,9),(2,0,10),(2,0,11),(2,0,50),(2,1,0),(2,1,1),(2,1,2),(2,1,3),(2,1,4),(2,1,5),(2,1,6),(2,1,7),(2,1,9),(2,1,10),(2,1,11),(2,1,50),(2,2,0),(2,2,1),(2,2,2),(2,2,3),(2,2,4),(2,2,5),(2,2,6),(2,2,7),(2,2,8),(2,2,9),(2,2,10),(2,2,11),(2,2,50),(2,3,0),(2,3,1),(2,3,2),(2,3,3),(2,3,4),(2,3,5),(2,3,6),(2,3,7),(2,3,9),(2,3,10),(2,3,11),(2,3,50),(2,5,0),(2,5,1),(2,5,2),(2,5,3),(2,5,4),(2,5,5),(2,5,6),(2,5,7),(2,5,9),(2,5,10),(2,5,11),(2,5,50),(2,100,100),(2,100,101); +/*!40000 ALTER TABLE `email_setting` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `field_visibility` +-- + +DROP TABLE IF EXISTS `field_visibility`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `field_visibility` ( + `field_id` mediumint(9) DEFAULT NULL, + `value_id` smallint(6) NOT NULL, + UNIQUE KEY `field_visibility_field_id_idx` (`field_id`,`value_id`), + CONSTRAINT `fk_field_visibility_field_id_fielddefs_id` FOREIGN KEY (`field_id`) REFERENCES `fielddefs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `field_visibility` +-- + +LOCK TABLES `field_visibility` WRITE; +/*!40000 ALTER TABLE `field_visibility` DISABLE KEYS */; +/*!40000 ALTER TABLE `field_visibility` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `fielddefs` +-- + +DROP TABLE IF EXISTS `fielddefs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `fielddefs` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `type` smallint(6) NOT NULL DEFAULT 0, + `custom` tinyint(4) NOT NULL DEFAULT 0, + `description` tinytext NOT NULL, + `long_desc` varchar(255) NOT NULL DEFAULT '', + `mailhead` tinyint(4) NOT NULL DEFAULT 0, + `sortkey` smallint(6) NOT NULL, + `obsolete` tinyint(4) NOT NULL DEFAULT 0, + `enter_bug` tinyint(4) NOT NULL DEFAULT 0, + `buglist` tinyint(4) NOT NULL DEFAULT 0, + `visibility_field_id` mediumint(9) DEFAULT NULL, + `value_field_id` mediumint(9) DEFAULT NULL, + `reverse_desc` tinytext DEFAULT NULL, + `is_mandatory` tinyint(4) NOT NULL DEFAULT 0, + `is_numeric` tinyint(4) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `fielddefs_name_idx` (`name`), + KEY `fielddefs_sortkey_idx` (`sortkey`), + KEY `fielddefs_value_field_id_idx` (`value_field_id`), + KEY `fielddefs_is_mandatory_idx` (`is_mandatory`), + KEY `fk_fielddefs_visibility_field_id_fielddefs_id` (`visibility_field_id`), + CONSTRAINT `fk_fielddefs_value_field_id_fielddefs_id` FOREIGN KEY (`value_field_id`) REFERENCES `fielddefs` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_fielddefs_visibility_field_id_fielddefs_id` FOREIGN KEY (`visibility_field_id`) REFERENCES `fielddefs` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=60 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `fielddefs` +-- + +LOCK TABLES `fielddefs` WRITE; +/*!40000 ALTER TABLE `fielddefs` DISABLE KEYS */; +INSERT INTO `fielddefs` VALUES (1,'bug_id',0,0,'Bug #','',1,100,0,0,1,NULL,NULL,NULL,0,1),(2,'short_desc',0,0,'Summary','',1,200,0,0,1,NULL,NULL,NULL,1,0),(3,'classification',2,0,'Classification','',1,300,0,0,1,NULL,NULL,NULL,0,0),(4,'product',2,0,'Product','',1,400,0,0,1,NULL,NULL,NULL,1,0),(5,'version',0,0,'Version','',1,500,0,0,1,NULL,NULL,NULL,1,0),(6,'rep_platform',2,0,'Platform','',1,600,0,0,1,NULL,NULL,NULL,0,0),(7,'bug_file_loc',0,0,'URL','',1,700,0,0,1,NULL,NULL,NULL,0,0),(8,'op_sys',2,0,'OS/Version','',1,800,0,0,1,NULL,NULL,NULL,0,0),(9,'bug_status',2,0,'Status','',1,900,0,0,1,NULL,NULL,NULL,0,0),(10,'status_whiteboard',0,0,'Status Whiteboard','',1,1000,0,0,1,NULL,NULL,NULL,0,0),(11,'keywords',8,0,'Keywords','',1,1100,0,0,1,NULL,NULL,NULL,0,0),(12,'resolution',2,0,'Resolution','',0,1200,0,0,1,NULL,NULL,NULL,0,0),(13,'bug_severity',2,0,'Severity','',1,1300,0,0,1,NULL,NULL,NULL,0,0),(14,'priority',2,0,'Priority','',1,1400,0,0,1,NULL,NULL,NULL,0,0),(15,'component',2,0,'Component','',1,1500,0,0,1,NULL,NULL,NULL,1,0),(16,'assigned_to',0,0,'AssignedTo','',1,1600,0,0,1,NULL,NULL,NULL,0,0),(17,'reporter',0,0,'ReportedBy','',1,1700,0,0,1,NULL,NULL,NULL,0,0),(18,'qa_contact',0,0,'QAContact','',1,1800,0,0,1,NULL,NULL,NULL,0,0),(19,'assigned_to_realname',0,0,'AssignedToName','',0,1900,0,0,1,NULL,NULL,NULL,0,0),(20,'reporter_realname',0,0,'ReportedByName','',0,2000,0,0,1,NULL,NULL,NULL,0,0),(21,'qa_contact_realname',0,0,'QAContactName','',0,2100,0,0,1,NULL,NULL,NULL,0,0),(22,'cc',0,0,'CC','',1,2200,0,0,0,NULL,NULL,NULL,0,0),(23,'dependson',0,0,'Depends on','',1,2300,0,0,1,NULL,NULL,NULL,0,1),(24,'blocked',0,0,'Blocks','',1,2400,0,0,1,NULL,NULL,NULL,0,1),(25,'attachments.description',0,0,'Attachment description','',0,2500,0,0,0,NULL,NULL,NULL,0,0),(26,'attachments.filename',0,0,'Attachment filename','',0,2600,0,0,0,NULL,NULL,NULL,0,0),(27,'attachments.mimetype',0,0,'Attachment mime type','',0,2700,0,0,0,NULL,NULL,NULL,0,0),(28,'attachments.ispatch',0,0,'Attachment is patch','',0,2800,0,0,0,NULL,NULL,NULL,0,1),(29,'attachments.isobsolete',0,0,'Attachment is obsolete','',0,2900,0,0,0,NULL,NULL,NULL,0,1),(30,'attachments.isprivate',0,0,'Attachment is private','',0,3000,0,0,0,NULL,NULL,NULL,0,1),(31,'attachments.submitter',0,0,'Attachment creator','',0,3100,0,0,0,NULL,NULL,NULL,0,0),(32,'target_milestone',0,0,'Target Milestone','',1,3200,0,0,1,NULL,NULL,NULL,0,0),(33,'creation_ts',0,0,'Creation date','',0,3300,0,0,1,NULL,NULL,NULL,0,0),(34,'delta_ts',0,0,'Last changed date','',0,3400,0,0,1,NULL,NULL,NULL,0,0),(35,'longdesc',0,0,'Comment','',0,3500,0,0,0,NULL,NULL,NULL,0,0),(36,'longdescs.isprivate',0,0,'Comment is private','',0,3600,0,0,0,NULL,NULL,NULL,0,1),(37,'longdescs.count',0,0,'Number of Comments','',0,3700,0,0,1,NULL,NULL,NULL,0,1),(38,'alias',0,0,'Alias','',0,3800,0,0,1,NULL,NULL,NULL,0,0),(39,'everconfirmed',0,0,'Ever Confirmed','',0,3900,0,0,0,NULL,NULL,NULL,0,1),(40,'reporter_accessible',0,0,'Reporter Accessible','',0,4000,0,0,0,NULL,NULL,NULL,0,1),(41,'cclist_accessible',0,0,'CC Accessible','',0,4100,0,0,0,NULL,NULL,NULL,0,1),(42,'bug_group',0,0,'Group','',1,4200,0,0,0,NULL,NULL,NULL,0,0),(43,'estimated_time',0,0,'Estimated Hours','',1,4300,0,0,1,NULL,NULL,NULL,0,1),(44,'remaining_time',0,0,'Remaining Hours','',0,4400,0,0,1,NULL,NULL,NULL,0,1),(45,'deadline',5,0,'Deadline','',1,4500,0,0,1,NULL,NULL,NULL,0,0),(46,'commenter',0,0,'Commenter','',0,4600,0,0,0,NULL,NULL,NULL,0,0),(47,'flagtypes.name',0,0,'Flags','',0,4700,0,0,1,NULL,NULL,NULL,0,0),(48,'requestees.login_name',0,0,'Flag Requestee','',0,4800,0,0,0,NULL,NULL,NULL,0,0),(49,'setters.login_name',0,0,'Flag Setter','',0,4900,0,0,0,NULL,NULL,NULL,0,0),(50,'work_time',0,0,'Hours Worked','',0,5000,0,0,1,NULL,NULL,NULL,0,1),(51,'percentage_complete',0,0,'Percentage Complete','',0,5100,0,0,1,NULL,NULL,NULL,0,1),(52,'content',0,0,'Content','',0,5200,0,0,0,NULL,NULL,NULL,0,0),(53,'attach_data.thedata',0,0,'Attachment data','',0,5300,0,0,0,NULL,NULL,NULL,0,0),(54,'owner_idle_time',0,0,'Time Since Assignee Touched','',0,5400,0,0,0,NULL,NULL,NULL,0,0),(55,'see_also',7,0,'See Also','',0,5500,0,0,0,NULL,NULL,NULL,0,0),(56,'tag',8,0,'Personal Tags','',0,5600,0,0,1,NULL,NULL,NULL,0,0),(57,'last_visit_ts',5,0,'Last Visit','',0,5700,0,0,1,NULL,NULL,NULL,0,0),(58,'comment_tag',0,0,'Comment Tag','',0,5800,0,0,0,NULL,NULL,NULL,0,0),(59,'days_elapsed',0,0,'Days since bug changed','',0,5900,0,0,0,NULL,NULL,NULL,0,0); +/*!40000 ALTER TABLE `fielddefs` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flagexclusions` +-- + +DROP TABLE IF EXISTS `flagexclusions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `flagexclusions` ( + `type_id` mediumint(9) NOT NULL, + `product_id` smallint(6) DEFAULT NULL, + `component_id` mediumint(9) DEFAULT NULL, + UNIQUE KEY `flagexclusions_type_id_idx` (`type_id`,`product_id`,`component_id`), + KEY `fk_flagexclusions_component_id_components_id` (`component_id`), + KEY `fk_flagexclusions_product_id_products_id` (`product_id`), + CONSTRAINT `fk_flagexclusions_component_id_components_id` FOREIGN KEY (`component_id`) REFERENCES `components` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_flagexclusions_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_flagexclusions_type_id_flagtypes_id` FOREIGN KEY (`type_id`) REFERENCES `flagtypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flagexclusions` +-- + +LOCK TABLES `flagexclusions` WRITE; +/*!40000 ALTER TABLE `flagexclusions` DISABLE KEYS */; +/*!40000 ALTER TABLE `flagexclusions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flaginclusions` +-- + +DROP TABLE IF EXISTS `flaginclusions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `flaginclusions` ( + `type_id` mediumint(9) NOT NULL, + `product_id` smallint(6) DEFAULT NULL, + `component_id` mediumint(9) DEFAULT NULL, + UNIQUE KEY `flaginclusions_type_id_idx` (`type_id`,`product_id`,`component_id`), + KEY `fk_flaginclusions_component_id_components_id` (`component_id`), + KEY `fk_flaginclusions_product_id_products_id` (`product_id`), + CONSTRAINT `fk_flaginclusions_component_id_components_id` FOREIGN KEY (`component_id`) REFERENCES `components` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_flaginclusions_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_flaginclusions_type_id_flagtypes_id` FOREIGN KEY (`type_id`) REFERENCES `flagtypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flaginclusions` +-- + +LOCK TABLES `flaginclusions` WRITE; +/*!40000 ALTER TABLE `flaginclusions` DISABLE KEYS */; +INSERT INTO `flaginclusions` VALUES (1,NULL,NULL); +/*!40000 ALTER TABLE `flaginclusions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flags` +-- + +DROP TABLE IF EXISTS `flags`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `flags` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `type_id` mediumint(9) NOT NULL, + `status` char(1) NOT NULL, + `bug_id` mediumint(9) NOT NULL, + `attach_id` mediumint(9) DEFAULT NULL, + `creation_date` datetime NOT NULL, + `modification_date` datetime DEFAULT NULL, + `setter_id` mediumint(9) NOT NULL, + `requestee_id` mediumint(9) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `flags_bug_id_idx` (`bug_id`,`attach_id`), + KEY `flags_setter_id_idx` (`setter_id`), + KEY `flags_requestee_id_idx` (`requestee_id`), + KEY `flags_type_id_idx` (`type_id`), + KEY `fk_flags_attach_id_attachments_attach_id` (`attach_id`), + CONSTRAINT `fk_flags_attach_id_attachments_attach_id` FOREIGN KEY (`attach_id`) REFERENCES `attachments` (`attach_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_flags_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_flags_requestee_id_profiles_userid` FOREIGN KEY (`requestee_id`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, + CONSTRAINT `fk_flags_setter_id_profiles_userid` FOREIGN KEY (`setter_id`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, + CONSTRAINT `fk_flags_type_id_flagtypes_id` FOREIGN KEY (`type_id`) REFERENCES `flagtypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flags` +-- + +LOCK TABLES `flags` WRITE; +/*!40000 ALTER TABLE `flags` DISABLE KEYS */; +INSERT INTO `flags` VALUES (1,1,'?',2,NULL,'2024-10-15 13:29:13','2024-10-15 13:29:13',1,2); +/*!40000 ALTER TABLE `flags` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flagtypes` +-- + +DROP TABLE IF EXISTS `flagtypes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `flagtypes` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL, + `description` mediumtext NOT NULL, + `cc_list` varchar(200) DEFAULT NULL, + `target_type` char(1) NOT NULL DEFAULT 'b', + `is_active` tinyint(4) NOT NULL DEFAULT 1, + `is_requestable` tinyint(4) NOT NULL DEFAULT 0, + `is_requesteeble` tinyint(4) NOT NULL DEFAULT 0, + `is_multiplicable` tinyint(4) NOT NULL DEFAULT 0, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `grant_group_id` mediumint(9) DEFAULT NULL, + `request_group_id` mediumint(9) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `fk_flagtypes_request_group_id_groups_id` (`request_group_id`), + KEY `fk_flagtypes_grant_group_id_groups_id` (`grant_group_id`), + CONSTRAINT `fk_flagtypes_grant_group_id_groups_id` FOREIGN KEY (`grant_group_id`) REFERENCES `groups` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `fk_flagtypes_request_group_id_groups_id` FOREIGN KEY (`request_group_id`) REFERENCES `groups` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flagtypes` +-- + +LOCK TABLES `flagtypes` WRITE; +/*!40000 ALTER TABLE `flagtypes` DISABLE KEYS */; +INSERT INTO `flagtypes` VALUES (1,'needinfo','Need more Info','','b',1,1,1,1,0,NULL,NULL); +/*!40000 ALTER TABLE `flagtypes` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `group_control_map` +-- + +DROP TABLE IF EXISTS `group_control_map`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `group_control_map` ( + `group_id` mediumint(9) NOT NULL, + `product_id` smallint(6) NOT NULL, + `entry` tinyint(4) NOT NULL DEFAULT 0, + `membercontrol` tinyint(4) NOT NULL DEFAULT 0, + `othercontrol` tinyint(4) NOT NULL DEFAULT 0, + `canedit` tinyint(4) NOT NULL DEFAULT 0, + `editcomponents` tinyint(4) NOT NULL DEFAULT 0, + `editbugs` tinyint(4) NOT NULL DEFAULT 0, + `canconfirm` tinyint(4) NOT NULL DEFAULT 0, + UNIQUE KEY `group_control_map_product_id_idx` (`product_id`,`group_id`), + KEY `group_control_map_group_id_idx` (`group_id`), + CONSTRAINT `fk_group_control_map_group_id_groups_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_group_control_map_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `group_control_map` +-- + +LOCK TABLES `group_control_map` WRITE; +/*!40000 ALTER TABLE `group_control_map` DISABLE KEYS */; +/*!40000 ALTER TABLE `group_control_map` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `group_group_map` +-- + +DROP TABLE IF EXISTS `group_group_map`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `group_group_map` ( + `member_id` mediumint(9) NOT NULL, + `grantor_id` mediumint(9) NOT NULL, + `grant_type` tinyint(4) NOT NULL DEFAULT 0, + UNIQUE KEY `group_group_map_member_id_idx` (`member_id`,`grantor_id`,`grant_type`), + KEY `fk_group_group_map_grantor_id_groups_id` (`grantor_id`), + CONSTRAINT `fk_group_group_map_grantor_id_groups_id` FOREIGN KEY (`grantor_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_group_group_map_member_id_groups_id` FOREIGN KEY (`member_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `group_group_map` +-- + +LOCK TABLES `group_group_map` WRITE; +/*!40000 ALTER TABLE `group_group_map` DISABLE KEYS */; +INSERT INTO `group_group_map` VALUES (1,1,0),(1,1,1),(1,1,2),(1,2,0),(1,2,1),(1,2,2),(1,3,0),(1,3,1),(1,3,2),(1,4,0),(1,4,1),(1,4,2),(1,5,0),(1,5,1),(1,5,2),(1,6,0),(1,6,1),(1,6,2),(1,7,0),(1,7,1),(1,7,2),(1,8,0),(1,8,1),(1,8,2),(1,9,0),(1,9,1),(1,9,2),(1,10,0),(1,10,1),(1,10,2),(1,11,0),(1,11,1),(1,11,2),(8,11,0),(10,11,0),(1,12,0),(1,12,1),(1,12,2),(1,13,0),(1,13,1),(1,13,2),(12,13,0),(1,14,0),(1,14,1),(1,14,2); +/*!40000 ALTER TABLE `group_group_map` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `groups` +-- + +DROP TABLE IF EXISTS `groups`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `groups` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `description` mediumtext NOT NULL, + `isbuggroup` tinyint(4) NOT NULL, + `userregexp` tinytext NOT NULL DEFAULT '', + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `icon_url` tinytext DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `groups_name_idx` (`name`) +) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `groups` +-- + +LOCK TABLES `groups` WRITE; +/*!40000 ALTER TABLE `groups` DISABLE KEYS */; +INSERT INTO `groups` VALUES (1,'admin','Administrators',0,'',1,NULL),(2,'tweakparams','Can change Parameters',0,'',1,NULL),(3,'editusers','Can edit or disable users',0,'',1,NULL),(4,'creategroups','Can create and destroy groups',0,'',1,NULL),(5,'editclassifications','Can create, destroy, and edit classifications',0,'',1,NULL),(6,'editcomponents','Can create, destroy, and edit components',0,'',1,NULL),(7,'editkeywords','Can create, destroy, and edit keywords',0,'',1,NULL),(8,'editbugs','Can edit all bug fields',0,'.*',1,NULL),(9,'canconfirm','Can confirm a bug or mark it a duplicate',0,'',1,NULL),(10,'bz_canusewhineatothers','Can configure whine reports for other users',0,'',1,NULL),(11,'bz_canusewhines','User can configure whine reports for self',0,'',1,NULL),(12,'bz_sudoers','Can perform actions as other users',0,'',1,NULL),(13,'bz_sudo_protect','Can not be impersonated by other users',0,'',1,NULL),(14,'bz_quip_moderators','Can moderate quips',0,'',1,NULL); +/*!40000 ALTER TABLE `groups` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `keyworddefs` +-- + +DROP TABLE IF EXISTS `keyworddefs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `keyworddefs` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `description` mediumtext NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `keyworddefs_name_idx` (`name`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `keyworddefs` +-- + +LOCK TABLES `keyworddefs` WRITE; +/*!40000 ALTER TABLE `keyworddefs` DISABLE KEYS */; +INSERT INTO `keyworddefs` VALUES (1,'FooBar','This needs no explanation'),(2,'LoremIpsum','dolor sit amet ...'); +/*!40000 ALTER TABLE `keyworddefs` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `keywords` +-- + +DROP TABLE IF EXISTS `keywords`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `keywords` ( + `bug_id` mediumint(9) NOT NULL, + `keywordid` smallint(6) NOT NULL, + UNIQUE KEY `keywords_bug_id_idx` (`bug_id`,`keywordid`), + KEY `keywords_keywordid_idx` (`keywordid`), + CONSTRAINT `fk_keywords_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_keywords_keywordid_keyworddefs_id` FOREIGN KEY (`keywordid`) REFERENCES `keyworddefs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `keywords` +-- + +LOCK TABLES `keywords` WRITE; +/*!40000 ALTER TABLE `keywords` DISABLE KEYS */; +INSERT INTO `keywords` VALUES (2,1); +/*!40000 ALTER TABLE `keywords` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `login_failure` +-- + +DROP TABLE IF EXISTS `login_failure`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `login_failure` ( + `user_id` mediumint(9) NOT NULL, + `login_time` datetime NOT NULL, + `ip_addr` varchar(40) NOT NULL, + KEY `login_failure_user_id_idx` (`user_id`), + CONSTRAINT `fk_login_failure_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `login_failure` +-- + +LOCK TABLES `login_failure` WRITE; +/*!40000 ALTER TABLE `login_failure` DISABLE KEYS */; +/*!40000 ALTER TABLE `login_failure` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `logincookies` +-- + +DROP TABLE IF EXISTS `logincookies`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `logincookies` ( + `cookie` varchar(16) NOT NULL, + `userid` mediumint(9) NOT NULL, + `ipaddr` varchar(40) DEFAULT NULL, + `lastused` datetime NOT NULL, + PRIMARY KEY (`cookie`), + KEY `logincookies_lastused_idx` (`lastused`), + KEY `fk_logincookies_userid_profiles_userid` (`userid`), + CONSTRAINT `fk_logincookies_userid_profiles_userid` FOREIGN KEY (`userid`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `logincookies` +-- + +LOCK TABLES `logincookies` WRITE; +/*!40000 ALTER TABLE `logincookies` DISABLE KEYS */; +INSERT INTO `logincookies` VALUES ('StQdHXDOZ2',1,NULL,'2024-10-15 14:02:53'); +/*!40000 ALTER TABLE `logincookies` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `longdescs` +-- + +DROP TABLE IF EXISTS `longdescs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `longdescs` ( + `comment_id` int(11) NOT NULL AUTO_INCREMENT, + `bug_id` mediumint(9) NOT NULL, + `who` mediumint(9) NOT NULL, + `bug_when` datetime NOT NULL, + `work_time` decimal(7,2) NOT NULL DEFAULT 0.00, + `thetext` mediumtext NOT NULL, + `isprivate` tinyint(4) NOT NULL DEFAULT 0, + `already_wrapped` tinyint(4) NOT NULL DEFAULT 0, + `type` smallint(6) NOT NULL DEFAULT 0, + `extra_data` varchar(255) DEFAULT NULL, + PRIMARY KEY (`comment_id`), + KEY `longdescs_bug_id_idx` (`bug_id`,`work_time`), + KEY `longdescs_who_idx` (`who`,`bug_id`), + KEY `longdescs_bug_when_idx` (`bug_when`), + CONSTRAINT `fk_longdescs_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_longdescs_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `longdescs` +-- + +LOCK TABLES `longdescs` WRITE; +/*!40000 ALTER TABLE `longdescs` DISABLE KEYS */; +INSERT INTO `longdescs` VALUES (1,1,1,'2023-11-27 15:35:33',0.00,'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\nAt vero eos et accusam et justo duo dolores et ea rebum.',0,0,0,NULL),(2,1,1,'2023-11-27 15:37:05',0.00,'Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.',0,0,0,NULL),(3,2,1,'2023-11-27 15:38:45',0.00,'Nobody expects the Spanish Inquisition! \n\nOur chief weapon is surprise, surprise and fear, fear and surprise. \n\nOur two weapons are fear and surprise, and ruthless efficiency. \n\nOur three weapons are fear and surprise and ruthless efficiency and an almost fanatical dedication to the pope.',0,0,0,NULL),(4,3,1,'2024-10-15 13:45:40',0.00,'lorem ipsum dolor sit amet',0,0,0,NULL); +/*!40000 ALTER TABLE `longdescs` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `longdescs_tags` +-- + +DROP TABLE IF EXISTS `longdescs_tags`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `longdescs_tags` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `comment_id` int(11) DEFAULT NULL, + `tag` varchar(24) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `longdescs_tags_idx` (`comment_id`,`tag`), + CONSTRAINT `fk_longdescs_tags_comment_id_longdescs_comment_id` FOREIGN KEY (`comment_id`) REFERENCES `longdescs` (`comment_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `longdescs_tags` +-- + +LOCK TABLES `longdescs_tags` WRITE; +/*!40000 ALTER TABLE `longdescs_tags` DISABLE KEYS */; +/*!40000 ALTER TABLE `longdescs_tags` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `longdescs_tags_activity` +-- + +DROP TABLE IF EXISTS `longdescs_tags_activity`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `longdescs_tags_activity` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `bug_id` mediumint(9) NOT NULL, + `comment_id` int(11) DEFAULT NULL, + `who` mediumint(9) NOT NULL, + `bug_when` datetime NOT NULL, + `added` varchar(24) DEFAULT NULL, + `removed` varchar(24) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `longdescs_tags_activity_bug_id_idx` (`bug_id`), + KEY `fk_longdescs_tags_activity_comment_id_longdescs_comment_id` (`comment_id`), + KEY `fk_longdescs_tags_activity_who_profiles_userid` (`who`), + CONSTRAINT `fk_longdescs_tags_activity_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_longdescs_tags_activity_comment_id_longdescs_comment_id` FOREIGN KEY (`comment_id`) REFERENCES `longdescs` (`comment_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_longdescs_tags_activity_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `longdescs_tags_activity` +-- + +LOCK TABLES `longdescs_tags_activity` WRITE; +/*!40000 ALTER TABLE `longdescs_tags_activity` DISABLE KEYS */; +/*!40000 ALTER TABLE `longdescs_tags_activity` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `longdescs_tags_weights` +-- + +DROP TABLE IF EXISTS `longdescs_tags_weights`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `longdescs_tags_weights` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `tag` varchar(24) NOT NULL, + `weight` mediumint(9) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `longdescs_tags_weights_tag_idx` (`tag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `longdescs_tags_weights` +-- + +LOCK TABLES `longdescs_tags_weights` WRITE; +/*!40000 ALTER TABLE `longdescs_tags_weights` DISABLE KEYS */; +/*!40000 ALTER TABLE `longdescs_tags_weights` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `mail_staging` +-- + +DROP TABLE IF EXISTS `mail_staging`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `mail_staging` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `message` longblob NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `mail_staging` +-- + +LOCK TABLES `mail_staging` WRITE; +/*!40000 ALTER TABLE `mail_staging` DISABLE KEYS */; +/*!40000 ALTER TABLE `mail_staging` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `milestones` +-- + +DROP TABLE IF EXISTS `milestones`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `milestones` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `product_id` smallint(6) NOT NULL, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `milestones_product_id_idx` (`product_id`,`value`), + CONSTRAINT `fk_milestones_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `milestones` +-- + +LOCK TABLES `milestones` WRITE; +/*!40000 ALTER TABLE `milestones` DISABLE KEYS */; +INSERT INTO `milestones` VALUES (1,1,'---',0,1),(2,2,'---',0,1),(3,3,'---',0,1); +/*!40000 ALTER TABLE `milestones` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `namedqueries` +-- + +DROP TABLE IF EXISTS `namedqueries`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `namedqueries` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `userid` mediumint(9) NOT NULL, + `name` varchar(64) NOT NULL, + `query` mediumtext NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `namedqueries_userid_idx` (`userid`,`name`), + CONSTRAINT `fk_namedqueries_userid_profiles_userid` FOREIGN KEY (`userid`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `namedqueries` +-- + +LOCK TABLES `namedqueries` WRITE; +/*!40000 ALTER TABLE `namedqueries` DISABLE KEYS */; +/*!40000 ALTER TABLE `namedqueries` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `namedqueries_link_in_footer` +-- + +DROP TABLE IF EXISTS `namedqueries_link_in_footer`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `namedqueries_link_in_footer` ( + `namedquery_id` mediumint(9) NOT NULL, + `user_id` mediumint(9) NOT NULL, + UNIQUE KEY `namedqueries_link_in_footer_id_idx` (`namedquery_id`,`user_id`), + KEY `namedqueries_link_in_footer_userid_idx` (`user_id`), + CONSTRAINT `fk_namedqueries_link_in_footer_namedquery_id_namedqueries_id` FOREIGN KEY (`namedquery_id`) REFERENCES `namedqueries` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_namedqueries_link_in_footer_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `namedqueries_link_in_footer` +-- + +LOCK TABLES `namedqueries_link_in_footer` WRITE; +/*!40000 ALTER TABLE `namedqueries_link_in_footer` DISABLE KEYS */; +/*!40000 ALTER TABLE `namedqueries_link_in_footer` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `namedquery_group_map` +-- + +DROP TABLE IF EXISTS `namedquery_group_map`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `namedquery_group_map` ( + `namedquery_id` mediumint(9) NOT NULL, + `group_id` mediumint(9) NOT NULL, + UNIQUE KEY `namedquery_group_map_namedquery_id_idx` (`namedquery_id`), + KEY `namedquery_group_map_group_id_idx` (`group_id`), + CONSTRAINT `fk_namedquery_group_map_group_id_groups_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_namedquery_group_map_namedquery_id_namedqueries_id` FOREIGN KEY (`namedquery_id`) REFERENCES `namedqueries` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `namedquery_group_map` +-- + +LOCK TABLES `namedquery_group_map` WRITE; +/*!40000 ALTER TABLE `namedquery_group_map` DISABLE KEYS */; +/*!40000 ALTER TABLE `namedquery_group_map` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `op_sys` +-- + +DROP TABLE IF EXISTS `op_sys`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `op_sys` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `visibility_value_id` smallint(6) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `op_sys_value_idx` (`value`), + KEY `op_sys_sortkey_idx` (`sortkey`,`value`), + KEY `op_sys_visibility_value_id_idx` (`visibility_value_id`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `op_sys` +-- + +LOCK TABLES `op_sys` WRITE; +/*!40000 ALTER TABLE `op_sys` DISABLE KEYS */; +INSERT INTO `op_sys` VALUES (1,'All',100,1,NULL),(2,'Windows',200,1,NULL),(3,'Mac OS',300,1,NULL),(4,'Linux',400,1,NULL),(5,'Other',500,1,NULL); +/*!40000 ALTER TABLE `op_sys` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `priority` +-- + +DROP TABLE IF EXISTS `priority`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `priority` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `visibility_value_id` smallint(6) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `priority_value_idx` (`value`), + KEY `priority_sortkey_idx` (`sortkey`,`value`), + KEY `priority_visibility_value_id_idx` (`visibility_value_id`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `priority` +-- + +LOCK TABLES `priority` WRITE; +/*!40000 ALTER TABLE `priority` DISABLE KEYS */; +INSERT INTO `priority` VALUES (1,'Highest',100,1,NULL),(2,'High',200,1,NULL),(3,'Normal',300,1,NULL),(4,'Low',400,1,NULL),(5,'Lowest',500,1,NULL),(6,'---',600,1,NULL); +/*!40000 ALTER TABLE `priority` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `products` +-- + +DROP TABLE IF EXISTS `products`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `products` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `classification_id` smallint(6) NOT NULL DEFAULT 1, + `description` mediumtext NOT NULL, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `defaultmilestone` varchar(64) NOT NULL DEFAULT '---', + `allows_unconfirmed` tinyint(4) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `products_name_idx` (`name`), + KEY `fk_products_classification_id_classifications_id` (`classification_id`), + CONSTRAINT `fk_products_classification_id_classifications_id` FOREIGN KEY (`classification_id`) REFERENCES `classifications` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `products` +-- + +LOCK TABLES `products` WRITE; +/*!40000 ALTER TABLE `products` DISABLE KEYS */; +INSERT INTO `products` VALUES (1,'TestProduct',1,'This is a test product. This ought to be blown away and replaced with real stuff in a finished installation of bugzilla.',1,'---',1),(2,'Red Hat Enterprise Linux 9',1,'Lorem ipsum',1,'---',1),(3,'SUSE Linux Enterprise Server 15 SP6',1,'Lorem ipsum dolor sit amet',1,'---',1); +/*!40000 ALTER TABLE `products` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `profile_search` +-- + +DROP TABLE IF EXISTS `profile_search`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `profile_search` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` mediumint(9) NOT NULL, + `bug_list` mediumtext NOT NULL, + `list_order` mediumtext DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `profile_search_user_id_idx` (`user_id`), + CONSTRAINT `fk_profile_search_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `profile_search` +-- + +LOCK TABLES `profile_search` WRITE; +/*!40000 ALTER TABLE `profile_search` DISABLE KEYS */; +INSERT INTO `profile_search` VALUES (1,1,'1','bug_status,priority,assigned_to,bug_id'),(2,1,'1,2','priority,bug_severity'),(3,1,'2','bug_status,priority,assigned_to,bug_id'); +/*!40000 ALTER TABLE `profile_search` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `profile_setting` +-- + +DROP TABLE IF EXISTS `profile_setting`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `profile_setting` ( + `user_id` mediumint(9) NOT NULL, + `setting_name` varchar(32) NOT NULL, + `setting_value` varchar(32) NOT NULL, + UNIQUE KEY `profile_setting_value_unique_idx` (`user_id`,`setting_name`), + KEY `fk_profile_setting_setting_name_setting_name` (`setting_name`), + CONSTRAINT `fk_profile_setting_setting_name_setting_name` FOREIGN KEY (`setting_name`) REFERENCES `setting` (`name`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_profile_setting_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `profile_setting` +-- + +LOCK TABLES `profile_setting` WRITE; +/*!40000 ALTER TABLE `profile_setting` DISABLE KEYS */; +/*!40000 ALTER TABLE `profile_setting` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `profiles` +-- + +DROP TABLE IF EXISTS `profiles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `profiles` ( + `userid` mediumint(9) NOT NULL AUTO_INCREMENT, + `login_name` varchar(255) NOT NULL, + `cryptpassword` varchar(128) DEFAULT NULL, + `realname` varchar(255) NOT NULL DEFAULT '', + `disabledtext` mediumtext NOT NULL DEFAULT '', + `disable_mail` tinyint(4) NOT NULL DEFAULT 0, + `mybugslink` tinyint(4) NOT NULL DEFAULT 1, + `extern_id` varchar(64) DEFAULT NULL, + `is_enabled` tinyint(4) NOT NULL DEFAULT 1, + `last_seen_date` datetime DEFAULT NULL, + PRIMARY KEY (`userid`), + UNIQUE KEY `profiles_login_name_idx` (`login_name`), + UNIQUE KEY `profiles_extern_id_idx` (`extern_id`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `profiles` +-- + +LOCK TABLES `profiles` WRITE; +/*!40000 ALTER TABLE `profiles` DISABLE KEYS */; +INSERT INTO `profiles` VALUES (1,'andreas@hasenkopf.xyz','2207pp7o,ialUTtf7x78ge5SbbN7+W+1lXGJBXmMlYt26C1egd4g{SHA-256}','Andreas','',0,1,NULL,1,'2024-10-15 00:00:00'),(2,'nemo@example.com','rimPrF6O,Y0jPDDD1IeOR5myBbCCkt5rW36hOlVe7k/IH8wG513Y{SHA-256}','Nemo','',1,1,NULL,1,NULL); +/*!40000 ALTER TABLE `profiles` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `profiles_activity` +-- + +DROP TABLE IF EXISTS `profiles_activity`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `profiles_activity` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `userid` mediumint(9) NOT NULL, + `who` mediumint(9) NOT NULL, + `profiles_when` datetime NOT NULL, + `fieldid` mediumint(9) NOT NULL, + `oldvalue` tinytext DEFAULT NULL, + `newvalue` tinytext DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `profiles_activity_userid_idx` (`userid`), + KEY `profiles_activity_profiles_when_idx` (`profiles_when`), + KEY `profiles_activity_fieldid_idx` (`fieldid`), + KEY `fk_profiles_activity_who_profiles_userid` (`who`), + CONSTRAINT `fk_profiles_activity_fieldid_fielddefs_id` FOREIGN KEY (`fieldid`) REFERENCES `fielddefs` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_profiles_activity_userid_profiles_userid` FOREIGN KEY (`userid`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_profiles_activity_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `profiles_activity` +-- + +LOCK TABLES `profiles_activity` WRITE; +/*!40000 ALTER TABLE `profiles_activity` DISABLE KEYS */; +INSERT INTO `profiles_activity` VALUES (1,1,1,'2023-09-20 13:12:55',33,NULL,'2023-09-20 13:12:55'),(2,2,1,'2024-10-15 13:28:58',33,NULL,'2024-10-15 13:28:58'); +/*!40000 ALTER TABLE `profiles_activity` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `quips` +-- + +DROP TABLE IF EXISTS `quips`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `quips` ( + `quipid` mediumint(9) NOT NULL AUTO_INCREMENT, + `userid` mediumint(9) DEFAULT NULL, + `quip` varchar(512) NOT NULL, + `approved` tinyint(4) NOT NULL DEFAULT 1, + PRIMARY KEY (`quipid`), + KEY `fk_quips_userid_profiles_userid` (`userid`), + CONSTRAINT `fk_quips_userid_profiles_userid` FOREIGN KEY (`userid`) REFERENCES `profiles` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `quips` +-- + +LOCK TABLES `quips` WRITE; +/*!40000 ALTER TABLE `quips` DISABLE KEYS */; +/*!40000 ALTER TABLE `quips` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `rep_platform` +-- + +DROP TABLE IF EXISTS `rep_platform`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `rep_platform` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `visibility_value_id` smallint(6) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `rep_platform_value_idx` (`value`), + KEY `rep_platform_sortkey_idx` (`sortkey`,`value`), + KEY `rep_platform_visibility_value_id_idx` (`visibility_value_id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `rep_platform` +-- + +LOCK TABLES `rep_platform` WRITE; +/*!40000 ALTER TABLE `rep_platform` DISABLE KEYS */; +INSERT INTO `rep_platform` VALUES (1,'All',100,1,NULL),(2,'PC',200,1,NULL),(3,'Macintosh',300,1,NULL),(4,'Other',400,1,NULL); +/*!40000 ALTER TABLE `rep_platform` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `reports` +-- + +DROP TABLE IF EXISTS `reports`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `reports` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `user_id` mediumint(9) NOT NULL, + `name` varchar(64) NOT NULL, + `query` mediumtext NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `reports_user_id_idx` (`user_id`,`name`), + CONSTRAINT `fk_reports_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `reports` +-- + +LOCK TABLES `reports` WRITE; +/*!40000 ALTER TABLE `reports` DISABLE KEYS */; +/*!40000 ALTER TABLE `reports` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `resolution` +-- + +DROP TABLE IF EXISTS `resolution`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `resolution` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `visibility_value_id` smallint(6) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `resolution_value_idx` (`value`), + KEY `resolution_sortkey_idx` (`sortkey`,`value`), + KEY `resolution_visibility_value_id_idx` (`visibility_value_id`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `resolution` +-- + +LOCK TABLES `resolution` WRITE; +/*!40000 ALTER TABLE `resolution` DISABLE KEYS */; +INSERT INTO `resolution` VALUES (1,'',100,1,NULL),(2,'FIXED',200,1,NULL),(3,'INVALID',300,1,NULL),(4,'WONTFIX',400,1,NULL),(5,'DUPLICATE',500,1,NULL),(6,'WORKSFORME',600,1,NULL); +/*!40000 ALTER TABLE `resolution` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `series` +-- + +DROP TABLE IF EXISTS `series`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `series` ( + `series_id` mediumint(9) NOT NULL AUTO_INCREMENT, + `creator` mediumint(9) DEFAULT NULL, + `category` smallint(6) NOT NULL, + `subcategory` smallint(6) NOT NULL, + `name` varchar(64) NOT NULL, + `frequency` smallint(6) NOT NULL, + `query` mediumtext NOT NULL, + `is_public` tinyint(4) NOT NULL DEFAULT 0, + PRIMARY KEY (`series_id`), + UNIQUE KEY `series_category_idx` (`category`,`subcategory`,`name`), + KEY `series_creator_idx` (`creator`), + KEY `fk_series_subcategory_series_categories_id` (`subcategory`), + CONSTRAINT `fk_series_category_series_categories_id` FOREIGN KEY (`category`) REFERENCES `series_categories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_series_creator_profiles_userid` FOREIGN KEY (`creator`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_series_subcategory_series_categories_id` FOREIGN KEY (`subcategory`) REFERENCES `series_categories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `series` +-- + +LOCK TABLES `series` WRITE; +/*!40000 ALTER TABLE `series` DISABLE KEYS */; +INSERT INTO `series` VALUES (1,1,1,2,'UNCONFIRMED',1,'bug_status=UNCONFIRMED&product=Red%20Hat%20Enterprise%20Linux%209',1),(2,1,1,2,'CONFIRMED',1,'bug_status=CONFIRMED&product=Red%20Hat%20Enterprise%20Linux%209',1),(3,1,1,2,'IN_PROGRESS',1,'bug_status=IN_PROGRESS&product=Red%20Hat%20Enterprise%20Linux%209',1),(4,1,1,2,'RESOLVED',1,'bug_status=RESOLVED&product=Red%20Hat%20Enterprise%20Linux%209',1),(5,1,1,2,'VERIFIED',1,'bug_status=VERIFIED&product=Red%20Hat%20Enterprise%20Linux%209',1),(6,1,1,2,'FIXED',1,'resolution=FIXED&product=Red%20Hat%20Enterprise%20Linux%209',1),(7,1,1,2,'INVALID',1,'resolution=INVALID&product=Red%20Hat%20Enterprise%20Linux%209',1),(8,1,1,2,'WONTFIX',1,'resolution=WONTFIX&product=Red%20Hat%20Enterprise%20Linux%209',1),(9,1,1,2,'DUPLICATE',1,'resolution=DUPLICATE&product=Red%20Hat%20Enterprise%20Linux%209',1),(10,1,1,2,'WORKSFORME',1,'resolution=WORKSFORME&product=Red%20Hat%20Enterprise%20Linux%209',1),(11,1,1,2,'All Open',1,'bug_status=UNCONFIRMED&bug_status=CONFIRMED&bug_status=IN_PROGRESS&product=Red%20Hat%20Enterprise%20Linux%209',1),(12,1,1,3,'All Open',1,'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.&product=Red%20Hat%20Enterprise%20Linux%209&component=python-bugzilla',1),(13,1,1,3,'All Closed',1,'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.&product=Red%20Hat%20Enterprise%20Linux%209&component=python-bugzilla',1),(14,1,4,2,'UNCONFIRMED',1,'bug_status=UNCONFIRMED&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(15,1,4,2,'CONFIRMED',1,'bug_status=CONFIRMED&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(16,1,4,2,'IN_PROGRESS',1,'bug_status=IN_PROGRESS&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(17,1,4,2,'RESOLVED',1,'bug_status=RESOLVED&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(18,1,4,2,'VERIFIED',1,'bug_status=VERIFIED&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(19,1,4,2,'FIXED',1,'resolution=FIXED&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(20,1,4,2,'INVALID',1,'resolution=INVALID&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(21,1,4,2,'WONTFIX',1,'resolution=WONTFIX&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(22,1,4,2,'DUPLICATE',1,'resolution=DUPLICATE&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(23,1,4,2,'WORKSFORME',1,'resolution=WORKSFORME&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(24,1,4,2,'All Open',1,'bug_status=UNCONFIRMED&bug_status=CONFIRMED&bug_status=IN_PROGRESS&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(25,1,4,5,'All Open',1,'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6&component=Kernel',1),(26,1,4,5,'All Closed',1,'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6&component=Kernel',1),(27,1,4,6,'All Open',1,'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6&component=Containers',1),(28,1,4,6,'All Closed',1,'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6&component=Containers',1); +/*!40000 ALTER TABLE `series` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `series_categories` +-- + +DROP TABLE IF EXISTS `series_categories`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `series_categories` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `series_categories_name_idx` (`name`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `series_categories` +-- + +LOCK TABLES `series_categories` WRITE; +/*!40000 ALTER TABLE `series_categories` DISABLE KEYS */; +INSERT INTO `series_categories` VALUES (2,'-All-'),(6,'Containers'),(5,'Kernel'),(3,'python-bugzilla'),(1,'Red Hat Enterprise Linux 9'),(4,'SUSE Linux Enterprise Server 15 SP6'); +/*!40000 ALTER TABLE `series_categories` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `series_data` +-- + +DROP TABLE IF EXISTS `series_data`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `series_data` ( + `series_id` mediumint(9) NOT NULL, + `series_date` datetime NOT NULL, + `series_value` mediumint(9) NOT NULL, + UNIQUE KEY `series_data_series_id_idx` (`series_id`,`series_date`), + CONSTRAINT `fk_series_data_series_id_series_series_id` FOREIGN KEY (`series_id`) REFERENCES `series` (`series_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `series_data` +-- + +LOCK TABLES `series_data` WRITE; +/*!40000 ALTER TABLE `series_data` DISABLE KEYS */; +/*!40000 ALTER TABLE `series_data` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `setting` +-- + +DROP TABLE IF EXISTS `setting`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `setting` ( + `name` varchar(32) NOT NULL, + `default_value` varchar(32) NOT NULL, + `is_enabled` tinyint(4) NOT NULL DEFAULT 1, + `subclass` varchar(32) DEFAULT NULL, + PRIMARY KEY (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `setting` +-- + +LOCK TABLES `setting` WRITE; +/*!40000 ALTER TABLE `setting` DISABLE KEYS */; +INSERT INTO `setting` VALUES ('bugmail_new_prefix','on',1,NULL),('comment_box_position','before_comments',1,NULL),('comment_sort_order','oldest_to_newest',1,NULL),('csv_colsepchar',',',1,NULL),('display_quips','on',1,NULL),('email_format','html',1,NULL),('lang','en',1,'Lang'),('possible_duplicates','on',1,NULL),('post_bug_submit_action','next_bug',1,NULL),('quicksearch_fulltext','on',1,NULL),('quote_replies','quoted_reply',1,NULL),('requestee_cc','on',1,NULL),('skin','Dusk',1,'Skin'),('state_addselfcc','cc_unless_role',1,NULL),('timezone','local',1,'Timezone'),('zoom_textareas','on',1,NULL); +/*!40000 ALTER TABLE `setting` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `setting_value` +-- + +DROP TABLE IF EXISTS `setting_value`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `setting_value` ( + `name` varchar(32) NOT NULL, + `value` varchar(32) NOT NULL, + `sortindex` smallint(6) NOT NULL, + UNIQUE KEY `setting_value_nv_unique_idx` (`name`,`value`), + UNIQUE KEY `setting_value_ns_unique_idx` (`name`,`sortindex`), + CONSTRAINT `fk_setting_value_name_setting_name` FOREIGN KEY (`name`) REFERENCES `setting` (`name`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `setting_value` +-- + +LOCK TABLES `setting_value` WRITE; +/*!40000 ALTER TABLE `setting_value` DISABLE KEYS */; +INSERT INTO `setting_value` VALUES ('bugmail_new_prefix','on',5),('bugmail_new_prefix','off',10),('comment_box_position','before_comments',5),('comment_box_position','after_comments',10),('comment_sort_order','oldest_to_newest',5),('comment_sort_order','newest_to_oldest',10),('comment_sort_order','newest_to_oldest_desc_first',15),('csv_colsepchar',',',5),('csv_colsepchar',';',10),('display_quips','on',5),('display_quips','off',10),('email_format','html',5),('email_format','text_only',10),('possible_duplicates','on',5),('possible_duplicates','off',10),('post_bug_submit_action','next_bug',5),('post_bug_submit_action','same_bug',10),('post_bug_submit_action','nothing',15),('quicksearch_fulltext','on',5),('quicksearch_fulltext','off',10),('quote_replies','quoted_reply',5),('quote_replies','simple_reply',10),('quote_replies','off',15),('requestee_cc','on',5),('requestee_cc','off',10),('state_addselfcc','always',5),('state_addselfcc','never',10),('state_addselfcc','cc_unless_role',15),('zoom_textareas','on',5),('zoom_textareas','off',10); +/*!40000 ALTER TABLE `setting_value` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `status_workflow` +-- + +DROP TABLE IF EXISTS `status_workflow`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `status_workflow` ( + `old_status` smallint(6) DEFAULT NULL, + `new_status` smallint(6) NOT NULL, + `require_comment` tinyint(4) NOT NULL DEFAULT 0, + UNIQUE KEY `status_workflow_idx` (`old_status`,`new_status`), + KEY `fk_status_workflow_new_status_bug_status_id` (`new_status`), + CONSTRAINT `fk_status_workflow_new_status_bug_status_id` FOREIGN KEY (`new_status`) REFERENCES `bug_status` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_status_workflow_old_status_bug_status_id` FOREIGN KEY (`old_status`) REFERENCES `bug_status` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `status_workflow` +-- + +LOCK TABLES `status_workflow` WRITE; +/*!40000 ALTER TABLE `status_workflow` DISABLE KEYS */; +INSERT INTO `status_workflow` VALUES (NULL,1,0),(NULL,2,0),(NULL,3,0),(1,2,0),(1,3,0),(1,4,0),(2,3,0),(2,4,0),(3,2,0),(3,4,0),(4,1,0),(4,2,0),(4,5,0),(5,1,0),(5,2,0); +/*!40000 ALTER TABLE `status_workflow` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `tag` +-- + +DROP TABLE IF EXISTS `tag`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `tag` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `user_id` mediumint(9) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `tag_user_id_idx` (`user_id`,`name`), + CONSTRAINT `fk_tag_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `tag` +-- + +LOCK TABLES `tag` WRITE; +/*!40000 ALTER TABLE `tag` DISABLE KEYS */; +/*!40000 ALTER TABLE `tag` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `tokens` +-- + +DROP TABLE IF EXISTS `tokens`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `tokens` ( + `userid` mediumint(9) DEFAULT NULL, + `issuedate` datetime NOT NULL, + `token` varchar(16) NOT NULL, + `tokentype` varchar(16) NOT NULL, + `eventdata` tinytext DEFAULT NULL, + PRIMARY KEY (`token`), + KEY `tokens_userid_idx` (`userid`), + CONSTRAINT `fk_tokens_userid_profiles_userid` FOREIGN KEY (`userid`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `tokens` +-- + +LOCK TABLES `tokens` WRITE; +/*!40000 ALTER TABLE `tokens` DISABLE KEYS */; +INSERT INTO `tokens` VALUES (1,'2023-11-27 15:46:15','5HVJhRRo6t','session','edit_parameters'),(1,'2024-10-15 13:06:14','5NG9DysR5W','session','edit_parameters'),(1,'2024-10-15 13:10:16','6m73C0nqfo','session','edit_parameters'),(1,'2024-10-15 13:10:09','7RlXVAQiOb','session','edit_parameters'),(1,'2023-11-27 12:25:54','a9MgwT7N7x','session','edit_product'),(1,'2024-10-15 13:27:02','bSVcXqgap4','session','edit_flagtype'),(1,'2024-10-15 13:06:09','BWsu8P8e2D','session','edit_parameters'),(1,'2023-11-27 15:42:50','CRSwDhzaXc','session','edit_parameters'),(1,'2024-10-15 14:02:08','dAVlRMDOg7','session','edit_component'),(1,'2023-11-27 12:29:18','DXFuAIZ5GH','session','edit_product'),(1,'2023-09-20 13:13:14','ery9F3ZaAV','session','edit_user_prefs'),(1,'2024-10-15 12:46:48','gEsxMu9BHz','api_token',''),(1,'2023-11-27 15:44:26','gnPazrbni2','session','edit_product'),(1,'2023-11-27 15:43:10','GZT1mYgIAF','session','edit_settings'),(1,'2023-11-27 15:42:57','hYkjAGXNIj','session','add_field'),(1,'2024-10-15 13:15:12','I9aiLWHFRJ','session','workflow_edit'),(1,'2023-11-27 15:46:35','ibDe8MPzGE','session','edit_parameters'),(1,'2024-10-15 14:00:21','ITqzn9Ed9n','session','edit_product'),(1,'2024-10-15 13:06:33','jK4PGdugR8','session','edit_parameters'),(1,'2024-10-15 14:02:05','JOhZj5gVqg','session','edit_product'),(1,'2023-09-20 13:13:14','oukIJJwYod','api_token',''),(1,'2023-11-27 12:26:29','PIjhZLJ29K','session','edit_product'),(1,'2023-11-27 12:23:39','pIrqNpsRDo','api_token',''),(1,'2024-10-15 13:28:58','qO1ZPdshDu','session','edit_user'),(1,'2023-11-27 15:44:36','rkyOtDBxr4','session','edit_group_controls'),(1,'2023-09-20 13:13:20','VLrgLovfH9','session','edit_user_prefs'),(1,'2024-10-15 13:10:07','w7KWafB5zu','session','edit_parameters'),(1,'2023-11-27 15:45:59','xgQpxIS10M','session','edit_user_prefs'),(1,'2024-10-15 14:02:53','YnDsGT0jbR','session','add_component'); +/*!40000 ALTER TABLE `tokens` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ts_error` +-- + +DROP TABLE IF EXISTS `ts_error`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ts_error` ( + `error_time` int(11) NOT NULL, + `jobid` int(11) NOT NULL, + `message` varchar(255) NOT NULL, + `funcid` int(11) NOT NULL DEFAULT 0, + KEY `ts_error_funcid_idx` (`funcid`,`error_time`), + KEY `ts_error_error_time_idx` (`error_time`), + KEY `ts_error_jobid_idx` (`jobid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ts_error` +-- + +LOCK TABLES `ts_error` WRITE; +/*!40000 ALTER TABLE `ts_error` DISABLE KEYS */; +/*!40000 ALTER TABLE `ts_error` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ts_exitstatus` +-- + +DROP TABLE IF EXISTS `ts_exitstatus`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ts_exitstatus` ( + `jobid` int(11) NOT NULL AUTO_INCREMENT, + `funcid` int(11) NOT NULL DEFAULT 0, + `status` smallint(6) DEFAULT NULL, + `completion_time` int(11) DEFAULT NULL, + `delete_after` int(11) DEFAULT NULL, + PRIMARY KEY (`jobid`), + KEY `ts_exitstatus_funcid_idx` (`funcid`), + KEY `ts_exitstatus_delete_after_idx` (`delete_after`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ts_exitstatus` +-- + +LOCK TABLES `ts_exitstatus` WRITE; +/*!40000 ALTER TABLE `ts_exitstatus` DISABLE KEYS */; +/*!40000 ALTER TABLE `ts_exitstatus` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ts_funcmap` +-- + +DROP TABLE IF EXISTS `ts_funcmap`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ts_funcmap` ( + `funcid` int(11) NOT NULL AUTO_INCREMENT, + `funcname` varchar(255) NOT NULL, + PRIMARY KEY (`funcid`), + UNIQUE KEY `ts_funcmap_funcname_idx` (`funcname`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ts_funcmap` +-- + +LOCK TABLES `ts_funcmap` WRITE; +/*!40000 ALTER TABLE `ts_funcmap` DISABLE KEYS */; +/*!40000 ALTER TABLE `ts_funcmap` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ts_job` +-- + +DROP TABLE IF EXISTS `ts_job`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ts_job` ( + `jobid` int(11) NOT NULL AUTO_INCREMENT, + `funcid` int(11) NOT NULL, + `arg` longblob DEFAULT NULL, + `uniqkey` varchar(255) DEFAULT NULL, + `insert_time` int(11) DEFAULT NULL, + `run_after` int(11) NOT NULL, + `grabbed_until` int(11) NOT NULL, + `priority` smallint(6) DEFAULT NULL, + `coalesce` varchar(255) DEFAULT NULL, + PRIMARY KEY (`jobid`), + UNIQUE KEY `ts_job_funcid_idx` (`funcid`,`uniqkey`), + KEY `ts_job_run_after_idx` (`run_after`,`funcid`), + KEY `ts_job_coalesce_idx` (`coalesce`,`funcid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ts_job` +-- + +LOCK TABLES `ts_job` WRITE; +/*!40000 ALTER TABLE `ts_job` DISABLE KEYS */; +/*!40000 ALTER TABLE `ts_job` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ts_note` +-- + +DROP TABLE IF EXISTS `ts_note`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ts_note` ( + `jobid` int(11) NOT NULL, + `notekey` varchar(255) DEFAULT NULL, + `value` longblob DEFAULT NULL, + UNIQUE KEY `ts_note_jobid_idx` (`jobid`,`notekey`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ts_note` +-- + +LOCK TABLES `ts_note` WRITE; +/*!40000 ALTER TABLE `ts_note` DISABLE KEYS */; +/*!40000 ALTER TABLE `ts_note` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `user_api_keys` +-- + +DROP TABLE IF EXISTS `user_api_keys`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `user_api_keys` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` mediumint(9) NOT NULL, + `api_key` varchar(40) NOT NULL, + `description` varchar(255) DEFAULT NULL, + `revoked` tinyint(4) NOT NULL DEFAULT 0, + `last_used` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `user_api_keys_api_key_idx` (`api_key`), + KEY `user_api_keys_user_id_idx` (`user_id`), + CONSTRAINT `fk_user_api_keys_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `user_api_keys` +-- + +LOCK TABLES `user_api_keys` WRITE; +/*!40000 ALTER TABLE `user_api_keys` DISABLE KEYS */; +INSERT INTO `user_api_keys` VALUES (1,1,'AxBntHGSL97CmoTahkey8RNyo2K65NEfJBuk5ATe','',0,NULL); +/*!40000 ALTER TABLE `user_api_keys` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `user_group_map` +-- + +DROP TABLE IF EXISTS `user_group_map`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `user_group_map` ( + `user_id` mediumint(9) NOT NULL, + `group_id` mediumint(9) NOT NULL, + `isbless` tinyint(4) NOT NULL DEFAULT 0, + `grant_type` tinyint(4) NOT NULL DEFAULT 0, + UNIQUE KEY `user_group_map_user_id_idx` (`user_id`,`group_id`,`grant_type`,`isbless`), + KEY `fk_user_group_map_group_id_groups_id` (`group_id`), + CONSTRAINT `fk_user_group_map_group_id_groups_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_user_group_map_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `user_group_map` +-- + +LOCK TABLES `user_group_map` WRITE; +/*!40000 ALTER TABLE `user_group_map` DISABLE KEYS */; +INSERT INTO `user_group_map` VALUES (1,1,0,0),(1,1,1,0),(1,3,0,0),(1,8,0,2),(2,8,0,2); +/*!40000 ALTER TABLE `user_group_map` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `versions` +-- + +DROP TABLE IF EXISTS `versions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `versions` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `product_id` smallint(6) NOT NULL, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `versions_product_id_idx` (`product_id`,`value`), + CONSTRAINT `fk_versions_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `versions` +-- + +LOCK TABLES `versions` WRITE; +/*!40000 ALTER TABLE `versions` DISABLE KEYS */; +INSERT INTO `versions` VALUES (1,'unspecified',1,1),(2,'unspecified',2,1),(3,'9.0',2,1),(4,'9.1',2,1),(5,'unspecified',3,1); +/*!40000 ALTER TABLE `versions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `watch` +-- + +DROP TABLE IF EXISTS `watch`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `watch` ( + `watcher` mediumint(9) NOT NULL, + `watched` mediumint(9) NOT NULL, + UNIQUE KEY `watch_watcher_idx` (`watcher`,`watched`), + KEY `watch_watched_idx` (`watched`), + CONSTRAINT `fk_watch_watched_profiles_userid` FOREIGN KEY (`watched`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_watch_watcher_profiles_userid` FOREIGN KEY (`watcher`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `watch` +-- + +LOCK TABLES `watch` WRITE; +/*!40000 ALTER TABLE `watch` DISABLE KEYS */; +/*!40000 ALTER TABLE `watch` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `whine_events` +-- + +DROP TABLE IF EXISTS `whine_events`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `whine_events` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `owner_userid` mediumint(9) NOT NULL, + `subject` varchar(128) DEFAULT NULL, + `body` mediumtext DEFAULT NULL, + `mailifnobugs` tinyint(4) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `fk_whine_events_owner_userid_profiles_userid` (`owner_userid`), + CONSTRAINT `fk_whine_events_owner_userid_profiles_userid` FOREIGN KEY (`owner_userid`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `whine_events` +-- + +LOCK TABLES `whine_events` WRITE; +/*!40000 ALTER TABLE `whine_events` DISABLE KEYS */; +/*!40000 ALTER TABLE `whine_events` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `whine_queries` +-- + +DROP TABLE IF EXISTS `whine_queries`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `whine_queries` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `eventid` mediumint(9) NOT NULL, + `query_name` varchar(64) NOT NULL DEFAULT '', + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `onemailperbug` tinyint(4) NOT NULL DEFAULT 0, + `title` varchar(128) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + KEY `whine_queries_eventid_idx` (`eventid`), + CONSTRAINT `fk_whine_queries_eventid_whine_events_id` FOREIGN KEY (`eventid`) REFERENCES `whine_events` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `whine_queries` +-- + +LOCK TABLES `whine_queries` WRITE; +/*!40000 ALTER TABLE `whine_queries` DISABLE KEYS */; +/*!40000 ALTER TABLE `whine_queries` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `whine_schedules` +-- + +DROP TABLE IF EXISTS `whine_schedules`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `whine_schedules` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `eventid` mediumint(9) NOT NULL, + `run_day` varchar(32) DEFAULT NULL, + `run_time` varchar(32) DEFAULT NULL, + `run_next` datetime DEFAULT NULL, + `mailto` mediumint(9) NOT NULL, + `mailto_type` smallint(6) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `whine_schedules_run_next_idx` (`run_next`), + KEY `whine_schedules_eventid_idx` (`eventid`), + CONSTRAINT `fk_whine_schedules_eventid_whine_events_id` FOREIGN KEY (`eventid`) REFERENCES `whine_events` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `whine_schedules` +-- + +LOCK TABLES `whine_schedules` WRITE; +/*!40000 ALTER TABLE `whine_schedules` DISABLE KEYS */; +/*!40000 ALTER TABLE `whine_schedules` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2024-10-15 16:07:04 diff --git a/tests/services/bugzilla.conf b/tests/services/bugzilla.conf new file mode 100644 index 00000000..c0de1250 --- /dev/null +++ b/tests/services/bugzilla.conf @@ -0,0 +1,9 @@ + + DocumentRoot /var/www/webapps/bugzilla + + AddHandler cgi-script .cgi + Options +ExecCGI + DirectoryIndex index.cgi index.html + AllowOverride All + + diff --git a/tests/services/bugzillarc b/tests/services/bugzillarc new file mode 100644 index 00000000..7f6dafaa --- /dev/null +++ b/tests/services/bugzillarc @@ -0,0 +1,2 @@ +[localhost] +api_key = AxBntHGSL97CmoTahkey8RNyo2K65NEfJBuk5ATe diff --git a/tests/services/localconfig b/tests/services/localconfig new file mode 100644 index 00000000..f3bddb99 --- /dev/null +++ b/tests/services/localconfig @@ -0,0 +1,19 @@ +$create_htaccess = 1; +$webservergroup = 'www-data'; +$use_suexec = 0; +$db_driver = 'mysql'; +$db_host = 'mariadb'; +$db_name = 'bugs'; +$db_user = 'bugs'; +$db_pass = 'secret'; +$db_port = 3306; +$db_sock = ''; +$db_check = 1; +$db_mysql_ssl_ca_file = ''; +$db_mysql_ssl_ca_path = ''; +$db_mysql_ssl_client_cert = ''; +$db_mysql_ssl_client_key = ''; +$index_html = 0; +$interdiffbin = ''; +$diffpath = '/usr/bin'; +$site_wide_secret = 'oCIbi5WC04h86lW7L8fDcPCrVjb3JNeA2St94QlQtfjZrorjKmOdeVV0feHNDeFH'; diff --git a/tests/services/params.json b/tests/services/params.json new file mode 100644 index 00000000..9a6a9034 --- /dev/null +++ b/tests/services/params.json @@ -0,0 +1,104 @@ +{ + "LDAPBaseDN" : "", + "LDAPbinddn" : "", + "LDAPfilter" : "", + "LDAPmailattribute" : "mail", + "LDAPserver" : "", + "LDAPstarttls" : "0", + "LDAPuidattribute" : "uid", + "RADIUS_NAS_IP" : "", + "RADIUS_email_suffix" : "", + "RADIUS_secret" : "", + "RADIUS_server" : "", + "ajax_user_autocompletion" : "1", + "allow_attachment_deletion" : "0", + "allow_attachment_display" : "0", + "allowbugdeletion" : "0", + "allowemailchange" : "1", + "allowuserdeletion" : "0", + "announcehtml" : "", + "attachment_base" : "", + "auth_env_email" : "", + "auth_env_id" : "", + "auth_env_realname" : "", + "chartgroup" : "editbugs", + "collapsed_comment_tags" : "obsolete, spam", + "comment_taggers_group" : "editbugs", + "commentonchange_resolution" : "0", + "commentonduplicate" : "0", + "confirmuniqueusermatch" : "1", + "cookiedomain" : "", + "cookiepath" : "/", + "createemailregexp" : ".*", + "debug_group" : "admin", + "default_search_limit" : "500", + "defaultopsys" : "", + "defaultplatform" : "", + "defaultpriority" : "---", + "defaultquery" : "resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring", + "defaultseverity" : "enhancement", + "duplicate_or_move_bug_status" : "RESOLVED", + "emailregexp" : "^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$", + "emailregexpdesc" : "A legal address must contain exactly one '@', and at least one '.' after the @.", + "emailsuffix" : "", + "font_file" : "", + "globalwatchers" : "", + "inbound_proxies" : "", + "insidergroup" : "editbugs", + "last_visit_keep_days" : "10", + "letsubmitterchoosemilestone" : "1", + "letsubmitterchoosepriority" : "1", + "mail_delivery_method" : "None", + "mailfrom" : "bugzilla-daemon", + "maintainer" : "andreas@hasenkopf.xyz", + "makeproductgroups" : "0", + "max_search_results" : "10000", + "maxattachmentsize" : "1000", + "maxlocalattachment" : "0", + "maxusermatches" : "1000", + "memcached_namespace" : "bugzilla:", + "memcached_servers" : "", + "musthavemilestoneonaccept" : "0", + "mybugstemplate" : "buglist.cgi?resolution=---&emailassigned_to1=1&emailreporter1=1&emailtype1=exact&email1=%userid%", + "noresolveonopenblockers" : "0", + "or_groups" : "1", + "password_check_on_login" : "1", + "password_complexity" : "no_constraints", + "proxy_url" : "", + "querysharegroup" : "editbugs", + "quip_list_entry_control" : "open", + "rememberlogin" : "on", + "requirelogin" : "0", + "search_allow_no_criteria" : "0", + "shadowdb" : "", + "shadowdbhost" : "", + "shadowdbport" : "3306", + "shadowdbsock" : "", + "shutdownhtml" : "", + "smtp_debug" : "0", + "smtp_password" : "", + "smtp_ssl" : "0", + "smtp_username" : "", + "smtpserver" : "localhost", + "ssl_redirect" : "0", + "sslbase" : "", + "strict_isolation" : "0", + "strict_transport_security" : "off", + "timetrackinggroup" : "editbugs", + "upgrade_notification" : "latest_stable_release", + "urlbase" : "", + "use_mailer_queue" : "0", + "use_see_also" : "1", + "useclassification" : "0", + "usemenuforusers" : "0", + "useqacontact" : "0", + "user_info_class" : "CGI", + "user_verify_class" : "DB", + "usestatuswhiteboard" : "0", + "usetargetmilestone" : "0", + "usevisibilitygroups" : "0", + "utf8" : "1", + "webdotbase" : "", + "webservice_email_filter" : "0", + "whinedays" : "7" +} diff --git a/tests/test_api_attachments.py b/tests/test_api_attachments.py new file mode 100644 index 00000000..3eb4c0d5 --- /dev/null +++ b/tests/test_api_attachments.py @@ -0,0 +1,53 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +import os + +import pytest + +import tests +import tests.mockbackend + + +def test_api_attachments(): + # misc coverage testing for Bugzilla attachment APIs + fakebz = tests.mockbackend.make_bz( + bug_attachment_get_all_args=( + "data/mockargs/test_attachments_getall1.txt"), + bug_attachment_get_all_return={}, + bug_attachment_update_args=( + "data/mockargs/test_attachments_update1.txt"), + bug_attachment_update_return={}, + bug_attachment_get_args=( + "data/mockargs/test_attachments_get1.txt"), + bug_attachment_get_return=( + "data/mockreturn/test_attach_get1.txt"), + bug_attachment_create_args=( + "data/mockargs/test_api_attachments_create1.txt"), + bug_attachment_create_return={ + "attachments": {"123456": {}, "456789": []}}, + ) + + # coverage for include/exclude handling + fakebz.get_attachments([123456], None, + include_fields=["foo"], exclude_fields="bar") + + # coverage for updateattachment + fakebz.updateattachmentflags(None, "112233", "needinfo", + value="foobar", is_patch=True) + + # coverage for openattachment + fobj = fakebz.openattachment(502352) + assert "Hooray" in str(fobj.read()) + + # Error on bad input type + with pytest.raises(TypeError): + fakebz.attachfile([123456], None, "some desc") + + # Misc attachfile() pieces + attachfile = os.path.dirname(__file__) + "/data/bz-attach-get1.txt" + ret = fakebz.attachfile([123456], attachfile, "some desc", + isprivate=True) + ret.sort() + assert ret == [123456, 456789] diff --git a/tests/test_api_authfiles.py b/tests/test_api_authfiles.py new file mode 100644 index 00000000..fcd6fbd8 --- /dev/null +++ b/tests/test_api_authfiles.py @@ -0,0 +1,164 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Test miscellaneous API bits +""" + +import os +import shutil +import tempfile + +import tests +import tests.mockbackend +import tests.utils + + +def test_tokenfile(monkeypatch): + dirname = os.path.dirname(__file__) + monkeypatch.setitem(os.environ, "HOME", dirname + "/data/homedir") + + bz = tests.mockbackend.make_bz(bz_kwargs={"use_creds": True}) + token = dirname + "/data/homedir/.cache/python-bugzilla/bugzillatoken" + + assert token == bz.tokenfile + del bz.tokenfile + assert bz.tokenfile is None + assert bz.cookiefile is None + + +def test_readconfig(): + # Testing for bugzillarc handling + bzapi = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) + bzapi.url = "example.com" + temp = tempfile.NamedTemporaryFile(mode="w") + + def _check(user, password, api_key, cert): + assert bzapi.user == user + assert bzapi.password == password + assert bzapi.api_key == api_key + assert bzapi.cert == cert + + def _write(c): + temp.seek(0) + temp.write(c) + temp.flush() + return temp.name + + # Check readconfig normal usage + content = """ +[example.com] +foo=1 +user=test1 +password=test2 +api_key=123abc +cert=/a/b/c +someunknownkey=someval +""" + bzapi.readconfig(_write(content)) + _check("test1", "test2", "123abc", "/a/b/c") + + # Check loading a different URL, that values aren't overwritten + content = """ +[foo.example.com] +user=test3 +password=test4 +api_key=567abc +cert=/newpath +""" + bzapi.readconfig(_write(content)) + _check("test1", "test2", "123abc", "/a/b/c") + + # Change URL, but check readconfig with overwrite=False + bzapi.url = "foo.example.com" + bzapi.readconfig(temp.name, overwrite=False) + _check("test1", "test2", "123abc", "/a/b/c") + + # With default overwrite=True, values will be updated + # Alter the config to have a / in the hostname, which hits different code + content = content.replace("example.com", "example.com/xmlrpc.cgi") + bzapi.url = "https://foo.example.com/xmlrpc.cgi" + bzapi.readconfig(_write(content)) + _check("test3", "test4", "567abc", "/newpath") + + # Confirm nothing overwritten for a totally different URL + bzapi.user = None + bzapi.password = None + bzapi.api_key = None + bzapi.cert = None + bzapi.url = "bugzilla.redhat.com" + bzapi.readconfig(temp.name) + _check(None, None, None, None) + + # Test confipath overwrite + assert [temp.name] == bzapi.configpath + del bzapi.configpath + assert [] == bzapi.configpath + bzapi.readconfig() + _check(None, None, None, None) + + +def test_authfiles_saving(monkeypatch): + tmpdir = tempfile.mkdtemp() + try: + monkeypatch.setitem(os.environ, "HOME", tmpdir) + + bzapi = tests.mockbackend.make_bz( + bz_kwargs={"use_creds": True, "cert": "foo-fake-cert"}) + bzapi.connect("https://example.com/fakebz") + + bzapi.cert = "foo-fake-path" + backend = bzapi._backend # pylint: disable=protected-access + bsession = backend._bugzillasession # pylint: disable=protected-access + btokencache = bzapi._tokencache # pylint: disable=protected-access + + # token testing, with repetitions to hit various code paths + btokencache.set_value(bzapi.url, None) + assert "Bugzilla_token" not in bsession.get_auth_params() + btokencache.set_value(bzapi.url, "MY-FAKE-TOKEN") + assert bsession.get_auth_params()["Bugzilla_token"] == "MY-FAKE-TOKEN" + btokencache.set_value(bzapi.url, "MY-FAKE-TOKEN") + btokencache.set_value(bzapi.url, None) + assert "Bugzilla_token" not in bsession.get_auth_params() + btokencache.set_value(bzapi.url, "MY-FAKE-TOKEN") + + dirname = os.path.dirname(__file__) + "/data/authfiles/" + output_token = dirname + "output-token.txt" + tests.utils.diff_compare(open(bzapi.tokenfile).read(), output_token) + + # Make sure file can re-read them and not error + bzapi = tests.mockbackend.make_bz( + bz_kwargs={"use_creds": True, + "tokenfile": output_token}) + assert bzapi.tokenfile == output_token + + # Test rcfile writing for api_key + rcfile = bzapi._rcfile # pylint: disable=protected-access + bzapi.url = "https://example.com/fake" + rcfile.save_api_key(bzapi.url, "TEST-API-KEY") + rcfilepath = tmpdir + "/.config/python-bugzilla/bugzillarc" + assert rcfile.get_configpaths()[-1] == rcfilepath + tests.utils.diff_compare(open(rcfilepath).read(), + dirname + "output-bugzillarc.txt") + + # Use that generated rcfile to test default URL lookup + fakeurl = "http://foo.bar.baz/wibble" + open(rcfilepath, "w").write("\n[DEFAULT]\nurl = %s" % fakeurl) + assert bzapi.get_rcfile_default_url() == fakeurl + finally: + shutil.rmtree(tmpdir) + + +def test_authfiles_nowrite(): + # Setting values tokenfile is None, should be fine + bzapi = tests.mockbackend.make_bz(bz_kwargs={"use_creds": False}) + bzapi.connect("https://example.com/foo") + btokencache = bzapi._tokencache # pylint: disable=protected-access + rcfile = bzapi._rcfile # pylint: disable=protected-access + + btokencache.set_value(bzapi.url, "NEW-TOKEN-VALUE") + assert rcfile.save_api_key(bzapi.url, "fookey") is None diff --git a/tests/test_api_bug.py b/tests/test_api_bug.py new file mode 100644 index 00000000..23448d6b --- /dev/null +++ b/tests/test_api_bug.py @@ -0,0 +1,223 @@ +# +# Copyright Red Hat, Inc. 2014 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Unit tests for testing some bug.py magic +""" + +import io +import pickle + +import pytest + +import tests +import tests.mockbackend +import tests.utils + +from bugzilla.bug import Bug + + +rhbz = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) + + +def testBasic(): + data = { + "bug_id": 123456, + "status": "NEW", + "assigned_to": "foo@bar.com", + "component": "foo", + "product": "bar", + "short_desc": "some short desc", + "cf_fixed_in": "nope", + "fixed_in": "1.2.3.4", + "devel_whiteboard": "some status value", + } + + bug = Bug(bugzilla=rhbz, dict=data) + + def _assert_bug(): + assert hasattr(bug, "component") is True + assert getattr(bug, "components") == ["foo"] + assert getattr(bug, "product") == "bar" + assert hasattr(bug, "short_desc") is True + assert getattr(bug, "summary") == "some short desc" + assert bool(getattr(bug, "cf_fixed_in")) is True + assert getattr(bug, "fixed_in") == "1.2.3.4" + assert bool(getattr(bug, "cf_devel_whiteboard")) is True + assert getattr(bug, "devel_whiteboard") == "some status value" + + _assert_bug() + + assert str(bug) == "#123456 NEW - foo@bar.com - some short desc" + assert repr(bug).startswith(")` includes the alias in `include_fields` + """ + fakebz = tests.mockbackend.make_bz( + bug_get_args=None, + bug_get_return="data/mockreturn/test_query_cve_getbug.txt") + bug = fakebz.getbug("CVE-1234-5678", include_fields=["id"]) + assert bug.alias == ["CVE-1234-5678"] + assert bug.id == 123456 + + def mock_bug_get(bug_ids, aliases, paramdict): + assert bug_ids == [] + assert aliases == ["CVE-1234-5678"] + assert "alias" in paramdict.get("include_fields", []) + return {"bugs": [bug.get_raw_data()]} + + backend = getattr(fakebz, "_backend") + setattr(backend, "bug_get", mock_bug_get) + + fakebz.getbug("CVE-1234-5678", include_fields=["id"]) + + +def test_bug_getattr(): + fakebz = tests.mockbackend.make_bz( + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + bug = fakebz.getbug(1165434) + + with pytest.raises(AttributeError): + # Hits a specific codepath in Bug.__getattr__ + dummy = bug.__baditem__ + + bug.autorefresh = True + summary = bug.summary + del bug.__dict__["summary"] + # Trigger autorefresh + assert bug.summary == summary + + +def test_bug_apis(): + def _get_fake_bug(apiname): + update_args = "data/mockargs/test_bug_apis_%s_update.txt" % apiname + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt", + bug_update_args=update_args, + bug_update_return={}) + return fakebz.getbug(1165434) + + # bug.setstatus, wrapper for update_bugs + bug = _get_fake_bug("setstatus") + bug.setstatus("POST", "foocomment", private=True) + + # bug.close, wrapper for update_bugs + bug = _get_fake_bug("close") + bug.close("UPSTREAM", dupeid=123456, comment="foocomment2", + isprivate=False, fixedin="1.2.3.4.5") + + # bug.setassignee, wrapper for update_bugs + bug = _get_fake_bug("setassignee") + bug.setassignee( + assigned_to="foo@example.com", qa_contact="bar@example.com", + comment="foocomment") + with pytest.raises(ValueError): + # Hits a validation path + bug.setassignee() + + # bug.addcc test + bug = _get_fake_bug("addcc") + bug.addcc("foo2@example.com", comment="foocomment") + + # bug.deletecc test + bug = _get_fake_bug("deletecc") + bug.deletecc("foo2@example.com", comment="foocomment") + + # bug.addcomment test + bug = _get_fake_bug("addcomment") + bug.addcomment("test comment", private=True) + + # bug.updateflags test + bug = _get_fake_bug("updateflags") + bug.updateflags({"someflag": "someval"}) + + # Some minor flag API tests + assert "creation_date" in bug.get_flag_type("needinfo") + assert bug.get_flag_type("NOPE") is None + assert bug.get_flags("NOPE") is None + assert bug.get_flag_status("NOPE") is None + + # bug.setsummary test + bug = _get_fake_bug("setsummary") + bug.setsummary("My new summary") + + # Minor get_history_raw wrapper + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_history_args="data/mockargs/test_bug_api_history.txt", + bug_history_return={}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt", + bug_comments_args="data/mockargs/test_bug_api_comments.txt", + bug_comments_return={"bugs": {"1165434": {"comments": []}}}, + bug_attachment_get_all_args=( + "data/mockargs/test_bug_api_get_attachments.txt"), + bug_attachment_get_all_return="data/mockreturn/test_attach_get2.txt", + ) + + # Stub API testing + bug = fakebz.getbug(1165434) + bug.get_history_raw() + bug.get_comments() + bug.getcomments() + + # Some hackery to hit a few attachment code paths + bug.id = 663674 + attachments = bug.get_attachments() + bug.attachments = attachments + assert [469147, 470041, 502352] == bug.get_attachment_ids() + + +def test_bug_weburl(): + fakebz = tests.mockbackend.make_bz( + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + bug_id = 1165434 + bug = fakebz.getbug(bug_id) + assert bug.weburl == f"https:///show_bug.cgi?id={bug_id}" diff --git a/tests/test_api_externalbugs.py b/tests/test_api_externalbugs.py new file mode 100644 index 00000000..c684bee6 --- /dev/null +++ b/tests/test_api_externalbugs.py @@ -0,0 +1,53 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Test miscellaneous API bits +""" + +import tests +import tests.mockbackend + + +def test_externalbugs(): + # Basic API testing of the ExternalBugs wrappers + fakebz = tests.mockbackend.make_bz( + externalbugs_add_args="data/mockargs/test_externalbugs_add.txt", + externalbugs_add_return={}, + externalbugs_update_args="data/mockargs/test_externalbugs_update.txt", + externalbugs_update_return={}, + externalbugs_remove_args="data/mockargs/test_externalbugs_remove.txt", + externalbugs_remove_return={}) + + fakebz.add_external_tracker( + bug_ids=[1234, 5678], + ext_bz_bug_id="externalid", + ext_type_id="launchpad", + ext_type_description="some-bug-add-description", + ext_type_url="https://example.com/launchpad/1234", + ext_status="CLOSED", + ext_description="link to launchpad", + ext_priority="bigly") + + fakebz.update_external_tracker( + ids=["external1", "external2"], + ext_bz_bug_id="externalid-update", + ext_type_id="mozilla", + ext_type_description="some-bug-update", + ext_type_url="https://mozilla.foo/bar/5678", + ext_status="OPEN", + bug_ids=["some", "bug", "id"], + ext_description="link to mozilla", + ext_priority="like, really bigly") + + fakebz.remove_external_tracker( + ids="remove1", + ext_bz_bug_id="99999", + ext_type_id="footype", + ext_type_description="foo-desc", + ext_type_url="foo-url", + bug_ids="blah") diff --git a/tests/test_api_groups.py b/tests/test_api_groups.py new file mode 100644 index 00000000..2f1f425c --- /dev/null +++ b/tests/test_api_groups.py @@ -0,0 +1,60 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Test miscellaneous API bits +""" + +import tests +import tests.mockbackend + + +def test_api_groups(): + # Basic API testing of the users APIs + group_ret = {"groups": [{ + "membership": [ + {"real_name": "Bugzilla User", + "can_login": 1, + "name": "user@bugzilla.org", + "login_denied_text": "", + "id": 85, + "email_enabled": 1, + "email": "user@bugzilla.org"}, + {"real_name": "Bugzilla User2", + "can_login": 0, + "name": "user2@bugzilla.org", + "login_denied_text": "", + "id": 77, + "email_enabled": 0, + "email": "user2@bugzilla.org"}, + ], + "is_active": 1, + "description": "Test Group", + "user_regexp": "", + "is_bug_group": 1, + "name": "TestGroup", + "id": 9 + }]} + + fakebz = tests.mockbackend.make_bz( + group_get_args="data/mockargs/test_api_groups_get1.txt", + group_get_return=group_ret) + + # getgroups testing + groupobj = fakebz.getgroups("TestGroups")[0] + assert groupobj.groupid == 9 + assert groupobj.member_emails == [ + "user2@bugzilla.org", "user@bugzilla.org"] + assert groupobj.name == "TestGroup" + + # getgroup testing + fakebz = tests.mockbackend.make_bz( + group_get_args="data/mockargs/test_api_groups_get2.txt", + group_get_return=group_ret) + groupobj = fakebz.getgroup("TestGroup", membership=True) + groupobj.membership = [] + assert groupobj.members() == group_ret["groups"][0]["membership"] diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py new file mode 100644 index 00000000..30889887 --- /dev/null +++ b/tests/test_api_misc.py @@ -0,0 +1,317 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Test miscellaneous API bits +""" + +import pytest + +import bugzilla + +import tests +import tests.mockbackend + + +def test_mock_rhbz(): + fakebz = tests.mockbackend.make_bz(rhbz=True) + assert fakebz.__class__ == bugzilla.RHBugzilla + + +def test_file_imports(): + # Ensure historically stable import paths continue to work + # pylint: disable=unused-import + from bugzilla.rhbugzilla import RHBugzilla + from bugzilla.bug import Bug + from bugzilla.base import Bugzilla + + +def testUserAgent(): + b3 = tests.mockbackend.make_bz(version="3.0.0") + assert "python-bugzilla" in b3.user_agent + + +def test_fixurl(): + assert (bugzilla.Bugzilla.fix_url("example.com") == + "https://example.com/xmlrpc.cgi") + assert (bugzilla.Bugzilla.fix_url("example.com", force_rest=True) == + "https://example.com/rest/") + assert (bugzilla.Bugzilla.fix_url("example.com/xmlrpc.cgi") == + "https://example.com/xmlrpc.cgi") + assert (bugzilla.Bugzilla.fix_url("http://example.com/somepath.cgi") == + "http://example.com/somepath.cgi") + assert bugzilla.Bugzilla.fix_url("http:///foo") == "http:///foo" + + +def testPostTranslation(): + def _testPostCompare(bz, indict, outexpect): + outdict = indict.copy() + bz.post_translation({}, outdict) + assert outdict == outexpect + + # Make sure multiple calls don't change anything + bz.post_translation({}, outdict) + assert outdict == outexpect + + bug3 = tests.mockbackend.make_bz(version="3.4.0") + rhbz = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) + + test1 = { + "component": ["comp1"], + "version": ["ver1", "ver2"], + + 'flags': [{ + 'is_active': 1, + 'name': 'qe_test_coverage', + 'setter': 'pm-rhel@redhat.com', + 'status': '?', + }, { + 'is_active': 1, + 'name': 'rhel-6.4.0', + 'setter': 'pm-rhel@redhat.com', + 'status': '+', + }], + + 'alias': ["FOO", "BAR"], + 'blocks': [782183, 840699, 923128], + 'keywords': ['Security'], + 'groups': ['redhat'], + } + + out_simple = test1.copy() + out_simple["components"] = out_simple["component"] + out_simple["component"] = out_simple["components"][0] + out_simple["versions"] = out_simple["version"] + out_simple["version"] = out_simple["versions"][0] + + _testPostCompare(bug3, test1, test1) + _testPostCompare(rhbz, test1, out_simple) + + +def test_rhbz_pre_translation(): + bz = tests.mockbackend.make_bz(rhbz=True) + input_query = { + "bug_id": "12345,6789", + "component": "comp1,comp2", + "column_list": ["field1", "field8"], + } + + bz.pre_translation(input_query) + output_query = { + 'component': ['comp1', 'comp2'], + 'id': ['12345', '6789'], + 'include_fields': ['field1', 'field8', 'id'], + } + + assert output_query == input_query + + +def testUpdateFailures(): + # sub_component without component also passed + bz = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) + with pytest.raises(ValueError): + bz.build_update(sub_component="some sub component") + + # Trying to update value that only rhbz supports + bz = tests.mockbackend.make_bz() + with pytest.raises(ValueError): + bz.build_update(fixed_in="some fixedin value") + + +def testCreatebugFieldConversion(): + bz4 = tests.mockbackend.make_bz(version="4.0.0") + vc = bz4._validate_createbug # pylint: disable=protected-access + out = vc(product="foo", component="bar", + version="12", description="foo", short_desc="bar", + check_args=False) + assert out == { + 'component': 'bar', 'description': 'foo', 'product': 'foo', + 'summary': 'bar', 'version': '12'} + + +def testURLSavedSearch(): + bz4 = tests.mockbackend.make_bz(version="4.0.0") + url = ("https://bugzilla.redhat.com/buglist.cgi?" + "cmdtype=dorem&list_id=2342312&namedcmd=" + "RHEL7%20new%20assigned%20virt-maint&remaction=run&" + "sharer_id=321167") + query = { + 'sharer_id': '321167', + 'savedsearch': 'RHEL7 new assigned virt-maint' + } + assert bz4.url_to_query(url) == query + + +def testStandardQuery(): + bz4 = tests.mockbackend.make_bz(version="4.0.0") + url = ("https://bugzilla.redhat.com/buglist.cgi?" + "component=virt-manager&query_format=advanced&classification=" + "Fedora&product=Fedora&bug_status=NEW&bug_status=ASSIGNED&" + "bug_status=MODIFIED&bug_status=ON_DEV&bug_status=ON_QA&" + "bug_status=VERIFIED&bug_status=FAILS_QA&bug_status=" + "RELEASE_PENDING&bug_status=POST&order=bug_status%2Cbug_id") + query = { + 'product': 'Fedora', + 'query_format': 'advanced', + 'bug_status': ['NEW', 'ASSIGNED', 'MODIFIED', 'ON_DEV', + 'ON_QA', 'VERIFIED', 'FAILS_QA', 'RELEASE_PENDING', 'POST'], + 'classification': 'Fedora', + 'component': 'virt-manager', + 'order': 'bug_status,bug_id' + } + assert bz4.url_to_query(url) == query + + # pylint: disable=use-implicit-booleaness-not-comparison + # Test with unknown URL + assert bz4.url_to_query("https://example.com") == {} + + +def test_api_login(): + with pytest.raises(TypeError): + # Missing explicit URL + bugzilla.Bugzilla() + + with pytest.raises(Exception): + # Calling connect() with passed in URL + bugzilla.Bugzilla(url="https:///FAKEURL") + + bz = tests.mockbackend.make_bz() + + with pytest.raises(ValueError): + # Errors on missing user + bz.login() + + bz.user = "FOO" + with pytest.raises(ValueError): + # Errors on missing pass + bz.login() + + bz.password = "BAR" + bz.api_key = "WIBBLE" + with pytest.raises(ValueError): + # Errors on api_key + login() + bz.login() + + # Hit default api_key code path + bz = tests.mockbackend.make_bz( + bz_kwargs={"api_key": "FAKE_KEY"}, + user_login_args="data/mockargs/test_api_login.txt", + user_login_return={}) + # Try reconnect, with RHBZ testing + bz.connect("https:///fake/bugzilla.redhat.com") + bz.connect() + + # Test auto login if user/password is set + bz = tests.mockbackend.make_bz( + bz_kwargs={"user": "FOO", "password": "BAR"}, + user_login_args="data/mockargs/test_api_login2.txt", + user_login_return={}, + user_logout_args=None, + user_logout_return={}) + + # Test logout + bz.logout() + + +def test_version_bad(): + # Hit version error handling + bz = tests.mockbackend.make_bz(version="badversion") + assert bz.bz_ver_major == 5 + assert bz.bz_ver_minor == 0 + + # pylint: disable=protected-access + assert bz._get_version() == 5.0 + + +def test_extensions_bad(): + # Hit bad extensions error handling + tests.mockbackend.make_bz(extensions="BADEXTENSIONS") + + +def test_bad_scheme(): + bz = tests.mockbackend.make_bz() + try: + bz.connect("ftp://example.com") + except Exception as e: + assert "Invalid URL scheme: ftp" in str(e) + + +def test_update_flags(): + # update_flags is just a compat wrapper for update_bugs + bz = tests.mockbackend.make_bz( + bug_update_args="data/mockargs/test_update_flags.txt", + bug_update_return={}) + bz.update_flags([12345, 6789], {"name": "needinfo", "status": "?"}) + + +def test_bugs_history_raw(): + # Stub test for bugs_history_raw + ids = ["12345", 567] + bz = tests.mockbackend.make_bz( + bug_history_args=(ids, {}), + bug_history_return={}) + bz.bugs_history_raw(ids) + + +def test_get_comments(): + # Stub test for get_commands + ids = ["12345", 567] + bz = tests.mockbackend.make_bz( + bug_comments_args=(ids, {}), + bug_comments_return={}) + bz.get_comments(ids) + + +def test_get_xmlrpc_proxy(): + # Ensure _proxy goes to a backend API + bz = tests.mockbackend.make_bz() + with pytest.raises(NotImplementedError): + dummy = bz._proxy # pylint: disable=protected-access + + assert bz.is_xmlrpc() is False + assert bz.is_rest() is False + assert hasattr(bz.get_requests_session(), "request") + + +def test_requests_session_passthrough(): + import requests + session = requests.Session() + + bz = tests.mockbackend.make_bz( + bz_kwargs={"requests_session": session, "sslverify": False}) + assert bz.get_requests_session() is session + assert session.verify is False + + +def test_query_url_fail(): + # test some handling of query from_url errors + query = {"query_format": "advanced", "product": "FOO"} + checkstr = "does not appear to support" + + exc = bugzilla.BugzillaError("FAKEERROR query_format", code=123) + bz = tests.mockbackend.make_bz(version="4.0.0", + bug_search_args=None, bug_search_return=exc) + try: + bz.query(query) + except Exception as e: + assert checkstr in str(e) + + bz = tests.mockbackend.make_bz(version="5.1.0", + bug_search_args=None, bug_search_return=exc) + try: + bz.query(query) + except Exception as e: + assert checkstr not in str(e) + + +def test_query_return_extra(): + bz = tests.mockbackend.make_bz(version="5.1.0", + bug_search_args=None, + bug_search_return="data/mockreturn/test_query1.txt") + dummy, extra = bz.query_return_extra({}) + assert extra['limit'] == 0 + assert extra['FOOFAKEVALUE'] == "hello" diff --git a/tests/test_api_products.py b/tests/test_api_products.py new file mode 100644 index 00000000..41991ae1 --- /dev/null +++ b/tests/test_api_products.py @@ -0,0 +1,116 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import pytest + +import bugzilla + +import tests +import tests.mockbackend + + +def test_api_component_edit(): + fakebz = tests.mockbackend.make_bz( + component_create_args="data/mockargs/test_api_component_create1.txt", + component_create_return={}, + component_update_args="data/mockargs/test_api_component_update1.txt", + component_update_return={}, + ) + + # addcomponent stub testing + fakebz.addcomponent({ + "initialowner": "foo@example.com", + "initialqacontact": "foo2@example.com", + "initialcclist": "foo3@example.com", + "product": "fooproduct", + "is_active": 0, + }) + + # editcomponent stub testing + fakebz.editcomponent({ + "initialowner": "foo@example.com", + "blaharg": "blahval", + "product": "fooproduct", + "component": "foocomponent", + "is_active": 0, + }) + + +def test_api_products(): + prod_list_return = {'ids': [1, 7]} + prod_get_return = {'products': [ + {'id': 7, 'name': 'test-fake-product', + 'foo': {"bar": "baz"}, + 'components': [ + {'default_assigned_to': 'Fake Guy', + 'name': 'client-interfaces'}, + {'default_assigned_to': 'ANother fake dude!', + 'name': 'configuration'}, + ]}, + ]} + + compnames = ["client-interfaces", "configuration"] + fakebz = tests.mockbackend.make_bz( + product_get_enterable_args=None, + product_get_enterable_return=prod_list_return, + product_get_selectable_args=None, + product_get_selectable_return=prod_list_return, + product_get_args="data/mockargs/test_api_products_get1.txt", + product_get_return=prod_get_return, + ) + + # enterable products + fakebz.product_get(ptype="enterable") + fakebz.product_get(ptype="selectable") + with pytest.raises(RuntimeError): + fakebz.product_get(ptype="idontknow") + + # Double refresh things + fakebz.getproducts(force_refresh=True, ptype="enterable") + fakebz.getproducts(force_refresh=True, ptype="enterable") + + # getcomponents etc. testing + fakebz = tests.mockbackend.make_bz( + product_get_args="data/mockargs/test_api_products_get2.txt", + product_get_return=prod_get_return, + ) + + # Lookup in product cache by name + ret = fakebz.getcomponents("test-fake-product") + assert ret == compnames + # Lookup in product cache by id + ret = fakebz.getcomponents(7) + assert ret == compnames + # force_refresh but its cool + ret = fakebz.getcomponents("test-fake-product", force_refresh=True) + assert ret == compnames + + # getcomponentsdetails usage + fakebz = tests.mockbackend.make_bz( + product_get_args="data/mockargs/test_api_products_get3.txt", + product_get_return=prod_get_return, + ) + fakebz.getcomponentdetails("test-fake-product", "configuration") + + # Some bit to test productget exclude_args + fakebz = tests.mockbackend.make_bz( + product_get_args="data/mockargs/test_api_products_get4.txt", + product_get_return=prod_get_return) + fakebz.product_get(ids=["7"], exclude_fields=["product.foo"]) + + # Unknown product + fakebz = tests.mockbackend.make_bz( + product_get_args="data/mockargs/test_api_products_get5.txt", + product_get_return=prod_get_return) + with pytest.raises(bugzilla.BugzillaError): + fakebz.getcomponents(0) + + + +def test_bug_fields(): + fakebz = tests.mockbackend.make_bz( + bug_fields_args="data/mockargs/test_bug_fields.txt", + bug_fields_return="data/mockreturn/test_bug_fields.txt", + ) + ret = fakebz.getbugfields(names=["bug_status"]) + assert ["bug_status"] == ret diff --git a/tests/test_api_users.py b/tests/test_api_users.py new file mode 100644 index 00000000..6bc5a5b1 --- /dev/null +++ b/tests/test_api_users.py @@ -0,0 +1,75 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Test miscellaneous API bits +""" + +import pytest + +import bugzilla + +import tests +import tests.mockbackend + + +def test_api_users(): + # Basic API testing of the users APIs + user_ret = {'users': [ + {'can_login': True, + 'email': 'example1@example.com', + 'id': 1010101, + 'name': 'example1@example.com', + 'real_name': 'Mr. Example Man'}, + {'can_login': False, + 'email': 'example2@example.com', + 'id': 2222333, + 'name': 'example name', + 'real_name': 'Example real name', + 'saved_reports': [], + 'saved_searches': [], + 'groups': [ + {"id": 1, "name": "testgroup", "description": "somedesc"} + ]}, + ]} + + # getusers and User testing + fakebz = tests.mockbackend.make_bz( + user_get_args="data/mockargs/test_api_users_get1.txt", + user_get_return=user_ret, + user_update_args="data/mockargs/test_api_users_update1.txt", + user_update_return={}) + userobj = fakebz.getuser("example2@example.com") + + # Some userobj testing + userobj.refresh() + assert userobj.userid == 2222333 + assert userobj.email == "example2@example.com" + assert userobj.name == "example name" + assert userobj.can_login is False + userobj.updateperms("rem", ["fedora_contrib"]) + + # Catch a validation error + with pytest.raises(bugzilla.BugzillaError): + userobj.updateperms("badaction", ["newgroup"]) + + # createuser tests + fakebz = tests.mockbackend.make_bz( + user_get_args="data/mockargs/test_api_users_get2.txt", + user_get_return=user_ret, + user_create_args="data/mockargs/test_api_users_create.txt", + user_create_return={}) + userobj = fakebz.createuser("example1@example.com", "fooname", "foopass") + assert userobj.email == "example1@example.com" + + + # searchuser tests + fakebz = tests.mockbackend.make_bz( + user_get_args="data/mockargs/test_api_users_get3.txt", + user_get_return=user_ret) + userlist = fakebz.searchusers("example1@example.com") + assert len(userlist) == 2 diff --git a/tests/test_backend_rest.py b/tests/test_backend_rest.py new file mode 100644 index 00000000..14836975 --- /dev/null +++ b/tests/test_backend_rest.py @@ -0,0 +1,67 @@ +from types import MethodType + +from bugzilla._backendrest import _BackendREST +from bugzilla._session import _BugzillaSession + + +class TestGetBug: + @property + def session(self): + return _BugzillaSession(url="http://example.com", + user_agent="py-bugzilla-test", + sslverify=False, + cert=None, + tokencache={}, + api_key="", + is_redhat_bugzilla=False) + + @property + def backend(self): + return _BackendREST(url="http://example.com", + bugzillasession=self.session) + + def test_getbug__not_permissive(self): + backend = self.backend + + def _assertion(self, *args): + self.assertion_called = True + assert args and args[0] == url + + setattr(backend, "_get", MethodType(_assertion, backend)) + + for _ids, aliases, url in ( + (1, None, "/bug/1"), + ([1], [], "/bug/1"), + (None, "CVE-1999-0001", "/bug/CVE-1999-0001"), + ([], ["CVE-1999-0001"], "/bug/CVE-1999-0001"), + (1, "CVE-1999-0001", "/bug"), + ([1, 2], None, "/bug") + ): + backend.assertion_called = False + + backend.bug_get(_ids, aliases, {}) + + assert backend.assertion_called is True + + def test_getbug__permissive(self): + backend = self.backend + + def _assertion(self, *args): + self.assertion_called = True + assert args and args[0] == url and args[1] == params + + setattr(backend, "_get", MethodType(_assertion, backend)) + + for _ids, aliases, url, params in ( + (1, None, "/bug", {"id": [1], "alias": None}), + ([1], [], "/bug", {"id": [1], "alias": []}), + (None, "CVE-1999-0001", "/bug", {"alias": ["CVE-1999-0001"], "id": None}), + ([], ["CVE-1999-0001"], "/bug", {"alias": ["CVE-1999-0001"], "id": []}), + (1, "CVE-1999-0001", "/bug", {"id": [1], "alias": ["CVE-1999-0001"]}), + ([1, 2], None, "/bug", {"id": [1, 2], "alias": None}) + ): + backend.assertion_called = False + + backend.bug_get(_ids, aliases, {"permissive": True}) + + assert backend.assertion_called is True diff --git a/tests/test_base.py b/tests/test_base.py new file mode 100644 index 00000000..d62428e5 --- /dev/null +++ b/tests/test_base.py @@ -0,0 +1,20 @@ +from bugzilla.base import Bugzilla + + +def test_build_createbug(): + bz = Bugzilla(url=None) + + args = {"product": "Ubuntu 33⅓", "summary": "Hello World", "alias": "CVE-2024-0000"} + result = bz.build_createbug(**args) + assert result == args + + result = bz.build_createbug(groups=None, **args) + assert result == args + + args["groups"] = [] + result = bz.build_createbug(**args) + assert result == args + + args["groups"] += ["the-group"] + result = bz.build_createbug(**args) + assert result == args diff --git a/tests/test_cli_attach.py b/tests/test_cli_attach.py new file mode 100644 index 00000000..584858f1 --- /dev/null +++ b/tests/test_cli_attach.py @@ -0,0 +1,104 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import os + +import tests +import tests.mockbackend +import tests.utils + + +################################## +# 'bugzilla attach' mock testing # +################################## + +def test_attach(run_cli): + attachfile = os.path.dirname(__file__) + "/data/bz-attach-get1.txt" + attachcontent = open(attachfile).read() + + # Hit error when no ID specified + fakebz = tests.mockbackend.make_bz() + out = run_cli("bugzilla attach", fakebz, expectfail=True) + assert "ID must be specified" in out + + # Hit error when using tty and no --file specified + out = run_cli("bugzilla attach 123456", fakebz, expectfail=True) + assert "--file must be specified" in out + + # Hit error when using stdin, but no --desc + out = run_cli("bugzilla attach 123456", fakebz, expectfail=True, + stdin=attachcontent) + assert "--description must be specified" in out + + # Basic CLI attach + cmd = "bugzilla attach 123456 --file=%s " % attachfile + cmd += "--type text/x-patch --private " + cmd += "--comment 'some comment to go with it'" + fakebz = tests.mockbackend.make_bz( + bug_attachment_create_args="data/mockargs/test_attach1.txt", + bug_attachment_create_return={'ids': [1557949]}) + out = run_cli(cmd, fakebz) + assert "Created attachment 1557949 on bug 123456" in out + + # Attach from stdin + cmd = "bugzilla attach 123456 --file=fake-file-name.txt " + cmd += "--description 'Some attachment description' " + fakebz = tests.mockbackend.make_bz( + bug_attachment_create_args="data/mockargs/test_attach2.txt", + bug_attachment_create_return={'ids': [1557949]}) + out = run_cli(cmd, fakebz, stdin=attachcontent) + assert "Created attachment 1557949 on bug 123456" in out + + # Test --field passthrough + cmd = "bugzilla attach 123456 --file=%s " % attachfile + cmd += "--field=is_obsolete=1 " + cmd += "--field-json " + cmd += ('\'{"flags": [{"name": "review"' + ', "requestee": "crobinso@redhat.com", "status": "-"}]}\'') + fakebz = tests.mockbackend.make_bz( + bug_attachment_create_args="data/mockargs/test_attach3.txt", + bug_attachment_create_return={'ids': [1557949]}) + out = run_cli(cmd, fakebz) + assert "Created attachment 1557949 on bug 123456" in out + + +def _test_attach_get(run_cli): + # Hit error when using ids with --get* + fakebz = tests.mockbackend.make_bz() + out = run_cli("bugzilla attach 123456 --getall 123456", + fakebz, expectfail=True) + assert "not used for" in out + + # Basic --get ATTID usage + filename = "Klíč memorial test file.txt" + cmd = "bugzilla attach --get 112233" + fakebz = tests.mockbackend.make_bz( + bug_attachment_get_args="data/mockargs/test_attach_get1.txt", + bug_attachment_get_return="data/mockreturn/test_attach_get1.txt") + out = run_cli(cmd, fakebz) + assert filename in out + + # Basic --getall with --ignore-obsolete + cmd = "bugzilla attach --getall 663674 --ignore-obsolete" + fakebz = tests.mockbackend.make_bz( + bug_attachment_get_all_args="data/mockargs/test_attach_get2.txt", + bug_attachment_get_all_return="data/mockreturn/test_attach_get2.txt") + out = run_cli(cmd, fakebz) + + os.system("ls %s" % os.getcwd()) + filename += ".1" + assert filename in out + assert "bugzilla-filename" in out + + +def test_attach_get(run_cli): + import tempfile + import shutil + tmpdir = tempfile.mkdtemp(dir=os.getcwd()) + origcwd = os.getcwd() + os.chdir(tmpdir) + try: + _test_attach_get(run_cli) + finally: + os.chdir(origcwd) + shutil.rmtree(tmpdir) diff --git a/tests/test_cli_info.py b/tests/test_cli_info.py new file mode 100644 index 00000000..fe91e03d --- /dev/null +++ b/tests/test_cli_info.py @@ -0,0 +1,87 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import tests +import tests.mockbackend +import tests.utils + + +################################ +# 'bugzilla info' mock testing # +################################ + +def test_info(run_cli): + funcname = tests.utils.get_funcname() + argsprefix = "data/mockargs/%s_" % funcname + cliprefix = "data/clioutput/%s_" % funcname + + prod_accessible = {'ids': [1, 7]} + prod_get = {'products': [ + {'id': 1, 'name': 'Prod 1 Test'}, + {'id': 7, 'name': 'test-fake-product'} + ]} + + # info --products + fakebz = tests.mockbackend.make_bz( + product_get_accessible_args=None, + product_get_accessible_return=prod_accessible, + product_get_args=argsprefix + "products.txt", + product_get_return=prod_get) + cmd = "bugzilla info --products" + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "products.txt") + + # info --versions + prod_get_ver = {'products': [ + {'id': 7, 'name': 'test-fake-product', + 'versions': [ + {'id': 360, 'is_active': True, 'name': '7.1'}, + {'id': 123, 'is_active': True, 'name': 'fooversion!'}, + ]}, + ]} + fakebz = tests.mockbackend.make_bz( + product_get_args=argsprefix + "versions.txt", + product_get_return=prod_get_ver) + cmd = "bugzilla info --versions test-fake-product" + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "versions.txt") + + # info --components + prod_get_comp_active = {'products': [ + {'id': 7, 'name': 'test-fake-product', + 'components': [ + {'is_active': True, 'name': 'backend/kernel'}, + {'is_active': True, 'name': 'client-interfaces'}, + ]}, + ]} + cmd = "bugzilla info --components test-fake-product" + fakebz = tests.mockbackend.make_bz( + product_get_args=argsprefix + "components.txt", + product_get_return=prod_get_comp_active) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "components.txt") + + # info --components --active-components + cmd = "bugzilla info --components test-fake-product --active-components" + fakebz = tests.mockbackend.make_bz( + product_get_args=argsprefix + "components-active.txt", + product_get_return=prod_get_comp_active) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "components-active.txt") + + # info --components_owners + cmd = "bugzilla info --component_owners test-fake-product" + prod_get_comp_owners = {'products': [ + {'id': 7, 'name': 'test-fake-product', + 'components': [ + {'default_assigned_to': 'Fake Guy', + 'name': 'client-interfaces'}, + {'default_assigned_to': 'ANother fake dude!', + 'name': 'configuration'}, + ]}, + ]} + fakebz = tests.mockbackend.make_bz( + product_get_args=argsprefix + "components-owners.txt", + product_get_return=prod_get_comp_owners) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "components-owners.txt") diff --git a/tests/test_cli_login.py b/tests/test_cli_login.py new file mode 100644 index 00000000..fad2b7dd --- /dev/null +++ b/tests/test_cli_login.py @@ -0,0 +1,125 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import tempfile + +import pytest + +import bugzilla + +import tests +import tests.mockbackend +import tests.utils + + +################################# +# 'bugzilla login' mock testing # +################################# + +def test_login(run_cli): + cmd = "bugzilla login FOO BAR" + + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login.txt", + user_login_return=RuntimeError("TEST ERROR")) + out = run_cli(cmd, fakebz, expectfail=True) + assert "Login failed: TEST ERROR" in out + + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login.txt", + user_login_return={}) + out = run_cli(cmd, fakebz) + assert "Login successful" in out + + cmd = "bugzilla --restrict-login --user FOO --password BAR login" + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login-restrict.txt", + user_login_return={}) + out = run_cli(cmd, fakebz) + assert "Login successful" in out + + cmd = "bugzilla --ensure-logged-in --user FOO --password BAR login" + # Raises raw error trying to see if we aren't logged in + with pytest.raises(NotImplementedError): + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login.txt", + user_login_return={}, + user_get_args=None, + user_get_return=NotImplementedError()) + out = run_cli(cmd, fakebz) + + # Errors with expected code + cmd = "bugzilla --ensure-logged-in --user FOO --password BAR login" + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login.txt", + user_login_return={}, + user_get_args=None, + user_get_return=bugzilla.BugzillaError("TESTMESSAGE", code=505)) + out = run_cli(cmd, fakebz, expectfail=True) + assert "--ensure-logged-in passed but you" in out + + # Returns success for logged_in check and hits a tokenfile line + cmd = "bugzilla --ensure-logged-in " + cmd += "login FOO BAR" + tmp = tempfile.NamedTemporaryFile() + fakebz = tests.mockbackend.make_bz( + bz_kwargs={"use_creds": True, "tokenfile": tmp.name}, + user_login_args="data/mockargs/test_login.txt", + user_login_return={'id': 1234, 'token': 'my-fake-token'}, + user_get_args=None, + user_get_return={}) + fakebz.connect("https://example.com") + out = run_cli(cmd, fakebz) + assert "Token cache saved" in out + assert fakebz.tokenfile in out + assert "Consider using bugzilla API" in out + tests.utils.diff_compare(open(tmp.name).read(), + "data/clioutput/tokenfile.txt") + + # Returns success for logged_in check and hits another tokenfile line + cmd = "bugzilla --ensure-logged-in " + cmd += "login FOO BAR" + fakebz = tests.mockbackend.make_bz( + bz_kwargs={"use_creds": True, "tokenfile": None}, + user_login_args="data/mockargs/test_login.txt", + user_login_return={'id': 1234, 'token': 'my-fake-token'}, + user_get_args=None, + user_get_return={}) + out = run_cli(cmd, fakebz) + assert "Token not saved" in out + + +def test_interactive_login(monkeypatch, run_cli): + bz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_interactive_login.txt", + user_login_return={}, + user_logout_args=None, + user_logout_return={}, + user_get_args=None, + user_get_return={}) + + tests.utils.monkeypatch_getpass(monkeypatch) + cmd = "bugzilla login" + fakestdin = "fakeuser\nfakepass\n" + out = run_cli(cmd, bz, stdin=fakestdin) + assert "Bugzilla Username:" in out + assert "Bugzilla Password:" in out + + # API key prompting and saving + tmp = tempfile.NamedTemporaryFile() + bz.configpath = [tmp.name] + bz.url = "https://example.com" + + cmd = "bugzilla login --api-key" + fakestdin = "MY-FAKE-KEY\n" + out = run_cli(cmd, bz, stdin=fakestdin) + assert "API Key:" in out + assert tmp.name in out + tests.utils.diff_compare(open(tmp.name).read(), + "data/clioutput/test_interactive_login_apikey_rcfile.txt") + + # Check that we don't attempt to log in if API key is configured + assert bz.api_key + cmd = "bugzilla login" + out = run_cli(cmd, bz) + assert "already have an API" in out diff --git a/tests/test_cli_misc.py b/tests/test_cli_misc.py new file mode 100644 index 00000000..5a898bef --- /dev/null +++ b/tests/test_cli_misc.py @@ -0,0 +1,148 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Test miscellaneous CLI bits to get build out our code coverage +""" + +import base64 +import datetime +import json +import xmlrpc.client + +import pytest +import requests + +import bugzilla + +import tests +import tests.mockbackend + + +def testHelp(run_cli): + out = run_cli("bugzilla --help", None) + assert len(out.splitlines()) > 18 + + +def testCmdHelp(run_cli): + out = run_cli("bugzilla query --help", None) + assert len(out.splitlines()) > 40 + + +def testVersion(run_cli): + out = run_cli("bugzilla --version", None) + assert out.strip() == bugzilla.__version__ + + +def testCookiefileDeprecated(run_cli): + with pytest.raises(TypeError) as e: + run_cli("bugzilla --cookiefile foobar login", + None, expectfail=True) + assert "cookiefile is deprecated" in str(e) + + +def testPositionalArgs(run_cli): + # Make sure cli correctly rejects ambiguous positional args + out = run_cli("bugzilla login --xbadarg foo", + None, expectfail=True) + assert "unrecognized arguments: --xbadarg" in out + + out = run_cli("bugzilla modify 123456 --foobar --status NEW", + None, expectfail=True) + assert "unrecognized arguments: --foobar" in out + + +def testDebug(run_cli): + # Coverage testing for debug options + run_cli("bugzilla --bugzilla https:///BADURI --verbose login", + None, expectfail=True) + run_cli("bugzilla --bugzilla https:///BADURI --debug login", + None, expectfail=True) + + +def testExceptions(run_cli): + """ + Test exception handling around main() + """ + fakebz = tests.mockbackend.make_bz( + bug_search_args=None, + bug_search_return=KeyboardInterrupt()) + out = run_cli("bugzilla query --bug_id 1", fakebz, expectfail=True) + assert "user request" in out + + fakebz = tests.mockbackend.make_bz( + bug_search_args=None, + bug_search_return=bugzilla.BugzillaError("foo")) + out = run_cli("bugzilla query --bug_id 1", fakebz, expectfail=True) + assert "Server error:" in out + + fakebz = tests.mockbackend.make_bz( + bug_search_args=None, + bug_search_return=requests.exceptions.SSLError()) + out = run_cli("bugzilla query --bug_id 1", fakebz, expectfail=True) + assert "trust the remote server" in out + + fakebz = tests.mockbackend.make_bz( + bug_search_args=None, + bug_search_return=requests.exceptions.ConnectionError()) + out = run_cli("bugzilla query --bug_id 1", fakebz, expectfail=True) + assert "Connection lost" in out + + +def testManualURL(run_cli): + """ + Test passing a manual URL, to hit those non-testsuite code paths + """ + try: + cmd = "bugzilla --bztype foobar " + cmd += "--bugzilla https:///FAKEURL query --bug_id 1" + run_cli(cmd, None) + except Exception as e: + assert "No host supplied" in str(e) + + +def test_json_xmlrpc(run_cli): + # Test --json output with XMLRPC type conversion + cmd = "bugzilla query --json --id 1165434" + + timestr = '20181209T19:12:12' + dateobj = datetime.datetime.strptime(timestr, '%Y%m%dT%H:%M:%S') + + attachfile = tests.utils.tests_path("data/bz-attach-get1.txt") + attachdata = open(attachfile, "rb").read() + + bugid = 1165434 + data = {"bugs": [{ + 'id': bugid, + 'timetest': xmlrpc.client.DateTime(dateobj), + 'binarytest': xmlrpc.client.Binary(attachdata), + }]} + + fakebz = tests.mockbackend.make_bz( + bug_search_args=None, + bug_search_return={"bugs": [{"id": bugid}]}, + bug_get_args=None, + bug_get_return=data) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(tests.utils.sanitize_json(out), + "data/clioutput/test_json_xmlrpc.txt") + + retdata = json.loads(out)["bugs"][0] + assert (base64.b64decode(retdata["binarytest"]) == + attachdata) + assert retdata["timetest"] == dateobj.isoformat() + "Z" + + + # Test an error case, json converter can't handle Exception class + data["bugs"][0]["foo"] = Exception("foo") + fakebz = tests.mockbackend.make_bz( + bug_search_args=None, + bug_search_return={"bugs": [{"id": bugid}]}, + bug_get_args=None, + bug_get_return=data) + with pytest.raises(RuntimeError): + run_cli(cmd, fakebz, expectfail=True) diff --git a/tests/test_cli_modify.py b/tests/test_cli_modify.py new file mode 100644 index 00000000..e3731451 --- /dev/null +++ b/tests/test_cli_modify.py @@ -0,0 +1,94 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import tests +import tests.mockbackend +import tests.utils + + +################################## +# 'bugzilla modify' mock testing # +################################## + +def test_modify(run_cli): + # errors on missing args + cmd = "bugzilla modify 123456" + fakebz = tests.mockbackend.make_bz() + out = run_cli(cmd, fakebz, expectfail=True) + assert "additional arguments" in out + + # Modify basic + cmd = "bugzilla modify 123456 1234567 " + cmd += "--status ASSIGNED --component NEWCOMP " + fakebz = tests.mockbackend.make_bz( + bug_update_args="data/mockargs/test_modify1.txt", + bug_update_return={}) + out = run_cli(cmd, fakebz) + assert not out + + # Modify with lots of opts + cmd = "bugzilla modify 123456 --component NEWCOMP " + cmd += "--keyword +FOO --groups=-BAR --blocked =123456,445566 " + cmd += "--flag=-needinfo,+somethingelse " + cmd += "--whiteboard =foo --whiteboard =thisone " + cmd += "--dupeid 555666 " + cmd += "--comment 'some example comment' --private " + fakebz = tests.mockbackend.make_bz( + bug_update_args="data/mockargs/test_modify2.txt", + bug_update_return={}) + out = run_cli(cmd, fakebz) + assert not out + + # Modify with tricky opts hitting other API calls + cmd = "bugzilla modify 1165434 " + cmd += "--tags +addtag --tags=-rmtag " + cmd += "--qa_whiteboard +yo-qa --qa_whiteboard=-foo " + cmd += "--internal_whiteboard +internal-hey --internal_whiteboard +bar " + cmd += "--devel_whiteboard +devel-duh --devel_whiteboard=-yay " + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_update_tags_args="data/mockargs/test_modify3-tags.txt", + bug_update_tags_return={}, + bug_update_args="data/mockargs/test_modify3.txt", + bug_update_return={}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + assert not out + + # Modify hitting some rhbz paths + cmd = "bugzilla modify 1165434 " + cmd += "--fixed_in foofixedin " + cmd += "--component lvm2 " + cmd += "--sub-component some-sub-component" + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_update_args="data/mockargs/test_modify4.txt", + bug_update_return={}) + out = run_cli(cmd, fakebz) + assert not out + + # Modify with a slew of misc opt coverage + cmd = "bugzilla modify 1165434 " + cmd += "--assigned_to foo@example.com --qa_contact qa@example.com " + cmd += "--product newproduct " + cmd += "--blocked +1234 --blocked -1235 --blocked = " + cmd += "--url https://example.com " + cmd += "--cc=+bar@example.com --cc=-steve@example.com " + cmd += "--dependson=+2234 --dependson=-2235 --dependson = " + cmd += "--groups +foogroup " + cmd += "--keywords +newkeyword --keywords=-byekeyword --keywords = " + cmd += "--os windows --arch mips " + cmd += "--priority high --severity low " + cmd += "--summary newsummary --version 1.2.3 " + cmd += "--reset-assignee --reset-qa-contact " + cmd += "--alias fooalias " + cmd += "--target_release 1.2.4 --target_milestone beta " + cmd += "--devel_whiteboard =DEVBOARD --internal_whiteboard =INTBOARD " + cmd += "--qa_whiteboard =QABOARD " + cmd += "--comment-tag FOOTAG --field bar=foo " + cmd += '--field-json \'{"cf_verified": ["Tested"], "cf_blah": {"1": 2}}\' ' + cmd += "--minor-update " + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_update_args="data/mockargs/test_modify5.txt", + bug_update_return={}) + out = run_cli(cmd, fakebz) + assert not out diff --git a/tests/test_cli_new.py b/tests/test_cli_new.py new file mode 100644 index 00000000..86e153ab --- /dev/null +++ b/tests/test_cli_new.py @@ -0,0 +1,53 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import tests +import tests.mockbackend +import tests.utils + + +############################### +# 'bugzilla new' mock testing # +############################### + +def test_new(run_cli): + # Test a simpler creation + cmd = "bugzilla new --product FOOPROD --component FOOCOMP " + cmd += "--summary 'Hey this is the title!' " + cmd += "--comment 'This is the first comment!\nWith newline & stuff.' " + cmd += "--keywords ADDKEY --groups FOOGROUP,BARGROUP " + cmd += "--blocked 12345,6789 --cc foo@example.com --cc bar@example.com " + cmd += "--dependson dependme --private " + + fakebz = tests.mockbackend.make_bz( + bug_create_args="data/mockargs/test_new1.txt", + bug_create_return={"id": 1694158}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_new1.txt") + + # Test every option + cmd = "bugzilla new --product FOOPROD --component FOOCOMP " + cmd += "--summary 'Hey this is the title!' " + cmd += "--comment 'This is the first comment!\nWith newline & stuff.' " + cmd += "--keywords ADDKEY --groups FOOGROUP,BARGROUP " + cmd += "--blocked 12345,6789 --cc foo@example.com --cc bar@example.com " + cmd += "--dependson dependme --private " + cmd += "--os linux --arch mips --severity high --priority low " + cmd += "--url https://some.example.com " + cmd += "--version 5.6.7 --alias somealias " + cmd += "--sub-component FOOCOMP " + cmd += "--assignee foo@example.com --qa_contact qa@example.com " + cmd += "--comment-tag FOO " + cmd += "--field foo=bar " + cmd += '--field-json \'{"cf_verified": ["Tested"], "cf_blah": {"1": 2}}\' ' + + fakebz = tests.mockbackend.make_bz( + bug_create_args="data/mockargs/test_new2.txt", + bug_create_return={"id": 1694158}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug.txt", + rhbz=True) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_new2.txt") diff --git a/tests/test_cli_query.py b/tests/test_cli_query.py new file mode 100644 index 00000000..5a751a53 --- /dev/null +++ b/tests/test_cli_query.py @@ -0,0 +1,180 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import json +import os +import re + +import tests +import tests.mockbackend +import tests.utils + + +################################# +# 'bugzilla query' mock testing # +################################# + +def test_query(run_cli): + # bad --field option + fakebz = tests.mockbackend.make_bz() + cmd = "bugzilla query --field FOO" + out = run_cli(cmd, fakebz, expectfail=True) + assert "Invalid field argument" in out + + # bad --field-json option + fakebz = tests.mockbackend.make_bz() + cmd = "bugzilla query --field-json='{1: 2}'" + out = run_cli(cmd, fakebz, expectfail=True) + assert "Invalid field-json" in out + + # Simple query with some comma opts + cmd = "bugzilla query " + cmd += "--product foo --component foo,bar --bug_id 1234,2480" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query1.txt", + bug_search_return="data/mockreturn/test_query1.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query1.txt") + + # RHBZ query with a ton of opts + cmd = "bugzilla query " + cmd += "--product foo --component foo,bar --bug_id 1234,2480 " + cmd += "--keywords fribkeyword --fixed_in amifixed " + cmd += "--qa_whiteboard some-example-whiteboard " + cmd += "--cc foo@example.com --qa_contact qa@example.com " + cmd += "--comment 'some comment string' " + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_search_args="data/mockargs/test_query1-rhbz.txt", + bug_search_return="data/mockreturn/test_query1.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query1-rhbz.txt") + + # --emailtype handling + cmd = "bugzilla query --cc foo@example.com --emailtype BAR " + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_search_args="data/mockargs/test_query2-rhbz.txt", + bug_search_return="data/mockreturn/test_query1.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query2-rhbz.txt") + + # Same but with --ids output + cmd = "bugzilla query --ids " + cmd += "--product foo --component foo,bar --bug_id 1234,2480" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query1-ids.txt", + bug_search_return="data/mockreturn/test_query1.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query1-ids.txt") + + # Same but with --raw output + cmd = "bugzilla query --raw --bug_id 1165434" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query2.txt", + bug_search_return={"bugs": [{"id": 1165434}]}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + # Dictionary ordering is random, so scrub it from our output + out = re.sub(r"\{.*\}", r"'DICT SCRUBBED'", out, re.MULTILINE) + tests.utils.diff_compare(out, "data/clioutput/test_query2.txt") + + # Test a bunch of different combinations for code coverage + cmd = "bugzilla query --status ALL --severity sev1,sev2 " + cmd += "--outputformat='%{foo}:%{bar}::%{whiteboard}:" + cmd += "%{flags}:%{flags_requestee}%{whiteboard:devel}::" + cmd += "%{flag:needinfo}::%{comments}::%{external_bugs}'" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query3.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query3.txt") + + # Test --status DEV and --full + cmd = "bugzilla query --status DEV --full" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query4.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query4.txt") + + # Test --status QE and --extra, and components-file + compfile = os.path.dirname(__file__) + "/data/components_file.txt" + cmd = "bugzilla query --status QE --extra " + cmd += "--components_file %s" % compfile + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query5.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query5.txt") + + # Test --status EOL and --oneline, and some --field usage + cmd = "bugzilla query --status EOL --oneline " + cmd += "--field FOO=1 --field=BAR=WIBBLE " + cmd += '--field-json \'{"cf_verified": ["Tested"], "cf_blah": {"1": 2}}\' ' + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query6.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt", + bug_get_args="data/mockargs/test_query_cve_getbug.txt", + bug_get_return="data/mockreturn/test_query_cve_getbug.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query6.txt") + + # Test --status OPEN and --from-url + url = "https://bugzilla.redhat.com/buglist.cgi?bug_status=NEW&bug_status=ASSIGNED&bug_status=MODIFIED&bug_status=ON_DEV&bug_status=ON_QA&bug_status=VERIFIED&bug_status=FAILS_QA&bug_status=RELEASE_PENDING&bug_status=POST&classification=Fedora&component=virt-manager&order=bug_status%2Cbug_id&product=Fedora&query_format=advanced" # noqa + cmd = "bugzilla query --status OPEN --from-url %s" % url + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query7.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query7.txt") + + # Test --json output + cmd = "bugzilla query --json --id 1165434" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query8.txt", + bug_search_return={"bugs": [{"id": 1165434}]}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(tests.utils.sanitize_json(out), + "data/clioutput/test_query8.txt") + assert json.loads(out) + + # Test --json output + cmd = ("bugzilla query --json --id 1165434 " + "--includefield foo --includefield bar " + "--excludefield excludeme " + "--extrafield extrame1 --extrafield extrame2 ") + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_search_args="data/mockargs/test_query9.txt", + bug_search_return={"bugs": [{"id": 1165434}]}, + bug_get_args="data/mockargs/test_getbug_query9.txt", + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(tests.utils.sanitize_json(out), + "data/clioutput/test_query9.txt") + assert json.loads(out) + + + # Test every remaining option + cmd = "bugzilla query " + cmd += "--sub-component FOOCOMP " + cmd += "--version 5.6.7 --reporter me@example.com " + cmd += "--summary 'search summary' " + cmd += "--assignee bar@example.com " + cmd += "--blocked 12345 --dependson 23456 " + cmd += "--keywords FOO --keywords_type substring " + cmd += "--url https://example.com --url_type sometype " + cmd += "--target_release foo --target_milestone bar " + cmd += "--quicksearch 1 --savedsearch 2 --savedsearch-sharer-id 3 " + cmd += "--tags +foo --flag needinfo --alias somealias " + cmd += "--devel_whiteboard DEVBOARD " + cmd += "--priority wibble " + cmd += "--fixed_in 5.5.5 --fixed_in_type substring " + cmd += "--whiteboard FOO --status_whiteboard_type substring " + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query10.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt", + rhbz=True) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query10.txt") diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py new file mode 100644 index 00000000..6bb770da --- /dev/null +++ b/tests/test_ro_functional.py @@ -0,0 +1,518 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Unit tests that do readonly functional tests against real bugzilla instances. +""" +from xmlrpc.client import Fault + +import pytest + +import bugzilla +from bugzilla.exceptions import BugzillaError +import tests + + +REDHAT_URL = (tests.CLICONFIG.REDHAT_URL or + "https://bugzilla.redhat.com") + + +def _open_bz(url, bzclass=None, **kwargs): + if "use_creds" not in kwargs: + kwargs["use_creds"] = False + return tests.utils.open_functional_bz(bzclass or bugzilla.Bugzilla, + url, kwargs) + + +def _check(out, mincount, expectstr): + # Since we are running these tests against bugzilla instances in + # the wild, we can't depend on certain data like product lists + # remaining static. Use lax sanity checks in this case + if mincount is not None: + assert len(out.splitlines()) >= mincount + assert expectstr in out + + +def _test_version(bz, bzversion): + assert bz.bz_ver_major == bzversion[0] + assert bz.bz_ver_minor == bzversion[1] + + +def test_bugzilla_override(): + class MyBugzilla(bugzilla.Bugzilla): + pass + + bz = _open_bz("bugzilla.redhat.com", bzclass=MyBugzilla) + assert bz.__class__ is MyBugzilla + assert bz._is_redhat_bugzilla is True # pylint: disable=protected-access + + +# See also: tests/integration/ro_api_test.py::test_rest_xmlrpc_detection +def test_rest_xmlrpc_detection(): + # The default: use XMLRPC + bz = _open_bz("bugzilla.redhat.com") + assert bz.is_xmlrpc() + assert "/xmlrpc.cgi" in bz.url + assert bz.__class__ is bugzilla.RHBugzilla + + # See /rest in the URL, so use REST + bz = _open_bz("bugzilla.redhat.com/rest") + assert bz.is_rest() + with pytest.raises(bugzilla.BugzillaError) as e: + dummy = bz._proxy # pylint: disable=protected-access + assert "raw XMLRPC access is not provided" in str(e) + + # See /xmlrpc.cgi in the URL, so use XMLRPC + bz = _open_bz("bugzilla.redhat.com/xmlrpc.cgi") + assert bz.is_xmlrpc() + assert bz._proxy # pylint: disable=protected-access + + +# See also: tests/integration/ro_api_test.py::test_apikey_error_scraping +def test_apikey_error_scraping(): + # Ensure the API key does not leak into any requests exceptions + fakekey = "FOOBARMYKEY" + + with pytest.raises(Exception) as e: + _open_bz("https://bugzilla.redhat.nopedontexist", + force_rest=True, api_key=fakekey) + assert fakekey not in str(e.value) + + with pytest.raises(Exception) as e: + _open_bz("https://bugzilla.redhat.nopedontexist", + force_xmlrpc=True, api_key=fakekey) + assert fakekey not in str(e.value) + + with pytest.raises(Exception) as e: + _open_bz("https://httpstat.us/502&foo", + force_xmlrpc=True, api_key=fakekey) + assert "Client Error" in str(e.value) + assert fakekey not in str(e.value) + + with pytest.raises(Exception) as e: + _open_bz("https://httpstat.us/502&foo", + force_rest=True, api_key=fakekey) + assert "Client Error" in str(e.value) + assert fakekey not in str(e.value) + + +# See also: tests/integration/ro_api_test.py::test_xmlrpc_bad_url +def test_xmlrpc_bad_url(): + with pytest.raises(bugzilla.BugzillaError) as e: + _open_bz("https://example.com/#xmlrpc") + assert "URL may not be an XMLRPC URL" in str(e) + + +################### +# mozilla testing # +################### + +def test_mozilla(backends): + url = "bugzilla.mozilla.org" + bz = _open_bz(url, **backends) + + # bugzilla.mozilla.org returns version values in YYYY-MM-DD + # format, so just try to confirm that + assert bz.__class__ == bugzilla.Bugzilla + assert bz.bz_ver_major >= 2016 + assert bz.bz_ver_minor in range(1, 13) + + +################## +# gentoo testing # +################## + +def test_gentoo(backends): + url = "bugs.gentoo.org" + bzversion = (5, 0) + bz = _open_bz(url, **backends) + _test_version(bz, bzversion) + + # This is a bugzilla 5.0 instance, which supports URL queries now + query_url = ("https://bugs.gentoo.org/buglist.cgi?" + "component=[CS]&product=Doc%20Translations" + "&query_format=advanced&resolution=FIXED") + ret = bz.query(bz.url_to_query(query_url)) + assert len(ret) > 0 + + +################## +# redhat testing # +################## + + +# See also: tests/integration/ro_cli_test.py::test_get_products +def testInfoProducts(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + out = run_cli("bugzilla info --products", bz) + _check(out, 123, "Virtualization Tools") + + +# See also: tests/integration/ro_cli_test.py::test_get_components +def testInfoComps(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + out = run_cli("bugzilla info --components 'Virtualization Tools'", bz) + _check(out, 8, "virtinst") + + +# See also: tests/integration/ro_cli_test.py::test_get_versions +def testInfoVers(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + out = run_cli("bugzilla info --versions Fedora", bz) + _check(out, 17, "rawhide") + + +# See also: tests/integration/ro_cli_test.py::test_get_component_owners +def testInfoCompOwners(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + out = run_cli("bugzilla info " + "--component_owners 'Virtualization Tools'", bz) + _check(out, None, "libvirt: Libvirt Maintainers") + + +# See also: tests/integration/ro_cli_test.py::test_query +def testQuery(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + args = "--product Fedora --component python-bugzilla --version 14" + cli = "bugzilla query %s --bug_status CLOSED" % args + mincount = 4 + expectbug = "621030" + out = run_cli(cli, bz) + + assert len(out.splitlines()) >= mincount + assert bool([l1 for l1 in out.splitlines() if + l1.startswith("#" + expectbug)]) + + # Check --ids output option + out2 = run_cli(cli + " --ids", bz) + assert len(out.splitlines()) == len(out2.splitlines()) + assert bool([l2 for l2 in out2.splitlines() if + l2 == expectbug]) + + +# See also: tests/integration/ro_cli_test.py::test_query_full +def testQueryFull(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + bugid = "621601" + out = run_cli("bugzilla query --full --bug_id %s" % bugid, bz) + _check(out, 60, "end-of-life (EOL)") + + +# See also: tests/integration/ro_cli_test.py::test_query_raw +def testQueryRaw(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + bugid = "307471" + out = run_cli("bugzilla query --raw --bug_id %s" % bugid, bz) + _check(out, 70, "ATTRIBUTE[whiteboard]: bzcl34nup") + + +# See also: tests/integration/ro_cli_test.py::test_query_oneline +def testQueryOneline(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + bugid = "785016" + out = run_cli("bugzilla query --oneline --bug_id %s" % bugid, bz) + assert len(out.splitlines()) == 1 + assert out.splitlines()[0].startswith("#%s" % bugid) + assert "[---] fedora-review+,fedora-cvs+" in out + + bugid = "720784" + out = run_cli("bugzilla query --oneline --bug_id %s" % bugid, bz) + assert len(out.splitlines()) == 1 + assert out.splitlines()[0].startswith("#%s" % bugid) + assert " CVE-2011-2527" in out + + +# See also: tests/integration/ro_cli_test.py::test_query_extra +def testQueryExtra(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + bugid = "307471" + out = run_cli("bugzilla query --extra --bug_id %s" % bugid, bz) + assert ("#%s" % bugid) in out + assert " +Status Whiteboard: bzcl34nup" in out + + +# See also: tests/integration/ro_cli_test.py::test_query_format +def testQueryFormat(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + args = ("--bug_id 307471 --outputformat=\"id=%{bug_id} " + "sw=%{whiteboard:status} needinfo=%{flag:needinfo} " + "sum=%{summary}\"") + out = run_cli("bugzilla query %s" % args, bz) + assert "id=307471 sw= bzcl34nup needinfo= " in out + + args = ("--bug_id 785016 --outputformat=\"id=%{bug_id} " + "sw=%{whiteboard:status} flag=%{flag:fedora-review} " + "sum=%{summary}\"") + out = run_cli("bugzilla query %s" % args, bz) + assert "id=785016 sw= flag=+" in out + + # Unicode in this bug's summary + args = "--bug_id 522796 --outputformat \"%{summary}\"" + out = run_cli("bugzilla query %s" % args, bz) + assert "V34 — system" in out + + +# See also: tests/integration/ro_cli_test.py::test_query_url +def testQueryURL(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + qurl = ("/buglist.cgi?f1=creation_ts" + "&list_id=973582&o1=greaterthaneq&classification=Fedora&" + "o2=lessthaneq&query_format=advanced&f2=creation_ts" + "&v1=2010-01-01&component=python-bugzilla&v2=2010-06-01" + "&product=Fedora") + + url = REDHAT_URL + if "/xmlrpc.cgi" in url: + url = url.replace("/xmlrpc.cgi", qurl) + else: + url += qurl + out = run_cli("bugzilla query --from-url \"%s\"" % url, bz) + _check(out, 10, "#553878 CLOSED") + + +def testQueryFixedIn(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + out = run_cli("bugzilla query --fixed_in anaconda-15.29-1", bz) + assert len(out.splitlines()) == 4 + assert "#629311 CLOSED" in out + + +def testQueryExtrafieldPool(run_cli, backends): + # rhbz has an agile 'pool' extension but doesn't return the field + # by default. Check that '-extrafield pool' returns it for --json output + bz = _open_bz(REDHAT_URL, **backends) + + out1 = run_cli("bugzilla query --id 1717616 --json", bz) + out2 = run_cli("bugzilla query --id 1717616 --json --extrafield pool", bz) + assert "current_sprint_id" not in out1 + assert "current_sprint_id" in out2 + + +# See also: tests/integration/ro_api_test.py::test_get_component_detail +def testComponentsDetails(backends): + """ + Fresh call to getcomponentsdetails should properly refresh + """ + bz = _open_bz(REDHAT_URL, **backends) + + assert bool(bz.getcomponentsdetails("Red Hat Developer Toolset")) + + +# See also: tests/integration/ro_api_test.py::test_get_bug_alias +def testGetBugAlias(backends): + """ + getbug() works if passed an alias + """ + bz = _open_bz(REDHAT_URL, **backends) + + bug = bz.getbug("CVE-2011-2527") + assert bug.bug_id == 720773 + + +# See also: tests/integration/ro_api_test.py::test_get_bug_404 +def testGetBug404(backends): + """ + getbug() is expected to raise an error, if a bug ID or alias does not exist + """ + bz = _open_bz(REDHAT_URL, **backends) + + try: + bz.getbug(100000000) + except Fault as error: # XMLRPC API + assert error.faultCode == 101 + except BugzillaError as error: # REST API + assert error.code == 101 + else: + raise AssertionError("No exception raised") + + +# See also: tests/integration/ro_api_test.py::test_get_bug_alias_404 +def testGetBugAlias404(backends): + """ + getbug() is expected to raise an error, if a bug ID or alias does not exist + """ + bz = _open_bz(REDHAT_URL, **backends) + + try: + bz.getbug("CVE-1234-4321") + except Fault as error: # XMLRPC API + assert error.faultCode == 100 + except BugzillaError as error: # REST API + assert error.code == 100 + else: + raise AssertionError("No exception raised") + + +# See also: tests/integration/ro_api_test.py::test_get_bug_alias_included_field +def testGetBugAliasIncludedField(backends): + bz = _open_bz(REDHAT_URL, **backends) + + bug = bz.getbug("CVE-2011-2527", include_fields=["id"]) + assert bug.bug_id == 720773 + + +def testQuerySubComponent(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + # Test special error wrappers in bugzilla/_cli.py + out = run_cli("bugzilla query --product 'Red Hat Enterprise Linux 7' " + "--component lvm2 --sub-component 'Thin Provisioning'", bz) + assert len(out.splitlines()) >= 3 + assert "#1060931 " in out + + +# See also: tests/integration/ro_api_test.py::test_get_bug_fields +def testBugFields(backends): + bz = _open_bz(REDHAT_URL, **backends) + + fields = bz.getbugfields(names=["product"])[:] + assert fields == ["product"] + bz.getbugfields(names=["product", "bug_status"], force_refresh=True) + assert set(bz.bugfields) == set(["product", "bug_status"]) + + +# See also: tests/integration/ro_api_test.py::test_get_product +def testProductGetMisc(backends): + bz = _open_bz(REDHAT_URL, **backends) + + assert bz.product_get(ptype="enterable", include_fields=["id"]) + assert bz.product_get(ptype="selectable", include_fields=["name"]) + + +# See also: tests/integration/ro_api_test.py::test_query_autorefresh +def testBugAutoRefresh(backends): + bz = _open_bz(REDHAT_URL, **backends) + + bz.bug_autorefresh = True + + bug = bz.query(bz.build_query(bug_id=720773, + include_fields=["summary"]))[0] + assert hasattr(bug, "component") + assert bool(bug.component) + + bz.bug_autorefresh = False + + bug = bz.query(bz.build_query(bug_id=720773, + include_fields=["summary"]))[0] + assert not hasattr(bug, "component") + try: + assert bool(bug.component) + except Exception as e: + assert "adjust your include_fields" in str(e) + + +# See also (in part): tests/integration/ro_api_test.py::test_get_bug_exclude_fields +def testExtraFields(backends): + bz = _open_bz(REDHAT_URL, **backends) + + # Check default extra_fields will pull in comments + bug = bz.getbug(720773, exclude_fields=["product"]) + assert "comments" in dir(bug) + assert "product" not in dir(bug) + + # Ensure that include_fields overrides default extra_fields + bug = bz.getbug(720773, include_fields=["summary"]) + assert "summary" in dir(bug) + assert "comments" not in dir(bug) + + +def testExternalBugsOutput(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + out = run_cli('bugzilla query --bug_id 989253 ' + '--outputformat="%{external_bugs}"', bz) + assert "bugzilla.gnome.org/show_bug.cgi?id=703421" in out + assert "External bug: https://bugs.launchpad.net/bugs/1203576" in out + + +# See also: tests/integration/ro_cli_test.py::test_get_active_components +def testActiveComps(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + out = run_cli("bugzilla info --components 'Virtualization Tools' " + "--active-components", bz) + assert "virtinst" not in out + out = run_cli("bugzilla info --component_owners 'Virtualization Tools' " + "--active-components", bz) + assert "virtinst" not in out + + +# See also: tests/integration/ro_cli_test.py::test_fails +def testFaults(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + # Test special error wrappers in bugzilla/_cli.py + out = run_cli("bugzilla query --field=IDONTEXIST=FOO", bz, + expectfail=True) + assert "Server error:" in out + + out = run_cli("bugzilla " + "--bugzilla https://example.com/xmlrpc.cgi " + "query --field=IDONTEXIST=FOO", None, expectfail=True) + assert "Connection lost/failed" in out + + out = run_cli("bugzilla " + "--bugzilla https://expired.badssl.com/ " + "query --bug_id 1234", None, expectfail=True) + assert "trust the remote server" in out + assert "--nosslverify" in out + + +# See also: tests/integration/ro_api_test.py::test_login_stubs +def test_login_stubs(backends): + bz = _open_bz(REDHAT_URL, **backends) + + # In 2024 bugzilla.redhat.com disabled User.login and User.logout APIs + # for xmlrpc API + + with pytest.raises(bugzilla.BugzillaError) as e: + bz.login("foo", "bar") + assert "Login failed" in str(e) + + is_rest = bz.is_rest() + is_xmlrpc = bz.is_xmlrpc() + + msg = None + try: + bz.logout() + except Exception as error: + msg = str(error) + + if is_rest and msg: + raise AssertionError("didn't expect exception: %s" % msg) + if is_xmlrpc: + assert "'User.logout' was not found" in str(msg) + + +def test_redhat_version(backends): + bzversion = (5, 0) + bz = _open_bz(REDHAT_URL, **backends) + + if not tests.CLICONFIG.REDHAT_URL: + _test_version(bz, bzversion) + + +# See also: tests/integration/ro_api_test.py::test_bug_url +def test_bug_misc(backends): + bz = _open_bz(REDHAT_URL, **backends) + + # Ensure weburl is generated consistently whether + # we are using XMLRPC or REST + bug = bz.getbug(720773) + assert bug.weburl == "https://bugzilla.redhat.com/show_bug.cgi?id=720773" diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py new file mode 100644 index 00000000..600fa7ed --- /dev/null +++ b/tests/test_rw_functional.py @@ -0,0 +1,1072 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Unit tests that do permanent functional against a real bugzilla instances. +""" + +import datetime +import inspect +import os +import random +import sys + +import bugzilla + +import tests +import tests.mockbackend +import tests.utils + + +RHURL = tests.CLICONFIG.REDHAT_URL or "bugzilla.stage.redhat.com" + + +################## +# helper methods # +################## + +def _split_int(s): + return [int(i) for i in s.split(",")] + + +def _open_bz(**kwargs): + return tests.utils.open_functional_bz(bugzilla.RHBugzilla, RHURL, kwargs) + + +if not _open_bz().logged_in: + print("\nR/W tests require cached login credentials for url=%s\n" % RHURL) + sys.exit(1) + + +def _check_have_admin(bz): + funcname = inspect.stack()[1][3] + + # groupnames is empty for any user if our logged in user does not + # have admin privs. + # Check a known account that likely won't ever go away + ret = bool(bz.getuser("anaconda-maint-list@redhat.com").groupnames) + if not ret: + print("\nNo admin privs, reduced testing of %s" % funcname) + return ret + + +def _set_have_dev(bug, assigned_to): + # This will only take effect if the logged in user has fedora dev perms + have_dev = bug.assigned_to == assigned_to + bug._testsuite_have_dev = have_dev # pylint: disable=protected-access + + +def _bug_close(run_cli, bug): + # Pre-close it + bz = bug.bugzilla + run_cli("bugzilla modify --close NOTABUG %s --minor-update" % bug.id, bz) + bug.refresh() + assert bug.status == "CLOSED" + assert bug.resolution == "NOTABUG" + + +def _makebug(run_cli, bz): + """ + Make a basic bug that the logged in user can maximally manipulate + """ + product = "Fedora" + component = "python-bugzilla" + version = "rawhide" + assigned_to = "triage@lists.fedoraproject.org" + summary = ("python-bugzilla test basic bug %s" % + datetime.datetime.today()) + newout = run_cli("bugzilla new " + f"--product '{product}' " + f"--component '{component}' " + f"--version '{version}' " + f"--assigned_to '{assigned_to}' " + f"--summary \"{summary}\" " + "--comment \"Test bug from the python-bugzilla test suite\" " + "--outputformat \"%{bug_id}\"", bz) + + bugid = int(newout.splitlines()[-1]) + bug = bz.getbug(bugid) + print("\nCreated bugid: %s" % bug.id) + + assert bug.component == component + assert bug.version == version + assert bug.summary == summary + + _set_have_dev(bug, assigned_to) + _bug_close(run_cli, bug) + + return bug + + +def _check_have_dev(bug): + funcname = inspect.stack()[1][3] + have_dev = bug._testsuite_have_dev # pylint: disable=protected-access + + if not have_dev: + print("\nNo dev privs, reduced testing of %s" % funcname) + return have_dev + + +class _BugCache: + cache = {} + + @classmethod + def get_bug(cls, run_cli, bz): + key = bz.is_xmlrpc() and "xmlrpc" or "rest" + if key not in cls.cache: + cls.cache[key] = _makebug(run_cli, bz) + return cls.cache[key] + + +def _make_subcomponent_bug(run_cli, bz): + """ + Helper for creating a bug that can handle rhbz sub components + """ + summary = ("python-bugzilla test manyfields bug %s" % + datetime.datetime.today()) + assigned_to = "triage@lists.fedoraproject.org" + url = "http://example.com" + osval = "Windows" + cc = "triage@lists.fedoraproject.org" + assigned_to = "triage@lists.fedoraproject.org" + blocked = "461686,461687" + dependson = "427301" + comment = "Test bug from python-bugzilla test suite" + # We use this product+component to test sub_component + product = "Bugzilla" + component = "Extensions" + version = "5.0" + sub_component = "AgileTools" + alias = "pybz-%s" % datetime.datetime.today().strftime("%s") + newout = run_cli("bugzilla new " + f"--product '{product}' " + f"--version '{version}' " + f"--component '{component}' " + f"--sub-component '{sub_component}' " + f"--summary \"{summary}\" " + f"--comment \"{comment}\" " + f"--url {url} " + f"--os {osval} " + f"--cc {cc} " + f"--assigned_to {assigned_to} " + f"--blocked {blocked} " + f"--dependson {dependson} " + f"--alias {alias} " + "--arch ppc --severity Urgent --priority Low " + "--outputformat \"%{bug_id}\"", bz) + + bugid = int(newout.splitlines()[-1]) + bug = bz.getbug(bugid, extra_fields=["sub_components"]) + print("\nCreated bugid: %s" % bugid) + + _set_have_dev(bug, assigned_to) + have_dev = _check_have_dev(bug) + + assert bug.summary == summary + assert bug.bug_file_loc == url + assert bug.op_sys == osval + assert all([e in bug.cc for e in cc.split(",")]) + assert bug.longdescs[0]["text"] == comment + assert bug.sub_components == {component: [sub_component]} + assert bug.alias == [alias] + + if have_dev: + assert bug.blocks == _split_int(blocked) + assert bug.depends_on == _split_int(dependson) + else: + # Using a non-dev account seems to fail to set these at bug create time + assert bug.blocks == [] + assert bug.depends_on == [] + + _bug_close(run_cli, bug) + + return bug + + +############## +# test cases # +############## + +# See also: tests/integration/rw_api_test.py::test_logged_in_no_creds +def test0LoggedInNoCreds(backends): + bz = _open_bz(**backends, use_creds=False) + assert not bz.logged_in + + +def test0ClassDetection(): + bz = bugzilla.Bugzilla(RHURL, use_creds=False) + assert bz.__class__ is bugzilla.RHBugzilla + + +# See also: tests/integration/rw_api_test.py::test_create_bug +# tests/integration/rw_api_test.py::test_create_bug_alias +# tests/integration/rw_api_test.py::test_update_bug +def test04NewBugAllFields(run_cli, backends): + """ + Create a bug using all 'new' fields, check some values, close it + """ + bz = _open_bz(**backends) + bug = _make_subcomponent_bug(run_cli, bz) + + # Verify hasattr works + assert hasattr(bug, "id") + assert hasattr(bug, "bug_id") + + # Close the bug + run_cli("bugzilla modify " + "--close WONTFIX %s " % + bug.id, bz) + bug.refresh() + assert bug.status == "CLOSED" + assert bug.resolution == "WONTFIX" + + # Check bug's minimal history + ret = bug.get_history_raw() + assert len(ret["bugs"]) == 1 + assert len(ret["bugs"][0]["history"]) + + +def test05ModifyStatus(run_cli, backends): + """ + Modify status and comment fields for an existing bug + """ + bz = _open_bz(**backends) + bug = _BugCache.get_bug(run_cli, bz) + have_dev = _check_have_dev(bug) + cmd = "bugzilla modify %s " % bug.id + + origstatus = bug.status + perm_error = "not allowed to (un)mark comments" + + # Set to ON_QA with a private comment + try: + status = "ON_QA" + comment = ("changing status to %s at %s" % + (status, datetime.datetime.today())) + run_cli(cmd + + "--status %s --comment \"%s\" --private" % (status, comment), bz) + + bug.refresh() + assert bug.status == status + assert bug.longdescs[-1]["is_private"] == 1 + assert bug.longdescs[-1]["text"] == comment + except RuntimeError as e: + if have_dev: + raise + assert perm_error in str(e) + + # Close bug as DEFERRED with a private comment + try: + resolution = "DEFERRED" + comment = ("changing status to CLOSED=%s at %s" % + (resolution, datetime.datetime.today())) + run_cli(cmd + + "--close %s --comment \"%s\" --private" % + (resolution, comment), bz) + + bug.refresh() + assert bug.status == "CLOSED" + assert bug.resolution == resolution + assert bug.comments[-1]["is_private"] == 1 + assert bug.comments[-1]["text"] == comment + except RuntimeError as e: + if have_dev: + raise + assert perm_error in str(e) + + # Set to assigned + run_cli(cmd + "--status ASSIGNED", bz) + bug.refresh() + assert bug.status == "ASSIGNED" + + # Close bug as dup with no comment + dupeid = "461686" + desclen = len(bug.longdescs) + run_cli(cmd + + "--close DUPLICATE --dupeid %s" % dupeid, bz) + + bug.refresh() + assert bug.dupe_of == int(dupeid) + assert len(bug.longdescs) == (desclen + 1) + assert "marked as a duplicate" in bug.longdescs[-1]["text"] + + # bz.setstatus test + try: + comment = ("adding lone comment at %s" % datetime.datetime.today()) + bug.setstatus("POST", comment=comment, private=True) + bug.refresh() + assert bug.longdescs[-1]["is_private"] == 1 + assert bug.longdescs[-1]["text"] == comment + assert bug.status == "POST" + except Exception as e: + if have_dev: + raise + assert perm_error in str(e) + + # See also: tests/integration/rw_api_test.py::test_close_bug + # bz.close test + fixed_in = str(datetime.datetime.today()) + bug.close("ERRATA", fixedin=fixed_in) + bug.refresh() + assert bug.status == "CLOSED" + assert bug.resolution == "ERRATA" + assert bug.fixed_in == fixed_in + + # See also: tests/integration/rw_api_test.py::test_add_comment + # bz.addcomment test + comment = ("yet another test comment %s" % datetime.datetime.today()) + bug.addcomment(comment, private=False) + bug.refresh() + assert bug.longdescs[-1]["text"] == comment + assert bug.longdescs[-1]["is_private"] == 0 + + # Confirm comments is same as get_comments + assert bug.comments == bug.get_comments() + # This method will be removed in a future version + assert bug.comments == bug.getcomments() + assert bug.get_comments() == bug.getcomments() + + # Reset state + run_cli(cmd + "--status %s" % origstatus, bz) + bug.refresh() + assert bug.status == origstatus + + +# See also: tests/integration/rw_api_test.py::test_update_bug +def test06ModifyEmails(run_cli, backends): + """ + Modify cc, assignee, qa_contact for existing bug + """ + bz = _open_bz(**backends) + bug = _BugCache.get_bug(run_cli, bz) + user = bug.creator + have_dev = _check_have_dev(bug) + + cmd = "bugzilla modify %s " % bug.id + + # Test CC list and reset it + email1 = "triage@lists.fedoraproject.org" + run_cli(cmd + "--cc %s --cc %s" % (email1, user), bz) + bug.refresh() + assert email1 in bug.cc + assert user in bug.cc + + # Remove CC via command line + # Unprivileged user can only add/remove their own CC value + run_cli(cmd + "--cc=-%s" % user, bz) + bug.refresh() + assert user not in bug.cc + + # Re-add CC via API + bug.addcc(user) + bug.refresh() + assert user in bug.cc + + # Remove it again, via API + bug.deletecc(user) + bug.refresh() + assert user not in bug.cc + assert bug.cc + + perm_error = "required permissions may change that field" + + # Test assigned and QA target + try: + run_cli(cmd + "--assignee %s --qa_contact %s" % (email1, email1), bz) + bug.refresh() + assert bug.assigned_to == email1 + assert bug.qa_contact == email1 + except RuntimeError as e: + if have_dev: + raise + assert perm_error in str(e) + + + # Test --reset options + try: + run_cli(cmd + "--reset-qa-contact --reset-assignee", bz) + bug.refresh() + assert bug.assigned_to != email1 + assert bug.qa_contact != email1 + except RuntimeError as e: + if have_dev: + raise + assert perm_error in str(e) + + +# See also: tests/integration/rw_api_test.py::test_update_flags +def test070ModifyMultiFlags(run_cli, backends): + """ + Modify flags and fixed_in for 2 bugs + """ + bz = _open_bz(**backends) + bugid1 = _BugCache.get_bug(run_cli, bz).id + bugid2 = _makebug(run_cli, bz).id + cmd = "bugzilla modify %s %s " % (bugid1, bugid2) + + def flagstr(b): + ret = [] + for flag in b.flags: + ret.append(flag["name"] + flag["status"]) + return " ".join(sorted(ret)) + + def cleardict_old(b): + """ + Clear flag dictionary, for format meant for bug.updateflags + """ + clearflags = {} + for flag in b.flags: + clearflags[flag["name"]] = "X" + return clearflags + + def cleardict_new(b): + """ + Clear flag dictionary, for format meant for update_bugs + """ + clearflags = [] + for flag in b.flags: + clearflags.append({"name": flag["name"], "status": "X"}) + return clearflags + + bug1 = bz.getbug(bugid1) + if cleardict_old(bug1): + bug1.updateflags(cleardict_old(bug1)) + bug2 = bz.getbug(bugid2) + if cleardict_old(bug2): + bug2.updateflags(cleardict_old(bug2)) + + + # Set flags and confirm + setflags = "fedora_prioritized_bug? needinfo+" + run_cli(cmd + + " ".join([(" --flag " + f) for f in setflags.split()]), bz) + + bug1.refresh() + bug2.refresh() + + assert flagstr(bug1) == setflags + assert flagstr(bug2) == setflags + assert bug1.get_flags("needinfo")[0]["status"] == "+" + assert bug1.get_flag_status("fedora_prioritized_bug") == "?" + + # Clear flags + if cleardict_new(bug1): + bz.update_flags(bug1.id, cleardict_new(bug1)) + bug1.refresh() + if cleardict_new(bug2): + bz.update_flags(bug2.id, cleardict_new(bug2)) + bug2.refresh() + + # pylint: disable=use-implicit-booleaness-not-comparison + assert cleardict_old(bug1) == {} + assert cleardict_old(bug2) == {} + + # Set "Fixed In" field + origfix1 = bug1.fixed_in + origfix2 = bug2.fixed_in + + newfix = origfix1 and (origfix1 + "-new1") or "blippy1" + if newfix == origfix2: + newfix = origfix2 + "-2" + + run_cli(cmd + "--fixed_in '%s'" % newfix, bz) + + bug1.refresh() + bug2.refresh() + assert bug1.fixed_in == newfix + assert bug2.fixed_in == newfix + + # Reset fixed_in + run_cli(cmd + "--fixed_in \"-\"", bz) + + bug1.refresh() + bug2.refresh() + assert bug1.fixed_in == "-" + assert bug2.fixed_in == "-" + + +def test071ModifyMisc(run_cli, backends): + bz = _open_bz(**backends) + bug = _BugCache.get_bug(run_cli, bz) + have_dev = _check_have_dev(bug) + cmd = "bugzilla modify %s " % bug.id + + # modify --dependson + run_cli(cmd + "--dependson 123456", bz) + bug.refresh() + assert 123456 in bug.depends_on + run_cli(cmd + "--dependson =111222", bz) + bug.refresh() + assert [111222] == bug.depends_on + run_cli(cmd + "--dependson=-111222", bz) + bug.refresh() + assert [] == bug.depends_on + + # modify --blocked + run_cli(cmd + "--blocked 123,456", bz) + bug.refresh() + assert [123, 456] == bug.blocks + run_cli(cmd + "--blocked =", bz) + bug.refresh() + assert [] == bug.blocks + + # modify --keywords + origkw = bug.keywords + run_cli(cmd + "--keywords +Documentation --keywords EasyFix", bz) + bug.refresh() + assert set(["Documentation", "EasyFix"] + origkw) == set(bug.keywords) + run_cli(cmd + "--keywords=-EasyFix --keywords=-Documentation", bz) + bug.refresh() + assert origkw == bug.keywords + + perm_error = "user with the required permissions" + + try: + # modify --target_release + # modify --target_milestone + targetbugid = 492463 + targetbug = bz.getbug(targetbugid) + targetcmd = "bugzilla modify %s " % targetbugid + run_cli(targetcmd + + "--target_milestone beta --target_release 6.2", bz) + targetbug.refresh() + assert targetbug.target_milestone == "beta" + assert targetbug.target_release == ["6.2"] + run_cli(targetcmd + + "--target_milestone rc --target_release 6.10", bz) + targetbug.refresh() + assert targetbug.target_milestone == "rc" + assert targetbug.target_release == ["6.10"] + except RuntimeError as e: + # As of Nov 2024 this needs even extra permissions, probably + # due to RHEL products being locked down + # if have_dev: + # raise + assert perm_error in str(e) + + try: + # modify --priority + # modify --severity + run_cli(cmd + "--priority low --severity high", bz) + bug.refresh() + assert bug.priority == "low" + assert bug.severity == "high" + run_cli(cmd + "--priority medium --severity medium", bz) + bug.refresh() + assert bug.priority == "medium" + assert bug.severity == "medium" + except RuntimeError as e: + if have_dev: + raise + assert perm_error in str(e) + + # modify --os + # modify --platform + # modify --version + run_cli(cmd + "--version rawhide --os Windows --arch ppc " + "--url http://example.com", bz) + bug.refresh() + assert bug.version == "rawhide" + assert bug.op_sys == "Windows" + assert bug.platform == "ppc" + assert bug.url == "http://example.com" + run_cli(cmd + "--version rawhide --os Linux --arch s390 " + "--url http://example.com/fribby", bz) + bug.refresh() + assert bug.version == "rawhide" + assert bug.op_sys == "Linux" + assert bug.platform == "s390" + assert bug.url == "http://example.com/fribby" + + # modify --field + run_cli(cmd + "--field cf_fixed_in=foo-bar-1.2.3 \ + --field=cf_release_notes=baz", bz) + + bug.refresh() + assert bug.fixed_in == "foo-bar-1.2.3" + assert bug.cf_release_notes == "baz" + + +def test08Attachments(run_cli, backends): + tmpdir = "__test_attach_output" + if tmpdir in os.listdir("."): + os.system("rm -r %s" % tmpdir) + os.mkdir(tmpdir) + os.chdir(tmpdir) + + try: + _test8Attachments(run_cli, backends) + finally: + os.chdir("..") + os.system("rm -r %s" % tmpdir) + + +def _test8Attachments(run_cli, backends): + """ + Get and set attachments for a bug + """ + bz = _open_bz(**backends) + cmd = "bugzilla attach " + testfile = "../tests/data/bz-attach-get1.txt" + + # Add attachment as CLI option + setbug = _BugCache.get_bug(run_cli, bz) + setbug = bz.getbug(setbug.id, extra_fields=["attachments"]) + orignumattach = len(setbug.attachments) + + # Add attachment from CLI with mime guessing + desc1 = "python-bugzilla cli upload %s" % datetime.datetime.today() + out1 = run_cli(cmd + "%s --description \"%s\" --file %s" % + (setbug.id, desc1, testfile), bz) + out1 = out1.splitlines()[-1] + + desc2 = "python-bugzilla cli upload %s" % datetime.datetime.today() + out2 = run_cli(cmd + "%s --file test --summary \"%s\"" % + (setbug.id, desc2), bz, stdin=open(testfile).read()) + + # Expected output format: + # Created attachment on bug + + setbug.refresh() + assert len(setbug.attachments) == (orignumattach + 2) + + att1 = setbug.attachments[-2] + attachid = att1["id"] + assert att1["summary"] == desc1 + assert att1["id"] == int(out1.splitlines()[0].split()[2]) + assert att1["content_type"] == "text/plain" + + att2 = setbug.attachments[-1] + assert att2["summary"] == desc2 + assert att2["id"] == int(out2.splitlines()[0].split()[2]) + assert att2["content_type"] == "application/octet-stream" + + # Set attachment flags + assert att1["flags"] == [] + bz.updateattachmentflags(setbug.id, att2["id"], "review", status="+") + setbug.refresh() + + assert len(setbug.attachments[-1]["flags"]) == 1 + assert setbug.attachments[-1]["flags"][0]["name"] == "review" + assert setbug.attachments[-1]["flags"][0]["status"] == "+" + + bz.updateattachmentflags(setbug.id, setbug.attachments[-1]["id"], + "review", status="X") + setbug.refresh() + assert setbug.attachments[-1]["flags"] == [] + + # Set attachment obsolete + bz._backend.bug_attachment_update( # pylint: disable=protected-access + [setbug.attachments[-1]["id"]], + {"is_obsolete": 1}) + setbug.refresh() + assert setbug.attachments[-1]["is_obsolete"] == 1 + + + # Get attachment, verify content + out = run_cli(cmd + "--get %s" % attachid, bz).splitlines() + + # Expect format: + # Wrote + fname = out[0].split()[1].strip() + + assert len(out) == 1 + assert fname == "bz-attach-get1.txt" + assert open(fname).read() == open(testfile).read() + os.unlink(fname) + + # Get all attachments + getbug = bz.getbug(setbug.id) + getbug.autorefresh = True + numattach = len(getbug.attachments) + out = run_cli(cmd + "--getall %s" % getbug.id, bz).splitlines() + + assert len(out) == numattach + fnames = [line.split(" ", 1)[1].strip() for line in out] + assert len(fnames) == numattach + for f in fnames: + if not os.path.exists(f): + raise AssertionError("filename '%s' not found" % f) + os.unlink(f) + + # Get all attachments, but ignore obsolete + ignorecmd = cmd + "--getall %s --ignore-obsolete" % getbug.id + out = run_cli(ignorecmd, bz).splitlines() + + assert len(out) == (numattach - 1) + fnames = [line.split(" ", 1)[1].strip() for line in out] + assert len(fnames) == (numattach - 1) + for f in fnames: + if not os.path.exists(f): + raise AssertionError("filename '%s' not found" % f) + os.unlink(f) + + +def test09Whiteboards(run_cli, backends): + bz = _open_bz(**backends) + bug = _BugCache.get_bug(run_cli, bz) + have_dev = _check_have_dev(bug) + cmd = "bugzilla modify %s " % bug.id + + # Set all whiteboards + initval = str(random.randint(1, 1024)) + statusstr = initval + "foo, bar, baz bar1" + devstr = initval + "devel" + internalstr = initval + "internal" + qastr = initval + "qa" + run_cmd = (cmd + f"--whiteboard '{statusstr}' ") + if have_dev: + run_cmd += ( + f"--devel_whiteboard '{devstr}' " + f"--internal_whiteboard '{internalstr}' " + f"--qa_whiteboard '{qastr}' ") + run_cli(run_cmd, bz) + + bug.refresh() + assert bug.whiteboard == statusstr + + if have_dev: + assert bug.qa_whiteboard == qastr + assert bug.devel_whiteboard == devstr + assert bug.internal_whiteboard == internalstr + + # Remove a tag + run_cli(cmd + "--whiteboard=-bar, ", bz) + bug.refresh() + statusstr = statusstr.replace("bar, ", "") + assert bug.status_whiteboard == statusstr + + run_cli(cmd + "--whiteboard NEWBIT", bz) + bug.refresh() + statusstr += " NEWBIT" + assert bug.whiteboard == statusstr + + # Clear whiteboards + update = bz.build_update( + whiteboard="", devel_whiteboard="", + internal_whiteboard="", qa_whiteboard="") + bz.update_bugs(bug.id, update) + + bug.refresh() + assert bug.whiteboard == "" + if have_dev: + assert bug.qa_whiteboard == "" + assert bug.devel_whiteboard == "" + assert bug.internal_whiteboard == "" + + +def test10Login(run_cli, monkeypatch): + """ + Failed login test, gives us a bit more coverage + """ + tests.utils.monkeypatch_getpass(monkeypatch) + + cmd = "bugzilla --no-cache-credentials --bugzilla %s" % RHURL + # Implied login with --username and --password + ret = run_cli("%s --user foobar@example.com " + "--password foobar query -b 123456" % cmd, + None, expectfail=True) + assert "Login failed: " in ret + + # 'login' with explicit options + ret = run_cli("%s --user foobar@example.com " + "--password foobar login" % cmd, + None, expectfail=True) + assert "Login failed: " in ret + + # 'login' with positional options + ret = run_cli("%s login foobar@example.com foobar" % cmd, + None, expectfail=True) + assert "Login failed: " in ret + + # bare 'login' + stdinstr = "foobar@example.com\n\rfoobar\n\r" + ret = run_cli("%s login" % cmd, + None, expectfail=True, stdin=stdinstr) + assert "Bugzilla Username:" in ret + assert "Bugzilla Password:" in ret + assert "Login failed: " in ret + + +def test11UserUpdate(backends): + # This won't work if run by the same user we are using + bz = _open_bz(**backends) + email = "anaconda-maint-list@redhat.com" + group = "fedora_contrib" + + have_admin = _check_have_admin(bz) + + user = bz.getuser(email) + if have_admin: + assert group in user.groupnames + origgroups = user.groupnames + + # Test group_get + try: + groupobj = bz.getgroup(group) + groupobj.refresh() + except Exception as e: + if have_admin: + raise + assert bugzilla.BugzillaError.get_bugzilla_error_code(e) == 805 + + # Remove the group + try: + bz.updateperms(email, "remove", [group]) + user.refresh() + assert group not in user.groupnames + except Exception as e: + if have_admin: + raise + assert "Sorry, you aren't a member" in str(e) + + # Re add it + try: + bz.updateperms(email, "add", group) + user.refresh() + assert group in user.groupnames + except Exception as e: + if have_admin: + raise + assert "Sorry, you aren't a member" in str(e) + + # Set groups + try: + newgroups = user.groupnames[:] + if have_admin: + newgroups.remove(group) + bz.updateperms(email, "set", newgroups) + user.refresh() + assert group not in user.groupnames + except Exception as e: + if have_admin: + raise + assert "Sorry, you aren't a member" in str(e) + + # Reset everything + try: + bz.updateperms(email, "set", origgroups) + except Exception as e: + if have_admin: + raise + assert "Sorry, you aren't a member" in str(e) + + user.refresh() + assert user.groupnames == origgroups + + # Try user create + try: + name = "pythonbugzilla-%s" % datetime.datetime.today() + bz.createuser(name + "@example.com", name, name) + except Exception as e: + if have_admin: + raise + assert "Sorry, you aren't a member" in str(e) + + +def test11ComponentEditing(backends): + bz = _open_bz(**backends) + component = ("python-bugzilla-testcomponent-%s" % + str(random.randint(1, 1024 * 1024 * 1024))) + basedata = { + "product": "Fedora Documentation", + "component": component, + } + + have_admin = _check_have_admin(bz) + + def compare(data, newid): + # pylint: disable=protected-access + products = bz._proxy.Product.get({"names": [basedata["product"]]}) + compdata = None + for c in products["products"][0]["components"]: + if int(c["id"]) == int(newid): + compdata = c + break + + assert bool(compdata) + assert data["component"] == compdata["name"] + assert data["description"] == compdata["description"] + assert data["initialowner"] == compdata["default_assigned_to"] + assert data["initialqacontact"] == compdata["default_qa_contact"] + assert data["is_active"] == compdata["is_active"] + + + # Create component + data = basedata.copy() + data.update({ + "description": "foo test bar", + "initialowner": "crobinso@redhat.com", + "initialqacontact": "extras-qa@fedoraproject.org", + "initialcclist": ["wwoods@redhat.com", "toshio@fedoraproject.org"], + "is_active": True, + }) + newid = None + try: + newid = bz.addcomponent(data)['id'] + print("Created product=%s component=%s" % ( + basedata["product"], basedata["component"])) + compare(data, newid) + except Exception as e: + if have_admin: + raise + assert (("Sorry, you aren't a member" in str(e)) or + # bugzilla 5 error string + ("You are not allowed" in str(e))) + + # Edit component + data = basedata.copy() + data.update({ + "description": "hey new desc!", + "initialowner": "extras-qa@fedoraproject.org", + "initialqacontact": "virt-mgr-maint@redhat.com", + "initialcclist": ["libvirt-maint@redhat.com", + "virt-maint@lists.fedoraproject.org"], + "is_active": False, + }) + try: + bz.editcomponent(data) + if newid is not None: + compare(data, newid) + except Exception as e: + if bz.is_rest(): + # redhat REST does not support component editing + assert "A REST API resource was not found" in str(e) + elif have_admin: + raise + else: + assert (("Sorry, you aren't a member" in str(e)) or + # bugzilla 5 error string + ("You are not allowed" in str(e))) + + +def test13SubComponents(run_cli, backends): + bz = _open_bz(**backends) + bug = _make_subcomponent_bug(run_cli, bz) + + bug.autorefresh = True + assert bug.component == "Extensions" + + bz.update_bugs(bug.id, bz.build_update( + component="Extensions", sub_component="RedHat")) + bug.refresh() + assert bug.sub_components == {"Extensions": ["RedHat"]} + + bz.update_bugs(bug.id, bz.build_update( + component="Extensions", sub_component="AgileTools")) + bug.refresh() + assert bug.sub_components == {"Extensions": ["AgileTools"]} + + +def _testExternalTrackers(run_cli, bz): + bugid = _BugCache.get_bug(run_cli, bz).id + ext_bug_id = 380489 + + # Delete any existing external trackers to get to a known state + ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] + if ids != []: + bz.remove_external_tracker(ids=ids) + + url = "https://bugzilla.mozilla.org" + if bz.bz_ver_major < 5: + url = "http://bugzilla.mozilla.org" + + # test adding tracker + kwargs = { + 'ext_type_id': 6, + 'ext_type_url': url, + 'ext_type_description': 'Mozilla Foundation', + } + bz.add_external_tracker(bugid, ext_bug_id, **kwargs) + added_bug = bz.getbug(bugid).external_bugs[0] + assert added_bug['type']['id'] == kwargs['ext_type_id'] + assert added_bug['type']['url'] == kwargs['ext_type_url'] + assert (added_bug['type']['description'] == + kwargs['ext_type_description']) + + # test updating status, description, and priority by id + kwargs = { + 'ids': bz.getbug(bugid).external_bugs[0]['id'], + 'ext_status': 'New Status', + 'ext_description': 'New Description', + 'ext_priority': 'New Priority' + } + bz.update_external_tracker(**kwargs) + updated_bug = bz.getbug(bugid).external_bugs[0] + assert updated_bug['ext_bz_bug_id'] == str(ext_bug_id) + assert updated_bug['ext_status'] == kwargs['ext_status'] + assert updated_bug['ext_description'] == kwargs['ext_description'] + assert updated_bug['ext_priority'] == kwargs['ext_priority'] + + # test removing tracker + ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] + assert len(ids) == 1 + bz.remove_external_tracker(ids=ids) + ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] + assert len(ids) == 0 + + +def test14ExternalTrackersAddUpdateRemoveQuery(run_cli, backends): + bz = _open_bz(**backends) + try: + _testExternalTrackers(run_cli, bz) + except Exception as e: + if not bz.is_rest(): + raise + assert "No REST API available" in str(e) + + +def test15EnsureLoggedIn(run_cli, backends): + bz = _open_bz(**backends) + comm = "bugzilla --ensure-logged-in query --bug_id 979546" + run_cli(comm, bz) + + # Test that we don't pollute the query dict with auth info + query = {"id": [1234567]} + origquery = query.copy() + bz.query(query) + assert query == origquery + + +def test16ModifyTags(run_cli, backends): + bz = _open_bz(**backends) + bug = _BugCache.get_bug(run_cli, bz) + cmd = "bugzilla modify %s " % bug.id + + try: + if bug.tags: + bz.update_tags(bug.id, tags_remove=bug.tags) + bug.refresh() + assert bug.tags == [] + + run_cli(cmd + "--tags foo --tags +bar --tags baz", bz) + bug.refresh() + assert bug.tags == ["foo", "bar", "baz"] + + run_cli(cmd + "--tags=-bar", bz) + bug.refresh() + assert bug.tags == ["foo", "baz"] + + bz.update_tags(bug.id, tags_remove=bug.tags) + bug.refresh() + assert bug.tags == [] + except Exception as e: + if not bz.is_rest(): + raise + assert "No REST API available" in str(e) + + +def test17LoginAPIKey(backends): + api_key = "somefakeapikey1234" + bz = _open_bz(use_creds=False, api_key=api_key, **backends) + try: + assert bz.logged_in is False + + # Use this to trigger a warning about api_key + bz.createbug(bz.build_createbug()) + except Exception as e: + assert "The API key you specified is invalid" in str(e) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..058d81bf --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,122 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import difflib +import getpass +import inspect +import io +import os +import pprint +import shlex +import sys + +from bugzilla import Bugzilla +import bugzilla._cli + +import tests + + +def get_funcname(): + # Return calling function name + return inspect.stack()[1][3] + + +def tests_path(filename): + testdir = os.path.dirname(__file__) + if testdir not in filename: + return os.path.join(testdir, filename) + return filename + + +def monkeypatch_getpass(monkeypatch): + monkeypatch.setattr(getpass, "getpass", input) + + +def sanitize_json(rawout): + # py2.7 leaves trailing whitespace after commas. strip it so + # tests pass on both python versions + return "\n".join([line.rstrip() for line in rawout.splitlines()]) + + +def open_functional_bz(bzclass, url, kwargs): + bz = bzclass(url, **kwargs) + + if kwargs.get("force_rest", False): + assert bz.is_rest() is True + if kwargs.get("force_xmlrpc", False): + assert bz.is_xmlrpc() is True + + # Set a request timeout of 60 seconds + os.environ["PYTHONBUGZILLA_REQUESTS_TIMEOUT"] = "60" + return bz + + +def open_bz(url, bzclass=Bugzilla, **kwargs): + return open_functional_bz(bzclass=bzclass, url=url, kwargs=kwargs) + + +def diff_compare(inputdata, filename, expect_out=None): + """Compare passed string output to contents of filename""" + def _process(data): + if isinstance(data, tuple) and len(data) == 1: + data = data[0] + if isinstance(data, (dict, tuple)): + out = pprint.pformat(data, width=81) + else: + out = str(data) + if not out.endswith("\n"): + out += "\n" + return out + + actual_out = _process(inputdata) + + if filename: + filename = tests_path(filename) + if not os.path.exists(filename) or tests.CLICONFIG.REGENERATE_OUTPUT: + open(filename, "w").write(actual_out) + expect_out = open(filename).read() + else: + expect_out = _process(expect_out) + + diff = "".join(difflib.unified_diff(expect_out.splitlines(1), + actual_out.splitlines(1), + fromfile=filename or "Manual input", + tofile="Generated Output")) + if diff: + raise AssertionError("Conversion outputs did not match.\n%s" % diff) + + +def do_run_cli(capsys, monkeypatch, + argvstr, bzinstance, + expectfail=False, stdin=None): + """ + Run bin/bugzilla.main() directly with passed argv + """ + argv = shlex.split(argvstr) + monkeypatch.setattr(sys, "argv", argv) + if stdin: + monkeypatch.setattr(sys, "stdin", io.StringIO(stdin)) + else: + monkeypatch.setattr(sys.stdin, "isatty", lambda: True) + + ret = 0 + try: + # pylint: disable=protected-access + if bzinstance is None: + bugzilla._cli.cli() + else: + bugzilla._cli.main(unittest_bz_instance=bzinstance) + except SystemExit as sys_e: + ret = sys_e.code + + out, err = capsys.readouterr() + outstr = out + err + + if ret != 0 and not expectfail: + raise RuntimeError("Command failed with %d\ncmd=%s\nout=%s" % + (ret, argvstr, outstr)) + if ret == 0 and expectfail: + raise RuntimeError("Command succeeded but we expected success\n" + "ret=%d\ncmd=%s\nout=%s" % + (ret, argvstr, outstr)) + return outstr diff --git a/tox.ini b/tox.ini index f53f4c1d..0d51036b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,38 @@ [tox] -envlist = py27,py33,py34,py35,py36,pypy +envlist = py34,py35,py36,py37,py38,py39,py310 [testenv] -sitepackages = True deps = -rrequirements.txt -rtest-requirements.txt commands = - python setup.py test [] + pytest [] + + +[pytest] +addopts = -q --tb=native tests/ + + +[coverage:run] +source = bugzilla/ +[coverage:report] +skip_covered = yes +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain if tests don't hit defensive assertion code: + raise NotImplementedError + + +[pycodestyle] +# [E125] Continuation indent isn't different from next block +# [E128] Not indented for visual style +# [E129] visually indented line with same indent as next logical line +# [E301] Blank lines between function definitions +# [E303] Too many blank lines +# [E402] module level import not at top of file +# [E731] do not assign a lambda expression, use a def +# [W504] line break after binary operator + +ignore=E125,E128,E129,E301,E303,E402,E731,W504