diff options
| author | Gabriel Falcao <gabriel@nacaolivre.org> | 2013-10-14 01:10:53 -0400 |
|---|---|---|
| committer | Gabriel Falcao <gabriel@nacaolivre.org> | 2013-10-14 01:10:53 -0400 |
| commit | c88bbe9dea6170788a5f4db4bdb9d2fc119fcab0 (patch) | |
| tree | 2d1be892ada39a99c34d2b325f4c9e2e3a0867b2 /tests | |
| download | httpretty-gh-pages.tar.gz | |
documentationgh-pages
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/__init__.py | 4 | ||||
| -rw-r--r-- | tests/functional/__init__.py | 28 | ||||
| -rw-r--r-- | tests/functional/base.py | 98 | ||||
| -rw-r--r-- | tests/functional/fixtures/.placeholder | 0 | ||||
| -rw-r--r-- | tests/functional/fixtures/playback-1.json | 58 | ||||
| -rw-r--r-- | tests/functional/test_bypass.py | 136 | ||||
| -rw-r--r-- | tests/functional/test_debug.py | 105 | ||||
| -rw-r--r-- | tests/functional/test_decorator.py | 48 | ||||
| -rw-r--r-- | tests/functional/test_httplib2.py | 306 | ||||
| -rw-r--r-- | tests/functional/test_requests.py | 751 | ||||
| -rw-r--r-- | tests/functional/test_urllib2.py | 337 | ||||
| -rw-r--r-- | tests/functional/testserver.py | 171 | ||||
| -rw-r--r-- | tests/unit/__init__.py | 25 | ||||
| -rw-r--r-- | tests/unit/test_core.py | 569 | ||||
| -rw-r--r-- | tests/unit/test_http.py | 17 | ||||
| -rw-r--r-- | tests/unit/test_httpretty.py | 386 | ||||
| -rw-r--r-- | tests/unit/test_main.py | 12 |
17 files changed, 3051 insertions, 0 deletions
diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..9047465 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import sure diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py new file mode 100644 index 0000000..8589d02 --- /dev/null +++ b/tests/functional/__init__.py @@ -0,0 +1,28 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> 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 warnings +warnings.simplefilter('ignore') diff --git a/tests/functional/base.py b/tests/functional/base.py new file mode 100644 index 0000000..4808752 --- /dev/null +++ b/tests/functional/base.py @@ -0,0 +1,98 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> 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. + +from __future__ import unicode_literals +import os +import threading +import traceback +import tornado.ioloop +import tornado.web +from functools import wraps +from sure import scenario +import json +from os.path import abspath, dirname, join +from httpretty.core import POTENTIAL_HTTP_PORTS + + +LOCAL_FILE = lambda *path: join(abspath(dirname(__file__)), *path) +FIXTURE_FILE = lambda name: LOCAL_FILE('fixtures', name) + + +class JSONEchoHandler(tornado.web.RequestHandler): + def get(self, matched): + payload = dict([(x, self.get_argument(x)) for x in self.request.arguments]) + self.write(json.dumps({matched or 'index': payload}, indent=4)) + + def post(self, matched): + payload = dict(self.request.arguments) + self.write(json.dumps({matched or 'index': payload}, indent=4)) + + +class JSONEchoServer(threading.Thread): + def __init__(self, lock, port=8888, *args, **kw): + self.lock = lock + self.port = int(port) + self._stop = threading.Event() + super(JSONEchoServer, self).__init__(*args, **kw) + self.daemon = True + + def stop(self): + self._stop.set() + + def stopped(self): + return self._stop.isSet() + + def setup_application(self): + return tornado.web.Application([ + (r"/(.*)", JSONEchoHandler), + ]) + + def run(self): + application = self.setup_application() + application.listen(self.port) + self.lock.release() + tornado.ioloop.IOLoop.instance().start() + + + +def use_tornado_server(callback): + lock = threading.Lock() + lock.acquire() + + @wraps(callback) + def func(*args, **kw): + server = JSONEchoServer(lock, os.getenv('TEST_PORT', 8888)) + server.start() + try: + lock.acquire() + callback(*args, **kw) + finally: + lock.release() + server.stop() + if 8888 in POTENTIAL_HTTP_PORTS: + POTENTIAL_HTTP_PORTS.remove(8888) + return func diff --git a/tests/functional/fixtures/.placeholder b/tests/functional/fixtures/.placeholder new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/functional/fixtures/.placeholder diff --git a/tests/functional/fixtures/playback-1.json b/tests/functional/fixtures/playback-1.json new file mode 100644 index 0000000..1eef307 --- /dev/null +++ b/tests/functional/fixtures/playback-1.json @@ -0,0 +1,58 @@ +[ + { + "request": { + "body": "", + "headers": { + "host": "localhost:8888", + "accept-encoding": "gzip, deflate, compress", + "content-length": "0", + "accept": "*/*", + "user-agent": "python-requests/1.1.0 CPython/2.7.5 Darwin/12.5.0" + }, + "querystring": { + "age": [ + "25" + ], + "name": [ + "Gabriel" + ] + }, + "uri": "http://localhost:8888/foobar?name=Gabriel&age=25", + "method": "GET" + }, + "response": { + "status": 200, + "body": "{\n \"foobar\": {\n \"age\": \"25\", \n \"name\": \"Gabriel\"\n }\n}", + "headers": { + "content-length": "73", + "etag": "\"6fdccaba6542114e7d1098d22a01623dc2aa5761\"", + "content-type": "text/html; charset=UTF-8", + "server": "TornadoServer/2.4" + } + } + }, + { + "request": { + "body": "{\"test\": \"123\"}", + "headers": { + "host": "localhost:8888", + "accept-encoding": "gzip, deflate, compress", + "content-length": "15", + "accept": "*/*", + "user-agent": "python-requests/1.1.0 CPython/2.7.5 Darwin/12.5.0" + }, + "querystring": {}, + "uri": "http://localhost:8888/foobar", + "method": "POST" + }, + "response": { + "status": 200, + "body": "{\n \"foobar\": {}\n}", + "headers": { + "content-length": "20", + "content-type": "text/html; charset=UTF-8", + "server": "TornadoServer/2.4" + } + } + } +]
\ No newline at end of file diff --git a/tests/functional/test_bypass.py b/tests/functional/test_bypass.py new file mode 100644 index 0000000..6409eb5 --- /dev/null +++ b/tests/functional/test_bypass.py @@ -0,0 +1,136 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <httpretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> 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. +from __future__ import unicode_literals + +try: + import urllib.request as urllib2 +except ImportError: + import urllib2 + +from .testserver import TornadoServer, TCPServer, TCPClient +from sure import expect, that_with_context + +import httpretty +from httpretty import core + + +def start_http_server(context): + context.server = TornadoServer(9999) + context.server.start() + httpretty.enable() + + +def stop_http_server(context): + context.server.stop() + httpretty.enable() + + +def start_tcp_server(context): + context.server = TCPServer(8888) + context.server.start() + context.client = TCPClient(8888) + httpretty.enable() + + +def stop_tcp_server(context): + context.server.stop() + context.client.close() + httpretty.enable() + + +@httpretty.activate +@that_with_context(start_http_server, stop_http_server) +def test_httpretty_bypasses_when_disabled(context): + "httpretty should bypass all requests by disabling it" + + httpretty.register_uri( + httpretty.GET, "http://localhost:9999/go-for-bubbles/", + body="glub glub") + + httpretty.disable() + + fd = urllib2.urlopen('http://localhost:9999/go-for-bubbles/') + got1 = fd.read() + fd.close() + + expect(got1).to.equal( + 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 .') + + fd = urllib2.urlopen('http://localhost:9999/come-again/') + got2 = fd.read() + fd.close() + + expect(got2).to.equal(b'<- HELLO WORLD ->') + + httpretty.enable() + + fd = urllib2.urlopen('http://localhost:9999/go-for-bubbles/') + got3 = fd.read() + fd.close() + + expect(got3).to.equal(b'glub glub') + core.POTENTIAL_HTTP_PORTS.remove(9999) + +@httpretty.activate +@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" + + httpretty.register_uri( + httpretty.GET, "http://localhost:9999/go-for-bubbles/", + body="glub glub") + + fd = urllib2.urlopen('http://localhost:9999/go-for-bubbles/') + got1 = fd.read() + fd.close() + + expect(got1).to.equal(b'glub glub') + + fd = urllib2.urlopen('http://localhost:9999/come-again/') + got2 = fd.read() + fd.close() + + expect(got2).to.equal(b'<- HELLO WORLD ->') + core.POTENTIAL_HTTP_PORTS.remove(9999) + + +@httpretty.activate +@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" + + httpretty.register_uri( + httpretty.GET, "http://falcao.it/foo/", + body="BAR") + + fd = urllib2.urlopen('http://falcao.it/foo/') + got1 = fd.read() + fd.close() + + expect(got1).to.equal(b'BAR') + + expect(context.client.send("foobar")).to.equal(b"RECEIVED: foobar") diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py new file mode 100644 index 0000000..1562be6 --- /dev/null +++ b/tests/functional/test_debug.py @@ -0,0 +1,105 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> 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. +from __future__ import unicode_literals +import socket +from sure import scenario, expect +from httpretty import httprettified + + +def create_socket(context): + context.sock = socket.socket( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + ) + context.sock.is_http = True + + +@httprettified +@scenario(create_socket) +def test_httpretty_debugs_socket_send(context): + "HTTPretty should debug socket.send" + + expect(context.sock.send).when.called.to.throw( + RuntimeError, + "HTTPretty intercepted and unexpected socket method call." + ) + + +@httprettified +@scenario(create_socket) +def test_httpretty_debugs_socket_sendto(context): + "HTTPretty should debug socket.sendto" + + expect(context.sock.sendto).when.called.to.throw( + RuntimeError, + "HTTPretty intercepted and unexpected socket method call." + ) + + +@httprettified +@scenario(create_socket) +def test_httpretty_debugs_socket_recv(context): + "HTTPretty should debug socket.recv" + + expect(context.sock.recv).when.called.to.throw( + RuntimeError, + "HTTPretty intercepted and unexpected socket method call." + ) + + +@httprettified +@scenario(create_socket) +def test_httpretty_debugs_socket_recvfrom(context): + "HTTPretty should debug socket.recvfrom" + + expect(context.sock.recvfrom).when.called.to.throw( + RuntimeError, + "HTTPretty intercepted and unexpected socket method call." + ) + + +@httprettified +@scenario(create_socket) +def test_httpretty_debugs_socket_recv_into(context): + "HTTPretty should debug socket.recv_into" + + expect(context.sock.recv_into).when.called.to.throw( + RuntimeError, + "HTTPretty intercepted and unexpected socket method call." + ) + + +@httprettified +@scenario(create_socket) +def test_httpretty_debugs_socket_recvfrom_into(context): + "HTTPretty should debug socket.recvfrom_into" + + expect(context.sock.recvfrom_into).when.called.to.throw( + RuntimeError, + "HTTPretty intercepted and unexpected socket method call." + ) diff --git a/tests/functional/test_decorator.py b/tests/functional/test_decorator.py new file mode 100644 index 0000000..65f6f2a --- /dev/null +++ b/tests/functional/test_decorator.py @@ -0,0 +1,48 @@ +# coding: utf-8 +from unittest import TestCase +from sure import expect +from httpretty import httprettified, HTTPretty + +try: + import urllib.request as urllib2 +except ImportError: + import urllib2 + + +@httprettified +def test_decor(): + HTTPretty.register_uri( + HTTPretty.GET, "http://localhost/", + body="glub glub") + + fd = urllib2.urlopen('http://localhost/') + got1 = fd.read() + fd.close() + + expect(got1).to.equal(b'glub glub') + + +@httprettified +class ClassDecorator(TestCase): + + def test_decorated(self): + HTTPretty.register_uri( + HTTPretty.GET, "http://localhost/", + body="glub glub") + + fd = urllib2.urlopen('http://localhost/') + got1 = fd.read() + fd.close() + + expect(got1).to.equal(b'glub glub') + + def test_decorated2(self): + HTTPretty.register_uri( + HTTPretty.GET, "http://localhost/", + body="buble buble") + + fd = urllib2.urlopen('http://localhost/') + got1 = fd.read() + fd.close() + + expect(got1).to.equal(b'buble buble')
\ No newline at end of file diff --git a/tests/functional/test_httplib2.py b/tests/functional/test_httplib2.py new file mode 100644 index 0000000..11baedc --- /dev/null +++ b/tests/functional/test_httplib2.py @@ -0,0 +1,306 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> 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. +from __future__ import unicode_literals + +import re +import httplib2 +from sure import expect, within, microseconds +from httpretty import HTTPretty, httprettified +from httpretty.core import decode_utf8 + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_mock_a_simple_get_with_httplib2_read(now): + "HTTPretty should mock a simple GET with httplib2.context.http" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + _, got = httplib2.Http().request('http://yipit.com', 'GET') + expect(got).to.equal(b'Find the best daily deals') + + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/') + + +@httprettified +@within(two=microseconds) +def test_httpretty_provides_easy_access_to_querystrings(now): + "HTTPretty should provide an easy access to the querystring" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + httplib2.Http().request('http://yipit.com?foo=bar&foo=baz&chuck=norris', 'GET') + expect(HTTPretty.last_request.querystring).to.equal({ + 'foo': ['bar', 'baz'], + 'chuck': ['norris'], + }) + + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_mock_headers_httplib2(now): + "HTTPretty should mock basic headers with httplib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", + body="this is supposed to be the response", + status=201) + + headers, _ = httplib2.Http().request('http://github.com', 'GET') + expect(headers['status']).to.equal('201') + expect(dict(headers)).to.equal({ + 'content-type': 'text/plain; charset=utf-8', + 'connection': 'close', + 'content-length': '35', + 'status': '201', + 'server': 'Python/HTTPretty', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_adding_and_overwritting_httplib2(now): + "HTTPretty should allow adding and overwritting headers with httplib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", + body="this is supposed to be the response", + adding_headers={ + 'Server': 'Apache', + 'Content-Length': '27', + 'Content-Type': 'application/json', + }) + + headers, _ = httplib2.Http().request('http://github.com/foo', 'GET') + + expect(dict(headers)).to.equal({ + 'content-type': 'application/json', + 'content-location': 'http://github.com/foo', + 'connection': 'close', + 'content-length': '27', + 'status': '200', + 'server': 'Apache', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_forcing_headers_httplib2(now): + "HTTPretty should allow forcing headers with httplib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", + body="this is supposed to be the response", + forcing_headers={ + 'Content-Type': 'application/xml', + }) + + headers, _ = httplib2.Http().request('http://github.com/foo', 'GET') + + expect(dict(headers)).to.equal({ + 'content-location': 'http://github.com/foo', # httplib2 FORCES + # content-location + # even if the + # server does not + # provide it + 'content-type': 'application/xml', + 'status': '200', # httplib2 also ALWAYS put status on headers + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_adding_and_overwritting_by_kwargs_u2(now): + "HTTPretty should allow adding and overwritting headers by keyword args " \ + "with httplib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", + body="this is supposed to be the response", + server='Apache', + content_length='27', + content_type='application/json') + + headers, _ = httplib2.Http().request('http://github.com/foo', 'GET') + + expect(dict(headers)).to.equal({ + 'content-type': 'application/json', + 'content-location': 'http://github.com/foo', # httplib2 FORCES + # content-location + # even if the + # server does not + # provide it + 'connection': 'close', + 'content-length': '27', + 'status': '200', + 'server': 'Apache', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_rotating_responses_with_httplib2(now): + "HTTPretty should support rotating responses with httplib2" + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + responses=[ + HTTPretty.Response(body="first response", status=201), + HTTPretty.Response(body='second and last response', status=202), + ]) + + headers1, body1 = httplib2.Http().request( + 'https://api.yahoo.com/test', 'GET') + + expect(headers1['status']).to.equal('201') + expect(body1).to.equal(b'first response') + + headers2, body2 = httplib2.Http().request( + 'https://api.yahoo.com/test', 'GET') + + expect(headers2['status']).to.equal('202') + expect(body2).to.equal(b'second and last response') + + headers3, body3 = httplib2.Http().request( + 'https://api.yahoo.com/test', 'GET') + + expect(headers3['status']).to.equal('202') + expect(body3).to.equal(b'second and last response') + + +@httprettified +@within(two=microseconds) +def test_can_inspect_last_request(now): + "HTTPretty.last_request is a mimetools.Message request from last match" + + HTTPretty.register_uri(HTTPretty.POST, "http://api.github.com/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + headers, body = httplib2.Http().request( + 'http://api.github.com', 'POST', + body='{"username": "gabrielfalcao"}', + headers={ + 'content-type': 'text/json', + }, + ) + + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.body).to.equal( + b'{"username": "gabrielfalcao"}', + ) + expect(HTTPretty.last_request.headers['content-type']).to.equal( + 'text/json', + ) + expect(body).to.equal(b'{"repositories": ["HTTPretty", "lettuce"]}') + + +@httprettified +@within(two=microseconds) +def test_can_inspect_last_request_with_ssl(now): + "HTTPretty.last_request is recorded even when mocking 'https' (SSL)" + + HTTPretty.register_uri(HTTPretty.POST, "https://secure.github.com/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + headers, body = httplib2.Http().request( + 'https://secure.github.com', 'POST', + body='{"username": "gabrielfalcao"}', + headers={ + 'content-type': 'text/json', + }, + ) + + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.body).to.equal( + b'{"username": "gabrielfalcao"}', + ) + expect(HTTPretty.last_request.headers['content-type']).to.equal( + 'text/json', + ) + expect(body).to.equal(b'{"repositories": ["HTTPretty", "lettuce"]}') + + +@httprettified +@within(two=microseconds) +def test_httpretty_ignores_querystrings_from_registered_uri(now): + "Registering URIs with query string cause them to be ignored" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/?id=123", + body="Find the best daily deals") + + _, got = httplib2.Http().request('http://yipit.com/?id=123', 'GET') + + expect(got).to.equal(b'Find the best daily deals') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/?id=123') + + +@httprettified +@within(two=microseconds) +def test_callback_response(now): + ("HTTPretty should all a callback function to be set as the body with" + " httplib2") + + def request_callback(request, uri, headers): + return [200,headers,"The {0} response from {1}".format(decode_utf8(request.method), uri)] + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + body=request_callback) + + headers1, body1 = httplib2.Http().request( + 'https://api.yahoo.com/test', 'GET') + + expect(body1).to.equal(b"The GET response from https://api.yahoo.com/test") + + HTTPretty.register_uri( + HTTPretty.POST, "https://api.yahoo.com/test_post", + body=request_callback) + + headers2, body2 = httplib2.Http().request( + 'https://api.yahoo.com/test_post', 'POST') + + expect(body2).to.equal(b"The POST response from https://api.yahoo.com/test_post") + + +@httprettified +def test_httpretty_should_allow_registering_regexes(): + "HTTPretty should allow registering regexes with httplib2" + + HTTPretty.register_uri( + HTTPretty.GET, + re.compile("https://api.yipit.com/v1/deal;brand=(?P<brand_name>\w+)"), + body="Found brand", + ) + + response, body = httplib2.Http().request('https://api.yipit.com/v1/deal;brand=gap', 'GET') + expect(body).to.equal(b'Found brand') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/v1/deal;brand=gap') diff --git a/tests/functional/test_requests.py b/tests/functional/test_requests.py new file mode 100644 index 0000000..0a4bf55 --- /dev/null +++ b/tests/functional/test_requests.py @@ -0,0 +1,751 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> 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. + +from __future__ import unicode_literals + +import os +import re +import json +import requests +from sure import within, microseconds, expect +from httpretty import HTTPretty, httprettified +from httpretty.core import decode_utf8 + +from base import FIXTURE_FILE, use_tornado_server + +try: + xrange = xrange +except NameError: + xrange = range + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + +PORT = int(os.getenv('TEST_PORT') or 8888) +server_url = lambda path: "http://localhost:{0}/{1}".format(PORT, path.lstrip('/')) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_mock_a_simple_get_with_requests_read(now): + "HTTPretty should mock a simple GET with requests.get" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + response = requests.get('http://yipit.com') + expect(response.text).to.equal('Find the best daily deals') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/') + + +@httprettified +@within(two=microseconds) +def test_httpretty_provides_easy_access_to_querystrings(now): + "HTTPretty should provide an easy access to the querystring" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + requests.get('http://yipit.com/?foo=bar&foo=baz&chuck=norris') + expect(HTTPretty.last_request.querystring).to.equal({ + 'foo': ['bar', 'baz'], + 'chuck': ['norris'], + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_mock_headers_requests(now): + "HTTPretty should mock basic headers with requests" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", + body="this is supposed to be the response", + status=201) + + response = requests.get('http://github.com') + expect(response.status_code).to.equal(201) + + expect(dict(response.headers)).to.equal({ + 'content-type': 'text/plain; charset=utf-8', + 'connection': 'close', + 'content-length': '35', + 'status': '201', + 'server': 'Python/HTTPretty', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_adding_and_overwritting_requests(now): + "HTTPretty should allow adding and overwritting headers with requests" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", + body="this is supposed to be the response", + adding_headers={ + 'Server': 'Apache', + 'Content-Length': '27', + 'Content-Type': 'application/json', + }) + + response = requests.get('http://github.com/foo') + + expect(dict(response.headers)).to.equal({ + 'content-type': 'application/json', + 'connection': 'close', + 'content-length': '27', + 'status': '200', + 'server': 'Apache', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_forcing_headers_requests(now): + "HTTPretty should allow forcing headers with requests" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", + body="<root><baz /</root>", + forcing_headers={ + 'Content-Type': 'application/xml', + 'Content-Length': '19', + }) + + response = requests.get('http://github.com/foo') + + expect(dict(response.headers)).to.equal({ + 'content-type': 'application/xml', + 'content-length': '19', + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_adding_and_overwritting_by_kwargs_u2(now): + "HTTPretty should allow adding and overwritting headers by keyword args " \ + "with requests" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/foo", + body="this is supposed to be the response", + server='Apache', + content_length='27', + content_type='application/json') + + response = requests.get('http://github.com/foo') + + expect(dict(response.headers)).to.equal({ + 'content-type': 'application/json', + 'connection': 'close', + 'content-length': '27', + 'status': '200', + 'server': 'Apache', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_rotating_responses_with_requests(now): + "HTTPretty should support rotating responses with requests" + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + responses=[ + HTTPretty.Response(body=b"first response", status=201), + HTTPretty.Response(body=b'second and last response', status=202), + ]) + + response1 = requests.get( + 'https://api.yahoo.com/test') + + expect(response1.status_code).to.equal(201) + expect(response1.text).to.equal('first response') + + response2 = requests.get( + 'https://api.yahoo.com/test') + + expect(response2.status_code).to.equal(202) + expect(response2.text).to.equal('second and last response') + + response3 = requests.get( + 'https://api.yahoo.com/test') + + expect(response3.status_code).to.equal(202) + expect(response3.text).to.equal('second and last response') + + +@httprettified +@within(two=microseconds) +def test_can_inspect_last_request(now): + "HTTPretty.last_request is a mimetools.Message request from last match" + + HTTPretty.register_uri(HTTPretty.POST, "http://api.github.com/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + response = requests.post( + 'http://api.github.com', + '{"username": "gabrielfalcao"}', + headers={ + 'content-type': 'text/json', + }, + ) + + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.body).to.equal( + b'{"username": "gabrielfalcao"}', + ) + expect(HTTPretty.last_request.headers['content-type']).to.equal( + 'text/json', + ) + expect(response.json()).to.equal({"repositories": ["HTTPretty", "lettuce"]}) + + +@httprettified +@within(two=microseconds) +def test_can_inspect_last_request_with_ssl(now): + "HTTPretty.last_request is recorded even when mocking 'https' (SSL)" + + HTTPretty.register_uri(HTTPretty.POST, "https://secure.github.com/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + response = requests.post( + 'https://secure.github.com', + '{"username": "gabrielfalcao"}', + headers={ + 'content-type': 'text/json', + }, + ) + + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.body).to.equal( + b'{"username": "gabrielfalcao"}', + ) + expect(HTTPretty.last_request.headers['content-type']).to.equal( + 'text/json', + ) + expect(response.json()).to.equal({"repositories": ["HTTPretty", "lettuce"]}) + + +@httprettified +@within(two=microseconds) +def test_httpretty_ignores_querystrings_from_registered_uri(now): + "HTTPretty should ignore querystrings from the registered uri (requests library)" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/?id=123", + body=b"Find the best daily deals") + + response = requests.get('http://yipit.com/', params={'id': 123}) + expect(response.text).to.equal('Find the best daily deals') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/?id=123') + + +@httprettified +@within(five=microseconds) +def test_streaming_responses(now): + """ + Mock a streaming HTTP response, like those returned by the Twitter streaming + API. + """ + from contextlib import contextmanager + + @contextmanager + def in_time(time, message): + """ + A context manager that uses signals to force a time limit in tests + (unlike the `@within` decorator, which only complains afterward), or + raise an AssertionError. + """ + import signal + + def handler(signum, frame): + raise AssertionError(message) + signal.signal(signal.SIGALRM, handler) + signal.setitimer(signal.ITIMER_REAL, time) + yield + signal.setitimer(signal.ITIMER_REAL, 0) + + #XXX this obviously isn't a fully functional twitter streaming client! + twitter_response_lines = [ + b'{"text":"If \\"for the boobs\\" requests to follow me one more time I\'m calling the police. http://t.co/a0mDEAD8"}\r\n', + b'\r\n', + b'{"text":"RT @onedirection: Thanks for all your #FollowMe1D requests Directioners! We\u2019ll be following 10 people throughout the day starting NOW. G ..."}\r\n' + ] + + TWITTER_STREAMING_URL = "https://stream.twitter.com/1/statuses/filter.json" + + HTTPretty.register_uri(HTTPretty.POST, TWITTER_STREAMING_URL, + body=(l for l in twitter_response_lines), + streaming=True) + + # taken from the requests docs + # Http://docs.python-requests.org/en/latest/user/advanced/#streaming-requests + response = requests.post(TWITTER_STREAMING_URL, data={'track': 'requests'}, + auth=('username', 'password'), stream=True) + + #test iterating by line + line_iter = response.iter_lines() + with in_time(0.01, 'Iterating by line is taking forever!'): + for i in xrange(len(twitter_response_lines)): + expect(next(line_iter).strip()).to.equal( + twitter_response_lines[i].strip()) + + #test iterating by line after a second request + response = requests.post(TWITTER_STREAMING_URL, data={'track': 'requests'}, + auth=('username', 'password'), stream=True) + + line_iter = response.iter_lines() + with in_time(0.01, 'Iterating by line is taking forever the second time ' + 'around!'): + for i in xrange(len(twitter_response_lines)): + expect(next(line_iter).strip()).to.equal( + twitter_response_lines[i].strip()) + + #test iterating by char + response = requests.post(TWITTER_STREAMING_URL, data={'track': 'requests'}, + auth=('username', 'password'), stream=True) + + twitter_expected_response_body = b''.join(twitter_response_lines) + with in_time(0.02, 'Iterating by char is taking forever!'): + twitter_body = b''.join(c for c in response.iter_content(chunk_size=1)) + + expect(twitter_body).to.equal(twitter_expected_response_body) + + #test iterating by chunks larger than the stream + + response = requests.post(TWITTER_STREAMING_URL, data={'track': 'requests'}, + auth=('username', 'password'), stream=True) + + with in_time(0.02, 'Iterating by large chunks is taking forever!'): + twitter_body = b''.join(c for c in + response.iter_content(chunk_size=1024)) + + expect(twitter_body).to.equal(twitter_expected_response_body) + + +@httprettified +def test_multiline(): + url = 'http://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', + 'Accept': 'text/plain', + } + HTTPretty.register_uri( + HTTPretty.POST, + url, + ) + 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.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) + + +@httprettified +def test_multipart(): + url = 'http://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( + HTTPretty.POST, + url, + ) + 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.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) + + +@httprettified +@within(two=microseconds) +def test_callback_response(now): + ("HTTPretty should call a callback function and set its return value as the body of the response" + " requests") + + def request_callback(request, uri, headers): + return [200, headers,"The {0} response from {1}".format(decode_utf8(request.method), uri)] + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + body=request_callback) + + response = requests.get('https://api.yahoo.com/test') + + expect(response.text).to.equal("The GET response from https://api.yahoo.com/test") + + HTTPretty.register_uri( + HTTPretty.POST, "https://api.yahoo.com/test_post", + body=request_callback) + + response = requests.post( + "https://api.yahoo.com/test_post", + {"username": "gabrielfalcao"} + ) + + expect(response.text).to.equal("The POST response from https://api.yahoo.com/test_post") + +@httprettified +@within(two=microseconds) +def test_callback_body_remains_callable_for_any_subsequent_requests(now): + ("HTTPretty should call a callback function more than one" + " requests") + + def request_callback(request, uri, headers): + return [200, headers,"The {0} response from {1}".format(decode_utf8(request.method), uri)] + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + body=request_callback) + + response = requests.get('https://api.yahoo.com/test') + expect(response.text).to.equal("The GET response from https://api.yahoo.com/test") + + response = requests.get('https://api.yahoo.com/test') + expect(response.text).to.equal("The GET response from https://api.yahoo.com/test") + +@httprettified +@within(two=microseconds) +def test_callback_setting_headers_and_status_response(now): + ("HTTPretty should call a callback function and uses it retur tuple as status code, headers and body" + " requests") + + def request_callback(request, uri, headers): + headers.update({'a':'b'}) + return [418,headers,"The {0} response from {1}".format(decode_utf8(request.method), uri)] + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + body=request_callback) + + response = requests.get('https://api.yahoo.com/test') + expect(response.text).to.equal("The GET response from https://api.yahoo.com/test") + expect(response.headers).to.have.key('a').being.equal("b") + expect(response.status_code).to.be(418) + + HTTPretty.register_uri( + HTTPretty.POST, "https://api.yahoo.com/test_post", + body=request_callback) + + response = requests.post( + "https://api.yahoo.com/test_post", + {"username": "gabrielfalcao"} + ) + + expect(response.text).to.equal("The POST response from https://api.yahoo.com/test_post") + expect(response.headers).to.have.key('a').being.equal("b") + expect(response.status_code).to.be(418) + +@httprettified +def test_httpretty_should_allow_registering_regexes_and_give_a_proper_match_to_the_callback(): + "HTTPretty should allow registering regexes with requests and giva a proper match to the callback" + + HTTPretty.register_uri( + HTTPretty.GET, + re.compile("https://api.yipit.com/v1/deal;brand=(?P<brand_name>\w+)"), + body=lambda method,uri,headers: [200,headers,uri] + ) + + response = requests.get('https://api.yipit.com/v1/deal;brand=gap?first_name=chuck&last_name=norris') + + expect(response.text).to.equal('https://api.yipit.com/v1/deal;brand=gap?first_name=chuck&last_name=norris') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/v1/deal;brand=gap?first_name=chuck&last_name=norris') + +@httprettified +def test_httpretty_should_allow_registering_regexes(): + "HTTPretty should allow registering regexes with requests" + + HTTPretty.register_uri( + HTTPretty.GET, + re.compile("https://api.yipit.com/v1/deal;brand=(?P<brand_name>\w+)"), + body="Found brand", + ) + + response = requests.get('https://api.yipit.com/v1/deal;brand=gap?first_name=chuck&last_name=norris' + ) + expect(response.text).to.equal('Found brand') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/v1/deal;brand=gap?first_name=chuck&last_name=norris') + + +@httprettified +def test_httpretty_provides_easy_access_to_querystrings_with_regexes(): + "HTTPretty should match regexes even if they have a different querystring" + + HTTPretty.register_uri( + HTTPretty.GET, + re.compile("https://api.yipit.com/v1/(?P<endpoint>\w+)/$"), + body="Find the best daily deals" + ) + + response = requests.get('https://api.yipit.com/v1/deals/?foo=bar&foo=baz&chuck=norris') + expect(response.text).to.equal("Find the best daily deals") + expect(HTTPretty.last_request.querystring).to.equal({ + 'foo': ['bar', 'baz'], + 'chuck': ['norris'], + }) + + +@httprettified +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" + + HTTPretty.register_uri( + HTTPretty.GET, + re.compile("https://example.org/(?P<endpoint>\w+)/$"), + body="Nudge, nudge, wink, wink. Know what I mean?", + match_querystring=True + ) + + response = requests.get('https://example.org/what/') + expect(response.text).to.equal('Nudge, nudge, wink, wink. Know what I mean?') + try: + requests.get('https://example.org/what/?flying=coconuts') + raised = False + except requests.ConnectionError: + raised = True + + assert raised is True + + +@httprettified +def test_httpretty_should_allow_multiple_methods_for_the_same_uri(): + "HTTPretty should allow registering multiple methods for the same uri" + + url = 'http://test.com/test' + methods = ['GET', 'POST', 'PUT', 'OPTIONS'] + for method in methods: + HTTPretty.register_uri( + getattr(HTTPretty, method), + url, + method + ) + + for method in methods: + request_action = getattr(requests, method.lower()) + expect(request_action(url).text).to.equal(method) + + +@httprettified +def test_httpretty_should_allow_registering_regexes_with_streaming_responses(): + "HTTPretty should allow registering regexes with streaming responses" + import os + os.environ['DEBUG'] = 'true' + + def my_callback(request, url, headers): + request.body.should.equal('hithere') + return 200, headers, "Received" + + HTTPretty.register_uri( + HTTPretty.POST, + re.compile("https://api.yipit.com/v1/deal;brand=(?P<brand_name>\w+)"), + body=my_callback, + ) + + def gen(): + yield b'hi' + yield b'there' + + response = requests.post( + 'https://api.yipit.com/v1/deal;brand=gap?first_name=chuck&last_name=norris', + data=gen(), + ) + expect(response.content).to.equal("Received") + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.path).to.equal('/v1/deal;brand=gap?first_name=chuck&last_name=norris') + + +@httprettified +def test_httpretty_should_allow_multiple_responses_with_multiple_methods(): + "HTTPretty should allow multiple responses when binding multiple methods to the same uri" + + url = 'http://test.com/list' + + #add get responses + HTTPretty.register_uri(HTTPretty.GET, url, + responses=[HTTPretty.Response(body='a'), + HTTPretty.Response(body='b') + ] + ) + + #add post responses + HTTPretty.register_uri(HTTPretty.POST, url, + responses=[HTTPretty.Response(body='c'), + HTTPretty.Response(body='d') + ] + ) + + expect(requests.get(url).text).to.equal('a') + expect(requests.post(url).text).to.equal('c') + + expect(requests.get(url).text).to.equal('b') + expect(requests.get(url).text).to.equal('b') + expect(requests.get(url).text).to.equal('b') + + expect(requests.post(url).text).to.equal('d') + expect(requests.post(url).text).to.equal('d') + expect(requests.post(url).text).to.equal('d') + + +@httprettified +def test_httpretty_should_normalize_url_patching(): + "HTTPretty should normalize all url patching" + + HTTPretty.register_uri( + HTTPretty.GET, + "http://yipit.com/foo(bar)", + body="Find the best daily deals") + + response = requests.get('http://yipit.com/foo%28bar%29') + expect(response.text).to.equal('Find the best daily deals') + + +@httprettified +def test_lack_of_trailing_slash(): + ("HTTPretty should automatically append a slash to given urls") + url = 'http://www.youtube.com' + HTTPretty.register_uri(HTTPretty.GET, url, body='') + response = requests.get(url) + response.status_code.should.equal(200) + + +@httprettified +def test_unicode_querystrings(): + ("Querystrings should accept unicode characters") + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/login", + body="Find the best daily deals") + requests.get('http://yipit.com/login?user=Gabriel+Falcão') + expect(HTTPretty.last_request.querystring['user'][0]).should.be.equal('Gabriel Falcão') + + +@use_tornado_server +def test_recording_calls(): + ("HTTPretty should be able to record calls") + # Given a destination path: + destination = FIXTURE_FILE("recording-.json") + + # When I record some calls + with HTTPretty.record(destination): + requests.get(server_url("/foobar?name=Gabriel&age=25")) + requests.post(server_url("/foobar"), data=json.dumps({'test': '123'})) + + # Then the destination path should exist + os.path.exists(destination).should.be.true + + # And the contents should be json + raw = open(destination).read() + json.loads.when.called_with(raw).should_not.throw(ValueError) + + # And the contents should be expected + data = json.loads(raw) + data.should.be.a(list) + data.should.have.length_of(2) + + # And the responses should have the expected keys + response = data[0] + response.should.have.key("request").being.length_of(5) + response.should.have.key("response").being.length_of(3) + + response['request'].should.have.key("method").being.equal("GET") + response['request'].should.have.key("headers").being.a(dict) + response['request'].should.have.key("querystring").being.equal({ + "age": [ + "25" + ], + "name": [ + "Gabriel" + ] + }) + response['response'].should.have.key("status").being.equal(200) + response['response'].should.have.key("body").being.an(unicode) + response['response'].should.have.key("headers").being.a(dict) + response['response']["headers"].should.have.key("server").being.equal("TornadoServer/2.4") + + +def test_playing_calls(): + ("HTTPretty should be able to record calls") + # Given a destination path: + destination = FIXTURE_FILE("playback-1.json") + + # When I playback some previously recorded calls + with HTTPretty.playback(destination): + # And make the expected requests + response1 = requests.get(server_url("/foobar?name=Gabriel&age=25")) + response2 = requests.post(server_url("/foobar"), data=json.dumps({'test': '123'})) + + # Then the responses should be the expected + response1.json().should.equal({"foobar": {"age": "25", "name": "Gabriel"}}) + response2.json().should.equal({"foobar": {}}) + + +@httprettified +def test_py26_callback_response(): + ("HTTPretty should call a callback function *once* and set its return value" + " as the body of the response requests") + + from mock import Mock + + def _request_callback(request, uri, headers): + return [200, headers,"The {0} response from {1}".format(decode_utf8(request.method), uri)] + + request_callback = Mock() + request_callback.side_effect = _request_callback + + HTTPretty.register_uri( + HTTPretty.POST, "https://api.yahoo.com/test_post", + body=request_callback) + + response = requests.post( + "https://api.yahoo.com/test_post", + {"username": "gabrielfalcao"} + ) + os.environ['STOP'] = 'true' + expect(request_callback.call_count).equal(1) + + +import json + + +def hello(): + return json.dumps({ + 'href': 'http://foobar.com' + }) diff --git a/tests/functional/test_urllib2.py b/tests/functional/test_urllib2.py new file mode 100644 index 0000000..cb84f00 --- /dev/null +++ b/tests/functional/test_urllib2.py @@ -0,0 +1,337 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> 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. +from __future__ import unicode_literals + +try: + from urllib.request import urlopen + import urllib.request as urllib2 +except ImportError: + import urllib2 + urlopen = urllib2.urlopen + +from sure import * +from httpretty import HTTPretty, httprettified +from httpretty.core import decode_utf8 + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_mock_a_simple_get_with_urllib2_read(): + "HTTPretty should mock a simple GET with urllib2.read()" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + fd = urlopen('http://yipit.com') + got = fd.read() + fd.close() + + expect(got).to.equal(b'Find the best daily deals') + + +@httprettified +@within(two=microseconds) +def test_httpretty_provides_easy_access_to_querystrings(now): + "HTTPretty should provide an easy access to the querystring" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + fd = urllib2.urlopen('http://yipit.com/?foo=bar&foo=baz&chuck=norris') + fd.read() + fd.close() + + expect(HTTPretty.last_request.querystring).to.equal({ + 'foo': ['bar', 'baz'], + 'chuck': ['norris'], + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_mock_headers_urllib2(now): + "HTTPretty should mock basic headers with urllib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", + body="this is supposed to be the response", + status=201) + + request = urlopen('http://github.com') + + headers = dict(request.headers) + request.close() + + expect(request.code).to.equal(201) + expect(headers).to.equal({ + 'content-type': 'text/plain; charset=utf-8', + 'connection': 'close', + 'content-length': '35', + 'status': '201', + 'server': 'Python/HTTPretty', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_adding_and_overwritting_urllib2(now): + "HTTPretty should allow adding and overwritting headers with urllib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", + body="this is supposed to be the response", + adding_headers={ + 'Server': 'Apache', + 'Content-Length': '27', + 'Content-Type': 'application/json', + }) + + request = urlopen('http://github.com') + headers = dict(request.headers) + request.close() + + expect(request.code).to.equal(200) + expect(headers).to.equal({ + 'content-type': 'application/json', + 'connection': 'close', + 'content-length': '27', + 'status': '200', + 'server': 'Apache', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_forcing_headers_urllib2(): + "HTTPretty should allow forcing headers with urllib2" + + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", + body="this is supposed to be the response", + forcing_headers={ + 'Content-Type': 'application/xml', + }) + + request = urlopen('http://github.com') + headers = dict(request.headers) + request.close() + + expect(headers).to.equal({ + 'content-type': 'application/xml', + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_allow_adding_and_overwritting_by_kwargs_u2(now): + "HTTPretty should allow adding and overwritting headers by " \ + "keyword args with urllib2" + + body = "this is supposed to be the response, indeed" + HTTPretty.register_uri(HTTPretty.GET, "http://github.com/", + body=body, + server='Apache', + content_length=len(body), + content_type='application/json') + + request = urlopen('http://github.com') + headers = dict(request.headers) + request.close() + + expect(request.code).to.equal(200) + expect(headers).to.equal({ + 'content-type': 'application/json', + 'connection': 'close', + 'content-length': str(len(body)), + 'status': '200', + 'server': 'Apache', + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + }) + + +@httprettified +@within(two=microseconds) +def test_httpretty_should_support_a_list_of_successive_responses_urllib2(now): + "HTTPretty should support adding a list of successive " \ + "responses with urllib2" + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + responses=[ + HTTPretty.Response(body="first response", status=201), + HTTPretty.Response(body='second and last response', status=202), + ]) + + request1 = urlopen('https://api.yahoo.com/test') + body1 = request1.read() + request1.close() + + expect(request1.code).to.equal(201) + expect(body1).to.equal(b'first response') + + request2 = urlopen('https://api.yahoo.com/test') + body2 = request2.read() + request2.close() + expect(request2.code).to.equal(202) + expect(body2).to.equal(b'second and last response') + + request3 = urlopen('https://api.yahoo.com/test') + body3 = request3.read() + request3.close() + expect(request3.code).to.equal(202) + expect(body3).to.equal(b'second and last response') + + +@httprettified +@within(two=microseconds) +def test_can_inspect_last_request(now): + "HTTPretty.last_request is a mimetools.Message request from last match" + + HTTPretty.register_uri(HTTPretty.POST, "http://api.github.com/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + request = urllib2.Request( + 'http://api.github.com', + b'{"username": "gabrielfalcao"}', + { + 'content-type': 'text/json', + }, + ) + fd = urlopen(request) + got = fd.read() + fd.close() + + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.body).to.equal( + b'{"username": "gabrielfalcao"}', + ) + expect(HTTPretty.last_request.headers['content-type']).to.equal( + 'text/json', + ) + expect(got).to.equal(b'{"repositories": ["HTTPretty", "lettuce"]}') + + +@httprettified +@within(two=microseconds) +def test_can_inspect_last_request_with_ssl(now): + "HTTPretty.last_request is recorded even when mocking 'https' (SSL)" + + HTTPretty.register_uri(HTTPretty.POST, "https://secure.github.com/", + body='{"repositories": ["HTTPretty", "lettuce"]}') + + request = urllib2.Request( + 'https://secure.github.com', + b'{"username": "gabrielfalcao"}', + { + 'content-type': 'text/json', + }, + ) + fd = urlopen(request) + got = fd.read() + fd.close() + + expect(HTTPretty.last_request.method).to.equal('POST') + expect(HTTPretty.last_request.body).to.equal( + b'{"username": "gabrielfalcao"}', + ) + expect(HTTPretty.last_request.headers['content-type']).to.equal( + 'text/json', + ) + expect(got).to.equal(b'{"repositories": ["HTTPretty", "lettuce"]}') + + +@httprettified +@within(two=microseconds) +def test_httpretty_ignores_querystrings_from_registered_uri(): + "HTTPretty should mock a simple GET with urllib2.read()" + + HTTPretty.register_uri(HTTPretty.GET, "http://yipit.com/?id=123", + body="Find the best daily deals") + + fd = urlopen('http://yipit.com/?id=123') + got = fd.read() + fd.close() + + expect(got).to.equal(b'Find the best daily deals') + expect(HTTPretty.last_request.method).to.equal('GET') + expect(HTTPretty.last_request.path).to.equal('/?id=123') + + +@httprettified +@within(two=microseconds) +def test_callback_response(now): + ("HTTPretty should all a callback function to be set as the body with" + " urllib2") + + def request_callback(request, uri, headers): + return [200, headers, "The {0} response from {1}".format(decode_utf8(request.method), uri)] + + HTTPretty.register_uri( + HTTPretty.GET, "https://api.yahoo.com/test", + body=request_callback) + + fd = urllib2.urlopen('https://api.yahoo.com/test') + got = fd.read() + fd.close() + + expect(got).to.equal(b"The GET response from https://api.yahoo.com/test") + + HTTPretty.register_uri( + HTTPretty.POST, "https://api.yahoo.com/test_post", + body=request_callback) + + request = urllib2.Request( + "https://api.yahoo.com/test_post", + b'{"username": "gabrielfalcao"}', + { + 'content-type': 'text/json', + }, + ) + fd = urllib2.urlopen(request) + got = fd.read() + fd.close() + + expect(got).to.equal(b"The POST response from https://api.yahoo.com/test_post") + + +@httprettified +def test_httpretty_should_allow_registering_regexes(): + "HTTPretty should allow registering regexes with urllib2" + + HTTPretty.register_uri( + HTTPretty.GET, + re.compile("https://api.yipit.com/v1/deal;brand=(?P<brand_name>\w+)"), + body="Found brand", + ) + + request = urllib2.Request( + "https://api.yipit.com/v1/deal;brand=GAP", + ) + fd = urllib2.urlopen(request) + got = fd.read() + fd.close() + + expect(got).to.equal(b"Found brand") diff --git a/tests/functional/testserver.py b/tests/functional/testserver.py new file mode 100644 index 0000000..db070ba --- /dev/null +++ b/tests/functional/testserver.py @@ -0,0 +1,171 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> 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. +from __future__ import unicode_literals + +import os +import sys + +try: + import io + StringIO = io.StringIO +except ImportError: + import StringIO + StringIO = StringIO.StringIO + +import time +import socket +from tornado.web import Application +from tornado.web import RequestHandler +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop +from multiprocessing import Process + +PY3 = sys.version_info[0] == 3 +if PY3: + text_type = str + byte_type = bytes +else: + text_type = unicode + byte_type = str + + +def utf8(s): + if isinstance(s, text_type): + s = s.encode('utf-8') + + return byte_type(s) + +true_socket = socket.socket + +PY3 = sys.version_info[0] == 3 + +if not PY3: + bytes = lambda s, *args: str(s) + + +class BubblesHandler(RequestHandler): + def get(self): + 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 .") + + +class ComeHandler(RequestHandler): + def get(self): + self.write("<- HELLO WORLD ->") + + +class TornadoServer(object): + is_running = False + + def __init__(self, port): + self.port = int(port) + self.process = None + + @classmethod + def get_handlers(cls): + return Application([ + (r"/go-for-bubbles/?", BubblesHandler), + (r"/come-again/?", ComeHandler), + ]) + + def start(self): + def go(app, port, data={}): + from httpretty import HTTPretty + HTTPretty.disable() + http = HTTPServer(app) + http.listen(int(port)) + IOLoop.instance().start() + + app = self.get_handlers() + + data = {} + args = (app, self.port, data) + self.process = Process(target=go, args=args) + self.process.start() + time.sleep(0.4) + + def stop(self): + try: + os.kill(self.process.pid, 9) + except OSError: + self.process.terminate() + finally: + self.is_running = False + + +class TCPServer(object): + def __init__(self, port): + self.port = int(port) + + def start(self): + def go(port): + from httpretty import HTTPretty + HTTPretty.disable() + import socket + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(('localhost', port)) + s.listen(True) + conn, addr = s.accept() + + while True: + data = conn.recv(1024) + conn.send(b"RECEIVED: " + bytes(data)) + + conn.close() + + args = [self.port] + self.process = Process(target=go, args=args) + self.process.start() + time.sleep(0.4) + + def stop(self): + try: + os.kill(self.process.pid, 9) + except OSError: + self.process.terminate() + finally: + self.is_running = False + + +class TCPClient(object): + def __init__(self, port): + self.port = int(port) + self.sock = true_socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect(('localhost', self.port)) + + def send(self, what): + data = bytes(what, 'utf-8') + self.sock.sendall(data) + return self.sock.recv(len(data) + 11) + + def close(self): + try: + self.sock.close() + except socket.error: + pass # already closed + + def __del__(self): + self.close() diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..a5df75e --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1,25 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> 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. diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py new file mode 100644 index 0000000..e2551fb --- /dev/null +++ b/tests/unit/test_core.py @@ -0,0 +1,569 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import json +import errno +from datetime import datetime + +from mock import Mock, patch, call +from sure import expect + +from httpretty.core import HTTPrettyRequest, FakeSSLSocket, fakesock, httpretty + + +class SocketErrorStub(Exception): + def __init__(self, errno): + self.errno = errno + + +def test_request_stubs_internals(): + ("HTTPrettyRequest is a BaseHTTPRequestHandler that replaces " + "real socket file descriptors with in-memory ones") + + # Given a valid HTTP request header string + headers = "\r\n".join([ + 'POST /somewhere/?name=foo&age=bar HTTP/1.1', + 'Accept-Encoding: identity', + 'Host: github.com', + 'Content-Type: application/json', + 'Connection: close', + 'User-Agent: Python-urllib/2.7', + ]) + + # When I create a HTTPrettyRequest with an empty body + request = HTTPrettyRequest(headers, body='') + + # Then it should have parsed the headers + dict(request.headers).should.equal({ + 'accept-encoding': 'identity', + 'connection': 'close', + 'content-type': 'application/json', + 'host': 'github.com', + 'user-agent': 'Python-urllib/2.7' + }) + + # And the `rfile` should be a StringIO + request.should.have.property('rfile').being.a('StringIO.StringIO') + + # And the `wfile` should be a StringIO + request.should.have.property('wfile').being.a('StringIO.StringIO') + + # And the `method` should be available + request.should.have.property('method').being.equal('POST') + + + +def test_request_parse_querystring(): + ("HTTPrettyRequest#parse_querystring should parse unicode data") + + # Given a request string containing a unicode encoded querystring + + headers = "\r\n".join([ + 'POST /create?name=Gabriel+Falcão HTTP/1.1', + 'Content-Type: multipart/form-data', + ]) + + # When I create a HTTPrettyRequest with an empty body + request = HTTPrettyRequest(headers, body='') + + # Then it should have a parsed querystring + request.querystring.should.equal({'name': ['Gabriel Falcão']}) + + +def test_request_parse_body_when_it_is_application_json(): + ("HTTPrettyRequest#parse_request_body recognizes the " + "content-type `application/json` and parses it") + + # Given a request string containing a unicode encoded querystring + headers = "\r\n".join([ + 'POST /create HTTP/1.1', + 'Content-Type: application/json', + ]) + # And a valid json body + body = json.dumps({'name': 'Gabriel Falcão'}) + + # When I create a HTTPrettyRequest with that data + request = HTTPrettyRequest(headers, body) + + # Then it should have a parsed body + request.parsed_body.should.equal({'name': 'Gabriel Falcão'}) + + +def test_request_parse_body_when_it_is_text_json(): + ("HTTPrettyRequest#parse_request_body recognizes the " + "content-type `text/json` and parses it") + + # Given a request string containing a unicode encoded querystring + headers = "\r\n".join([ + 'POST /create HTTP/1.1', + 'Content-Type: text/json', + ]) + # And a valid json body + body = json.dumps({'name': 'Gabriel Falcão'}) + + # When I create a HTTPrettyRequest with that data + request = HTTPrettyRequest(headers, body) + + # Then it should have a parsed body + request.parsed_body.should.equal({'name': 'Gabriel Falcão'}) + + +def test_request_parse_body_when_it_is_urlencoded(): + ("HTTPrettyRequest#parse_request_body recognizes the " + "content-type `application/x-www-form-urlencoded` and parses it") + + # Given a request string containing a unicode encoded querystring + headers = "\r\n".join([ + 'POST /create HTTP/1.1', + 'Content-Type: application/x-www-form-urlencoded', + ]) + # And a valid urlencoded body + body = "name=Gabriel+Falcão&age=25&projects=httpretty&projects=sure&projects=lettuce" + + # When I create a HTTPrettyRequest with that data + request = HTTPrettyRequest(headers, body) + + # Then it should have a parsed body + request.parsed_body.should.equal({ + 'name': ['Gabriel Falcão'], + 'age': ["25"], + 'projects': ["httpretty", "sure", "lettuce"] + }) + + +def test_request_parse_body_when_unrecognized(): + ("HTTPrettyRequest#parse_request_body returns the value as " + "is if the Content-Type is not recognized") + + # Given a request string containing a unicode encoded querystring + headers = "\r\n".join([ + 'POST /create HTTP/1.1', + 'Content-Type: whatever', + ]) + # And a valid urlencoded body + body = "foobar:\nlalala" + + # When I create a HTTPrettyRequest with that data + request = HTTPrettyRequest(headers, body) + + # Then it should have a parsed body + request.parsed_body.should.equal("foobar:\nlalala") + + +def test_request_string_representation(): + ("HTTPrettyRequest should have a debug-friendly " + "string representation") + + # Given a request string containing a unicode encoded querystring + headers = "\r\n".join([ + 'POST /create HTTP/1.1', + 'Content-Type: JPEG-baby', + ]) + # And a valid urlencoded body + body = "foobar:\nlalala" + + # When I create a HTTPrettyRequest with that data + request = HTTPrettyRequest(headers, body) + + # Then its string representation should show the headers and the body + str(request).should.equal('<HTTPrettyRequest("JPEG-baby", total_headers=1, body_length=14)>') + + +def test_fake_ssl_socket_proxies_its_ow_socket(): + ("FakeSSLSocket is a simpel wrapper around its own socket, " + "which was designed to be a HTTPretty fake socket") + + # Given a sentinel mock object + socket = Mock() + + # And a FakeSSLSocket wrapping it + ssl = FakeSSLSocket(socket) + + # When I make a method call + ssl.send("FOO") + + # Then it should bypass any method calls to its own socket + socket.send.assert_called_once_with("FOO") + + +@patch('httpretty.core.datetime') +def test_fakesock_socket_getpeercert(dt): + ("fakesock.socket#getpeercert should return a hardcoded fake certificate") + # Background: + dt.now.return_value = datetime(2013, 10, 4, 4, 20, 0) + + # Given a fake socket instance + socket = fakesock.socket() + + # And that it's bound to some host and port + socket.connect(('somewhere.com', 80)) + + # When I retrieve the peer certificate + certificate = socket.getpeercert() + + # Then it should return a hardcoded value + certificate.should.equal({ + u'notAfter': 'Sep 29 04:20:00 GMT', + u'subject': ( + ((u'organizationName', u'*.somewhere.com'),), + ((u'organizationalUnitName', u'Domain Control Validated'),), + ((u'commonName', u'*.somewhere.com'),)), + u'subjectAltName': ( + (u'DNS', u'*somewhere.com'), + (u'DNS', u'somewhere.com'), + (u'DNS', u'*') + ) + }) + + +def test_fakesock_socket_ssl(): + ("fakesock.socket#ssl should take a socket instance and return itself") + # Given a fake socket instance + socket = fakesock.socket() + + # And a stubbed socket sentinel + sentinel = Mock() + + # When I call `ssl` on that mock + result = socket.ssl(sentinel) + + # Then it should have returned its first argument + result.should.equal(sentinel) + + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_connect_fallback(POTENTIAL_HTTP_PORTS, old_socket): + ("fakesock.socket#connect should open a real connection if the " + "given port is not a potential http port") + # Background: the potential http ports are 80 and 443 + POTENTIAL_HTTP_PORTS.__contains__.side_effect = lambda other: int(other) in (80, 443) + + # Given a fake socket instance + socket = fakesock.socket() + + # When it is connected to a remote server in a port that isn't 80 nor 443 + socket.connect(('somewhere.com', 42)) + + # Then it should have open a real connection in the background + old_socket.return_value.connect.assert_called_once_with(('somewhere.com', 42)) + + # And _closed is set to False + socket._closed.should.be.false + + +@patch('httpretty.core.old_socket') +def test_fakesock_socket_close(old_socket): + ("fakesock.socket#close should close the actual socket in case " + "it's not http and _closed is False") + # Given a fake socket instance that is synthetically open + socket = fakesock.socket() + socket._closed = False + + # When I close it + socket.close() + + # Then its real socket should have been closed + old_socket.return_value.close.assert_called_once_with() + + # And _closed is set to True + socket._closed.should.be.true + + +@patch('httpretty.core.old_socket') +def test_fakesock_socket_makefile(old_socket): + ("fakesock.socket#makefile should set the mode, " + "bufsize and return its mocked file descriptor") + + # Given a fake socket that has a mocked Entry associated with it + socket = fakesock.socket() + socket._entry = Mock() + + # When I call makefile() + fd = socket.makefile(mode='rw', bufsize=512) + + # Then it should have returned the socket's own filedescriptor + expect(fd).to.equal(socket.fd) + # And the mode should have been set in the socket instance + socket._mode.should.equal('rw') + # And the bufsize should have been set in the socket instance + socket._bufsize.should.equal(512) + + # And the entry should have been filled with that filedescriptor + socket._entry.fill_filekind.assert_called_once_with(fd) + + +@patch('httpretty.core.old_socket') +def test_fakesock_socket_real_sendall(old_socket): + ("fakesock.socket#real_sendall sends data and buffers " + "the response in the file descriptor") + # Background: the real socket will stop returning bytes after the + # first call + real_socket = old_socket.return_value + real_socket.recv.side_effect = ['response from server', ""] + + # Given a fake socket + socket = fakesock.socket() + + # When I call real_sendall with data, some args and kwargs + socket.real_sendall("SOMEDATA", 'some extra args...', foo='bar') + + # Then it should have called sendall in the real socket + real_socket.sendall.assert_called_once_with("SOMEDATA", 'some extra args...', foo='bar') + + # And the timeout was set to 0 + real_socket.settimeout.assert_called_once_with(0) + + # And recv was called with the bufsize + real_socket.recv.assert_has_calls([ + call(16), + call(16), + ]) + + # And the buffer should contain the data from the server + socket.fd.getvalue().should.equal("response from server") + + # And connect was never called + real_socket.connect.called.should.be.false + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.socket') +def test_fakesock_socket_real_sendall_continue_eagain(socket, old_socket): + ("fakesock.socket#real_sendall should continue if the socket error was EAGAIN") + socket.error = SocketErrorStub + # Background: the real socket will stop returning bytes after the + # first call + real_socket = old_socket.return_value + real_socket.recv.side_effect = [SocketErrorStub(errno.EAGAIN), 'after error', ""] + + # Given a fake socket + socket = fakesock.socket() + + + # When I call real_sendall with data, some args and kwargs + socket.real_sendall("SOMEDATA", 'some extra args...', foo='bar') + + # Then it should have called sendall in the real socket + real_socket.sendall.assert_called_once_with("SOMEDATA", 'some extra args...', foo='bar') + + # And the timeout was set to 0 + real_socket.settimeout.assert_called_once_with(0) + + # And recv was called with the bufsize + real_socket.recv.assert_has_calls([ + call(16), + call(16), + ]) + + # And the buffer should contain the data from the server + socket.fd.getvalue().should.equal("after error") + + # And connect was never called + real_socket.connect.called.should.be.false + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.socket') +def test_fakesock_socket_real_sendall_socket_error(socket, old_socket): + ("fakesock.socket#real_sendall should continue if the socket error was EAGAIN") + socket.error = SocketErrorStub + # Background: the real socket will stop returning bytes after the + # first call + real_socket = old_socket.return_value + real_socket.recv.side_effect = [SocketErrorStub(42), 'after error', ""] + + # Given a fake socket + socket = fakesock.socket() + + # When I call real_sendall with data, some args and kwargs + socket.real_sendall("SOMEDATA", 'some extra args...', foo='bar') + + # Then it should have called sendall in the real socket + real_socket.sendall.assert_called_once_with("SOMEDATA", 'some extra args...', foo='bar') + + # And the timeout was set to 0 + real_socket.settimeout.assert_called_once_with(0) + + # And recv was called with the bufsize + real_socket.recv.assert_called_once_with(16) + + # And the buffer should contain the data from the server + socket.fd.getvalue().should.equal("") + + # And connect was never called + real_socket.connect.called.should.be.false + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_real_sendall_when_http(POTENTIAL_HTTP_PORTS, old_socket): + ("fakesock.socket#real_sendall should connect before sending data") + # Background: the real socket will stop returning bytes after the + # first call + real_socket = old_socket.return_value + real_socket.recv.side_effect = ['response from foobar :)', ""] + + # And the potential http port is 4000 + POTENTIAL_HTTP_PORTS.__contains__.side_effect = lambda other: int(other) == 4000 + + # Given a fake socket + socket = fakesock.socket() + + # When I call connect to a server in a port that is considered HTTP + socket.connect(('foobar.com', 4000)) + + # And send some data + socket.real_sendall("SOMEDATA") + + # Then connect should have been called + real_socket.connect.assert_called_once_with(('foobar.com', 4000)) + + # And the timeout was set to 0 + real_socket.settimeout.assert_called_once_with(0) + + # And recv was called with the bufsize + real_socket.recv.assert_has_calls([ + call(16), + call(16), + ]) + + # And the buffer should contain the data from the server + socket.fd.getvalue().should.equal("response from foobar :)") + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.httpretty') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_sendall_with_valid_requestline(POTENTIAL_HTTP_PORTS, httpretty, old_socket): + ("fakesock.socket#sendall should create an entry if it's given a valid request line") + matcher = Mock() + info = Mock() + httpretty.match_uriinfo.return_value = (matcher, info) + httpretty.register_uri(httpretty.GET, 'http://foo.com/foobar') + + # Background: + # using a subclass of socket that mocks out real_sendall + class MySocket(fakesock.socket): + def real_sendall(self, data, *args, **kw): + raise AssertionError('should never call this...') + + # Given an instance of that socket + socket = MySocket() + + # And that is is considered http + socket.connect(('foo.com', 80)) + + # When I try to send data + socket.sendall("GET /foobar HTTP/1.1\r\nContent-Type: application/json\r\n\r\n") + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.httpretty') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_sendall_with_valid_requestline(POTENTIAL_HTTP_PORTS, httpretty, old_socket): + ("fakesock.socket#sendall should create an entry if it's given a valid request line") + matcher = Mock() + info = Mock() + httpretty.match_uriinfo.return_value = (matcher, info) + httpretty.register_uri(httpretty.GET, 'http://foo.com/foobar') + + # Background: + # using a subclass of socket that mocks out real_sendall + class MySocket(fakesock.socket): + def real_sendall(self, data, *args, **kw): + raise AssertionError('should never call this...') + + # Given an instance of that socket + socket = MySocket() + + # And that is is considered http + socket.connect(('foo.com', 80)) + + # When I try to send data + socket.sendall("GET /foobar HTTP/1.1\r\nContent-Type: application/json\r\n\r\n") + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_sendall_with_body_data_no_entry(POTENTIAL_HTTP_PORTS, old_socket): + ("fakesock.socket#sendall should call real_sendall when not parsing headers and there is no entry") + # Background: + # Using a subclass of socket that mocks out real_sendall + class MySocket(fakesock.socket): + def real_sendall(self, data): + data.should.equal('BLABLABLABLA') + return 'cool' + + # Given an instance of that socket + socket = MySocket() + socket._entry = None + + # And that is is considered http + socket.connect(('foo.com', 80)) + + # When I try to send data + result = socket.sendall("BLABLABLABLA") + + # Then the result should be the return value from real_sendall + result.should.equal('cool') + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_sendall_with_body_data_with_entry(POTENTIAL_HTTP_PORTS, old_socket): + ("fakesock.socket#sendall should call real_sendall when not ") + # Background: + # Using a subclass of socket that mocks out real_sendall + class MySocket(fakesock.socket): + def real_sendall(self, data): + raise AssertionError('should have never been called') + # Using a mocked entry + entry = Mock() + entry.request.headers = {} + entry.request.body = '' + + # Given an instance of that socket + socket = MySocket() + socket._entry = entry + + + # And that is is considered http + socket.connect(('foo.com', 80)) + + # When I try to send data + socket.sendall("BLABLABLABLA") + + # Then the entry should have that body + entry.request.body.should.equal('BLABLABLABLA') + + +@patch('httpretty.core.old_socket') +@patch('httpretty.core.POTENTIAL_HTTP_PORTS') +def test_fakesock_socket_sendall_with_body_data_with_chunked_entry(POTENTIAL_HTTP_PORTS, old_socket): + ("fakesock.socket#sendall should call real_sendall when not ") + # Background: + # Using a subclass of socket that mocks out real_sendall + class MySocket(fakesock.socket): + def real_sendall(self, data): + raise AssertionError('should have never been called') + # Using a mocked entry + entry = Mock() + entry.request.headers = { + 'transfer-encoding': 'chunked', + } + entry.request.body = '' + + # Given an instance of that socket + socket = MySocket() + socket._entry = entry + + # And that is is considered http + socket.connect(('foo.com', 80)) + + # When I try to send data + socket.sendall("BLABLABLABLA") + + # Then the entry should have that body + httpretty.last_request.body.should.equal('BLABLABLABLA') diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py new file mode 100644 index 0000000..7ea5fe1 --- /dev/null +++ b/tests/unit/test_http.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from httpretty.http import parse_requestline + + +def test_parse_request_line_connect(): + ("parse_requestline should parse the CONNECT method appropriately") + + # Given a valid request line string that has the CONNECT method + line = "CONNECT / HTTP/1.1" + + # When I parse it + result = parse_requestline(line) + + # Then it should return a tuple + result.should.equal(("CONNECT", "/", "1.1")) diff --git a/tests/unit/test_httpretty.py b/tests/unit/test_httpretty.py new file mode 100644 index 0000000..420aaee --- /dev/null +++ b/tests/unit/test_httpretty.py @@ -0,0 +1,386 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# <HTTPretty - HTTP client mock for Python> +# Copyright (C) <2011-2013> 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. +from __future__ import unicode_literals +import json +from sure import expect +from httpretty import HTTPretty, HTTPrettyError, core +from httpretty.core import URIInfo, BaseClass, Entry, FakeSockFile, HTTPrettyRequest +from httpretty.http import STATUSES + +try: + from mock import MagicMock +except ImportError: + from unittest.mock import MagicMock + +TEST_HEADER = """ +GET /test/test.html HTTP/1.1 +Host: www.host1.com:80 +Content-Type: %(content_type)s +""" + + +def test_httpretty_should_raise_proper_exception_on_inconsistent_length(): + "HTTPretty should raise proper exception on inconsistent Content-Length / "\ + "registered response body" + expect(HTTPretty.register_uri).when.called_with( + HTTPretty.GET, + "http://github.com/gabrielfalcao", + body="that's me!", + adding_headers={ + 'Content-Length': '999' + } + ).to.throw( + HTTPrettyError, + 'HTTPretty got inconsistent parameters. The header Content-Length you registered expects size "999" ' + 'but the body you registered for that has actually length "10".' + ) + + +def test_httpretty_should_raise_on_socket_send_when_uri_registered(): + """HTTPretty should raise a RuntimeError when the fakesocket is used in + an invalid usage. + """ + import socket + HTTPretty.enable() + + HTTPretty.register_uri(HTTPretty.GET, + 'http://127.0.0.1:5000') + expect(core.POTENTIAL_HTTP_PORTS).to.be.equal(set([80, 443, 5000])) + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('127.0.0.1', 5000)) + expect(sock.send).when.called_with(b'whatever').to.throw(RuntimeError) + sock.close() + + # restore the previous value + core.POTENTIAL_HTTP_PORTS.remove(5000) + HTTPretty.reset() + HTTPretty.disable() + + +def test_httpretty_should_not_raise_on_socket_send_when_uri_not_registered(): + """HTTPretty should not raise a RuntimeError when the fakesocket is used in + an invalid usage. + """ + import socket + HTTPretty.enable() + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) + sock.setblocking(0) + expect(sock.sendto).when.called_with(b'whatever', + ('127.0.0.1', 53) + ).should_not.throw(RuntimeError) + + sock.close() + HTTPretty.reset() + HTTPretty.disable() + + +def test_does_not_have_last_request_by_default(): + 'HTTPretty.last_request is a dummy object by default' + HTTPretty.reset() + + expect(HTTPretty.last_request.headers).to.be.empty + expect(HTTPretty.last_request.body).to.be.empty + + +def test_status_codes(): + "HTTPretty supports N status codes" + + expect(STATUSES).to.equal({ + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 306: "Switch Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request a Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", + 420: "Enhance Your Calm", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 424: "Method Failure", + 425: "Unordered Collection", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 444: "No Response", + 449: "Retry With", + 450: "Blocked by Windows Parental Controls", + 451: "Unavailable For Legal Reasons", + 451: "Redirect", + 494: "Request Header Too Large", + 495: "Cert Error", + 496: "No Cert", + 497: "HTTP to HTTPS", + 499: "Client Closed Request", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 509: "Bandwidth Limit Exceeded", + 510: "Not Extended", + 511: "Network Authentication Required", + 598: "Network read timeout error", + 599: "Network connect timeout error", + }) + +def test_uri_info_full_url(): + uri_info = URIInfo( + username='johhny', + password='password', + hostname=b'google.com', + port=80, + path=b'/', + query=b'foo=bar&baz=test', + fragment='', + scheme='', + ) + + expect(uri_info.full_url()).to.equal( + "http://johhny:password@google.com/?foo=bar&baz=test" + ) + + expect(uri_info.full_url(use_querystring=False)).to.equal( + "http://johhny:password@google.com/" + ) + +def test_uri_info_eq_ignores_case(): + """Test that URIInfo.__eq__ method ignores case for + hostname matching. + """ + uri_info_uppercase = URIInfo( + username='johhny', + password='password', + hostname=b'GOOGLE.COM', + port=80, + path=b'/', + query=b'foo=bar&baz=test', + fragment='', + scheme='', + ) + uri_info_lowercase = URIInfo( + username='johhny', + password='password', + hostname=b'google.com', + port=80, + path=b'/', + query=b'foo=bar&baz=test', + fragment='', + scheme='', + ) + expect(uri_info_uppercase).to.equal(uri_info_lowercase) + +def test_global_boolean_enabled(): + expect(HTTPretty.is_enabled()).to.be.falsy + HTTPretty.enable() + expect(HTTPretty.is_enabled()).to.be.truthy + HTTPretty.disable() + expect(HTTPretty.is_enabled()).to.be.falsy + + +def test_py3kobject_implements_valid__repr__based_on__str__(): + class MyObject(BaseClass): + def __str__(self): + return 'hi' + + myobj = MyObject() + expect(repr(myobj)).to.be.equal('hi') + + +def test_Entry_class_normalizes_headers(): + entry = Entry(HTTPretty.GET, 'http://example.com', 'example', + host='example.com', cache_control='no-cache', x_forward_for='proxy') + + expect(entry.adding_headers).to.equal({ + 'Host':'example.com', + 'Cache-Control':'no-cache', + 'X-Forward-For':'proxy' + }) + + +def test_Entry_class_counts_multibyte_characters_in_bytes(): + entry = Entry(HTTPretty.GET, 'http://example.com', 'こんにちは') + buf = FakeSockFile() + entry.fill_filekind(buf) + response = buf.getvalue() + expect(b'content-length: 15\n').to.be.within(response) + + +def test_fake_socket_passes_through_setblocking(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.setblocking).called_with(0).should_not.throw(AttributeError) + s.truesock.setblocking.assert_called_with(0) + +def test_fake_socket_passes_through_fileno(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.fileno).called_with().should_not.throw(AttributeError) + s.truesock.fileno.assert_called_with() + + +def test_fake_socket_passes_through_getsockopt(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.getsockopt).called_with(socket.SOL_SOCKET, 1).should_not.throw(AttributeError) + s.truesock.getsockopt.assert_called_with(socket.SOL_SOCKET, 1) + +def test_fake_socket_passes_through_bind(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.bind).called_with().should_not.throw(AttributeError) + s.truesock.bind.assert_called_with() + +def test_fake_socket_passes_through_connect_ex(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.connect_ex).called_with().should_not.throw(AttributeError) + s.truesock.connect_ex.assert_called_with() + +def test_fake_socket_passes_through_listen(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.listen).called_with().should_not.throw(AttributeError) + s.truesock.listen.assert_called_with() + +def test_fake_socket_passes_through_getpeername(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.getpeername).called_with().should_not.throw(AttributeError) + s.truesock.getpeername.assert_called_with() + +def test_fake_socket_passes_through_getsockname(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.getsockname).called_with().should_not.throw(AttributeError) + s.truesock.getsockname.assert_called_with() + +def test_fake_socket_passes_through_gettimeout(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.gettimeout).called_with().should_not.throw(AttributeError) + s.truesock.gettimeout.assert_called_with() + +def test_fake_socket_passes_through_shutdown(): + import socket + HTTPretty.enable() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.shutdown).called_with(socket.SHUT_RD).should_not.throw(AttributeError) + s.truesock.shutdown.assert_called_with(socket.SHUT_RD) + + +def test_HTTPrettyRequest_json_body(): + """ A content-type of application/json should parse a valid json body """ + header = TEST_HEADER % {'content_type': 'application/json'} + test_dict = {'hello': 'world'} + request = HTTPrettyRequest(header, json.dumps(test_dict)) + expect(request.parsed_body).to.equal(test_dict) + + +def test_HTTPrettyRequest_invalid_json_body(): + """ A content-type of application/json with an invalid json body should return the content unaltered """ + header = TEST_HEADER % {'content_type': 'application/json'} + invalid_json = u"{'hello', 'world','thisstringdoesntstops}" + request = HTTPrettyRequest(header, invalid_json) + expect(request.parsed_body).to.equal(invalid_json) + + +def test_HTTPrettyRequest_queryparam(): + """ A content-type of x-www-form-urlencoded with a valid queryparam body should return parsed content """ + header = TEST_HEADER % {'content_type': 'application/x-www-form-urlencoded'} + valid_queryparam = u"hello=world&this=isavalidquerystring" + valid_results = {'hello': ['world'], 'this': ['isavalidquerystring']} + request = HTTPrettyRequest(header, valid_queryparam) + expect(request.parsed_body).to.equal(valid_results) + + +def test_HTTPrettyRequest_arbitrarypost(): + """ A non-handled content type request's post body should return the content unaltered """ + header = TEST_HEADER % {'content_type': 'thisis/notarealcontenttype'} + gibberish_body = "1234567890!@#$%^&*()" + request = HTTPrettyRequest(header, gibberish_body) + expect(request.parsed_body).to.equal(gibberish_body) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py new file mode 100644 index 0000000..efa977c --- /dev/null +++ b/tests/unit/test_main.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from mock import patch +import httpretty + + +@patch('httpretty.httpretty') +def test_last_request(original): + ("httpretty.last_request() should return httpretty.core.last_request") + + httpretty.last_request().should.equal(original.last_request) |
