Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding an in-memory stub version of treq #96

Merged
merged 26 commits into from
Jul 13, 2015
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
43e8a5c
Import RequestTraversalAgent and dependencies from mimic
cyli Jul 2, 2015
ec03fd8
Add a StubTreq class which mimics the treq interface, and tests to en…
cyli Jul 3, 2015
0e87015
Allow injection of an IBodyProducer callable into the HTTP client, so…
cyli Jul 3, 2015
d454d77
Remove invalid scheme in tests, because that won't even work. HEAD r…
cyli Jul 3, 2015
a1607d6
Support stubbing HTTPS URLs as well.
cyli Jul 3, 2015
c7aac7a
Include test that passing the file kwarg raises an AssertionError.
cyli Jul 3, 2015
575cf6f
Add a default resource that accepts string bodies and returns string …
cyli Jul 4, 2015
754fc7d
Add HasHeaders, because Twisted Agent adds extra request headers, so …
cyli Jul 7, 2015
381febd
Add a type of IStringResponseStub that takes a sequence of expected r…
cyli Jul 7, 2015
63dcb1e
Fix bug with HTTPS scheme not appearing in the request URL
cyli Jul 7, 2015
b6591b0
Different error/failure reporting method.
cyli Jul 7, 2015
ca78637
Merge remote-tracking branch 'origin/master' into fake-treq
glyph Jul 8, 2015
dadb419
Rename bodyproducer to data_to_body_producer as per review comment
cyli Jul 8, 2015
649d27e
Fix bug in test that was referring to treq.collect when it should be …
cyli Jul 8, 2015
886bd1f
Merge branch 'master' into fake-treq
cyli Jul 8, 2015
f0fb775
Add tests for HasHeaders case sensitivity and repr-ing
cyli Jul 8, 2015
c9b1cba
Remove the try/except block around importing IAgent, since we've bump…
cyli Jul 8, 2015
2b073c6
Add test to verify SequenceStringStubs actually implements IStringRes…
cyli Jul 8, 2015
bf63d03
Fix StubTreq API verification so that it has full branch coverage.
cyli Jul 8, 2015
de192a4
Change SynchronousProducer to be private, since it's inaccessible, an…
cyli Jul 8, 2015
1c11f8f
Make RequestTraversalAgent handle asynchronous requests as well, and …
cyli Jul 9, 2015
fca2a9b
Merge branch 'master' into fake-treq
cyli Jul 9, 2015
75b49b1
Remove some code, to make it easier to review. This commit can be re…
cyli Jul 9, 2015
cc9196c
Merge branch 'master' into fake-treq
cyli Jul 9, 2015
5d50066
Handle the case where the response already has a result (such as Sche…
cyli Jul 12, 2015
c672632
Merge branch 'master' into fake-treq
cyli Jul 13, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pep8
sphinx
mock
requests>=2.1.0
six
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
version=__version__,
packages=find_packages(),
install_requires=[
"Twisted >= 13.2.0", "requests >= 2.1.0", "service_identity", "pyOpenSSL >= 0.11"
"Twisted >= 13.2.0", "requests >= 2.1.0", "service_identity",
"pyOpenSSL >= 0.11", "six"
],
package_data={"treq": ["_version"]},
author="David Reid",
Expand Down
6 changes: 4 additions & 2 deletions treq/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ def deliverBody(self, protocol):


class HTTPClient(object):
def __init__(self, agent, cookiejar=None):
def __init__(self, agent, cookiejar=None,
data_to_body_producer=IBodyProducer):
self._agent = agent
self._cookiejar = cookiejar or cookiejar_from_dict({})
self._data_to_body_producer = data_to_body_producer

def get(self, url, **kwargs):
return self.request('GET', url, **kwargs)
Expand Down Expand Up @@ -161,7 +163,7 @@ def request(self, method, url, **kwargs):
headers.setRawHeaders(
'content-type', ['application/x-www-form-urlencoded'])
data = urlencode(data, doseq=True)
bodyProducer = IBodyProducer(data)
bodyProducer = self._data_to_body_producer(data)

cookies = kwargs.get('cookies', {})

Expand Down
323 changes: 323 additions & 0 deletions treq/test/test_testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
"""
In-memory treq returns stubbed responses.
"""
from inspect import getmembers, isfunction

from six import text_type, binary_type

from twisted.web.client import ResponseFailed
from twisted.web.resource import Resource
from twisted.web.server import NOT_DONE_YET

from zope.interface import implementer
from zope.interface.verify import verifyObject

import treq

from treq.test.util import TestCase
from treq.testing import (
HasHeaders,
IStringResponseStubs,
SequenceStringStubs,
StringStubbingResource,
StubTreq
)


class _StaticTestResource(Resource):
"""Resource that always returns 418 "I'm a teapot"""
isLeaf = True

def render(self, request):
request.setResponseCode(418)
request.setHeader("x-teapot", "teapot!")
return "I'm a teapot"


class _NonResponsiveTestResource(Resource):
"""Resource that returns NOT_DONE_YET and never finishes the request"""
isLeaf = True

def render(self, request):
return NOT_DONE_YET


class StubbingTests(TestCase):
"""
Tests for :class:`StubTreq`.
"""
def test_stubtreq_provides_all_functions_in_treq_all(self):
"""
Every single function and attribute exposed by :obj:`treq.__all__` is
provided by :obj:`StubTreq`.
"""
treq_things = [(name, obj) for name, obj in getmembers(treq)
if name in treq.__all__]
stub = StubTreq(_StaticTestResource())

api_things = [(name, obj) for name, obj in treq_things
if obj.__module__ == "treq.api"]
content_things = [(name, obj) for name, obj in treq_things
if obj.__module__ == "treq.content"]

# sanity checks - this test should fail if treq exposes a new API
# without changes being made to StubTreq and this test.
msg = ("At the time this test was written, StubTreq only knew about "
"treq exposing functions from treq.api and treq.content. If "
"this has changed, StubTreq will need to be updated, as will "
"this test.")
self.assertTrue(all(isfunction(obj) for name, obj in treq_things), msg)
self.assertEqual(set(treq_things), set(api_things + content_things),
msg)

for name, obj in api_things:
self.assertTrue(
isfunction(getattr(stub, name, None)),
"StubTreq.{0} should be a function.".format(name))

for name, obj in content_things:
self.assertIs(
getattr(stub, name, None), obj,
"StubTreq.{0} should just expose treq.{0}".format(name))

def test_providing_resource_to_stub_treq(self):
"""
The resource provided to StubTreq is responds to every request no
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stray "is" here

matter what the URI or parameters or data.
"""
verbs = ('GET', 'PUT', 'HEAD', 'PATCH', 'DELETE', 'POST')
urls = (
'http://supports-http.com',
'https://supports-https.com',
'http://this/has/a/path/and/invalid/domain/name'
'https://supports-https.com:8080',
'http://supports-http.com:8080',
)
params = (None, {}, {'page': [1]})
headers = (None, {}, {'x-random-header': ['value', 'value2']})
data = (None, "", 'some data', '{"some": "json"}')

stub = StubTreq(_StaticTestResource())

combos = (
(verb, {"url": url, "params": p, "headers": h, "data": d})
for verb in verbs
for url in urls
for p in params
for h in headers
for d in data
)
for combo in combos:
verb, kwargs = combo
deferreds = (stub.request(verb, **kwargs),
getattr(stub, verb.lower())(**kwargs))
for d in deferreds:
resp = self.successResultOf(d)
self.assertEqual(418, resp.code)
self.assertEqual(['teapot!'],
resp.headers.getRawHeaders('x-teapot'))
self.assertEqual("" if verb == "HEAD" else "I'm a teapot",
self.successResultOf(stub.content(resp)))

def test_files_are_rejected(self):
"""
StubTreq does not handle files yet - it should reject requests which
attempt to pass files.
"""
stub = StubTreq(_StaticTestResource())
self.assertRaises(
AssertionError, stub.request,
'method', 'http://url', files='some file')

def test_passing_in_strange_data_is_rejected(self):
"""
StubTreq rejects data that isn't list/dictionary/tuple/bytes/unicode.
"""
stub = StubTreq(_StaticTestResource())
self.assertRaises(
AssertionError, stub.request, 'method', 'http://url',
data=object())
self.successResultOf(stub.request('method', 'http://url', data={}))
self.successResultOf(stub.request('method', 'http://url', data=[]))
self.successResultOf(stub.request('method', 'http://url', data=()))
self.successResultOf(
stub.request('method', 'http://url', data=binary_type("")))
self.successResultOf(
stub.request('method', 'http://url', data=text_type("")))

def test_handles_asynchronous_requests(self):
"""
Handle a resource returning NOT_DONE_YET.
"""
stub = StubTreq(_NonResponsiveTestResource())
d = stub.request('method', 'http://url', data="1234")
self.assertNoResult(d)
d.cancel()
self.failureResultOf(d, ResponseFailed)


class HasHeadersTests(TestCase):
"""
Tests for :obj:`HasHeaders`.
"""
def test_equality_and_strict_subsets_succeed(self):
"""
The :obj:`HasHeaders` returns True if both sets of headers are
equivalent, or the first is a strict subset of the second.
"""
self.assertEqual(HasHeaders({'one': ['two', 'three']}),
{'one': ['two', 'three']},
"Equivalent headers do not match.")
self.assertEqual(HasHeaders({'one': ['two', 'three']}),
{'one': ['two', 'three', 'four'],
'ten': ['six']},
"Strict subset headers do not match")

def test_partial_or_zero_intersection_subsets_fail(self):
"""
The :obj:`HasHeaders` returns False if both sets of headers overlap
but the first is not a strict subset of the second. It also returns
False if there is no overlap.
"""
self.assertNotEqual(HasHeaders({'one': ['two', 'three']}),
{'one': ['three', 'four']},
"Partial value overlap matches")
self.assertNotEqual(HasHeaders({'one': ['two', 'three']}),
{'one': ['two']},
"Missing value matches")
self.assertNotEqual(HasHeaders({'one': ['two', 'three']}),
{'ten': ['six']},
"Complete inequality matches")

def test_case_insensitive_keys(self):
"""
The :obj:`HasHeaders` equality function ignores the case of the header
keys.
"""
self.assertEqual(HasHeaders({'A': ['1'], 'b': ['2']}),
{'a': ['1'], 'B': ['2']})

def test_case_sensitive_values(self):
"""
The :obj:`HasHeaders` equality function does care about the case of
the header value.
"""
self.assertNotEqual(HasHeaders({'a': ['a']}), {'a': ['A']})

def test_repr(self):
"""
:obj:`HasHeaders` returns a nice string repr.
"""
self.assertEqual("HasHeaders({'a': ['b']})",
repr(HasHeaders({'A': ['b']})))


class StringStubbingTests(TestCase):
"""
Tests for :obj:`StringStubbingResource`.
"""
def _get_response_stub(self, expected_args, response):
"""
Make a :obj:`IStringResponseStubs` that checks the expected args and
returns the given response.
"""
method, url, params, headers, data = expected_args

@implementer(IStringResponseStubs)
class Stubber(object):
def get_response_for(_, _method, _url, _params, _headers, _data):
self.assertEqual((method, url, params, data),
(_method, _url, _params, _data))
self.assertEqual(HasHeaders(headers), _headers)
return response

return Stubber()

def test_interacts_successfully_with_istub(self):
"""
The :obj:`IStringResponseStubs` is passed the correct parameters with
which to evaluate the response, and the response is returned.
"""
resource = StringStubbingResource(self._get_response_stub(
('DELETE', 'http://what/a/thing', {'page': ['1']},
{'x-header': ['eh']}, 'datastr'),
(418, {'x-response': 'responseheader'}, 'response body')))

stub = StubTreq(resource)

d = stub.delete('http://what/a/thing', headers={'x-header': 'eh'},
params={'page': '1'}, data='datastr')
resp = self.successResultOf(d)
self.assertEqual(418, resp.code)
self.assertEqual(['responseheader'],
resp.headers.getRawHeaders('x-response'))
self.assertEqual('response body',
self.successResultOf(stub.content(resp)))


class SequenceStringStubsTests(TestCase):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers: I plan to write docs in a later PR, because don't want to make this PR already bigger than the monster it already is. But the API should look something like how these tests are written - maybe we can provide a convenience API like string_stub_treq([((expected request args), (expected response))]) instead of having to construct StubTreq(StringStubbingResource(SequenceStringStubs([...])))?

"""
Tests for :obj:`SequenceStringStubs`.
"""
def test_implements_interface(self):
"""
:obj:`SequenceStringStubs` implements :obj:`IStringResponseStubs`.
"""
verifyObject(IStringResponseStubs, SequenceStringStubs([]))

def test_only_check_args_that_are_not_None(self):
"""
`None` is used as a sentinel value to mean "anything for this value is
valid".
"""
sequence = SequenceStringStubs(
[(('get', None, None, None, None), (418, {}, 'body'))])
stub = StubTreq(StringStubbingResource(sequence))
d = stub.get('https://anything', data='what', headers={'1': '1'})
resp = self.successResultOf(d)
self.assertEqual((), sequence.failures)
self.assertEqual(418, resp.code)
self.assertEqual('body', self.successResultOf(stub.content(resp)))

sequence = SequenceStringStubs(
[(('get', None, None, None, None), (418, {}, 'body'))])
stub = StubTreq(StringStubbingResource(sequence))
d = stub.delete('https://anything', data='what', headers={'1': '1'})
resp = self.successResultOf(d)
self.assertNotEqual((), sequence.failures)
self.assertEqual(500, resp.code)

def test_unexpected_next_request_causes_failure(self):
"""
If a request is made that is not expected as the next request,
causes a failure.
"""
sequence = SequenceStringStubs(
[(('get', 'https://anything', {}, {'1': ['1']}, 'what'),
(418, {}, 'body')),
(('get', 'http://anything', {}, {'2': ['1']}, 'what'),
(202, {}, 'deleted'))])
stub = StubTreq(StringStubbingResource(sequence))

d = stub.get('https://anything', data='what', headers={'1': '1'})
resp = self.successResultOf(d)
self.assertEqual((), sequence.failures)
self.assertEqual(418, resp.code)
self.assertEqual('body', self.successResultOf(stub.content(resp)))

d = stub.get('https://anything', data='what', headers={'1': '1'})
resp = self.successResultOf(d)
self.assertNotEqual((), sequence.failures)
self.assertEqual(500, resp.code)

def test_no_more_expected_requests_causes_failure(self):
"""
If there are no more expected requests, making a request causes a
failure.
"""
sequence = SequenceStringStubs([])
stub = StubTreq(StringStubbingResource(sequence))
d = stub.get('https://anything', data='what', headers={'1': '1'})
resp = self.successResultOf(d)
self.assertNotEqual((), sequence.failures)
self.assertEqual(500, resp.code)