├── .flake8 ├── .github └── workflows │ └── pyenv.yml ├── .gitignore ├── .release ├── COPYING ├── FUNDING.yml ├── MANIFEST.in ├── Makefile ├── README.rst ├── circle.yml ├── development.txt ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ ├── guide-callback-regex-ipdb.py │ ├── logo.svg │ ├── read-timeout.py │ ├── regex-example.py │ └── tmplun_dcms-ascii.cast │ ├── acks.rst │ ├── api.rst │ ├── changelog.rst │ ├── conf.py │ ├── contributing.rst │ ├── guides.rst │ ├── index.rst │ └── introduction.rst ├── httpretty ├── __init__.py ├── compat.py ├── core.py ├── errors.py ├── http.py ├── utils.py └── version.py ├── renovate.json ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── bugfixes │ ├── nosetests │ │ ├── __init__.py │ │ ├── test_242_ssl_bad_handshake.py │ │ ├── test_387_regex_port.py │ │ ├── test_388_unmocked_error_with_url.py │ │ ├── test_413_regex.py │ │ ├── test_414_httpx.py │ │ ├── test_416_boto3.py │ │ ├── test_417_openssl.py │ │ ├── test_425_latest_requests.py │ │ ├── test_430_respect_timeout.py │ │ ├── test_eventlet.py │ │ ├── test_redis.py │ │ └── test_tornado_bind_unused_port.py │ └── pytest │ │ └── test_426_mypy_segfault.py ├── compat.py ├── functional │ ├── __init__.py │ ├── base.py │ ├── fixtures │ │ ├── .placeholder │ │ └── playback-1.json │ ├── test_bypass.py │ ├── test_debug.py │ ├── test_decorator.py │ ├── test_fakesocket.py │ ├── test_httplib2.py │ ├── test_passthrough.py │ ├── test_requests.py │ ├── test_urllib2.py │ └── testserver.py ├── pyopenssl │ ├── __init__.py │ └── test_mock.py └── unit │ ├── __init__.py │ ├── test_core.py │ ├── test_http.py │ ├── test_httpretty.py │ └── test_main.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,E731,F401,F821,E901 3 | max-line-length = 120 4 | exclude=patterns = .git,__pycache__,.tox,.eggs,*.egg 5 | -------------------------------------------------------------------------------- /.github/workflows/pyenv.yml: -------------------------------------------------------------------------------- 1 | name: HTTPretty Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | python: 11 | name: "Python" 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python: 16 | - 3.6.5 17 | - 3.7.3 18 | - 3.8.6 19 | - 3.9.0 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Install python version 24 | uses: gabrielfalcao/pyenv-action@v7 25 | with: 26 | default: "${{ matrix.python }}" 27 | command: make setup 28 | 29 | - name: Unit Tests 30 | run: make unit 31 | 32 | - name: Test Bugfixes 33 | run: make bugfixes 34 | 35 | - name: PyOpenSSL tests 36 | run: make pyopenssl 37 | 38 | - name: Functional Tests 39 | run: make functional 40 | 41 | - name: Upload Test Coverage Report 42 | uses: Atrox/codecov-action@v0.1.3 43 | 44 | - name: Codecov link 45 | run: echo "https://app.codecov.io/gh/gabrielfalcao/httpretty/" 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .coverage 3 | docs/_build/ 4 | httpretty.egg-info/ 5 | build/ 6 | dist/ 7 | .DS_Store 8 | *.swp 9 | .#* 10 | #* 11 | .tox/ 12 | _public/ 13 | tests/functional/fixtures/recording-*.json 14 | #* 15 | *#* 16 | .idea 17 | venv 18 | .eggs 19 | 20 | README.html 21 | Pipfile.lock 22 | .venv/ 23 | .noseids 24 | coverage.xml 25 | -------------------------------------------------------------------------------- /.release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | declare newversion 4 | declare current_version 5 | declare sure 6 | 7 | 8 | current_version="$(grep -E version ./httpretty/version.py | sed 's,version *= *.\(\([0-9]*[.]*\)\{3\,4\}\).,\1,g')" 9 | echo -en "The current version is \033[1;33m$current_version\033[0m, type a new one\n\033[1;32mnew version:\033[0m " 10 | read -r newversion 11 | 12 | 13 | function find_files () { 14 | for name in $(find . -name 'README.rst' -or -name version.py -or -name conf.py | grep -v '\(\.venv\|build\)[/]'); do 15 | echo "${name}" 16 | done 17 | 18 | } 19 | 20 | function update_files (){ 21 | find_files | xargs gsed -i "s,$current_version,$newversion,g" 22 | } 23 | function revert_files (){ 24 | find_files | xargs gsed -i "s,$newversion,$current_version,g" 25 | } 26 | 27 | echo -en "\033[A\033[A\rI will make a new commit named \033[1;33m'New release $newversion'\033[0m\n" 28 | echo -en "Are you sure? [\033[1;32myes\033[0m or \033[1;31mno\033[0m]\n" 29 | read -r sure 30 | 31 | 32 | if [ "${sure}" == "yes" ]; then 33 | echo "updating relevant files with new version..." 34 | if update_files; then 35 | echo "committing and pushing changes..." 36 | echo -en "New release: \033[1;32m$newversion\033[0m\n" 37 | if git add -f $(find_files); then 38 | if git commit $(find_files) -m "New release: $newversion"; then 39 | if git push; then 40 | echo "creating tag ${newversion}..." 41 | if git tag "${newversion}"; then 42 | echo "pushing tag ${newversion}..." 43 | git push --tags 44 | else 45 | echo "failed to create tag ${newversion}" 46 | echo "you might want to revert the last commit and check what happened" 47 | exit 1 48 | fi 49 | else 50 | echo "failed to push, skipping release and reverting changes" 51 | revert_files 52 | exit 1 53 | fi 54 | else 55 | echo "failed to commit, skipping release and reverting changes" 56 | revert_files 57 | exit 1 58 | fi 59 | else 60 | echo "no files to git add, skipping release" 61 | exit 1 62 | fi; 63 | else 64 | echo "no files were updated, skipping release" 65 | exit 1 66 | fi 67 | else 68 | echo "kthankxbye" 69 | fi 70 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (C) <2011-2021> Gabriel Falcão 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://xscode.com/gabrielfalcao/HTTPretty"] 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include COPYING 2 | include Makefile 3 | include README.rst 4 | include requirements.txt 5 | include tests/functional/fixtures/playback-*.json 6 | include tox.ini 7 | recursive-include docs *.* 8 | recursive-include tests *.py 9 | include *.cfg *.rst *.txt -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tests all unit functional clean dependencies tdd docs html purge dist setup 2 | 3 | GIT_ROOT := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 4 | DOCS_ROOT := $(GIT_ROOT)/docs 5 | HTML_ROOT := $(DOCS_ROOT)/build/html 6 | VENV_ROOT := $(GIT_ROOT)/.venv 7 | VENV ?= $(VENV_ROOT) 8 | DOCS_INDEX := $(HTML_ROOT)/index.html 9 | 10 | export VENV 11 | export PYTHONASYNCIODEBUG :=1 12 | 13 | 14 | all: dependencies tests 15 | 16 | $(VENV): # creates $(VENV) folder if does not exist 17 | python3 -mvenv $(VENV) 18 | $(VENV)/bin/pip install -U pip setuptools 19 | 20 | $(VENV)/bin/sphinx-build $(VENV)/bin/twine $(VENV)/bin/nosetests $(VENV)/bin/pytest $(VENV)/bin/python $(VENV)/bin/pip: # installs latest pip 21 | test -e $(VENV)/bin/pip || make $(VENV) 22 | $(MAKE) setup 23 | 24 | setup: | $(VENV)/bin/pip 25 | $(VENV)/bin/pip install -r development.txt 26 | $(VENV)/bin/pip install -e . 27 | 28 | # Runs the unit and functional tests 29 | tests: unit bugfixes functional pyopenssl 30 | 31 | 32 | tdd: $(VENV)/bin/nosetests # runs all tests 33 | $(VENV)/bin/nosetests tests --with-watch --cover-erase 34 | 35 | # Install dependencies 36 | dependencies: | setup $(VENV)/bin/nosetests 37 | 38 | # runs unit tests 39 | unit: $(VENV)/bin/nosetests # runs only unit tests 40 | $(VENV)/bin/nosetests --cover-erase tests/unit 41 | 42 | 43 | pyopenssl: $(VENV)/bin/nosetests 44 | $(VENV)/bin/nosetests --cover-erase tests/pyopenssl 45 | 46 | bugfixes: $(VENV)/bin/nosetests $(VENV)/bin/pytest # runs tests for specific bugfixes 47 | $(VENV)/bin/nosetests tests/bugfixes/nosetests 48 | $(VENV)/bin/pytest --maxfail=1 --mypy tests/bugfixes/pytest 49 | 50 | # runs functional tests 51 | functional: $(VENV)/bin/nosetests # runs functional tests 52 | $(VENV)/bin/nosetests tests/functional 53 | 54 | 55 | 56 | $(DOCS_INDEX): | $(VENV)/bin/sphinx-build 57 | cd docs && make html 58 | 59 | html: $(DOCS_INDEX) $(VENV)/bin/sphinx-build 60 | 61 | docs: $(DOCS_INDEX) $(VENV)/bin/sphinx-build 62 | open $(DOCS_INDEX) 63 | 64 | release: | clean tests html 65 | @rm -rf dist/* 66 | @./.release 67 | @make pypi 68 | 69 | dist: | clean 70 | $(VENV)/bin/python setup.py build sdist 71 | 72 | pypi: dist | $(VENV)/bin/twine 73 | $(VENV)/bin/twine upload dist/*.tar.gz 74 | 75 | # cleanup temp files 76 | clean: 77 | rm -rf $(HTML_ROOT) build dist 78 | 79 | 80 | # purge all virtualenv and temp files, causes everything to be rebuilt 81 | # from scratch by other tasks 82 | purge: clean 83 | rm -rf $(VENV) 84 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | HTTPretty 1.1.4 2 | =============== 3 | 4 | .. image:: https://github.com/gabrielfalcao/HTTPretty/raw/master/docs/source/_static/logo.svg?sanitize=true 5 | 6 | HTTP Client mocking tool for Python created by `Gabriel Falcão `_ . It provides a full fake TCP socket module. Inspired by `FakeWeb `_ 7 | 8 | 9 | - `Github Repository `_ 10 | - `Documentation `_ 11 | - `PyPI Package `_ 12 | 13 | 14 | **Python Support:** 15 | 16 | - **3.6** 17 | - **3.7** 18 | - **3.8** 19 | - **3.9** 20 | 21 | .. image:: https://img.shields.io/pypi/dm/HTTPretty 22 | :target: https://pypi.org/project/HTTPretty 23 | 24 | .. image:: https://img.shields.io/codecov/c/github/gabrielfalcao/HTTPretty 25 | :target: https://codecov.io/gh/gabrielfalcao/HTTPretty 26 | 27 | .. image:: https://img.shields.io/github/workflow/status/gabrielfalcao/HTTPretty/HTTPretty%20Tests?label=Python%203.6%20-%203.9 28 | :target: https://github.com/gabrielfalcao/HTTPretty/actions 29 | 30 | .. image:: https://img.shields.io/readthedocs/httpretty 31 | :target: https://httpretty.readthedocs.io/ 32 | 33 | .. image:: https://img.shields.io/github/license/gabrielfalcao/HTTPretty?label=Github%20License 34 | :target: https://github.com/gabrielfalcao/HTTPretty/blob/master/COPYING 35 | 36 | .. image:: https://img.shields.io/pypi/v/HTTPretty 37 | :target: https://pypi.org/project/HTTPretty 38 | 39 | .. image:: https://img.shields.io/pypi/l/HTTPretty?label=PyPi%20License 40 | :target: https://pypi.org/project/HTTPretty 41 | 42 | .. image:: https://img.shields.io/pypi/format/HTTPretty 43 | :target: https://pypi.org/project/HTTPretty 44 | 45 | .. image:: https://img.shields.io/pypi/status/HTTPretty 46 | :target: https://pypi.org/project/HTTPretty 47 | 48 | .. image:: https://img.shields.io/pypi/pyversions/HTTPretty 49 | :target: https://pypi.org/project/HTTPretty 50 | 51 | .. image:: https://img.shields.io/pypi/implementation/HTTPretty 52 | :target: https://pypi.org/project/HTTPretty 53 | 54 | .. image:: https://img.shields.io/snyk/vulnerabilities/github/gabrielfalcao/HTTPretty 55 | :target: https://github.com/gabrielfalcao/HTTPretty/network/alerts 56 | 57 | .. image:: https://img.shields.io/github/v/tag/gabrielfalcao/HTTPretty 58 | :target: https://github.com/gabrielfalcao/HTTPretty/releases 59 | 60 | .. |Join the chat at https://gitter.im/gabrielfalcao/HTTPretty| image:: https://badges.gitter.im/gabrielfalcao/HTTPretty.svg 61 | :target: https://gitter.im/gabrielfalcao/HTTPretty?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 62 | 63 | Install 64 | ------- 65 | 66 | .. code:: bash 67 | 68 | pip install httpretty 69 | 70 | 71 | 72 | Common Use Cases 73 | ================ 74 | 75 | - Test-driven development of API integrations 76 | - Fake responses of external APIs 77 | - Record and playback HTTP requests 78 | 79 | 80 | Simple Example 81 | -------------- 82 | 83 | .. code:: python 84 | 85 | import sure 86 | import httpretty 87 | import requests 88 | 89 | 90 | @httpretty.activate(verbose=True, allow_net_connect=False) 91 | def test_httpbin(): 92 | httpretty.register_uri( 93 | httpretty.GET, 94 | "https://httpbin.org/ip", 95 | body='{"origin": "127.0.0.1"}' 96 | ) 97 | 98 | response = requests.get('https://httpbin.org/ip') 99 | response.json().should.equal({'origin': '127.0.0.1'}) 100 | 101 | httpretty.latest_requests().should.have.length_of(1) 102 | httpretty.last_request().should.equal(httpretty.latest_requests()[0]) 103 | httpretty.last_request().body.should.equal('{"origin": "127.0.0.1"}') 104 | 105 | 106 | checking multiple responses 107 | --------------------------- 108 | 109 | .. code:: python 110 | 111 | @httpretty.activate(verbose=True, allow_net_connect=False) 112 | def test_post_bodies(): 113 | url = 'http://httpbin.org/post' 114 | httpretty.register_uri(httpretty.POST, url, status=200) 115 | httpretty.register_uri(httpretty.POST, url, status=400) 116 | requests.post(url, data={'foo': 'bar'}) 117 | requests.post(url, data={'zoo': 'zoo'}) 118 | assert 'foo=bar' in httpretty.latest_requests()[0].body 119 | assert 'zoo=bar' in httpretty.latest_requests()[1].body 120 | 121 | 122 | License 123 | ======= 124 | 125 | :: 126 | 127 | 128 | Copyright (C) <2011-2021> Gabriel Falcão 129 | 130 | Permission is hereby granted, free of charge, to any person 131 | obtaining a copy of this software and associated documentation 132 | files (the "Software"), to deal in the Software without 133 | restriction, including without limitation the rights to use, 134 | copy, modify, merge, publish, distribute, sublicense, and/or sell 135 | copies of the Software, and to permit persons to whom the 136 | Software is furnished to do so, subject to the following 137 | conditions: 138 | 139 | The above copyright notice and this permission notice shall be 140 | included in all copies or substantial portions of the Software. 141 | 142 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 143 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 144 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 145 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 146 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 147 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 148 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 149 | OTHER DEALINGS IN THE SOFTWARE. 150 | 151 | Main contributors 152 | ================= 153 | 154 | HTTPretty has received `many contributions `_ 155 | but some folks made remarkable contributions and deserve extra credit: 156 | 157 | - Andrew Gross ~> `@andrewgross `_ 158 | - Hugh Saunders ~> `@hughsaunders `_ 159 | - James Rowe ~> `@JNRowe `_ 160 | - Matt Luongo ~> `@mhluongo `_ 161 | - Steve Pulec ~> `@spulec `_ 162 | - Miro Hrončok ~> `@hroncok `_ 163 | - Mario Jonke ~> `@mariojonke `_ 164 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | LANG: en_US.UTF-8 4 | 5 | post: 6 | - pyenv global 2.7 3.6 7 | 8 | dependencies: 9 | override: 10 | - pip install -U pip 11 | - pip install pipenv 12 | - pipenv install --dev 13 | 14 | test: 15 | override: 16 | - make test 17 | 18 | post: 19 | - bash <(curl -s https://codecov.io/bash) 20 | -------------------------------------------------------------------------------- /development.txt: -------------------------------------------------------------------------------- 1 | check-manifest==0.41 2 | coverage>=5.0.3 3 | cryptography>=2.8 4 | eventlet==0.25.1 # issue #254 5 | flake8>=3.7.9 6 | freezegun>=0.3.15 7 | httplib2>=0.17.0 8 | httpx>=0.18.1 9 | ipdb>=0.13.2 10 | mccabe>=0.6.1 11 | mock>=3.0.5;python_version<"3.3" 12 | ndg-httpsclient>=0.5.1 13 | nose-randomly>=1.2.6 14 | nose>=1.3.7 15 | pathlib2>=2.3.5 16 | pyOpenSSL>=19.1.0 17 | redis==3.4.1 18 | rednose>=1.3.0 19 | requests-toolbelt>=0.9.1 20 | singledispatch>=3.4.0.3 21 | sphinx-rtd-theme>=0.5.2 22 | sphinx>=4.0.2 23 | sure>=1.4.11 24 | tornado>=6.0.4 25 | tox>=3.14.5 26 | twine>=1.15.0 27 | urllib3>=1.25.8 28 | boto3>=1.17.72 29 | ndg-httpsclient>=0.5.1 30 | pytest-mypy==0.8.1 31 | sphinxcontrib.asciinema==0.3.2 32 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | GIT_ROOT := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))/..) 4 | VENV_ROOT := $(GIT_ROOT)/.venv 5 | VENV ?= $(VENV_ROOT) 6 | 7 | # You can set these variables from the command line. 8 | SPHINXOPTS = 9 | SPHINXBUILD = $(VENV)/bin/sphinx-build 10 | SPHINXPROJ = HTTPretty 11 | SOURCEDIR = source 12 | BUILDDIR = build 13 | 14 | # Put it first so that "make" without argument is like "make help". 15 | help: 16 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 17 | 18 | .PHONY: help Makefile 19 | 20 | # Catch-all target: route all unknown targets to Sphinx using the new 21 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 22 | %: Makefile 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 24 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=HTTPretty 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/_static/guide-callback-regex-ipdb.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import requests 4 | from httpretty import httprettified, HTTPretty 5 | 6 | 7 | @httprettified(verbose=True, allow_net_connect=False) 8 | def test_basic_body(): 9 | 10 | def my_callback(request, url, headers): 11 | body = {} 12 | import ipdb;ipdb.set_trace() 13 | return (200, headers, json.dumps(body)) 14 | 15 | # Match any url via the regular expression 16 | HTTPretty.register_uri(HTTPretty.GET, re.compile(r'.*'), body=my_callback) 17 | HTTPretty.register_uri(HTTPretty.POST, re.compile(r'.*'), body=my_callback) 18 | 19 | # will trigger ipdb 20 | response = requests.post('https://test.com', data=json.dumps({'hello': 'world'})) 21 | -------------------------------------------------------------------------------- /docs/source/_static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 14 | 15 | 27 | 39 | 40 | 52 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/source/_static/read-timeout.py: -------------------------------------------------------------------------------- 1 | import requests, time 2 | from threading import Event 3 | 4 | from httpretty import httprettified 5 | from httpretty import HTTPretty 6 | 7 | 8 | @httprettified(allow_net_connect=False) 9 | def test_read_timeout(): 10 | event = Event() 11 | wait_seconds = 10 12 | connect_timeout = 0.1 13 | read_timeout = 0.1 14 | 15 | def my_callback(request, url, headers): 16 | event.wait(wait_seconds) 17 | return 200, headers, "Received" 18 | 19 | HTTPretty.register_uri( 20 | HTTPretty.GET, "http://example.com", 21 | body=my_callback 22 | ) 23 | 24 | requested_at = time.time() 25 | try: 26 | requests.get( 27 | "http://example.com", 28 | timeout=(connect_timeout, read_timeout)) 29 | except requests.exceptions.ReadTimeout: 30 | pass 31 | 32 | event_set_at = time.time() 33 | event.set() 34 | 35 | now = time.time() 36 | 37 | assert now - event_set_at < 0.2 38 | total_duration = now - requested_at 39 | assert total_duration < 0.2 40 | -------------------------------------------------------------------------------- /docs/source/_static/regex-example.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | import httpretty 4 | 5 | 6 | @httpretty.activate(allow_net_connect=False, verbose=True) 7 | def test_regex(): 8 | httpretty.register_uri(httpretty.GET, re.compile(r'.*'), status=418) 9 | 10 | response1 = requests.get('http://foo.com') 11 | assert response1.status_code == 418 12 | 13 | response2 = requests.get('http://test.com') 14 | assert response2.status_code == 418 15 | -------------------------------------------------------------------------------- /docs/source/_static/tmplun_dcms-ascii.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 115, "height": 30, "timestamp": 1621882710, "env": {"SHELL": "/usr/local/bin/bash", "TERM": "xterm-256color"}} 2 | [1.115441, "o", "HTTPretty $ "] 3 | [1.648012, "o", "."] 4 | [1.67985, "o", "v"] 5 | [1.767955, "o", "e"] 6 | [2.045646, "o", "nv/"] 7 | [2.888016, "o", "b"] 8 | [2.960179, "o", "i"] 9 | [3.046058, "o", "n/"] 10 | [4.703758, "o", "o"] 11 | [5.007876, "o", "\b\u001b[K"] 12 | [5.183768, "o", "n"] 13 | [5.249014, "o", "o"] 14 | [5.286233, "o", "s"] 15 | [5.423938, "o", "e"] 16 | [5.553841, "o", "\u0007tests"] 17 | [6.562148, "o", " "] 18 | [6.680246, "o", "d"] 19 | [6.791965, "o", "o"] 20 | [6.880409, "o", "cs/"] 21 | [7.533022, "o", "s"] 22 | [7.596079, "o", "ource/"] 23 | [7.636732, "o", "o"] 24 | [7.728134, "o", "u"] 25 | [8.176255, "o", "\b\u001b[K"] 26 | [8.321962, "o", "\b\u001b[K"] 27 | [8.463491, "o", "_"] 28 | [8.584109, "o", "s"] 29 | [8.727909, "o", "t"] 30 | [8.780703, "o", "atic/"] 31 | [9.403611, "o", "\u0007"] 32 | [10.393067, "o", "\r\n"] 33 | [10.393402, "o", "__pycache__/ guide-callback-regex-ipdb.py logo.svg\r\nHTTPretty $ .venv/bin/nosetests docs/source/_static/"] 34 | [10.517051, "o", "\r\n"] 35 | [10.517371, "o", "__pycache__/ guide-callback-regex-ipdb.py logo.svg\r\nHTTPretty $ .venv/bin/nosetests docs/source/_static/"] 36 | [11.423948, "o", "g"] 37 | [11.532343, "o", "uide-callback-regex-ipdb.py "] 38 | [14.504165, "o", "\r\n"] 39 | [16.477912, "o", "#190 guide-callback-regex-ipdb.test_basic_body ... "] 40 | [18.01153, "o", "> \u001b[0;32m/Users/gabrielfalcao/projects/personal/HTTPretty/docs/source/_static/guide-callback-regex-ipdb.py\u001b[0m(13)\u001b[0;36mmy_callback\u001b[0;34m()\u001b[0m\r\n\u001b[0;32m 12 \u001b[0;31m \u001b[0;32mimport\u001b[0m \u001b[0mipdb\u001b[0m\u001b[0;34m;\u001b[0m\u001b[0mipdb\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mset_trace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[0m\u001b[0;32m---> 13 \u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m200\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mheaders\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mjson\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdumps\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mbody\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[0m\u001b[0;32m 14 \u001b[0;31m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[0m\r\n"] 41 | [18.014165, "o", "\u001b[?1l"] 42 | [18.014537, "o", "\u001b[6n"] 43 | [18.030527, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[6D\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 44 | [18.071481, "o", "\u001b[?25l\u001b[?7l\u001b[6D\u001b[0;38;5;28mipdb> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \b\u001b[22A\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 45 | [21.666012, "o", "\u001b[?25l\u001b[?7l\u001b[0ml\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 46 | [21.879456, "o", "\u001b[?25l\u001b[?7l\u001b[7D\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[0ml\u001b[7D\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] 47 | [21.882208, "o", "\u001b[1;32m 8 \u001b[0m\u001b[0;32mdef\u001b[0m \u001b[0mtest_basic_body\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[1;32m 9 \u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[1;32m 10 \u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mmy_callback\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrequest\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0murl\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mheaders\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[1;32m 11 \u001b[0m \u001b[0mbody\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[1;32m 12 \u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mipdb\u001b[0m\u001b[0;34m;\u001b[0m\u001b[0mipdb\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mset_trace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[0;32m---> 13 \u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m200\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mheaders\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mjson\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdumps\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mbody\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[0m\u001b[1;32m 14 \u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[1;32m 15 \u001b[0m \u001b[0;31m# Match any url via the regular expression\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[1;32m 16 \u001b[0m \u001b[0mHTTPrett"] 48 | [21.882467, "o", "y\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mregister_uri\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mHTTPretty\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mGET\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mre\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcompile\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mr'.*'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbody\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmy_callback\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[1;32m 17 \u001b[0m \u001b[0mHTTPretty\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mregister_uri\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mHTTPretty\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPOST\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mre\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcompile\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mr'.*'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbody\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmy_callback\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\u001b[1;32m 18 \u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\r\n\r\n"] 49 | [21.883476, "o", "\u001b[?1l"] 50 | [21.883604, "o", "\u001b[6n"] 51 | [21.887032, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[6D\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 52 | [21.892331, "o", "\u001b[?25l\u001b[?7l\u001b[6D\u001b[0;38;5;28mipdb> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \b\u001b[9A\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 53 | [22.81778, "o", "\u001b[?25l\u001b[?7l\u001b[0mp\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 54 | [22.969469, "o", "\u001b[?25l\u001b[?7l\u001b[0mp\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 55 | [23.181313, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 56 | [23.322972, "o", "\u001b[?25l\u001b[?7l\u001b[0mr\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 57 | [23.378349, "o", "\u001b[?25l\u001b[?7l\u001b[0me\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 58 | [23.544925, "o", "\u001b[?25l\u001b[?7l\u001b[0mq\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 59 | [23.665954, "o", "\u001b[?25l\u001b[?7l\u001b[0mu\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 60 | [23.741902, "o", "\u001b[?25l\u001b[?7l\u001b[0me\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 61 | [23.879494, "o", "\u001b[?25l\u001b[?7l\u001b[0ms\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 62 | [24.000226, "o", "\u001b[?25l\u001b[?7l\u001b[0mt\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 63 | [24.365902, "o", "\u001b[?25l\u001b[?7l\u001b[16D\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[0mpp request\u001b[16D\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] 64 | [24.367405, "o", "\r\n"] 65 | [24.368531, "o", "\u001b[?1l"] 66 | [24.368635, "o", "\u001b[6n"] 67 | [24.371795, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[6D\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 68 | [24.376988, "o", "\u001b[?25l\u001b[?7l\u001b[6D\u001b[0;38;5;28mipdb> \u001b[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\u001b[0m \b\u001b[5A\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 69 | [27.359342, "o", "\u001b[?25l\u001b[?7l\u001b[0mpp request\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 70 | [27.82273, "o", "\u001b[?25l\u001b[?7l\u001b[0m.\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 71 | [28.255766, "o", "\u001b[?25l\u001b[?7l\u001b[0mh\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 72 | [28.313307, "o", "\u001b[?25l\u001b[?7l\u001b[0me\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 73 | [28.405472, "o", "\u001b[?25l\u001b[?7l\u001b[0ma\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 74 | [28.850422, "o", "\u001b[?25l\u001b[?7l\u001b[0md\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 75 | [28.912138, "o", "\u001b[?25l\u001b[?7l\u001b[0me\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 76 | [29.015229, "o", "\u001b[?25l\u001b[?7l\u001b[0mr\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 77 | [29.112081, "o", "\u001b[?25l\u001b[?7l\u001b[0ms\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 78 | [29.429921, "o", "\u001b[?25l\u001b[?7l\u001b[24D\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[0mpp request.headers\u001b[24D\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] 79 | [29.431801, "o", "\r\n"] 80 | [29.433016, "o", "\u001b[?1l"] 81 | [29.433238, "o", "\u001b[6n"] 82 | [29.436901, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[6D\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 83 | [29.444544, "o", "\u001b[?25l\u001b[?7l\u001b[6D\u001b[0;38;5;28mipdb> \u001b[0m\r\r\n\r\r\n\r\r\n\u001b[0m \b\u001b[3A\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 84 | [30.156471, "o", "\u001b[?25l\u001b[?7l\u001b[0mpp request.headers\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 85 | [30.608228, "o", "\u001b[?25l\u001b[?7l\u001b[7D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 86 | [30.765725, "o", "\u001b[?25l\u001b[?7l\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 87 | [30.902549, "o", "\u001b[?25l\u001b[?7l\u001b[7D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 88 | [31.381908, "o", "\u001b[?25l\u001b[?7l\u001b[0mdrequest.headers\u001b[15D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 89 | [31.510943, "o", "\u001b[?25l\u001b[?7l\u001b[0mirequest.headers\u001b[15D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 90 | [31.57281, "o", "\u001b[?25l\u001b[?7l\u001b[0mcrequest.headers\u001b[15D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 91 | [31.725778, "o", "\u001b[?25l\u001b[?7l\u001b[0mtrequest.headers\u001b[15D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 92 | [31.890771, "o", "\u001b[?25l\u001b[?7l\u001b[0m(request.headers\u001b[15D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 93 | [32.215385, "o", "\u001b[?25l\u001b[?7l\u001b[15C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 94 | [32.495352, "o", "\u001b[?25l\u001b[?7l\u001b[0m)\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 95 | [32.983501, "o", "\u001b[?25l\u001b[?7l\u001b[30D\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[0mpp dict(request.headers)\u001b[30D\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] 96 | [32.98525, "o", "{'Accept': '*/*',\r\n 'Accept-Encoding': 'gzip, deflate',\r\n 'Connection': 'keep-alive',\r\n 'Content-Length': '18',\r\n 'Host': 'test.com',\r\n 'User-Agent': 'python-requests/2.25.1'}\r\n"] 97 | [32.986518, "o", "\u001b[?1l"] 98 | [32.986746, "o", "\u001b[6n"] 99 | [32.990263, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[6D\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 100 | [32.996419, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 101 | [35.671881, "o", "\u001b[?25l\u001b[?7l\u001b[0mp\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 102 | [35.856772, "o", "\u001b[?25l\u001b[?7l\u001b[0mp\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 103 | [37.215658, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 104 | [37.693648, "o", "\u001b[?25l\u001b[?7l\u001b[0mr\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 105 | [37.758114, "o", "\u001b[?25l\u001b[?7l\u001b[0me\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 106 | [38.110067, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m\u001b[K\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 107 | [38.247765, "o", "\u001b[?25l\u001b[?7l\u001b[2D\u001b[0m\u001b[K\u001b[C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 108 | [38.397251, "o", "\u001b[?25l\u001b[?7l\u001b[0mh\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 109 | [38.444177, "o", "\u001b[?25l\u001b[?7l\u001b[0me\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 110 | [38.561504, "o", "\u001b[?25l\u001b[?7l\u001b[0ma\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 111 | [38.790802, "o", "\u001b[?25l\u001b[?7l\u001b[0md\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 112 | [38.926065, "o", "\u001b[?25l\u001b[?7l\u001b[0me\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 113 | [39.04009, "o", "\u001b[?25l\u001b[?7l\u001b[0mr\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 114 | [39.140742, "o", "\u001b[?25l\u001b[?7l\u001b[0ms\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 115 | [39.437672, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 116 | [39.886449, "o", "\u001b[?25l\u001b[?7l\u001b[17D\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[0mpp headers\u001b[16D\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] 117 | [39.888255, "o", "{'connection': 'close',\r\n 'date': 'Mon, 24 May 2021 18:58:47 GMT',\r\n 'server': 'Python/HTTPretty',\r\n 'status': 200}\r\n"] 118 | [39.890098, "o", "\u001b[?1l"] 119 | [39.890285, "o", "\u001b[6n"] 120 | [39.894169, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[6D\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 121 | [39.898533, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 122 | [45.717659, "o", "\u001b[?25l\u001b[?7l\u001b[0mh\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 123 | [45.733999, "o", "\u001b[?25l\u001b[?7l\u001b[0me\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 124 | [45.856076, "o", "\u001b[?25l\u001b[?7l\u001b[0ma\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 125 | [45.94174, "o", "\u001b[?25l\u001b[?7l\u001b[0md\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 126 | [46.030913, "o", "\u001b[?25l\u001b[?7l\u001b[0me\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 127 | [46.118401, "o", "\u001b[?25l\u001b[?7l\u001b[0mr\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 128 | [46.205205, "o", "\u001b[?25l\u001b[?7l\u001b[0ms\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 129 | [46.470445, "o", "\u001b[?25l\u001b[?7l\u001b[0m[\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 130 | [46.589064, "o", "\u001b[?25l\u001b[?7l\u001b[0m]\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 131 | [46.916657, "o", "\u001b[?25l\u001b[?7l\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 132 | [47.326705, "o", "\u001b[?25l\u001b[?7l\u001b[0m'']\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 133 | [47.354431, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 134 | [47.384967, "o", "\u001b[?25l\u001b[?7l\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 135 | [47.47368, "o", "\u001b[?25l\u001b[?7l\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 136 | [48.062051, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 137 | [48.345923, "o", "\u001b[?25l\u001b[?7l\u001b[0mC']\u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 138 | [48.59897, "o", "\u001b[?25l\u001b[?7l\u001b[0mo']\u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 139 | [48.663284, "o", "\u001b[?25l\u001b[?7l\u001b[0mn']\u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 140 | [48.750459, "o", "\u001b[?25l\u001b[?7l\u001b[0mt']\u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 141 | [48.809164, "o", "\u001b[?25l\u001b[?7l\u001b[0me']\u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 142 | [48.888522, "o", "\u001b[?25l\u001b[?7l\u001b[0mn']\u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 143 | [48.949883, "o", "\u001b[?25l\u001b[?7l\u001b[0mt']\u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 144 | [49.088369, "o", "\u001b[?25l\u001b[?7l\u001b[0m-']\u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 145 | [49.265468, "o", "\u001b[?25l\u001b[?7l\u001b[0mT']\u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 146 | [49.450521, "o", "\u001b[?25l\u001b[?7l\u001b[0my']\u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 147 | [49.516735, "o", "\u001b[?25l\u001b[?7l\u001b[0mp']\u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 148 | [49.662079, "o", "\u001b[?25l\u001b[?7l\u001b[0me']\u001b[2D\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 149 | [50.01415, "o", "\u001b[?25l\u001b[?7l\u001b[2C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 150 | [50.444668, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 151 | [50.506213, "o", "\u001b[?25l\u001b[?7l\u001b[0m=\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 152 | [50.566841, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 153 | [51.624083, "o", "\u001b[?25l\u001b[?7l\u001b[0m'\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 154 | [51.970211, "o", "\u001b[?25l\u001b[?7l\u001b[0m'\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 155 | [52.078158, "o", "\u001b[?25l\u001b[?7l\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 156 | [52.254808, "o", "\u001b[?25l\u001b[?7l\u001b[0ma'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 157 | [52.349951, "o", "\u001b[?25l\u001b[?7l\u001b[0mp'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 158 | [52.515528, "o", "\u001b[?25l\u001b[?7l\u001b[0mp'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 159 | [52.629864, "o", "\u001b[?25l\u001b[?7l\u001b[0ml'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 160 | [52.8266, "o", "\u001b[?25l\u001b[?7l\u001b[0mi'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 161 | [52.912236, "o", "\u001b[?25l\u001b[?7l\u001b[0mc'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 162 | [52.96185, "o", "\u001b[?25l\u001b[?7l\u001b[0ma'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 163 | [53.099131, "o", "\u001b[?25l\u001b[?7l\u001b[0mt'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 164 | [53.185005, "o", "\u001b[?25l\u001b[?7l\u001b[0mio'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 165 | [53.254551, "o", "\u001b[?25l\u001b[?7l\u001b[0mn'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 166 | [53.439432, "o", "\u001b[?25l\u001b[?7l\u001b[0m/'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 167 | [53.67937, "o", "\u001b[?25l\u001b[?7l\u001b[0mj'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 168 | [53.792533, "o", "\u001b[?25l\u001b[?7l\u001b[0ms'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 169 | [53.843024, "o", "\u001b[?25l\u001b[?7l\u001b[0mo'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 170 | [53.905637, "o", "\u001b[?25l\u001b[?7l\u001b[0mn'\b\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 171 | [54.32111, "o", "\u001b[?25l\u001b[?7l\u001b[49D\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[0mheaders['Content-Type'] = 'application/json'\u001b[50D\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h\u001b[?2004l"] 172 | [54.323453, "o", "\u001b[?1l"] 173 | [54.323703, "o", "\u001b[6n"] 174 | [54.32769, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[6D\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 175 | [54.33208, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 176 | [55.431101, "o", "\u001b[?25l\u001b[?7l\u001b[0mb\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 177 | [55.502203, "o", "\u001b[?25l\u001b[?7l\u001b[0mo\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 178 | [55.577597, "o", "\u001b[?25l\u001b[?7l\u001b[0md\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 179 | [55.702116, "o", "\u001b[?25l\u001b[?7l\u001b[0my\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 180 | [55.887196, "o", "\u001b[?25l\u001b[?7l\u001b[10D\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[0mbody\u001b[10D\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 181 | [55.887473, "o", "\u001b[?2004l"] 182 | [55.888628, "o", "{}\r\n"] 183 | [55.889744, "o", "\u001b[?1l"] 184 | [55.889857, "o", "\u001b[6n"] 185 | [55.893146, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[6D\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 186 | [55.89828, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 187 | [56.247068, "o", "\u001b[?25l\u001b[?7l\u001b[0mb\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 188 | [56.319912, "o", "\u001b[?25l\u001b[?7l\u001b[0mo\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 189 | [56.448876, "o", "\u001b[?25l\u001b[?7l\u001b[0md\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 190 | [56.521946, "o", "\u001b[?25l\u001b[?7l\u001b[0my\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 191 | [56.870316, "o", "\u001b[?25l\u001b[?7l\u001b[0m=\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 192 | [57.286911, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m\u001b[K\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 193 | [57.598176, "o", "\u001b[?25l\u001b[?7l\u001b[0m[\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 194 | [58.044934, "o", "\u001b[?25l\u001b[?7l\u001b[0m\"\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 195 | [58.398262, "o", "\u001b[?25l\u001b[?7l\u001b[0mo\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 196 | [58.568438, "o", "\u001b[?25l\u001b[?7l\u001b[0mo\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 197 | [58.814278, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m\u001b[K\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 198 | [58.98533, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m\u001b[K\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 199 | [59.183923, "o", "\u001b[?25l\u001b[?7l\b\u001b[0m\u001b[K\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 200 | [60.874043, "o", "\u001b[?25l\u001b[?7l\u001b[0m\"\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 201 | [61.07425, "o", "\u001b[?25l\u001b[?7l\u001b[0mf\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 202 | [61.154785, "o", "\u001b[?25l\u001b[?7l\u001b[0mo\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 203 | [61.28069, "o", "\u001b[?25l\u001b[?7l\u001b[0mo\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 204 | [61.831178, "o", "\u001b[?25l\u001b[?7l\u001b[0m\"\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 205 | [61.927243, "o", "\u001b[?25l\u001b[?7l\u001b[0m]\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 206 | [62.205321, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 207 | [62.288705, "o", "\u001b[?25l\u001b[?7l\u001b[0m=\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 208 | [62.381831, "o", "\u001b[?25l\u001b[?7l\u001b[C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 209 | [62.827726, "o", "\u001b[?25l\u001b[?7l\u001b[0m\"\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 210 | [62.999464, "o", "\u001b[?25l\u001b[?7l\u001b[0mb\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 211 | [63.070687, "o", "\u001b[?25l\u001b[?7l\u001b[0ma\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 212 | [63.157669, "o", "\u001b[?25l\u001b[?7l\u001b[0mr\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 213 | [63.421688, "o", "\u001b[?25l\u001b[?7l\u001b[0m\"\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 214 | [63.710915, "o", "\u001b[?25l\u001b[?7l\u001b[25D\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[0mbody[\"foo\"] = \"bar\"\u001b[25D\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 215 | [63.711171, "o", "\u001b[?2004l"] 216 | [63.713816, "o", "\u001b[?1l"] 217 | [63.71398, "o", "\u001b[6n"] 218 | [63.717705, "o", "\u001b[?2004h\u001b[?25l\u001b[0m\u001b[?7l\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[6D\u001b[6C\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 219 | [63.721978, "o", "\u001b[?25l\u001b[?7l\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 220 | [64.750191, "o", "\u001b[?25l\u001b[?7l\u001b[0mc\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 221 | [65.540761, "o", "\u001b[?25l\u001b[?7l\u001b[7D\u001b[0m\u001b[J\u001b[0;38;5;28mipdb> \u001b[0mc\u001b[7D\u001b[0m\r\r\n\u001b[J\u001b[?7h\u001b[0m\u001b[?12l\u001b[?25h"] 222 | [65.541109, "o", "\u001b[?2004l"] 223 | [65.544155, "o", "\u001b[32mpassed\u001b[0m\r\n"] 224 | [65.54465, "o", "\r\n"] 225 | [65.849412, "o", "Name Stmts Miss Branch BrPart Cover\r\n---------------------------------------------------------\r\n"] 226 | [65.849701, "o", "httpretty/__init__.py 37 2 0 0 95%\r\nhttpretty/compat.py 17 2 0 0 88%\r\nhttpretty/core.py 1007 200 304 59 76%\r\nhttpretty/errors.py 16 2 6 3 77%\r\nhttpretty/http.py 28 0 4 1 97%\r\nhttpretty/utils.py 8 0 4 0 100%\r\nhttpretty/version.py 1 0 0 0 100%\r\n---------------------------------------------------------\r\nTOTAL 1114 206 318 63 77%\r\n"] 227 | [66.339561, "o", "\u001b[30m-----------------------------------------------------------------------------\u001b[0m\r\n"] 228 | [66.33975, "o", "1 test run in 49.067 seconds\u001b[32m (1 test passed)\u001b[0m\r\n"] 229 | [66.340776, "o", "Error in atexit._run_exitfuncs:\r\nTraceback (most recent call last):\r\n"] 230 | [66.340975, "o", " File \"/Users/gabrielfalcao/projects/personal/HTTPretty/.venv/lib/python3.8/site-packages/IPython/core/history.py\", line 780, in writeout_cache\r\n"] 231 | [66.342214, "o", " self._writeout_input_cache(conn)\r\n"] 232 | [66.342401, "o", " File \"/Users/gabrielfalcao/projects/personal/HTTPretty/.venv/lib/python3.8/site-packages/IPython/core/history.py\", line 763, in _writeout_input_cache\r\n"] 233 | [66.343137, "o", " conn.execute(\"INSERT INTO history VALUES (?, ?, ?, ?)\",\r\nsqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. The object was created in thread id 123145518436352 and this is thread id 4682546624.\r\n"] 234 | [66.624442, "o", "HTTPretty $ "] 235 | [67.88294, "o", "exit\r\n"] 236 | -------------------------------------------------------------------------------- /docs/source/acks.rst: -------------------------------------------------------------------------------- 1 | Acknowledgements 2 | ################ 3 | 4 | Caveats 5 | ======= 6 | 7 | ``forcing_headers`` + ``Content-Length`` 8 | ---------------------------------------- 9 | 10 | When using the ``forcing_headers`` option make sure to add the header 11 | ``Content-Length`` otherwise calls using :py:mod:`requests` will try 12 | to load the response endlessly. 13 | 14 | Supported Libraries 15 | ------------------- 16 | 17 | Because HTTPretty works in the socket level it should work with any HTTP client libraries, although it is `battle tested `_ against: 18 | 19 | * `requests `_ 20 | * `httplib2 `_ 21 | * `urllib2 `_ 22 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API Reference 4 | ============= 5 | 6 | .. _httpretty: 7 | 8 | register_uri 9 | ------------ 10 | 11 | .. automethod:: httpretty.core.httpretty.register_uri 12 | :noindex: 13 | 14 | enable 15 | ------ 16 | 17 | .. automethod:: httpretty.core.httpretty.enable 18 | :noindex: 19 | 20 | disable 21 | ------- 22 | 23 | .. automethod:: httpretty.core.httpretty.disable 24 | :noindex: 25 | 26 | is_enabled 27 | ---------- 28 | 29 | .. automethod:: httpretty.core.httpretty.is_enabled 30 | :noindex: 31 | 32 | last_request 33 | ------------ 34 | 35 | .. autofunction:: httpretty.last_request 36 | :noindex: 37 | 38 | latest_requests 39 | --------------- 40 | 41 | .. autofunction:: httpretty.latest_requests 42 | :noindex: 43 | 44 | 45 | .. automodule:: httpretty 46 | 47 | .. _activate: 48 | 49 | activate 50 | -------- 51 | 52 | .. autoclass:: httpretty.activate 53 | :members: 54 | :noindex: 55 | 56 | 57 | .. _httprettified: 58 | 59 | httprettified 60 | ------------- 61 | 62 | .. autofunction:: httpretty.core.httprettified 63 | :noindex: 64 | 65 | 66 | .. _enabled: 67 | 68 | enabled 69 | ------- 70 | 71 | .. autoclass:: httpretty.enabled 72 | :members: 73 | :noindex: 74 | 75 | 76 | .. _httprettized: 77 | 78 | httprettized 79 | ------------ 80 | 81 | .. autoclass:: httpretty.core.httprettized 82 | :members: 83 | :noindex: 84 | 85 | 86 | 87 | .. _HTTPrettyRequest: 88 | 89 | HTTPrettyRequest 90 | ---------------- 91 | 92 | .. autoclass:: httpretty.core.HTTPrettyRequest 93 | :members: 94 | :noindex: 95 | 96 | 97 | .. _HTTPrettyRequestEmpty: 98 | 99 | HTTPrettyRequestEmpty 100 | --------------------- 101 | 102 | .. autoclass:: httpretty.core.HTTPrettyRequestEmpty 103 | :members: 104 | :noindex: 105 | 106 | .. _FakeSockFile: 107 | 108 | FakeSockFile 109 | ------------ 110 | 111 | .. autoclass:: httpretty.core.FakeSockFile 112 | :members: 113 | :noindex: 114 | 115 | 116 | .. _FakeSSLSocket: 117 | 118 | FakeSSLSocket 119 | ------------- 120 | 121 | .. autoclass:: httpretty.core.FakeSSLSocket 122 | :members: 123 | :noindex: 124 | 125 | 126 | .. _URIInfo: 127 | 128 | URIInfo 129 | ------- 130 | 131 | .. autoclass:: httpretty.URIInfo 132 | :members: 133 | :noindex: 134 | 135 | 136 | .. _URIMatcher: 137 | 138 | URIMatcher 139 | ---------- 140 | 141 | .. autoclass:: httpretty.URIMatcher 142 | :members: 143 | :noindex: 144 | 145 | 146 | .. _Entry: 147 | 148 | Entry 149 | ----- 150 | 151 | .. autoclass:: httpretty.Entry 152 | :members: 153 | :noindex: 154 | 155 | 156 | .. _api modules: 157 | 158 | Modules 159 | ======= 160 | 161 | .. _api module core: 162 | 163 | Core 164 | ---- 165 | 166 | .. automodule:: httpretty.core 167 | :members: 168 | 169 | .. _api module http: 170 | 171 | Http 172 | ---- 173 | 174 | .. automodule:: httpretty.http 175 | :members: 176 | 177 | .. _api module utils: 178 | 179 | Utils 180 | ----- 181 | 182 | .. automodule:: httpretty.utils 183 | :members: 184 | 185 | .. _api module exceptions: 186 | 187 | Exceptions 188 | ---------- 189 | 190 | .. automodule:: httpretty.errors 191 | :members: 192 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | Release 1.1.4 5 | ------------- 6 | 7 | - Bugfix: `#435 `_ Fallback to WARNING when logging.getLogger().level is None. 8 | 9 | Release 1.1.3 10 | ------------- 11 | 12 | - Bugfix: `#430 `_ Respect socket timeout. 13 | 14 | Release 1.1.2 15 | ------------- 16 | 17 | - Bugfix: `#426 `_ Segmentation fault when running against a large amount of tests with ``pytest --mypy``. 18 | 19 | Release 1.1.1 20 | ------------- 21 | 22 | - Bugfix: `httpretty.disable()` injects pyopenssl into :py:mod:`urllib3` even if it originally wasn't `#417 `_ 23 | - Bugfix: "Incompatibility with boto3 S3 put_object" `#416 `_ 24 | - Bugfix: "Regular expression for URL -> TypeError: wrap_socket() missing 1 required" `#413 `_ 25 | - Bugfix: "Making requests to non-stadard port throws TimeoutError "`#387 `_ 26 | 27 | 28 | Release 1.1.0 29 | ------------- 30 | 31 | - Feature: Display mismatched URL within ``UnmockedError`` whenever possible. `#388 `_ 32 | - Feature: Display mismatched URL via logging. `#419 `_ 33 | - Add new properties to :py:class:`httpretty.core.HTTPrettyRequest` (``protocol, host, url, path, method``). 34 | 35 | Example usage: 36 | 37 | .. testcode:: 38 | 39 | import httpretty 40 | import requests 41 | 42 | @httpretty.activate(verbose=True, allow_net_connect=False) 43 | def test_mismatches(): 44 | requests.get('http://sql-server.local') 45 | requests.get('https://redis.local') 46 | 47 | 48 | Release 1.0.5 49 | ------------- 50 | 51 | - Bugfix: Support `socket.socketpair() `_ . `#402 `_ 52 | - Bugfix: Prevent exceptions from re-applying monkey patches. `#406 `_ 53 | 54 | Release 1.0.4 55 | ------------- 56 | 57 | - Python 3.8 and 3.9 support. `#407 `_ 58 | 59 | Release 1.0.3 60 | ------------- 61 | 62 | - Fix compatibility with urllib3>=1.26. `#410 `_ 63 | 64 | Release 1.0.0 65 | ------------- 66 | 67 | - Drop Python 2 support. 68 | - Fix usage with redis and improve overall real-socket passthrough. `#271 `_. 69 | - Fix TypeError: wrap_socket() missing 1 required positional argument: 'sock' (`#393 `_) 70 | - Merge pull request `#364 `_ 71 | - Merge pull request `#371 `_ 72 | - Merge pull request `#379 `_ 73 | - Merge pull request `#386 `_ 74 | - Merge pull request `#302 `_ 75 | - Merge pull request `#373 `_ 76 | - Merge pull request `#383 `_ 77 | - Merge pull request `#385 `_ 78 | - Merge pull request `#389 `_ 79 | - Merge pull request `#391 `_ 80 | - Fix simple typo: neighter -> neither. 81 | - Updated documentation for register_uri concerning using ports. 82 | - Clarify relation between ``enabled`` and ``httprettized`` in API docs. 83 | - Align signature with builtin socket. 84 | 85 | Release 0.9.4 86 | ------------- 87 | 88 | Improvements: 89 | 90 | - Official Python 3.6 support 91 | - Normalized coding style to comform with PEP8 (partially) 92 | - Add more API reference coverage in docstrings of members such as :py:class:`httpretty.core.Entry` 93 | - Continuous Integration building python 2.7 and 3.6 94 | - Migrate from `pip `_ to `pipenv `_ 95 | 96 | 97 | Release 0.8.4 98 | ------------- 99 | 100 | Improvements: 101 | 102 | - Refactored ``core.py`` and increased its unit test coverage to 80%. 103 | HTTPretty is slightly more robust now. 104 | 105 | Bug fixes: 106 | 107 | - POST requests being called twice 108 | `#100 `__ 109 | 110 | Release 0.6.5 111 | ------------- 112 | 113 | Applied pull requests: 114 | 115 | - continue on EAGAIN socket errors: 116 | `#102 `__ by 117 | `kouk `__. 118 | - Fix ``fake_gethostbyname`` for requests 2.0: 119 | `#101 `__ by 120 | `mgood `__ 121 | - Add a way to match the querystrings: 122 | `#98 `__ by 123 | `ametaireau `__ 124 | - Use common string case for URIInfo hostname comparison: 125 | `#95 `__ by 126 | `mikewaters `__ 127 | - Expose httpretty.reset() to public API: 128 | `#91 `__ by 129 | `imankulov `__ 130 | - Don't duplicate http ports number: 131 | `#89 `__ by 132 | `mardiros `__ 133 | - Adding parsed\_body parameter to simplify checks: 134 | `#88 `__ by 135 | `toumorokoshi `__ 136 | - Use the real socket if it's not HTTP: 137 | `#87 `__ by 138 | `mardiros `__ 139 | 140 | Release 0.6.2 141 | ------------- 142 | 143 | - Fixing bug of lack of trailing slashes 144 | `#73 `__ 145 | - Applied pull requests 146 | `#71 `__ and 147 | `#72 `__ by 148 | @andresriancho 149 | - Keyword arg coercion fix by @dupuy 150 | - @papaeye fixed content-length calculation. 151 | 152 | Release 0.6.1 153 | ------------- 154 | 155 | - New API, no more camel case and everything is available through a 156 | simple import: 157 | 158 | .. code:: python 159 | 160 | import httpretty 161 | 162 | @httpretty.activate 163 | def test_function(): 164 | # httpretty.register_uri(...) 165 | # make request... 166 | pass 167 | 168 | - Re-organized module into submodules 169 | 170 | Release 0.5.14 171 | -------------- 172 | 173 | - Delegate calls to other methods on socket 174 | 175 | - `Normalized 176 | header `__ 177 | strings 178 | 179 | - Callbacks are `more intelligent 180 | now `__ 181 | 182 | - Normalize urls matching for url quoting 183 | 184 | Release 0.5.12 185 | -------------- 186 | 187 | - HTTPretty doesn't hang when using other application protocols under a 188 | @httprettified decorated test. 189 | 190 | Release 0.5.11 191 | -------------- 192 | 193 | - Ability to know whether HTTPretty is or not enabled through 194 | ``httpretty.is_enabled()`` 195 | 196 | Release 0.5.10 197 | -------------- 198 | 199 | - Support to multiple methods per registered URL. Thanks @hughsaunders 200 | 201 | Release 0.5.9 202 | ------------- 203 | 204 | - Fixed python 3 support. Thanks @spulec 205 | 206 | Release 0.5.8 207 | ------------- 208 | 209 | - Support to `register regular expressions to match 210 | urls <#matching-regular-expressions>`__ 211 | - `Body callback <#dynamic-responses-through-callbacks>`__ suppport 212 | - Python 3 support 213 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import sphinx_rtd_theme 5 | try: 6 | from pathlib2 import Path 7 | except ImportError: 8 | from pathlib import Path 9 | 10 | project_path = Path(__file__).absolute().parent.joinpath('../..') 11 | 12 | sys.path.insert(0, project_path.as_posix()) 13 | 14 | from httpretty.version import version # noqa 15 | 16 | 17 | project = 'HTTPretty' 18 | copyright = '2011-2021, Gabriel Falcao' 19 | author = 'Gabriel Falcao' 20 | 21 | # The short X.Y version 22 | version = version 23 | # The full version, including alpha/beta/rc tags 24 | release = version 25 | 26 | 27 | extensions = [ 28 | 'sphinx.ext.napoleon', 29 | 'sphinx.ext.autodoc', 30 | 'sphinx.ext.doctest', 31 | 'sphinx.ext.intersphinx', 32 | 'sphinx.ext.coverage', 33 | 'sphinx.ext.ifconfig', 34 | 'sphinx.ext.viewcode', 35 | 'sphinx.ext.githubpages', 36 | 'sphinx.ext.autosummary', 37 | 'sphinx.ext.autosummary', 38 | 'sphinxcontrib.asciinema', 39 | ] 40 | 41 | templates_path = ['_templates'] 42 | 43 | source_suffix = '.rst' 44 | master_doc = 'index' 45 | language = None 46 | exclude_patterns = [] 47 | pygments_style = 'friendly' 48 | html_theme = "sphinx_rtd_theme" 49 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 50 | # html_theme_options = {} 51 | html_static_path = ['_static'] 52 | 53 | htmlhelp_basename = 'HTTPrettydoc' 54 | 55 | latex_elements = {} 56 | 57 | latex_documents = [ 58 | (master_doc, 'HTTPretty.tex', 'HTTPretty Documentation', 59 | 'Gabriel Falcao', 'manual'), 60 | ] 61 | 62 | man_pages = [ 63 | (master_doc, 'httpretty', 'HTTPretty Documentation', 64 | [author], 1) 65 | ] 66 | 67 | 68 | texinfo_documents = [ 69 | (master_doc, 'HTTPretty', 'HTTPretty Documentation', 70 | author, 'HTTPretty', 'One line description of project.', 71 | 'Miscellaneous'), 72 | ] 73 | 74 | 75 | epub_title = project 76 | epub_author = author 77 | epub_publisher = author 78 | epub_copyright = copyright 79 | 80 | epub_exclude_files = ['search.html'] 81 | intersphinx_mapping = { 82 | 'python': ('https://docs.python.org/3', None), 83 | 84 | 'httplib2': ('https://httplib2.readthedocs.io/en/latest/', None), 85 | 'requests': ('https://requests.readthedocs.io/en/master/', None), 86 | 'urllib3': ('https://urllib3.readthedocs.io/en/latest/', None), 87 | } 88 | -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | Hacking on HTTPretty 2 | ==================== 3 | 4 | install development dependencies 5 | -------------------------------- 6 | 7 | 8 | .. note:: HTTPretty uses `GNU Make 9 | `_ as default build 10 | tool. 11 | 12 | 13 | .. code:: bash 14 | 15 | make dependencies 16 | 17 | 18 | next steps 19 | ---------- 20 | 21 | 1. run the tests with make: 22 | 23 | .. code:: bash 24 | 25 | make tests 26 | 27 | 2. hack at will 28 | 3. commit, push etc 29 | 4. send a pull request 30 | 31 | 32 | License 33 | ======= 34 | 35 | :: 36 | 37 | 38 | Copyright (C) <2011-2021> Gabriel Falcão 39 | 40 | Permission is hereby granted, free of charge, to any person 41 | obtaining a copy of this software and associated documentation 42 | files (the "Software"), to deal in the Software without 43 | restriction, including without limitation the rights to use, 44 | copy, modify, merge, publish, distribute, sublicense, and/or sell 45 | copies of the Software, and to permit persons to whom the 46 | Software is furnished to do so, subject to the following 47 | conditions: 48 | 49 | The above copyright notice and this permission notice shall be 50 | included in all copies or substantial portions of the Software. 51 | 52 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 53 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 54 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 55 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 56 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 57 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 58 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 59 | OTHER DEALINGS IN THE SOFTWARE. 60 | 61 | Main contributors 62 | ================= 63 | 64 | HTTPretty has received `many contributions `_ 65 | but some folks made remarkable contributions and deserve extra credit: 66 | 67 | - Andrew Gross ~> `@andrewgross `_ 68 | - Hugh Saunders ~> `@hughsaunders `_ 69 | - James Rowe ~> `@JNRowe `_ 70 | - Matt Luongo ~> `@mhluongo `_ 71 | - Steve Pulec ~> `@spulec `_ 72 | -------------------------------------------------------------------------------- /docs/source/guides.rst: -------------------------------------------------------------------------------- 1 | .. _guides: 2 | 3 | 4 | Guides 5 | ###### 6 | 7 | A series of guides to using HTTPretty for various interesting 8 | purposes. 9 | 10 | 11 | .. _matching_urls_via_regular_expressions: 12 | 13 | Matching URLs via regular expressions 14 | ===================================== 15 | 16 | You can pass a compiled regular expression via :py:func:`re.compile`, 17 | for example for intercepting all requests to a specific host. 18 | 19 | **Example:** 20 | 21 | 22 | .. literalinclude:: _static/regex-example.py 23 | :emphasize-lines: 8,10,13 24 | 25 | 26 | Response Callbacks 27 | ================== 28 | 29 | 30 | 31 | You can use the `body` parameter of 32 | :py:meth:`~httpretty.core.httpretty.register_uri` in useful, practical 33 | ways because it accepts a :py:func:`callable` as value. 34 | 35 | As matter of example, this is analogous to `defining routes in Flask 36 | `_ when combined with :ref:`matching urls via regular expressions ` 37 | 38 | This analogy breaks down, though, because HTTPretty does not provide 39 | tools to make it easy to handle cookies, parse querystrings etc. 40 | 41 | So far this has been a deliberate decision to keep HTTPretty operating 42 | mostly at the TCP socket level. 43 | 44 | Nothing prevents you from being creative with callbacks though, and as 45 | you will see in the examples below, the request parameter is an 46 | instance of :py:class:`~httpretty.core.HTTPrettyRequest` which has 47 | everything you need to create elaborate fake APIs. 48 | 49 | 50 | Defining callbacks 51 | ------------------ 52 | 53 | The body parameter callback must: 54 | 55 | - Accept 3 arguments: 56 | 57 | - `request` - :py:class:`~httpretty.core.HTTPrettyRequest` 58 | - `uri` - :py:class:`str` 59 | - `headers` - :py:class:`dict` with default response headers (including the ones from the parameters ``adding_headers`` and ``forcing_headers`` of :py:meth:`~httpretty.core.httpretty.register_uri` 60 | 61 | - Return 3 a tuple (or list) with 3 values 62 | 63 | - :py:class:`int` - HTTP Status Code 64 | - :py:class:`dict` - Response Headers 65 | - :py:class:`st` - Response Body 66 | 67 | .. important:: 68 | The **Content-Length** should match the byte length of the body. 69 | 70 | Changing **Content-Length** it in your handler can cause your HTTP 71 | client to misbehave, be very intentional when modifying it in our 72 | callback. 73 | 74 | The suggested way to manipulate headers is by modifying the 75 | response headers passed as argument and returning them in the tuple 76 | at the end. 77 | 78 | .. code:: python 79 | 80 | from typing import Tuple 81 | from httpretty.core import HTTPrettyRequest 82 | 83 | def my_callback( 84 | request: HTTPrettyRequest, 85 | url: str, 86 | headers: dict 87 | 88 | ) -> Tuple[int, dict, str]: 89 | 90 | headers['Content-Type'] = 'text/plain' 91 | return (200, headers, "the body") 92 | 93 | HTTPretty.register_uri(HTTPretty.GET, "https://test.com", body=my_callback) 94 | 95 | 96 | Debug requests interactively with ipdb 97 | -------------------------------------- 98 | 99 | The library `ipdb `_ comes in handy to 100 | introspect the request interactively with auto-complete via IPython. 101 | 102 | .. literalinclude:: _static/guide-callback-regex-ipdb.py 103 | :emphasize-lines: 12,16,17 104 | 105 | .. asciinema:: 415981 106 | :preload: 1 107 | 108 | 109 | Emulating timeouts 110 | ------------------ 111 | 112 | In the bug report `#430 113 | `_ the contributor `@mariojonke 114 | `_ provided a neat example of how to 115 | emulate read timeout errors by "waiting" inside of a body callback. 116 | 117 | 118 | .. literalinclude:: _static/read-timeout.py 119 | :emphasize-lines: 11-13,21,28 120 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. HTTPretty documentation master file, created by 2 | sphinx-quickstart on Sun Dec 13 07:25:00 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | HTTPretty's - HTTP Client Mocking for Python 7 | ============================================ 8 | 9 | .. image:: https://github.com/gabrielfalcao/HTTPretty/raw/master/docs/source/_static/logo.svg?sanitize=true 10 | 11 | HTTP Client mocking tool for Python created by `Gabriel Falcão `_ . It provides a full fake TCP socket module. Inspired by `FakeWeb `_ 12 | 13 | Looking for the `Github Repository `_ ? 14 | 15 | **Python Support:** 16 | 17 | - **3.6** 18 | - **3.7** 19 | - **3.8** 20 | - **3.9** 21 | 22 | .. image:: https://img.shields.io/pypi/dm/HTTPretty 23 | :target: https://pypi.org/project/HTTPretty 24 | 25 | .. image:: https://img.shields.io/codecov/c/github/gabrielfalcao/HTTPretty 26 | :target: https://codecov.io/gh/gabrielfalcao/HTTPretty 27 | 28 | .. image:: https://img.shields.io/github/workflow/status/gabrielfalcao/HTTPretty/HTTPretty%20Tests?label=Python%203.6%20-%203.9 29 | :target: https://github.com/gabrielfalcao/HTTPretty/actions 30 | 31 | .. image:: https://img.shields.io/readthedocs/httpretty 32 | :target: https://httpretty.readthedocs.io/ 33 | 34 | .. image:: https://img.shields.io/github/license/gabrielfalcao/HTTPretty?label=Github%20License 35 | :target: https://github.com/gabrielfalcao/HTTPretty/blob/master/COPYING 36 | 37 | .. image:: https://img.shields.io/pypi/v/HTTPretty 38 | :target: https://pypi.org/project/HTTPretty 39 | 40 | .. image:: https://img.shields.io/pypi/l/HTTPretty?label=PyPi%20License 41 | :target: https://pypi.org/project/HTTPretty 42 | 43 | .. image:: https://img.shields.io/pypi/format/HTTPretty 44 | :target: https://pypi.org/project/HTTPretty 45 | 46 | .. image:: https://img.shields.io/pypi/status/HTTPretty 47 | :target: https://pypi.org/project/HTTPretty 48 | 49 | .. image:: https://img.shields.io/pypi/pyversions/HTTPretty 50 | :target: https://pypi.org/project/HTTPretty 51 | 52 | .. image:: https://img.shields.io/pypi/implementation/HTTPretty 53 | :target: https://pypi.org/project/HTTPretty 54 | 55 | .. image:: https://img.shields.io/snyk/vulnerabilities/github/gabrielfalcao/HTTPretty 56 | :target: https://github.com/gabrielfalcao/HTTPretty/network/alerts 57 | 58 | .. image:: https://img.shields.io/github/v/tag/gabrielfalcao/HTTPretty 59 | :target: https://github.com/gabrielfalcao/HTTPretty/releases 60 | 61 | .. |Join the chat at https://gitter.im/gabrielfalcao/HTTPretty| image:: https://badges.gitter.im/gabrielfalcao/HTTPretty.svg 62 | :target: https://gitter.im/gabrielfalcao/HTTPretty?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 63 | 64 | 65 | .. toctree:: 66 | :maxdepth: 2 67 | 68 | introduction 69 | guides 70 | acks 71 | api 72 | contributing 73 | changelog 74 | 75 | Indices and tables 76 | ================== 77 | 78 | * :ref:`genindex` 79 | * :ref:`modindex` 80 | * :ref:`search` 81 | -------------------------------------------------------------------------------- /docs/source/introduction.rst: -------------------------------------------------------------------------------- 1 | .. _introduction: 2 | 3 | `Github `_ 4 | 5 | 6 | What is HTTPretty ? 7 | ################### 8 | 9 | .. highlight:: python 10 | 11 | Once upon a time a python developer wanted to use a RESTful api, 12 | everything was fine but until the day he needed to test the code that 13 | hits the RESTful API: what if the API server is down? What if its 14 | content has changed ? 15 | 16 | Don't worry, HTTPretty is here for you: 17 | 18 | :: 19 | 20 | import logging 21 | import requests 22 | import httpretty 23 | 24 | from sure import expect 25 | 26 | logging.getLogger('httpretty.core').setLevel(logging.DEBUG) 27 | 28 | @httpretty.activate(allow_net_connect=False) 29 | def test_yipit_api_returning_deals(): 30 | httpretty.register_uri(httpretty.GET, "http://api.yipit.com/v1/deals/", 31 | body='[{"title": "Test Deal"}]', 32 | content_type="application/json") 33 | 34 | response = requests.get('http://api.yipit.com/v1/deals/') 35 | 36 | expect(response.json()).to.equal([{"title": "Test Deal"}]) 37 | 38 | 39 | A more technical description 40 | ============================ 41 | 42 | HTTPretty is a python library that swaps the modules :py:mod:`socket` 43 | and :py:mod:`ssl` with fake implementations that intercept HTTP 44 | requests at the level of a TCP connection. 45 | 46 | It is inspired on Ruby's `FakeWeb `_. 47 | 48 | If you come from the Ruby programming language this would probably sound familiar :smiley: 49 | 50 | Installing 51 | ========== 52 | 53 | Installing httpretty is as easy as: 54 | 55 | .. highlight:: bash 56 | 57 | :: 58 | 59 | pip install httpretty 60 | 61 | 62 | Demo 63 | #### 64 | 65 | expecting a simple response body 66 | ================================ 67 | 68 | 69 | .. code:: python 70 | 71 | import requests 72 | import httpretty 73 | 74 | def test_one(): 75 | httpretty.enable(verbose=True, allow_net_connect=False) # enable HTTPretty so that it will monkey patch the socket module 76 | httpretty.register_uri(httpretty.GET, "http://yipit.com/", 77 | body="Find the best daily deals") 78 | 79 | response = requests.get('http://yipit.com') 80 | 81 | assert response.text == "Find the best daily deals" 82 | 83 | httpretty.disable() # disable afterwards, so that you will have no problems in code that uses that socket module 84 | httpretty.reset() # reset HTTPretty state (clean up registered urls and request history) 85 | 86 | 87 | making assertions in a callback that generates the response body 88 | ================================================================ 89 | 90 | .. code:: python 91 | 92 | import requests 93 | import json 94 | import httpretty 95 | 96 | @httpretty.activate 97 | def test_with_callback_response(): 98 | def request_callback(request, uri, response_headers): 99 | content_type = request.headers.get('Content-Type') 100 | assert request.body == '{"nothing": "here"}', 'unexpected body: {}'.format(request.body) 101 | assert content_type == 'application/json', 'expected application/json but received Content-Type: {}'.format(content_type) 102 | return [200, response_headers, json.dumps({"hello": "world"})] 103 | 104 | httpretty.register_uri( 105 | httpretty.POST, "https://httpretty.example.com/api", 106 | body=request_callback) 107 | 108 | response = requests.post('https://httpretty.example.com/api', headers={'Content-Type': 'application/json'}, data='{"nothing": "here"}') 109 | 110 | expect(response.json()).to.equal({"hello": "world"}) 111 | 112 | 113 | Link headers 114 | ============ 115 | 116 | 117 | Tests link headers by using the `adding_headers` parameter. 118 | 119 | 120 | .. code:: python 121 | 122 | import requests 123 | from sure import expect 124 | import httpretty 125 | 126 | 127 | @httpretty.activate 128 | def test_link_response(): 129 | first_url = "http://foo-api.com/data" 130 | second_url = "http://foo-api.com/data?page=2" 131 | link_str = "<%s>; rel='next'" % second_url 132 | 133 | httpretty.register_uri( 134 | httpretty.GET, 135 | first_url, 136 | body='{"success": true}', 137 | status=200, 138 | content_type="text/json", 139 | adding_headers={"Link": link_str}, 140 | ) 141 | httpretty.register_uri( 142 | httpretty.GET, 143 | second_url, 144 | body='{"success": false}', 145 | status=500, 146 | content_type="text/json", 147 | ) 148 | # Performs a request to `first_url` followed by some testing 149 | response = requests.get(first_url) 150 | expect(response.json()).to.equal({"success": True}) 151 | expect(response.status_code).to.equal(200) 152 | next_url = response.links["next"]["url"] 153 | expect(next_url).to.equal(second_url) 154 | 155 | # Follow the next URL and perform some testing. 156 | response2 = requests.get(next_url) 157 | expect(response2.json()).to.equal({"success": False}) 158 | expect(response2.status_code).to.equal(500) 159 | 160 | 161 | Motivation 162 | ########## 163 | 164 | When building systems that access external resources such as RESTful 165 | webservices, XMLRPC or even simple HTTP requests, we stumble in the 166 | problem: 167 | 168 | *"I'm gonna need to mock all those requests"* 169 | 170 | It can be a bit of a hassle to use something like 171 | :py:class:`mock.Mock` to stub the requests, this can work well for 172 | low-level unit tests but when writing functional or integration tests 173 | we should be able to allow the http calls to go through the TCP socket 174 | module. 175 | 176 | HTTPretty `monkey patches 177 | `_ Python's 178 | :py:mod:`socket` core module with a fake version of the module. 179 | 180 | Because HTTPretty implements a fake the modules :py:mod:`socket` and 181 | :py:mod:`ssl` you can use write tests to code against any HTTP library 182 | that use those modules. 183 | -------------------------------------------------------------------------------- /httpretty/__init__.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) <2011-2021> Gabriel Falcão 5 | # 6 | # Permission is hereby granted, free of charge, to any person 7 | # obtaining a copy of this software and associated documentation 8 | # files (the "Software"), to deal in the Software without 9 | # restriction, including without limitation the rights to use, 10 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the 12 | # Software is furnished to do so, subject to the following 13 | # conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | # OTHER DEALINGS IN THE SOFTWARE. 26 | # flake8: noqa 27 | 28 | 29 | from . import core 30 | 31 | from .core import httpretty, httprettified, EmptyRequestHeaders 32 | from .core import set_default_thread_timeout, get_default_thread_timeout 33 | from .errors import HTTPrettyError, UnmockedError 34 | from .version import version 35 | 36 | __version__ = version 37 | 38 | # aliases 39 | EmptyRequestHeaders = core.EmptyRequestHeaders 40 | Entry = core.Entry 41 | HTTPrettyRequestEmpty = core.HTTPrettyRequestEmpty 42 | URIInfo = core.URIInfo 43 | URIMatcher = core.URIMatcher 44 | httprettified = core.httprettified 45 | httprettized = core.httprettized 46 | httpretty = core.httpretty 47 | 48 | HTTPretty = httpretty 49 | activate = httprettified 50 | 51 | enabled = httprettized 52 | 53 | enable = httpretty.enable 54 | register_uri = httpretty.register_uri 55 | disable = httpretty.disable 56 | is_enabled = httpretty.is_enabled 57 | reset = httpretty.reset 58 | Response = httpretty.Response 59 | 60 | GET = httpretty.GET 61 | """Match requests of GET method""" 62 | PUT = httpretty.PUT 63 | """Match requests of PUT method""" 64 | POST = httpretty.POST 65 | """Match requests of POST method""" 66 | DELETE = httpretty.DELETE 67 | """Match requests of DELETE method""" 68 | HEAD = httpretty.HEAD 69 | """Match requests of HEAD method""" 70 | PATCH = httpretty.PATCH 71 | """Match requests of OPTIONS method""" 72 | OPTIONS = httpretty.OPTIONS 73 | """Match requests of OPTIONS method""" 74 | CONNECT = httpretty.CONNECT 75 | """Match requests of CONNECT method""" 76 | 77 | 78 | def last_request(): 79 | """ 80 | :returns: the last :py:class:`~httpretty.core.HTTPrettyRequest` 81 | """ 82 | return httpretty.last_request 83 | 84 | 85 | def latest_requests(): 86 | """returns the history of made requests""" 87 | return httpretty.latest_requests 88 | 89 | 90 | def has_request(): 91 | """ 92 | :returns: bool - whether any request has been made 93 | """ 94 | return not isinstance(httpretty.last_request.headers, EmptyRequestHeaders) 95 | -------------------------------------------------------------------------------- /httpretty/compat.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # 5 | # Copyright (C) <2011-2021> Gabriel Falcão 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | import io 29 | import types 30 | from urllib.parse import urlsplit 31 | from urllib.parse import urlunsplit 32 | from urllib.parse import parse_qs 33 | from urllib.parse import quote 34 | from urllib.parse import quote_plus 35 | from urllib.parse import unquote 36 | from urllib.parse import urlencode 37 | from http.server import BaseHTTPRequestHandler 38 | 39 | unquote_utf8 = unquote 40 | 41 | 42 | def encode_obj(in_obj): 43 | return in_obj 44 | 45 | 46 | class BaseClass(object): 47 | def __repr__(self): 48 | return self.__str__() 49 | 50 | 51 | __all__ = [ 52 | 'BaseClass', 53 | 'BaseHTTPRequestHandler', 54 | 'quote', 55 | 'quote_plus', 56 | 'urlencode', 57 | 'urlunsplit', 58 | 'urlsplit', 59 | 'parse_qs', 60 | ] 61 | -------------------------------------------------------------------------------- /httpretty/errors.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # 5 | # Copyright (C) <2011-2021> Gabriel Falcão 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | from __future__ import unicode_literals 28 | import json 29 | 30 | class HTTPrettyError(Exception): 31 | pass 32 | 33 | 34 | class UnmockedError(HTTPrettyError): 35 | def __init__(self, message='Failed to handle network request', request=None, address=None): 36 | hint = 'Tip: You could try setting (allow_net_connect=True) to allow unregistered requests through a real TCP connection in addition to (verbose=True) to debug the issue.' 37 | if request: 38 | headers = json.dumps(dict(request.headers), indent=2) 39 | message = '{message}.\n\nIntercepted unknown {request.method} request {request.url}\n\nWith headers {headers}'.format(**locals()) 40 | 41 | if isinstance(address, (tuple, list)): 42 | address = ":".join(map(str, address)) 43 | 44 | if address: 45 | hint = 'address: {address} | {hint}'.format(**locals()) 46 | 47 | self.request = request 48 | super(UnmockedError, self).__init__('{message}\n\n{hint}'.format(**locals())) 49 | -------------------------------------------------------------------------------- /httpretty/http.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) <2011-2021> Gabriel Falcão 5 | # 6 | # Permission is hereby granted, free of charge, to any person 7 | # obtaining a copy of this software and associated documentation 8 | # files (the "Software"), to deal in the Software without 9 | # restriction, including without limitation the rights to use, 10 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the 12 | # Software is furnished to do so, subject to the following 13 | # conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | # OTHER DEALINGS IN THE SOFTWARE. 26 | from __future__ import unicode_literals 27 | 28 | import re 29 | from .compat import BaseClass 30 | from .utils import decode_utf8 31 | 32 | 33 | STATUSES = { 34 | 100: "Continue", 35 | 101: "Switching Protocols", 36 | 102: "Processing", 37 | 200: "OK", 38 | 201: "Created", 39 | 202: "Accepted", 40 | 203: "Non-Authoritative Information", 41 | 204: "No Content", 42 | 205: "Reset Content", 43 | 206: "Partial Content", 44 | 207: "Multi-Status", 45 | 208: "Already Reported", 46 | 226: "IM Used", 47 | 300: "Multiple Choices", 48 | 301: "Moved Permanently", 49 | 302: "Found", 50 | 303: "See Other", 51 | 304: "Not Modified", 52 | 305: "Use Proxy", 53 | 306: "Switch Proxy", 54 | 307: "Temporary Redirect", 55 | 308: "Permanent Redirect", 56 | 400: "Bad Request", 57 | 401: "Unauthorized", 58 | 402: "Payment Required", 59 | 403: "Forbidden", 60 | 404: "Not Found", 61 | 405: "Method Not Allowed", 62 | 406: "Not Acceptable", 63 | 407: "Proxy Authentication Required", 64 | 408: "Request a Timeout", 65 | 409: "Conflict", 66 | 410: "Gone", 67 | 411: "Length Required", 68 | 412: "Precondition Failed", 69 | 413: "Request Entity Too Large", 70 | 414: "Request-URI Too Long", 71 | 415: "Unsupported Media Type", 72 | 416: "Requested Range Not Satisfiable", 73 | 417: "Expectation Failed", 74 | 418: "I'm a teapot", 75 | 420: "Enhance Your Calm", 76 | 421: "Misdirected Request", 77 | 422: "Unprocessable Entity", 78 | 423: "Locked", 79 | 424: "Failed Dependency", 80 | 425: "Unordered Collection", 81 | 426: "Upgrade Required", 82 | 428: "Precondition Required", 83 | 429: "Too Many Requests", 84 | 431: "Request Header Fields Too Large", 85 | 444: "No Response", 86 | 449: "Retry With", 87 | 450: "Blocked by Windows Parental Controls", 88 | 451: "Unavailable For Legal Reasons", 89 | 494: "Request Header Too Large", 90 | 495: "Cert Error", 91 | 496: "No Cert", 92 | 497: "HTTP to HTTPS", 93 | 499: "Client Closed Request", 94 | 500: "Internal Server Error", 95 | 501: "Not Implemented", 96 | 502: "Bad Gateway", 97 | 503: "Service Unavailable", 98 | 504: "Gateway Timeout", 99 | 505: "HTTP Version Not Supported", 100 | 506: "Variant Also Negotiates", 101 | 507: "Insufficient Storage", 102 | 508: "Loop Detected", 103 | 509: "Bandwidth Limit Exceeded", 104 | 510: "Not Extended", 105 | 511: "Network Authentication Required", 106 | 598: "Network read timeout error", 107 | 599: "Network connect timeout error", 108 | } 109 | 110 | 111 | class HttpBaseClass(BaseClass): 112 | GET = 'GET' 113 | PUT = 'PUT' 114 | POST = 'POST' 115 | DELETE = 'DELETE' 116 | HEAD = 'HEAD' 117 | PATCH = 'PATCH' 118 | OPTIONS = 'OPTIONS' 119 | CONNECT = 'CONNECT' 120 | METHODS = (GET, PUT, POST, DELETE, HEAD, PATCH, OPTIONS, CONNECT) 121 | 122 | 123 | def parse_requestline(s): 124 | """ 125 | http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5 126 | 127 | >>> parse_requestline('GET / HTTP/1.0') 128 | ('GET', '/', '1.0') 129 | >>> parse_requestline('post /testurl htTP/1.1') 130 | ('POST', '/testurl', '1.1') 131 | >>> parse_requestline('Im not a RequestLine') 132 | Traceback (most recent call last): 133 | ... 134 | ValueError: Not a Request-Line 135 | """ 136 | methods = '|'.join(HttpBaseClass.METHODS) 137 | m = re.match(r'(' + methods + r')\s+(.*)\s+HTTP/(1.[0|1])', s, re.I) 138 | if m: 139 | return m.group(1).upper(), m.group(2), m.group(3) 140 | else: 141 | raise ValueError('Not a Request-Line') 142 | 143 | 144 | def last_requestline(sent_data): 145 | """ 146 | Find the last line in sent_data that can be parsed with parse_requestline 147 | """ 148 | for line in reversed(sent_data): 149 | try: 150 | parse_requestline(decode_utf8(line)) 151 | except ValueError: 152 | pass 153 | else: 154 | return line 155 | -------------------------------------------------------------------------------- /httpretty/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) <2011-2021> Gabriel Falcão 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | def utf8(s): 27 | if isinstance(s, str): 28 | s = s.encode('utf-8') 29 | 30 | return bytes(s) 31 | 32 | 33 | def decode_utf8(s): 34 | if isinstance(s, bytes): 35 | s = s.decode("utf-8") 36 | 37 | return str(s) 38 | -------------------------------------------------------------------------------- /httpretty/version.py: -------------------------------------------------------------------------------- 1 | version = '1.1.4' 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=2 3 | rednose=1 4 | with-coverage=1 5 | cover-inclusive=1 6 | cover-package=httpretty 7 | cover-branches=1 8 | nocapture=1 9 | nologcapture=1 10 | stop=1 11 | with-id=1 12 | cover-xml=1 13 | cover-xml-file=coverage.xml 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # Copyright (C) <2011-2021> Gabriel Falcão 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | import io 28 | import os 29 | from setuptools import setup, find_packages 30 | 31 | 32 | def read_version(): 33 | ctx = {} 34 | exec(local_file('httpretty', 'version.py'), ctx) 35 | return ctx['version'] 36 | 37 | 38 | local_file = lambda *f: \ 39 | io.open( 40 | os.path.join(os.path.dirname(__file__), *f), encoding='utf-8').read() 41 | 42 | 43 | install_requires = [] 44 | tests_requires = ['nose', 'sure', 'coverage', 'mock;python_version<"3.3"', 45 | 'rednose'] 46 | 47 | 48 | setup( 49 | name='httpretty', 50 | version=read_version(), 51 | description='HTTP client mock for Python', 52 | long_description=local_file('README.rst'), 53 | author='Gabriel Falcao', 54 | author_email='gabriel@nacaolivre.org', 55 | url='https://httpretty.readthedocs.io/en/latest/', 56 | zip_safe=False, 57 | packages=find_packages(exclude=['*tests*']), 58 | tests_require=local_file('development.txt').splitlines(), 59 | install_requires=install_requires, 60 | license='MIT', 61 | test_suite='nose.collector', 62 | project_urls={ 63 | "Documentation": "https://httpretty.readthedocs.io/en/latest/", 64 | "Source Code": "https://github.com/gabrielfalcao/httpretty", 65 | "Issue Tracker": "https://github.com/gabrielfalcao/httpretty/issues", 66 | "Continuous Integration": "https://github.com/gabrielfalcao/HTTPretty/actions/workflows/pyenv.yml?query=branch%3Amaster+event%3Apush", 67 | "Test Coverage": "https://codecov.io/gh/gabrielfalcao/httpretty", 68 | }, 69 | python_requires='>=3', 70 | classifiers=[ 71 | 'Development Status :: 5 - Production/Stable', 72 | 'Intended Audience :: Developers', 73 | 'License :: OSI Approved :: MIT License', 74 | 'Programming Language :: Python :: 3', 75 | 'Programming Language :: Python :: 3.6', 76 | 'Programming Language :: Python :: 3.7', 77 | 'Programming Language :: Python :: 3.8', 78 | 'Programming Language :: Python :: 3.9', 79 | 'Programming Language :: Python', 80 | 'Topic :: Internet :: WWW/HTTP', 81 | 'Topic :: Software Development :: Testing' 82 | ], 83 | ) 84 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals 4 | import sure 5 | -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielfalcao/HTTPretty/f9f012711597634d40066d144a36888b3addcc46/tests/bugfixes/nosetests/__init__.py -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/test_242_ssl_bad_handshake.py: -------------------------------------------------------------------------------- 1 | import httpretty 2 | import requests 3 | 4 | 5 | @httpretty.activate 6 | def test_test_ssl_bad_handshake(): 7 | # Reproduces https://github.com/gabrielfalcao/HTTPretty/issues/242 8 | 9 | url_http = 'http://httpbin.org/status/200' 10 | url_https = 'https://github.com/gabrielfalcao/HTTPretty' 11 | 12 | httpretty.register_uri(httpretty.GET, url_http, body='insecure') 13 | httpretty.register_uri(httpretty.GET, url_https, body='encrypted') 14 | 15 | requests.get(url_http).text.should.equal('insecure') 16 | requests.get(url_https).text.should.equal('encrypted') 17 | 18 | httpretty.latest_requests().should.have.length_of(2) 19 | insecure_request, secure_request = httpretty.latest_requests()[:2] 20 | 21 | insecure_request.protocol.should.be.equal('http') 22 | secure_request.protocol.should.be.equal('https') 23 | 24 | insecure_request.url.should.be.equal(url_http) 25 | secure_request.url.should.be.equal(url_https) 26 | -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/test_387_regex_port.py: -------------------------------------------------------------------------------- 1 | # based on the snippet from https://github.com/gabrielfalcao/HTTPretty/issues/387 2 | 3 | import httpretty 4 | import requests 5 | from sure import expect 6 | 7 | @httpretty.activate(allow_net_connect=False, verbose=True) 8 | def test_match_with_port_no_slashes(): 9 | "Reproduce #387 registering host:port without trailing slash" 10 | httpretty.register_uri(httpretty.GET, 'http://fakeuri.com:8080', body='{"hello":"world"}') 11 | req = requests.get('http://fakeuri.com:8080', timeout=1) 12 | expect(req.status_code).to.equal(200) 13 | expect(req.json()).to.equal({"hello": "world"}) 14 | 15 | 16 | @httpretty.activate(allow_net_connect=False, verbose=True) 17 | def test_match_with_port_trailing_slash(): 18 | "Reproduce #387 registering host:port with trailing slash" 19 | httpretty.register_uri(httpretty.GET, 'https://fakeuri.com:443/', body='{"hello":"world"}') 20 | req = requests.get('https://fakeuri.com:443', timeout=1) 21 | expect(req.status_code).to.equal(200) 22 | expect(req.json()).to.equal({"hello": "world"}) 23 | 24 | req = requests.get('https://fakeuri.com:443/', timeout=1) 25 | expect(req.status_code).to.equal(200) 26 | expect(req.json()).to.equal({"hello": "world"}) 27 | -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/test_388_unmocked_error_with_url.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) <2011-2021> Gabriel Falcão 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | import requests 25 | import httpretty 26 | from httpretty.errors import UnmockedError 27 | 28 | from unittest import skip 29 | from sure import expect 30 | 31 | 32 | def http(): 33 | sess = requests.Session() 34 | adapter = requests.adapters.HTTPAdapter(pool_connections=1, pool_maxsize=1) 35 | sess.mount('http://', adapter) 36 | sess.mount('https://', adapter) 37 | return sess 38 | 39 | @httpretty.activate(allow_net_connect=False) 40 | def test_https_forwarding(): 41 | "#388 UnmockedError is raised with details about the mismatched request" 42 | httpretty.register_uri(httpretty.GET, 'http://google.com/', body="Not Google") 43 | httpretty.register_uri(httpretty.GET, 'https://google.com/', body="Not Google") 44 | response1 = http().get('http://google.com/') 45 | response2 = http().get('https://google.com/') 46 | 47 | http().get.when.called_with("https://github.com/gabrielfalcao/HTTPretty").should.have.raised(UnmockedError, 'https://github.com/gabrielfalcao/HTTPretty') 48 | 49 | response1.text.should.equal(response2.text) 50 | try: 51 | http().get("https://github.com/gabrielfalcao/HTTPretty") 52 | except UnmockedError as exc: 53 | expect(exc).to.have.property('request') 54 | expect(exc.request).to.have.property('host').being.equal('github.com') 55 | expect(exc.request).to.have.property('protocol').being.equal('https') 56 | expect(exc.request).to.have.property('url').being.equal('https://github.com/gabrielfalcao/HTTPretty') 57 | -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/test_413_regex.py: -------------------------------------------------------------------------------- 1 | # File based on the snippet provided in https://github.com/gabrielfalcao/HTTPretty/issues/413#issue-787264551 2 | import requests 3 | import httpretty 4 | import re 5 | 6 | 7 | def mock_body(request, url, response_headers): 8 | return [200, response_headers, "Mocked " + url] 9 | 10 | 11 | @httpretty.activate(verbose=True, allow_net_connect=False) 12 | def test_works_with_regex_path(): 13 | "Issue #413 regex with path" 14 | patmatchpat = re.compile("/file-one") 15 | 16 | httpretty.register_uri(httpretty.GET, patmatchpat, body=mock_body) 17 | 18 | response = requests.get("https://example.com/file-one.html") 19 | response.status_code.should.equal(200) 20 | response.text.should.equal("Mocked https://example.com/file-one.html") 21 | 22 | response = requests.get("https://github.com/file-one.json") 23 | response.status_code.should.equal(200) 24 | response.text.should.equal("Mocked https://github.com/file-one.json") 25 | 26 | @httpretty.activate(verbose=True, allow_net_connect=False) 27 | def test_works_with_regex_dotall(): 28 | "Issue #413 regex with .*" 29 | patmatchpat = re.compile(".*/file-two.*") 30 | 31 | httpretty.register_uri(httpretty.GET, patmatchpat, body=mock_body) 32 | 33 | response = requests.get("https://example.com/file-two.html") 34 | response.status_code.should.equal(200) 35 | response.text.should.equal("Mocked https://example.com/file-two.html") 36 | 37 | response = requests.get("https://github.com/file-two.json") 38 | response.status_code.should.equal(200) 39 | response.text.should.equal("Mocked https://github.com/file-two.json") 40 | -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/test_414_httpx.py: -------------------------------------------------------------------------------- 1 | import httpretty 2 | import httpx 3 | from sure import expect 4 | 5 | @httpretty.activate(verbose=True, allow_net_connect=False) 6 | def test_httpx(): 7 | "#414 httpx support" 8 | httpretty.register_uri(httpretty.GET, "https://blog.falcao.it/", 9 | body="Posts") 10 | 11 | response = httpx.get('https://blog.falcao.it') 12 | 13 | expect(response.text).to.equal("Posts") 14 | -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/test_416_boto3.py: -------------------------------------------------------------------------------- 1 | import httpretty 2 | import boto3 3 | from botocore.exceptions import ClientError 4 | 5 | from sure import expect 6 | 7 | 8 | @httpretty.activate(allow_net_connect=False, verbose=True) 9 | def test_boto3(): 10 | "#416 boto3 issue" 11 | httpretty.register_uri( 12 | httpretty.PUT, 13 | "https://foo-bucket.s3.amazonaws.com/foo-object", 14 | body=""" 15 | 16 | AccessDenied 17 | Access Denied 18 | foo 19 | foo 20 | """, 21 | status=403 22 | ) 23 | 24 | session = boto3.Session(aws_access_key_id="foo", aws_secret_access_key="foo") 25 | s3_client = session.client('s3') 26 | 27 | put_object = expect(s3_client.put_object).when.called_with( 28 | Bucket="foo-bucket", 29 | Key="foo-object", 30 | Body=b"foo" 31 | ) 32 | 33 | put_object.should.have.raised(ClientError, 'Access Denied') 34 | -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/test_417_openssl.py: -------------------------------------------------------------------------------- 1 | # This test is based on @ento's example snippet: 2 | # https://gist.github.com/ento/e1e33d7d67e406bf03fe61f018404c21 3 | 4 | # Original Issue: 5 | # https://github.com/gabrielfalcao/HTTPretty/issues/417 6 | import httpretty 7 | import requests 8 | import urllib3 9 | from sure import expect 10 | from unittest import skipIf 11 | try: 12 | from urllib3.contrib.pyopenssl import extract_from_urllib3 13 | except Exception: 14 | extract_from_urllib3 = None 15 | 16 | 17 | @skipIf(extract_from_urllib3 is None, 18 | "urllib3.contrib.pyopenssl.extract_from_urllib3 does not exist") 19 | def test_enable_disable_httpretty_extract(): 20 | "#417 urllib3.contrib.pyopenssl enable -> disable extract" 21 | expect(urllib3.util.IS_PYOPENSSL).to.be.false 22 | httpretty.enable() 23 | httpretty.disable() 24 | extract_from_urllib3() 25 | expect(urllib3.util.IS_PYOPENSSL).to.be.false 26 | 27 | def test_enable_disable_httpretty(): 28 | "#417 urllib3.contrib.pyopenssl enable -> disable extract" 29 | expect(urllib3.util.IS_PYOPENSSL).to.be.false 30 | httpretty.enable() 31 | httpretty.disable() 32 | extract_from_urllib3() 33 | expect(urllib3.util.IS_PYOPENSSL).to.be.false 34 | -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/test_425_latest_requests.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import httpretty 3 | from httpretty.errors import UnmockedError 4 | 5 | from unittest import skip 6 | from sure import expect 7 | 8 | 9 | @httpretty.activate(allow_net_connect=True) 10 | def test_latest_requests(): 11 | "#425 - httpretty.latest_requests() can be called multiple times" 12 | httpretty.register_uri(httpretty.GET, 'http://google.com/', body="Not Google") 13 | httpretty.register_uri(httpretty.GET, 'https://google.com/', body="Not Google") 14 | 15 | requests.get('http://google.com/') 16 | httpretty.latest_requests()[-1].url.should.equal('http://google.com/') 17 | requests.get('https://google.com/') 18 | httpretty.latest_requests()[-1].url.should.equal('https://google.com/') 19 | 20 | httpretty.latest_requests().should.have.length_of(2) 21 | httpretty.latest_requests()[-1].url.should.equal('https://google.com/') 22 | 23 | requests.get('https://google.com/') 24 | httpretty.latest_requests().should.have.length_of(3) 25 | httpretty.latest_requests()[-1].url.should.equal('https://google.com/') 26 | 27 | requests.get('http://google.com/') 28 | httpretty.latest_requests().should.have.length_of(4) 29 | httpretty.latest_requests()[-1].url.should.equal('http://google.com/') 30 | -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/test_430_respect_timeout.py: -------------------------------------------------------------------------------- 1 | # This test is based on @mariojonke snippet: 2 | # https://github.com/gabrielfalcao/HTTPretty/issues/430 3 | import time 4 | from requests import Session 5 | from requests.adapters import HTTPAdapter 6 | from requests.exceptions import ReadTimeout 7 | 8 | from threading import Event 9 | 10 | from httpretty import httprettified 11 | from httpretty import HTTPretty 12 | 13 | 14 | def http(max_connections=1): 15 | session = Session() 16 | adapter = HTTPAdapter( 17 | pool_connections=max_connections, 18 | pool_maxsize=max_connections 19 | ) 20 | session.mount('http://', adapter) 21 | session.mount('https://', adapter) 22 | return session 23 | 24 | 25 | 26 | @httprettified(verbose=True, allow_net_connect=False) 27 | def test_read_timeout(): 28 | "#430 httpretty should respect read timeout" 29 | event = Event() 30 | uri = "http://example.com" 31 | 32 | # Given that I register a uri with a callback body that delays 10 seconds 33 | wait_seconds = 10 34 | 35 | def my_callback(request, url, headers): 36 | event.wait(wait_seconds) 37 | return 200, headers, "Received" 38 | 39 | HTTPretty.register_uri(HTTPretty.GET, uri, body=my_callback) 40 | 41 | # And I use a thread pool with 1 TCP connection max 42 | max_connections = 1 43 | request = http(max_connections) 44 | started_at = time.time() 45 | # When I make an HTTP request with a read timeout of 0.1 and an indefinite connect timeout 46 | when_called = request.get.when.called_with(uri, timeout=(None, 0.1)) 47 | 48 | # Then the request should have raised a connection timeout 49 | when_called.should.have.raised(ReadTimeout) 50 | 51 | # And the total execution time should be less than 0.2 seconds 52 | event.set() 53 | total_time = time.time() - started_at 54 | total_time.should.be.lower_than(0.2) 55 | -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/test_eventlet.py: -------------------------------------------------------------------------------- 1 | import httpretty 2 | import requests 3 | import eventlet 4 | eventlet.monkey_patch(all=False, socket=True) 5 | 6 | 7 | @httpretty.activate 8 | def test_something(): 9 | 10 | httpretty.register_uri(httpretty.GET, 'https://example.com', body='foo') 11 | requests.get('https://example.com').text.should.equal('foo') 12 | -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/test_redis.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import httpretty 4 | 5 | try: 6 | from redis import Redis 7 | except ImportError: 8 | Redis = None 9 | 10 | from unittest import skipUnless 11 | 12 | 13 | def redis_available(): 14 | if Redis is None: 15 | return False 16 | 17 | params = dict( 18 | host=os.getenv('REDIS_HOST') or '127.0.0.1', 19 | port=int(os.getenv('REDIS_PORT') or 6379) 20 | ) 21 | conn = Redis(**params) 22 | try: 23 | conn.keys('*') 24 | conn.close() 25 | return True 26 | except Exception: 27 | return False 28 | 29 | 30 | @skipUnless(redis_available(), reason='no redis server available for test') 31 | @httpretty.activate() 32 | def test_work_in_parallel_to_redis(): 33 | "HTTPretty should passthrough redis connections" 34 | 35 | redis = Redis() 36 | 37 | keys = redis.keys('*') 38 | for key in keys: 39 | redis.delete(key) 40 | 41 | redis.append('item1', 'value1') 42 | redis.append('item2', 'value2') 43 | 44 | sorted(redis.keys('*')).should.equal([b'item1', b'item2']) 45 | 46 | httpretty.register_uri( 47 | httpretty.GET, 48 | "http://redis.io", 49 | body="salvatore") 50 | 51 | response = requests.get('http://redis.io') 52 | response.text.should.equal('salvatore') 53 | -------------------------------------------------------------------------------- /tests/bugfixes/nosetests/test_tornado_bind_unused_port.py: -------------------------------------------------------------------------------- 1 | import httpretty 2 | from unittest import skip 3 | from tornado.testing import bind_unused_port 4 | 5 | 6 | @skip('') 7 | @httpretty.activate(allow_net_connect=True) 8 | def test_passthrough_binding_socket(): 9 | # issue #247 10 | 11 | result = bind_unused_port() 12 | result.should.be.a(tuple) 13 | result.should.have.length_of(2) 14 | 15 | socket, port = result 16 | 17 | port.should.be.an(int) 18 | socket.close() 19 | -------------------------------------------------------------------------------- /tests/bugfixes/pytest/test_426_mypy_segfault.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | import json 4 | import unittest 5 | import re 6 | import httpretty 7 | 8 | 9 | class GenerateTests(type): 10 | def __init__(cls, name, bases, attrs): 11 | if name in ('GenerateTestMeta',): return 12 | 13 | count = getattr(cls, '__generate_count__', attrs.get('__generate_count__')) 14 | if not isinstance(count, int): 15 | raise SyntaxError(f'Metaclass requires def `__generate_count__ = NUMBER_OF_TESTS` to be set to an integer') 16 | 17 | generate_method = getattr(cls, '__generate_method__', attrs.get('__generate_method__')) 18 | if not callable(generate_method): 19 | raise SyntaxError(f'Metaclass requires def `__generate_method__(test_name):` to be implemented') 20 | 21 | 22 | for x in range(count): 23 | test_name = "test_{}".format(x) 24 | def test_func(self, *args, **kwargs): 25 | run_test = generate_method(test_name) 26 | run_test(self, *args, **kwargs) 27 | 28 | test_func.__name__ = test_name 29 | attrs[test_name] = test_func 30 | setattr(cls, test_name, test_func) 31 | 32 | 33 | class TestBug426MypySegfaultWithCallbackAndPayload(unittest.TestCase, metaclass=GenerateTests): 34 | __generate_count__ = 1000 35 | 36 | def __generate_method__(test_name): 37 | @httpretty.httprettified(allow_net_connect=False) 38 | def test_func(self): 39 | httpretty.register_uri(httpretty.GET, 'http://github.com', body=self.json_response_callback({"kind": "insecure"})) 40 | httpretty.register_uri(httpretty.GET, 'https://github.com', body=self.json_response_callback({"kind": "secure"})) 41 | httpretty.register_uri(httpretty.POST, re.compile('github.com/.*'), body=self.json_response_callback({"kind": "regex"}) ) 42 | 43 | response = requests.post( 44 | 'https://github.com/foo', 45 | headers={ 46 | "Content-Type": "application/json" 47 | }, 48 | data=json.dumps({test_name: time.time()})) 49 | 50 | assert response.status_code == 200 51 | 52 | try: 53 | response = requests.get('https://gitlab.com') 54 | assert response.status_code == 200 55 | except Exception: 56 | pass 57 | 58 | return test_func 59 | 60 | def json_response_callback(self, data): 61 | 62 | payload = dict(data) 63 | payload.update({ 64 | "time": time.time() 65 | }) 66 | 67 | def request_callback(request, path, headers): 68 | return [200, headers, json.dumps(payload)] 69 | 70 | return request_callback 71 | 72 | 73 | class TestBug426MypySegfaultWithEmptyMethod(unittest.TestCase, metaclass=GenerateTests): 74 | __generate_count__ = 10000 75 | 76 | def __generate_method__(test_name): 77 | @httpretty.httprettified(allow_net_connect=False) 78 | def test_func(self): 79 | pass 80 | 81 | return test_func 82 | -------------------------------------------------------------------------------- /tests/compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | from unittest.mock import Mock, patch, call, MagicMock 3 | except ImportError: 4 | from mock import Mock, patch, call, MagicMock 5 | -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) <2011-2021> Gabriel Falcão 5 | # 6 | # Permission is hereby granted, free of charge, to any person 7 | # obtaining a copy of this software and associated documentation 8 | # files (the "Software"), to deal in the Software without 9 | # restriction, including without limitation the rights to use, 10 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the 12 | # Software is furnished to do so, subject to the following 13 | # conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | # OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | import warnings 28 | warnings.simplefilter('ignore') 29 | -------------------------------------------------------------------------------- /tests/functional/base.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # Copyright (C) <2011-2021> Gabriel Falcão 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | from __future__ import unicode_literals 29 | 30 | import os 31 | import json 32 | import socket 33 | import threading 34 | 35 | import tornado.ioloop 36 | import tornado.web 37 | from functools import wraps 38 | 39 | from os.path import abspath, dirname, join 40 | from httpretty.core import POTENTIAL_HTTP_PORTS, old_socket 41 | 42 | 43 | def get_free_tcp_port(): 44 | """returns a TCP port that can be used for listen in the host. 45 | """ 46 | tcp = old_socket(socket.AF_INET, socket.SOCK_STREAM) 47 | tcp.bind(('', 0)) 48 | host, port = tcp.getsockname() 49 | tcp.close() 50 | return port 51 | 52 | 53 | LOCAL_FILE = lambda *path: join(abspath(dirname(__file__)), *path) 54 | FIXTURE_FILE = lambda name: LOCAL_FILE('fixtures', name) 55 | 56 | 57 | class JSONEchoHandler(tornado.web.RequestHandler): 58 | def get(self, matched): 59 | payload = dict([(x, self.get_argument(x)) for x in self.request.arguments]) 60 | self.write(json.dumps({matched or 'index': payload}, indent=4)) 61 | 62 | def post(self, matched): 63 | payload = dict(self.request.arguments) 64 | self.write(json.dumps({ 65 | matched or 'index': payload, 66 | 'req_body': self.request.body.decode('utf-8'), 67 | 'req_headers': dict(self.request.headers.items()), 68 | }, indent=4)) 69 | 70 | 71 | class JSONEchoServer(threading.Thread): 72 | def __init__(self, lock, port, *args, **kw): 73 | self.lock = lock 74 | self.port = int(port) 75 | self._stop = threading.Event() 76 | super(JSONEchoServer, self).__init__(*args, **kw) 77 | self.daemon = True 78 | 79 | def stop(self): 80 | self._stop.set() 81 | 82 | def stopped(self): 83 | return self._stop.isSet() 84 | 85 | def setup_application(self): 86 | return tornado.web.Application([ 87 | (r"/(.*)", JSONEchoHandler), 88 | ]) 89 | 90 | def run(self): 91 | loop = tornado.ioloop.IOLoop() 92 | 93 | application = self.setup_application() 94 | application.listen(self.port) 95 | self.lock.release() 96 | loop.start() 97 | 98 | 99 | def use_tornado_server(callback): 100 | lock = threading.Lock() 101 | lock.acquire() 102 | 103 | @wraps(callback) 104 | def func(*args, **kw): 105 | port = os.getenv('TEST_PORT', get_free_tcp_port()) 106 | POTENTIAL_HTTP_PORTS.add(port) 107 | kw['port'] = port 108 | server = JSONEchoServer(lock, port) 109 | server.start() 110 | try: 111 | lock.acquire() 112 | callback(*args, **kw) 113 | finally: 114 | lock.release() 115 | server.stop() 116 | if port in POTENTIAL_HTTP_PORTS: 117 | POTENTIAL_HTTP_PORTS.remove(port) 118 | return func 119 | -------------------------------------------------------------------------------- /tests/functional/fixtures/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielfalcao/HTTPretty/f9f012711597634d40066d144a36888b3addcc46/tests/functional/fixtures/.placeholder -------------------------------------------------------------------------------- /tests/functional/fixtures/playback-1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "", 5 | "headers": { 6 | "host": "localhost:8888", 7 | "accept-encoding": "gzip, deflate, compress", 8 | "content-length": "0", 9 | "accept": "*/*", 10 | "user-agent": "python-requests/1.1.0 CPython/2.7.5 Darwin/12.5.0" 11 | }, 12 | "querystring": { 13 | "age": [ 14 | "25" 15 | ], 16 | "name": [ 17 | "Gabriel" 18 | ] 19 | }, 20 | "uri": "http://localhost:8888/foobar?name=Gabriel&age=25", 21 | "method": "GET" 22 | }, 23 | "response": { 24 | "status": 200, 25 | "body": "{\n \"foobar\": {\n \"age\": \"25\", \n \"name\": \"Gabriel\"\n }\n}", 26 | "headers": { 27 | "content-length": "73", 28 | "etag": "\"6fdccaba6542114e7d1098d22a01623dc2aa5761\"", 29 | "content-type": "text/html; charset=UTF-8", 30 | "server": "TornadoServer/2.4" 31 | } 32 | } 33 | }, 34 | { 35 | "request": { 36 | "body": "{\"test\": \"123\"}", 37 | "headers": { 38 | "host": "localhost:8888", 39 | "accept-encoding": "gzip, deflate, compress", 40 | "content-length": "15", 41 | "accept": "*/*", 42 | "user-agent": "python-requests/1.1.0 CPython/2.7.5 Darwin/12.5.0" 43 | }, 44 | "querystring": {}, 45 | "uri": "http://localhost:8888/foobar", 46 | "method": "POST" 47 | }, 48 | "response": { 49 | "status": 200, 50 | "body": "{\n \"foobar\": {}\n}", 51 | "headers": { 52 | "content-length": "20", 53 | "content-type": "text/html; charset=UTF-8", 54 | "server": "TornadoServer/2.4" 55 | } 56 | } 57 | } 58 | ] -------------------------------------------------------------------------------- /tests/functional/test_bypass.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # Copyright (C) <2011-2021> Gabriel Falcão 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | from __future__ import unicode_literals 28 | import time 29 | import requests 30 | try: 31 | import urllib.request as urllib2 32 | except ImportError: 33 | import urllib2 34 | 35 | from .testserver import TornadoServer, TCPServer, TCPClient 36 | from .base import get_free_tcp_port 37 | from sure import expect, that_with_context 38 | 39 | import functools 40 | 41 | import httpretty 42 | from httpretty import core, HTTPretty 43 | 44 | 45 | def start_http_server(context): 46 | if httpretty.httpretty._is_enabled: 47 | allow_net_connect = httpretty.httpretty.allow_net_connect 48 | else: 49 | allow_net_connect = True 50 | httpretty.disable() 51 | context.http_port = get_free_tcp_port() 52 | context.server = TornadoServer(context.http_port) 53 | context.server.start() 54 | ready = False 55 | timeout = 2 56 | started_at = time.time() 57 | while not ready: 58 | httpretty.disable() 59 | time.sleep(.1) 60 | try: 61 | requests.get('http://localhost:{}/'.format(context.http_port)) 62 | ready = True 63 | except Exception: 64 | if time.time() - started_at >= timeout: 65 | break 66 | 67 | httpretty.enable(allow_net_connect=allow_net_connect) 68 | 69 | 70 | def stop_http_server(context): 71 | context.server.stop() 72 | httpretty.enable() 73 | 74 | 75 | def start_tcp_server(context): 76 | context.tcp_port = get_free_tcp_port() 77 | context.server = TCPServer(context.tcp_port) 78 | context.server.start() 79 | context.client = TCPClient(context.tcp_port) 80 | httpretty.enable() 81 | 82 | 83 | def stop_tcp_server(context): 84 | context.server.stop() 85 | context.client.close() 86 | httpretty.enable() 87 | 88 | 89 | @httpretty.activate 90 | @that_with_context(start_http_server, stop_http_server) 91 | def test_httpretty_bypasses_when_disabled(context): 92 | "httpretty should bypass all requests by disabling it" 93 | 94 | httpretty.register_uri( 95 | httpretty.GET, "http://localhost:{}/go-for-bubbles/".format(context.http_port), 96 | body="glub glub") 97 | 98 | httpretty.disable() 99 | 100 | fd = urllib2.urlopen('http://localhost:{}/go-for-bubbles/'.format(context.http_port)) 101 | got1 = fd.read() 102 | fd.close() 103 | 104 | expect(got1).to.equal( 105 | b'. o O 0 O o . o O 0 O o . o O 0 O o . o O 0 O o . o O 0 O o .') 106 | 107 | fd = urllib2.urlopen('http://localhost:{}/come-again/'.format(context.http_port)) 108 | got2 = fd.read() 109 | fd.close() 110 | 111 | expect(got2).to.equal(b'<- HELLO WORLD ->') 112 | 113 | httpretty.enable() 114 | 115 | fd = urllib2.urlopen('http://localhost:{}/go-for-bubbles/'.format(context.http_port)) 116 | got3 = fd.read() 117 | fd.close() 118 | 119 | expect(got3).to.equal(b'glub glub') 120 | core.POTENTIAL_HTTP_PORTS.remove(context.http_port) 121 | 122 | 123 | @httpretty.activate(verbose=True) 124 | @that_with_context(start_http_server, stop_http_server) 125 | def test_httpretty_bypasses_a_unregistered_request(context): 126 | "httpretty should bypass a unregistered request by disabling it" 127 | 128 | httpretty.register_uri( 129 | httpretty.GET, "http://localhost:{}/go-for-bubbles/".format(context.http_port), 130 | body="glub glub") 131 | 132 | fd = urllib2.urlopen('http://localhost:{}/go-for-bubbles/'.format(context.http_port)) 133 | got1 = fd.read() 134 | fd.close() 135 | 136 | expect(got1).to.equal(b'glub glub') 137 | 138 | fd = urllib2.urlopen('http://localhost:{}/come-again/'.format(context.http_port)) 139 | got2 = fd.read() 140 | fd.close() 141 | 142 | expect(got2).to.equal(b'<- HELLO WORLD ->') 143 | core.POTENTIAL_HTTP_PORTS.remove(context.http_port) 144 | 145 | 146 | @httpretty.activate(verbose=True) 147 | @that_with_context(start_tcp_server, stop_tcp_server) 148 | def test_using_httpretty_with_other_tcp_protocols(context): 149 | "httpretty should work even when testing code that also use other TCP-based protocols" 150 | 151 | httpretty.register_uri( 152 | httpretty.GET, "http://falcao.it/foo/", 153 | body="BAR") 154 | 155 | fd = urllib2.urlopen('http://falcao.it/foo/') 156 | got1 = fd.read() 157 | fd.close() 158 | 159 | expect(got1).to.equal(b'BAR') 160 | 161 | expect(context.client.send("foobar")).to.equal(b"RECEIVED: foobar") 162 | 163 | 164 | @httpretty.activate(allow_net_connect=False) 165 | @that_with_context(start_http_server, stop_http_server) 166 | def test_disallow_net_connect_1(context, verbose=True): 167 | """ 168 | When allow_net_connect = False, a request that otherwise 169 | would have worked results in UnmockedError. 170 | """ 171 | httpretty.register_uri(httpretty.GET, "http://falcao.it/foo/", 172 | body="BAR") 173 | 174 | def foo(): 175 | fd = None 176 | try: 177 | fd = urllib2.urlopen('http://localhost:{}/go-for-bubbles/'.format(context.http_port)) 178 | finally: 179 | if fd: 180 | fd.close() 181 | 182 | foo.should.throw(httpretty.UnmockedError) 183 | 184 | 185 | @httpretty.activate(allow_net_connect=False) 186 | def test_disallow_net_connect_2(): 187 | """ 188 | When allow_net_connect = False, a request that would have 189 | failed results in UnmockedError. 190 | """ 191 | 192 | def foo(): 193 | fd = None 194 | try: 195 | fd = urllib2.urlopen('http://example.com/nonsense') 196 | finally: 197 | if fd: 198 | fd.close() 199 | 200 | foo.should.throw(httpretty.UnmockedError) 201 | 202 | 203 | @httpretty.activate(allow_net_connect=False) 204 | def test_disallow_net_connect_3(): 205 | "When allow_net_connect = False, mocked requests still work correctly." 206 | 207 | httpretty.register_uri(httpretty.GET, "http://falcao.it/foo/", 208 | body="BAR") 209 | fd = urllib2.urlopen('http://falcao.it/foo/') 210 | got1 = fd.read() 211 | fd.close() 212 | expect(got1).to.equal(b'BAR') 213 | -------------------------------------------------------------------------------- /tests/functional/test_debug.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) <2011-2021> Gabriel Falcão 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | import socket 25 | from unittest import skip 26 | from sure import scenario, expect 27 | from httpretty import httprettified 28 | 29 | 30 | def create_socket(context): 31 | context.sock = socket.socket( 32 | socket.AF_INET, 33 | socket.SOCK_STREAM, 34 | socket.IPPROTO_TCP, 35 | ) 36 | context.sock.is_http = True 37 | 38 | 39 | @skip('not currently supported') 40 | @httprettified 41 | @scenario(create_socket) 42 | def test_httpretty_debugs_socket_send(context): 43 | "HTTPretty should forward_and_trace socket.send" 44 | 45 | expect(context.sock.send).when.called_with(b'data').to.throw( 46 | "not connected" 47 | ) 48 | 49 | 50 | @skip('not currently supported') 51 | @httprettified 52 | @scenario(create_socket) 53 | def test_httpretty_debugs_socket_sendto(context): 54 | "HTTPretty should forward_and_trace socket.sendto" 55 | 56 | expect(context.sock.sendto).when.called.to.throw( 57 | "not connected" 58 | ) 59 | 60 | 61 | @skip('not currently supported') 62 | @httprettified 63 | @scenario(create_socket) 64 | def test_httpretty_debugs_socket_recvfrom(context): 65 | "HTTPretty should forward_and_trace socket.recvfrom" 66 | 67 | expect(context.sock.recvfrom).when.called.to.throw( 68 | "not connected" 69 | ) 70 | 71 | 72 | @skip('not currently supported') 73 | @httprettified 74 | @scenario(create_socket) 75 | def test_httpretty_debugs_socket_recv_into(context): 76 | "HTTPretty should forward_and_trace socket.recv_into" 77 | buf = bytearray() 78 | expect(context.sock.recv_into).when.called_with(buf).to.throw( 79 | "not connected" 80 | ) 81 | 82 | 83 | @skip('not currently supported') 84 | @httprettified 85 | @scenario(create_socket) 86 | def test_httpretty_debugs_socket_recvfrom_into(context): 87 | "HTTPretty should forward_and_trace socket.recvfrom_into" 88 | 89 | expect(context.sock.recvfrom_into).when.called.to.throw( 90 | "not connected" 91 | ) 92 | -------------------------------------------------------------------------------- /tests/functional/test_decorator.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from unittest import TestCase 3 | from sure import expect 4 | from httpretty import httprettified, HTTPretty 5 | 6 | try: 7 | import urllib.request as urllib2 8 | except ImportError: 9 | import urllib2 10 | 11 | 12 | @httprettified 13 | def test_decor(): 14 | HTTPretty.register_uri( 15 | HTTPretty.GET, "http://localhost/", 16 | body="glub glub") 17 | 18 | fd = urllib2.urlopen('http://localhost/') 19 | got1 = fd.read() 20 | fd.close() 21 | 22 | expect(got1).to.equal(b'glub glub') 23 | 24 | 25 | @httprettified 26 | class DecoratedNonUnitTest(object): 27 | 28 | def test_fail(self): 29 | raise AssertionError('Tests in this class should not ' 30 | 'be executed by the test runner.') 31 | 32 | def test_decorated(self): 33 | HTTPretty.register_uri( 34 | HTTPretty.GET, "http://localhost/", 35 | body="glub glub") 36 | 37 | fd = urllib2.urlopen('http://localhost/') 38 | got1 = fd.read() 39 | fd.close() 40 | 41 | expect(got1).to.equal(b'glub glub') 42 | 43 | 44 | class NonUnitTestTest(TestCase): 45 | """ 46 | Checks that the test methods in DecoratedNonUnitTest were decorated. 47 | """ 48 | 49 | def test_decorated(self): 50 | DecoratedNonUnitTest().test_decorated() 51 | 52 | 53 | @httprettified 54 | class ClassDecorator(TestCase): 55 | 56 | def test_decorated(self): 57 | HTTPretty.register_uri( 58 | HTTPretty.GET, "http://localhost/", 59 | body="glub glub") 60 | 61 | fd = urllib2.urlopen('http://localhost/') 62 | got1 = fd.read() 63 | fd.close() 64 | 65 | expect(got1).to.equal(b'glub glub') 66 | 67 | def test_decorated2(self): 68 | HTTPretty.register_uri( 69 | HTTPretty.GET, "http://localhost/", 70 | body="buble buble") 71 | 72 | fd = urllib2.urlopen('http://localhost/') 73 | got1 = fd.read() 74 | fd.close() 75 | 76 | expect(got1).to.equal(b'buble buble') 77 | 78 | 79 | @httprettified 80 | class ClassDecoratorWithSetUp(TestCase): 81 | 82 | def setUp(self): 83 | HTTPretty.register_uri( 84 | HTTPretty.GET, "http://localhost/", 85 | responses=[ 86 | HTTPretty.Response("glub glub"), 87 | HTTPretty.Response("buble buble"), 88 | ]) 89 | 90 | def test_decorated(self): 91 | 92 | fd = urllib2.urlopen('http://localhost/') 93 | got1 = fd.read() 94 | fd.close() 95 | 96 | expect(got1).to.equal(b'glub glub') 97 | 98 | fd = urllib2.urlopen('http://localhost/') 99 | got2 = fd.read() 100 | fd.close() 101 | 102 | expect(got2).to.equal(b'buble buble') 103 | 104 | def test_decorated2(self): 105 | 106 | fd = urllib2.urlopen('http://localhost/') 107 | got1 = fd.read() 108 | fd.close() 109 | 110 | expect(got1).to.equal(b'glub glub') 111 | 112 | fd = urllib2.urlopen('http://localhost/') 113 | got2 = fd.read() 114 | fd.close() 115 | 116 | expect(got2).to.equal(b'buble buble') 117 | -------------------------------------------------------------------------------- /tests/functional/test_fakesocket.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # Copyright (C) <2011-2021> Gabriel Falcão 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | import functools 29 | import socket 30 | 31 | import mock 32 | 33 | 34 | class FakeSocket(socket.socket): 35 | """ 36 | Just an editable socket factory 37 | It allows mock to patch readonly functions 38 | """ 39 | connect = sendall = lambda *args, **kw: None 40 | 41 | 42 | fake_socket_interupter_flag = {} 43 | 44 | 45 | def recv(flag, size): 46 | """ 47 | Two pass recv implementation 48 | 49 | This implementation will for the first time send something that is smaller than 50 | the asked size passed in argument. 51 | Any further call will just raise RuntimeError 52 | """ 53 | if 'was_here' in flag: 54 | raise RuntimeError('Already sent everything') 55 | else: 56 | flag['was_here'] = None 57 | return 'a' * (size - 1) 58 | 59 | 60 | recv = functools.partial(recv, fake_socket_interupter_flag) 61 | 62 | 63 | @mock.patch('httpretty.old_socket', new=FakeSocket) 64 | def _test_shorten_response(): 65 | u"HTTPretty shouldn't try to read from server when communication is over" 66 | from sure import expect 67 | import httpretty 68 | 69 | fakesocket = httpretty.fakesock.socket(socket.AF_INET, 70 | socket.SOCK_STREAM) 71 | with mock.patch.object(fakesocket.truesock, 'recv', recv): 72 | fakesocket.connect(('localhost', 80)) 73 | fakesocket._true_sendall('WHATEVER') 74 | expect(fakesocket.fd.read()).to.equal( 75 | 'a' * (httpretty.socket_buffer_size - 1)) 76 | -------------------------------------------------------------------------------- /tests/functional/test_httplib2.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # Copyright (C) <2011-2021> Gabriel Falcão 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | from __future__ import unicode_literals 28 | 29 | import re 30 | import httplib2 31 | from freezegun import freeze_time 32 | from sure import expect, within, miliseconds 33 | from httpretty import HTTPretty, httprettified 34 | from httpretty.core import decode_utf8 35 | 36 | 37 | @httprettified 38 | @within(two=miliseconds) 39 | def test_httpretty_should_mock_a_simple_get_with_httplib2_read(now): 40 | "HTTPretty should mock a simple GET with httplib2.context.http" 41 | 42 | HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", 43 | body="Find the best daily deals") 44 | 45 | _, got = httplib2.Http().request('http://yipit.com', 'GET') 46 | expect(got).to.equal(b'Find the best daily deals') 47 | 48 | expect(HTTPretty.last_request.method).to.equal('GET') 49 | expect(HTTPretty.last_request.path).to.equal('/') 50 | 51 | 52 | @httprettified 53 | @within(two=miliseconds) 54 | def test_httpretty_provides_easy_access_to_querystrings(now): 55 | "HTTPretty should provide an easy access to the querystring" 56 | 57 | HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", 58 | body="Find the best daily deals") 59 | 60 | httplib2.Http().request('http://yipit.com?foo=bar&foo=baz&chuck=norris', 'GET') 61 | expect(HTTPretty.last_request.querystring).to.equal({ 62 | 'foo': ['bar', 'baz'], 63 | 'chuck': ['norris'], 64 | }) 65 | 66 | 67 | @httprettified 68 | @freeze_time("2013-10-04 04:20:00") 69 | def test_httpretty_should_mock_headers_httplib2(): 70 | "HTTPretty should mock basic headers with httplib2" 71 | 72 | HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", 73 | body="this is supposed to be the response", 74 | status=201) 75 | 76 | headers, _ = httplib2.Http().request('http://github.com', 'GET') 77 | expect(headers['status']).to.equal('201') 78 | expect(dict(headers)).to.equal({ 79 | 'content-type': 'text/plain; charset=utf-8', 80 | 'connection': 'close', 81 | 'content-length': '35', 82 | 'status': '201', 83 | 'server': 'Python/HTTPretty', 84 | 'date': 'Fri, 04 Oct 2013 04:20:00 GMT', 85 | }) 86 | 87 | 88 | @httprettified 89 | @freeze_time("2013-10-04 04:20:00") 90 | def test_httpretty_should_allow_adding_and_overwritting_httplib2(): 91 | "HTTPretty should allow adding and overwritting headers with httplib2" 92 | 93 | HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", 94 | body="this is supposed to be the response", 95 | adding_headers={ 96 | 'Server': 'Apache', 97 | 'Content-Length': '27', 98 | 'Content-Type': 'application/json', 99 | }) 100 | 101 | headers, _ = httplib2.Http().request('http://github.com/foo', 'GET') 102 | 103 | expect(dict(headers)).to.equal({ 104 | 'content-type': 'application/json', 105 | 'content-location': 'http://github.com/foo', 106 | 'connection': 'close', 107 | 'content-length': '27', 108 | 'status': '200', 109 | 'server': 'Apache', 110 | 'date': 'Fri, 04 Oct 2013 04:20:00 GMT', 111 | }) 112 | 113 | 114 | @httprettified 115 | @within(two=miliseconds) 116 | def test_httpretty_should_allow_forcing_headers_httplib2(now): 117 | "HTTPretty should allow forcing headers with httplib2" 118 | 119 | HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", 120 | body="this is supposed to be the response", 121 | forcing_headers={ 122 | 'Content-Type': 'application/xml', 123 | }) 124 | 125 | headers, _ = httplib2.Http().request('http://github.com/foo', 'GET') 126 | 127 | expect(dict(headers)).to.equal({ 128 | 'content-location': 'http://github.com/foo', # httplib2 FORCES 129 | # content-location 130 | # even if the 131 | # server does not 132 | # provide it 133 | 'content-type': 'application/xml', 134 | 'status': '200', # httplib2 also ALWAYS put status on headers 135 | }) 136 | 137 | 138 | @httprettified 139 | @freeze_time("2013-10-04 04:20:00") 140 | def test_httpretty_should_allow_adding_and_overwritting_by_kwargs_u2(): 141 | "HTTPretty should allow adding and overwritting headers by keyword args " \ 142 | "with httplib2" 143 | 144 | HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", 145 | body="this is supposed to be the response", 146 | server='Apache', 147 | content_length='27', 148 | content_type='application/json') 149 | 150 | headers, _ = httplib2.Http().request('http://github.com/foo', 'GET') 151 | 152 | expect(dict(headers)).to.equal({ 153 | 'content-type': 'application/json', 154 | 'content-location': 'http://github.com/foo', # httplib2 FORCES 155 | # content-location 156 | # even if the 157 | # server does not 158 | # provide it 159 | 'connection': 'close', 160 | 'content-length': '27', 161 | 'status': '200', 162 | 'server': 'Apache', 163 | 'date': 'Fri, 04 Oct 2013 04:20:00 GMT', 164 | }) 165 | 166 | 167 | @httprettified 168 | @within(two=miliseconds) 169 | def test_rotating_responses_with_httplib2(now): 170 | "HTTPretty should support rotating responses with httplib2" 171 | 172 | HTTPretty.register_uri( 173 | HTTPretty.GET, "https://api.yahoo.com/test", 174 | responses=[ 175 | HTTPretty.Response(body="first response", status=201), 176 | HTTPretty.Response(body='second and last response', status=202), 177 | ]) 178 | 179 | headers1, body1 = httplib2.Http().request( 180 | 'https://api.yahoo.com/test', 'GET') 181 | 182 | expect(headers1['status']).to.equal('201') 183 | expect(body1).to.equal(b'first response') 184 | 185 | headers2, body2 = httplib2.Http().request( 186 | 'https://api.yahoo.com/test', 'GET') 187 | 188 | expect(headers2['status']).to.equal('202') 189 | expect(body2).to.equal(b'second and last response') 190 | 191 | headers3, body3 = httplib2.Http().request( 192 | 'https://api.yahoo.com/test', 'GET') 193 | 194 | expect(headers3['status']).to.equal('202') 195 | expect(body3).to.equal(b'second and last response') 196 | 197 | 198 | @httprettified 199 | @within(two=miliseconds) 200 | def test_can_inspect_last_request(now): 201 | "HTTPretty.last_request is a mimetools.Message request from last match" 202 | 203 | HTTPretty.register_uri(HTTPretty.POST, "http://api.github.com/", 204 | body='{"repositories": ["HTTPretty", "lettuce"]}') 205 | 206 | headers, body = httplib2.Http().request( 207 | 'http://api.github.com', 'POST', 208 | body='{"username": "gabrielfalcao"}', 209 | headers={ 210 | 'content-type': 'text/json', 211 | }, 212 | ) 213 | 214 | expect(HTTPretty.last_request.method).to.equal('POST') 215 | expect(HTTPretty.last_request.body).to.equal( 216 | b'{"username": "gabrielfalcao"}', 217 | ) 218 | expect(HTTPretty.last_request.headers['content-type']).to.equal( 219 | 'text/json', 220 | ) 221 | expect(body).to.equal(b'{"repositories": ["HTTPretty", "lettuce"]}') 222 | 223 | 224 | @httprettified 225 | @within(two=miliseconds) 226 | def test_can_inspect_last_request_with_ssl(now): 227 | "HTTPretty.last_request is recorded even when mocking 'https' (SSL)" 228 | 229 | HTTPretty.register_uri(HTTPretty.POST, "https://secure.github.com/", 230 | body='{"repositories": ["HTTPretty", "lettuce"]}') 231 | 232 | headers, body = httplib2.Http().request( 233 | 'https://secure.github.com', 'POST', 234 | body='{"username": "gabrielfalcao"}', 235 | headers={ 236 | 'content-type': 'text/json', 237 | }, 238 | ) 239 | 240 | expect(HTTPretty.last_request.method).to.equal('POST') 241 | expect(HTTPretty.last_request.body).to.equal( 242 | b'{"username": "gabrielfalcao"}', 243 | ) 244 | expect(HTTPretty.last_request.headers['content-type']).to.equal( 245 | 'text/json', 246 | ) 247 | expect(body).to.equal(b'{"repositories": ["HTTPretty", "lettuce"]}') 248 | 249 | 250 | @httprettified 251 | @within(two=miliseconds) 252 | def test_httpretty_ignores_querystrings_from_registered_uri(now): 253 | "Registering URIs with query string cause them to be ignored" 254 | 255 | HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/?id=123", 256 | body="Find the best daily deals") 257 | 258 | _, got = httplib2.Http().request('http://yipit.com/?id=123', 'GET') 259 | 260 | expect(got).to.equal(b'Find the best daily deals') 261 | expect(HTTPretty.last_request.method).to.equal('GET') 262 | expect(HTTPretty.last_request.path).to.equal('/?id=123') 263 | 264 | 265 | @httprettified 266 | @within(two=miliseconds) 267 | def test_callback_response(now): 268 | ("HTTPretty should call a callback function to be set as the body with" 269 | " httplib2") 270 | 271 | def request_callback(request, uri, headers): 272 | return [200, headers, "The {} response from {}".format(decode_utf8(request.method), uri)] 273 | 274 | HTTPretty.register_uri( 275 | HTTPretty.GET, "https://api.yahoo.com/test", 276 | body=request_callback) 277 | 278 | headers1, body1 = httplib2.Http().request( 279 | 'https://api.yahoo.com/test', 'GET') 280 | 281 | expect(body1).to.equal(b"The GET response from https://api.yahoo.com/test") 282 | 283 | HTTPretty.register_uri( 284 | HTTPretty.POST, "https://api.yahoo.com/test_post", 285 | body=request_callback) 286 | 287 | headers2, body2 = httplib2.Http().request( 288 | 'https://api.yahoo.com/test_post', 'POST') 289 | 290 | expect(body2).to.equal(b"The POST response from https://api.yahoo.com/test_post") 291 | 292 | 293 | @httprettified 294 | def test_httpretty_should_allow_registering_regexes(): 295 | "HTTPretty should allow registering regexes with httplib2" 296 | 297 | HTTPretty.register_uri( 298 | HTTPretty.GET, 299 | 'http://api.yipit.com/v1/deal;brand=gap', 300 | body="Found brand", 301 | ) 302 | 303 | response, body = httplib2.Http().request('http://api.yipit.com/v1/deal;brand=gap', 'GET') 304 | expect(body).to.equal(b'Found brand') 305 | expect(HTTPretty.last_request.method).to.equal('GET') 306 | expect(HTTPretty.last_request.path).to.equal('/v1/deal;brand=gap') 307 | -------------------------------------------------------------------------------- /tests/functional/test_passthrough.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) <2011-2021> Gabriel Falcão 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | import requests 25 | import httpretty 26 | 27 | from sure import expect 28 | 29 | 30 | def http(): 31 | sess = requests.Session() 32 | adapter = requests.adapters.HTTPAdapter(pool_connections=1, pool_maxsize=1) 33 | sess.mount('http://', adapter) 34 | sess.mount('https://', adapter) 35 | return sess 36 | 37 | 38 | def test_http_passthrough(): 39 | url = 'http://httpbin.org/status/200' 40 | response1 = http().get(url) 41 | 42 | response1 = http().get(url) 43 | 44 | httpretty.enable(allow_net_connect=False, verbose=True) 45 | httpretty.register_uri(httpretty.GET, 'http://google.com/', body="Not Google") 46 | httpretty.register_uri(httpretty.GET, url, body="mocked") 47 | 48 | response2 = http().get('http://google.com/') 49 | expect(response2.content).to.equal(b'Not Google') 50 | 51 | response3 = http().get(url) 52 | response3.content.should.equal(b"mocked") 53 | 54 | httpretty.disable() 55 | 56 | response4 = http().get(url) 57 | (response4.content).should.equal(response1.content) 58 | 59 | 60 | def test_https_passthrough(): 61 | url = 'https://httpbin.org/status/200' 62 | 63 | response1 = http().get(url) 64 | 65 | httpretty.enable(allow_net_connect=False, verbose=True) 66 | httpretty.register_uri(httpretty.GET, 'https://google.com/', body="Not Google") 67 | httpretty.register_uri(httpretty.GET, url, body="mocked") 68 | 69 | response2 = http().get('https://google.com/') 70 | expect(response2.content).to.equal(b'Not Google') 71 | 72 | response3 = http().get(url) 73 | (response3.text).should.equal('mocked') 74 | 75 | httpretty.disable() 76 | 77 | response4 = http().get(url) 78 | (response4.content).should.equal(response1.content) 79 | -------------------------------------------------------------------------------- /tests/functional/test_urllib2.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # Copyright (C) <2011-2021> Gabriel Falcão 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | from __future__ import unicode_literals 28 | 29 | import re 30 | try: 31 | from urllib.request import urlopen 32 | import urllib.request as urllib2 33 | except ImportError: 34 | import urllib2 35 | urlopen = urllib2.urlopen 36 | 37 | from freezegun import freeze_time 38 | from sure import within, miliseconds 39 | from httpretty import HTTPretty, httprettified 40 | from httpretty.core import decode_utf8 41 | 42 | 43 | @httprettified 44 | @within(two=miliseconds) 45 | def test_httpretty_should_mock_a_simple_get_with_urllib2_read(): 46 | "HTTPretty should mock a simple GET with urllib2.read()" 47 | 48 | HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", 49 | body="Find the best daily deals") 50 | 51 | fd = urlopen('http://yipit.com') 52 | got = fd.read() 53 | fd.close() 54 | 55 | got.should.equal(b'Find the best daily deals') 56 | 57 | 58 | @httprettified 59 | @within(two=miliseconds) 60 | def test_httpretty_provides_easy_access_to_querystrings(now): 61 | "HTTPretty should provide an easy access to the querystring" 62 | 63 | HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", 64 | body="Find the best daily deals") 65 | 66 | fd = urllib2.urlopen('http://yipit.com/?foo=bar&foo=baz&chuck=norris') 67 | fd.read() 68 | fd.close() 69 | 70 | HTTPretty.last_request.querystring.should.equal({ 71 | 'foo': ['bar', 'baz'], 72 | 'chuck': ['norris'], 73 | }) 74 | 75 | 76 | @httprettified 77 | @freeze_time("2013-10-04 04:20:00") 78 | def test_httpretty_should_mock_headers_urllib2(): 79 | "HTTPretty should mock basic headers with urllib2" 80 | 81 | HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", 82 | body="this is supposed to be the response", 83 | status=201) 84 | 85 | request = urlopen('http://github.com') 86 | 87 | headers = dict(request.headers) 88 | request.close() 89 | 90 | request.code.should.equal(201) 91 | headers.should.equal({ 92 | 'content-type': 'text/plain; charset=utf-8', 93 | 'connection': 'close', 94 | 'content-length': '35', 95 | 'status': '201', 96 | 'server': 'Python/HTTPretty', 97 | 'date': 'Fri, 04 Oct 2013 04:20:00 GMT', 98 | }) 99 | 100 | 101 | @httprettified 102 | @freeze_time("2013-10-04 04:20:00") 103 | def test_httpretty_should_allow_adding_and_overwritting_urllib2(): 104 | "HTTPretty should allow adding and overwritting headers with urllib2" 105 | 106 | HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", 107 | body="this is supposed to be the response", 108 | adding_headers={ 109 | 'Server': 'Apache', 110 | 'Content-Length': '27', 111 | 'Content-Type': 'application/json', 112 | }) 113 | 114 | request = urlopen('http://github.com') 115 | headers = dict(request.headers) 116 | request.close() 117 | 118 | request.code.should.equal(200) 119 | headers.should.equal({ 120 | 'content-type': 'application/json', 121 | 'connection': 'close', 122 | 'content-length': '27', 123 | 'status': '200', 124 | 'server': 'Apache', 125 | 'date': 'Fri, 04 Oct 2013 04:20:00 GMT', 126 | }) 127 | 128 | 129 | @httprettified 130 | @within(two=miliseconds) 131 | def test_httpretty_should_allow_forcing_headers_urllib2(): 132 | "HTTPretty should allow forcing headers with urllib2" 133 | 134 | HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", 135 | body="this is supposed to be the response", 136 | forcing_headers={ 137 | 'Content-Type': 'application/xml', 138 | 'Content-Length': '35a', 139 | }) 140 | 141 | request = urlopen('http://github.com') 142 | headers = dict(request.headers) 143 | request.close() 144 | 145 | headers.should.equal({ 146 | 'content-type': 'application/xml', 147 | 'content-length': '35a', 148 | }) 149 | 150 | 151 | @httprettified 152 | @freeze_time("2013-10-04 04:20:00") 153 | def test_httpretty_should_allow_adding_and_overwritting_by_kwargs_u2(): 154 | ("HTTPretty should allow adding and overwritting headers by " 155 | "keyword args with urllib2") 156 | 157 | body = "this is supposed to be the response, indeed" 158 | HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", 159 | body=body, 160 | server='Apache', 161 | content_length=len(body), 162 | content_type='application/json') 163 | 164 | request = urlopen('http://github.com') 165 | headers = dict(request.headers) 166 | request.close() 167 | 168 | request.code.should.equal(200) 169 | headers.should.equal({ 170 | 'content-type': 'application/json', 171 | 'connection': 'close', 172 | 'content-length': str(len(body)), 173 | 'status': '200', 174 | 'server': 'Apache', 175 | 'date': 'Fri, 04 Oct 2013 04:20:00 GMT', 176 | }) 177 | 178 | 179 | @httprettified 180 | @within(two=miliseconds) 181 | def test_httpretty_should_support_a_list_of_successive_responses_urllib2(now): 182 | ("HTTPretty should support adding a list of successive " 183 | "responses with urllib2") 184 | 185 | HTTPretty.register_uri( 186 | HTTPretty.GET, "https://api.yahoo.com/test", 187 | responses=[ 188 | HTTPretty.Response(body="first response", status=201), 189 | HTTPretty.Response(body='second and last response', status=202), 190 | ]) 191 | 192 | request1 = urlopen('https://api.yahoo.com/test') 193 | body1 = request1.read() 194 | request1.close() 195 | 196 | request1.code.should.equal(201) 197 | body1.should.equal(b'first response') 198 | 199 | request2 = urlopen('https://api.yahoo.com/test') 200 | body2 = request2.read() 201 | request2.close() 202 | request2.code.should.equal(202) 203 | body2.should.equal(b'second and last response') 204 | 205 | request3 = urlopen('https://api.yahoo.com/test') 206 | body3 = request3.read() 207 | request3.close() 208 | request3.code.should.equal(202) 209 | body3.should.equal(b'second and last response') 210 | 211 | 212 | @httprettified 213 | @within(two=miliseconds) 214 | def test_can_inspect_last_request(now): 215 | "HTTPretty.last_request is a mimetools.Message request from last match" 216 | 217 | HTTPretty.register_uri(HTTPretty.POST, "http://api.github.com/", 218 | body='{"repositories": ["HTTPretty", "lettuce"]}') 219 | 220 | request = urllib2.Request( 221 | 'http://api.github.com', 222 | b'{"username": "gabrielfalcao"}', 223 | { 224 | 'content-type': 'text/json', 225 | }, 226 | ) 227 | fd = urlopen(request) 228 | got = fd.read() 229 | fd.close() 230 | 231 | HTTPretty.last_request.method.should.equal('POST') 232 | HTTPretty.last_request.body.should.equal( 233 | b'{"username": "gabrielfalcao"}', 234 | ) 235 | HTTPretty.last_request.headers['content-type'].should.equal( 236 | 'text/json', 237 | ) 238 | got.should.equal(b'{"repositories": ["HTTPretty", "lettuce"]}') 239 | 240 | 241 | @httprettified 242 | @within(two=miliseconds) 243 | def test_can_inspect_last_request_with_ssl(now): 244 | "HTTPretty.last_request is recorded even when mocking 'https' (SSL)" 245 | 246 | HTTPretty.register_uri(HTTPretty.POST, "https://secure.github.com/", 247 | body='{"repositories": ["HTTPretty", "lettuce"]}') 248 | 249 | request = urllib2.Request( 250 | 'https://secure.github.com', 251 | b'{"username": "gabrielfalcao"}', 252 | { 253 | 'content-type': 'text/json', 254 | }, 255 | ) 256 | fd = urlopen(request) 257 | got = fd.read() 258 | fd.close() 259 | 260 | HTTPretty.last_request.method.should.equal('POST') 261 | HTTPretty.last_request.body.should.equal( 262 | b'{"username": "gabrielfalcao"}', 263 | ) 264 | HTTPretty.last_request.headers['content-type'].should.equal( 265 | 'text/json', 266 | ) 267 | got.should.equal(b'{"repositories": ["HTTPretty", "lettuce"]}') 268 | 269 | 270 | @httprettified 271 | @within(two=miliseconds) 272 | def test_httpretty_ignores_querystrings_from_registered_uri(): 273 | "HTTPretty should mock a simple GET with urllib2.read()" 274 | 275 | HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/?id=123", 276 | body="Find the best daily deals") 277 | 278 | fd = urlopen('http://yipit.com/?id=123') 279 | got = fd.read() 280 | fd.close() 281 | 282 | got.should.equal(b'Find the best daily deals') 283 | HTTPretty.last_request.method.should.equal('GET') 284 | HTTPretty.last_request.path.should.equal('/?id=123') 285 | 286 | 287 | @httprettified 288 | @within(two=miliseconds) 289 | def test_callback_response(now): 290 | ("HTTPretty should call a callback function to be set as the body with" 291 | " urllib2") 292 | 293 | def request_callback(request, uri, headers): 294 | return [200, headers, "The {} response from {}".format(decode_utf8(request.method), uri)] 295 | 296 | HTTPretty.register_uri( 297 | HTTPretty.GET, "https://api.yahoo.com/test", 298 | body=request_callback) 299 | 300 | fd = urllib2.urlopen('https://api.yahoo.com/test') 301 | got = fd.read() 302 | fd.close() 303 | 304 | got.should.equal(b"The GET response from https://api.yahoo.com/test") 305 | 306 | HTTPretty.register_uri( 307 | HTTPretty.POST, "https://api.yahoo.com/test_post", 308 | body=request_callback) 309 | 310 | request = urllib2.Request( 311 | "https://api.yahoo.com/test_post", 312 | b'{"username": "gabrielfalcao"}', 313 | { 314 | 'content-type': 'text/json', 315 | }, 316 | ) 317 | fd = urllib2.urlopen(request) 318 | got = fd.read() 319 | fd.close() 320 | 321 | got.should.equal(b"The POST response from https://api.yahoo.com/test_post") 322 | 323 | 324 | @httprettified 325 | def test_httpretty_should_allow_registering_regexes(): 326 | "HTTPretty should allow registering regexes with urllib2" 327 | 328 | HTTPretty.register_uri( 329 | HTTPretty.GET, 330 | re.compile(r"https://api.yipit.com/v1/deal;brand=(?P\w+)"), 331 | body="Found brand", 332 | ) 333 | 334 | request = urllib2.Request( 335 | "https://api.yipit.com/v1/deal;brand=GAP", 336 | ) 337 | fd = urllib2.urlopen(request) 338 | got = fd.read() 339 | fd.close() 340 | 341 | got.should.equal(b"Found brand") 342 | -------------------------------------------------------------------------------- /tests/functional/testserver.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # 5 | # Copyright (C) <2011-2021> Gabriel Falcão 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | from __future__ import unicode_literals 28 | 29 | import os 30 | import time 31 | import socket 32 | 33 | from tornado.web import Application 34 | from tornado.web import RequestHandler 35 | from tornado.httpserver import HTTPServer 36 | from tornado.ioloop import IOLoop 37 | from httpretty import HTTPretty 38 | from httpretty.core import old_socket as true_socket 39 | from multiprocessing import Process 40 | 41 | 42 | def utf8(s): 43 | if isinstance(s, str): 44 | s = s.encode('utf-8') 45 | 46 | return bytes(s) 47 | 48 | 49 | class BubblesHandler(RequestHandler): 50 | def get(self): 51 | self.write(". o O 0 O o . o O 0 O o . o O 0 O o . o O 0 O o . o O 0 O o .") 52 | 53 | 54 | class ComeHandler(RequestHandler): 55 | def get(self): 56 | self.write("<- HELLO WORLD ->") 57 | 58 | 59 | def subprocess_server_tornado(app, port, data={}): 60 | from httpretty import HTTPretty 61 | HTTPretty.disable() 62 | 63 | http = HTTPServer(app) 64 | HTTPretty.disable() 65 | 66 | http.listen(int(port)) 67 | IOLoop.instance().start() 68 | 69 | 70 | class TornadoServer(object): 71 | is_running = False 72 | 73 | def __init__(self, port): 74 | self.port = int(port) 75 | self.process = None 76 | 77 | @classmethod 78 | def get_handlers(cls): 79 | return Application([ 80 | (r"/go-for-bubbles/?", BubblesHandler), 81 | (r"/come-again/?", ComeHandler), 82 | ]) 83 | 84 | def start(self): 85 | 86 | app = self.get_handlers() 87 | 88 | data = {} 89 | args = (app, self.port, data) 90 | HTTPretty.disable() 91 | self.process = Process(target=subprocess_server_tornado, args=args) 92 | self.process.start() 93 | time.sleep(1) 94 | 95 | def stop(self): 96 | try: 97 | os.kill(self.process.pid, 9) 98 | except OSError: 99 | self.process.terminate() 100 | finally: 101 | self.is_running = False 102 | 103 | 104 | def subprocess_server_tcp(port): 105 | from httpretty import HTTPretty 106 | HTTPretty.disable() 107 | import socket 108 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 109 | s.bind(('localhost', port)) 110 | s.listen(True) 111 | conn, addr = s.accept() 112 | 113 | while True: 114 | data = conn.recv(1024) 115 | conn.send(b"RECEIVED: " + bytes(data)) 116 | 117 | conn.close() 118 | 119 | 120 | class TCPServer(object): 121 | def __init__(self, port): 122 | self.port = int(port) 123 | 124 | def start(self): 125 | HTTPretty.disable() 126 | 127 | 128 | args = [self.port] 129 | self.process = Process(target=subprocess_server_tcp, args=args) 130 | self.process.start() 131 | time.sleep(1) 132 | 133 | def stop(self): 134 | try: 135 | os.kill(self.process.pid, 9) 136 | except OSError: 137 | self.process.terminate() 138 | finally: 139 | self.is_running = False 140 | 141 | 142 | class TCPClient(object): 143 | def __init__(self, port): 144 | self.port = int(port) 145 | self.sock = true_socket(socket.AF_INET, socket.SOCK_STREAM) 146 | self.sock.connect(('localhost', self.port)) 147 | 148 | def send(self, data): 149 | if isinstance(data, str): 150 | data = data.encode('utf-8') 151 | 152 | self.sock.sendall(data) 153 | return self.sock.recv(len(data) + 11) 154 | 155 | def close(self): 156 | try: 157 | self.sock.close() 158 | except socket.error: 159 | pass # already closed 160 | 161 | def __del__(self): 162 | self.close() 163 | -------------------------------------------------------------------------------- /tests/pyopenssl/__init__.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) <2011-2021> Gabriel Falcão 5 | # 6 | # Permission is hereby granted, free of charge, to any person 7 | # obtaining a copy of this software and associated documentation 8 | # files (the "Software"), to deal in the Software without 9 | # restriction, including without limitation the rights to use, 10 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the 12 | # Software is furnished to do so, subject to the following 13 | # conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | # OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | import warnings 28 | warnings.simplefilter('ignore') 29 | -------------------------------------------------------------------------------- /tests/pyopenssl/test_mock.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # Copyright (C) <2011-2021> Gabriel Falcão 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | from __future__ import unicode_literals 28 | 29 | import requests 30 | 31 | from httpretty import HTTPretty, httprettified 32 | from sure import expect 33 | 34 | 35 | @httprettified 36 | def test_httpretty_overrides_when_pyopenssl_installed(): 37 | ('HTTPretty should remove PyOpenSSLs urllib3 mock if it is installed') 38 | # And HTTPretty works successfully 39 | HTTPretty.register_uri(HTTPretty.GET, "https://yipit.com/", 40 | body="Find the best daily deals") 41 | 42 | response = requests.get('https://yipit.com') 43 | expect(response.text).to.equal('Find the best daily deals') 44 | expect(HTTPretty.last_request.method).to.equal('GET') 45 | expect(HTTPretty.last_request.path).to.equal('/') 46 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) <2011-2021> Gabriel Falcão 5 | # 6 | # Permission is hereby granted, free of charge, to any person 7 | # obtaining a copy of this software and associated documentation 8 | # files (the "Software"), to deal in the Software without 9 | # restriction, including without limitation the rights to use, 10 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the 12 | # Software is furnished to do so, subject to the following 13 | # conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | # OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /tests/unit/test_core.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import errno 4 | 5 | from freezegun import freeze_time 6 | from sure import expect 7 | 8 | from httpretty.core import HTTPrettyRequest, FakeSSLSocket, fakesock, httpretty 9 | from httpretty.core import URIMatcher, URIInfo 10 | 11 | from tests.compat import Mock, patch, call 12 | 13 | 14 | class SocketErrorStub(Exception): 15 | def __init__(self, errno): 16 | self.errno = errno 17 | 18 | 19 | def test_request_stubs_internals(): 20 | ("HTTPrettyRequest is a BaseHTTPRequestHandler that replaces " 21 | "real socket file descriptors with in-memory ones") 22 | 23 | # Given a valid HTTP request header string 24 | headers = "\r\n".join([ 25 | 'POST /somewhere/?name=foo&age=bar HTTP/1.1', 26 | 'accept-encoding: identity', 27 | 'host: github.com', 28 | 'content-type: application/json', 29 | 'connection: close', 30 | 'user-agent: Python-urllib/2.7', 31 | ]) 32 | 33 | # When I create a HTTPrettyRequest with an empty body 34 | request = HTTPrettyRequest(headers, body='') 35 | 36 | # Then it should have parsed the headers 37 | dict(request.headers).should.equal({ 38 | 'accept-encoding': 'identity', 39 | 'connection': 'close', 40 | 'content-type': 'application/json', 41 | 'host': 'github.com', 42 | 'user-agent': 'Python-urllib/2.7' 43 | }) 44 | 45 | # And the `rfile` should be a io.BytesIO 46 | type_as_str = io.BytesIO.__module__ + '.' + io.BytesIO.__name__ 47 | 48 | request.should.have.property('rfile').being.a(type_as_str) 49 | 50 | # And the `wfile` should be a io.BytesIO 51 | request.should.have.property('wfile').being.a(type_as_str) 52 | 53 | # And the `method` should be available 54 | request.should.have.property('method').being.equal('POST') 55 | 56 | 57 | def test_request_parse_querystring(): 58 | ("HTTPrettyRequest#parse_querystring should parse unicode data") 59 | 60 | # Given a request string containing a unicode encoded querystring 61 | 62 | headers = "\r\n".join([ 63 | 'POST /create?name=Gabriel+Falcão HTTP/1.1', 64 | 'Content-Type: multipart/form-data', 65 | ]) 66 | 67 | # When I create a HTTPrettyRequest with an empty body 68 | request = HTTPrettyRequest(headers, body='') 69 | 70 | # Then it should have a parsed querystring 71 | request.querystring.should.equal({'name': ['Gabriel Falcão']}) 72 | 73 | 74 | def test_request_parse_body_when_it_is_application_json(): 75 | ("HTTPrettyRequest#parse_request_body recognizes the " 76 | "content-type `application/json` and parses it") 77 | 78 | # Given a request string containing a unicode encoded querystring 79 | headers = "\r\n".join([ 80 | 'POST /create HTTP/1.1', 81 | 'Content-Type: application/json', 82 | ]) 83 | # And a valid json body 84 | body = json.dumps({'name': 'Gabriel Falcão'}) 85 | 86 | # When I create a HTTPrettyRequest with that data 87 | request = HTTPrettyRequest(headers, body) 88 | 89 | # Then it should have a parsed body 90 | request.parsed_body.should.equal({'name': 'Gabriel Falcão'}) 91 | 92 | 93 | def test_request_parse_body_when_it_is_text_json(): 94 | ("HTTPrettyRequest#parse_request_body recognizes the " 95 | "content-type `text/json` and parses it") 96 | 97 | # Given a request string containing a unicode encoded querystring 98 | headers = "\r\n".join([ 99 | 'POST /create HTTP/1.1', 100 | 'Content-Type: text/json', 101 | ]) 102 | # And a valid json body 103 | body = json.dumps({'name': 'Gabriel Falcão'}) 104 | 105 | # When I create a HTTPrettyRequest with that data 106 | request = HTTPrettyRequest(headers, body) 107 | 108 | # Then it should have a parsed body 109 | request.parsed_body.should.equal({'name': 'Gabriel Falcão'}) 110 | 111 | 112 | def test_request_parse_body_when_it_is_urlencoded(): 113 | ("HTTPrettyRequest#parse_request_body recognizes the " 114 | "content-type `application/x-www-form-urlencoded` and parses it") 115 | 116 | # Given a request string containing a unicode encoded querystring 117 | headers = "\r\n".join([ 118 | 'POST /create HTTP/1.1', 119 | 'Content-Type: application/x-www-form-urlencoded', 120 | ]) 121 | # And a valid urlencoded body 122 | body = "name=Gabriel+Falcão&age=25&projects=httpretty&projects=sure&projects=lettuce" 123 | 124 | # When I create a HTTPrettyRequest with that data 125 | request = HTTPrettyRequest(headers, body) 126 | 127 | # Then it should have a parsed body 128 | request.parsed_body.should.equal({ 129 | 'name': ['Gabriel Falcão'], 130 | 'age': ["25"], 131 | 'projects': ["httpretty", "sure", "lettuce"] 132 | }) 133 | 134 | 135 | def test_request_parse_body_when_unrecognized(): 136 | ("HTTPrettyRequest#parse_request_body returns the value as " 137 | "is if the Content-Type is not recognized") 138 | 139 | # Given a request string containing a unicode encoded querystring 140 | headers = "\r\n".join([ 141 | 'POST /create HTTP/1.1', 142 | 'Content-Type: whatever', 143 | ]) 144 | # And a valid urlencoded body 145 | body = "foobar:\nlalala" 146 | 147 | # When I create a HTTPrettyRequest with that data 148 | request = HTTPrettyRequest(headers, body) 149 | 150 | # Then it should have a parsed body 151 | request.parsed_body.should.equal("foobar:\nlalala") 152 | 153 | 154 | def test_request_string_representation(): 155 | ("HTTPrettyRequest should have a forward_and_trace-friendly " 156 | "string representation") 157 | 158 | # Given a request string containing a unicode encoded querystring 159 | headers = "\r\n".join([ 160 | 'POST /create HTTP/1.1', 161 | 'Content-Type: JPEG-baby', 162 | 'Host: blog.falcao.it' 163 | ]) 164 | # And a valid urlencoded body 165 | body = "foobar:\nlalala" 166 | 167 | # When I create a HTTPrettyRequest with that data 168 | request = HTTPrettyRequest(headers, body, sock=Mock(is_https=True)) 169 | 170 | # Then its string representation should show the headers and the body 171 | str(request).should.equal('') 172 | 173 | 174 | def test_fake_ssl_socket_proxies_its_ow_socket(): 175 | ("FakeSSLSocket is a simpel wrapper around its own socket, " 176 | "which was designed to be a HTTPretty fake socket") 177 | 178 | # Given a sentinel mock object 179 | socket = Mock() 180 | 181 | # And a FakeSSLSocket wrapping it 182 | ssl = FakeSSLSocket(socket) 183 | 184 | # When I make a method call 185 | ssl.send("FOO") 186 | 187 | # Then it should bypass any method calls to its own socket 188 | socket.send.assert_called_once_with("FOO") 189 | 190 | 191 | @freeze_time("2013-10-04 04:20:00") 192 | def test_fakesock_socket_getpeercert(): 193 | ("fakesock.socket#getpeercert should return a hardcoded fake certificate") 194 | # Given a fake socket instance 195 | socket = fakesock.socket() 196 | 197 | # And that it's bound to some host 198 | socket._host = 'somewhere.com' 199 | 200 | # When I retrieve the peer certificate 201 | certificate = socket.getpeercert() 202 | 203 | # Then it should return a hardcoded value 204 | certificate.should.equal({ 205 | u'notAfter': 'Sep 29 04:20:00 GMT', 206 | u'subject': ( 207 | ((u'organizationName', u'*.somewhere.com'),), 208 | ((u'organizationalUnitName', u'Domain Control Validated'),), 209 | ((u'commonName', u'*.somewhere.com'),)), 210 | u'subjectAltName': ( 211 | (u'DNS', u'*.somewhere.com'), 212 | (u'DNS', u'somewhere.com'), 213 | (u'DNS', u'*') 214 | ) 215 | }) 216 | 217 | 218 | def test_fakesock_socket_ssl(): 219 | ("fakesock.socket#ssl should take a socket instance and return itself") 220 | # Given a fake socket instance 221 | socket = fakesock.socket() 222 | 223 | # And a stubbed socket sentinel 224 | sentinel = Mock() 225 | 226 | # When I call `ssl` on that mock 227 | result = socket.ssl(sentinel) 228 | 229 | # Then it should have returned its first argument 230 | result.should.equal(sentinel) 231 | 232 | 233 | @patch('httpretty.core.old_socket') 234 | @patch('httpretty.core.POTENTIAL_HTTP_PORTS') 235 | def test_fakesock_socket_connect_fallback(POTENTIAL_HTTP_PORTS, old_socket): 236 | ("fakesock.socket#connect should open a real connection if the " 237 | "given port is not a potential http port") 238 | # Background: the potential http ports are 80 and 443 239 | POTENTIAL_HTTP_PORTS.__contains__.side_effect = lambda other: int(other) in (80, 443) 240 | 241 | # Given a fake socket instance 242 | socket = fakesock.socket() 243 | 244 | # When it is connected to a remote server in a port that isn't 80 nor 443 245 | socket.connect(('somewhere.com', 42)) 246 | 247 | # Then it should have open a real connection in the background 248 | old_socket.return_value.connect.assert_called_once_with(('somewhere.com', 42)) 249 | 250 | 251 | @patch('httpretty.core.old_socket') 252 | def test_fakesock_socket_close(old_socket): 253 | ("fakesock.socket#close should close the actual socket in case " 254 | "it's not http and __truesock_is_connected__ is True") 255 | # Given a fake socket instance that is synthetically open 256 | socket = fakesock.socket() 257 | socket.__truesock_is_connected__ = True 258 | 259 | # When I close it 260 | socket.close() 261 | 262 | # Then its real socket should have been closed 263 | old_socket.return_value.close.assert_called_once_with() 264 | socket.__truesock_is_connected__.should.be.false 265 | 266 | 267 | @patch('httpretty.core.old_socket') 268 | def test_fakesock_socket_makefile(old_socket): 269 | ("fakesock.socket#makefile should set the mode, " 270 | "bufsize and return its mocked file descriptor") 271 | 272 | # Given a fake socket that has a mocked Entry associated with it 273 | socket = fakesock.socket() 274 | socket._entry = Mock() 275 | 276 | # When I call makefile() 277 | fd = socket.makefile(mode='rw', bufsize=512) 278 | 279 | # Then it should have returned the socket's own filedescriptor 280 | expect(fd).to.equal(socket.fd) 281 | # And the mode should have been set in the socket instance 282 | socket._mode.should.equal('rw') 283 | # And the bufsize should have been set in the socket instance 284 | socket._bufsize.should.equal(512) 285 | 286 | # And the entry should have been filled with that filedescriptor 287 | socket._entry.fill_filekind.assert_called_once_with(fd) 288 | 289 | 290 | @patch('httpretty.core.old_socket') 291 | def test_fakesock_socket_real_sendall(old_socket): 292 | ("fakesock.socket#real_sendall calls truesock#connect and bails " 293 | "out when not http") 294 | # Background: the real socket will stop returning bytes after the 295 | # first call 296 | real_socket = old_socket.return_value 297 | real_socket.recv.side_effect = [b'response from server', b""] 298 | 299 | # Given a fake socket 300 | socket = fakesock.socket() 301 | socket._address = ('1.2.3.4', 42) 302 | 303 | # When I call real_sendall with data, some args and kwargs 304 | socket.real_sendall(b"SOMEDATA", b'some extra args...', foo=b'bar') 305 | 306 | # Then it should have called sendall in the real socket 307 | real_socket.sendall.assert_called_once_with(b"SOMEDATA", b'some extra args...', foo=b'bar') 308 | 309 | # # And setblocking was never called 310 | # real_socket.setblocking.called.should.be.false 311 | 312 | # And recv was never called 313 | real_socket.recv.called.should.be.false 314 | 315 | # And the buffer is empty 316 | socket.fd.read().should.equal(b'') 317 | 318 | # And connect was never called 319 | real_socket.connect.called.should.be.false 320 | 321 | 322 | @patch('httpretty.core.old_socket') 323 | def test_fakesock_socket_real_sendall_when_http(old_socket): 324 | ("fakesock.socket#real_sendall sends data and buffers " 325 | "the response in the file descriptor") 326 | # Background: the real socket will stop returning bytes after the 327 | # first call 328 | real_socket = old_socket.return_value 329 | real_socket.recv.side_effect = [b'response from server', b""] 330 | 331 | # Given a fake socket 332 | socket = fakesock.socket() 333 | socket._address = ('1.2.3.4', 42) 334 | socket.is_http = True 335 | 336 | # When I call real_sendall with data, some args and kwargs 337 | socket.real_sendall(b"SOMEDATA", b'some extra args...', foo=b'bar') 338 | 339 | # Then it should have called sendall in the real socket 340 | real_socket.sendall.assert_called_once_with(b"SOMEDATA", b'some extra args...', foo=b'bar') 341 | 342 | # And the socket was set to blocking 343 | real_socket.setblocking.assert_called_once_with(1) 344 | 345 | # And recv was called with the bufsize 346 | real_socket.recv.assert_has_calls([ 347 | call(socket._bufsize) 348 | ]) 349 | 350 | # And the buffer should contain the data from the server 351 | socket.fd.read().should.equal(b"response from server") 352 | 353 | # And connect was called 354 | real_socket.connect.called.should.be.true 355 | 356 | 357 | @patch('httpretty.core.old_socket') 358 | @patch('httpretty.core.socket') 359 | def test_fakesock_socket_real_sendall_continue_eagain_when_http(socket, old_socket): 360 | ("fakesock.socket#real_sendall should continue if the socket error was EAGAIN") 361 | socket.error = SocketErrorStub 362 | # Background: the real socket will stop returning bytes after the 363 | # first call 364 | real_socket = old_socket.return_value 365 | real_socket.recv.side_effect = [SocketErrorStub(errno.EAGAIN), b'after error', b""] 366 | 367 | # Given a fake socket 368 | socket = fakesock.socket() 369 | socket._address = ('1.2.3.4', 42) 370 | socket.is_http = True 371 | 372 | # When I call real_sendall with data, some args and kwargs 373 | socket.real_sendall(b"SOMEDATA", b'some extra args...', foo=b'bar') 374 | 375 | # Then it should have called sendall in the real socket 376 | real_socket.sendall.assert_called_once_with(b"SOMEDATA", b'some extra args...', foo=b'bar') 377 | 378 | # And the socket was set to blocking 379 | real_socket.setblocking.assert_called_once_with(1) 380 | 381 | # And recv was called with the bufsize 382 | real_socket.recv.assert_has_calls([ 383 | call(socket._bufsize) 384 | ]) 385 | 386 | # And the buffer should contain the data from the server 387 | socket.fd.read().should.equal(b"after error") 388 | 389 | # And connect was called 390 | real_socket.connect.called.should.be.true 391 | 392 | 393 | @patch('httpretty.core.old_socket') 394 | @patch('httpretty.core.socket') 395 | def test_fakesock_socket_real_sendall_socket_error_when_http(socket, old_socket): 396 | ("fakesock.socket#real_sendall should continue if the socket error was EAGAIN") 397 | socket.error = SocketErrorStub 398 | # Background: the real socket will stop returning bytes after the 399 | # first call 400 | real_socket = old_socket.return_value 401 | real_socket.recv.side_effect = [SocketErrorStub(42), b'after error', ""] 402 | 403 | # Given a fake socket 404 | socket = fakesock.socket() 405 | socket._address = ('1.2.3.4', 42) 406 | socket.is_http = True 407 | 408 | # When I call real_sendall with data, some args and kwargs 409 | socket.real_sendall(b"SOMEDATA", b'some extra args...', foo=b'bar') 410 | 411 | # Then it should have called sendall in the real socket 412 | real_socket.sendall.assert_called_once_with(b"SOMEDATA", b'some extra args...', foo=b'bar') 413 | 414 | # And the socket was set to blocking 415 | real_socket.setblocking.assert_called_once_with(1) 416 | 417 | # And recv was called with the bufsize 418 | real_socket.recv.assert_called_once_with(socket._bufsize) 419 | 420 | # And the buffer should contain the data from the server 421 | socket.fd.read().should.equal(b"") 422 | 423 | # And connect was called 424 | real_socket.connect.called.should.be.true 425 | 426 | 427 | @patch('httpretty.core.old_socket') 428 | @patch('httpretty.core.POTENTIAL_HTTP_PORTS') 429 | def test_fakesock_socket_real_sendall_when_sending_data(POTENTIAL_HTTP_PORTS, old_socket): 430 | ("fakesock.socket#real_sendall should connect before sending data") 431 | # Background: the real socket will stop returning bytes after the 432 | # first call 433 | real_socket = old_socket.return_value 434 | real_socket.recv.side_effect = [b'response from foobar :)', b""] 435 | 436 | # And the potential http port is 4000 437 | POTENTIAL_HTTP_PORTS.__contains__.side_effect = lambda other: int(other) == 4000 438 | POTENTIAL_HTTP_PORTS.union.side_effect = lambda other: POTENTIAL_HTTP_PORTS 439 | 440 | # Given a fake socket 441 | socket = fakesock.socket() 442 | 443 | # When I call connect to a server in a port that is considered HTTP 444 | socket.connect(('foobar.com', 4000)) 445 | 446 | # And send some data 447 | socket.real_sendall(b"SOMEDATA") 448 | 449 | # Then connect should have been called 450 | real_socket.connect.assert_called_once_with(('foobar.com', 4000)) 451 | 452 | # And the socket was set to blocking 453 | real_socket.setblocking.assert_called_once_with(1) 454 | 455 | # And recv was called with the bufsize 456 | real_socket.recv.assert_has_calls([ 457 | call(socket._bufsize) 458 | ]) 459 | 460 | # And the buffer should contain the data from the server 461 | socket.fd.read().should.equal(b"response from foobar :)") 462 | 463 | 464 | @patch('httpretty.core.old_socket') 465 | @patch('httpretty.core.httpretty') 466 | @patch('httpretty.core.POTENTIAL_HTTP_PORTS') 467 | def test_fakesock_socket_sendall_with_valid_requestline(POTENTIAL_HTTP_PORTS, httpretty, old_socket): 468 | ("fakesock.socket#sendall should create an entry if it's given a valid request line") 469 | matcher = Mock(name='matcher') 470 | info = Mock(name='info') 471 | httpretty.match_uriinfo.return_value = (matcher, info) 472 | httpretty.register_uri(httpretty.GET, 'http://foo.com/foobar') 473 | 474 | # Background: 475 | # using a subclass of socket that mocks out real_sendall 476 | class MySocket(fakesock.socket): 477 | def real_sendall(self, data, *args, **kw): 478 | raise AssertionError('should never call this...') 479 | 480 | # Given an instance of that socket 481 | socket = MySocket() 482 | 483 | # And that is is considered http 484 | socket.connect(('foo.com', 80)) 485 | 486 | # When I try to send data 487 | socket.sendall(b"GET /foobar HTTP/1.1\r\nContent-Type: application/json\r\n\r\n") 488 | 489 | 490 | @patch('httpretty.core.old_socket') 491 | @patch('httpretty.core.httpretty') 492 | @patch('httpretty.core.POTENTIAL_HTTP_PORTS') 493 | def test_fakesock_socket_sendall_with_valid_requestline_2(POTENTIAL_HTTP_PORTS, httpretty, old_socket): 494 | ("fakesock.socket#sendall should create an entry if it's given a valid request line") 495 | matcher = Mock(name='matcher') 496 | info = Mock(name='info') 497 | httpretty.match_uriinfo.return_value = (matcher, info) 498 | httpretty.register_uri(httpretty.GET, 'http://foo.com/foobar') 499 | 500 | # Background: 501 | # using a subclass of socket that mocks out real_sendall 502 | class MySocket(fakesock.socket): 503 | def real_sendall(self, data, *args, **kw): 504 | raise AssertionError('should never call this...') 505 | 506 | # Given an instance of that socket 507 | socket = MySocket() 508 | 509 | # And that is is considered http 510 | socket.connect(('foo.com', 80)) 511 | 512 | # When I try to send data 513 | socket.sendall(b"GET /foobar HTTP/1.1\r\nContent-Type: application/json\r\n\r\n") 514 | 515 | 516 | @patch('httpretty.core.old_socket') 517 | def test_fakesock_socket_sendall_with_body_data_no_entry(old_socket): 518 | ("fakesock.socket#sendall should call real_sendall when not parsing headers and there is no entry") 519 | # Background: 520 | # Using a subclass of socket that mocks out real_sendall 521 | 522 | class MySocket(fakesock.socket): 523 | def real_sendall(self, data): 524 | data.should.equal(b'BLABLABLABLA') 525 | return 'cool' 526 | 527 | # Given an instance of that socket 528 | socket = MySocket() 529 | socket._entry = None 530 | 531 | # And that is is considered http 532 | socket.connect(('foo.com', 80)) 533 | 534 | # When I try to send data 535 | result = socket.sendall(b"BLABLABLABLA") 536 | 537 | # Then the result should be the return value from real_sendall 538 | result.should.equal('cool') 539 | 540 | 541 | @patch('httpretty.core.old_socket') 542 | @patch('httpretty.core.POTENTIAL_HTTP_PORTS') 543 | def test_fakesock_socket_sendall_with_body_data_with_entry(POTENTIAL_HTTP_PORTS, old_socket): 544 | ("fakesock.socket#sendall should call real_sendall when there is no entry") 545 | # Background: 546 | # Using a subclass of socket that mocks out real_sendall 547 | data_sent = [] 548 | 549 | class MySocket(fakesock.socket): 550 | def real_sendall(self, data): 551 | data_sent.append(data) 552 | 553 | # Given an instance of that socket 554 | socket = MySocket() 555 | 556 | # And that is is considered http 557 | socket.connect(('foo.com', 80)) 558 | 559 | # When I try to send data 560 | socket.sendall(b"BLABLABLABLA") 561 | 562 | # Then it should have called real_sendall 563 | data_sent.should.equal([b'BLABLABLABLA']) 564 | 565 | 566 | @patch('httpretty.core.httpretty.match_uriinfo') 567 | @patch('httpretty.core.old_socket') 568 | @patch('httpretty.core.POTENTIAL_HTTP_PORTS') 569 | def test_fakesock_socket_sendall_with_body_data_with_chunked_entry(POTENTIAL_HTTP_PORTS, old_socket, match_uriinfo): 570 | ("fakesock.socket#sendall should call real_sendall when not ") 571 | # Background: 572 | # Using a subclass of socket that mocks out real_sendall 573 | 574 | class MySocket(fakesock.socket): 575 | def real_sendall(self, data): 576 | raise AssertionError('should have never been called') 577 | 578 | matcher = Mock(name='matcher') 579 | info = Mock(name='info') 580 | httpretty.match_uriinfo.return_value = (matcher, info) 581 | 582 | # Using a mocked entry 583 | entry = Mock() 584 | entry.method = 'GET' 585 | entry.info.path = '/foo' 586 | 587 | entry.request.headers = { 588 | 'transfer-encoding': 'chunked', 589 | } 590 | entry.request.body = b'' 591 | 592 | # Given an instance of that socket 593 | socket = MySocket() 594 | socket._entry = entry 595 | 596 | # And that is is considered http 597 | socket.connect(('foo.com', 80)) 598 | 599 | # When I try to send data 600 | socket.sendall(b"BLABLABLABLA") 601 | 602 | # Then the entry should have that body 603 | httpretty.last_request.body.should.equal(b'BLABLABLABLA') 604 | 605 | 606 | def test_fakesock_socket_sendall_with_path_starting_with_two_slashes(): 607 | ("fakesock.socket#sendall handles paths starting with // well") 608 | 609 | httpretty.register_uri(httpretty.GET, 'http://example.com//foo') 610 | 611 | class MySocket(fakesock.socket): 612 | def real_sendall(self, data, *args, **kw): 613 | raise AssertionError('should never call this...') 614 | 615 | # Given an instance of that socket 616 | socket = MySocket() 617 | 618 | # And that is is considered http 619 | socket.connect(('example.com', 80)) 620 | 621 | # When I try to send data 622 | socket.sendall(b"GET //foo HTTP/1.1\r\nContent-Type: application/json\r\n\r\n") 623 | 624 | 625 | def test_URIMatcher_respects_querystring(): 626 | ("URIMatcher response querystring") 627 | matcher = URIMatcher('http://www.foo.com/?query=true', None) 628 | info = URIInfo.from_uri('http://www.foo.com/', None) 629 | assert matcher.matches(info) 630 | 631 | matcher = URIMatcher('http://www.foo.com/?query=true', None, match_querystring=True) 632 | info = URIInfo.from_uri('http://www.foo.com/', None) 633 | assert not matcher.matches(info) 634 | 635 | matcher = URIMatcher('http://www.foo.com/?query=true', None, match_querystring=True) 636 | info = URIInfo.from_uri('http://www.foo.com/?query=true', None) 637 | assert matcher.matches(info) 638 | 639 | matcher = URIMatcher('http://www.foo.com/?query=true&unquery=false', None, match_querystring=True) 640 | info = URIInfo.from_uri('http://www.foo.com/?unquery=false&query=true', None) 641 | assert matcher.matches(info) 642 | 643 | matcher = URIMatcher('http://www.foo.com/?unquery=false&query=true', None, match_querystring=True) 644 | info = URIInfo.from_uri('http://www.foo.com/?query=true&unquery=false', None) 645 | assert matcher.matches(info) 646 | 647 | 648 | def test_URIMatcher_equality_respects_querystring(): 649 | ("URIMatcher equality check should check querystring") 650 | matcher_a = URIMatcher('http://www.foo.com/?query=true', None) 651 | matcher_b = URIMatcher('http://www.foo.com/?query=false', None) 652 | assert matcher_a == matcher_b 653 | 654 | matcher_a = URIMatcher('http://www.foo.com/?query=true', None) 655 | matcher_b = URIMatcher('http://www.foo.com/', None) 656 | assert matcher_a == matcher_b 657 | 658 | matcher_a = URIMatcher('http://www.foo.com/?query=true', None, match_querystring=True) 659 | matcher_b = URIMatcher('http://www.foo.com/?query=false', None, match_querystring=True) 660 | assert not matcher_a == matcher_b 661 | 662 | matcher_a = URIMatcher('http://www.foo.com/?query=true', None, match_querystring=True) 663 | matcher_b = URIMatcher('http://www.foo.com/', None, match_querystring=True) 664 | assert not matcher_a == matcher_b 665 | 666 | matcher_a = URIMatcher('http://www.foo.com/?query=true&unquery=false', None, match_querystring=True) 667 | matcher_b = URIMatcher('http://www.foo.com/?unquery=false&query=true', None, match_querystring=True) 668 | assert matcher_a == matcher_b 669 | -------------------------------------------------------------------------------- /tests/unit/test_http.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals 4 | from httpretty.http import parse_requestline 5 | 6 | 7 | def test_parse_request_line_connect(): 8 | ("parse_requestline should parse the CONNECT method appropriately") 9 | 10 | # Given a valid request line string that has the CONNECT method 11 | line = "CONNECT / HTTP/1.1" 12 | 13 | # When I parse it 14 | result = parse_requestline(line) 15 | 16 | # Then it should return a tuple 17 | result.should.equal(("CONNECT", "/", "1.1")) 18 | -------------------------------------------------------------------------------- /tests/unit/test_httpretty.py: -------------------------------------------------------------------------------- 1 | # #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # Copyright (C) <2011-2021> Gabriel Falcão 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | from __future__ import unicode_literals 28 | import re 29 | import json 30 | from sure import expect 31 | import httpretty 32 | from httpretty import HTTPretty 33 | from httpretty import HTTPrettyError 34 | from httpretty import core 35 | from httpretty.core import URIInfo, BaseClass, Entry, FakeSockFile, HTTPrettyRequest 36 | from httpretty.http import STATUSES 37 | 38 | from tests.compat import MagicMock, patch 39 | 40 | 41 | TEST_HEADER = """ 42 | GET /test/test.html HTTP/1.1 43 | Host: www.host1.com:80 44 | Content-Type: %(content_type)s 45 | """ 46 | 47 | 48 | def test_httpretty_should_raise_proper_exception_on_inconsistent_length(): 49 | ("HTTPretty should raise proper exception on inconsistent Content-Length / " 50 | "registered response body") 51 | 52 | HTTPretty.register_uri.when.called_with( 53 | HTTPretty.GET, 54 | "http://github.com/gabrielfalcao", 55 | body="that's me!", 56 | adding_headers={ 57 | 'Content-Length': '999' 58 | } 59 | ).should.have.raised( 60 | HTTPrettyError, 61 | 'HTTPretty got inconsistent parameters. The header Content-Length you registered expects size "999" ' 62 | 'but the body you registered for that has actually length "10".' 63 | ) 64 | 65 | 66 | def test_does_not_have_last_request_by_default(): 67 | 'HTTPretty.last_request is a dummy object by default' 68 | HTTPretty.reset() 69 | 70 | expect(HTTPretty.last_request.headers).to.be.empty 71 | expect(HTTPretty.last_request.body).to.be.empty 72 | 73 | 74 | def test_status_codes(): 75 | "HTTPretty supports N status codes" 76 | 77 | expect(STATUSES).to.equal({ 78 | 100: "Continue", 79 | 101: "Switching Protocols", 80 | 102: "Processing", 81 | 200: "OK", 82 | 201: "Created", 83 | 202: "Accepted", 84 | 203: "Non-Authoritative Information", 85 | 204: "No Content", 86 | 205: "Reset Content", 87 | 206: "Partial Content", 88 | 207: "Multi-Status", 89 | 208: "Already Reported", 90 | 226: "IM Used", 91 | 300: "Multiple Choices", 92 | 301: "Moved Permanently", 93 | 302: "Found", 94 | 303: "See Other", 95 | 304: "Not Modified", 96 | 305: "Use Proxy", 97 | 306: "Switch Proxy", 98 | 307: "Temporary Redirect", 99 | 308: "Permanent Redirect", 100 | 400: "Bad Request", 101 | 401: "Unauthorized", 102 | 402: "Payment Required", 103 | 403: "Forbidden", 104 | 404: "Not Found", 105 | 405: "Method Not Allowed", 106 | 406: "Not Acceptable", 107 | 407: "Proxy Authentication Required", 108 | 408: "Request a Timeout", 109 | 409: "Conflict", 110 | 410: "Gone", 111 | 411: "Length Required", 112 | 412: "Precondition Failed", 113 | 413: "Request Entity Too Large", 114 | 414: "Request-URI Too Long", 115 | 415: "Unsupported Media Type", 116 | 416: "Requested Range Not Satisfiable", 117 | 417: "Expectation Failed", 118 | 418: "I'm a teapot", 119 | 420: "Enhance Your Calm", 120 | 421: "Misdirected Request", 121 | 422: "Unprocessable Entity", 122 | 423: "Locked", 123 | 424: "Failed Dependency", 124 | 425: "Unordered Collection", 125 | 426: "Upgrade Required", 126 | 428: "Precondition Required", 127 | 429: "Too Many Requests", 128 | 431: "Request Header Fields Too Large", 129 | 444: "No Response", 130 | 449: "Retry With", 131 | 450: "Blocked by Windows Parental Controls", 132 | 451: "Unavailable For Legal Reasons", 133 | 494: "Request Header Too Large", 134 | 495: "Cert Error", 135 | 496: "No Cert", 136 | 497: "HTTP to HTTPS", 137 | 499: "Client Closed Request", 138 | 500: "Internal Server Error", 139 | 501: "Not Implemented", 140 | 502: "Bad Gateway", 141 | 503: "Service Unavailable", 142 | 504: "Gateway Timeout", 143 | 505: "HTTP Version Not Supported", 144 | 506: "Variant Also Negotiates", 145 | 507: "Insufficient Storage", 146 | 508: "Loop Detected", 147 | 509: "Bandwidth Limit Exceeded", 148 | 510: "Not Extended", 149 | 511: "Network Authentication Required", 150 | 598: "Network read timeout error", 151 | 599: "Network connect timeout error", 152 | }) 153 | 154 | 155 | def test_uri_info_full_url(): 156 | uri_info = URIInfo( 157 | username='johhny', 158 | password='password', 159 | hostname=b'google.com', 160 | port=80, 161 | path=b'/', 162 | query=b'foo=bar&baz=test', 163 | fragment='', 164 | scheme='', 165 | ) 166 | 167 | expect(uri_info.full_url()).to.equal( 168 | "http://johhny:password@google.com/?baz=test&foo=bar" 169 | ) 170 | 171 | expect(uri_info.full_url(use_querystring=False)).to.equal( 172 | "http://johhny:password@google.com/" 173 | ) 174 | 175 | 176 | def test_uri_info_eq_ignores_case(): 177 | """Test that URIInfo.__eq__ method ignores case for 178 | hostname matching. 179 | """ 180 | uri_info_uppercase = URIInfo( 181 | username='johhny', 182 | password='password', 183 | hostname=b'GOOGLE.COM', 184 | port=80, 185 | path=b'/', 186 | query=b'foo=bar&baz=test', 187 | fragment='', 188 | scheme='', 189 | ) 190 | uri_info_lowercase = URIInfo( 191 | username='johhny', 192 | password='password', 193 | hostname=b'google.com', 194 | port=80, 195 | path=b'/', 196 | query=b'foo=bar&baz=test', 197 | fragment='', 198 | scheme='', 199 | ) 200 | expect(uri_info_uppercase).to.equal(uri_info_lowercase) 201 | 202 | 203 | def test_global_boolean_enabled(): 204 | HTTPretty.disable() 205 | expect(HTTPretty.is_enabled()).to.be.falsy 206 | HTTPretty.enable() 207 | expect(HTTPretty.is_enabled()).to.be.truthy 208 | HTTPretty.disable() 209 | expect(HTTPretty.is_enabled()).to.be.falsy 210 | 211 | 212 | def test_py3kobject_implements_valid__repr__based_on__str__(): 213 | class MyObject(BaseClass): 214 | def __str__(self): 215 | return 'hi' 216 | 217 | myobj = MyObject() 218 | expect(repr(myobj)).to.be.equal('hi') 219 | 220 | 221 | def test_Entry_class_normalizes_headers(): 222 | entry = Entry(HTTPretty.GET, 'http://example.com', 'example', 223 | host='example.com', cache_control='no-cache', x_forward_for='proxy') 224 | 225 | entry.adding_headers.should.equal({ 226 | 'Host': 'example.com', 227 | 'Cache-Control': 'no-cache', 228 | 'X-Forward-For': 'proxy' 229 | }) 230 | 231 | 232 | def test_Entry_class_counts_multibyte_characters_in_bytes(): 233 | entry = Entry(HTTPretty.GET, 'http://example.com', 'こんにちは') 234 | buf = FakeSockFile() 235 | entry.fill_filekind(buf) 236 | response = buf.read() 237 | expect(b'content-length: 15\n').to.be.within(response) 238 | 239 | 240 | def test_Entry_class_counts_dynamic(): 241 | result = (200, {}, 'こんにちは'.encode('utf-8')) 242 | entry = Entry(HTTPretty.GET, 'http://example.com', lambda *args: result) 243 | entry.info = URIInfo.from_uri('http://example.com', entry) 244 | buf = FakeSockFile() 245 | entry.fill_filekind(buf) 246 | response = buf.getvalue() 247 | expect(b'content-length: 15\n').to.be.within(response) 248 | 249 | 250 | def test_fake_socket_passes_through_setblocking(): 251 | import socket 252 | HTTPretty.enable() 253 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 254 | s.truesock = MagicMock() 255 | expect(s.setblocking).called_with(0).should_not.throw(AttributeError) 256 | s.truesock.setblocking.assert_called_with(0) 257 | 258 | 259 | def test_fake_socket_passes_through_fileno(): 260 | import socket 261 | with httpretty.enabled(): 262 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 263 | s.truesock = MagicMock() 264 | expect(s.fileno).called_with().should_not.throw(AttributeError) 265 | s.truesock.fileno.assert_called_with() 266 | 267 | 268 | def test_fake_socket_passes_through_getsockopt(): 269 | import socket 270 | HTTPretty.enable() 271 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 272 | s.truesock = MagicMock() 273 | expect(s.getsockopt).called_with(socket.SOL_SOCKET, 1).should_not.throw(AttributeError) 274 | s.truesock.getsockopt.assert_called_with(socket.SOL_SOCKET, 1) 275 | 276 | 277 | def test_fake_socket_passes_through_bind(): 278 | import socket 279 | HTTPretty.enable() 280 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 281 | s.truesock = MagicMock() 282 | expect(s.bind).called_with(('127.0.0.1', 1000)).should_not.throw(AttributeError) 283 | s.truesock.bind.assert_called_with(('127.0.0.1', 1000)) 284 | 285 | 286 | def test_fake_socket_passes_through_connect_ex(): 287 | import socket 288 | HTTPretty.enable() 289 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 290 | s.truesock = MagicMock() 291 | expect(s.connect_ex).called_with().should_not.throw(AttributeError) 292 | s.truesock.connect_ex.assert_called_with() 293 | 294 | 295 | def test_fake_socket_passes_through_listen(): 296 | import socket 297 | HTTPretty.enable() 298 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 299 | s.truesock = MagicMock() 300 | expect(s.listen).called_with().should_not.throw(AttributeError) 301 | s.truesock.listen.assert_called_with() 302 | 303 | 304 | def test_fake_socket_passes_through_getpeername(): 305 | import socket 306 | HTTPretty.enable() 307 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 308 | s.truesock = MagicMock() 309 | expect(s.getpeername).called_with().should_not.throw(AttributeError) 310 | s.truesock.getpeername.assert_called_with() 311 | 312 | 313 | def test_fake_socket_passes_through_getsockname(): 314 | import socket 315 | HTTPretty.enable() 316 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 317 | s.truesock = MagicMock() 318 | expect(s.getsockname).called_with().should_not.throw(AttributeError) 319 | s.truesock.getsockname.assert_called_with() 320 | 321 | 322 | def test_fake_socket_passes_through_gettimeout(): 323 | import socket 324 | HTTPretty.enable() 325 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 326 | s.truesock = MagicMock() 327 | expect(s.gettimeout).called_with().should_not.throw(AttributeError) 328 | s.truesock.gettimeout.assert_called_with() 329 | 330 | 331 | def test_fake_socket_passes_through_shutdown(): 332 | import socket 333 | HTTPretty.enable() 334 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 335 | s.truesock = MagicMock() 336 | expect(s.shutdown).called_with(socket.SHUT_RD).should_not.throw(AttributeError) 337 | s.truesock.shutdown.assert_called_with(socket.SHUT_RD) 338 | 339 | 340 | def test_unix_socket(): 341 | import socket 342 | HTTPretty.enable() 343 | 344 | # Create a UDS socket 345 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 346 | server_address = './not-exist-socket' 347 | try: 348 | sock.connect(server_address) 349 | except socket.error: 350 | # We expect this, since the server_address does not exist 351 | pass 352 | 353 | 354 | def test_HTTPrettyRequest_json_body(): 355 | """ A content-type of application/json should parse a valid json body """ 356 | header = TEST_HEADER % {'content_type': 'application/json'} 357 | test_dict = {'hello': 'world'} 358 | request = HTTPrettyRequest(header, json.dumps(test_dict)) 359 | expect(request.parsed_body).to.equal(test_dict) 360 | 361 | 362 | def test_HTTPrettyRequest_invalid_json_body(): 363 | """ A content-type of application/json with an invalid json body should return the content unaltered """ 364 | header = TEST_HEADER % {'content_type': 'application/json'} 365 | invalid_json = u"{'hello', 'world','thisstringdoesntstops}" 366 | request = HTTPrettyRequest(header, invalid_json) 367 | expect(request.parsed_body).to.equal(invalid_json) 368 | 369 | 370 | def test_HTTPrettyRequest_queryparam(): 371 | """ A content-type of x-www-form-urlencoded with a valid queryparam body should return parsed content """ 372 | header = TEST_HEADER % {'content_type': 'application/x-www-form-urlencoded'} 373 | valid_queryparam = u"hello=world&this=isavalidquerystring" 374 | valid_results = {'hello': ['world'], 'this': ['isavalidquerystring']} 375 | request = HTTPrettyRequest(header, valid_queryparam) 376 | expect(request.parsed_body).to.equal(valid_results) 377 | 378 | 379 | def test_HTTPrettyRequest_arbitrarypost(): 380 | """ A non-handled content type request's post body should return the content unaltered """ 381 | header = TEST_HEADER % {'content_type': 'thisis/notarealcontenttype'} 382 | gibberish_body = "1234567890!@#$%^&*()" 383 | request = HTTPrettyRequest(header, gibberish_body) 384 | expect(request.parsed_body).to.equal(gibberish_body) 385 | 386 | 387 | def test_socktype_bad_python_version_regression(): 388 | """ Some versions of python accidentally internally shadowed the SockType 389 | variable, so it was no longer the socket object but and int Enum representing 390 | the socket type e.g. AF_INET. Make sure we don't patch SockType in these cases 391 | https://bugs.python.org/issue20386 392 | """ 393 | import socket 394 | someObject = object() 395 | with patch('socket.SocketType', someObject): 396 | HTTPretty.enable() 397 | expect(socket.SocketType).to.equal(someObject) 398 | HTTPretty.disable() 399 | 400 | 401 | def test_socktype_good_python_version(): 402 | import socket 403 | with patch('socket.SocketType', socket.socket): 404 | HTTPretty.enable() 405 | expect(socket.SocketType).to.equal(socket.socket) 406 | HTTPretty.disable() 407 | 408 | 409 | def test_httpretty_should_allow_registering_regex_hostnames(): 410 | "HTTPretty should allow registering regexes with requests" 411 | 412 | HTTPretty.register_uri( 413 | HTTPretty.GET, 414 | re.compile(r'^http://\w+\.foo\.com/baz$'), 415 | body="yay", 416 | ) 417 | 418 | assert HTTPretty.match_http_address('www.foo.com', 80) 419 | 420 | 421 | def test_httpretty_should_allow_registering_regex_hostnames_ssl(): 422 | "HTTPretty should allow registering regexes with requests (ssl version)" 423 | 424 | HTTPretty.register_uri( 425 | HTTPretty.GET, 426 | re.compile(r'^https://\w+\.foo\.com/baz$'), 427 | body="yay", 428 | ) 429 | 430 | assert HTTPretty.match_https_hostname('www.foo.com') 431 | -------------------------------------------------------------------------------- /tests/unit/test_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals 4 | import httpretty 5 | from httpretty.core import HTTPrettyRequest 6 | 7 | try: 8 | from unittest.mock import patch 9 | except ImportError: 10 | from mock import patch 11 | 12 | 13 | @patch('httpretty.httpretty') 14 | def test_last_request(original): 15 | ("httpretty.last_request() should return httpretty.core.last_request") 16 | 17 | httpretty.last_request().should.equal(original.last_request) 18 | 19 | 20 | @patch('httpretty.httpretty') 21 | def test_latest_requests(original): 22 | ("httpretty.latest_requests() should return httpretty.core.latest_requests") 23 | 24 | httpretty.latest_requests().should.equal(original.latest_requests) 25 | 26 | 27 | def test_has_request(): 28 | ("httpretty.has_request() correctly detects " 29 | "whether or not a request has been made") 30 | httpretty.reset() 31 | httpretty.has_request().should.be.false 32 | with patch('httpretty.httpretty.last_request', return_value=HTTPrettyRequest('')): 33 | httpretty.has_request().should.be.true 34 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py36, py37 3 | 4 | [testenv] 5 | deps = pipenv 6 | 7 | commands = make test 8 | --------------------------------------------------------------------------------