PageRenderTime 43ms CodeModel.GetById 3ms app.highlight 34ms RepoModel.GetById 1ms app.codeStats 1ms

/tests/unit/test_core.py

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