├── .coveragerc ├── .github └── workflows │ ├── docker.yaml │ └── tests.yaml ├── .gitignore ├── .pyup.yml ├── .readthedocs.yaml ├── .stestr.conf ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.rst ├── docs └── source │ ├── _static │ └── theme_override.css │ ├── conf.py │ ├── example.py │ ├── example.rst │ ├── example.yaml │ ├── faq.rst │ ├── fixtures.rst │ ├── format.rst │ ├── gabbi.rst │ ├── handlers.rst │ ├── host.rst │ ├── index.rst │ ├── jsonpath.rst │ ├── loader.rst │ ├── pytest-example.py │ ├── pytest3.0-example.py │ ├── release.rst │ └── runner.rst ├── gabbi ├── __init__.py ├── case.py ├── driver.py ├── exception.py ├── fixture.py ├── handlers │ ├── __init__.py │ ├── base.py │ ├── core.py │ ├── jsonhandler.py │ └── yaml_disk_loading_jsonhandler.py ├── httpclient.py ├── json_parser.py ├── pytester.py ├── reporter.py ├── runner.py ├── suite.py ├── suitemaker.py ├── tests │ ├── README │ ├── __init__.py │ ├── custom_response_handler.py │ ├── external_server.py │ ├── gabbits_handlers │ │ ├── cat.json │ │ ├── data.json │ │ ├── pets.json │ │ ├── subdir │ │ │ ├── data.yaml │ │ │ ├── pets.yaml │ │ │ └── values.yaml │ │ ├── values.json │ │ └── yaml-from-disk.yaml │ ├── gabbits_inner │ │ └── inner.yaml │ ├── gabbits_intercept │ │ ├── backref.yaml │ │ ├── casting.yaml │ │ ├── cat.json │ │ ├── coerce.yaml │ │ ├── contenttype.yaml │ │ ├── cookie.yaml │ │ ├── data.json │ │ ├── data.yaml │ │ ├── disable-response-handler.yaml │ │ ├── doubleresponse.yaml │ │ ├── failskip.yaml │ │ ├── fixture.yaml │ │ ├── forbiddenheaders.yaml │ │ ├── header-key.yaml │ │ ├── horse │ │ ├── host-header.yaml │ │ ├── integer-req-header.yaml │ │ ├── json-extensions.yaml │ │ ├── json-left-side.yaml │ │ ├── json-right-side.yaml │ │ ├── jsonbody.yaml │ │ ├── kitten.png │ │ ├── last-url.yaml │ │ ├── method-shortcut.yaml │ │ ├── pets.json │ │ ├── poll.yaml │ │ ├── prefix.yaml │ │ ├── queryparams.yaml │ │ ├── regex.yaml │ │ ├── self.yaml │ │ ├── skipall.yaml │ │ ├── subdir │ │ │ └── values.yaml │ │ ├── utf8.txt │ │ ├── values.json │ │ └── verbosity.yaml │ ├── gabbits_live │ │ ├── google.yaml │ │ └── host-header.yaml │ ├── gabbits_runner │ │ ├── failure.yaml │ │ ├── nan.yaml │ │ ├── subdir │ │ │ └── sample.json │ │ ├── success.yaml │ │ ├── success_alt.yaml │ │ ├── test_data.yaml │ │ ├── test_verbose.yaml │ │ └── verbosity.yaml │ ├── gabbits_unsafe_yaml │ │ └── nan.yaml │ ├── simple_wsgi.py │ ├── test_data_to_string.py │ ├── test_driver.py │ ├── test_fixtures.py │ ├── test_gabbits │ │ └── sample.yaml │ ├── test_handlers.py │ ├── test_history.py │ ├── test_inner_fixture.py │ ├── test_intercept.py │ ├── test_jsonpath.py │ ├── test_live.py │ ├── test_load_data_file.py │ ├── test_parse_url.py │ ├── test_replacers.py │ ├── test_runner.py │ ├── test_suite.py │ ├── test_suitemaker.py │ ├── test_syntax_warning.py │ ├── test_unsafe_yaml.py │ ├── test_use_prior_test.py │ ├── test_utils.py │ ├── test_yaml_disk_loading_jsonhandler.py │ ├── util.py │ └── warning_gabbits │ │ └── underscore_sample.yaml └── utils.py ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-failskip.sh ├── test-limit.sh ├── test-requirements.txt ├── test-verbosity.sh └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = gabbi/tests/* 3 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: docker 2 | on: 3 | push: 4 | tags: 5 | - '[2-9].[0-9]+.[0-9]+' 6 | jobs: 7 | push_to_registry: 8 | name: Push Docker image to Docker Hub 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check out the repo 12 | uses: actions/checkout@v2 13 | 14 | - name: Log in to Docker Hub 15 | uses: docker/login-action@v1 16 | with: 17 | username: ${{ secrets.DOCKER_USERNAME }} 18 | password: ${{ secrets.DOCKER_PASSWORD }} 19 | 20 | - name: Extract metadata (tags, labels) for Docker 21 | id: meta 22 | uses: docker/metadata-action@v3 23 | with: 24 | images: cdent/gabbi 25 | tags: | 26 | type=semver,pattern={{version}} 27 | type=semver,pattern={{major}}.{{minor}} 28 | type=semver,pattern={{major}} 29 | 30 | - name: Build and push Docker image 31 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 32 | with: 33 | context: . 34 | push: true 35 | tags: ${{ steps.meta.outputs.tags }} 36 | labels: ${{ steps.meta.outputs.labels }} 37 | build-args: 38 | GABBI_VERSION=${{steps.meta.outputs.version}} 39 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | - push 4 | - pull_request 5 | - workflow_dispatch 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | include: 12 | - python: 3.x 13 | toxenv: pep8 14 | - python: 3.9 15 | toxenv: py39 16 | - python: "3.10" 17 | toxenv: py310 18 | - python: "3.11" 19 | toxenv: py311 20 | - python: "3.12" 21 | toxenv: py312 22 | - python: "3.13" 23 | toxenv: py313 24 | - python: pypy-3.10 25 | toxenv: pypy3 26 | - python: 3.9 27 | toxenv: py39-pytest 28 | - python: "3.10" 29 | toxenv: py310-pytest 30 | - python: "3.11" 31 | toxenv: py311-pytest 32 | - python: "3.12" 33 | toxenv: py312-pytest 34 | - python: "3.13" 35 | toxenv: py313-pytest 36 | - python: "3.13" 37 | toxenv: py313-failskip 38 | - python: "3.13" 39 | toxenv: py313-limit 40 | - python: "3.13" 41 | toxenv: py313-prefix 42 | name: ${{ matrix.toxenv }} on Python ${{ matrix.python }} 43 | steps: 44 | - uses: actions/checkout@v2 45 | - uses: actions/setup-python@v2 46 | with: 47 | python-version: ${{ matrix.python }} 48 | - run: pip install tox 49 | - run: tox 50 | env: 51 | TOXENV: ${{ matrix.toxenv }} 52 | # Skip network using tests as they are unreliable 53 | GABBI_SKIP_NETWORK: true 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | build 4 | .egg 5 | .eggs 6 | .tox 7 | dist 8 | # Generated by pbr 9 | AUTHORS 10 | ChangeLog 11 | # Generated by testrepository 12 | .testrepository 13 | .stestr 14 | .idea 15 | .cache/ 16 | # coverage related 17 | .coverage 18 | cover/ 19 | htmlcov/ 20 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # autogenerated pyup.io config file 2 | # see https://pyup.io/docs/configuration/ for all available options 3 | 4 | update: insecure 5 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-lts-latest 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/source/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | # python: 21 | # install: 22 | # - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /.stestr.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | test_path=gabbi/tests 3 | test_command=${PYTHON:-python} -m subunit.run discover gabbi $LISTOPT $IDOPTION 4 | test_id_option=--load-list $IDFILE 5 | test_list_option=--list 6 | group_regex=(?:gabbi\.tests\.test_(?:\w+)\.([^_]+)) 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | Gabbi gets better because of the contributions from the people who use 3 | it. These contributions come in many forms: 4 | 5 | * Joining the #gabbi channel on OFTC IRC at irc.oftc.net and 6 | having a chat. 7 | * Improvements to make the documentation more complete, more correct 8 | and typo free. 9 | * Reporting and reviewing bugs in the 10 | [issues](https://github.com/cdent/gabbi/issues) 11 | * Providing [pull requests](https://github.com/cdent/gabbi/pulls) 12 | containing fixes and new features. See [below](#pull-requests) for 13 | guidelines. 14 | 15 | If you have an idea for a new feature it is best to review the 16 | [Ideas](https://github.com/cdent/gabbi/wiki/Ideas) wiki page and the 17 | existing issues and pull requests to see if there is existing work you 18 | can contribute to. It's also worthwhile to ask around in IRC. 19 | 20 | In general the default stance with gabbi is to avoid adding new features 21 | if we can come up with some way to use the existing features to solve 22 | the requirements of your tests. This helps to keep the test format 23 | as clean and readable as possible. 24 | 25 | If you reach an impasse, create an issue and provide as much info as you 26 | can about your situation and together we can try to figure it out. 27 | 28 | # Pull Requests 29 | 30 | If you want to make a pull request, fork the gabbi repository and create 31 | a new branch that will contain your changes. Name the branch something 32 | meaningful and related to your change. 33 | 34 | See the "Testing and Developing Gabbi" section of the the `README` for 35 | information on setting up a reasonable working environment. 36 | 37 | You should provide verbose commit messages on each of your commits. You 38 | should not feel obliged to squash your commits into one commit. We want 39 | to the see the full expression of your process and thinking. 40 | 41 | When you push your branch back to Github please never force push. 42 | 43 | If your pull request receives some comments and you need to make some 44 | changes, please do them as _an additional commit_ on the branch used for 45 | the pull request. 46 | 47 | Any code you submit should follow the rules of 48 | [pep8](https://www.python.org/dev/peps/pep-0008/). You can test that 49 | it does by running `tox -epep8` in your checkout. Note that when you 50 | run that the code will also be evaluated to be sure it follows some 51 | standards established in the OpenStack development community (mostly 52 | to do with import handling and line breaks). 53 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | MAINTAINER Chris Dent 3 | 4 | ARG GABBI_VERSION 5 | RUN python -m venv /app 6 | COPY . /app/ 7 | RUN cd /app && PBR_VERSION=${GABBI_VERSION} /app/bin/pip --no-cache-dir install . 8 | 9 | ENTRYPOINT ["/app/bin/python", "-m", "gabbi.runner"] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2015-2016 Chris Dent 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); you 5 | may not use this file except in compliance with the License. You may 6 | obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | implied. See the License for the specific language governing 14 | permissions and limitations under the License. 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # simple Makefile for some common tasks 2 | .PHONY: clean test dist release pypi tagv docs gh 3 | 4 | gabbi-version := $(shell python -c 'import gabbi; print(gabbi.__version__)') 5 | 6 | clean: 7 | find . -name "*.pyc" |xargs rm || true 8 | rm -r dist || true 9 | rm -r build || true 10 | rm -rf .tox || true 11 | rm -r .testrepository || true 12 | rm -r cover .coverage || true 13 | rm -r .eggs || true 14 | rm -r gabbi.egg-info || true 15 | 16 | tagv: 17 | git tag -s -m ${gabbi-version} ${gabbi-version} 18 | git push origin main --tags 19 | 20 | cleanagain: 21 | find . -name "*.pyc" |xargs rm || true 22 | rm -r dist || true 23 | rm -r build || true 24 | rm -r .tox || true 25 | rm -r .testrepository || true 26 | rm -r cover .coverage || true 27 | rm -r .eggs || true 28 | rm -r gabbi.egg-info || true 29 | 30 | docs: 31 | cd docs ; $(MAKE) html 32 | 33 | test: 34 | tox --skip-missing-interpreters 35 | 36 | dist: test 37 | python3 setup.py sdist bdist_wheel 38 | 39 | release: clean test cleanagain tagv pypi gh 40 | 41 | gh: 42 | gh release create ${gabbi-version} --generate-notes dist/* 43 | 44 | pypi: 45 | python3 setup.py sdist bdist_wheel 46 | twine upload dist/* 47 | 48 | docker: 49 | docker build --build-arg GABBI_VERSION=${gabbi-version} -t gabbi:${gabbi-version} . 50 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/cdent/gabbi/workflows/tests/badge.svg 2 | :target: https://github.com/cdent/gabbi/actions 3 | .. image:: https://readthedocs.org/projects/gabbi/badge/?version=latest 4 | :target: https://gabbi.readthedocs.io/en/latest/ 5 | :alt: Documentation Status 6 | 7 | Gabbi 8 | ===== 9 | 10 | `Release Notes`_ 11 | 12 | Gabbi is a tool for running HTTP tests where requests and responses 13 | are represented in a declarative YAML-based form. The simplest test 14 | looks like this:: 15 | 16 | tests: 17 | - name: A test 18 | GET: /api/resources/id 19 | 20 | See the docs_ for more details on the many features and formats for 21 | setting request headers and bodies and evaluating responses. 22 | 23 | Gabbi is tested with Python 3.9, 3.10, 3.11, 3.12, 3.13 and pypy3.10. 24 | 25 | Tests can be run using `unittest`_ style test runners, `pytest`_ 26 | or from the command line with a `gabbi-run`_ script. 27 | 28 | There is a `gabbi-demo`_ repository which provides a tutorial via 29 | its commit history. The demo builds a simple API using gabbi to 30 | facilitate test driven development. 31 | 32 | .. _Release Notes: https://gabbi.readthedocs.io/en/latest/release.html 33 | .. _docs: https://gabbi.readthedocs.io/ 34 | .. _gabbi-demo: https://github.com/cdent/gabbi-demo 35 | .. _unittest: https://gabbi.readthedocs.io/en/latest/example.html#loader 36 | .. _pytest: http://pytest.org/ 37 | .. _loader docs: https://gabbi.readthedocs.io/en/latest/example.html#pytest 38 | .. _gabbi-run: https://gabbi.readthedocs.io/en/latest/runner.html 39 | 40 | Purpose 41 | ------- 42 | 43 | Gabbi works to bridge the gap between human readable YAML files that 44 | represent HTTP requests and expected responses and the obscured realm of 45 | Python-based, object-oriented unit tests in the style of the unittest 46 | module and its derivatives. 47 | 48 | Each YAML file represents an ordered list of HTTP requests along with 49 | the expected responses. This allows a single file to represent a 50 | process in the API being tested. For example: 51 | 52 | * Create a resource. 53 | * Retrieve a resource. 54 | * Delete a resource. 55 | * Retrieve a resource again to confirm it is gone. 56 | 57 | At the same time it is still possible to ask gabbi to run just one 58 | request. If it is in a sequence of tests, those tests prior to it in 59 | the YAML file will be run (in order). In any single process any test 60 | will only be run once. Concurrency is handled such that one file 61 | runs in one process. 62 | 63 | These features mean that it is possible to create tests that are 64 | useful for both humans (as tools for improving and developing APIs) 65 | and automated CI systems. 66 | 67 | Testing and Developing Gabbi 68 | ---------------------------- 69 | 70 | To get started, after cloning the `repository`_, you should install the 71 | development dependencies:: 72 | 73 | $ pip install -r requirements-dev.txt 74 | 75 | If you prefer to keep things isolated you can create a virtual 76 | environment:: 77 | 78 | $ virtualenv gabbi-venv 79 | $ . gabbi-venv/bin/activate 80 | $ pip install -r requirements-dev.txt 81 | 82 | Gabbi is set up to be developed and tested using `tox`_ (installed via 83 | ``requirements-dev.txt``). To run the built-in tests (the YAML files 84 | are in the directories ``gabbi/tests/gabbits_*`` and loaded by the file 85 | ``gabbi/test_*.py``), you call ``tox``:: 86 | 87 | tox -epep8,py311 88 | 89 | If you have the dependencies installed (or a warmed up 90 | virtualenv) you can run the tests by hand and exit on the first 91 | failure:: 92 | 93 | python -m subunit.run discover -f gabbi | subunit2pyunit 94 | 95 | Testing can be limited to individual modules by specifying them 96 | after the tox invocation:: 97 | 98 | tox -epep8,py311 -- test_driver test_handlers 99 | 100 | If you wish to avoid running tests that connect to internet hosts, 101 | set ``GABBI_SKIP_NETWORK`` to ``True``. 102 | 103 | .. _tox: https://tox.readthedocs.io/ 104 | .. _repository: https://github.com/cdent/gabbi 105 | -------------------------------------------------------------------------------- /docs/source/_static/theme_override.css: -------------------------------------------------------------------------------- 1 | .wy-table-responsive table td, .wy-table-responsive table th { 2 | white-space: normal; 3 | } 4 | 5 | .wy-table-responsive td p { 6 | font-size: inherit; 7 | } 8 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Gabbi documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Dec 31 17:07:32 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | docroot = os.path.abspath('../..') 22 | sys.path.insert(0, docroot) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix of source filenames. 40 | source_suffix = '.rst' 41 | 42 | # The encoding of source files. 43 | #source_encoding = 'utf-8-sig' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = u'Gabbi' 50 | copyright = u'' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = '' 58 | # The full version, including alpha/beta/rc tags. 59 | release = '' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | #language = None 64 | 65 | # There are two options for replacing |today|: either, you set today to some 66 | # non-false value, then it is used: 67 | #today = '' 68 | # Else, today_fmt is used as the format for a strftime call. 69 | #today_fmt = '%B %d, %Y' 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | exclude_patterns = [] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all 76 | # documents. 77 | #default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | #add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | #add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | #show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = 'sphinx' 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | #modindex_common_prefix = [] 95 | 96 | # If true, keep warnings as "system message" paragraphs in the built documents. 97 | #keep_warnings = False 98 | 99 | 100 | # -- Options for HTML output ---------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. See the documentation for 103 | # a list of builtin themes. 104 | html_theme = 'nature' 105 | 106 | # Add any paths that contain custom static files (such as style sheets) here, 107 | # relative to this directory. They are copied after the builtin static files, 108 | # so a file named "default.css" will overwrite the builtin "default.css". 109 | html_static_path = ['_static'] 110 | 111 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 112 | html_show_copyright = False 113 | 114 | # If true, an OpenSearch description file will be output, and all pages will 115 | # contain a tag referring to it. The value of this option must be the 116 | # base URL from which the finished HTML is served. 117 | #html_use_opensearch = '' 118 | 119 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 120 | #html_file_suffix = None 121 | 122 | # Output file base name for HTML help builder. 123 | htmlhelp_basename = 'Gabbidoc' 124 | -------------------------------------------------------------------------------- /docs/source/example.py: -------------------------------------------------------------------------------- 1 | """A sample test module.""" 2 | 3 | # For pathname munging 4 | import os 5 | 6 | # The module that build_tests comes from. 7 | from gabbi import driver 8 | 9 | # We need access to the WSGI application that hosts our service 10 | from myapp import wsgiapp 11 | 12 | 13 | # We're using fixtures in the YAML files, we need to know where to 14 | # load them from. 15 | from myapp.test import fixtures 16 | 17 | # By convention the YAML files are put in a directory named 18 | # "gabbits" that is in the same directory as the Python test file. 19 | TESTS_DIR = 'gabbits' 20 | 21 | 22 | def load_tests(loader, tests, pattern): 23 | """Provide a TestSuite to the discovery process.""" 24 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 25 | # Pass "require_ssl=True" as an argument to force all tests 26 | # to use SSL in requests. 27 | return driver.build_tests(test_dir, loader, 28 | intercept=wsgiapp.app, 29 | fixture_module=fixtures) 30 | -------------------------------------------------------------------------------- /docs/source/example.rst: -------------------------------------------------------------------------------- 1 | Example Tests 2 | ============= 3 | 4 | .. _example: 5 | 6 | What follows is a commented example of some tests in a single 7 | file demonstrating many of the :doc:`format` features. See 8 | :doc:`loader` for the Python needed to integrate with a testing 9 | harness. 10 | 11 | .. literalinclude:: example.yaml 12 | :language: yaml 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/source/faq.rst: -------------------------------------------------------------------------------- 1 | 2 | Frequently Asked Questions 3 | ========================== 4 | 5 | .. note:: This section provides a collection of questions with 6 | answers that don't otherwise fit in the rest of the 7 | documentation. If something is missing, please create an 8 | issue_. 9 | 10 | As this document grows it will gain a more refined 11 | structure. 12 | 13 | .. highlight:: yaml 14 | 15 | General 16 | ~~~~~~~ 17 | 18 | Is gabbi only for testing Python-based APIs? 19 | -------------------------------------------- 20 | 21 | No, you can use :doc:`gabbi-run ` to test an HTTP service 22 | built in any programming language. 23 | 24 | How do I run just one test? 25 | --------------------------- 26 | 27 | Each YAML file contains a sequence of tests, each test within each file has a 28 | name. That name is translated to the name of the test by replacing spaces with 29 | an ``_``. 30 | 31 | When running tests that are :doc:`generated dynamically `, filtering 32 | based on the test name prior to the test being collected will not work in some 33 | test runners. Test runners that use a ``--load-list`` functionality can be 34 | convinced to filter after discovery. 35 | 36 | `pytest` does this directly with the ``-k`` keyword flag. 37 | 38 | When using testrepository with tox as used in gabbi's own tests it is possible 39 | to pass a filter in the tox command:: 40 | 41 | tox -epy27 -- get_the_widget 42 | 43 | When using ``testtools.run`` and similar test runners it's a bit more 44 | complicated. It is necessary to provide the full name of the test as a list to 45 | ``--load-list``:: 46 | 47 | python -m testtools.run --load-list \ 48 | <(echo package.tests.test_api.yamlfile_get_the_widge.test_request) 49 | 50 | How do I run just one test, without running prior tests in a sequence? 51 | ---------------------------------------------------------------------- 52 | 53 | By default, when you select a single test to run, all tests prior to that one 54 | in a file will be run as well: the file is treated as as sequence of dependent 55 | tests. If you do not want this you can adjust the ``use_prior_test`` test 56 | :ref:`metadata ` in one of three ways: 57 | 58 | * Set it in the YAML file for the one test you are concerned with. 59 | * Set the ``defaults`` for all tests in that file. 60 | * set ``use_prior_test`` to false when calling :func:`~gabbi.driver.build_tests` 61 | 62 | Be aware that doing this breaks a fundamental assumption that gabbi 63 | makes about how tests work. Any :ref:`substitutions ` 64 | will fail. 65 | 66 | Testing Style 67 | ~~~~~~~~~~~~~ 68 | 69 | Can I have variables in my YAML file? 70 | ------------------------------------- 71 | 72 | Gabbi provides the ``$ENVIRON`` :ref:`substitution 73 | ` which can operate a bit like variables that 74 | are set elsewhere and then used in the tests defined by the YAML. 75 | 76 | If you find it necessary to have variables within a single YAML file 77 | you take advantage of YAML `alias nodes`_ list this:: 78 | 79 | vars: 80 | - &uuid_1 5613AABF-BAED-4BBA-887A-252B2D3543F8 81 | 82 | tests: 83 | - name: send a uuid to a post 84 | POST: /resource 85 | request_headers: 86 | content-type: application/json 87 | data: 88 | uuid: *uuid_1 89 | 90 | You can alias all sorts of nodes, not just single items. Be aware 91 | that the replacement of an alias node happens while the YAML is 92 | being loaded, before gabbi does any processing. 93 | 94 | .. _alias nodes: http://www.yaml.org/spec/1.2/spec.html#id2786196 95 | 96 | How many tests should be put in one YAML file? 97 | ---------------------------------------------- 98 | 99 | For the sake of readability it is best to keep each YAML file 100 | relatively short. Since each YAML file represents a sequence of 101 | requests, it usually makes sense to create a new file when a test is 102 | not dependent on any before it. 103 | 104 | It's tempting to put all the tests for any resource or URL in the 105 | same file, but this eventually leads to files that are too long and 106 | are thus difficult to read. 107 | 108 | .. _issue: https://github.com/cdent/gabbi/issues 109 | 110 | -------------------------------------------------------------------------------- /docs/source/fixtures.rst: -------------------------------------------------------------------------------- 1 | Fixtures 2 | ======== 3 | 4 | Each suite of tests is represented by a single YAML file, and may 5 | optionally use one or more fixtures to provide the necessary 6 | environment required by the tests in that file. 7 | 8 | Fixtures are implemented as nested context managers. Subclasses 9 | of :class:`~gabbi.fixture.GabbiFixture` must implement 10 | ``start_fixture`` and ``stop_fixture`` methods for creating and 11 | destroying, respectively, any resources managed by the fixture. 12 | While the subclass may choose to implement ``__init__`` it is 13 | important that no exceptions are thrown in that method, otherwise 14 | the stack of context managers will fail in unexpected ways. Instead 15 | initialization of real resources should happen in ``start_fixture``. 16 | 17 | At this time there is no mechanism for the individual tests to have any 18 | direct awareness of the fixtures. The fixtures exist, conceptually, on 19 | the server side of the API being tested. 20 | 21 | Fixtures may do whatever is required by the testing environment, 22 | however there are two common scenarios: 23 | 24 | * Establishing (and then resetting when a test suite has finished) any 25 | baseline configuration settings and persistence systems required for 26 | the tests. 27 | * Creating sample data for use by the tests. 28 | 29 | If a fixture raises ``unittest.case.SkipTest`` during 30 | ``start_fixture`` all the tests in the current file will be skipped. 31 | This makes it possible to skip the tests if some optional 32 | configuration (such as a particular type of database) is not 33 | available. 34 | 35 | If an exception is raised while a fixture is being used, information 36 | about the exception will be stored on the fixture so that the 37 | ``stop_fixture`` method can decide if the exception should change how 38 | the fixture should clean up. The exception information can be found on 39 | ``exc_type``, ``exc_value`` and ``traceback`` method attributes. 40 | 41 | If an exception is raised when a fixture is started (in 42 | ``start_fixture``) the first test in the suite using the fixture 43 | will be marked with an error using the traceback from the exception 44 | and all the tests in the suite will be skipped. This ensures that 45 | fixture failure is adequately captured and reported by test runners. 46 | 47 | .. _inner-fixtures: 48 | 49 | Inner Fixtures 50 | ============== 51 | 52 | In some contexts (for example CI environments with a large 53 | number of tests being run in a broadly concurrent environment where 54 | output is logged to a single file) it can be important to capture and 55 | consolidate stray output that is produced during the tests and display 56 | it associated with an individual test. This can help debugging and 57 | avoids unusable output that is the result of multiple streams being 58 | interleaved. 59 | 60 | Inner fixtures have been added to support this. These are fixtures 61 | more in line with the tradtional ``unittest`` concept of fixtures: 62 | a class on which ``setUp`` and ``cleanUp`` is automatically called. 63 | 64 | :func:`~gabbi.driver.build_tests` accepts a named parameter 65 | arguments of ``inner_fixtures``. The value of that argument may be 66 | an ordered list of fixtures.Fixture_ classes that will be called 67 | when each individual test is set up. 68 | 69 | An example fixture that could be useful is the FakeLogger_. 70 | 71 | .. note:: At this time ``inner_fixtures`` are not supported when 72 | using the pytest :doc:`loader `. 73 | 74 | .. _fixtures.Fixture: https://pypi.python.org/pypi/fixtures 75 | .. _FakeLogger: https://pypi.python.org/pypi/fixtures#fakelogger 76 | -------------------------------------------------------------------------------- /docs/source/gabbi.rst: -------------------------------------------------------------------------------- 1 | gabbi Package 2 | ============= 3 | 4 | :mod:`case` Module 5 | ------------------ 6 | 7 | .. automodule:: gabbi.case 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`driver` Module 13 | -------------------- 14 | 15 | .. automodule:: gabbi.driver 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`suitemaker` Module 21 | ------------------------ 22 | 23 | .. automodule:: gabbi.suitemaker 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | :mod:`fixture` Module 29 | --------------------- 30 | 31 | .. automodule:: gabbi.fixture 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | :mod:`handlers` Module 37 | ---------------------- 38 | 39 | .. automodule:: gabbi.handlers 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | :mod:`handlers.base` Module 45 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 46 | 47 | .. automodule:: gabbi.handlers.base 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | :mod:`handlers.core` Module 53 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 54 | 55 | .. automodule:: gabbi.handlers.core 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | :mod:`handlers.jsonhandler` Module 61 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | 63 | .. automodule:: gabbi.handlers.jsonhandler 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | :mod:`handlers.yaml_disk_loading_jsonhandler` Module 69 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 70 | 71 | .. automodule:: gabbi.handlers.yaml_disk_loading_jsonhandler 72 | :members: 73 | :undoc-members: 74 | :show-inheritance: 75 | 76 | :mod:`suite` Module 77 | ------------------- 78 | 79 | .. automodule:: gabbi.suite 80 | :members: 81 | :undoc-members: 82 | :show-inheritance: 83 | 84 | :mod:`runner` Module 85 | -------------------- 86 | 87 | .. automodule:: gabbi.runner 88 | :members: 89 | :undoc-members: 90 | :show-inheritance: 91 | 92 | :mod:`reporter` Module 93 | ---------------------- 94 | 95 | .. automodule:: gabbi.reporter 96 | :members: 97 | :undoc-members: 98 | :show-inheritance: 99 | 100 | :mod:`utils` Module 101 | ------------------- 102 | 103 | .. automodule:: gabbi.utils 104 | :members: 105 | :undoc-members: 106 | :show-inheritance: 107 | 108 | :mod:`exception` Module 109 | ----------------------- 110 | 111 | .. automodule:: gabbi.exception 112 | :members: 113 | :undoc-members: 114 | :show-inheritance: 115 | 116 | :mod:`httpclient` Module 117 | ------------------------ 118 | 119 | .. automodule:: gabbi.httpclient 120 | :members: 121 | :undoc-members: 122 | :show-inheritance: 123 | 124 | :mod:`json_parser` Module 125 | ------------------------- 126 | 127 | .. automodule:: gabbi.json_parser 128 | :members: 129 | :undoc-members: 130 | :show-inheritance: 131 | -------------------------------------------------------------------------------- /docs/source/handlers.rst: -------------------------------------------------------------------------------- 1 | 2 | Content Handlers 3 | ================ 4 | 5 | Content handlers are responsible for preparing request data and 6 | evaluating response data based on the content-type of the request 7 | and response. A content handler operates as follows: 8 | 9 | * Structured YAML data provided via the ``data`` attribute is 10 | converted to a string or bytes sequence and used as request body. 11 | * The response body (a string or sequence of bytes) is transformed 12 | into a content-type dependent structure and stored in an internal 13 | attribute named ``response_data`` that is: 14 | 15 | * used when evaluating the response body 16 | * used in ``$RESPONSE[]`` :ref:`substitutions ` 17 | 18 | By default, gabbi provides content handlers for JSON. In that 19 | content handler the ``data`` test key is converted from structured 20 | YAML into a JSON string. Response bodies are converted from a JSON 21 | string into a data structure in ``response_data`` that is used when 22 | evaluating ``response_json_paths`` entries in a test or doing 23 | JSONPath-based ``$RESPONSE[]`` substitutions. 24 | 25 | A YAMLDiskLoadingJSONHandler has been added to extend the JSON handler. 26 | It works the same way as the JSON handler except for when evaluating the 27 | ``response_json_paths`` handle, data that is read from disk can be either in 28 | JSON or YAML format. The YAMLDiskLoadingJSONHandler is not enabled by default 29 | and must be added as shown in the :ref:`Extensions` section in order to be 30 | used in the tests. 31 | 32 | Further content handlers can be added as extensions. Test authors 33 | may need these extensions for their own suites, or enterprising 34 | developers may wish to create and distribute extensions for others 35 | to use. 36 | 37 | .. note:: One extension that is likely to be useful is a content handler 38 | that turns ``data`` into url-encoded form data suitable 39 | for POST and turns an HTML response into a DOM object. 40 | 41 | .. _Extensions: 42 | 43 | Extensions 44 | ---------- 45 | 46 | Content handlers are an evolution of the response handler concept in 47 | earlier versions gabbi. To preserve backwards compatibility with 48 | existing response handlers, old style response handlers are still 49 | allowed, but new handlers should implement the content handler 50 | interface (described below). 51 | 52 | .. highlight:: python 53 | 54 | Registering additional custom handlers is done by passing a subclass 55 | of :class:`~gabbi.handlers.base.ContentHandler` to 56 | :meth:`~gabbi.driver.build_tests`:: 57 | 58 | driver.build_tests(test_dir, loader, host=None, 59 | intercept=simple_wsgi.SimpleWsgi, 60 | content_handlers=[MyContentHandler]) 61 | 62 | If pytest is being used:: 63 | 64 | driver.py_test_generator(test_dir, intercept=simple_wsgi.SimpleWsgi, 65 | test_loader_name=__name__, 66 | content_handlers=[MyContenHandler]) 67 | 68 | Gabbi provides an additional custom handler named YAMLDiskLoadingJSONHandler. 69 | This can be used for loading JSON and YAML files from disk when evaluating the 70 | ``response_json_paths`` handle. 71 | 72 | .. warning:: YAMLDiskLoadingJSONHandler shares the same content-type as 73 | the default JSONHandler. When there are multiple handlers 74 | listed that accept the same content-type, the one that is 75 | earliest in the list will be used. 76 | 77 | With ``gabbi-run``, custom handlers can be loaded via the 78 | ``--response-handler`` option -- see 79 | :meth:`~gabbi.runner.load_response_handlers` for details. 80 | 81 | .. note:: The use of the ``--response-handler`` argument is done to 82 | preserve backwards compatibility and avoid excessive arguments. 83 | Both types of handler may be passed to the argument. 84 | 85 | Implementation Details 86 | ~~~~~~~~~~~~~~~~~~~~~~ 87 | 88 | Creating a content handler requires subclassing 89 | :class:`~gabbi.handlers.base.ContentHandler` and implementing several methods. 90 | These methods are described below, but inspecting 91 | :class:`~gabbi.handlers.jsonhandler.JSONHandler` will be instructive in 92 | highlighting required arguments and techniques. 93 | 94 | To provide a ``response_`` response-body evaluator a subclass 95 | must define: 96 | 97 | * ``test_key_suffix``: This, along with the prefix ``response_``, forms 98 | the key used in the test structure. It is a class level string. 99 | * ``test_key_value``: The key's default value, either an empty list (``[]``) 100 | or empty dict (``{}``). It is a class level value. 101 | * ``action``: An instance method which tests the expected values 102 | against the HTTP response - it is invoked for each entry, with the parameters 103 | depending on the default value. The arguments to ``action`` are (in order): 104 | 105 | * ``self``: The current instance. 106 | * ``test``: The currently active ``HTTPTestCase`` 107 | * ``item``: The current entry if ``test_key_value`` is a 108 | list, otherwise the key half of the key/value pair at this entry. 109 | * ``value``: ``None`` if ``test_key_value`` is a list, otherwise the 110 | value half of the key/value pair at this entry. 111 | 112 | To translate request or response bodies to or from structured data a 113 | subclass must define an ``accepts`` method. This should return 114 | ``True`` if this class is willing to translate the provided 115 | content-type. During request processing it is given the value of the 116 | content-type header that will be sent in the request. During 117 | response processing it is given the value of the content-type header of 118 | the response. This makes it possible to handle different request and 119 | response bodies in the same handler, if desired. For example a 120 | handler might accept ``application/x-www-form-urlencoded`` and 121 | ``text/html``. 122 | 123 | If ``accepts`` is defined two additional static methods should be defined: 124 | 125 | * ``dumps``: Turn structured Python data from the ``data`` key in a test into a 126 | string or byte stream. The optional ``test`` param allows you to access the 127 | current test case which may help with manipulations for custom content 128 | handlers, e.g. ``multipart/form-data`` needs to add a ``boundary`` to the 129 | ``Content-Type`` header in order to mark the appropriate sections of the 130 | body. 131 | * ``loads``: Turn a string or byte stream in a response into a Python data 132 | structure. Gabbi will put this data on the ``response_data`` 133 | attribute on the test, where it can be used in the evaluations 134 | described above (in the ``action`` method) or in ``$RESPONSE`` handling. 135 | An example usage here would be to turn HTML into a DOM. 136 | * ``load_data_file``: Load data from disk into a Python data structure. Gabbi 137 | will call this method when ``response_`` contains an item where 138 | the right hand side value starts with ``<@``. The ``test`` param allows you 139 | to access the current test case and provides a load_data_file method 140 | which should be used because it verifies the data is loaded within the test 141 | diectory and returns the file source as a string. The ``load_data_file`` 142 | method was introduced to re-use the JSONHandler in order to support loading 143 | YAML files from disk through the implementation of an additional custom 144 | handler, see 145 | :class:`~gabbi.handlers.yaml_disk_loading_jsonhandler.YAMLDiskLoadingJSONHandler` 146 | for details. 147 | 148 | 149 | Finally if a ``replacer`` class method is defined, then when a 150 | ``$RESPONSE`` substitution is encountered, ``replacer`` will be 151 | passed the ``response_data`` of the prior test and the argument within the 152 | ``$RESPONSE``. 153 | 154 | Please see the `JSONHandler source`_ for additional detail. 155 | 156 | .. _JSONHandler source: https://github.com/cdent/gabbi/blob/master/gabbi/handlers/jsonhandler.py 157 | -------------------------------------------------------------------------------- /docs/source/host.rst: -------------------------------------------------------------------------------- 1 | Target Host 2 | =========== 3 | 4 | The target host is the host on which the API to be tested can be found. 5 | Gabbi intends to preserve the flow and semantics of HTTP interactions 6 | as much as possible, and every HTTP request needs to be directed at a host 7 | of some form. Gabbi provides three ways to control this: 8 | 9 | * Using `WSGITransport` of httpx to provide a ``WSGI`` environment on 10 | directly attached to a ``WSGI`` application (see `intercept examples`_). 11 | * Using fully qualified ``url`` values in the YAML defined tests (see 12 | `full examples`_). 13 | * Using a host and (optionally) port defined at test build time (see 14 | `live examples`_). 15 | 16 | The intercept and live methods are mutually exclusive per test builder, 17 | but either kind of test can freely intermix fully qualified URLs into the 18 | sequence of tests in a YAML file. 19 | 20 | For Python-based test driven development and local tests the intercept 21 | style of testing lowers test requirements (no web server required) and 22 | is fast. Interception is performed as part of the per-test-case http 23 | client. Configuration or database setup may be performed using 24 | :doc:`fixtures`. 25 | 26 | For the implementation of the above see :meth:`~gabbi.driver.build_tests`. 27 | 28 | .. _WSGITransport: https://www.python-httpx.org/advanced/transports/#wsgi-transport 29 | .. _intercept examples: https://github.com/cdent/gabbi/blob/main/gabbi/tests/test_intercept.py 30 | .. _full examples: https://github.com/cdent/gabbi/blob/main/gabbi/tests/gabbits_live/google.yaml 31 | .. _live examples: https://github.com/cdent/gabbi/blob/main/gabbi/tests/test_live.py 32 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Gabbi documentation master file, created by 2 | sphinx-quickstart on Wed Dec 31 17:07:32 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | :hidden: 9 | 10 | format 11 | loader 12 | example 13 | jsonpath 14 | host 15 | fixtures 16 | handlers 17 | runner 18 | release 19 | faq 20 | gabbi 21 | 22 | Gabbi 23 | ===== 24 | 25 | .. highlight:: yaml 26 | 27 | Gabbi is a tool for running HTTP tests where requests and responses 28 | are expressed as declarations in YAML files:: 29 | 30 | tests: 31 | - name: retrieve items 32 | GET: /items 33 | 34 | See the rest of these docs for more details on the many features and 35 | formats for setting request headers and bodies and evaluating responses. 36 | 37 | Tests can be run from the command line with :doc:`gabbi-run ` or 38 | programmatically using either py.test or 39 | :ref:`unittest `-style test runners. See 40 | :ref:`installation instructions ` below. 41 | 42 | The name is derived from "gabby": excessively talkative. In a test 43 | environment having visibility of what a test is actually doing is a 44 | good thing. This is especially true when the goal of a test is to 45 | test the HTTP, not the testing infrastructure. Gabbi tries to put 46 | the HTTP interaction in the foreground of testing. 47 | 48 | If you want to get straight to creating tests look at 49 | :doc:`example`, the test files in the `source distribution`_ 50 | and :doc:`format`. A `gabbi-demo`_ repository provides a tutorial 51 | of using gabbi to build an API, via the commit history of the repo. 52 | 53 | .. _source distribution: https://github.com/cdent/gabbi 54 | .. _gabbi-demo: https://github.com/cdent/gabbi-demo 55 | 56 | Purpose 57 | ------- 58 | 59 | .. highlight:: none 60 | 61 | Gabbi works to bridge the gap between human readable YAML files (see 62 | :doc:`format` for details) that represent HTTP requests and expected 63 | responses and the rather complex world of automated testing. 64 | 65 | Each YAML file represents an ordered list of HTTP requests along with 66 | the expected responses. This allows a single file to represent a 67 | process in the API being tested. For example: 68 | 69 | * Create a resource. 70 | * Retrieve a resource. 71 | * Delete a resource. 72 | * Retrieve a resource again to confirm it is gone. 73 | 74 | At the same time it is still possible to ask gabbi to run just one 75 | request. If it is in a sequence of tests, those tests prior to it in 76 | the YAML file will be run (in order). In any single process any test 77 | will only be run once. Concurrency is handled such that one file runs 78 | in one process. 79 | 80 | These features mean that it is possible to create tests that are useful 81 | for both humans (as tools for learning, improving and developing APIs) 82 | and automated CI systems. 83 | 84 | Significant flexibility and power is available in the :doc:`format` to 85 | make it relatively straightforward to test existing complex APIs. 86 | This extended functionality includes the use of `JSONPath`_ to query 87 | response bodies and templating of test data to allow access to the prior 88 | HTTP response in the current request. For APIs which do not use JSON 89 | additional :doc:`handlers` can be created. 90 | 91 | Care should be taken when using this functionality when you are 92 | creating a new API. If your API is so complex that it needs complex 93 | test files then you may wish to take that as a sign that your API 94 | itself too complex. One goal of gabbi is to encourage transparent 95 | and comprehensible APIs. 96 | 97 | Though gabbi is written in Python and under the covers uses 98 | ``unittest`` data structures and processes, there is no requirement 99 | that the :doc:`host` be a Python-based service. Anything talking 100 | HTTP can be tested. A :doc:`runner` makes it possible to simply 101 | create YAML files and point them at a running server. 102 | 103 | .. _JSONPath: http://goessner.net/articles/JsonPath/ 104 | 105 | .. _installation: 106 | 107 | Installation 108 | ------------ 109 | 110 | As a Python package, gabbi is typically installed via pip:: 111 | 112 | pip install gabbi 113 | 114 | You might want to create a virtual environment; an isolated context for 115 | Python packages, keeping gabbi cleany separated from the rest of your 116 | system. 117 | 118 | Python 3 comes with a built-in tool to create virtual environments:: 119 | 120 | python3 -m venv venv 121 | . venv/bin/activate 122 | 123 | pip install gabbi 124 | 125 | This way we can later use ``deactivate`` and safely remove the ``venv`` 126 | directory, thus erasing any trace of gabbi from the system. 127 | 128 | If you prefer to not install gabbi, or perhaps want to use it in a dynamic 129 | fashion in a CI setting, there is an official container image hosted at 130 | `docker hub`_ as ``cdent/gabbi``. It allows running :doc:`gabbi-run ` 131 | with any arguments you might need, providing tests on STDIN of via a mounted 132 | volume. 133 | 134 | .. _docker hub: https://hub.docker.com/repository/docker/cdent/gabbi 135 | .. _virtualenv: https://virtualenv.pypa.io 136 | -------------------------------------------------------------------------------- /docs/source/jsonpath.rst: -------------------------------------------------------------------------------- 1 | .. _jsonpath: 2 | 3 | JSONPath 4 | ======== 5 | 6 | Gabbi supports JSONPath both for validating JSON response bodies and within 7 | :ref:`substitutions `. 8 | 9 | JSONPath expressions are provided by `jsonpath_rw`_, with 10 | `jsonpath_rw_ext`_ custom extensions to address common requirements: 11 | 12 | #. Sorting via ``sorted`` and ``[/property]``. 13 | #. Filtering via ``[?property = value]``. 14 | #. Returning the respective length via ``len``. 15 | 16 | (These apply both to arrays and key-value pairs.) 17 | 18 | .. highlight:: json 19 | 20 | Here is a JSONPath example demonstrating some of these features. Given 21 | JSON data as follows:: 22 | 23 | { 24 | "pets": [ 25 | {"type": "cat", "sound": "meow"}, 26 | {"type": "dog", "sound": "woof"} 27 | ] 28 | } 29 | 30 | .. highlight:: yaml 31 | 32 | If the ordering of the list in ``pets`` is predictable and 33 | reliable it is relatively straightforward to test values:: 34 | 35 | response_json_paths: 36 | # length of list is two 37 | $.pets.`len`: 2 38 | # sound of second item in list is woof 39 | $.pets[1].sound: woof 40 | 41 | If the ordering is *not* predictable additional effort is required:: 42 | 43 | response_json_paths: 44 | # sort by type 45 | $.pets[/type][0].sound: meow 46 | # sort by type, reversed 47 | $.pets[\type][0].sound: woof 48 | # all the sounds 49 | $.pets[/type]..sound: ['meow', 'woof'] 50 | # filter by type = dog 51 | $.pets[?type = "dog"].sound: woof 52 | 53 | If it is necessary to validate the entire JSON response use a 54 | JSONPath of ``$``:: 55 | 56 | response_json_paths: 57 | $: 58 | pets: 59 | - type: cat 60 | sound: meow 61 | - type: dog 62 | sound: woof 63 | 64 | This is not a technique that should be used frequently as it can 65 | lead to difficult to read tests and it also indicates that your 66 | gabbi tests are being used to test your serializers and data models, 67 | not just your API interactions. 68 | 69 | It is also possible to read raw JSON from disk for either all or 70 | some of a JSON response:: 71 | 72 | response_json_paths: 73 | $: @` can be made in both the 146 | left (query) and right (expected) hand sides of the json path 147 | expression. When subtitutions are used in the query, care must be 148 | taken to ensure proper quoting of the resulting value. For example 149 | if there is a uuid (with hyphens) at ``$RESPONSE['$.id']`` then this 150 | expression may fail:: 151 | 152 | $.nested.structure.$RESPONSE['$.id'].name: foobar 153 | 154 | as it will evaluate to something like:: 155 | 156 | $.nested.structure.ADC8AAFC-D564-40D1-9724-7680D3C010C2.name: foobar 157 | 158 | which may be treated as an arithemtic expression by the json path 159 | parser. The test author should write:: 160 | 161 | $.nested.structure["$RESPONSE['$.id']"].name: foobar 162 | 163 | to quote the result of the substitution. 164 | 165 | .. _jsonpath_rw: http://jsonpath-rw.readthedocs.io/en/latest/ 166 | .. _jsonpath_rw_ext: https://python-jsonpath-rw-ext.readthedocs.io/en/latest/ 167 | .. _own tests: https://github.com/cdent/gabbi/blob/master/gabbi/tests/gabbits_intercept/data.yaml 168 | .. _yaml-from-disk tests: https://github.com/cdent/gabbi/blob/master/gabbi/tests/gabbits_handlers/yaml-from-disk.yaml 169 | -------------------------------------------------------------------------------- /docs/source/loader.rst: -------------------------------------------------------------------------------- 1 | 2 | Loading and Running Tests 3 | ========================= 4 | 5 | .. _test_loaders: 6 | 7 | To run gabbi tests with a test harness they must be generated in 8 | some fashion and then run. This is accomplished by a test loader. 9 | Initially gabbi only supported those test harnesses that supported 10 | the ``load_tests`` protocol in UnitTest. It now possible to also 11 | build and run tests with pytest_ with some limitations described below. 12 | 13 | .. note:: It is also possible to run gabbi tests from the command 14 | line. See :doc:`runner`. 15 | 16 | .. note:: By default gabbi will load YAML files using the ``safe_load`` 17 | function. This means only basic YAML types are allowed in the 18 | file. For most use cases this is fine. If you need custom types 19 | (for example, to match NaN) it is possible to set the ``safe_yaml`` 20 | parameter of :meth:`~gabbi.driver.build_tests` to ``False``. 21 | If custom types are used, please keep in mind that this can limit 22 | the portability of the YAML files to other contexts. 23 | 24 | .. warning:: If test are being run with a runner that supports 25 | concurrency (such as ``testrepository``) it is critical 26 | that the test runner is informed of how to group the 27 | tests into their respective suites. The usual way to do 28 | this is to use a regular expression that groups based 29 | on the name of the yaml files. For example, when using 30 | ``testrepository`` the ``.testr.conf`` file needs an 31 | entry similar to the following:: 32 | 33 | group_regex=gabbi\.suitemaker\.(test_[^_]+_[^_]+) 34 | 35 | UnitTest Style Loader 36 | ~~~~~~~~~~~~~~~~~~~~~ 37 | 38 | To run the tests with a ``load_tests`` style loader a test file containing 39 | a ``load_tests`` method is required. That will look a bit like: 40 | 41 | .. literalinclude:: example.py 42 | :language: python 43 | 44 | For details on the arguments available when building tests see 45 | :meth:`~gabbi.driver.build_tests`. 46 | 47 | Once the test loader has been created, it needs to be run. There are *many* 48 | options. Which is appropriate depends very much on your environment. Here are 49 | some examples using ``unittest`` or ``testtools`` that require minimal 50 | knowledge to get started. 51 | 52 | By file:: 53 | 54 | python -m testtools.run -v test/test_loader.py 55 | 56 | By module:: 57 | 58 | python -m testttols.run -v test.test_loader 59 | 60 | python -m unittest -v test.test_loader 61 | 62 | Using test discovery to locate all tests in a directory tree:: 63 | 64 | python -m testtools.run discover 65 | 66 | python -m unittest discover test 67 | 68 | See the `source distribution`_ and `the tutorial repo`_ for more 69 | advanced options, including using ``testrepository`` and 70 | ``subunit``. 71 | 72 | pytest 73 | ~~~~~~ 74 | 75 | .. _pytest_loader: 76 | 77 | Since pytest does not support the ``load_tests`` system, a different 78 | way of generating tests is required. Two techniques are supported. 79 | 80 | The original method (described below) used yield statements to 81 | generate tests which pytest would collect. This style of tests is 82 | deprecated as of ``pytest>=3.0`` so a new style using pytest 83 | fixtures has been developed. 84 | 85 | .. warning:: The pytest loader now requires that ``test_loader_name`` be set 86 | when :meth:`gabbi.driver.py_test_generator` is called. 87 | 88 | pytest >= 3.0 89 | ------------- 90 | 91 | In the newer technique, a test file is created that uses the 92 | ``pytest_generate_tests`` hook. Special care must be taken to always 93 | import the ``test_pytest`` method which is the base test that the 94 | pytest hook parametrizes to generate the tests from the YAML files. 95 | Without the method, the hook will not be called and no tests generated. 96 | 97 | Here is a simple example file: 98 | 99 | .. literalinclude:: pytest3.0-example.py 100 | :language: python 101 | 102 | This can then be run with the usual pytest commands. For example:: 103 | 104 | py.test -svx pytest3.0-example.py 105 | 106 | pytest < 3.0 107 | ------------ 108 | 109 | When using the older technique, test file must be created 110 | that calls :meth:`~gabbi.driver.py_test_generator` and yields the 111 | generated tests. That will look a bit like this: 112 | 113 | .. literalinclude:: pytest-example.py 114 | :language: python 115 | 116 | This can then be run with the usual pytest commands. For example:: 117 | 118 | py.test -svx pytest-example.py 119 | 120 | The older technique will continue to work with all versions of 121 | ``pytest<4.0`` but ``>=3.0`` will produce warnings. If you want to 122 | use the older technique but not see the warnings add 123 | ``--disable-pytest-warnings`` parameter to the invocation of 124 | ``py.test``. 125 | 126 | .. _source distribution: https://github.com/cdent/gabbi 127 | .. _the tutorial repo: https://github.com/cdent/gabbi-demo 128 | .. _pytest: http://pytest.org/ 129 | -------------------------------------------------------------------------------- /docs/source/pytest-example.py: -------------------------------------------------------------------------------- 1 | """A sample pytest module.""" 2 | 3 | # For pathname munging 4 | import os 5 | 6 | # The module that build_tests comes from. 7 | from gabbi import driver 8 | 9 | # We need access to the WSGI application that hosts our service 10 | from myapp import wsgiapp 11 | 12 | # We're using fixtures in the YAML files, we need to know where to 13 | # load them from. 14 | from myapp.test import fixtures 15 | 16 | # By convention the YAML files are put in a directory named 17 | # "gabbits" that is in the same directory as the Python test file. 18 | TESTS_DIR = 'gabbits' 19 | 20 | 21 | def test_gabbits(): 22 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 23 | # Pass "require_ssl=True" as an argument to force all tests 24 | # to use SSL in requests. 25 | test_generator = driver.py_test_generator( 26 | test_loader_name=__name__, 27 | test_dir, intercept=wsgiapp.app, 28 | fixture_module=fixtures) 29 | 30 | for test in test_generator: 31 | yield test 32 | -------------------------------------------------------------------------------- /docs/source/pytest3.0-example.py: -------------------------------------------------------------------------------- 1 | """A sample pytest module for pytest >= 3.0.""" 2 | 3 | # For pathname munging 4 | import os 5 | 6 | # The module that py_test_generator comes from. 7 | from gabbi import driver 8 | 9 | # We need test_pytest so that pytest test collection works properly. 10 | # Without this, the pytest_generate_tests method below will not be 11 | # called. 12 | from gabbi.driver import test_pytest # noqa 13 | 14 | # We need access to the WSGI application that hosts our service 15 | from myapp import wsgiapp 16 | 17 | # We're using fixtures in the YAML files, we need to know where to 18 | # load them from. 19 | from myapp.test import fixtures 20 | 21 | # By convention the YAML files are put in a directory named 22 | # "gabbits" that is in the same directory as the Python test file. 23 | TESTS_DIR = 'gabbits' 24 | 25 | 26 | def pytest_generate_tests(metafunc): 27 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 28 | driver.py_test_generator( 29 | test_dir, intercept=wsgiapp.app, 30 | test_loader_name=__name__, 31 | fixture_module=fixtures, metafunc=metafunc) 32 | -------------------------------------------------------------------------------- /docs/source/runner.rst: -------------------------------------------------------------------------------- 1 | YAML Runner 2 | =========== 3 | 4 | If there is a running web service that needs to be tested and 5 | creating a test loader with :meth:`~gabbi.driver.build_tests` is 6 | either inconvenient or overkill it is possible to run YAML test 7 | files directly from the command line with the console-script 8 | ``gabbi-run``. It accepts YAML on ``stdin`` or as multiple file 9 | arguments, and generates and runs tests and outputs a summary of 10 | the results. 11 | 12 | The provided YAML may not use custom :doc:`fixtures` but otherwise 13 | uses the default :doc:`format`. :doc:`host` information is either 14 | expressed directly in the YAML file or provided on the command 15 | line:: 16 | 17 | gabbi-run [host[:port]] < /my/test.yaml 18 | 19 | or:: 20 | 21 | gabbi-run http://host:port < /my/test.yaml 22 | 23 | To test with one or more files the following command syntax may be 24 | used:: 25 | 26 | gabbi-run http://host:port -- /my/test.yaml /my/other.yaml 27 | 28 | .. note:: The filename arguments must come after a ``--`` and all 29 | other arguments (host, port, prefix, failfast) must come 30 | before the ``--``. 31 | 32 | .. note:: If files are provided, test output will use names 33 | including the name of the file. If any single file includes 34 | an error, the name of the file will be included in a summary 35 | of failed files at the end of the test report. 36 | 37 | To facilitate using the same tests against the same application mounted 38 | in different locations in a WSGI server, a ``prefix`` may be provided 39 | as a second argument:: 40 | 41 | gabbi-run host[:port] [prefix] < /my/test.yaml 42 | 43 | or in the target URL:: 44 | 45 | gabbi-run http://host:port/prefix < /my/test.yaml 46 | 47 | The value of prefix will be prepended to the path portion of URLs that 48 | are not fully qualified. 49 | 50 | Anywhere host is used, if it is a raw IPV6 address it should be 51 | wrapped in ``[`` and ``]``. 52 | 53 | If ``https`` is used in the target, then the tests in the provided 54 | YAML will default to ``ssl: True``. 55 | 56 | Use ``-k`` or ``--insecure`` to **not** validate certificates when making 57 | ``https`` connections. 58 | 59 | If a ``-x`` or ``--failfast`` argument is provided then ``gabbi-run`` will 60 | exit after the first test failure. 61 | 62 | Use ``-v`` or ``--verbose`` with a value of ``all``, ``headers`` or ``body`` 63 | to turn on :ref:`verbosity ` for all tests being run. 64 | 65 | Use ``-q`` or ``--quiet`` to silence test runner output. 66 | 67 | Use ``-r`` or ``--response-handler`` to load a custom response or content 68 | handler for use with tests. 69 | 70 | Use ``-l`` to load response handlers relative to the current working directory. 71 | 72 | For example, to load a handler named ``HTMLHandler`` from the ``handlers.html`` 73 | module relative to the current directory: 74 | 75 | gabbi-run -l -r handlers.html:HTMLHandler http://example.com < my.yaml 76 | -------------------------------------------------------------------------------- /gabbi/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """See gabbi.driver and gabbbi.case.""" 14 | 15 | __version__ = '4.1.1' 16 | -------------------------------------------------------------------------------- /gabbi/exception.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Gabbi specific exceptions.""" 14 | 15 | 16 | class GabbiDataLoadError(ValueError): 17 | """An exception to alert when data streams cannot be loaded.""" 18 | pass 19 | 20 | 21 | class GabbiFormatError(ValueError): 22 | """An exception to encapsulate poorly formed test data.""" 23 | pass 24 | 25 | 26 | class GabbiSyntaxWarning(SyntaxWarning): 27 | """A warning about syntax that is not desirable.""" 28 | pass 29 | -------------------------------------------------------------------------------- /gabbi/fixture.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Manage fixtures for gabbi at the test suite level.""" 14 | 15 | import contextlib 16 | import sys 17 | from unittest import case 18 | 19 | 20 | class GabbiFixtureError(Exception): 21 | """Generic exception for GabbiFixture.""" 22 | pass 23 | 24 | 25 | class GabbiFixture: 26 | """A context manager that operates as a fixture. 27 | 28 | Subclasses must implement ``start_fixture`` and ``stop_fixture``, each 29 | of which contain the logic for stopping and starting whatever the 30 | fixture is. What a fixture is is left as an exercise for the implementor. 31 | 32 | These context managers will be nested so any actual work needs to 33 | happen in ``start_fixture`` and ``stop_fixture`` and not in ``__init__``. 34 | Otherwise exception handling will not work properly. 35 | """ 36 | 37 | def __init__(self): 38 | self.exc_type = None 39 | self.exc_value = None 40 | self.traceback = None 41 | 42 | def __enter__(self): 43 | self.start_fixture() 44 | 45 | def __exit__(self, exc_type, value, traceback): 46 | self.exc_type = exc_type 47 | self.exc_value = value 48 | self.traceback = traceback 49 | self.stop_fixture() 50 | 51 | def start_fixture(self): 52 | """Implement the actual workings of starting the fixture here.""" 53 | pass 54 | 55 | def stop_fixture(self): 56 | """Implement the actual workings of stopping the fixture here.""" 57 | pass 58 | 59 | 60 | class SkipAllFixture(GabbiFixture): 61 | """A fixture that skips all the tests in the current suite.""" 62 | 63 | def start_fixture(self): 64 | raise case.SkipTest('entire suite skipped') 65 | 66 | 67 | @contextlib.contextmanager 68 | def nest(fixtures): 69 | """Nest a series of fixtures. 70 | 71 | This is duplicated from ``nested`` in the stdlib, which has been 72 | deprecated because of issues with how exceptions are difficult to 73 | handle during ``__init__``. Gabbi needs to nest an unknown number 74 | of fixtures dynamically, so the ``with`` syntax that replaces 75 | ``nested`` will not work. 76 | """ 77 | contexts = [] 78 | exits = [] 79 | exc = (None, None, None) 80 | try: 81 | for fixture in fixtures: 82 | enter_func = fixture.__enter__ 83 | exit_func = fixture.__exit__ 84 | contexts.append(enter_func()) 85 | exits.append(exit_func) 86 | yield contexts 87 | except Exception: 88 | exc = sys.exc_info() 89 | finally: 90 | while exits: 91 | exit_func = exits.pop() 92 | try: 93 | if exit_func(*exc): 94 | exc = (None, None, None) 95 | except Exception: 96 | exc = sys.exc_info() 97 | if exc != (None, None, None): 98 | raise exc[1].with_traceback(exc[2]) 99 | -------------------------------------------------------------------------------- /gabbi/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Package for response and content handlers that process the body of 14 | a response in various ways. 15 | """ 16 | 17 | from gabbi.handlers import core 18 | from gabbi.handlers import jsonhandler 19 | 20 | # A list of the default handlers 21 | RESPONSE_HANDLERS = [ 22 | core.ForbiddenHeadersResponseHandler, 23 | core.HeadersResponseHandler, 24 | core.StringResponseHandler, 25 | jsonhandler.JSONHandler, 26 | ] 27 | -------------------------------------------------------------------------------- /gabbi/handlers/base.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Base classes for response and content handlers.""" 14 | 15 | from gabbi.exception import GabbiFormatError 16 | 17 | 18 | class ResponseHandler: 19 | """Add functionality for making assertions about an HTTP response. 20 | 21 | A subclass may implement two methods: ``action`` and ``preprocess``. 22 | 23 | ``preprocess`` takes one argument, the ``TestCase``. It is called exactly 24 | once for each test before looping across the assertions. It is used, 25 | rarely, to copy the ``test.output`` into a useful form (such as a parsed 26 | DOM). 27 | 28 | ``action`` takes two or three arguments. If ``test_key_value`` is a list 29 | ``action`` is called with the test case and a single list item. If 30 | ``test_key_value`` is a dict then ``action`` is called with the test case 31 | and a key and value pair. 32 | """ 33 | 34 | test_key_suffix = '' 35 | test_key_value = [] 36 | 37 | def __init__(self): 38 | self._register() 39 | 40 | def __call__(self, test): 41 | if test.test_data[self._key]: 42 | self.preprocess(test) 43 | if not isinstance( 44 | test.test_data[self._key], type(self.test_key_value)): 45 | raise GabbiFormatError( 46 | "%s in '%s' has incorrect type, must be %s" 47 | % (self._key, test.test_data['name'], 48 | type(self.test_key_value))) 49 | for item in test.test_data[self._key]: 50 | try: 51 | value = test.test_data[self._key][item] 52 | except (TypeError, KeyError): 53 | value = None 54 | self.action(test, item, value=value) 55 | 56 | def preprocess(self, test): 57 | """Do any pre-single-test preprocessing.""" 58 | pass 59 | 60 | def action(self, test, item, value=None): 61 | """Test an individual entry for this response handler. 62 | 63 | If the entry is a key value pair the key is in item and the 64 | value in value. Otherwise the entry is considered a single item 65 | from a list. 66 | """ 67 | pass 68 | 69 | def is_regex(self, value): 70 | """Check if the value is formatted to looks like a regular expression. 71 | 72 | Meaning it starts and ends with "/". 73 | """ 74 | return ( 75 | value.startswith('/') and value.endswith('/') and len(value) > 1 76 | ) 77 | 78 | def _register(self): 79 | """Register this handler on the provided test class.""" 80 | self.response_handler = None 81 | self.content_handler = None 82 | if self.test_key_suffix: 83 | self._key = 'response_%s' % self.test_key_suffix 84 | self.test_base = {self._key: self.test_key_value} 85 | self.response_handler = self 86 | if hasattr(self, 'accepts'): 87 | self.content_handler = self 88 | 89 | def __eq__(self, other): 90 | if isinstance(other, ResponseHandler): 91 | return self.__class__ == other.__class__ 92 | return False 93 | 94 | def __ne__(self, other): 95 | return not self.__eq__(other) 96 | 97 | 98 | class ContentHandler(ResponseHandler): 99 | """A subclass of ResponseHandlers that adds content handling.""" 100 | 101 | @staticmethod 102 | def accepts(content_type): 103 | """Return True if this handler can handler this type.""" 104 | return False 105 | 106 | @classmethod 107 | def replacer(cls, response_data, path): 108 | """Return the string that is replacing RESPONSE.""" 109 | return path 110 | 111 | @staticmethod 112 | def dumps(data, pretty=False, test=None): 113 | """Return structured data as a string. 114 | 115 | If pretty is true, prettify. 116 | """ 117 | return data 118 | 119 | @staticmethod 120 | def loads(data): 121 | """Create structured (Python) data from a stream. 122 | 123 | If there is a failure decoding then the handler should 124 | repackage the error as a gabbi.exception.GabbiDataLoadError. 125 | """ 126 | return data 127 | 128 | @staticmethod 129 | def load_data_file(test, file_path): 130 | """Return the string content of the file specified by the file_path.""" 131 | return test.load_data_file(file_path) 132 | -------------------------------------------------------------------------------- /gabbi/handlers/core.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Core response handlers.""" 14 | 15 | from gabbi.handlers import base 16 | 17 | 18 | class StringResponseHandler(base.ResponseHandler): 19 | """Test for matching strings in the the response body.""" 20 | 21 | test_key_suffix = 'strings' 22 | test_key_value = [] 23 | 24 | def action(self, test, expected, value=None): 25 | is_regex = self.is_regex(expected) 26 | expected = test.replace_template(expected, escape_regex=is_regex) 27 | 28 | if is_regex: 29 | # Trim off / 30 | expected = expected[1:-1] 31 | test.assertRegex( 32 | test.output, expected, 33 | 'Expect resonse body %s to match /%s/' % 34 | (test.output, expected)) 35 | else: 36 | test.assert_in_or_print_output(expected, test.output) 37 | 38 | 39 | class ForbiddenHeadersResponseHandler(base.ResponseHandler): 40 | """Test that listed headers are not in the response.""" 41 | 42 | test_key_suffix = 'forbidden_headers' 43 | test_key_value = [] 44 | 45 | def action(self, test, forbidden, value=None): 46 | # normalize forbidden header to lower case 47 | forbidden = test.replace_template(forbidden).lower() 48 | test.assertNotIn(forbidden, test.response, 49 | 'Forbidden header %s found in response' % forbidden) 50 | 51 | 52 | class HeadersResponseHandler(base.ResponseHandler): 53 | """Compare expected headers with actual headers. 54 | 55 | If a header value is wrapped in ``/`` it is treated as a raw 56 | regular expression. 57 | 58 | Headers values are always treated as strings. 59 | """ 60 | 61 | test_key_suffix = 'headers' 62 | test_key_value = {} 63 | 64 | def action(self, test, header, value=None): 65 | header = header.lower() # case-insensitive comparison 66 | response = test.response 67 | 68 | header_value = str(value) 69 | is_regex = self.is_regex(header_value) 70 | header_value = test.replace_template(header_value, 71 | escape_regex=is_regex) 72 | 73 | try: 74 | response_value = str(response[header]) 75 | except KeyError: 76 | raise AssertionError( 77 | "'%s' header not present in response: %s" % ( 78 | header, response.keys())) 79 | 80 | if is_regex: 81 | header_value = header_value[1:-1] 82 | test.assertRegex( 83 | response_value, header_value, 84 | 'Expect header %s to match /%s/, got %s' % 85 | (header, header_value, response_value)) 86 | else: 87 | test.assertEqual(header_value, response_value, 88 | 'Expect header %s with value %s, got %s' % 89 | (header, header_value, response[header])) 90 | -------------------------------------------------------------------------------- /gabbi/handlers/jsonhandler.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """JSON-related content handling.""" 14 | 15 | import json 16 | 17 | from gabbi.exception import GabbiDataLoadError 18 | from gabbi.handlers import base 19 | from gabbi import json_parser 20 | 21 | 22 | class JSONHandler(base.ContentHandler): 23 | """A ContentHandler for JSON 24 | 25 | * Structured test ``data`` is turned into JSON when request 26 | content-type is JSON. 27 | * Response bodies that are JSON strings are made into Python 28 | data on the test ``response_data`` attribute when the response 29 | content-type is JSON. 30 | * A ``response_json_paths`` response handler is added. 31 | * JSONPaths in $RESPONSE substitutions are supported. 32 | """ 33 | 34 | test_key_suffix = 'json_paths' 35 | test_key_value = {} 36 | 37 | @staticmethod 38 | def accepts(content_type): 39 | content_type = content_type.lower() 40 | parameters = '' 41 | if ';' in content_type: 42 | content_type, parameters = content_type.split(';', 1) 43 | content_type = content_type.strip() 44 | return (content_type.endswith('+json') or 45 | content_type == 'application/json' 46 | and 'stream=' not in parameters) 47 | 48 | @classmethod 49 | def replacer(cls, response_data, match): 50 | return cls.extract_json_path_value(response_data, match) 51 | 52 | @staticmethod 53 | def dumps(data, pretty=False, test=None): 54 | if pretty: 55 | return json.dumps(data, indent=2, separators=(',', ': ')) 56 | else: 57 | return json.dumps(data) 58 | 59 | @staticmethod 60 | def loads(data): 61 | try: 62 | return json.loads(data) 63 | except ValueError as exc: 64 | raise GabbiDataLoadError('unable to parse data') from exc 65 | 66 | @staticmethod 67 | def load_data_file(test, file_path): 68 | info = test.load_data_file(file_path) 69 | info = str(info, 'UTF-8') 70 | return json.loads(info) 71 | 72 | @staticmethod 73 | def extract_json_path_value(data, path): 74 | """Extract the value at JSON Path path from the data. 75 | 76 | The input data is a Python datastructure, not a JSON string. 77 | """ 78 | path_expr = json_parser.parse(path) 79 | matches = [match.value for match in path_expr.find(data)] 80 | if matches: 81 | if len(matches) > 1: 82 | return matches 83 | else: 84 | return matches[0] 85 | else: 86 | raise ValueError( 87 | "JSONPath '%s' failed to match on data: '%s'" % (path, data)) 88 | 89 | def action(self, test, path, value=None): 90 | """Test json_paths against json data.""" 91 | # Do template expansion in the left hand side. 92 | lhs_path = test.replace_template(path) 93 | rhs_path = rhs_match = None 94 | try: 95 | lhs_match = self.extract_json_path_value( 96 | test.response_data, lhs_path) 97 | except AttributeError: 98 | raise AssertionError('unable to extract JSON from test results') 99 | except ValueError: 100 | raise AssertionError('left hand side json path %s cannot match ' 101 | '%s' % (path, test.response_data)) 102 | 103 | # read data from disk if the value starts with '<@' 104 | if isinstance(value, str) and value.startswith('<@'): 105 | # Do template expansion in the rhs if rhs_path is provided. 106 | if ':' in value: 107 | value, rhs_path = value.split(':$', 1) 108 | rhs_path = test.replace_template('$' + rhs_path) 109 | value = self.load_data_file(test, value.replace('<@', '', 1)) 110 | if rhs_path: 111 | try: 112 | rhs_match = self.extract_json_path_value(value, rhs_path) 113 | except AttributeError: 114 | raise AssertionError('unable to extract JSON from data on ' 115 | 'disk') 116 | except ValueError: 117 | raise AssertionError('right hand side json path %s cannot ' 118 | 'match %s' % (rhs_path, value)) 119 | 120 | # If expected is a string, check to see if it is a regex. 121 | is_regex = isinstance(value, str) and self.is_regex(value) 122 | expected = (rhs_match or 123 | test.replace_template(value, escape_regex=is_regex)) 124 | match = lhs_match 125 | if is_regex and not rhs_match: 126 | expected = expected[1:-1] 127 | # match may be a number so stringify 128 | match = str(match) 129 | test.assertRegex( 130 | match, expected, 131 | 'Expect jsonpath %s to match /%s/, got %s' % 132 | (path, expected, match)) 133 | else: 134 | test.assertEqual(expected, match, 135 | 'Unable to match %s as %s, got %s' % 136 | (path, expected, match)) 137 | -------------------------------------------------------------------------------- /gabbi/handlers/yaml_disk_loading_jsonhandler.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """JSON-related content handling with YAML data disk loading.""" 14 | 15 | import yaml 16 | 17 | from gabbi.handlers import jsonhandler 18 | 19 | 20 | class YAMLDiskLoadingJSONHandler(jsonhandler.JSONHandler): 21 | """A ContentHandler for JSON responses that loads YAML from disk 22 | 23 | * Structured test ``data`` is turned into JSON when request 24 | content-type is JSON. 25 | * Response bodies that are JSON strings are made into Python 26 | data on the test ``response_data`` attribute when the response 27 | content-type is JSON. 28 | * A ``response_json_paths`` response handler is added. Data read 29 | from disk during this handle will be loaded with the yaml.safe_load 30 | method to support both JSON and YAML data sources from disk. 31 | * JSONPaths in $RESPONSE substitutions are supported. 32 | """ 33 | 34 | @staticmethod 35 | def load_data_file(test, file_path): 36 | info = test.load_data_file(file_path) 37 | info = str(info, 'UTF-8') 38 | return yaml.safe_load(info) 39 | -------------------------------------------------------------------------------- /gabbi/json_parser.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Keep one single global jsonpath parser.""" 14 | 15 | from jsonpath_rw_ext import parser 16 | 17 | 18 | PARSER = None 19 | 20 | 21 | def parse(path): 22 | """Parse a JSONPath expression use the global parser.""" 23 | global PARSER 24 | if not PARSER: 25 | PARSER = parser.ExtentedJsonPathParser() 26 | return PARSER.parse(path) 27 | -------------------------------------------------------------------------------- /gabbi/pytester.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """A pytest plugin that runs under the covers with gabbi. 14 | 15 | This is a backwards compatible improvement to the way gabbi can work with 16 | pytest. Tests are loaded (and yielded in the same way) but they are filtered 17 | more correctly, and fixture start and stops are done more correctly. 18 | 19 | This allows the test count to be accurate, which is nice. 20 | """ 21 | 22 | import pytest 23 | 24 | 25 | # Globals storing the test-like functions to be used when starting 26 | # and stopping a suite. 27 | STARTS = {} 28 | STOPS = {} 29 | 30 | 31 | def get_cleanname(item): 32 | """Extract a test name from a pytest Function item.""" 33 | if '[' in item.name: 34 | cleanname = item.name.split('[', 1)[1] 35 | cleanname = cleanname.split(']', 1)[0] 36 | return cleanname 37 | return item.name 38 | 39 | 40 | def get_suitename(name): 41 | """Extract a test suite from a clean name. 42 | 43 | This is fragile. It assumes there are no underscores in 44 | suite names, which is not always true. 45 | """ 46 | prefix, rest = name.split(':', 1) 47 | if prefix.startswith('start_') or prefix.startswith('stop_'): 48 | prefix = prefix.split('_', 1)[1] 49 | suite = rest.split('_', 1)[0] 50 | return prefix + ':' + suite 51 | 52 | 53 | def c_pytest_collection_modifyitems(items, config): 54 | """Set the starters and stoppers for a limited collection of tests.""" 55 | latest_suite = None 56 | latest_item = None 57 | for item in items: 58 | cleanname = get_cleanname(item) 59 | if ':' not in cleanname: 60 | continue 61 | prefix, testname = cleanname.split('_', 1) 62 | suitename = get_suitename(cleanname) 63 | if prefix == 'start' or prefix == 'stop': 64 | continue 65 | if latest_suite != suitename: 66 | item.starter = STARTS[suitename] 67 | if latest_item: 68 | latest_item.stopper = STOPS[ 69 | get_suitename(get_cleanname(latest_item))] 70 | latest_suite = suitename 71 | latest_item = item 72 | # Set the last stopper in the list 73 | if latest_item: 74 | latest_item.stopper = STOPS[get_suitename(get_cleanname(latest_item))] 75 | 76 | 77 | def a_pytest_collection_modifyitems(items, config): 78 | """Traverse collected tests to save START and STOPS. 79 | 80 | Remove those START and STOPS from the tests to run. 81 | """ 82 | remaining = [] 83 | deselected = [] 84 | for item in items: 85 | cleanname = get_cleanname(item) 86 | if ':' not in cleanname: 87 | remaining.append(item) 88 | continue 89 | suitename = get_suitename(cleanname) 90 | if cleanname.startswith('start_'): 91 | test = item.callspec.params['test'] 92 | result = item.callspec.params['result'] 93 | # TODO(cdent): Consider a named tuple here 94 | STARTS[suitename] = (test, result, []) 95 | deselected.append(item) 96 | elif cleanname.startswith('stop_'): 97 | test = item.callspec.params['test'] 98 | STOPS[suitename] = test 99 | deselected.append(item) 100 | else: 101 | remaining.append(item) 102 | # Add each kept test to the start fixture 103 | # in case we need to skip all the tests. 104 | STARTS[suitename][2].append(item) 105 | 106 | if deselected: 107 | items[:] = remaining 108 | 109 | 110 | @pytest.hookimpl(hookwrapper=True) 111 | def pytest_collection_modifyitems(items, config): 112 | """Hook for processing collected tests. 113 | 114 | Discover start and stops, then use the default hook 115 | for filter for keywords and markers, then attach 116 | starter and stopper to the remaining tests. 117 | """ 118 | a_pytest_collection_modifyitems(items, config) 119 | yield 120 | c_pytest_collection_modifyitems(items, config) 121 | 122 | 123 | def pytest_runtest_setup(item): 124 | """Run a starter if a test has one. 125 | 126 | This is done before run, so it means that a single test will 127 | run its priors after running this. 128 | """ 129 | if hasattr(item, 'starter'): 130 | test, result, tests = item.starter 131 | test(result, tests) 132 | 133 | 134 | def pytest_runtest_teardown(item, nextitem): 135 | """Run a stopper if a test has one.""" 136 | if hasattr(item, 'stopper'): 137 | item.stopper() 138 | -------------------------------------------------------------------------------- /gabbi/reporter.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | """TestRunner and TestResult for gabbi-run.""" 15 | 16 | from unittest import TestResult 17 | from unittest import TextTestResult 18 | from unittest import TextTestRunner 19 | 20 | import pytest 21 | 22 | from gabbi import utils 23 | 24 | 25 | class ConciseTestResult(TextTestResult): 26 | """A TextTestResult with simple but useful output. 27 | 28 | If the output is a tty or GABBI_FORCE_COLOR is set in the 29 | environment, output will be colorized. 30 | """ 31 | 32 | def __init__(self, stream, descriptions, verbosity): 33 | super(ConciseTestResult, self).__init__( 34 | stream, descriptions, verbosity) 35 | self.colorize = utils.get_colorizer(stream) 36 | 37 | def startTest(self, test): 38 | super(TextTestResult, self).startTest(test) 39 | if self.showAll: 40 | self.stream.write('... ') 41 | self.stream.flush() 42 | 43 | def addSuccess(self, test): 44 | super(TextTestResult, self).addSuccess(test) 45 | if self.showAll: 46 | self.stream.write(self.colorize('GREEN', '✓ ')) 47 | self.stream.writeln(self.getDescription(test)) 48 | 49 | def addFailure(self, test, err): 50 | super(TextTestResult, self).addFailure(test, err) 51 | if self.showAll: 52 | self.stream.write(self.colorize('RED', '✗ ')) 53 | self.stream.writeln(self.getDescription(test)) 54 | 55 | def addError(self, test, err): 56 | super(TextTestResult, self).addError(test, err) 57 | if self.showAll: 58 | self.stream.write(self.colorize('RED', 'E ')) 59 | self.stream.writeln(self.getDescription(test)) 60 | 61 | def addSkip(self, test, reason): 62 | super(TextTestResult, self).addSkip(test, reason) 63 | if self.showAll: 64 | self.stream.write('- ') 65 | self.stream.writeln(self.getDescription(test)) 66 | self.stream.writeln('\t[skipped] {0!r}'.format(reason)) 67 | 68 | def addExpectedFailure(self, test, err): 69 | super(TextTestResult, self).addExpectedFailure(test, err) 70 | if self.showAll: 71 | self.stream.write('o ') 72 | self.stream.writeln(self.getDescription(test)) 73 | self.stream.writeln('\t[expected failure]') 74 | 75 | def addUnexpectedSuccess(self, test): 76 | super(TextTestResult, self).addUnexpectedSuccess(test) 77 | if self.showAll: 78 | self.stream.write('! ') 79 | self.stream.writeln(self.getDescription(test)) 80 | self.stream.writeln('\t[unexpected success]') 81 | 82 | def getDescription(self, test): 83 | # Chop the test method ('test_request') off the test.id(). 84 | name = test.id().rsplit('.', 1)[0] 85 | desc = test.test_data.get('desc', None) 86 | return ': '.join((name, desc)) if desc else name 87 | 88 | def _exc_info_to_string(self, err, test): 89 | """Override exception to string handling 90 | 91 | The default does too much. We don't want doctoring. We want 92 | information! 93 | """ 94 | return err 95 | 96 | def printErrorList(self, flavor, errors): 97 | for test, err in errors: 98 | # err[0] is the type of exception 99 | # err[1] is the args of the exception 100 | # err[3] is the traceback, not currently used 101 | self.stream.writeln('%s: %s' % (flavor, self.getDescription(test))) 102 | message = str(err[1]) 103 | for line in message.splitlines(): 104 | self.stream.writeln('\t%s' % line) 105 | 106 | 107 | class PyTestResult(TestResult): 108 | """Wrap a test result to allow it to work with pytest. 109 | 110 | The main behaviors here are: 111 | 112 | * to turn what had been exceptions back into exceptions 113 | * use pytest's skip and xfail methods 114 | """ 115 | 116 | def addFailure(self, test, err): 117 | raise err[1] 118 | 119 | def addError(self, test, err): 120 | raise err[1] 121 | 122 | def addSkip(self, test, reason): 123 | pytest.skip(reason) 124 | 125 | def addExpectedFailure(self, test, err): 126 | pytest.xfail('%s' % err[1]) 127 | 128 | 129 | class ConciseTestRunner(TextTestRunner): 130 | """A TextTestRunner that uses ConciseTestResult for reporting results.""" 131 | resultclass = ConciseTestResult 132 | -------------------------------------------------------------------------------- /gabbi/suite.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """A TestSuite for containing gabbi tests. 14 | 15 | This suite has two features: the contained tests are ordered and there 16 | are suite-level fixtures that operate as context managers. 17 | """ 18 | 19 | import sys 20 | import unittest 21 | 22 | from gabbi import fixture 23 | 24 | 25 | def noop(*args): 26 | """A noop method used to disable collected tests.""" 27 | pass 28 | 29 | 30 | class GabbiSuite(unittest.TestSuite): 31 | """A TestSuite with fixtures. 32 | 33 | The suite wraps the tests with a set of nested context managers that 34 | operate as fixtures. 35 | 36 | If a fixture raises unittest.case.SkipTest during setup, all the 37 | tests in this suite will be skipped. 38 | """ 39 | 40 | def run(self, result, debug=False): 41 | """Override TestSuite run to start suite-level fixtures. 42 | 43 | To avoid exception confusion, use a null Fixture when there 44 | are no fixtures. 45 | """ 46 | 47 | fixtures, host, port = self._get_fixtures() 48 | 49 | try: 50 | with fixture.nest([fix() for fix in fixtures]): 51 | result = super(GabbiSuite, self).run(result, debug) 52 | except unittest.SkipTest as exc: 53 | for test in self._tests: 54 | result.addSkip(test, str(exc)) 55 | # If we have an exception in the nested fixtures, that means 56 | # there's been an exception somewhere in the cycle other 57 | # than a specific test (as that would have been caught 58 | # already), thus from a fixture. If that exception were to 59 | # continue to raise here, then some test runners would 60 | # swallow it and the traceback of the failure would be 61 | # undiscoverable. To ensure the traceback is reported (via 62 | # the testrunner) to a human, the first test in the suite is 63 | # marked as having an error (it's fixture failed) and then 64 | # the entire suite is skipped, and the result stream told 65 | # we're done. If there are no tests (an empty suite) the 66 | # exception is re-raised. 67 | except Exception: 68 | if self._tests: 69 | result.addError(self._tests[0], sys.exc_info()) 70 | for test in self._tests: 71 | result.addSkip(test, 'fixture failure') 72 | result.stop() 73 | else: 74 | raise 75 | 76 | return result 77 | 78 | def start(self, result, tests=None): 79 | """Start fixtures when using pytest.""" 80 | tests = tests or [] 81 | fixtures, host, port = self._get_fixtures() 82 | 83 | self.used_fixtures = [] 84 | try: 85 | for fix in fixtures: 86 | fix_object = fix() 87 | fix_object.__enter__() 88 | self.used_fixtures.append(fix_object) 89 | except unittest.SkipTest as exc: 90 | # Disable the already collected tests that we now wish 91 | # to skip. 92 | for test in tests: 93 | test.run = noop 94 | test.add_marker('skip') 95 | result.addSkip(self, str(exc)) 96 | 97 | def stop(self): 98 | """Stop fixtures when using pytest.""" 99 | for fix in reversed(self.used_fixtures): 100 | fix.__exit__(None, None, None) 101 | 102 | def _get_fixtures(self): 103 | fixtures = [fixture.GabbiFixture] 104 | host = port = None 105 | try: 106 | first_test = self._find_first_full_test() 107 | fixtures = first_test.fixtures 108 | host = first_test.host 109 | port = first_test.port 110 | 111 | except AttributeError: 112 | pass 113 | 114 | return fixtures, host, port 115 | 116 | def _find_first_full_test(self): 117 | """Traverse a sparse test suite to find the first HTTPTestCase. 118 | 119 | When only some tests are requested empty TestSuites replace the 120 | unrequested tests. 121 | """ 122 | for test in self._tests: 123 | if hasattr(test, 'fixtures'): 124 | return test 125 | raise AttributeError('no fixtures found') 126 | -------------------------------------------------------------------------------- /gabbi/tests/README: -------------------------------------------------------------------------------- 1 | Some of the tests in this collection will attempt to connect to 2 | google over the internet to validate some behaviors using real 3 | socket connections. If this is not desirable (for example behind 4 | firewalls or in packaging situations) set GABBI_SKIP_NETWORK to 5 | true in the environment running the tests. 6 | -------------------------------------------------------------------------------- /gabbi/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdent/gabbi/b15ee55310fdf71c8b16a4412a5f9ef39c24268f/gabbi/tests/__init__.py -------------------------------------------------------------------------------- /gabbi/tests/custom_response_handler.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | 14 | from gabbi.handlers import base 15 | 16 | 17 | def gabbi_response_handlers(): 18 | return [CustomResponseHandler] 19 | 20 | 21 | class CustomResponseHandler(base.ResponseHandler): 22 | 23 | test_key_suffix = 'custom' 24 | test_key_value = [] 25 | 26 | def action(self, test, item, value=None): 27 | test.assertIn(item, test.output) 28 | -------------------------------------------------------------------------------- /gabbi/tests/external_server.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | 14 | import sys 15 | from wsgiref import simple_server 16 | 17 | from gabbi.tests import simple_wsgi 18 | 19 | 20 | def run(host, port): 21 | server = simple_server.make_server( 22 | host, 23 | int(port), 24 | simple_wsgi.SimpleWsgi(), 25 | ) 26 | server.serve_forever() 27 | 28 | 29 | if __name__ == "__main__": 30 | host = sys.argv[1] 31 | port = sys.argv[2] 32 | run(host, port) 33 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_handlers/cat.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "cat", 3 | "sound": "meow" 4 | } 5 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_handlers/data.json: -------------------------------------------------------------------------------- 1 | {"foo": {"bár": 1}} 2 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_handlers/pets.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "cat", 4 | "sound": "meow" 5 | }, 6 | { 7 | "type": "dog", 8 | "sound": "woof" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_handlers/subdir/data.yaml: -------------------------------------------------------------------------------- 1 | foo: 2 | bár: 1 3 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_handlers/subdir/pets.yaml: -------------------------------------------------------------------------------- 1 | - type: cat 2 | sound: meow 3 | - type: dog 4 | sound: woof 5 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_handlers/subdir/values.yaml: -------------------------------------------------------------------------------- 1 | values: 2 | - pets: 3 | - type: cat 4 | sound: meow 5 | - type: dog 6 | sound: woof 7 | - people: 8 | - name: chris 9 | id: 1 10 | - name: justin 11 | id: 2 12 | 13 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_handlers/values.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": [{ 3 | "pets": [{ 4 | "type": "cat", 5 | "sound": "meow" 6 | }, { 7 | "type": "dog", 8 | "sound": "woof" 9 | }] 10 | }, { 11 | "people": [{ 12 | "name": "chris", 13 | "id": 1 14 | }, { 15 | "name": "justin", 16 | "id": 2 17 | }] 18 | }] 19 | } 20 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_handlers/yaml-from-disk.yaml: -------------------------------------------------------------------------------- 1 | # Test loading expected data from file on disk with JSONPath 2 | # 3 | defaults: 4 | method: POST 5 | url: /somewhere 6 | request_headers: 7 | content-type: application/json 8 | verbose: True 9 | 10 | tests: 11 | - name: yaml encoded value from disk 12 | data: <@data.json 13 | response_json_paths: 14 | $.foo['bár']: <@subdir/data.yaml:$.foo['bár'] 15 | 16 | - name: json encoded value from disk 17 | data: <@data.json 18 | response_json_paths: 19 | $.foo['bár']: <@data.json:$.foo['bár'] 20 | 21 | - name: yaml parital from disk 22 | data: <@cat.json 23 | response_json_paths: 24 | $: <@subdir/pets.yaml:$[?type = "cat"] 25 | 26 | - name: yaml partial both sides 27 | data: <@pets.json 28 | response_json_paths: 29 | $[?type = "cat"].sound: <@subdir/values.yaml:$.values[0].pets[?type = "cat"].sound 30 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_inner/inner.yaml: -------------------------------------------------------------------------------- 1 | 2 | fixtures: 3 | - OuterFixture 4 | 5 | tests: 6 | 7 | - name: get one 8 | GET: / 9 | 10 | - name: get two 11 | GET: / 12 | 13 | - name: get three 14 | GET: / 15 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/backref.yaml: -------------------------------------------------------------------------------- 1 | # Make reference to prior request response data via json path. 2 | # 3 | 4 | tests: 5 | - name: post some json 6 | url: /posterchild 7 | request_headers: 8 | content-type: application/json 9 | data: '{"a": 1, "b": 2, "link": "/v2"}' 10 | method: POST 11 | response_json_paths: 12 | a: 1 13 | b: 2 14 | link: "/v2" 15 | response_headers: 16 | location: $SCHEME://$NETLOC/posterchild 17 | 18 | - name: post some more json 19 | url: $RESPONSE["link"] 20 | request_headers: 21 | content-type: application/json 22 | method: POST 23 | data: 24 | a: $RESPONSE['a'] 25 | c: $RESPONSE['link'] 26 | d: 27 | z: $RESPONSE['b'] 28 | response_json_paths: 29 | a: $RESPONSE["a"] 30 | c: /v2 31 | d: 32 | z: $RESPONSE['b'] 33 | response_headers: 34 | x-gabbi-url: $SCHEME://$NETLOC/v2 35 | 36 | - name: post even more json 37 | url: $RESPONSE['c'] 38 | request_headers: 39 | content-type: application/json 40 | method: POST 41 | data: | 42 | {"a": "$RESPONSE['a']", 43 | "c": "$RESPONSE['c']"} 44 | response_strings: 45 | - '"a": "$RESPONSE[''a'']"' 46 | - '"c": "/v2"' 47 | response_headers: 48 | location: $SCHEME://$NETLOC$RESPONSE['c'] 49 | x-gabbi-url: $SCHEME://$NETLOC/v2 50 | content-type: $HEADERS['content-type'] 51 | 52 | - name: post even more json quote different 53 | url: $RESPONSE["c"] 54 | request_headers: 55 | content-type: application/json 56 | method: POST 57 | data: | 58 | {"a": $RESPONSE["a"], 59 | "c": "$RESPONSE["c"]"} 60 | response_strings: 61 | - '"a": $RESPONSE["a"]' 62 | - '"c": "/v2"' 63 | response_headers: 64 | location: $SCHEME://$NETLOC$RESPONSE['c'] 65 | x-gabbi-url: $SCHEME://$NETLOC/v2 66 | content-type: $HEADERS['content-type'] 67 | 68 | - name: use raw json from response 69 | POST: $LAST_URL 70 | request_headers: 71 | content-type: application/json 72 | # the value of '$' here should be {"c": "/v2", "a": 1} 73 | data: $RESPONSE['$'] 74 | response_json_paths: 75 | $.c: /v2 76 | $.a: 1 77 | 78 | - name: post a raw int as json 79 | POST: / 80 | request_headers: 81 | content-type: application/json 82 | data: 1 83 | response_json_paths: 84 | $: 1 85 | 86 | - name: repost that raw int 87 | POST: / 88 | request_headers: 89 | content-type: application/json 90 | data: $RESPONSE['$'] 91 | response_json_paths: 92 | $: 1 93 | 94 | - name: backref json fail start 95 | url: / 96 | method: POST 97 | data: '' 98 | 99 | - name: backref json fail end 100 | xfail: true 101 | url: $RESPONSE['url'] 102 | 103 | - name: get a historical response 104 | GET: /$HISTORY['post some json'].$RESPONSE['a'] 105 | response_headers: 106 | x-gabbi-url: $SCHEME://$NETLOC/1 107 | 108 | - name: get a historical response via jsonpath 109 | GET: /$HISTORY['post some json'].$RESPONSE['$.b'] 110 | response_headers: 111 | x-gabbi-url: $SCHEME://$NETLOC/2 112 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/casting.yaml: -------------------------------------------------------------------------------- 1 | # Various tests to get casting in replacers working as 2 | # expected. 3 | 4 | 5 | fixtures: 6 | - EnvironFixture 7 | 8 | defaults: 9 | verbose: True 10 | request_headers: 11 | content-type: application/json 12 | accept: application/json 13 | 14 | tests: 15 | - name: default casts 16 | desc: anything that looks like a number or bool becomes one 17 | POST: / 18 | data: 19 | int: $ENVIRON['INT'] 20 | float: $ENVIRON['FLOAT'] 21 | string: $ENVIRON['STR'] 22 | tbool: $ENVIRON['TBOOL'] 23 | fbool: $ENVIRON['FBOOL'] 24 | response_json_paths: 25 | int: 1 26 | float: 1.5 27 | string: 2 28 | tbool: true 29 | fbool: false 30 | 31 | - name: cast to string 32 | POST: / 33 | data: 34 | string: $ENVIRON:str['STR'] 35 | response_json_paths: 36 | string: "2" 37 | 38 | - name: cast to int internal 39 | desc: This will fail because the cast is across the entire message "foo ... bar" 40 | xfail: true 41 | POST: / 42 | data: 43 | int: foo $ENVIRON:int['INT'] bar 44 | response_json_paths: 45 | string: "foo 1 bar" 46 | 47 | - name: json set up 48 | POST: / 49 | data: 50 | int: $ENVIRON['INT'] 51 | float: $ENVIRON['FLOAT'] 52 | string: $ENVIRON:str['STR'] 53 | tbool: $ENVIRON['TBOOL'] 54 | fbool: $ENVIRON['FBOOL'] 55 | response_json_paths: 56 | int: 1 57 | float: 1.5 58 | string: "2" 59 | tbool: true 60 | fbool: false 61 | 62 | - name: send casted json 63 | POST: / 64 | data: 65 | casted: $RESPONSE:int['$.string'] 66 | response_json_paths: 67 | casted: 2 68 | 69 | - name: historic casted json 70 | POST: / 71 | data: 72 | casted: $HISTORY['json set up'].$RESPONSE:int['$.string'] 73 | response_json_paths: 74 | casted: 2 75 | 76 | - name: internal json fail 77 | xfail: True 78 | desc: This produces an expected run time error because casting here is not useful 79 | POST: / 80 | data: 81 | casted: in this $HISTORY['json set up'].$RESPONSE:int['$.string'] is errors 82 | response_json_paths: 83 | casted: in this 2 is errors 84 | 85 | - name: internal json fine 86 | POST: / 87 | data: 88 | casted: in this $HISTORY['json set up'].$RESPONSE['$.string'] is not errors 89 | response_json_paths: 90 | casted: in this 2 is not errors 91 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/cat.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "cat", 3 | "sound": "meow" 4 | } 5 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/coerce.yaml: -------------------------------------------------------------------------------- 1 | defaults: 2 | request_headers: 3 | content-type: application/json 4 | verbose: True 5 | 6 | tests: 7 | - name: post data 8 | POST: / 9 | data: 10 | one_string: "1" 11 | one_int: 1 12 | one_float: 1.1 13 | response_json_paths: 14 | $.one_string: "1" 15 | $.one_int: 1 16 | $.one_float: 1.1 17 | response_strings: 18 | - '"one_string": "1"' 19 | - '"one_int": 1' 20 | - '"one_float": 1.1' 21 | 22 | - name: use data 23 | desc: data will be coerced because templates in use 24 | POST: / 25 | data: 26 | one_string: !!str "$RESPONSE['$.one_string']" 27 | one_int: $RESPONSE['$.one_int'] 28 | one_float: $RESPONSE['$.one_float'] 29 | response_json_paths: 30 | $.one_string: "1" 31 | $.one_int: 1 32 | $.one_float: 1.1 33 | response_strings: 34 | - '"one_string": "1"' 35 | - '"one_int": 1' 36 | - '"one_float": 1.1' 37 | 38 | - name: from environ 39 | POST: / 40 | data: 41 | one_environ: $ENVIRON['ONE'] 42 | response_json_paths: 43 | $.one_environ: 1 44 | response_strings: 45 | - '"one_environ": 1' 46 | 47 | - name: with list 48 | POST: / 49 | data: 50 | - $ENVIRON['ONE'] 51 | - 2 52 | - "3" 53 | response_json_paths: 54 | $[0]: 1 55 | $[1]: 2 56 | $[2]: "3" 57 | response_strings: 58 | - '[1, 2, "3"]' 59 | 60 | - name: object with list 61 | desc: without recursive handling no coersion 62 | POST: / 63 | data: 64 | collection: 65 | - alpha: $ENVIRON['ONE'] 66 | beta: max 67 | - alpha: 2 68 | beta: climb 69 | response_json_paths: 70 | $.collection[0].alpha: 1 71 | $.collection[0].beta: max 72 | $.collection[1].alpha: 2 73 | $.collection[1].beta: climb 74 | response_strings: 75 | - '"alpha": 1' 76 | - '"beta": "max"' 77 | 78 | - name: post extra data 79 | POST: / 80 | data: 81 | a: 1 82 | b: 1.0 83 | c: '[1,2,3]' 84 | d: true 85 | e: false 86 | f: 87 | key: val 88 | g: null 89 | h: 90 | key: 91 | less_key: [1, true, null] 92 | more_key: 1 93 | response_json_paths: 94 | a: 1 95 | b: 1.0 96 | c: '[1,2,3]' 97 | d: true 98 | e: false 99 | f: 100 | key: val 101 | g: null 102 | h: 103 | key: 104 | less_key: [1, true, null] 105 | more_key: 1 106 | 107 | - name: check posted data 108 | POST: / 109 | data: 110 | a: $RESPONSE['$.a'] 111 | b: $RESPONSE['$.b'] 112 | c: $RESPONSE['$.c'] 113 | d: $RESPONSE['$.d'] 114 | e: $RESPONSE['$.e'] 115 | f: $RESPONSE['$.f'] 116 | g: $RESPONSE['$.g'] 117 | h: $RESPONSE['$.h'] 118 | response_json_paths: 119 | a: 1 120 | b: 1.0 121 | c: '[1,2,3]' 122 | d: true 123 | e: false 124 | f: 125 | key: val 126 | g: null 127 | h: 128 | key: 129 | less_key: [1, true, null] 130 | more_key: 1 131 | 132 | - name: Post again and check the results 133 | POST: / 134 | data: 135 | a: $HISTORY['post extra data'].$RESPONSE['$.a'] 136 | b: $HISTORY['post extra data'].$RESPONSE['$.b'] 137 | c: $HISTORY['post extra data'].$RESPONSE['$.c'] 138 | d: $HISTORY['post extra data'].$RESPONSE['$.d'] 139 | e: $HISTORY['post extra data'].$RESPONSE['$.e'] 140 | f: $HISTORY['post extra data'].$RESPONSE['$.f'] 141 | g: $HISTORY['post extra data'].$RESPONSE['$.g'] 142 | h: $HISTORY['post extra data'].$RESPONSE['$.h'] 143 | response_json_paths: 144 | a: $ENVIRON['ONE'] 145 | b: $ENVIRON['DECIMAL'] 146 | c: $ENVIRON['ARRAY_STRING'] 147 | d: $ENVIRON['TRUE'] 148 | e: $ENVIRON['FALSE'] 149 | f: 150 | key: $ENVIRON['STRING'] 151 | g: $ENVIRON['NULL'] 152 | h: 153 | key: 154 | less_key: 155 | - $ENVIRON['ONE'] 156 | - $ENVIRON['TRUE'] 157 | - $ENVIRON['NULL'] 158 | more_key: $ENVIRON['ONE'] 159 | 160 | - name: Post again and check the results (reversed) 161 | POST: / 162 | data: 163 | a: $ENVIRON['ONE'] 164 | b: $ENVIRON['DECIMAL'] 165 | c: $ENVIRON['ARRAY_STRING'] 166 | d: $ENVIRON['TRUE'] 167 | e: $ENVIRON['FALSE'] 168 | f: 169 | key: $ENVIRON['STRING'] 170 | g: $ENVIRON['NULL'] 171 | h: 172 | key: 173 | less_key: 174 | - $ENVIRON['ONE'] 175 | - $ENVIRON['TRUE'] 176 | - $ENVIRON['NULL'] 177 | more_key: $ENVIRON['ONE'] 178 | response_json_paths: 179 | a: $HISTORY['check posted data'].$RESPONSE['$.a'] 180 | b: $HISTORY['check posted data'].$RESPONSE['$.b'] 181 | c: $HISTORY['check posted data'].$RESPONSE['$.c'] 182 | d: $HISTORY['check posted data'].$RESPONSE['$.d'] 183 | e: $HISTORY['check posted data'].$RESPONSE['$.e'] 184 | f: $HISTORY['check posted data'].$RESPONSE['$.f'] 185 | g: $HISTORY['check posted data'].$RESPONSE['$.g'] 186 | h: 187 | key: 188 | less_key: 189 | - $HISTORY['check posted data'].$RESPONSE['$.h.key.less_key[0]'] 190 | - $HISTORY['check posted data'].$RESPONSE['$.h.key.less_key[1]'] 191 | - $HISTORY['check posted data'].$RESPONSE['$.h.key.less_key[2]'] 192 | more_key: $HISTORY['check posted data'].$RESPONSE['$.h.key.more_key'] 193 | 194 | - name: string internal replace 195 | POST: / 196 | data: 197 | endpoint_resp: /api/0.1/item/$HISTORY['check posted data'].$RESPONSE['$.a'] 198 | endpoint_var: /api/0.1/item/$ENVIRON['ONE'] 199 | response_json_paths: 200 | endpoint_resp: /api/0.1/item/1 201 | endpoint_var: /api/0.1/item/1 202 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/contenttype.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Don't require content type on PUT, POST and PATCH requests 3 | # in the test case. Instead allow testing of how the WSGI app handles 4 | # it. 5 | # 6 | # https://github.com/cdent/gabbi/issues/16 7 | # 8 | # This had been done initially to try to enforce some accuracy 9 | # in the created tests, but real-world use has shown we need to 10 | # be able to create tests that demonstrate that a server is behaving 11 | # poorly (and not requiring a content-type). 12 | # 13 | 14 | tests: 15 | - name: put no content-type 16 | desc: P methods should 400 when no content-type 17 | url: / 18 | data: '"moo"' 19 | method: PUT 20 | status: 400 21 | 22 | - name: post no content-type 23 | desc: P methods should 400 when no content-type 24 | url: / 25 | data: '"moo"' 26 | method: POST 27 | status: 400 28 | 29 | - name: patch no content-type 30 | desc: P methods should 400 when no content-type 31 | url: / 32 | data: '"moo"' 33 | method: PATCH 34 | status: 400 35 | 36 | - name: put content-type 37 | desc: P methods should 400 when no content-type 38 | url: / 39 | data: '"moo"' 40 | request_headers: 41 | content-type: application/json 42 | method: PUT 43 | 44 | - name: post content-type 45 | desc: P methods should 400 when no content-type 46 | url: / 47 | data: '"moo"' 48 | request_headers: 49 | content-type: application/json 50 | method: POST 51 | 52 | - name: patch content-type 53 | desc: P methods should 400 when no content-type 54 | url: / 55 | data: '"moo"' 56 | request_headers: 57 | content-type: application/json 58 | method: PATCH 59 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/cookie.yaml: -------------------------------------------------------------------------------- 1 | 2 | tests: 3 | - name: get a cookie 4 | GET: /cookie 5 | response_headers: 6 | set-cookie: session=1234; domain=.example.com 7 | 8 | - name: use that cookie in a URL 9 | desc: to get it somewhere we can test it and confirm domain is dropped 10 | GET: /foobar?$COOKIE 11 | response_headers: 12 | x-gabbi-url: $SCHEME://$NETLOC/foobar?session=1234 13 | 14 | - name: confirm no cookies causes error 15 | desc: "this will cause data unavailable: set-cookie" 16 | xfail: true 17 | GET: /foobar?$COOKIE 18 | response_headers: 19 | x-gabbi-url: $SCHEME://$NETLOC/foobar 20 | 21 | - name: use a historical cookie 22 | desc: Use a cookie from a test other than the last 23 | GET: /foobar?$HISTORY['get a cookie'].$COOKIE 24 | response_headers: 25 | x-gabbi-url: $SCHEME://$NETLOC/foobar?session=1234 26 | 27 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/data.json: -------------------------------------------------------------------------------- 1 | {"foo": {"bár": 1}} 2 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/data.yaml: -------------------------------------------------------------------------------- 1 | # Test loading POST data via data structures and file 2 | # 3 | 4 | tests: 5 | - name: load data dictionary 6 | url: / 7 | method: POST 8 | request_headers: 9 | content-type: application/json 10 | data: 11 | foo: 1 12 | bar: 2 13 | response_json_paths: 14 | foo: 1 15 | bar: 2 16 | 17 | - name: load data list 18 | url: / 19 | method: POST 20 | request_headers: 21 | content-type: application/json 22 | data: 23 | - 1 24 | - 2 25 | response_json_paths: 26 | $[0]: 1 27 | $[1]: 2 28 | $.`len`: 2 29 | 30 | - name: load json file 31 | url: / 32 | method: POST 33 | request_headers: 34 | content-type: application/json 35 | data: <@data.json 36 | response_json_paths: 37 | foo['bár']: 1 38 | 39 | - name: load image file 40 | url: / 41 | method: POST 42 | request_headers: 43 | content-type: image/png 44 | data: <@kitten.png 45 | 46 | - name: load encoded text 47 | url: / 48 | method: POST 49 | request_headers: 50 | content-type: text/plain 51 | data: <@utf8.txt 52 | 53 | - name: json value from disk 54 | POST: / 55 | request_headers: 56 | content-type: application/json 57 | data: <@data.json 58 | response_json_paths: 59 | foo['bár']: 1 60 | $: <@data.json 61 | 62 | - name: partial json from disk 63 | POST: / 64 | request_headers: 65 | content-type: application/json 66 | data: 67 | pets: 68 | - type: cat 69 | sound: meow 70 | - type: dog 71 | sound: woof 72 | response_json_paths: 73 | $.pets: <@pets.json 74 | $.pets[0]: <@cat.json 75 | 76 | - name: post data for next 77 | POST: / 78 | request_headers: 79 | content-type: application/json 80 | data: 81 | pets: 82 | type: cat 83 | 84 | - name: post data from prior response 85 | POST: / 86 | request_headers: 87 | content-type: application/json 88 | data: 89 | pets: 90 | type: $RESPONSE['$.pets.type'] 91 | response_json_paths: 92 | $.pets.type: cat 93 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/disable-response-handler.yaml: -------------------------------------------------------------------------------- 1 | # Test that disabling the response handler despite an accept match 2 | # works as required. 3 | 4 | tests: 5 | 6 | - name: get some not json fail 7 | desc: This will cause an error, presented as a test failure 8 | xfail: True 9 | GET: /notjson 10 | response_headers: 11 | content-type: application/json 12 | response_strings: 13 | - not valid json 14 | 15 | - name: get some not json gloss 16 | desc: this will not error because we do not parse 17 | GET: /notjson 18 | response_headers: 19 | content-type: application/json 20 | disable_response_handler: True 21 | response_strings: 22 | - not valid json 23 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/doubleresponse.yaml: -------------------------------------------------------------------------------- 1 | # Use double of the same formatter, some with mixed quotes 2 | 3 | tests: 4 | - name: post some json 5 | url: /posterchild 6 | request_headers: 7 | content-type: application/json 8 | data: 9 | baseURL: $SCHEME://$NETLOC/posterchild 10 | method: POST 11 | response_json_paths: 12 | baseURL: $SCHEME://$NETLOC/posterchild 13 | 14 | - name: generate that url 15 | desc: This would fail because of regex issues without post 4.1.0 changes 16 | verbose: true 17 | GET: $RESPONSE['$.baseURL']$RESPONSE['$.baseURL'] 18 | 19 | - name: generate another url 20 | desc: This would fail because of regex issues without post 4.1.0 changes 21 | verbose: true 22 | GET: $HISTORY['post some json'].$RESPONSE["$.baseURL"]$HISTORY['post some json'].$RESPONSE["$.baseURL"] 23 | 24 | - name: generate yet another url 25 | verbose: true 26 | GET: $HISTORY["post some json"].$RESPONSE['$.baseURL']$HISTORY['post some json'].$RESPONSE["$.baseURL"] 27 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/failskip.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Tests to confirm that xfail and skip are working. 3 | # 4 | 5 | tests: 6 | 7 | - name: wrong status 8 | xfail: True 9 | url: / 10 | status: 404 11 | 12 | - name: non existent header 13 | xfail: True 14 | url: / 15 | response_headers: 16 | unlikely_header: no way 17 | 18 | - name: skip me 19 | skip: Skipping for now because we can't do it 20 | url: http://nowhere.example.com/house 21 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/fixture.yaml: -------------------------------------------------------------------------------- 1 | 2 | fixtures: 3 | - FixtureOne 4 | - FixtureTwo 5 | 6 | tests: 7 | - name: just to see 8 | url: / 9 | - name: just to see one 10 | url: / 11 | - name: just to see two 12 | url: / 13 | - name: just to see three 14 | url: / 15 | 16 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/forbiddenheaders.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Test that headers that should not be there are definitely not 3 | # there. These mostly test in a negative fashion. 4 | # 5 | 6 | tests: 7 | 8 | - name: header not there basic 9 | GET: /foobar 10 | response_headers: 11 | x-gabbi-url: $SCHEME://$NETLOC/foobar 12 | response_forbidden_headers: 13 | - no-there-one 14 | - no-there-two 15 | 16 | - name: header is there fail 17 | xfail: True 18 | GET: /foobar 19 | response_headers: 20 | x-gabbi-url: $SCHEME://$NETLOC/foobar 21 | response_forbidden_headers: 22 | - x-gabbi-url 23 | 24 | - name: header is there fail case insensitive 25 | xfail: True 26 | GET: /foobar 27 | response_headers: 28 | x-gabbi-url: $SCHEME://$NETLOC/foobar 29 | response_forbidden_headers: 30 | - x-gaBBi-uRl 31 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/header-key.yaml: -------------------------------------------------------------------------------- 1 | # Test that header keys run the template handling. 2 | 3 | tests: 4 | 5 | - name: header named http 6 | verbose: True 7 | GET: /header_key 8 | request_headers: 9 | $SCHEME: some-scheme 10 | status: 200 11 | response_headers: 12 | $SCHEME: some-scheme 13 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/horse: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdent/gabbi/b15ee55310fdf71c8b16a4412a5f9ef39c24268f/gabbi/tests/gabbits_intercept/horse -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/host-header.yaml: -------------------------------------------------------------------------------- 1 | # Test, against intercepted WSGI app, that SNI and host header handling behaves. 2 | 3 | tests: 4 | 5 | - name: ssl no host 6 | ssl: true 7 | url: / 8 | 9 | - name: ssl with host 10 | ssl: true 11 | url: / 12 | request_headers: 13 | host: httpbin.org 14 | 15 | - name: ssl with capitalised host 16 | ssl: true 17 | url: / 18 | request_headers: 19 | Host: httpbin.org 20 | 21 | - name: host without ssl 22 | url: / 23 | request_headers: 24 | host: httpbin.org 25 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/integer-req-header.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - name: request with integer header value 3 | url: /foo 4 | request_headers: 5 | integer: 13 6 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/json-extensions.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Gabbi has extensions to JSONPath, so far just `len`, 3 | # we need to test it. 4 | # 5 | 6 | tests: 7 | 8 | - name: test len 9 | url: /foobar 10 | method: POST 11 | request_headers: 12 | content-type: application/json 13 | data: 14 | alpha: 15 | - one 16 | - two 17 | beta: hello 18 | response_json_paths: 19 | # the dict has two keys 20 | $.`len`: 2 21 | $.alpha[0]: one 22 | $.alpha.[1]: two 23 | # the list at alpha has two items 24 | $.alpha.`len`: 2 25 | $.beta: hello 26 | # the string at beta has five chars 27 | $.beta.`len`: 5 28 | 29 | - name: test sort 30 | url: /barfoo 31 | method: POST 32 | request_headers: 33 | content-type: application/json 34 | data: 35 | objects: 36 | - name: cow 37 | value: moo 38 | - name: cat 39 | value: meow 40 | response_json_paths: 41 | $.objects[/name][0].value: meow 42 | $.objects[/name][1].value: moo 43 | $.objects[\name][1].value: meow 44 | $.objects[\name][0].value: moo 45 | $.objects[/name]..value: ['meow', 'moo'] 46 | 47 | - name: test filtered 48 | url: /barfoo 49 | method: POST 50 | request_headers: 51 | content-type: application/json 52 | data: 53 | objects: 54 | - name: cow 55 | value: moo 56 | - name: cat 57 | value: meow 58 | response_json_paths: 59 | $.objects[?name = "cow"].value: moo 60 | $.objects[?name = "cat"].value: meow 61 | $.objects[?value = "meow"].name: cat 62 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/json-left-side.yaml: -------------------------------------------------------------------------------- 1 | defaults: 2 | request_headers: 3 | content-type: application/json 4 | verbose: True 5 | 6 | tests: 7 | - name: left side json one 8 | desc: for reuse on the next test 9 | POST: / 10 | data: 11 | alpha: alpha1 12 | beta: beta1 13 | 14 | - name: expand left side 15 | POST: / 16 | data: 17 | alpha1: alpha 18 | beta1: beta 19 | response_json_paths: 20 | $["$RESPONSE['$.alpha']"]: alpha 21 | 22 | - name: expand environ left side 23 | POST: / 24 | data: 25 | alpha1: alpha 26 | beta1: beta 27 | 1: cow 28 | response_json_paths: 29 | $.['$ENVIRON['ONE']']: cow 30 | 31 | - name: set key and value 32 | GET: /jsonator?key=$ENVIRON['ONE']&value=10 33 | 34 | - name: check key and value 35 | GET: /jsonator?key=$ENVIRON['ONE']&value=10 36 | response_json_paths: 37 | $.["$ENVIRON['ONE']"]: $RESPONSE['$["1"]'] 38 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/json-right-side.yaml: -------------------------------------------------------------------------------- 1 | # Test loading expected data from file on disk with JSONPath 2 | # 3 | defaults: 4 | request_headers: 5 | content-type: application/json 6 | verbose: True 7 | 8 | tests: 9 | - name: json encoded value from disk 10 | POST: / 11 | data: <@data.json 12 | response_json_paths: 13 | $.foo['bár']: <@data.json:$.foo['bár'] 14 | 15 | - name: json parital from disk 16 | POST: / 17 | data: <@cat.json 18 | response_json_paths: 19 | $: <@pets.json:$[?type = "cat"] 20 | 21 | - name: json partial both sides 22 | POST: / 23 | data: <@pets.json 24 | response_json_paths: 25 | $[?type = "cat"].sound: <@values.json:$.values[0].pets[?type = "cat"].sound 26 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/jsonbody.yaml: -------------------------------------------------------------------------------- 1 | # See if $ is the whole thing 2 | 3 | tests: 4 | 5 | - name: test fully body 6 | url: /foobar 7 | method: POST 8 | request_headers: 9 | content-type: application/json 10 | data: 11 | alpha: 12 | - one 13 | - two 14 | beta: hello 15 | response_json_paths: 16 | $: 17 | alpha: 18 | - one 19 | - two 20 | beta: hello 21 | 22 | - name: test empty dict 23 | url: /foobar 24 | method: POST 25 | request_headers: 26 | content-type: application/json 27 | data: {} 28 | response_json_paths: 29 | $: {} 30 | 31 | - name: test empty list 32 | url: /foobar 33 | method: POST 34 | request_headers: 35 | content-type: application/json 36 | data: [] 37 | response_json_paths: 38 | $: [] 39 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/kitten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdent/gabbi/b15ee55310fdf71c8b16a4412a5f9ef39c24268f/gabbi/tests/gabbits_intercept/kitten.png -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/last-url.yaml: -------------------------------------------------------------------------------- 1 | 2 | tests: 3 | - name: get a url the first time 4 | GET: /3ADE1BBB 5 | response_headers: 6 | x-gabbi-url: $SCHEME://$NETLOC/3ADE1BBB 7 | 8 | - name: get that same url again 9 | GET: $LAST_URL 10 | response_headers: 11 | x-gabbi-url: $SCHEME://$NETLOC/3ADE1BBB 12 | 13 | - name: get it a third time 14 | GET: $LAST_URL 15 | response_headers: 16 | x-gabbi-url: $SCHEME://$NETLOC/3ADE1BBB 17 | 18 | - name: add some query params 19 | GET: $LAST_URL 20 | query_parameters: 21 | key1: value1 22 | response_headers: 23 | x-gabbi-url: $SCHEME://$NETLOC/3ADE1BBB?key1=value1 24 | 25 | - name: now last url does not have those query params 26 | GET: $LAST_URL 27 | response_headers: 28 | x-gabbi-url: $SCHEME://$NETLOC/3ADE1BBB 29 | 30 | - name: last with adjusted parameters 31 | GET: $LAST_URL 32 | query_parameters: 33 | key1: value2 34 | response_headers: 35 | x-gabbi-url: $SCHEME://$NETLOC/3ADE1BBB?key1=value2 36 | 37 | - name: get a historical url 38 | GET: $HISTORY['get a url the first time'].$URL 39 | response_headers: 40 | x-gabbi-url: $SCHEME://$NETLOC/3ADE1BBB 41 | 42 | - name: get prior url 43 | GET: $URL 44 | response_headers: 45 | x-gabbi-url: $SCHEME://$NETLOC/3ADE1BBB 46 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/method-shortcut.yaml: -------------------------------------------------------------------------------- 1 | 2 | tests: 3 | - name: simple POST 4 | POST: /somewhere 5 | data: 6 | cow: barn 7 | request_headers: 8 | content-type: application/json 9 | response_json_paths: 10 | $.cow: barn 11 | 12 | - name: POST with query 13 | POST: /somewhere?chicken=coop 14 | data: 15 | cow: barn 16 | request_headers: 17 | content-type: application/json 18 | response_json_paths: 19 | $.cow: barn 20 | $.chicken[0]: coop 21 | 22 | - name: simple GET 23 | GET: / 24 | ssl: True 25 | response_headers: 26 | x-gabbi-url: https://$NETLOC/ 27 | 28 | - name: arbitrary method 29 | IMAGINARY: / 30 | status: 405 31 | response_headers: 32 | allow: GET, PUT, POST, DELETE, PATCH 33 | x-gabbi-method: IMAGINARY 34 | x-gabbi-url: $SCHEME://$NETLOC/ 35 | 36 | # Can't do this because format validation is during test generation not 37 | # test running. xfail only works during test running :( 38 | # See gabbi/tests/test_driver for a test of this. 39 | # - name: duplicate shortcut 40 | # GET: / 41 | # POST: / 42 | # xfail: true 43 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/pets.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "cat", 4 | "sound": "meow" 5 | }, 6 | { 7 | "type": "dog", 8 | "sound": "woof" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/poll.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Test polling until a desired response. 3 | # 4 | 5 | tests: 6 | 7 | # This will attempt /poller up to 8 times waiting for a status 8 | # 200, waiting one second between attempts. If the test passes 9 | # during any attempt then we move on to the next test. 10 | # If no pass happens, the last failure is raised as the 11 | # assertion failure. 12 | - name: poller 13 | url: /poller 14 | poll: 15 | delay: .01 16 | count: 8 17 | 18 | # This one we expect to fail because /poller in SimpleWSGI only 19 | # counts up to five attempts. 20 | - name: poller fail 21 | url: /poller 22 | xfail: True 23 | poll: 24 | delay: .01 25 | count: 3 26 | 27 | # Confirm that $LOCATION and $RESPONSE behave in poll. 28 | - name: create a thing 29 | url: /poller?count=2&x=1&y=2&z=3.4 30 | method: POST 31 | request_headers: 32 | content-type: application/json 33 | poll: 34 | count: 3 35 | delay: .01 36 | response_json_paths: 37 | $.x[0]: "1" 38 | $.y[0]: "2" 39 | $.z[0]: "3.4" 40 | 41 | 42 | - name: loop location 43 | url: $LOCATION 44 | verbose: True 45 | poll: 46 | count: $RESPONSE['$.z[0]'] 47 | delay: .01 48 | response_json_paths: 49 | $.x[0]: $RESPONSE['$.x[0]'] 50 | $.y[0]: $RESPONSE['$.y[0]'] 51 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/prefix.yaml: -------------------------------------------------------------------------------- 1 | 2 | tests: 3 | 4 | - name: provide a link 5 | POST: / 6 | request_headers: 7 | content-type: application/json 8 | data: 9 | link: $ENVIRON['GABBI_PREFIX']/barnabas 10 | relative: link 11 | 12 | - name: get that link 13 | GET: $RESPONSE['$.link'] 14 | response_headers: 15 | x-gabbi-url: "///[a-f0-9:-]+$ENVIRON['GABBI_PREFIX']/barnabas/" 16 | 17 | - name: get relative link 18 | GET: $HISTORY['provide a link'].$RESPONSE['$.relative'] 19 | response_headers: 20 | x-gabbi-url: "///[a-f0-9:-]+$ENVIRON['GABBI_PREFIX']/link/" 21 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/queryparams.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # As a convenience a URL can be augmented with structured declaration 3 | # of query parameters. 4 | # 5 | 6 | tests: 7 | 8 | - name: simple param 9 | url: /foo 10 | query_parameters: 11 | bar: 1 12 | response_headers: 13 | x-gabbi-url: $SCHEME://$NETLOC/foo?bar=1 14 | 15 | - name: joined params 16 | url: /foo?cow=moo 17 | query_parameters: 18 | bar: 1 19 | response_headers: 20 | x-gabbi-url: $SCHEME://$NETLOC/foo?cow=moo&bar=1 21 | 22 | - name: multi params 23 | url: /foo 24 | request_headers: 25 | accept: application/json 26 | query_parameters: 27 | bar: 28 | - 1 29 | - 2 30 | response_headers: 31 | x-gabbi-url: $SCHEME://$NETLOC/foo?bar=1&bar=2 32 | content-type: application/json 33 | response_json_paths: 34 | $.bar[0]: "1" 35 | $.bar[1]: "2" 36 | 37 | - name: replacers in params 38 | url: /foo 39 | query_parameters: 40 | fromjson: $RESPONSE['$.bar[0]'] 41 | response_headers: 42 | x-gabbi-url: $SCHEME://$NETLOC/foo?fromjson=1 43 | 44 | - name: unicode 45 | url: /foo 46 | query_parameters: 47 | snowman: ☃ 48 | response_headers: 49 | x-gabbi-url: $SCHEME://$NETLOC/foo?snowman=%E2%98%83 50 | 51 | - name: url in param 52 | url: /foo 53 | query_parameters: 54 | redirect: http://example.com/treehouse?secret=true&password=hello 55 | response_headers: 56 | x-gabbi-url: $SCHEME://$NETLOC/foo?redirect=http%3A%2F%2Fexample.com%2Ftreehouse%3Fsecret%3Dtrue%26password%3Dhello 57 | 58 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/regex.yaml: -------------------------------------------------------------------------------- 1 | # Confirm regex handling in response headers, strings and json path handlers 2 | tests: 3 | - name: regex header test 4 | url: /cow?alpha=1 5 | method: PUT 6 | response_headers: 7 | x-gabbi-url: /cow\?al.*=1/ 8 | location: $SCHEME://$NETLOC/cow?alpha=1 9 | 10 | - name: regex jsonpath test 11 | PUT: /cow 12 | request_headers: 13 | content-type: application/json 14 | data: 15 | alpha: cow 16 | beta: pig 17 | gamma: 1 18 | response_json_paths: 19 | $.alpha: /ow$/ 20 | $.beta: /(?!cow).*/ 21 | $.gamma: /\d+/ 22 | 23 | - name: regex string test json 24 | PUT: /cow 25 | request_headers: 26 | content-type: application/json 27 | data: 28 | alpha: cow 29 | beta: pig 30 | gamma: 1 31 | response_strings: 32 | - '/"alpha": "cow",/' 33 | 34 | - name: regex string test multiline 35 | GET: /presenter 36 | response_strings: 37 | - '/Hello World/' 38 | - '/dolor sit/' 39 | 40 | - name: regex string test splat 41 | GET: /presenter 42 | response_strings: 43 | - '/dolor.*amet/' 44 | 45 | - name: regex string test mix 46 | GET: /presenter 47 | response_strings: 48 | - '/[Hh]el{2}o [Ww]orld/' 49 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/self.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Tests for testing gabbi, using the built in SimpleWsgi app. 3 | # 4 | 5 | defaults: 6 | url: /cow?alpha=1 7 | request_headers: 8 | x-random-header: ya 9 | 10 | tests: 11 | - name: get simple page 12 | url: / 13 | verbose: True 14 | 15 | - name: inheritance of defaults 16 | response_headers: 17 | x-gabbi-url: $SCHEME://$NETLOC/cow?alpha=1 18 | response_strings: 19 | - '"alpha": ["1"]' 20 | response_json_paths: 21 | alpha[0]: "1" 22 | 23 | - name: bogus method 24 | url: / 25 | method: UNREAL 26 | status: 405 27 | response_headers: 28 | allow: GET, PUT, POST, DELETE, PATCH 29 | x-gabbi-method: UNREAL 30 | x-gabbi-url: $SCHEME://$NETLOC/ 31 | 32 | - name: query returned 33 | url: /somewhere?foo=1&bar=2&bar=3 34 | response_strings: 35 | - "\"bar\": [\"2\", \"3\"]" 36 | response_json_paths: 37 | bar: 38 | - "2" 39 | - "3" 40 | 41 | - name: simple post 42 | url: /named/thing 43 | method: POST 44 | response_headers: 45 | location: $SCHEME://$NETLOC/named/thing 46 | 47 | - name: use prior location 48 | url: $LOCATION 49 | response_headers: 50 | x-gabbi-url: $SCHEME://$NETLOC/named/thing 51 | 52 | - name: use a historical location 53 | url: $HISTORY['simple post'].$LOCATION 54 | response_headers: 55 | x-gabbi-url: $SCHEME://$NETLOC/named/thing 56 | 57 | - name: checklimit 58 | url: / 59 | 60 | - name: post a body 61 | url: /somewhere 62 | method: POST 63 | data: 64 | cow: barn 65 | request_headers: 66 | content-type: application/json 67 | response_json_paths: 68 | $.cow: barn 69 | 70 | - name: get location from headers 71 | url: $HEADERS['locaTion'] 72 | response_headers: 73 | x-gabbi-url: $SCHEME://$NETLOC/somewhere 74 | 75 | - name: get historical location from headers 76 | url: $HISTORY['post a body'].$HEADERS['locaTion'] 77 | response_headers: 78 | x-gabbi-url: $SCHEME://$NETLOC/somewhere 79 | 80 | - name: post a body with query 81 | url: /somewhere?chicken=coop 82 | method: POST 83 | data: 84 | cow: barn 85 | request_headers: 86 | content-type: application/json 87 | response_json_paths: 88 | $.cow: barn 89 | $.chicken[0]: coop 90 | 91 | - name: get ssl page 92 | url: / 93 | ssl: True 94 | response_headers: 95 | x-gabbi-url: https://$NETLOC/ 96 | 97 | - name: test binary handling 98 | url: / 99 | request_headers: 100 | accept: image/png 101 | response_headers: 102 | content-type: image/png 103 | 104 | - name: confirm environ 105 | url: /$ENVIRON['GABBI_TEST_URL'] 106 | response_headers: 107 | x-gabbi-url: $SCHEME://$NETLOC/takingnames 108 | 109 | - name: confirm environ no key fail 110 | desc: this confirms that no key leads to failure rather than error 111 | xfail: true 112 | url: /$ENVIRON['1385F1EB-DC5C-4A95-8928-58673FB272DC'] 113 | 114 | - name: test pluggable response 115 | url: /foo?alpha=1 116 | response_test: 117 | - 'COW"alpha": ["1"]' 118 | - COWAnother line 119 | 120 | - name: fail pluggable response 121 | desc: this one will fail because COW is not removable 122 | url: /foo?alpha=1 123 | xfail: true 124 | response_test: 125 | - 'CO"alpha": ["1"]' 126 | 127 | - name: test exception wrapper 128 | desc: simple wsgi will raise exception 129 | url: / 130 | xfail: true 131 | method: DIE 132 | 133 | - name: non json response failure 134 | desc: asking for json in a non json test should be failure not error 135 | url: / 136 | xfail: true 137 | method: GET 138 | request_headers: 139 | accept: text/plain 140 | response_json_paths: 141 | $.data: hello 142 | 143 | - name: json derived content type 144 | desc: +json types should work for json paths 145 | url: /?data=hello 146 | method: GET 147 | request_headers: 148 | accept: application/vnd.complex+json 149 | response_json_paths: 150 | $.data[0]: hello 151 | 152 | - name: xml derived content type 153 | desc: +xml types should not work for json paths 154 | xfail: true 155 | url: /?data=hello 156 | method: GET 157 | request_headers: 158 | accept: application/vnd.complex+xml 159 | response_json_paths: 160 | $.data[0]: hello 161 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/skipall.yaml: -------------------------------------------------------------------------------- 1 | 2 | fixtures: 3 | - SkipAllFixture 4 | 5 | tests: 6 | - name: a skipped test 7 | url: / 8 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/subdir/values.yaml: -------------------------------------------------------------------------------- 1 | values: 2 | - pets: 3 | - type: cat 4 | sound: meow 5 | - type: dog 6 | sound: woof 7 | - people: 8 | - name: chris 9 | id: 1 10 | - name: justin 11 | id: 2 12 | 13 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/values.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": [{ 3 | "pets": [{ 4 | "type": "cat", 5 | "sound": "meow" 6 | }, { 7 | "type": "dog", 8 | "sound": "woof" 9 | }] 10 | }, { 11 | "people": [{ 12 | "name": "chris", 13 | "id": 1 14 | }, { 15 | "name": "justin", 16 | "id": 2 17 | }] 18 | }] 19 | } 20 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_intercept/verbosity.yaml: -------------------------------------------------------------------------------- 1 | # Testing for https://github.com/cdent/gabbi/issues/282 2 | 3 | 4 | tests: 5 | - name: confirm notempty 6 | verbose: true 7 | GET: /notempty 8 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_live/google.yaml: -------------------------------------------------------------------------------- 1 | fixtures: 2 | - LiveSkipFixture 3 | 4 | defaults: 5 | ssl: True 6 | 7 | tests: 8 | - name: google 9 | url: / 10 | status: 302 || 301 11 | 12 | - name: follow redirects 13 | desc: Confirm redirects are followed when we ask 14 | url: / 15 | redirects: True 16 | status: 200 17 | 18 | # Explicit hosts 19 | - name: google full url 20 | url: https://google.com/ 21 | status: 302 || 301 22 | 23 | - name: google russia 24 | desc: Test handling of non-utf-8 encoding 25 | url: https://www.google.ru/ 26 | 27 | - name: follow redirects full url 28 | desc: Confirm redirects are followed when we ask 29 | url: https://google.com 30 | redirects: True 31 | status: 200 32 | 33 | - name: google with HTTP/1.1 34 | desc: Test that Google replies with HTTP/1.1 35 | url: https://www.google.com/ 36 | response_headers: 37 | http_protocol_version: HTTP/1.1 38 | 39 | - name: google with HTTP/2 40 | desc: Test that Google replies with HTTP/2 41 | url: https://www.google.com/ 42 | http_version: 2 43 | response_headers: 44 | http_protocol_version: HTTP/2 45 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_live/host-header.yaml: -------------------------------------------------------------------------------- 1 | # Test, against httpbin.org that SNI and host header handling behaves. 2 | 3 | fixtures: 4 | - LiveSkipFixture 5 | 6 | 7 | tests: 8 | 9 | - name: ssl no host 10 | url: https://httpbin.org/ 11 | 12 | - name: ssl with host 13 | url: https://httpbin.org/ 14 | request_headers: 15 | host: httpbin.org 16 | 17 | - name: ssl wrong host 18 | desc: This is expected to fail with an error from urllib 19 | xfail: true 20 | url: https://httpbin.org/ 21 | request_headers: 22 | host: bin.org 23 | 24 | - name: host without ssl 25 | url: http://httpbin.org 26 | request_headers: 27 | host: httpbin.org 28 | 29 | - name: wrong host without ssl 30 | url: http://httpbin.org 31 | request_headers: 32 | host: bin.org 33 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_runner/failure.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | 3 | - name: expected failure 4 | GET: / 5 | 6 | status: 666 7 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_runner/nan.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - name: test NAN 3 | url: /nan 4 | method: GET 5 | request_headers: 6 | content-type: application/json 7 | response_json_paths: 8 | $.nan: !!python/object:gabbi.tests.util.NanChecker {} 9 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_runner/subdir/sample.json: -------------------------------------------------------------------------------- 1 | {"items": {"house": "blue"}} 2 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_runner/success.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | 3 | - name: expected success 4 | GET: /baz 5 | 6 | status: 200 7 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_runner/success_alt.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | 3 | - name: expected success 4 | GET: /baz 5 | 6 | status: 200 7 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_runner/test_data.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | 3 | - name: POST data from file 4 | verbose: true 5 | POST: / 6 | request_headers: 7 | content-type: application/json 8 | data: <@subdir/sample.json 9 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_runner/test_verbose.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | 3 | - name: POST data with verbose true 4 | verbose: true 5 | POST: / 6 | request_headers: 7 | content-type: application/json 8 | data: 9 | - our text 10 | 11 | - name: structured data 12 | verbose: true 13 | POST: / 14 | request_headers: 15 | content-type: application/json 16 | data: 17 | cow: moo 18 | dog: bark 19 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_runner/verbosity.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | 3 | - name: simple data post 4 | POST: / 5 | request_headers: 6 | content-type: application/json 7 | data: 8 | cat: poppy 9 | -------------------------------------------------------------------------------- /gabbi/tests/gabbits_unsafe_yaml/nan.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - name: test NAN 3 | url: /nan 4 | method: GET 5 | request_headers: 6 | content-type: application/json 7 | response_json_paths: 8 | $.nan: !NanChecker {} 9 | $.nan: !IsNAN 10 | -------------------------------------------------------------------------------- /gabbi/tests/simple_wsgi.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """ 14 | SimpleWsgi provides a WSGI callable that can be used in tests to 15 | reflect posted data and otherwise confirm headers and queries. 16 | """ 17 | 18 | import json 19 | import urllib.parse as urlparse 20 | 21 | 22 | CURRENT_POLL = 0 23 | METHODS = ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'] 24 | 25 | 26 | class SimpleWsgi: 27 | """A simple wsgi application to use in tests.""" 28 | 29 | def __call__(self, environ, start_response): 30 | global METHODS 31 | global CURRENT_POLL 32 | 33 | script_name = environ.get('SCRIPT_NAME', '') 34 | path_info = environ.get('PATH_INFO', '').removeprefix(script_name) 35 | request_method = environ['REQUEST_METHOD'].upper() 36 | query_string = environ.get('QUERY_STRING', '') 37 | query_data = urlparse.parse_qs(query_string) 38 | request_url = script_name + path_info 39 | accept_header = environ.get('HTTP_ACCEPT') 40 | content_type_header = environ.get('CONTENT_TYPE', '') 41 | 42 | full_request_url = self._fully_qualify( 43 | environ, 44 | request_url, 45 | query_string, 46 | ) 47 | 48 | if accept_header and accept_header != '*/*': 49 | response_content_type = accept_header 50 | else: 51 | # JSON doesn't need a charset but we throw one in here 52 | # to exercise the decoding code 53 | response_content_type = ( 54 | 'application/json ; charset=utf-8 ; stop=no') 55 | 56 | headers = [ 57 | ('X-Gabbi-method', request_method), 58 | ('Content-Type', response_content_type), 59 | ('X-Gabbi-url', full_request_url), 60 | ] 61 | 62 | if request_method == 'DIE': 63 | raise Exception('because you asked me to') 64 | 65 | if request_method not in METHODS: 66 | headers.append( 67 | ('Allow', ', '.join(METHODS))) 68 | start_response('405 Method Not Allowed', headers) 69 | return [] 70 | 71 | if request_method.startswith('P'): 72 | length = int(environ.get('CONTENT_LENGTH', '0')) 73 | body = environ['wsgi.input'].read(length) 74 | if body: 75 | if not content_type_header: 76 | start_response('400 Bad request', headers) 77 | return [] 78 | if content_type_header == 'application/json': 79 | body_data = json.loads(body.decode('utf-8')) 80 | if query_data: 81 | query_data.update(body_data) 82 | else: 83 | query_data = body_data 84 | headers.append(('Location', full_request_url)) 85 | 86 | if path_info == '/presenter': 87 | start_response('200 OK', [('Content-Type', 'text/html')]) 88 | return [b""" 89 | 90 | 91 | 92 | Hello World 93 | 94 | 95 |

Hello World

96 |

lorem ipsum dolor sit amet

97 | 98 | 99 | """] 100 | # Provide response that claims to be json but is not. 101 | elif path_info.startswith('/notjson'): 102 | start_response('200 OK', [('Content-Type', 'application/json')]) 103 | return [b'not valid json'] 104 | elif path_info.startswith('/poller'): 105 | if CURRENT_POLL == 0: 106 | CURRENT_POLL = int(query_data.get('count', [5])[0]) 107 | start_response('400 Bad Reqest', []) 108 | return [] 109 | else: 110 | CURRENT_POLL -= 1 111 | if CURRENT_POLL > 0: 112 | start_response('400 Bad Reqest', []) 113 | return [] 114 | else: 115 | CURRENT_POLL = 0 116 | # fall through if we've ended the loop 117 | elif path_info == '/cookie': 118 | headers.append(('Set-Cookie', 'session=1234; domain=.example.com')) 119 | elif path_info == '/jsonator': 120 | json_data = json.dumps({query_data['key'][0]: 121 | query_data['value'][0]}) 122 | start_response('200 OK', [('Content-Type', 'application/json')]) 123 | return [json_data.encode('utf-8')] 124 | elif path_info == '/nan': 125 | start_response('200 OK', [('Content-Type', 'application/json')]) 126 | return [json.dumps({ 127 | "nan": float('nan') 128 | }).encode('utf-8')] 129 | elif path_info == '/header_key': 130 | scheme_header = environ.get('HTTP_HTTP', False) 131 | 132 | if scheme_header: 133 | headers.append(('HTTP', scheme_header)) 134 | start_response('200 OK', headers) 135 | else: 136 | start_response('500 SERVER ERROR', headers) 137 | 138 | query_output = json.dumps(query_data) 139 | return [query_output.encode('utf-8')] 140 | elif path_info == '/notempty': 141 | # This block is used to experiment with verbosity handling. 142 | # See: https://github.com/cdent/gabbi/issues/282 143 | content_type = query_data.get('content-type', [None])[0] 144 | headers = [] 145 | if content_type: 146 | headers.append(('Content-type', content_type)) 147 | start_response('200 OK', headers) 148 | return ['notempty'.encode('utf-8')] 149 | 150 | start_response('200 OK', headers) 151 | 152 | query_output = json.dumps(query_data) 153 | return [query_output.encode('utf-8')] 154 | 155 | @staticmethod 156 | def _fully_qualify(environ, url, query_data): 157 | """Turn a URL path into a fully qualified URL.""" 158 | split_url = urlparse.urlsplit(url) 159 | server_name = environ.get('SERVER_NAME') 160 | server_port = str(environ.get('SERVER_PORT')) 161 | server_scheme = environ.get('wsgi.url_scheme') 162 | if server_port not in ['80', '443']: 163 | netloc = '%s:%s' % (server_name, server_port) 164 | else: 165 | netloc = server_name 166 | 167 | return urlparse.urlunsplit((server_scheme, netloc, split_url.path, 168 | query_data, split_url.fragment)) 169 | -------------------------------------------------------------------------------- /gabbi/tests/test_data_to_string.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Test handling of data field in tests. 14 | """ 15 | 16 | import unittest 17 | 18 | from gabbi import case 19 | from gabbi import handlers 20 | 21 | 22 | class TestDataToString(unittest.TestCase): 23 | 24 | def setUp(self): 25 | self.case = case.HTTPTestCase('test_request') 26 | self.case.content_handlers = [] 27 | for handler in handlers.RESPONSE_HANDLERS: 28 | h = handler() 29 | if hasattr(h, 'content_handler') and h.content_handler: 30 | self.case.content_handlers.append(h) 31 | 32 | def testHappyPath(self): 33 | data = [{"hi": "low"}, {"yes": "no"}] 34 | content_type = 'application/json' 35 | body = self.case._test_data_to_string(data, content_type) 36 | self.assertEqual('[{"hi": "low"}, {"yes": "no"}]', body) 37 | 38 | def testNoContentType(self): 39 | data = [{"hi": "low"}, {"yes": "no"}] 40 | content_type = '' 41 | with self.assertRaises(ValueError) as exc: 42 | self.case._test_data_to_string(data, content_type) 43 | self.assertEqual( 44 | 'no content-type available for processing data', 45 | str(exc.exception)) 46 | 47 | def testNoHandler(self): 48 | data = [{"hi": "low"}, {"yes": "no"}] 49 | content_type = 'application/xml' 50 | with self.assertRaises(ValueError) as exc: 51 | self.case._test_data_to_string(data, content_type) 52 | self.assertEqual( 53 | 'unable to process data to application/xml', 54 | str(exc.exception)) 55 | -------------------------------------------------------------------------------- /gabbi/tests/test_driver.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Test that the driver can build tests effectively.""" 14 | 15 | import os 16 | import unittest 17 | 18 | from gabbi import driver 19 | 20 | 21 | TESTS_DIR = 'test_gabbits' 22 | 23 | 24 | class DriverTest(unittest.TestCase): 25 | 26 | def setUp(self): 27 | super(DriverTest, self).setUp() 28 | self.loader = unittest.defaultTestLoader 29 | self.test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 30 | 31 | def test_driver_loads_three_tests(self): 32 | suite = driver.build_tests(self.test_dir, self.loader, 33 | host='localhost', port=8001) 34 | self.assertEqual(1, len(suite._tests), 35 | 'top level suite contains one suite') 36 | self.assertEqual(3, len(suite._tests[0]._tests), 37 | 'contained suite contains three tests') 38 | the_one_test = suite._tests[0]._tests[0] 39 | self.assertEqual('test_driver_sample_one', 40 | the_one_test.__class__.__name__, 41 | 'test class name maps') 42 | self.assertEqual('one', 43 | the_one_test.test_data['name']) 44 | self.assertEqual('/', the_one_test.test_data['url']) 45 | 46 | def test_driver_prefix(self): 47 | suite = driver.build_tests(self.test_dir, self.loader, 48 | host='localhost', port=8001, 49 | prefix='/mountpoint') 50 | the_one_test = suite._tests[0]._tests[0] 51 | the_two_test = suite._tests[0]._tests[1] 52 | self.assertEqual('/mountpoint', the_one_test.prefix) 53 | self.assertEqual('/mountpoint', the_two_test.prefix) 54 | 55 | def test_build_requires_host_or_intercept(self): 56 | with self.assertRaises(AssertionError): 57 | driver.build_tests(self.test_dir, self.loader) 58 | 59 | def test_build_with_url_provides_host(self): 60 | """This confirms that url provides the required host.""" 61 | suite = driver.build_tests(self.test_dir, self.loader, 62 | url='https://foo.example.com') 63 | first_test = suite._tests[0]._tests[0] 64 | full_url = first_test._parse_url(first_test.test_data['url']) 65 | ssl = first_test.test_data['ssl'] 66 | self.assertEqual('https://foo.example.com/', full_url) 67 | self.assertTrue(ssl) 68 | 69 | def test_build_require_ssl(self): 70 | suite = driver.build_tests(self.test_dir, self.loader, 71 | host='localhost', 72 | require_ssl=True) 73 | first_test = suite._tests[0]._tests[0] 74 | full_url = first_test._parse_url(first_test.test_data['url']) 75 | self.assertEqual('https://localhost:8001/', full_url) 76 | 77 | suite = driver.build_tests(self.test_dir, self.loader, 78 | host='localhost', 79 | require_ssl=False) 80 | first_test = suite._tests[0]._tests[0] 81 | full_url = first_test._parse_url(first_test.test_data['url']) 82 | self.assertEqual('http://localhost:8001/', full_url) 83 | 84 | def test_build_url_target(self): 85 | suite = driver.build_tests(self.test_dir, self.loader, 86 | host='localhost', port='999', 87 | url='https://example.com:1024/theend') 88 | first_test = suite._tests[0]._tests[0] 89 | full_url = first_test._parse_url(first_test.test_data['url']) 90 | self.assertEqual('https://example.com:1024/theend/', full_url) 91 | 92 | def test_build_url_target_forced_ssl(self): 93 | suite = driver.build_tests(self.test_dir, self.loader, 94 | host='localhost', port='999', 95 | url='http://example.com:1024/theend', 96 | require_ssl=True) 97 | first_test = suite._tests[0]._tests[0] 98 | full_url = first_test._parse_url(first_test.test_data['url']) 99 | self.assertEqual('https://example.com:1024/theend/', full_url) 100 | 101 | def test_build_url_use_prior_test(self): 102 | suite = driver.build_tests(self.test_dir, self.loader, 103 | host='localhost', 104 | use_prior_test=True) 105 | for test in suite._tests[0]._tests: 106 | if test.test_data['name'] != 'use_prior_false': 107 | expected_use_prior = True 108 | else: 109 | expected_use_prior = False 110 | 111 | self.assertEqual(expected_use_prior, 112 | test.test_data['use_prior_test']) 113 | 114 | suite = driver.build_tests(self.test_dir, self.loader, 115 | host='localhost', 116 | use_prior_test=False) 117 | for test in suite._tests[0]._tests: 118 | self.assertEqual(False, test.test_data['use_prior_test']) 119 | -------------------------------------------------------------------------------- /gabbi/tests/test_fixtures.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Use mocks to confirm that fixtures operate as context managers. 14 | """ 15 | 16 | import unittest 17 | from unittest import mock 18 | 19 | from gabbi import fixture 20 | 21 | 22 | class FakeFixture(fixture.GabbiFixture): 23 | 24 | def __init__(self, _mock): 25 | super(FakeFixture, self).__init__() 26 | self.mock = _mock 27 | 28 | def start_fixture(self): 29 | self.mock.start() 30 | 31 | def stop_fixture(self): 32 | if self.exc_type: 33 | self.mock.handle(self.exc_type) 34 | self.mock.stop() 35 | 36 | 37 | class FixtureTest(unittest.TestCase): 38 | 39 | def setUp(self): 40 | super(FixtureTest, self).setUp() 41 | self.magic = mock.MagicMock(['start', 'stop', 'handle']) 42 | 43 | def test_fixture_starts_and_stop(self): 44 | with FakeFixture(self.magic): 45 | pass 46 | self.magic.start.assert_called_once_with() 47 | self.magic.stop.assert_called_once_with() 48 | 49 | def test_fixture_informs_on_exception(self): 50 | """Test that the stop fixture is passed exception info.""" 51 | try: 52 | with FakeFixture(self.magic): 53 | raise ValueError() 54 | except ValueError: 55 | pass 56 | self.magic.start.assert_called_once_with() 57 | self.magic.stop.assert_called_once_with() 58 | self.magic.handle.assert_called_once_with(ValueError) 59 | -------------------------------------------------------------------------------- /gabbi/tests/test_gabbits/sample.yaml: -------------------------------------------------------------------------------- 1 | defaults: 2 | use_prior_test: True 3 | 4 | tests: 5 | - name: one 6 | url: / 7 | - name: two 8 | url: http://example.com/moo 9 | - name: use_prior_false 10 | url: http://example.com/foo 11 | use_prior_test: False 12 | -------------------------------------------------------------------------------- /gabbi/tests/test_inner_fixture.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Test the works of inner and outer fixtures. 14 | 15 | An "outer" fixture runs once per test suite. An "inner" is per test request. 16 | """ 17 | 18 | import os 19 | import sys 20 | 21 | from gabbi import driver 22 | # TODO(cdent): test_pytest allows pytest to see the tests this module 23 | # produces. Without it, the generator will not run. It is a todo because 24 | # needing to do this is annoying and gross. 25 | from gabbi.driver import test_pytest # noqa 26 | from gabbi import fixture 27 | from gabbi.tests import simple_wsgi 28 | 29 | 30 | TESTS_DIR = 'gabbits_inner' 31 | COUNT_INNER = 0 32 | COUNT_OUTER = 0 33 | 34 | 35 | class OuterFixture(fixture.GabbiFixture): 36 | """Assert an outer fixture is only started once and is stopped.""" 37 | 38 | def start_fixture(self): 39 | global COUNT_OUTER 40 | COUNT_OUTER += 1 41 | 42 | def stop_fixture(self): 43 | assert COUNT_OUTER == 1 44 | 45 | 46 | class InnerFixture: 47 | """Test that setUp is called 3 times.""" 48 | 49 | def setUp(self): 50 | global COUNT_INNER 51 | COUNT_INNER += 1 52 | 53 | def cleanUp(self): 54 | assert 1 <= COUNT_INNER <= 3 55 | 56 | 57 | BUILD_TEST_ARGS = dict( 58 | intercept=simple_wsgi.SimpleWsgi, 59 | fixture_module=sys.modules[__name__], 60 | inner_fixtures=[InnerFixture], 61 | ) 62 | 63 | 64 | def load_tests(loader, tests, pattern): 65 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 66 | return driver.build_tests(test_dir, loader, 67 | test_loader_name=__name__, 68 | **BUILD_TEST_ARGS) 69 | 70 | 71 | def pytest_generate_tests(metafunc): 72 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 73 | driver.py_test_generator(test_dir, metafunc=metafunc, 74 | test_loader_name=__name__, 75 | **BUILD_TEST_ARGS) 76 | -------------------------------------------------------------------------------- /gabbi/tests/test_intercept.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | 14 | """A sample test module to exercise the code. 15 | 16 | For the sake of exploratory development. 17 | """ 18 | 19 | import os 20 | import sys 21 | 22 | from gabbi import driver 23 | # TODO(cdent): test_pytest allows pytest to see the tests this module 24 | # produces. Without it, the generator will not run. It is a todo because 25 | # needing to do this is annoying and gross. 26 | from gabbi.driver import test_pytest # noqa 27 | from gabbi import fixture 28 | from gabbi.handlers import base 29 | from gabbi.tests import simple_wsgi 30 | from gabbi.tests import util 31 | 32 | 33 | TESTS_DIR = 'gabbits_intercept' 34 | 35 | 36 | class FixtureOne(fixture.GabbiFixture): 37 | """Drive the fixture testing weakly.""" 38 | pass 39 | 40 | 41 | class FixtureTwo(fixture.GabbiFixture): 42 | """Drive the fixture testing weakly.""" 43 | pass 44 | 45 | 46 | class EnvironFixture(fixture.GabbiFixture): 47 | """Set some stuff in the environment.""" 48 | # In the shell environment, environment variables are 49 | # always strings, so make that explicit here. 50 | os.environ['INT'] = "1" 51 | os.environ['FLOAT'] = "1.5" 52 | # For making sure something that looks like a number stays 53 | # a string? 54 | os.environ['STR'] = "2" 55 | os.environ['TBOOL'] = "True" 56 | os.environ['FBOOL'] = "False" 57 | 58 | 59 | class StubResponseHandler(base.ResponseHandler): 60 | """A sample response handler just to test.""" 61 | 62 | test_key_suffix = 'test' 63 | test_key_value = [] 64 | 65 | def preprocess(self, test): 66 | """Add some data if the data is a string.""" 67 | try: 68 | test.output = test.output + '\nAnother line' 69 | except TypeError: 70 | pass 71 | 72 | def action(self, test, item, value=None): 73 | item = item.replace('COW', '', 1) 74 | test.assertIn(item, test.output) 75 | 76 | 77 | # Incorporate the SkipAllFixture into this namespace so it can be used 78 | # by tests (cf. skipall.yaml). 79 | SkipAllFixture = fixture.SkipAllFixture 80 | 81 | 82 | BUILD_TEST_ARGS = dict( 83 | intercept=simple_wsgi.SimpleWsgi, 84 | fixture_module=sys.modules[__name__], 85 | prefix=os.environ.get('GABBI_PREFIX'), 86 | response_handlers=[StubResponseHandler] 87 | ) 88 | 89 | 90 | def load_tests(loader, tests, pattern): 91 | """Provide a TestSuite to the discovery process.""" 92 | # Set and environment variable for one of the tests. 93 | util.set_test_environ() 94 | 95 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 96 | return driver.build_tests(test_dir, loader, 97 | test_loader_name=__name__, 98 | **BUILD_TEST_ARGS) 99 | 100 | 101 | def pytest_generate_tests(metafunc): 102 | util.set_test_environ() 103 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 104 | driver.py_test_generator(test_dir, metafunc=metafunc, 105 | test_loader_name=__name__, 106 | **BUILD_TEST_ARGS) 107 | -------------------------------------------------------------------------------- /gabbi/tests/test_jsonpath.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Test jsonpath handling 14 | """ 15 | 16 | import unittest 17 | 18 | from gabbi.handlers import jsonhandler 19 | 20 | 21 | extract = jsonhandler.JSONHandler.extract_json_path_value 22 | nested_data = { 23 | 'objects': [ 24 | {'name': 'one', 'value': 'alpha'}, 25 | {'name': 'two', 'value': 'beta'}, 26 | ] 27 | } 28 | simple_list = { 29 | 'objects': [ 30 | 'alpha', 31 | 'gamma', 32 | 'gabba', 33 | 'hey', 34 | 'carlton', 35 | ] 36 | } 37 | 38 | 39 | class JSONPathTest(unittest.TestCase): 40 | 41 | def test_basic_match(self): 42 | data = ['hi'] 43 | match = extract(data, '$[0]') 44 | self.assertEqual('hi', match) 45 | 46 | def test_list_handling(self): 47 | data = ['hi', 'bye'] 48 | match = extract(data, '$') 49 | self.assertEqual(data, match) 50 | 51 | def test_embedded_list_handling(self): 52 | match = extract(nested_data, '$.objects..name') 53 | self.assertEqual(['one', 'two'], match) 54 | 55 | def test_sorted_object_list(self): 56 | match = extract(nested_data, r'$.objects[\name][0].value') 57 | self.assertEqual('beta', match) 58 | 59 | def test_filtered_list(self): 60 | match = extract(nested_data, r'$.objects[?name = "one"].value') 61 | self.assertEqual('alpha', match) 62 | 63 | def test_sorted_simple_list(self): 64 | match = extract(simple_list, r'$.objects.`sorted`[-1]') 65 | self.assertEqual('hey', match) 66 | 67 | def test_len_simple_list(self): 68 | match = extract(simple_list, r'$.objects.`len`') 69 | self.assertEqual(5, match) 70 | 71 | def test_len_object_list(self): 72 | match = extract(nested_data, '$.objects.`len`') 73 | self.assertEqual(2, match) 74 | -------------------------------------------------------------------------------- /gabbi/tests/test_live.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | 14 | """A test module to exercise the live requests functionality""" 15 | 16 | 17 | import os 18 | import sys 19 | from unittest import case 20 | 21 | from gabbi import driver 22 | # TODO(cdent): test_pytest allows pytest to see the tests this module 23 | # produces. Without it, the generator will not run. It is a todo because 24 | # needing to do this is annoying and gross. 25 | from gabbi.driver import test_pytest # noqa 26 | from gabbi import fixture 27 | 28 | 29 | TESTS_DIR = 'gabbits_live' 30 | 31 | 32 | class LiveSkipFixture(fixture.GabbiFixture): 33 | """Skip a test file when we don't want to use the internet.""" 34 | 35 | def start_fixture(self): 36 | if os.environ.get('GABBI_SKIP_NETWORK', 'False').lower() == 'true': 37 | raise case.SkipTest('live tests skipped') 38 | 39 | 40 | BUILD_TEST_ARGS = dict( 41 | host='google.com', 42 | fixture_module=sys.modules[__name__], 43 | port=443 44 | ) 45 | 46 | 47 | def load_tests(loader, tests, pattern): 48 | """Provide a TestSuite to the discovery process.""" 49 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 50 | return driver.build_tests( 51 | test_dir, loader, test_loader_name=__name__, **BUILD_TEST_ARGS) 52 | 53 | 54 | def pytest_generate_tests(metafunc): 55 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 56 | driver.py_test_generator(test_dir, metafunc=metafunc, 57 | test_loader_name=__name__, 58 | **BUILD_TEST_ARGS) 59 | -------------------------------------------------------------------------------- /gabbi/tests/test_load_data_file.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Test loading data from files with <@. 14 | """ 15 | 16 | import unittest 17 | from unittest import mock 18 | 19 | from gabbi import case 20 | 21 | 22 | @mock.patch( 23 | 'gabbi.case.open', 24 | new_callable=mock.mock_open, 25 | read_data='dummy content', 26 | create=True, 27 | ) 28 | class DataFileTest(unittest.TestCase): 29 | """Test reading files in tests. 30 | 31 | Reading from local files is only allowed at or below the 32 | test_directory level. 33 | """ 34 | 35 | def setUp(self): 36 | self.http_case = case.HTTPTestCase('test_request') 37 | 38 | def _assert_content_read(self, filepath): 39 | self.assertEqual( 40 | 'dummy content', self.http_case.load_data_file(filepath)) 41 | 42 | def test_load_file(self, m_open): 43 | self.http_case.test_directory = '.' 44 | self._assert_content_read('data.json') 45 | m_open.assert_called_with('./data.json', mode='rb') 46 | 47 | def test_load_file_in_directory(self, m_open): 48 | self.http_case.test_directory = '.' 49 | self._assert_content_read('a/b/c/data.json') 50 | m_open.assert_called_with('./a/b/c/data.json', mode='rb') 51 | 52 | def test_load_file_in_root(self, m_open): 53 | self.http_case.test_directory = '.' 54 | filepath = '/top-level.private' 55 | 56 | with self.assertRaises(ValueError): 57 | self.http_case.load_data_file(filepath) 58 | self.assertFalse(m_open.called) 59 | 60 | def test_load_file_in_parent_dir(self, m_open): 61 | self.http_case.test_directory = '.' 62 | filepath = '../file-in-parent-dir.txt' 63 | 64 | with self.assertRaises(ValueError): 65 | self.http_case.load_data_file(filepath) 66 | self.assertFalse(m_open.called) 67 | 68 | def test_load_file_within_test_directory(self, m_open): 69 | self.http_case.test_directory = '/a/b/c' 70 | self._assert_content_read('../../b/c/file-in-test-dir.txt') 71 | m_open.assert_called_with( 72 | '/a/b/c/../../b/c/file-in-test-dir.txt', mode='rb') 73 | 74 | def test_load_file_not_within_test_directory(self, m_open): 75 | self.http_case.test_directory = '/a/b/c' 76 | filepath = '../../b/E/file-in-test-dir.txt' 77 | with self.assertRaises(ValueError): 78 | self.http_case.load_data_file(filepath) 79 | self.assertFalse(m_open.called) 80 | -------------------------------------------------------------------------------- /gabbi/tests/test_parse_url.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """A place to put tests of URL parsing. 14 | 15 | These verbosely cover the _parse_url method to make sure it 16 | behaves. 17 | """ 18 | 19 | from collections import OrderedDict 20 | import copy 21 | import unittest 22 | import uuid 23 | 24 | from gabbi import case 25 | 26 | 27 | class UrlParseTest(unittest.TestCase): 28 | 29 | @staticmethod 30 | def make_test_case(host, port=8000, prefix='', ssl=False, params=None): 31 | # Attributes used are port, prefix and host and they must 32 | # be set manually here, due to metaclass magics elsewhere. 33 | # test_data must have a base value. 34 | http_case = case.HTTPTestCase('test_request') 35 | http_case.test_data = copy.copy(case.BASE_TEST) 36 | http_case.host = host 37 | http_case.port = port 38 | http_case.prefix = prefix 39 | http_case.test_data['ssl'] = ssl 40 | http_case.test_data['query_parameters'] = params or {} 41 | return http_case 42 | 43 | def test_parse_url(self): 44 | host = uuid.uuid4().hex 45 | http_case = self.make_test_case(host) 46 | parsed_url = http_case._parse_url('/foobar') 47 | 48 | self.assertEqual('http://%s:8000/foobar' % host, parsed_url) 49 | 50 | def test_parse_prefix(self): 51 | host = uuid.uuid4().hex 52 | http_case = self.make_test_case(host, prefix='/noise') 53 | parsed_url = http_case._parse_url('/foobar') 54 | 55 | self.assertEqual('http://%s:8000/noise/foobar' % host, parsed_url) 56 | 57 | def test_parse_full(self): 58 | host = uuid.uuid4().hex 59 | http_case = self.make_test_case(host) 60 | parsed_url = http_case._parse_url('http://example.com/house') 61 | 62 | self.assertEqual('http://example.com/house', parsed_url) 63 | 64 | def test_with_ssl(self): 65 | host = uuid.uuid4().hex 66 | http_case = self.make_test_case(host, ssl=True) 67 | parsed_url = http_case._parse_url('/foobar') 68 | 69 | self.assertEqual('https://%s:8000/foobar' % host, parsed_url) 70 | 71 | def test_default_port_http(self): 72 | host = uuid.uuid4().hex 73 | http_case = self.make_test_case(host, port='80') 74 | parsed_url = http_case._parse_url('/foobar') 75 | 76 | self.assertEqual('http://%s/foobar' % host, parsed_url) 77 | 78 | def test_default_port_int(self): 79 | host = uuid.uuid4().hex 80 | http_case = self.make_test_case(host, port=80) 81 | parsed_url = http_case._parse_url('/foobar') 82 | 83 | self.assertEqual('http://%s/foobar' % host, parsed_url) 84 | 85 | def test_default_port_https(self): 86 | host = uuid.uuid4().hex 87 | http_case = self.make_test_case(host, port='443', ssl=True) 88 | parsed_url = http_case._parse_url('/foobar') 89 | 90 | self.assertEqual('https://%s/foobar' % host, parsed_url) 91 | 92 | def test_default_port_https_no_ssl(self): 93 | host = uuid.uuid4().hex 94 | http_case = self.make_test_case(host, port='443') 95 | parsed_url = http_case._parse_url('/foobar') 96 | 97 | self.assertEqual('http://%s:443/foobar' % host, parsed_url) 98 | 99 | def test_https_port_80_ssl(self): 100 | host = uuid.uuid4().hex 101 | http_case = self.make_test_case(host, port='80', ssl=True) 102 | parsed_url = http_case._parse_url('/foobar') 103 | 104 | self.assertEqual('https://%s:80/foobar' % host, parsed_url) 105 | 106 | def test_ipv6_url(self): 107 | host = '::1' 108 | http_case = self.make_test_case(host, port='80', ssl=True) 109 | parsed_url = http_case._parse_url('/foobar') 110 | 111 | self.assertEqual('https://[%s]:80/foobar' % host, parsed_url) 112 | 113 | def test_ipv6_full_url(self): 114 | host = '::1' 115 | http_case = self.make_test_case(host, port='80', ssl=True) 116 | parsed_url = http_case._parse_url( 117 | 'http://[2001:4860:4860::8888]/foobar') 118 | 119 | self.assertEqual('http://[2001:4860:4860::8888]/foobar', parsed_url) 120 | 121 | def test_ipv6_no_double_colon_wacky_ssl(self): 122 | host = 'FEDC:BA98:7654:3210:FEDC:BA98:7654:3210' 123 | http_case = self.make_test_case(host, port='80', ssl=True) 124 | parsed_url = http_case._parse_url('/foobar') 125 | 126 | self.assertEqual( 127 | 'https://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/foobar', 128 | parsed_url) 129 | 130 | http_case = self.make_test_case(host, ssl=True) 131 | parsed_url = http_case._parse_url('/foobar') 132 | 133 | self.assertEqual( 134 | 'https://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:8000/foobar', 135 | parsed_url) 136 | 137 | def test_add_query_params(self): 138 | host = uuid.uuid4().hex 139 | # Use a sequence of tuples to ensure order. 140 | query = OrderedDict([('x', 1), ('y', 2)]) 141 | http_case = self.make_test_case(host, params=query) 142 | parsed_url = http_case._parse_url('/foobar') 143 | 144 | self.assertEqual('http://%s:8000/foobar?x=1&y=2' % host, parsed_url) 145 | 146 | def test_extend_query_params(self): 147 | host = uuid.uuid4().hex 148 | # Use a sequence of tuples to ensure order. 149 | query = OrderedDict([('x', 1), ('y', 2)]) 150 | http_case = self.make_test_case(host, params=query) 151 | parsed_url = http_case._parse_url('/foobar?alpha=beta') 152 | 153 | self.assertEqual('http://%s:8000/foobar?alpha=beta&x=1&y=2' 154 | % host, parsed_url) 155 | 156 | def test_extend_query_params_full_url(self): 157 | host = 'stub' 158 | query = OrderedDict([('x', 1), ('y', 2)]) 159 | http_case = self.make_test_case(host, params=query) 160 | parsed_url = http_case._parse_url( 161 | 'http://example.com/foobar?alpha=beta') 162 | 163 | self.assertEqual('http://example.com/foobar?alpha=beta&x=1&y=2', 164 | parsed_url) 165 | -------------------------------------------------------------------------------- /gabbi/tests/test_replacers.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """A place to put test of the replacers. 14 | """ 15 | 16 | import os 17 | 18 | import unittest 19 | 20 | from gabbi import case 21 | from gabbi import exception 22 | 23 | 24 | class EnvironReplaceTest(unittest.TestCase): 25 | 26 | def test_environ_boolean(self): 27 | """Environment variables are always strings 28 | 29 | That doesn't always suit our purposes, so test that "True" 30 | and "False" become booleans as a special case. 31 | """ 32 | http_case = case.HTTPTestCase('test_request') 33 | message = "$ENVIRON['moo']" 34 | 35 | os.environ['moo'] = "True" 36 | self.assertEqual(True, http_case._environ_replace(message)) 37 | 38 | os.environ['moo'] = "False" 39 | self.assertEqual(False, http_case._environ_replace(message)) 40 | 41 | os.environ['moo'] = "true" 42 | self.assertEqual(True, http_case._environ_replace(message)) 43 | 44 | os.environ['moo'] = "faLse" 45 | self.assertEqual(False, http_case._environ_replace(message)) 46 | 47 | os.environ['moo'] = "null" 48 | self.assertEqual(None, http_case._environ_replace(message)) 49 | 50 | os.environ['moo'] = "1" 51 | self.assertEqual(1, http_case._environ_replace(message)) 52 | 53 | os.environ['moo'] = "cow" 54 | self.assertEqual("cow", http_case._environ_replace(message)) 55 | 56 | message = '$ENVIRON["moo"]' 57 | 58 | os.environ['moo'] = "True" 59 | self.assertEqual(True, http_case._environ_replace(message)) 60 | 61 | 62 | class TestReplaceHeaders(unittest.TestCase): 63 | 64 | def test_empty_headers(self): 65 | """A None value in headers should cause a GabbiFormatError.""" 66 | http_case = case.HTTPTestCase('test_request') 67 | self.assertRaises( 68 | exception.GabbiFormatError, 69 | http_case._replace_headers_template, 'foo', None) 70 | -------------------------------------------------------------------------------- /gabbi/tests/test_suite.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Unit tests for the gabbi.suite. 14 | """ 15 | 16 | import sys 17 | import unittest 18 | 19 | from gabbi import fixture 20 | from gabbi import suitemaker 21 | 22 | VALUE_ERROR = 'value error sentinel' 23 | FIXTURE_METHOD = 'start_fixture' 24 | 25 | 26 | class FakeFixture(fixture.GabbiFixture): 27 | 28 | def start_fixture(self): 29 | raise ValueError(VALUE_ERROR) 30 | 31 | 32 | class SuiteTest(unittest.TestCase): 33 | 34 | def test_suite_catches_fixture_fail(self): 35 | """Verify fixture failure. 36 | 37 | When a fixture fails in start_fixture it should fail 38 | the first test in the suite and skip the others. 39 | """ 40 | loader = unittest.defaultTestLoader 41 | result = unittest.TestResult() 42 | test_data = {'fixtures': ['FakeFixture'], 43 | 'tests': [{'name': 'alpha', 'GET': '/'}, 44 | {'name': 'beta', 'GET': '/'}]} 45 | test_suite = suitemaker.test_suite_from_dict( 46 | loader, 'foo', test_data, '.', 'localhost', 47 | 80, sys.modules[__name__], None) 48 | 49 | test_suite.run(result) 50 | 51 | self.assertEqual(2, len(result.skipped)) 52 | self.assertEqual(1, len(result.errors)) 53 | 54 | errored_test, trace = result.errors[0] 55 | 56 | self.assertIn('foo_alpha', str(errored_test)) 57 | self.assertIn(VALUE_ERROR, trace) 58 | self.assertIn(FIXTURE_METHOD, trace) 59 | -------------------------------------------------------------------------------- /gabbi/tests/test_syntax_warning.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Test that the driver warns on bad yaml name.""" 14 | 15 | import os 16 | import unittest 17 | import warnings 18 | 19 | from gabbi import driver 20 | from gabbi import exception 21 | 22 | 23 | TESTS_DIR = 'warning_gabbits' 24 | 25 | 26 | class DriverTest(unittest.TestCase): 27 | 28 | def setUp(self): 29 | super(DriverTest, self).setUp() 30 | self.loader = unittest.defaultTestLoader 31 | self.test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 32 | 33 | def test_driver_warnings_on_files(self): 34 | with warnings.catch_warnings(record=True) as the_warnings: 35 | driver.build_tests( 36 | self.test_dir, self.loader, host='localhost', port=8001) 37 | self.assertEqual(1, len(the_warnings)) 38 | the_warning = the_warnings[-1] 39 | self.assertEqual( 40 | the_warning.category, exception.GabbiSyntaxWarning) 41 | self.assertIn("'_' in test filename", str(the_warning.message)) 42 | -------------------------------------------------------------------------------- /gabbi/tests/test_unsafe_yaml.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | 14 | """A sample test module to exercise the code. 15 | 16 | For the sake of exploratory development. 17 | """ 18 | 19 | import os 20 | 21 | import yaml 22 | 23 | from gabbi import driver 24 | # TODO(cdent): test_pytest allows pytest to see the tests this module 25 | # produces. Without it, the generator will not run. It is a todo because 26 | # needing to do this is annoying and gross. 27 | from gabbi.driver import test_pytest # noqa 28 | from gabbi.tests import simple_wsgi 29 | from gabbi.tests import util 30 | 31 | 32 | TESTS_DIR = 'gabbits_unsafe_yaml' 33 | 34 | 35 | yaml.add_constructor(u'!IsNAN', lambda loader, node: util.NanChecker()) 36 | 37 | 38 | BUILD_TEST_ARGS = dict( 39 | intercept=simple_wsgi.SimpleWsgi, 40 | safe_yaml=False 41 | ) 42 | 43 | 44 | def load_tests(loader, tests, pattern): 45 | """Provide a TestSuite to the discovery process.""" 46 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 47 | return driver.build_tests(test_dir, loader, 48 | test_loader_name=__name__, 49 | **BUILD_TEST_ARGS) 50 | 51 | 52 | def pytest_generate_tests(metafunc): 53 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 54 | driver.py_test_generator(test_dir, metafunc=metafunc, 55 | test_loader_name=__name__, 56 | **BUILD_TEST_ARGS) 57 | -------------------------------------------------------------------------------- /gabbi/tests/test_use_prior_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Test use_prior_test directive. 14 | """ 15 | 16 | import copy 17 | import unittest 18 | from unittest import mock 19 | 20 | from gabbi import case 21 | 22 | 23 | class UsePriorTest(unittest.TestCase): 24 | 25 | @staticmethod 26 | def make_test_case(use_prior_test=None): 27 | http_case = case.HTTPTestCase('test_request') 28 | http_case.test_data = copy.copy(case.BASE_TEST) 29 | if use_prior_test is not None: 30 | http_case.test_data['use_prior_test'] = use_prior_test 31 | return http_case 32 | 33 | @mock.patch('gabbi.case.HTTPTestCase._run_test') 34 | def test_use_prior_true(self, m_run_test): 35 | http_case = self.make_test_case(True) 36 | http_case.has_run = False 37 | http_case.prior = self.make_test_case(True) 38 | http_case.prior.run = mock.MagicMock(unsafe=True) 39 | http_case.prior.has_run = False 40 | 41 | http_case.test_request() 42 | http_case.prior.run.assert_called_once() 43 | 44 | @mock.patch('gabbi.case.HTTPTestCase._run_test') 45 | def test_use_prior_false(self, m_run_test): 46 | http_case = self.make_test_case(False) 47 | http_case.has_run = False 48 | http_case.prior = self.make_test_case(True) 49 | http_case.prior.run = mock.MagicMock(unsafe=True) 50 | http_case.prior.has_run = False 51 | 52 | http_case.test_request() 53 | http_case.prior.run.assert_not_called() 54 | 55 | @mock.patch('gabbi.case.HTTPTestCase._run_test') 56 | def test_use_prior_default_true(self, m_run_test): 57 | http_case = self.make_test_case() 58 | http_case.has_run = False 59 | http_case.prior = self.make_test_case(True) 60 | http_case.prior.run = mock.MagicMock(unsafe=True) 61 | http_case.prior.has_run = False 62 | 63 | http_case.test_request() 64 | http_case.prior.run.assert_called_once() 65 | -------------------------------------------------------------------------------- /gabbi/tests/test_yaml_disk_loading_jsonhandler.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | 14 | """A sample test module to exercise the code. 15 | 16 | For the sake of exploratory development. 17 | """ 18 | 19 | import os 20 | 21 | from gabbi import driver 22 | # TODO(cdent): test_pytest allows pytest to see the tests this module 23 | # produces. Without it, the generator will not run. It is a todo because 24 | # needing to do this is annoying and gross. 25 | from gabbi.driver import test_pytest # noqa 26 | from gabbi.handlers import yaml_disk_loading_jsonhandler 27 | from gabbi.tests import simple_wsgi 28 | 29 | 30 | TESTS_DIR = 'gabbits_handlers' 31 | 32 | BUILD_TEST_ARGS = dict( 33 | intercept=simple_wsgi.SimpleWsgi, 34 | content_handlers=[yaml_disk_loading_jsonhandler.YAMLDiskLoadingJSONHandler] 35 | ) 36 | 37 | 38 | def load_tests(loader, tests, pattern): 39 | """Provide a TestSuite to the discovery process.""" 40 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 41 | return driver.build_tests(test_dir, loader, 42 | test_loader_name=__name__, 43 | **BUILD_TEST_ARGS) 44 | 45 | 46 | def pytest_generate_tests(metafunc): 47 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 48 | driver.py_test_generator(test_dir, metafunc=metafunc, 49 | test_loader_name=__name__, 50 | **BUILD_TEST_ARGS) 51 | -------------------------------------------------------------------------------- /gabbi/tests/util.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Utility methods shared by some tests.""" 14 | 15 | import math 16 | import os 17 | 18 | import yaml 19 | 20 | 21 | def set_test_environ(): 22 | """Set some environment variables used in tests.""" 23 | os.environ['GABBI_TEST_URL'] = 'takingnames' 24 | 25 | # Setup environment variables for `coerce.yaml` 26 | os.environ['ONE'] = '1' 27 | os.environ['DECIMAL'] = '1.0' 28 | os.environ['ARRAY_STRING'] = '[1,2,3]' 29 | os.environ['TRUE'] = 'true' 30 | os.environ['FALSE'] = 'false' 31 | os.environ['STRING'] = 'val' 32 | os.environ['NULL'] = 'null' 33 | 34 | 35 | class NanChecker(yaml.YAMLObject): 36 | yaml_tag = u'!NanChecker' 37 | 38 | def __eq__(self, other): 39 | try: 40 | return math.isnan(other) 41 | except ValueError: 42 | return False 43 | -------------------------------------------------------------------------------- /gabbi/tests/warning_gabbits/underscore_sample.yaml: -------------------------------------------------------------------------------- 1 | 2 | tests: 3 | - name: one 4 | url: / 5 | - name: two 6 | url: http://example.com/moo 7 | -------------------------------------------------------------------------------- /gabbi/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | """Utility functions grab bag.""" 14 | 15 | import io 16 | import os 17 | import urllib.parse as urlparse 18 | 19 | import colorama 20 | import yaml 21 | 22 | ConnectionRefused = ConnectionRefusedError 23 | 24 | 25 | def create_url(base_url, host, port=None, prefix='', ssl=False): 26 | """Given pieces of a path-based url, return a fully qualified url.""" 27 | scheme = 'http' 28 | 29 | # A host with : in it at this stage is assumed to be an IPv6 30 | # address of some kind (they come in many forms). Port should 31 | # already have been stripped off. 32 | if ':' in host and not (host.startswith('[') and host.endswith(']')): 33 | host = '[%s]' % host 34 | 35 | if port and not _port_follows_standard(port, ssl): 36 | netloc = '%s:%s' % (host, port) 37 | else: 38 | netloc = host 39 | 40 | if ssl: 41 | scheme = 'https' 42 | 43 | parsed_url = urlparse.urlsplit(base_url) 44 | query_string = parsed_url.query 45 | path = parsed_url.path 46 | 47 | # Guard against a prefix of None or the url already having the 48 | # prefix. Without the startswith check, the tests in prefix.yaml 49 | # fail. This is a pragmatic fix which does this for any URL in a 50 | # test request that does not have a scheme and does not 51 | # distinguish between URLs in a gabbi test file and those 52 | # generated by the server. Idealy we would not mutate nor need 53 | # to check URLs returned from the server. Doing that, however, 54 | # would require more complex data handling than we have now and 55 | # this covers most common cases and will be okay until someone 56 | # reports a bug. 57 | if prefix and not path.startswith(prefix): 58 | prefix = prefix.rstrip('/') 59 | path = path.lstrip('/') 60 | path = '%s/%s' % (prefix, path) 61 | 62 | return urlparse.urlunsplit((scheme, netloc, path, query_string, '')) 63 | 64 | 65 | def decode_response_content(header_dict, content): 66 | """Decode content to a proper string.""" 67 | content_type, charset = extract_content_type(header_dict) 68 | 69 | if not_binary(content_type) and isinstance(content, bytes): 70 | return content.decode(charset) 71 | else: 72 | return content 73 | 74 | 75 | def extract_content_type(header_dict, default='application/binary'): 76 | """Extract parsed content-type from headers.""" 77 | content_type = header_dict.get('content-type', 78 | default).strip().lower() 79 | return parse_content_type(content_type) 80 | 81 | 82 | def get_colorizer(stream): 83 | """Return a function to colorize a string. 84 | 85 | Only if stream is a tty . 86 | """ 87 | if stream.isatty() or os.environ.get('GABBI_FORCE_COLOR', False): 88 | colorama.init() 89 | return _colorize 90 | else: 91 | return lambda x, y: y 92 | 93 | 94 | def load_yaml(handle=None, yaml_file=None, safe=True): 95 | """Read and parse any YAML file or filehandle. 96 | 97 | Let exceptions flow where they may. 98 | 99 | If no file or handle is provided, read from STDIN. 100 | """ 101 | loader = yaml.SafeLoader if safe else yaml.Loader 102 | 103 | if yaml_file: 104 | with io.open(yaml_file, encoding='utf-8') as source: 105 | return yaml.load(source.read(), Loader=loader) 106 | 107 | # This will intentionally raise AttributeError if handle is None. 108 | return yaml.load(handle.read(), Loader=loader) 109 | 110 | 111 | def not_binary(content_type): 112 | """Decide if something is content we'd like to treat as a string.""" 113 | return (content_type.startswith('text/') or 114 | content_type.endswith('+xml') or 115 | content_type.endswith('+json') or 116 | content_type == 'application/javascript' or 117 | content_type.startswith('application/json') or 118 | content_type.startswith('application/xml')) 119 | 120 | 121 | def parse_content_type(content_type, default_charset='utf-8'): 122 | """Parse content type value for media type and charset.""" 123 | charset = default_charset 124 | if ';' in content_type: 125 | content_type, parameter_strings = (attr.strip() for attr 126 | in content_type.split(';', 1)) 127 | try: 128 | parameter_pairs = [atom.strip().split('=') 129 | for atom in parameter_strings.split(';')] 130 | parameters = {name: value for name, value in parameter_pairs} 131 | charset = parameters['charset'] 132 | except (ValueError, KeyError): 133 | # KeyError when no charset found. 134 | # ValueError when the parameter_strings are poorly 135 | # formed (for example trailing ;) 136 | pass 137 | 138 | return (content_type, charset) 139 | 140 | 141 | def host_info_from_target(target, prefix=None): 142 | """Turn url or host:port and target into test destination.""" 143 | force_ssl = False 144 | # If we have a bare host prefix it with a scheme. 145 | if '//' not in target and not target.startswith('http'): 146 | target = 'http://' + target 147 | if prefix: 148 | target = target + prefix 149 | split_url = urlparse.urlparse(target) 150 | 151 | if split_url.scheme == 'https': 152 | force_ssl = True 153 | return split_url.hostname, split_url.port, split_url.path, force_ssl 154 | 155 | 156 | def _colorize(color, message): 157 | """Add a color to the message.""" 158 | try: 159 | return getattr(colorama.Fore, color) + message + colorama.Fore.RESET 160 | except AttributeError: 161 | return message 162 | 163 | 164 | def _port_follows_standard(port, ssl): 165 | """Return True if a standard port is using a non-standard ssl setting.""" 166 | port = int(port) 167 | return (port == 443 and ssl) or (port == 80 and not ssl) 168 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | tox 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pbr 2 | pytest 3 | PyYAML 4 | httpx[http2] 5 | certifi 6 | jsonpath-rw-ext>=1.0.0 7 | colorama 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = gabbi 3 | author = Chris Dent 4 | author_email = cdent@anticdent.org 5 | summary = Declarative HTTP testing library 6 | description_file = README.rst 7 | license = Apache-2 8 | home_page = https://github.com/cdent/gabbi 9 | python_requires = >=3.9 10 | classifier = 11 | Intended Audience :: Developers 12 | Intended Audience :: Information Technology 13 | Environment :: Web Environment 14 | License :: OSI Approved :: Apache Software License 15 | Operating System :: POSIX 16 | Programming Language :: Python 17 | Programming Language :: Python :: 3 18 | Programming Language :: Python :: 3.9 19 | Programming Language :: Python :: 3.10 20 | Programming Language :: Python :: 3.11 21 | Programming Language :: Python :: 3.12 22 | Programming Language :: Python :: 3.13 23 | Programming Language :: Python :: 3 :: Only 24 | Topic :: Internet :: WWW/HTTP :: WSGI 25 | Topic :: Software Development :: Testing 26 | 27 | [files] 28 | packages = 29 | gabbi 30 | 31 | [build_sphinx] 32 | all_files = 1 33 | build-dir = docs/build 34 | source-dir = docs/source 35 | 36 | [entry_points] 37 | console_scripts = 38 | gabbi-run = gabbi.runner:run 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import setuptools 17 | 18 | 19 | setuptools.setup( 20 | setup_requires=['pbr'], 21 | pbr=True) 22 | -------------------------------------------------------------------------------- /test-failskip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | # Run the tests and confirm that the stuff we expect to skip or fail 3 | # does. 4 | 5 | # this would be somewhat less complex in bash4.. 6 | shopt -s nocasematch 7 | [[ "${GABBI_SKIP_NETWORK:-false}" == "true" ]] && SKIP=14 || SKIP=2 8 | [[ "${GABBI_SKIP_NETWORK:-false}" == "true" ]] && FAILS=15 || FAILS=16 9 | shopt -u nocasematch 10 | 11 | GREP_FAIL_MATCH="expected failures=$FAILS" 12 | GREP_SKIP_MATCH="skipped=$SKIP," 13 | PYTEST_MATCH="$SKIP skipped, $FAILS xfailed" 14 | 15 | stestr run && \ 16 | for match in "${GREP_FAIL_MATCH}" "${GREP_SKIP_MATCH}"; do 17 | stestr last --subunit | subunit2pyunit 2>&1 | \ 18 | grep "${match}" 19 | done 20 | 21 | # Make sure pytest failskips too 22 | py.test gabbi | grep "$PYTEST_MATCH" 23 | -------------------------------------------------------------------------------- /test-limit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run a test which is limited to just one request from a file that 3 | # contains many requests and confirm that only one was run and that 4 | # it did actually run. 5 | # 6 | # This covers a situation where the change of intercepts to fixtures 7 | # broke limiting tests and we never knew. 8 | 9 | GREP_TEST_MATCH='tests.test_intercept.self_checklimit.test_request ... ok' 10 | GREP_COUNT_MATCH='Ran: 1 ' 11 | 12 | stestr run "checklimit" && \ 13 | stestr last --subunit | subunit2pyunit 2>&1 | \ 14 | grep "${GREP_TEST_MATCH}" && \ 15 | stestr last | grep "${GREP_COUNT_MATCH}" 16 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | mock ; python_version < '3.3' 2 | stestr>4.0.0 3 | coverage 4 | pytest-cov 5 | hacking 6 | sphinx 7 | -------------------------------------------------------------------------------- /test-verbosity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run a test that confirms that a verbose test will output a response body 3 | # when there is no content-type. 4 | 5 | stestr run "intercept.verbosity" | grep '^notempty' >/dev/null 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.1.1 3 | skipsdist = True 4 | envlist = pep8,py39,py310,py311,py312,py313,pypy3,pep8,limit,failskip,docs,py313-prefix,py313-limit,py313-verbosity,py313-failskip,py36-pytest,py39-pytest,py310-pytest,py311-pytest,py312-pytest,py313-pytest 5 | 6 | [testenv] 7 | deps = -r{toxinidir}/requirements.txt 8 | -r{toxinidir}/test-requirements.txt 9 | allowlist_externals = rm 10 | usedevelop = True 11 | install_command = pip install --no-cache -U {opts} {packages} 12 | commands = 13 | stestr run {posargs} 14 | setenv = GABBI_PREFIX= 15 | passenv = GABBI_*, HOME 16 | 17 | [testenv:venv] 18 | deps = -r{toxinidir}/requirements.txt 19 | -r{toxinidir}/test-requirements.txt 20 | commands = {posargs} 21 | 22 | [testenv:py36-pytest] 23 | commands = py.test gabbi 24 | 25 | [testenv:py39-pytest] 26 | commands = py.test gabbi 27 | 28 | [testenv:py310-pytest] 29 | commands = py.test gabbi 30 | 31 | [testenv:py311-pytest] 32 | commands = py.test gabbi 33 | 34 | [testenv:py312-pytest] 35 | commands = py.test gabbi 36 | 37 | [testenv:py313-pytest] 38 | commands = py.test {posargs} gabbi 39 | 40 | [testenv:py313-prefix] 41 | setenv = GABBI_PREFIX=/snoopy 42 | 43 | [testenv:pep8] 44 | basepython = python3 45 | deps = hacking 46 | commands = 47 | flake8 48 | 49 | [testenv:py313-limit] 50 | allowlist_externals = {toxinidir}/test-limit.sh 51 | commands = {toxinidir}/test-limit.sh 52 | 53 | [testenv:py313-verbosity] 54 | allowlist_externals = {toxinidir}/test-verbosity.sh 55 | commands = {toxinidir}/test-verbosity.sh 56 | 57 | [testenv:py313-failskip] 58 | allowlist_externals = {toxinidir}/test-failskip.sh 59 | commands = {toxinidir}/test-failskip.sh 60 | 61 | # Use pytest when in pypy3 because stestr fails on loading readline. 62 | [testenv:pypy3] 63 | commands = py.test gabbi 64 | 65 | [testenv:cover] 66 | basepython = python3 67 | setenv = 68 | {[testenv]setenv} 69 | PYTHON=coverage run --source gabbi --parallel-mode 70 | commands = 71 | coverage erase 72 | find . -type f -name "*.pyc" -delete 73 | stestr run {posargs} 74 | coverage combine 75 | coverage html -d cover 76 | coverage xml -o cover/coverage.xml 77 | coverage report 78 | 79 | [testenv:pytest-cov] 80 | basepython = python3 81 | commands = py.test --cov=gabbi gabbi/tests --cov-config .coveragerc --cov-report html 82 | 83 | [testenv:docs] 84 | commands = 85 | rm -rf docs/build 86 | sphinx-build docs/source docs/build 87 | deps = sphinx 88 | allowlist_externals = 89 | rm 90 | 91 | [flake8] 92 | exclude=.venv,.git,.tox,dist,*egg,*.egg-info,build,examples,docs 93 | show-source = True 94 | --------------------------------------------------------------------------------