PageRenderTime 52ms CodeModel.GetById 2ms app.highlight 44ms RepoModel.GetById 1ms app.codeStats 1ms

/tests/unit/test_core.py

https://github.com/gabrielfalcao/HTTPretty
Python | 610 lines | 485 code | 65 blank | 60 comment | 2 complexity | 6f79b8349aa0df96686b95e1e785bd27 MD5 | raw file
  1# #!/usr/bin/env python
  2# -*- coding: utf-8 -*-
  3from __future__ import unicode_literals
  4import json
  5import errno
  6from datetime import datetime
  7
  8from mock import Mock, patch, call
  9from sure import expect
 10
 11from httpretty.compat import StringIO
 12from httpretty.core import HTTPrettyRequest, FakeSSLSocket, fakesock, httpretty
 13
 14
 15class SocketErrorStub(Exception):
 16    def __init__(self, errno):
 17        self.errno = errno
 18
 19
 20def test_request_stubs_internals():
 21    ("HTTPrettyRequest is a BaseHTTPRequestHandler that replaces "
 22     "real socket file descriptors with in-memory ones")
 23
 24    # Given a valid HTTP request header string
 25    headers = "\r\n".join([
 26        'POST /somewhere/?name=foo&age=bar HTTP/1.1',
 27        'accept-encoding: identity',
 28        'host: github.com',
 29        'content-type: application/json',
 30        'connection: close',
 31        'user-agent: Python-urllib/2.7',
 32    ])
 33
 34    # When I create a HTTPrettyRequest with an empty body
 35    request = HTTPrettyRequest(headers, body='')
 36
 37    # Then it should have parsed the headers
 38    dict(request.headers).should.equal({
 39        'accept-encoding': 'identity',
 40        'connection': 'close',
 41        'content-type': 'application/json',
 42        'host': 'github.com',
 43        'user-agent': 'Python-urllib/2.7'
 44    })
 45
 46    # And the `rfile` should be a StringIO
 47    type_as_str = StringIO.__module__ + '.' + StringIO.__name__
 48
 49    request.should.have.property('rfile').being.a(type_as_str)
 50
 51    # And the `wfile` should be a StringIO
 52    request.should.have.property('wfile').being.a(type_as_str)
 53
 54    # And the `method` should be available
 55    request.should.have.property('method').being.equal('POST')
 56
 57
 58
 59def test_request_parse_querystring():
 60    ("HTTPrettyRequest#parse_querystring should parse unicode data")
 61
 62    # Given a request string containing a unicode encoded querystring
 63
 64    headers = "\r\n".join([
 65        'POST /create?name=Gabriel+Falcão HTTP/1.1',
 66        'Content-Type: multipart/form-data',
 67    ])
 68
 69    # When I create a HTTPrettyRequest with an empty body
 70    request = HTTPrettyRequest(headers, body='')
 71
 72    # Then it should have a parsed querystring
 73    request.querystring.should.equal({'name': ['Gabriel Falcão']})
 74
 75
 76def test_request_parse_body_when_it_is_application_json():
 77    ("HTTPrettyRequest#parse_request_body recognizes the "
 78     "content-type `application/json` and parses it")
 79
 80    # Given a request string containing a unicode encoded querystring
 81    headers = "\r\n".join([
 82        'POST /create HTTP/1.1',
 83        'Content-Type: application/json',
 84    ])
 85    # And a valid json body
 86    body = json.dumps({'name': 'Gabriel Falcão'})
 87
 88    # When I create a HTTPrettyRequest with that data
 89    request = HTTPrettyRequest(headers, body)
 90
 91    # Then it should have a parsed body
 92    request.parsed_body.should.equal({'name': 'Gabriel Falcão'})
 93
 94
 95def test_request_parse_body_when_it_is_text_json():
 96    ("HTTPrettyRequest#parse_request_body recognizes the "
 97     "content-type `text/json` and parses it")
 98
 99    # Given a request string containing a unicode encoded querystring
100    headers = "\r\n".join([
101        'POST /create HTTP/1.1',
102        'Content-Type: text/json',
103    ])
104    # And a valid json body
105    body = json.dumps({'name': 'Gabriel Falcão'})
106
107    # When I create a HTTPrettyRequest with that data
108    request = HTTPrettyRequest(headers, body)
109
110    # Then it should have a parsed body
111    request.parsed_body.should.equal({'name': 'Gabriel Falcão'})
112
113
114def test_request_parse_body_when_it_is_urlencoded():
115    ("HTTPrettyRequest#parse_request_body recognizes the "
116     "content-type `application/x-www-form-urlencoded` and parses it")
117
118    # Given a request string containing a unicode encoded querystring
119    headers = "\r\n".join([
120        'POST /create HTTP/1.1',
121        'Content-Type: application/x-www-form-urlencoded',
122    ])
123    # And a valid urlencoded body
124    body = "name=Gabriel+Falcão&age=25&projects=httpretty&projects=sure&projects=lettuce"
125
126    # When I create a HTTPrettyRequest with that data
127    request = HTTPrettyRequest(headers, body)
128
129    # Then it should have a parsed body
130    request.parsed_body.should.equal({
131        'name': ['Gabriel Falcão'],
132        'age': ["25"],
133        'projects': ["httpretty", "sure", "lettuce"]
134    })
135
136
137def test_request_parse_body_when_unrecognized():
138    ("HTTPrettyRequest#parse_request_body returns the value as "
139     "is if the Content-Type is not recognized")
140
141    # Given a request string containing a unicode encoded querystring
142    headers = "\r\n".join([
143        'POST /create HTTP/1.1',
144        'Content-Type: whatever',
145    ])
146    # And a valid urlencoded body
147    body = "foobar:\nlalala"
148
149    # When I create a HTTPrettyRequest with that data
150    request = HTTPrettyRequest(headers, body)
151
152    # Then it should have a parsed body
153    request.parsed_body.should.equal("foobar:\nlalala")
154
155
156def test_request_string_representation():
157    ("HTTPrettyRequest should have a debug-friendly "
158     "string representation")
159
160    # Given a request string containing a unicode encoded querystring
161    headers = "\r\n".join([
162        'POST /create HTTP/1.1',
163        'Content-Type: JPEG-baby',
164    ])
165    # And a valid urlencoded body
166    body = "foobar:\nlalala"
167
168    # When I create a HTTPrettyRequest with that data
169    request = HTTPrettyRequest(headers, body)
170
171    # Then its string representation should show the headers and the body
172    str(request).should.equal('<HTTPrettyRequest("JPEG-baby", total_headers=1, body_length=14)>')
173
174
175def test_fake_ssl_socket_proxies_its_ow_socket():
176    ("FakeSSLSocket is a simpel wrapper around its own socket, "
177     "which was designed to be a HTTPretty fake socket")
178
179    # Given a sentinel mock object
180    socket = Mock()
181
182    # And a FakeSSLSocket wrapping it
183    ssl = FakeSSLSocket(socket)
184
185    # When I make a method call
186    ssl.send("FOO")
187
188    # Then it should bypass any method calls to its own socket
189    socket.send.assert_called_once_with("FOO")
190
191
192@patch('httpretty.core.datetime')
193def test_fakesock_socket_getpeercert(dt):
194    ("fakesock.socket#getpeercert should return a hardcoded fake certificate")
195    # Background:
196    dt.now.return_value = datetime(2013, 10, 4, 4, 20, 0)
197
198    # Given a fake socket instance
199    socket = fakesock.socket()
200
201    # And that it's bound to some host and port
202    socket.connect(('somewhere.com', 80))
203
204    # When I retrieve the peer certificate
205    certificate = socket.getpeercert()
206
207    # Then it should return a hardcoded value
208    certificate.should.equal({
209        u'notAfter': 'Sep 29 04:20:00 GMT',
210        u'subject': (
211            ((u'organizationName', u'*.somewhere.com'),),
212            ((u'organizationalUnitName', u'Domain Control Validated'),),
213            ((u'commonName', u'*.somewhere.com'),)),
214        u'subjectAltName': (
215            (u'DNS', u'*somewhere.com'),
216            (u'DNS', u'somewhere.com'),
217            (u'DNS', u'*')
218        )
219    })
220
221
222def test_fakesock_socket_ssl():
223    ("fakesock.socket#ssl should take a socket instance and return itself")
224    # Given a fake socket instance
225    socket = fakesock.socket()
226
227    # And a stubbed socket sentinel
228    sentinel = Mock()
229
230    # When I call `ssl` on that mock
231    result = socket.ssl(sentinel)
232
233    # Then it should have returned its first argument
234    result.should.equal(sentinel)
235
236
237
238@patch('httpretty.core.old_socket')
239@patch('httpretty.core.POTENTIAL_HTTP_PORTS')
240def test_fakesock_socket_connect_fallback(POTENTIAL_HTTP_PORTS, old_socket):
241    ("fakesock.socket#connect should open a real connection if the "
242     "given port is not a potential http port")
243    # Background: the potential http ports are 80 and 443
244    POTENTIAL_HTTP_PORTS.__contains__.side_effect = lambda other: int(other) in (80, 443)
245
246    # Given a fake socket instance
247    socket = fakesock.socket()
248
249    # When it is connected to a remote server in a port that isn't 80 nor 443
250    socket.connect(('somewhere.com', 42))
251
252    # Then it should have open a real connection in the background
253    old_socket.return_value.connect.assert_called_once_with(('somewhere.com', 42))
254
255    # And _closed is set to False
256    socket._closed.should.be.false
257
258
259@patch('httpretty.core.old_socket')
260def test_fakesock_socket_close(old_socket):
261    ("fakesock.socket#close should close the actual socket in case "
262     "it's not http and _closed is False")
263    # Given a fake socket instance that is synthetically open
264    socket = fakesock.socket()
265    socket._closed = False
266
267    # When I close it
268    socket.close()
269
270    # Then its real socket should have been closed
271    old_socket.return_value.close.assert_called_once_with()
272
273    # And _closed is set to True
274    socket._closed.should.be.true
275
276
277@patch('httpretty.core.old_socket')
278def test_fakesock_socket_makefile(old_socket):
279    ("fakesock.socket#makefile should set the mode, "
280     "bufsize and return its mocked file descriptor")
281
282    # Given a fake socket that has a mocked Entry associated with it
283    socket = fakesock.socket()
284    socket._entry = Mock()
285
286    # When I call makefile()
287    fd = socket.makefile(mode='rw', bufsize=512)
288
289    # Then it should have returned the socket's own filedescriptor
290    expect(fd).to.equal(socket.fd)
291    # And the mode should have been set in the socket instance
292    socket._mode.should.equal('rw')
293    # And the bufsize should have been set in the socket instance
294    socket._bufsize.should.equal(512)
295
296    # And the entry should have been filled with that filedescriptor
297    socket._entry.fill_filekind.assert_called_once_with(fd)
298
299
300@patch('httpretty.core.old_socket')
301def test_fakesock_socket_real_sendall(old_socket):
302    ("fakesock.socket#real_sendall calls truesock#connect and bails "
303     "out when not http")
304    # Background: the real socket will stop returning bytes after the
305    # first call
306    real_socket = old_socket.return_value
307    real_socket.recv.side_effect = [b'response from server', b""]
308
309    # Given a fake socket
310    socket = fakesock.socket()
311
312    # When I call real_sendall with data, some args and kwargs
313    socket.real_sendall(b"SOMEDATA", b'some extra args...', foo=b'bar')
314
315    # Then it should have called sendall in the real socket
316    real_socket.sendall.assert_called_once_with(b"SOMEDATA", b'some extra args...', foo=b'bar')
317
318    # And setblocking was never called
319    real_socket.setblocking.called.should.be.false
320
321    # And recv was never called
322    real_socket.recv.called.should.be.false
323
324    # And the buffer is empty
325    socket.fd.getvalue().should.equal(b'')
326
327    # And connect was never called
328    real_socket.connect.called.should.be.false
329
330
331@patch('httpretty.core.old_socket')
332def test_fakesock_socket_real_sendall_when_http(old_socket):
333    ("fakesock.socket#real_sendall sends data and buffers "
334     "the response in the file descriptor")
335    # Background: the real socket will stop returning bytes after the
336    # first call
337    real_socket = old_socket.return_value
338    real_socket.recv.side_effect = [b'response from server', b""]
339
340    # Given a fake socket
341    socket = fakesock.socket()
342    socket.is_http = True
343
344    # When I call real_sendall with data, some args and kwargs
345    socket.real_sendall(b"SOMEDATA", b'some extra args...', foo=b'bar')
346
347    # Then it should have called sendall in the real socket
348    real_socket.sendall.assert_called_once_with(b"SOMEDATA", b'some extra args...', foo=b'bar')
349
350    # And the socket was set to blocking
351    real_socket.setblocking.assert_called_once_with(1)
352
353    # And recv was called with the bufsize
354    real_socket.recv.assert_has_calls([
355        call(socket._bufsize)
356    ])
357
358    # And the buffer should contain the data from the server
359    socket.fd.getvalue().should.equal(b"response from server")
360
361    # And connect was called
362    real_socket.connect.called.should.be.true
363
364
365@patch('httpretty.core.old_socket')
366@patch('httpretty.core.socket')
367def test_fakesock_socket_real_sendall_continue_eagain_when_http(socket, old_socket):
368    ("fakesock.socket#real_sendall should continue if the socket error was EAGAIN")
369    socket.error = SocketErrorStub
370    # Background: the real socket will stop returning bytes after the
371    # first call
372    real_socket = old_socket.return_value
373    real_socket.recv.side_effect = [SocketErrorStub(errno.EAGAIN), b'after error', b""]
374
375    # Given a fake socket
376    socket = fakesock.socket()
377    socket.is_http = True
378
379    # When I call real_sendall with data, some args and kwargs
380    socket.real_sendall(b"SOMEDATA", b'some extra args...', foo=b'bar')
381
382    # Then it should have called sendall in the real socket
383    real_socket.sendall.assert_called_once_with(b"SOMEDATA", b'some extra args...', foo=b'bar')
384
385    # And the socket was set to blocking
386    real_socket.setblocking.assert_called_once_with(1)
387
388    # And recv was called with the bufsize
389    real_socket.recv.assert_has_calls([
390        call(socket._bufsize)
391    ])
392
393    # And the buffer should contain the data from the server
394    socket.fd.getvalue().should.equal(b"after error")
395
396    # And connect was called
397    real_socket.connect.called.should.be.true
398
399
400@patch('httpretty.core.old_socket')
401@patch('httpretty.core.socket')
402def test_fakesock_socket_real_sendall_socket_error_when_http(socket, old_socket):
403    ("fakesock.socket#real_sendall should continue if the socket error was EAGAIN")
404    socket.error = SocketErrorStub
405    # Background: the real socket will stop returning bytes after the
406    # first call
407    real_socket = old_socket.return_value
408    real_socket.recv.side_effect = [SocketErrorStub(42), b'after error', ""]
409
410    # Given a fake socket
411    socket = fakesock.socket()
412    socket.is_http = True
413
414    # When I call real_sendall with data, some args and kwargs
415    socket.real_sendall(b"SOMEDATA", b'some extra args...', foo=b'bar')
416
417    # Then it should have called sendall in the real socket
418    real_socket.sendall.assert_called_once_with(b"SOMEDATA", b'some extra args...', foo=b'bar')
419
420    # And the socket was set to blocking
421    real_socket.setblocking.assert_called_once_with(1)
422
423    # And recv was called with the bufsize
424    real_socket.recv.assert_called_once_with(socket._bufsize)
425
426    # And the buffer should contain the data from the server
427    socket.fd.getvalue().should.equal(b"")
428
429    # And connect was called
430    real_socket.connect.called.should.be.true
431
432
433@patch('httpretty.core.old_socket')
434@patch('httpretty.core.POTENTIAL_HTTP_PORTS')
435def test_fakesock_socket_real_sendall_when_http(POTENTIAL_HTTP_PORTS, old_socket):
436    ("fakesock.socket#real_sendall should connect before sending data")
437    # Background: the real socket will stop returning bytes after the
438    # first call
439    real_socket = old_socket.return_value
440    real_socket.recv.side_effect = [b'response from foobar :)', b""]
441
442    # And the potential http port is 4000
443    POTENTIAL_HTTP_PORTS.__contains__.side_effect = lambda other: int(other) == 4000
444    POTENTIAL_HTTP_PORTS.union.side_effect = lambda other: POTENTIAL_HTTP_PORTS
445
446    # Given a fake socket
447    socket = fakesock.socket()
448
449    # When I call connect to a server in a port that is considered HTTP
450    socket.connect(('foobar.com', 4000))
451
452    # And send some data
453    socket.real_sendall(b"SOMEDATA")
454
455    # Then connect should have been called
456    real_socket.connect.assert_called_once_with(('foobar.com', 4000))
457
458    # And the socket was set to blocking
459    real_socket.setblocking.assert_called_once_with(1)
460
461    # And recv was called with the bufsize
462    real_socket.recv.assert_has_calls([
463        call(socket._bufsize)
464    ])
465
466    # And the buffer should contain the data from the server
467    socket.fd.getvalue().should.equal(b"response from foobar :)")
468
469
470@patch('httpretty.core.old_socket')
471@patch('httpretty.core.httpretty')
472@patch('httpretty.core.POTENTIAL_HTTP_PORTS')
473def test_fakesock_socket_sendall_with_valid_requestline(POTENTIAL_HTTP_PORTS, httpretty, old_socket):
474    ("fakesock.socket#sendall should create an entry if it's given a valid request line")
475    matcher = Mock(name='matcher')
476    info = Mock(name='info')
477    httpretty.match_uriinfo.return_value = (matcher, info)
478    httpretty.register_uri(httpretty.GET, 'http://foo.com/foobar')
479
480    # Background:
481    # using a subclass of socket that mocks out real_sendall
482    class MySocket(fakesock.socket):
483        def real_sendall(self, data, *args, **kw):
484            raise AssertionError('should never call this...')
485
486    # Given an instance of that socket
487    socket = MySocket()
488
489    # And that is is considered http
490    socket.connect(('foo.com', 80))
491
492    # When I try to send data
493    socket.sendall(b"GET /foobar HTTP/1.1\r\nContent-Type: application/json\r\n\r\n")
494
495
496@patch('httpretty.core.old_socket')
497@patch('httpretty.core.httpretty')
498@patch('httpretty.core.POTENTIAL_HTTP_PORTS')
499def test_fakesock_socket_sendall_with_valid_requestline_2(POTENTIAL_HTTP_PORTS, httpretty, old_socket):
500    ("fakesock.socket#sendall should create an entry if it's given a valid request line")
501    matcher = Mock(name='matcher')
502    info = Mock(name='info')
503    httpretty.match_uriinfo.return_value = (matcher, info)
504    httpretty.register_uri(httpretty.GET, 'http://foo.com/foobar')
505
506    # Background:
507    # using a subclass of socket that mocks out real_sendall
508    class MySocket(fakesock.socket):
509        def real_sendall(self, data, *args, **kw):
510            raise AssertionError('should never call this...')
511
512    # Given an instance of that socket
513    socket = MySocket()
514
515    # And that is is considered http
516    socket.connect(('foo.com', 80))
517
518    # When I try to send data
519    socket.sendall(b"GET /foobar HTTP/1.1\r\nContent-Type: application/json\r\n\r\n")
520
521
522@patch('httpretty.core.old_socket')
523@patch('httpretty.core.POTENTIAL_HTTP_PORTS')
524def test_fakesock_socket_sendall_with_body_data_no_entry(POTENTIAL_HTTP_PORTS, old_socket):
525    ("fakesock.socket#sendall should call real_sendall when not parsing headers and there is no entry")
526    # Background:
527    # Using a subclass of socket that mocks out real_sendall
528
529    class MySocket(fakesock.socket):
530        def real_sendall(self, data):
531            data.should.equal(b'BLABLABLABLA')
532            return 'cool'
533
534    # Given an instance of that socket
535    socket = MySocket()
536    socket._entry = None
537
538    # And that is is considered http
539    socket.connect(('foo.com', 80))
540
541    # When I try to send data
542    result = socket.sendall(b"BLABLABLABLA")
543
544    # Then the result should be the return value from real_sendall
545    result.should.equal('cool')
546
547
548@patch('httpretty.core.old_socket')
549@patch('httpretty.core.POTENTIAL_HTTP_PORTS')
550def test_fakesock_socket_sendall_with_body_data_with_entry(POTENTIAL_HTTP_PORTS, old_socket):
551    ("fakesock.socket#sendall should call real_sendall when there is no entry")
552    # Background:
553    # Using a subclass of socket that mocks out real_sendall
554    data_sent = []
555
556    class MySocket(fakesock.socket):
557        def real_sendall(self, data):
558            data_sent.append(data)
559
560    # Given an instance of that socket
561    socket = MySocket()
562
563    # And that is is considered http
564    socket.connect(('foo.com', 80))
565
566    # When I try to send data
567    socket.sendall(b"BLABLABLABLA")
568
569    # Then it should have called real_sendall
570    data_sent.should.equal([b'BLABLABLABLA'])
571
572
573@patch('httpretty.core.httpretty.match_uriinfo')
574@patch('httpretty.core.old_socket')
575@patch('httpretty.core.POTENTIAL_HTTP_PORTS')
576def test_fakesock_socket_sendall_with_body_data_with_chunked_entry(POTENTIAL_HTTP_PORTS, old_socket, match_uriinfo):
577    ("fakesock.socket#sendall should call real_sendall when not ")
578    # Background:
579    # Using a subclass of socket that mocks out real_sendall
580
581    class MySocket(fakesock.socket):
582        def real_sendall(self, data):
583            raise AssertionError('should have never been called')
584
585    matcher = Mock(name='matcher')
586    info = Mock(name='info')
587    httpretty.match_uriinfo.return_value = (matcher, info)
588
589    # Using a mocked entry
590    entry = Mock()
591    entry.method = 'GET'
592    entry.info.path = '/foo'
593
594    entry.request.headers = {
595        'transfer-encoding': 'chunked',
596    }
597    entry.request.body = b''
598
599    # Given an instance of that socket
600    socket = MySocket()
601    socket._entry = entry
602
603    # And that is is considered http
604    socket.connect(('foo.com', 80))
605
606    # When I try to send data
607    socket.sendall(b"BLABLABLABLA")
608
609    # Then the entry should have that body
610    httpretty.last_request.body.should.equal(b'BLABLABLABLA')