summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Falcão <gabrielfalcao@users.noreply.github.com>2021-05-13 02:02:36 +0200
committerGitHub <noreply@github.com>2021-05-13 02:02:36 +0200
commitb6161cebfdaf5f68ef49526e51b460b8e0c0c6c2 (patch)
tree698ce62d3455dc0e5631a99e111807a417d69cdd
parent3528d0d8ea29e5aaff0341b1b3cfe986031bad31 (diff)
downloadhttpretty-b6161cebfdaf5f68ef49526e51b460b8e0c0c6c2.tar.gz
Improve debugging experience via `UnmockedError` and logging (#419)
Feature: Display mismatched URL within UnmockedError whenever possible. Closes #388 Feature: Display mismatched URL via logging. #419 Add new properties to `httpretty.core.HTTPrettyRequest` (protocol, host, url, path, method).
-rw-r--r--Makefile10
-rw-r--r--development.txt4
-rw-r--r--docs/Makefile7
-rw-r--r--docs/source/acks.rst4
-rw-r--r--docs/source/api.rst18
-rw-r--r--docs/source/changelog.rst20
-rw-r--r--docs/source/introduction.rst41
-rw-r--r--httpretty/core.py199
-rw-r--r--httpretty/errors.py21
-rw-r--r--tests/functional/bugfixes/test_242_ssl_bad_handshake.py9
-rw-r--r--tests/functional/bugfixes/test_388_unmocked_error_with_url.py56
-rw-r--r--tests/functional/test_bypass.py6
-rw-r--r--tests/functional/test_passthrough.py9
-rw-r--r--tests/functional/test_requests.py22
-rw-r--r--tests/unit/test_core.py6
15 files changed, 319 insertions, 113 deletions
diff --git a/Makefile b/Makefile
index 34d28ae..3712aef 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: tests all unit functional clean dependencies tdd docs html purge dist
+.PHONY: tests all unit functional clean dependencies tdd docs html purge dist setup
GIT_ROOT := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
DOCS_ROOT := $(GIT_ROOT)/docs
@@ -17,7 +17,7 @@ $(VENV): # creates $(VENV) folder if does not exist
python3 -mvenv $(VENV)
$(VENV)/bin/pip install -U pip setuptools
-$(VENV)/bin/sphinx-build $(VENV)/bin/twine $(VENV)/bin/nosetests $(VENV)/bin/python $(VENV)/bin/pip: # installs latest pip
+setup $(VENV)/bin/sphinx-build $(VENV)/bin/twine $(VENV)/bin/nosetests $(VENV)/bin/python $(VENV)/bin/pip: # installs latest pip
test -e $(VENV)/bin/pip || make $(VENV)
$(VENV)/bin/pip install -r development.txt
$(VENV)/bin/pip install -e .
@@ -49,12 +49,12 @@ functional: $(VENV)/bin/nosetests # runs functional tests
-$(DOCS_INDEX): | $(VENV)/bin/sphinx-build
+$(DOCS_INDEX): $(VENV)/bin/sphinx-build
cd docs && make html
-html: $(DOCS_INDEX)
+html: $(DOCS_INDEX) $(VENV)/bin/sphinx-build
-docs: $(DOCS_INDEX)
+docs: $(DOCS_INDEX) $(VENV)/bin/sphinx-build
open $(DOCS_INDEX)
release: | clean unit functional tests html
diff --git a/development.txt b/development.txt
index b86aa6f..acbde2f 100644
--- a/development.txt
+++ b/development.txt
@@ -17,8 +17,8 @@ redis==3.4.1
rednose>=1.3.0
requests-toolbelt>=0.9.1
singledispatch>=3.4.0.3
-sphinx-rtd-theme>=0.4.3
-sphinx>=2.4.4
+sphinx-rtd-theme>=0.5.2
+sphinx>=4.0.1
sure>=1.4.11
tornado>=6.0.4
tox>=3.14.5
diff --git a/docs/Makefile b/docs/Makefile
index 898ba26..cf144d4 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -1,9 +1,12 @@
# Minimal makefile for Sphinx documentation
#
+GIT_ROOT := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))/..)
+VENV_ROOT := $(GIT_ROOT)/.venv
+VENV ?= $(VENV_ROOT)
# You can set these variables from the command line.
SPHINXOPTS =
-SPHINXBUILD = sphinx-build
+SPHINXBUILD = $(VENV)/bin/sphinx-build
SPHINXPROJ = HTTPretty
SOURCEDIR = source
BUILDDIR = build
@@ -17,4 +20,4 @@ help:
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
- @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/source/acks.rst b/docs/source/acks.rst
index ceffe06..036b795 100644
--- a/docs/source/acks.rst
+++ b/docs/source/acks.rst
@@ -1,7 +1,7 @@
Acknowledgements
################
-caveats
+Caveats
=======
``forcing_headers`` + ``Content-Length``
@@ -11,7 +11,7 @@ When using the ``forcing_headers`` option make sure to add the header
``Content-Length`` otherwise calls using :py:mod:`requests` will try
to load the response endlessly.
-supported libraries
+Supported Libraries
-------------------
Because HTTPretty works in the socket level it should work with any HTTP client libraries, although it is `battle tested <https://github.com/gabrielfalcao/HTTPretty/tree/master/tests/functional>`_ against:
diff --git a/docs/source/api.rst b/docs/source/api.rst
index a94f65b..71fe992 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -9,31 +9,37 @@ register_uri
------------
.. automethod:: httpretty.core.httpretty.register_uri
+ :noindex:
enable
------
.. automethod:: httpretty.core.httpretty.enable
+ :noindex:
disable
-------
.. automethod:: httpretty.core.httpretty.disable
+ :noindex:
is_enabled
----------
.. automethod:: httpretty.core.httpretty.is_enabled
+ :noindex:
last_request
------------
.. autofunction:: httpretty.last_request
+ :noindex:
latest_requests
---------------
.. autofunction:: httpretty.latest_requests
+ :noindex:
.. automodule:: httpretty
@@ -45,6 +51,7 @@ activate
.. autoclass:: httpretty.activate
:members:
+ :noindex:
.. _httprettified:
@@ -53,6 +60,7 @@ httprettified
-------------
.. autofunction:: httpretty.core.httprettified
+ :noindex:
.. _enabled:
@@ -62,6 +70,7 @@ enabled
.. autoclass:: httpretty.enabled
:members:
+ :noindex:
.. _httprettized:
@@ -71,6 +80,8 @@ httprettized
.. autoclass:: httpretty.core.httprettized
:members:
+ :noindex:
+
.. _HTTPrettyRequest:
@@ -80,6 +91,7 @@ HTTPrettyRequest
.. autoclass:: httpretty.core.HTTPrettyRequest
:members:
+ :noindex:
.. _HTTPrettyRequestEmpty:
@@ -89,6 +101,7 @@ HTTPrettyRequestEmpty
.. autoclass:: httpretty.core.HTTPrettyRequestEmpty
:members:
+ :noindex:
.. _FakeSockFile:
@@ -97,6 +110,7 @@ FakeSockFile
.. autoclass:: httpretty.core.FakeSockFile
:members:
+ :noindex:
.. _FakeSSLSocket:
@@ -106,6 +120,7 @@ FakeSSLSocket
.. autoclass:: httpretty.core.FakeSSLSocket
:members:
+ :noindex:
.. _URIInfo:
@@ -115,6 +130,7 @@ URIInfo
.. autoclass:: httpretty.URIInfo
:members:
+ :noindex:
.. _URIMatcher:
@@ -124,6 +140,7 @@ URIMatcher
.. autoclass:: httpretty.URIMatcher
:members:
+ :noindex:
.. _Entry:
@@ -133,6 +150,7 @@ Entry
.. autoclass:: httpretty.Entry
:members:
+ :noindex:
.. _api modules:
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index 2075f71..270764e 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -1,6 +1,26 @@
Release Notes
=============
+1.1.0
+-----
+
+- Feature: Display mismatched URL within ``UnmockedError`` whenever possible. `#388 <https://github.com/gabrielfalcao/HTTPretty/issues/388>`_
+- Feature: Display mismatched URL via logging. `#419 <https://github.com/gabrielfalcao/HTTPretty/pull/419>`_
+- Add new properties to :py:class:`httpretty.core.HTTPrettyRequest` (``protocol, host, url, path, method``).
+
+Example usage:
+
+.. testcode::
+
+ import httpretty
+ import requests
+
+ @httpretty.activate(verbose=True, allow_net_connect=False)
+ def test_mismatches():
+ requests.get('http://sql-server.local')
+ requests.get('https://redis.local')
+
+
1.0.5
-----
diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst
index 511b2c2..a5339d2 100644
--- a/docs/source/introduction.rst
+++ b/docs/source/introduction.rst
@@ -17,13 +17,15 @@ Don't worry, HTTPretty is here for you:
::
+ import logging
import requests
import httpretty
from sure import expect
+ logging.getLogger('httpretty.core').setLevel(logging.DEBUG)
- @httpretty.activate
+ @httpretty.activate(allow_net_connect=False)
def test_yipit_api_returning_deals():
httpretty.register_uri(httpretty.GET, "http://api.yipit.com/v1/deals/",
body='[{"title": "Test Deal"}]',
@@ -37,8 +39,13 @@ Don't worry, HTTPretty is here for you:
A more technical description
============================
-HTTPretty is a HTTP client mock library for Python 100% inspired on ruby's [FakeWeb](http://fakeweb.rubyforge.org/).
-If you come from ruby this would probably sound familiar :smiley:
+HTTPretty is a python library that swaps the modules :py:mod:`socket`
+and :py:mod:`ssl` with fake implementations that intercept HTTP
+requests at the level of a TCP connection.
+
+It is inspired on Ruby's `FakeWeb <http://fakeweb.rubyforge.org/>`_.
+
+If you come from the Ruby programming language this would probably sound familiar :smiley:
Installing
==========
@@ -65,7 +72,7 @@ expecting a simple response body
import httpretty
def test_one():
- httpretty.enable() # enable HTTPretty so that it will monkey patch the socket module
+ httpretty.enable(verbose=True, allow_net_connect=False) # enable HTTPretty so that it will monkey patch the socket module
httpretty.register_uri(httpretty.GET, "http://yipit.com/",
body="Find the best daily deals")
@@ -160,20 +167,16 @@ problem:
*"I'm gonna need to mock all those requests"*
-It brings a lot of hassle, you will need to use a generic mocking
-tool, mess with scope and so on.
-
-The idea behind HTTPretty (how it works)
-========================================
-
-
-HTTPretty `monkey patches <http://en.wikipedia.org/wiki/Monkey_patch>`_
-Python's `socket <http://docs.python.org/library/socket.html>`_ core
-module, reimplementing the HTTP protocol, by mocking requests and
-responses.
+It can be a bit of a hassle to use something like
+:py:class:`mock.Mock` to stub the requests, this can work well for
+low-level unit tests but when writing functional or integration tests
+we should be able to allow the http calls to go through the TCP socket
+module.
-As for how it works this way, you don't need to worry what http
-library you're gonna use.
+HTTPretty `monkey patches
+<http://en.wikipedia.org/wiki/Monkey_patch>`_ Python's
+:py:mod:`socket` core module with a fake version of the module.
-HTTPretty will mock the response for you :) *(and also give you the
-latest requests so that you can check them)*
+Because HTTPretty implements a fake the modules :py:mod:`socket` and
+:py:mod:`ssl` you can use write tests to code against any HTTP library
+that use those modules.
diff --git a/httpretty/core.py b/httpretty/core.py
index 924551e..f381eaf 100644
--- a/httpretty/core.py
+++ b/httpretty/core.py
@@ -23,6 +23,7 @@
# OTHER DEALINGS IN THE SOFTWARE.
import io
+import time
import codecs
import contextlib
import functools
@@ -147,8 +148,7 @@ def FALLBACK_FUNCTION(x):
class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass):
- r"""
- Represents a HTTP request. It takes a valid multi-line,
+ r"""Represents a HTTP request. It takes a valid multi-line,
``\r\n`` separated string with HTTP headers and parse them out using
the internal `parse_request` method.
@@ -161,15 +161,24 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass):
``headers`` -> a mimetype object that can be cast into a dictionary,
contains all the request headers
- ``method`` -> the HTTP method used in this request
+ ``protocol`` -> the protocol of this host, inferred from the port
+ of the underlying fake TCP socket.
+
+ ``host`` -> the hostname of this request.
+
+ ``url`` -> the full url of this request.
+
+ ``path`` -> the path of the request.
+
+ ``method`` -> the HTTP method used in this request.
``querystring`` -> a dictionary containing lists with the
attributes. Please notice that if you need a single value from a
query string you will need to get it manually like:
- ``body`` -> the request body as a string
+ ``body`` -> the request body as a string.
- ``parsed_body`` -> the request body parsed by ``parse_request_body``
+ ``parsed_body`` -> the request body parsed by ``parse_request_body``.
.. testcode::
@@ -177,16 +186,15 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass):
{'name': ['Gabriel Falcao']}
>>> print request.querystring['name'][0]
-
-
"""
- def __init__(self, headers, body=''):
+ def __init__(self, headers, body='', sock=None, path_encoding = 'iso-8859-1'):
# first of all, lets make sure that if headers or body are
# unicode strings, it must be converted into a utf-8 encoded
# byte string
+ self.created_at = time.time()
self.raw_headers = utf8(headers.strip())
self._body = utf8(body)
-
+ self.connection = sock
# Now let's concatenate the headers with the body, and create
# `rfile` based on it
self.rfile = io.BytesIO(b'\r\n\r\n'.join([self.raw_headers, self.body]))
@@ -206,14 +214,13 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass):
if not self.parse_request():
return
- # making the HTTP method string available as the command
- self.method = self.command
-
# Now 2 convenient attributes for the HTTPretty API:
- # `querystring` holds a dictionary with the parsed query string
+ # - `path`
+ # - `querystring` holds a dictionary with the parsed query string
+ # - `parsed_body` a string
try:
- self.path = self.path.encode('iso-8859-1')
+ self.path = self.path.encode(path_encoding)
except UnicodeDecodeError:
pass
@@ -233,6 +240,25 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass):
self.parsed_body = self.parse_request_body(self._body)
@property
+ def method(self):
+ """the HTTP method used in this request"""
+ return self.command
+
+ @property
+ def protocol(self):
+ """the protocol used in this request"""
+ proto = ''
+ if not self.connection:
+ return ''
+ elif self.connection.is_http:
+ proto = 'http'
+
+ if self.connection.is_secure:
+ proto = 'https'
+
+ return proto
+
+ @property
def body(self):
return self._body
@@ -247,11 +273,21 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass):
def __nonzero__(self):
return bool(self.body) or bool(self.raw_headers)
+ @property
+ def url(self):
+ """the full url of this recorded request"""
+ return "{}://{}{}".format(self.protocol, self.host, self.path)
+
+ @property
+ def host(self):
+ return self.headers.get('Host') or '<unknown>'
+
def __str__(self):
- tmpl = '<HTTPrettyRequest("{}", total_headers={}, body_length={})>'
+ tmpl = '<HTTPrettyRequest("{}", "{}", headers={}, body={})>'
return tmpl.format(
- self.headers.get('content-type', ''),
- len(self.headers),
+ self.method,
+ self.url,
+ dict(self.headers),
len(self.body),
)
@@ -375,7 +411,7 @@ class fakesock(object):
_entry = None
debuglevel = 0
_sent_data = []
-
+ is_secure = False
def __init__(
self,
family=socket.AF_INET,
@@ -400,7 +436,14 @@ class fakesock(object):
self.is_http = False
self._bufsize = 32 * 1024
- def create_socket(self):
+ def __repr__(self):
+ return '{self.__class__.__module__}.{self.__class__.__name__}("{self.host}")'.format(**locals())
+
+ @property
+ def host(self):
+ return ":".join(map(str, self._address))
+
+ def create_socket(self, address=None):
return old_socket(self.socket_family, self.socket_type, self.socket_proto)
def getpeercert(self, *a, **kw):
@@ -452,14 +495,15 @@ class fakesock(object):
ports_to_check = (
POTENTIAL_HTTP_PORTS.union(POTENTIAL_HTTPS_PORTS))
self.is_http = self._port in ports_to_check
+ self.is_secure = self._port in POTENTIAL_HTTPS_PORTS
if not self.is_http:
- self.connect_truesock()
+ self.connect_truesock(address=address)
elif self.truesock and not self.real_socket_is_connected():
# TODO: remove nested if
matcher = httpretty.match_http_address(self._host, self._port)
if matcher is None:
- self.connect_truesock()
+ self.connect_truesock(address=address)
def bind(self, address):
self._address = (self._host, self._port) = address
@@ -470,18 +514,28 @@ class fakesock(object):
if httpretty.allow_net_connect and not self.truesock:
self.truesock = self.create_socket()
elif not self.truesock:
- raise UnmockedError()
+ raise UnmockedError('Failed to socket.bind() because because a real socket was never created.', address=address)
return self.truesock.bind(address)
- def connect_truesock(self):
+ def connect_truesock(self, request=None, address=None):
+ address = address or self._address
+
if self.__truesock_is_connected__:
return self.truesock
+ if request:
+ logger.warning('real call to socket.connect() for {request}'.format(**locals()))
+ elif address:
+ logger.warning('real call to socket.connect() for {address}'.format(**locals()))
+ else:
+ logger.warning('real call to socket.connect()')
+
if httpretty.allow_net_connect and not self.truesock:
- self.truesock = self.create_socket()
+ self.truesock = self.create_socket(address)
elif not self.truesock:
- raise UnmockedError()
+ raise UnmockedError('Failed to socket.connect() because because a real socket was never created.', request=request, address=address)
+
undo_patch_socket()
try:
hostname = self._address[0]
@@ -549,17 +603,22 @@ class fakesock(object):
buffer so that HTTPretty can return it accordingly when
necessary.
"""
+ request = kw.pop('request', None)
+ if request:
+ bytecount = len(data)
+ logger.warning('{self}.real_sendall({bytecount} bytes) to {request.url} via {request.method} at {request.created_at}'.format(**locals()))
if httpretty.allow_net_connect and not self.truesock:
- self.connect_truesock()
+
+ self.connect_truesock(request=request)
elif not self.truesock:
- raise UnmockedError()
+ raise UnmockedError(request=request)
if not self.is_http:
self.truesock.setblocking(1)
return self.truesock.sendall(data, *args, **kw)
- sock = self.connect_truesock()
+ sock = self.connect_truesock(request=request)
sock.setblocking(1)
sock.sendall(data, *args, **kw)
@@ -621,7 +680,7 @@ class fakesock(object):
else:
self._entry.request.body += body
- httpretty.historify_request(headers, body, False)
+ httpretty.historify_request(headers, body, sock=self)
return
if path[:2] == '//':
@@ -636,7 +695,7 @@ class fakesock(object):
headers = ''
body = data
- request = httpretty.historify_request(headers, body)
+ request = httpretty.historify_request(headers, body, sock=self)
info = URIInfo(
hostname=self._host,
@@ -650,14 +709,14 @@ class fakesock(object):
if not entries:
self._entry = None
- self.real_sendall(data)
+ self.real_sendall(data, request=request)
return
self._entry = matcher.get_next_entry(method, info, request)
def forward_and_trace(self, function_name, *a, **kw):
if not self.truesock:
- raise UnmockedError()
+ raise UnmockedError('Failed to socket.{}() because because a real socket was never created.'.format(function_name))
callback = getattr(self.truesock, function_name)
return callback(*a, **kw)
@@ -688,7 +747,9 @@ class fakesock(object):
return self.forward_and_trace('recv', buffersize, *args, **kwargs)
def __getattr__(self, name):
- if httpretty.allow_net_connect and not self.truesock:
+ if name in ('getsockopt', ) and not self.truesock:
+ self.truesock = self.create_socket()
+ elif httpretty.allow_net_connect and not self.truesock:
# can't call self.connect_truesock() here because we
# don't know if user wants to execute server of client
# calls (or can they?)
@@ -702,22 +763,34 @@ class fakesock(object):
"(see issue https://github.com/gabrielfalcao/HTTPretty/issues/409). "
"Please open an issue if this error causes further unexpected issues."
)
- raise UnmockedError()
+
+ raise UnmockedError('Failed to socket.{} because because a real socket does not exist'.format(name))
+
return getattr(self.truesock, name)
+def with_socket_is_secure(sock, kw):
+ sock.is_secure = True
+ sock.kwargs = kw
+ for k, v in kw.items():
+ setattr(sock, k, v)
+ return sock
def fake_wrap_socket(orig_wrap_socket_fn, *args, **kw):
"""drop-in replacement for py:func:`ssl.wrap_socket`
"""
+ if 'sock' in kw:
+ sock = kw['sock']
+ else:
+ sock = args[0]
+
server_hostname = kw.get('server_hostname')
if server_hostname is not None:
matcher = httpretty.match_https_hostname(server_hostname)
if matcher is None:
- return orig_wrap_socket_fn(*args, **kw)
- if 'sock' in kw:
- return kw['sock']
- else:
- return args[0]
+ logger.debug('no requests registered for hostname: "{}"'.format(server_hostname))
+ return with_socket_is_secure(sock, kw)
+
+ return with_socket_is_secure(sock, kw)
def create_fake_connection(
@@ -1285,7 +1358,7 @@ class httpretty(HttpBaseClass):
@classmethod
@contextlib.contextmanager
- def record(cls, filename, indentation=4, encoding='utf-8'):
+ def record(cls, filename, indentation=4, encoding='utf-8', verbose=False, allow_net_connect=True, pool_manager_params=None):
"""
.. testcode::
@@ -1315,9 +1388,9 @@ class httpretty(HttpBaseClass):
)
raise RuntimeError(msg)
- http = urllib3.PoolManager()
+ http = urllib3.PoolManager(**pool_manager_params or {})
- cls.enable()
+ cls.enable(allow_net_connect, verbose=verbose)
calls = []
def record_request(request, uri, headers):
@@ -1346,7 +1419,7 @@ class httpretty(HttpBaseClass):
'headers': dict(response.headers.items())
}
})
- cls.enable()
+ cls.enable(allow_net_connect, verbose=verbose)
return response.status, response.headers, response.data
for method in cls.METHODS:
@@ -1359,7 +1432,7 @@ class httpretty(HttpBaseClass):
@classmethod
@contextlib.contextmanager
- def playback(cls, filename):
+ def playback(cls, filename, allow_net_connect=True, verbose=False):
"""
.. testcode::
@@ -1377,7 +1450,7 @@ class httpretty(HttpBaseClass):
:param filename: a string
:returns: a `context-manager <https://docs.python.org/3/reference/datamodel.html#context-managers>`_
"""
- cls.enable()
+ cls.enable(allow_net_connect, verbose=verbose)
data = json.loads(open(filename).read())
for item in data:
@@ -1401,7 +1474,7 @@ class httpretty(HttpBaseClass):
cls.last_request = HTTPrettyRequestEmpty()
@classmethod
- def historify_request(cls, headers, body='', append=True):
+ def historify_request(cls, headers, body='', sock=None):
"""appends request to a list for later retrieval
.. testcode::
@@ -1414,12 +1487,16 @@ class httpretty(HttpBaseClass):
assert httpretty.latest_requests[-1].url == 'https://httpbin.org/ip'
"""
- request = HTTPrettyRequest(headers, body)
+ request = HTTPrettyRequest(headers, body, sock=sock)
cls.last_request = request
- if append or not cls.latest_requests:
+
+ if request not in cls.latest_requests:
cls.latest_requests.append(request)
else:
- cls.latest_requests[-1] = request
+ pos = cls.latest_requests.index(request)
+ cls.latest_requests[pos] = request
+
+ logger.info("captured: {}".format(request))
return request
@classmethod
@@ -1577,16 +1654,18 @@ class httpretty(HttpBaseClass):
return cls._is_enabled
@classmethod
- def enable(cls, allow_net_connect=True):
+ def enable(cls, allow_net_connect=True, verbose=False):
"""Enables HTTPretty.
- When ``allow_net_connect`` is ``False`` any connection to an unregistered uri will throw :py:class:`httpretty.errors.UnmockedError`.
+
+ :param allow_net_connect: boolean to determine if unmatched requests are forwarded to a real network connection OR throw :py:class:`httpretty.errors.UnmockedError`.
+ :param verbose: boolean to set HTTPretty's logging level to DEBUG
.. testcode::
import re, json
import httpretty
- httpretty.enable()
+ httpretty.enable(allow_net_connect=True, verbose=True)
httpretty.register_uri(
httpretty.GET,
@@ -1603,9 +1682,13 @@ class httpretty(HttpBaseClass):
.. warning:: after calling this method the original :py:mod:`socket` is replaced with :py:class:`httpretty.core.fakesock`. Make sure to call :py:meth:`~httpretty.disable` after done with your tests or use the :py:class:`httpretty.enabled` as decorator or `context-manager <https://docs.python.org/3/reference/datamodel.html#context-managers>`_
"""
- cls.allow_net_connect = allow_net_connect
+ httpretty.allow_net_connect = allow_net_connect
apply_patch_socket()
cls._is_enabled = True
+ if verbose:
+ logger.setLevel(logging.DEBUG)
+ else:
+ logger.setLevel(logging.getLogger().level)
def apply_patch_socket():
@@ -1741,19 +1824,20 @@ class httprettized(object):
assert httpretty.latest_requests[-1].url == 'https://httpbin.org/ip'
assert response.json() == {'origin': '42.42.42.42'}
"""
- def __init__(self, allow_net_connect=True):
+ def __init__(self, allow_net_connect=True, verbose=False):
self.allow_net_connect = allow_net_connect
+ self.verbose = verbose
def __enter__(self):
httpretty.reset()
- httpretty.enable(allow_net_connect=self.allow_net_connect)
+ httpretty.enable(allow_net_connect=self.allow_net_connect, verbose=self.verbose)
def __exit__(self, exc_type, exc_value, db):
httpretty.disable()
httpretty.reset()
-def httprettified(test=None, allow_net_connect=True):
+def httprettified(test=None, allow_net_connect=True, verbose=False):
"""decorator for test functions
.. tip:: Also available under the alias :py:func:`httpretty.activate`
@@ -1811,7 +1895,7 @@ def httprettified(test=None, allow_net_connect=True):
def new_setUp(self):
httpretty.reset()
- httpretty.enable(allow_net_connect)
+ httpretty.enable(allow_net_connect, verbose=verbose)
if use_addCleanup:
self.addCleanup(httpretty.disable)
if original_setUp:
@@ -1851,7 +1935,6 @@ def httprettified(test=None, allow_net_connect=True):
except ImportError:
return False
- "A decorator for tests that use HTTPretty"
def decorate_class(klass):
if is_unittest_TestCase(klass):
return decorate_unittest_TestCase_setUp(klass)
diff --git a/httpretty/errors.py b/httpretty/errors.py
index d0a55a9..10faca3 100644
--- a/httpretty/errors.py
+++ b/httpretty/errors.py
@@ -25,15 +25,24 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
from __future__ import unicode_literals
-
+import json
class HTTPrettyError(Exception):
pass
class UnmockedError(HTTPrettyError):
- def __init__(self):
- super(UnmockedError, self).__init__(
- 'No mocking was registered, and real connections are '
- 'not allowed (httpretty.allow_net_connect = False).'
- )
+ def __init__(self, message='Failed to handle network request', request=None, address=None):
+ 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.'
+ if request:
+ headers = json.dumps(dict(request.headers), indent=2)
+ message = '{message}.\n\nIntercepted unknown {request.method} request {request.url}\n\nWith headers {headers}'.format(**locals())
+
+ if isinstance(address, (tuple, list)):
+ address = ":".join(map(str, address))
+
+ if address:
+ hint = 'address: {address} | {hint}'.format(**locals())
+
+ self.request = request
+ super(UnmockedError, self).__init__('{message}\n\n{hint}'.format(**locals()))
diff --git a/tests/functional/bugfixes/test_242_ssl_bad_handshake.py b/tests/functional/bugfixes/test_242_ssl_bad_handshake.py
index 7e4bbc0..0653b41 100644
--- a/tests/functional/bugfixes/test_242_ssl_bad_handshake.py
+++ b/tests/functional/bugfixes/test_242_ssl_bad_handshake.py
@@ -14,3 +14,12 @@ def test_test_ssl_bad_handshake():
requests.get(url_http).text.should.equal('insecure')
requests.get(url_https).text.should.equal('encrypted')
+
+ httpretty.latest_requests().should.have.length_of(2)
+ insecure_request, secure_request = httpretty.latest_requests()[:2]
+
+ insecure_request.protocol.should.be.equal('http')
+ secure_request.protocol.should.be.equal('https')
+
+ insecure_request.url.should.be.equal(url_http)
+ secure_request.url.should.be.equal(url_https)
diff --git a/tests/functional/bugfixes/test_388_unmocked_error_with_url.py b/tests/functional/bugfixes/test_388_unmocked_error_with_url.py
new file mode 100644
index 0000000..751d0ad
--- /dev/null
+++ b/tests/functional/bugfixes/test_388_unmocked_error_with_url.py
@@ -0,0 +1,56 @@
+# <HTTPretty - HTTP client mock for Python>
+# Copyright (C) <2011-2021> Gabriel Falcão <gabriel@nacaolivre.org>
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+import requests
+import httpretty
+from httpretty.errors import UnmockedError
+
+from unittest import skip
+from sure import expect
+
+
+def http():
+ sess = requests.Session()
+ adapter = requests.adapters.HTTPAdapter(pool_connections=1, pool_maxsize=1)
+ sess.mount('http://', adapter)
+ sess.mount('https://', adapter)
+ return sess
+
+@httpretty.activate(allow_net_connect=False)
+def test_https_forwarding():
+ "UnmockedError is raised with details about the mismatched request"
+ httpretty.register_uri(httpretty.GET, 'http://google.com/', body="Not Google")
+ httpretty.register_uri(httpretty.GET, 'https://google.com/', body="Not Google")
+ response1 = http().get('http://google.com/')
+ response2 = http().get('https://google.com/')
+
+ http().get.when.called_with("https://github.com/gabrielfalcao/HTTPretty").should.have.raised(UnmockedError, 'https://github.com/gabrielfalcao/HTTPretty')
+
+ response1.text.should.equal(response2.text)
+ try:
+ http().get("https://github.com/gabrielfalcao/HTTPretty")
+ except UnmockedError as exc:
+ expect(exc).to.have.property('request')
+ expect(exc.request).to.have.property('host').being.equal('github.com')
+ expect(exc.request).to.have.property('protocol').being.equal('https')
+ expect(exc.request).to.have.property('url').being.equal('https://github.com/gabrielfalcao/HTTPretty')
diff --git a/tests/functional/test_bypass.py b/tests/functional/test_bypass.py
index 97dec3d..904bc12 100644
--- a/tests/functional/test_bypass.py
+++ b/tests/functional/test_bypass.py
@@ -120,7 +120,7 @@ def test_httpretty_bypasses_when_disabled(context):
core.POTENTIAL_HTTP_PORTS.remove(context.http_port)
-@httpretty.activate
+@httpretty.activate(verbose=True)
@that_with_context(start_http_server, stop_http_server)
def test_httpretty_bypasses_a_unregistered_request(context):
"httpretty should bypass a unregistered request by disabling it"
@@ -143,7 +143,7 @@ def test_httpretty_bypasses_a_unregistered_request(context):
core.POTENTIAL_HTTP_PORTS.remove(context.http_port)
-@httpretty.activate
+@httpretty.activate(verbose=True)
@that_with_context(start_tcp_server, stop_tcp_server)
def test_using_httpretty_with_other_tcp_protocols(context):
"httpretty should work even when testing code that also use other TCP-based protocols"
@@ -163,7 +163,7 @@ def test_using_httpretty_with_other_tcp_protocols(context):
@httpretty.activate(allow_net_connect=False)
@that_with_context(start_http_server, stop_http_server)
-def test_disallow_net_connect_1(context):
+def test_disallow_net_connect_1(context, verbose=True):
"""
When allow_net_connect = False, a request that otherwise
would have worked results in UnmockedError.
diff --git a/tests/functional/test_passthrough.py b/tests/functional/test_passthrough.py
index 5bb3485..14ad2ae 100644
--- a/tests/functional/test_passthrough.py
+++ b/tests/functional/test_passthrough.py
@@ -24,7 +24,6 @@
import requests
import httpretty
-from unittest import skip
from sure import expect
@@ -42,15 +41,15 @@ def test_http_passthrough():
response1 = http().get(url)
- httpretty.enable()
+ httpretty.enable(allow_net_connect=False, verbose=True)
httpretty.register_uri(httpretty.GET, 'http://google.com/', body="Not Google")
- httpretty.register_uri(httpretty.GET, url, body="")
+ httpretty.register_uri(httpretty.GET, url, body="mocked")
response2 = http().get('http://google.com/')
expect(response2.content).to.equal(b'Not Google')
response3 = http().get(url)
- (response3.content).should.equal(response1.content)
+ response3.content.should.equal(b"mocked")
httpretty.disable()
@@ -63,7 +62,7 @@ def test_https_passthrough():
response1 = http().get(url)
- httpretty.enable()
+ httpretty.enable(allow_net_connect=True, verbose=True)
httpretty.register_uri(httpretty.GET, 'https://google.com/', body="Not Google")
httpretty.register_uri(httpretty.GET, url, body="mocked")
diff --git a/tests/functional/test_requests.py b/tests/functional/test_requests.py
index 0ee6ab9..7da9ce3 100644
--- a/tests/functional/test_requests.py
+++ b/tests/functional/test_requests.py
@@ -30,7 +30,6 @@ import signal
import httpretty
from freezegun import freeze_time
-from unittest import skip
from contextlib import contextmanager
from sure import within, microseconds, expect
from tornado import version as tornado_version
@@ -388,7 +387,7 @@ def test_streaming_responses(now):
@httprettified
def test_multiline():
- url = 'http://httpbin.org/post'
+ url = 'https://httpbin.org/post'
data = b'content=Im\r\na multiline\r\n\r\nsentence\r\n'
headers = {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
@@ -402,16 +401,18 @@ def test_multiline():
expect(response.status_code).to.equal(200)
expect(HTTPretty.last_request.method).to.equal('POST')
+ expect(HTTPretty.last_request.url).to.equal('https://httpbin.org/post')
+ expect(HTTPretty.last_request.protocol).to.equal('https')
expect(HTTPretty.last_request.path).to.equal('/post')
expect(HTTPretty.last_request.body).to.equal(data)
expect(HTTPretty.last_request.headers['content-length']).to.equal('37')
expect(HTTPretty.last_request.headers['content-type']).to.equal('application/x-www-form-urlencoded; charset=utf-8')
- expect(len(HTTPretty.latest_requests)).to.equal(1)
+ expect(len(HTTPretty.latest_requests)).to.equal(2)
@httprettified
def test_octet_stream():
- url = 'http://httpbin.org/post'
+ url = 'https://httpbin.org/post'
data = b"\xf5\x00\x00\x00" # utf-8 with invalid start byte
headers = {
'Content-Type': 'application/octet-stream',
@@ -424,16 +425,18 @@ def test_octet_stream():
expect(response.status_code).to.equal(200)
expect(HTTPretty.last_request.method).to.equal('POST')
+ expect(HTTPretty.last_request.url).to.equal('https://httpbin.org/post')
+ expect(HTTPretty.last_request.protocol).to.equal('https')
expect(HTTPretty.last_request.path).to.equal('/post')
expect(HTTPretty.last_request.body).to.equal(data)
expect(HTTPretty.last_request.headers['content-length']).to.equal('4')
expect(HTTPretty.last_request.headers['content-type']).to.equal('application/octet-stream')
- expect(len(HTTPretty.latest_requests)).to.equal(1)
+ expect(len(HTTPretty.latest_requests)).to.equal(2)
@httprettified
def test_multipart():
- url = 'http://httpbin.org/post'
+ url = 'https://httpbin.org/post'
data = b'--xXXxXXyYYzzz\r\nContent-Disposition: form-data; name="content"\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 68\r\n\r\nAction: comment\nText: Comment with attach\nAttachment: x1.txt, x2.txt\r\n--xXXxXXyYYzzz\r\nContent-Disposition: form-data; name="attachment_2"; filename="x.txt"\r\nContent-Type: text/plain\r\nContent-Length: 4\r\n\r\nbye\n\r\n--xXXxXXyYYzzz\r\nContent-Disposition: form-data; name="attachment_1"; filename="x.txt"\r\nContent-Type: text/plain\r\nContent-Length: 4\r\n\r\nbye\n\r\n--xXXxXXyYYzzz--\r\n'
headers = {'Content-Length': '495', 'Content-Type': 'multipart/form-data; boundary=xXXxXXyYYzzz', 'Accept': 'text/plain'}
HTTPretty.register_uri(
@@ -443,11 +446,13 @@ def test_multipart():
response = requests.post(url, data=data, headers=headers)
expect(response.status_code).to.equal(200)
expect(HTTPretty.last_request.method).to.equal('POST')
+ expect(HTTPretty.last_request.url).to.equal('https://httpbin.org/post')
+ expect(HTTPretty.last_request.protocol).to.equal('https')
expect(HTTPretty.last_request.path).to.equal('/post')
expect(HTTPretty.last_request.body).to.equal(data)
expect(HTTPretty.last_request.headers['content-length']).to.equal('495')
expect(HTTPretty.last_request.headers['content-type']).to.equal('multipart/form-data; boundary=xXXxXXyYYzzz')
- expect(len(HTTPretty.latest_requests)).to.equal(1)
+ expect(len(HTTPretty.latest_requests)).to.equal(2)
@httprettified
@@ -621,8 +626,7 @@ def test_httpretty_provides_easy_access_to_querystrings_with_regexes():
})
-@skip('TODO: refactor this flaky test')
-@httprettified
+@httprettified(verbose=True)
def test_httpretty_allows_to_chose_if_querystring_should_be_matched():
"HTTPretty should provide a way to not match regexes that have a different querystring"
diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py
index c73836c..80c4a86 100644
--- a/tests/unit/test_core.py
+++ b/tests/unit/test_core.py
@@ -159,15 +159,16 @@ def test_request_string_representation():
headers = "\r\n".join([
'POST /create HTTP/1.1',
'Content-Type: JPEG-baby',
+ 'Host: blog.falcao.it'
])
# And a valid urlencoded body
body = "foobar:\nlalala"
# When I create a HTTPrettyRequest with that data
- request = HTTPrettyRequest(headers, body)
+ request = HTTPrettyRequest(headers, body, sock=Mock(is_https=True))
# Then its string representation should show the headers and the body
- str(request).should.equal('<HTTPrettyRequest("JPEG-baby", total_headers=1, body_length=14)>')
+ str(request).should.equal('<HTTPrettyRequest("POST", "https://blog.falcao.it/create", headers={\'Content-Type\': \'JPEG-baby\', \'Host\': \'blog.falcao.it\'}, body=14)>')
def test_fake_ssl_socket_proxies_its_ow_socket():
@@ -297,6 +298,7 @@ def test_fakesock_socket_real_sendall(old_socket):
# Given a fake socket
socket = fakesock.socket()
+ socket._address = ('1.2.3.4', 42)
# When I call real_sendall with data, some args and kwargs
socket.real_sendall(b"SOMEDATA", b'some extra args...', foo=b'bar')