/tests/unit/test_core.py
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')