1# -*- coding: utf-8 -*-
2
3from datetime import datetime
4import functools
5import random
6
7import pytest
8
9import falcon
10from falcon import testing
11from falcon import util
12from falcon.util import compat, json, misc, structures, uri
13
14
15def _arbitrary_uris(count, length):
16    return (
17        u''.join(
18            [random.choice(uri._ALL_ALLOWED)
19             for _ in range(length)]
20        ) for __ in range(count)
21    )
22
23
24class TestFalconUtils(object):
25
26    def setup_method(self, method):
27        # NOTE(cabrera): for DRYness - used in uri.[de|en]code tests
28        # below.
29        self.uris = _arbitrary_uris(count=100, length=32)
30
31    def test_deprecated_decorator(self):
32        msg = 'Please stop using this thing. It is going away.'
33
34        @util.deprecated(msg)
35        def old_thing():
36            pass
37
38        with pytest.warns(UserWarning) as rec:
39            old_thing()
40
41        warn = rec.pop()
42        assert msg in str(warn.message)
43
44    def test_http_now(self):
45        expected = datetime.utcnow()
46        actual = falcon.http_date_to_dt(falcon.http_now())
47
48        delta = actual - expected
49        delta_sec = abs(delta.days * 86400 + delta.seconds)
50
51        assert delta_sec <= 1
52
53    def test_dt_to_http(self):
54        assert falcon.dt_to_http(datetime(2013, 4, 4)) == 'Thu, 04 Apr 2013 00:00:00 GMT'
55
56        assert falcon.dt_to_http(
57            datetime(2013, 4, 4, 10, 28, 54)
58        ) == 'Thu, 04 Apr 2013 10:28:54 GMT'
59
60    def test_http_date_to_dt(self):
61        assert falcon.http_date_to_dt('Thu, 04 Apr 2013 00:00:00 GMT') == datetime(2013, 4, 4)
62
63        assert falcon.http_date_to_dt(
64            'Thu, 04 Apr 2013 10:28:54 GMT'
65        ) == datetime(2013, 4, 4, 10, 28, 54)
66
67        with pytest.raises(ValueError):
68            falcon.http_date_to_dt('Thu, 04-Apr-2013 10:28:54 GMT')
69
70        assert falcon.http_date_to_dt(
71            'Thu, 04-Apr-2013 10:28:54 GMT', obs_date=True
72        ) == datetime(2013, 4, 4, 10, 28, 54)
73
74        with pytest.raises(ValueError):
75            falcon.http_date_to_dt('Sun Nov  6 08:49:37 1994')
76
77        with pytest.raises(ValueError):
78            falcon.http_date_to_dt('Nov  6 08:49:37 1994', obs_date=True)
79
80        assert falcon.http_date_to_dt(
81            'Sun Nov  6 08:49:37 1994', obs_date=True
82        ) == datetime(1994, 11, 6, 8, 49, 37)
83
84        assert falcon.http_date_to_dt(
85            'Sunday, 06-Nov-94 08:49:37 GMT', obs_date=True
86        ) == datetime(1994, 11, 6, 8, 49, 37)
87
88    def test_pack_query_params_none(self):
89        assert falcon.to_query_str({}) == ''
90
91    def test_pack_query_params_one(self):
92        assert falcon.to_query_str({'limit': 10}) == '?limit=10'
93
94        assert falcon.to_query_str(
95            {'things': [1, 2, 3]}) == '?things=1,2,3'
96
97        assert falcon.to_query_str({'things': ['a']}) == '?things=a'
98
99        assert falcon.to_query_str(
100            {'things': ['a', 'b']}) == '?things=a,b'
101
102        expected = ('?things=a&things=b&things=&things=None'
103                    '&things=true&things=false&things=0')
104
105        actual = falcon.to_query_str(
106            {'things': ['a', 'b', '', None, True, False, 0]},
107            comma_delimited_lists=False
108        )
109
110        assert actual == expected
111
112    def test_pack_query_params_several(self):
113        garbage_in = {
114            'limit': 17,
115            'echo': True,
116            'doit': False,
117            'x': 'val',
118            'y': 0.2
119        }
120
121        query_str = falcon.to_query_str(garbage_in)
122        fields = query_str[1:].split('&')
123
124        garbage_out = {}
125        for field in fields:
126            k, v = field.split('=')
127            garbage_out[k] = v
128
129        expected = {
130            'echo': 'true',
131            'limit': '17',
132            'x': 'val',
133            'y': '0.2',
134            'doit': 'false'}
135
136        assert expected == garbage_out
137
138    def test_uri_encode(self):
139        url = 'http://example.com/v1/fizbit/messages?limit=3&echo=true'
140        assert uri.encode(url) == url
141
142        url = 'http://example.com/v1/fiz bit/messages'
143        expected = 'http://example.com/v1/fiz%20bit/messages'
144        assert uri.encode(url) == expected
145
146        url = u'http://example.com/v1/fizbit/messages?limit=3&e\u00e7ho=true'
147        expected = ('http://example.com/v1/fizbit/messages'
148                    '?limit=3&e%C3%A7ho=true')
149        assert uri.encode(url) == expected
150
151    def test_uri_encode_double(self):
152        url = 'http://example.com/v1/fiz bit/messages'
153        expected = 'http://example.com/v1/fiz%20bit/messages'
154        assert uri.encode(uri.encode(url)) == expected
155
156        url = u'http://example.com/v1/fizbit/messages?limit=3&e\u00e7ho=true'
157        expected = ('http://example.com/v1/fizbit/messages'
158                    '?limit=3&e%C3%A7ho=true')
159        assert uri.encode(uri.encode(url)) == expected
160
161        url = 'http://example.com/v1/fiz%bit/mess%ages/%'
162        expected = 'http://example.com/v1/fiz%25bit/mess%25ages/%25'
163        assert uri.encode(uri.encode(url)) == expected
164
165        url = 'http://example.com/%%'
166        expected = 'http://example.com/%25%25'
167        assert uri.encode(uri.encode(url)) == expected
168
169        # NOTE(kgriffs): Specific example cited in GH issue
170        url = 'http://something?redirect_uri=http%3A%2F%2Fsite'
171        assert uri.encode(url) == url
172
173        hex_digits = 'abcdefABCDEF0123456789'
174        for c1 in hex_digits:
175            for c2 in hex_digits:
176                url = 'http://example.com/%' + c1 + c2
177                encoded = uri.encode(uri.encode(url))
178                assert encoded == url
179
180    def test_uri_encode_value(self):
181        assert uri.encode_value('abcd') == 'abcd'
182        assert uri.encode_value(u'abcd') == u'abcd'
183        assert uri.encode_value(u'ab cd') == u'ab%20cd'
184        assert uri.encode_value(u'\u00e7') == '%C3%A7'
185        assert uri.encode_value(u'\u00e7\u20ac') == '%C3%A7%E2%82%AC'
186        assert uri.encode_value('ab/cd') == 'ab%2Fcd'
187        assert uri.encode_value('ab+cd=42,9') == 'ab%2Bcd%3D42%2C9'
188
189    def test_uri_decode(self):
190        assert uri.decode('abcd') == 'abcd'
191        assert uri.decode(u'abcd') == u'abcd'
192        assert uri.decode(u'ab%20cd') == u'ab cd'
193
194        assert uri.decode('This thing is %C3%A7') == u'This thing is \u00e7'
195
196        assert uri.decode('This thing is %C3%A7%E2%82%AC') == u'This thing is \u00e7\u20ac'
197
198        assert uri.decode('ab%2Fcd') == 'ab/cd'
199
200        assert uri.decode(
201            'http://example.com?x=ab%2Bcd%3D42%2C9'
202        ) == 'http://example.com?x=ab+cd=42,9'
203
204    def test_uri_decode_unquote_plus(self):
205        assert uri.decode('/disk/lost+found/fd0') == '/disk/lost found/fd0'
206        assert uri.decode('/disk/lost+found/fd0', unquote_plus=True) == (
207            '/disk/lost found/fd0')
208        assert uri.decode('/disk/lost+found/fd0', unquote_plus=False) == (
209            '/disk/lost+found/fd0')
210
211        assert uri.decode('http://example.com?x=ab%2Bcd%3D42%2C9') == (
212            'http://example.com?x=ab+cd=42,9')
213        assert uri.decode('http://example.com?x=ab%2Bcd%3D42%2C9', unquote_plus=True) == (
214            'http://example.com?x=ab+cd=42,9')
215        assert uri.decode('http://example.com?x=ab%2Bcd%3D42%2C9', unquote_plus=False) == (
216            'http://example.com?x=ab+cd=42,9')
217
218    def test_prop_uri_encode_models_stdlib_quote(self):
219        equiv_quote = functools.partial(
220            compat.quote, safe=uri._ALL_ALLOWED
221        )
222        for case in self.uris:
223            expect = equiv_quote(case)
224            actual = uri.encode(case)
225            assert expect == actual
226
227    def test_prop_uri_encode_value_models_stdlib_quote_safe_tilde(self):
228        equiv_quote = functools.partial(
229            compat.quote, safe='~'
230        )
231        for case in self.uris:
232            expect = equiv_quote(case)
233            actual = uri.encode_value(case)
234            assert expect == actual
235
236    def test_prop_uri_decode_models_stdlib_unquote_plus(self):
237        stdlib_unquote = compat.unquote_plus
238        for case in self.uris:
239            case = uri.encode_value(case)
240
241            expect = stdlib_unquote(case)
242            actual = uri.decode(case)
243            assert expect == actual
244
245    def test_unquote_string(self):
246        assert uri.unquote_string('v') == 'v'
247        assert uri.unquote_string('not-quoted') == 'not-quoted'
248        assert uri.unquote_string('partial-quoted"') == 'partial-quoted"'
249        assert uri.unquote_string('"partial-quoted') == '"partial-quoted'
250        assert uri.unquote_string('"partial-quoted"') == 'partial-quoted'
251
252    def test_parse_query_string(self):
253        query_strinq = (
254            'a=http%3A%2F%2Ffalconframework.org%3Ftest%3D1'
255            '&b=%7B%22test1%22%3A%20%22data1%22%'
256            '2C%20%22test2%22%3A%20%22data2%22%7D'
257            '&c=1,2,3'
258            '&d=test'
259            '&e=a,,%26%3D%2C'
260            '&f=a&f=a%3Db'
261            '&%C3%A9=a%3Db'
262        )
263        decoded_url = 'http://falconframework.org?test=1'
264        decoded_json = '{"test1": "data1", "test2": "data2"}'
265
266        result = uri.parse_query_string(query_strinq)
267        assert result['a'] == decoded_url
268        assert result['b'] == decoded_json
269        assert result['c'] == ['1', '2', '3']
270        assert result['d'] == 'test'
271        assert result['e'] == ['a', '&=,']
272        assert result['f'] == ['a', 'a=b']
273        assert result[u'é'] == 'a=b'
274
275        result = uri.parse_query_string(query_strinq, True)
276        assert result['a'] == decoded_url
277        assert result['b'] == decoded_json
278        assert result['c'] == ['1', '2', '3']
279        assert result['d'] == 'test'
280        assert result['e'] == ['a', '', '&=,']
281        assert result['f'] == ['a', 'a=b']
282        assert result[u'é'] == 'a=b'
283
284    def test_parse_host(self):
285        assert uri.parse_host('::1') == ('::1', None)
286        assert uri.parse_host('2001:ODB8:AC10:FE01::') == ('2001:ODB8:AC10:FE01::', None)
287        assert uri.parse_host(
288            '2001:ODB8:AC10:FE01::', default_port=80
289        ) == ('2001:ODB8:AC10:FE01::', 80)
290
291        ipv6_addr = '2001:4801:1221:101:1c10::f5:116'
292
293        assert uri.parse_host(ipv6_addr) == (ipv6_addr, None)
294        assert uri.parse_host('[' + ipv6_addr + ']') == (ipv6_addr, None)
295        assert uri.parse_host('[' + ipv6_addr + ']:28080') == (ipv6_addr, 28080)
296        assert uri.parse_host('[' + ipv6_addr + ']:8080') == (ipv6_addr, 8080)
297        assert uri.parse_host('[' + ipv6_addr + ']:123') == (ipv6_addr, 123)
298        assert uri.parse_host('[' + ipv6_addr + ']:42') == (ipv6_addr, 42)
299
300        assert uri.parse_host('173.203.44.122') == ('173.203.44.122', None)
301        assert uri.parse_host('173.203.44.122', default_port=80) == ('173.203.44.122', 80)
302        assert uri.parse_host('173.203.44.122:27070') == ('173.203.44.122', 27070)
303        assert uri.parse_host('173.203.44.122:123') == ('173.203.44.122', 123)
304        assert uri.parse_host('173.203.44.122:42') == ('173.203.44.122', 42)
305
306        assert uri.parse_host('example.com') == ('example.com', None)
307        assert uri.parse_host('example.com', default_port=443) == ('example.com', 443)
308        assert uri.parse_host('falcon.example.com') == ('falcon.example.com', None)
309        assert uri.parse_host('falcon.example.com:9876') == ('falcon.example.com', 9876)
310        assert uri.parse_host('falcon.example.com:42') == ('falcon.example.com', 42)
311
312    def test_get_http_status(self):
313        assert falcon.get_http_status(404) == falcon.HTTP_404
314        assert falcon.get_http_status(404.3) == falcon.HTTP_404
315        assert falcon.get_http_status('404.3') == falcon.HTTP_404
316        assert falcon.get_http_status(404.9) == falcon.HTTP_404
317        assert falcon.get_http_status('404') == falcon.HTTP_404
318        assert falcon.get_http_status(123) == '123 Unknown'
319        with pytest.raises(ValueError):
320            falcon.get_http_status('not_a_number')
321        with pytest.raises(ValueError):
322            falcon.get_http_status(0)
323        with pytest.raises(ValueError):
324            falcon.get_http_status(0)
325        with pytest.raises(ValueError):
326            falcon.get_http_status(99)
327        with pytest.raises(ValueError):
328            falcon.get_http_status(-404.3)
329        with pytest.raises(ValueError):
330            falcon.get_http_status('-404')
331        with pytest.raises(ValueError):
332            falcon.get_http_status('-404.3')
333        assert falcon.get_http_status(123, 'Go Away') == '123 Go Away'
334
335    def test_etag_dumps_to_header_format(self):
336        etag = structures.ETag('67ab43')
337
338        assert etag.dumps() == '"67ab43"'
339
340        etag.is_weak = True
341        assert etag.dumps() == 'W/"67ab43"'
342
343        assert structures.ETag('67a b43').dumps() == '"67a b43"'
344
345    def test_etag_strong_vs_weak_comparison(self):
346        strong_67ab43_one = structures.ETag.loads('"67ab43"')
347        strong_67ab43_too = structures.ETag.loads('"67ab43"')
348        strong_67aB43 = structures.ETag.loads('"67aB43"')
349        weak_67ab43_one = structures.ETag.loads('W/"67ab43"')
350        weak_67ab43_two = structures.ETag.loads('W/"67ab43"')
351        weak_67aB43 = structures.ETag.loads('W/"67aB43"')
352
353        assert strong_67aB43 == strong_67aB43
354        assert weak_67aB43 == weak_67aB43
355        assert strong_67aB43 == weak_67aB43
356        assert weak_67aB43 == strong_67aB43
357        assert strong_67ab43_one == strong_67ab43_too
358        assert weak_67ab43_one == weak_67ab43_two
359
360        assert strong_67aB43 != strong_67ab43_one
361        assert strong_67ab43_one != strong_67aB43
362
363        assert strong_67aB43.strong_compare(strong_67aB43)
364        assert strong_67ab43_one.strong_compare(strong_67ab43_too)
365        assert not strong_67aB43.strong_compare(strong_67ab43_one)
366        assert not strong_67ab43_one.strong_compare(strong_67aB43)
367
368        assert not strong_67ab43_one.strong_compare(weak_67ab43_one)
369        assert not weak_67ab43_one.strong_compare(strong_67ab43_one)
370
371        assert not weak_67aB43.strong_compare(weak_67aB43)
372        assert not weak_67ab43_one.strong_compare(weak_67ab43_two)
373
374        assert not weak_67ab43_one.strong_compare(weak_67aB43)
375        assert not weak_67aB43.strong_compare(weak_67ab43_one)
376
377
378@pytest.mark.parametrize(
379    'protocol,method',
380    zip(
381        ['https'] * len(falcon.HTTP_METHODS) + ['http'] * len(falcon.HTTP_METHODS),
382        falcon.HTTP_METHODS * 2
383    )
384)
385def test_simulate_request_protocol(protocol, method):
386    sink_called = [False]
387
388    def sink(req, resp):
389        sink_called[0] = True
390        assert req.protocol == protocol
391
392    app = falcon.API()
393    app.add_sink(sink, '/test')
394
395    client = testing.TestClient(app)
396
397    try:
398        simulate = client.getattr('simulate_' + method.lower())
399        simulate('/test', protocol=protocol)
400        assert sink_called[0]
401    except AttributeError:
402        # NOTE(kgriffs): simulate_* helpers do not exist for all methods
403        pass
404
405
406@pytest.mark.parametrize('simulate', [
407    testing.simulate_get,
408    testing.simulate_head,
409    testing.simulate_post,
410    testing.simulate_put,
411    testing.simulate_options,
412    testing.simulate_patch,
413    testing.simulate_delete,
414])
415def test_simulate_free_functions(simulate):
416    sink_called = [False]
417
418    def sink(req, resp):
419        sink_called[0] = True
420
421    app = falcon.API()
422    app.add_sink(sink, '/test')
423
424    simulate(app, '/test')
425    assert sink_called[0]
426
427
428class TestFalconTestingUtils(object):
429    """Verify some branches not covered elsewhere."""
430
431    def test_path_escape_chars_in_create_environ(self):
432        env = testing.create_environ('/hello%20world%21')
433        assert env['PATH_INFO'] == '/hello world!'
434
435    def test_no_prefix_allowed_for_query_strings_in_create_environ(self):
436        with pytest.raises(ValueError):
437            testing.create_environ(query_string='?foo=bar')
438
439    @pytest.mark.skipif(compat.PY3, reason='Test does not apply to Py3K')
440    def test_unicode_path_in_create_environ(self):
441        env = testing.create_environ(u'/fancy/unícode')
442        assert env['PATH_INFO'] == '/fancy/un\xc3\xadcode'
443
444        env = testing.create_environ(u'/simple')
445        assert env['PATH_INFO'] == '/simple'
446
447    def test_plus_in_path_in_create_environ(self):
448        env = testing.create_environ('/mnt/grub2/lost+found/inode001')
449        assert env['PATH_INFO'] == '/mnt/grub2/lost+found/inode001'
450
451    def test_none_header_value_in_create_environ(self):
452        env = testing.create_environ('/', headers={'X-Foo': None})
453        assert env['HTTP_X_FOO'] == ''
454
455    def test_decode_empty_result(self):
456        app = falcon.API()
457        client = testing.TestClient(app)
458        response = client.simulate_request(path='/')
459        assert response.text == ''
460
461    def test_httpnow_alias_for_backwards_compat(self):
462        assert testing.httpnow is util.http_now
463
464    def test_default_headers(self):
465        app = falcon.API()
466        resource = testing.SimpleTestResource()
467        app.add_route('/', resource)
468
469        headers = {
470            'Authorization': 'Bearer 123',
471        }
472
473        client = testing.TestClient(app, headers=headers)
474
475        client.simulate_get()
476        assert resource.captured_req.auth == headers['Authorization']
477
478        client.simulate_get(headers=None)
479        assert resource.captured_req.auth == headers['Authorization']
480
481    def test_default_headers_with_override(self):
482        app = falcon.API()
483        resource = testing.SimpleTestResource()
484        app.add_route('/', resource)
485
486        override_before = 'something-something'
487        override_after = 'something-something'[::-1]
488
489        headers = {
490            'Authorization': 'Bearer XYZ',
491            'Accept': 'application/vnd.siren+json',
492            'X-Override-Me': override_before,
493        }
494
495        client = testing.TestClient(app, headers=headers)
496        client.simulate_get(headers={'X-Override-Me': override_after})
497
498        assert resource.captured_req.auth == headers['Authorization']
499        assert resource.captured_req.accept == headers['Accept']
500        assert resource.captured_req.get_header('X-Override-Me') == override_after
501
502    def test_status(self):
503        app = falcon.API()
504        resource = testing.SimpleTestResource(status=falcon.HTTP_702)
505        app.add_route('/', resource)
506        client = testing.TestClient(app)
507
508        result = client.simulate_get()
509        assert result.status == falcon.HTTP_702
510
511    def test_wsgi_iterable_not_closeable(self):
512        result = testing.Result([], falcon.HTTP_200, [])
513        assert not result.content
514        assert result.json is None
515
516    def test_path_must_start_with_slash(self):
517        app = falcon.API()
518        app.add_route('/', testing.SimpleTestResource())
519        client = testing.TestClient(app)
520        with pytest.raises(ValueError):
521            client.simulate_get('foo')
522
523    def test_cached_text_in_result(self):
524        app = falcon.API()
525        app.add_route('/', testing.SimpleTestResource(body='test'))
526        client = testing.TestClient(app)
527
528        result = client.simulate_get()
529        assert result.text == result.text
530
531    def test_simple_resource_body_json_xor(self):
532        with pytest.raises(ValueError):
533            testing.SimpleTestResource(body='', json={})
534
535    def test_query_string(self):
536        class SomeResource(object):
537            def on_get(self, req, resp):
538                doc = {}
539
540                doc['oid'] = req.get_param_as_int('oid')
541                doc['detailed'] = req.get_param_as_bool('detailed')
542                doc['things'] = req.get_param_as_list('things', int)
543                doc['query_string'] = req.query_string
544
545                resp.body = json.dumps(doc)
546
547        app = falcon.API()
548        app.req_options.auto_parse_qs_csv = True
549        app.add_route('/', SomeResource())
550        client = testing.TestClient(app)
551
552        result = client.simulate_get(query_string='oid=42&detailed=no&things=1')
553        assert result.json['oid'] == 42
554        assert not result.json['detailed']
555        assert result.json['things'] == [1]
556
557        params = {'oid': 42, 'detailed': False}
558        result = client.simulate_get(params=params)
559        assert result.json['oid'] == params['oid']
560        assert not result.json['detailed']
561        assert result.json['things'] is None
562
563        params = {'oid': 1978, 'detailed': 'yes', 'things': [1, 2, 3]}
564        result = client.simulate_get(params=params)
565        assert result.json['oid'] == params['oid']
566        assert result.json['detailed']
567        assert result.json['things'] == params['things']
568
569        expected_qs = 'things=1,2,3'
570        result = client.simulate_get(params={'things': [1, 2, 3]})
571        assert result.json['query_string'] == expected_qs
572
573        expected_qs = 'things=1&things=2&things=3'
574        result = client.simulate_get(params={'things': [1, 2, 3]},
575                                     params_csv=False)
576        assert result.json['query_string'] == expected_qs
577
578    def test_query_string_no_question(self):
579        app = falcon.API()
580        app.add_route('/', testing.SimpleTestResource())
581        client = testing.TestClient(app)
582        with pytest.raises(ValueError):
583            client.simulate_get(query_string='?x=1')
584
585    def test_query_string_in_path(self):
586        app = falcon.API()
587        resource = testing.SimpleTestResource()
588        app.add_route('/thing', resource)
589        client = testing.TestClient(app)
590
591        with pytest.raises(ValueError):
592            client.simulate_get(path='/thing?x=1', query_string='things=1,2,3')
593        with pytest.raises(ValueError):
594            client.simulate_get(path='/thing?x=1', params={'oid': 1978})
595        with pytest.raises(ValueError):
596            client.simulate_get(path='/thing?x=1', query_string='things=1,2,3',
597                                params={'oid': 1978})
598
599        client.simulate_get(path='/thing?detailed=no&oid=1337')
600        assert resource.captured_req.path == '/thing'
601        assert resource.captured_req.query_string == 'detailed=no&oid=1337'
602
603    @pytest.mark.parametrize('document', [
604        # NOTE(vytas): using an exact binary fraction here to avoid special
605        # code branch for approximate equality as it is not the focus here
606        16.0625,
607        123456789,
608        True,
609        '',
610        u'I am a \u1d0a\ua731\u1d0f\u0274 string.',
611        [1, 3, 3, 7],
612        {u'message': u'\xa1Hello Unicode! \U0001F638'},
613        {
614            'count': 4,
615            'items': [
616                {'number': 'one'},
617                {'number': 'two'},
618                {'number': 'three'},
619                {'number': 'four'},
620            ],
621            'next': None,
622        },
623    ])
624    def test_simulate_json_body(self, document):
625        app = falcon.API()
626        resource = testing.SimpleTestResource()
627        app.add_route('/', resource)
628
629        json_types = ('application/json', 'application/json; charset=UTF-8')
630        client = testing.TestClient(app)
631        client.simulate_post('/', json=document)
632        captured_body = resource.captured_req.bounded_stream.read().decode('utf-8')
633        assert json.loads(captured_body) == document
634        assert resource.captured_req.content_type in json_types
635
636        headers = {
637            'Content-Type': 'x-falcon/peregrine',
638            'X-Falcon-Type': 'peregrine',
639        }
640        body = 'If provided, `json` parameter overrides `body`.'
641        client.simulate_post('/', headers=headers, body=body, json=document)
642        assert resource.captured_req.media == document
643        assert resource.captured_req.content_type in json_types
644        assert resource.captured_req.get_header('X-Falcon-Type') == 'peregrine'
645
646    @pytest.mark.parametrize('remote_addr', [
647        None,
648        '127.0.0.1',
649        '8.8.8.8',
650        '104.24.101.85',
651        '2606:4700:30::6818:6455',
652    ])
653    def test_simulate_remote_addr(self, remote_addr):
654        class ShowMyIPResource(object):
655            def on_get(self, req, resp):
656                resp.body = req.remote_addr
657                resp.content_type = falcon.MEDIA_TEXT
658
659        app = falcon.API()
660        app.add_route('/', ShowMyIPResource())
661
662        client = testing.TestClient(app)
663        resp = client.simulate_get('/', remote_addr=remote_addr)
664        assert resp.status_code == 200
665
666        if remote_addr is None:
667            assert resp.text == '127.0.0.1'
668        else:
669            assert resp.text == remote_addr
670
671    def test_simulate_hostname(self):
672        app = falcon.API()
673        resource = testing.SimpleTestResource()
674        app.add_route('/', resource)
675
676        client = testing.TestClient(app)
677        client.simulate_get('/', protocol='https',
678                            host='falcon.readthedocs.io')
679        assert resource.captured_req.uri == 'https://falcon.readthedocs.io/'
680
681    @pytest.mark.parametrize('extras,expected_headers', [
682        (
683            {},
684            (('user-agent', 'curl/7.24.0 (x86_64-apple-darwin12.0)'),),
685        ),
686        (
687            {'HTTP_USER_AGENT': 'URL/Emacs', 'HTTP_X_FALCON': 'peregrine'},
688            (('user-agent', 'URL/Emacs'), ('x-falcon', 'peregrine')),
689        ),
690    ])
691    def test_simulate_with_environ_extras(self, extras, expected_headers):
692        app = falcon.API()
693        resource = testing.SimpleTestResource()
694        app.add_route('/', resource)
695
696        client = testing.TestClient(app)
697        client.simulate_get('/', extras=extras)
698
699        for header, value in expected_headers:
700            assert resource.captured_req.get_header(header) == value
701
702    def test_override_method_with_extras(self):
703        app = falcon.API()
704        app.add_route('/', testing.SimpleTestResource(body='test'))
705        client = testing.TestClient(app)
706
707        with pytest.raises(ValueError):
708            client.simulate_get('/', extras={'REQUEST_METHOD': 'PATCH'})
709
710        resp = client.simulate_get('/', extras={'REQUEST_METHOD': 'GET'})
711        assert resp.status_code == 200
712        assert resp.text == 'test'
713
714
715class TestNoApiClass(testing.TestCase):
716    def test_something(self):
717        self.assertTrue(isinstance(self.app, falcon.API))
718
719
720class TestSetupApi(testing.TestCase):
721    def setUp(self):
722        super(TestSetupApi, self).setUp()
723        self.api = falcon.API()
724
725    def test_something(self):
726        self.assertTrue(isinstance(self.api, falcon.API))
727
728
729def test_get_argnames():
730    def foo(a, b, c):
731        pass
732
733    class Bar(object):
734        def __call__(self, a, b):
735            pass
736
737    assert misc.get_argnames(foo) == ['a', 'b', 'c']
738    assert misc.get_argnames(Bar()) == ['a', 'b']
739
740    # NOTE(kgriffs): This difference will go away once we drop Python 2.7
741    # support, so we just use this regression test to ensure the status quo.
742    expected = ['b', 'c'] if compat.PY3 else ['a', 'b', 'c']
743    assert misc.get_argnames(functools.partial(foo, 42)) == expected
744
745
746class TestContextType(object):
747
748    class CustomContextType(structures.Context):
749        def __init__(self):
750            pass
751
752    @pytest.mark.parametrize('context_type', [
753        CustomContextType,
754        structures.Context,
755    ])
756    def test_attributes(self, context_type):
757        ctx = context_type()
758
759        ctx.foo = 'bar'
760        ctx.details = None
761        ctx._cache = {}
762
763        assert ctx.foo == 'bar'
764        assert ctx.details is None
765        assert ctx._cache == {}
766
767        with pytest.raises(AttributeError):
768            ctx.cache_strategy
769
770    @pytest.mark.parametrize('context_type', [
771        CustomContextType,
772        structures.Context,
773    ])
774    def test_items_from_attributes(self, context_type):
775        ctx = context_type()
776
777        ctx.foo = 'bar'
778        ctx.details = None
779        ctx._cache = {}
780
781        assert ctx['foo'] == 'bar'
782        assert ctx['details'] is None
783        assert ctx['_cache'] == {}
784
785        with pytest.raises(KeyError):
786            ctx['cache_strategy']
787
788        assert 'foo' in ctx
789        assert '_cache' in ctx
790        assert 'cache_strategy' not in ctx
791
792    @pytest.mark.parametrize('context_type', [
793        CustomContextType,
794        structures.Context,
795    ])
796    def test_attributes_from_items(self, context_type):
797        ctx = context_type()
798
799        ctx['foo'] = 'bar'
800        ctx['details'] = None
801        ctx['_cache'] = {}
802        ctx['cache_strategy'] = 'lru'
803
804        assert ctx['cache_strategy'] == 'lru'
805        del ctx['cache_strategy']
806
807        assert ctx['foo'] == 'bar'
808        assert ctx['details'] is None
809        assert ctx['_cache'] == {}
810
811        with pytest.raises(KeyError):
812            ctx['cache_strategy']
813
814    @pytest.mark.parametrize('context_type,type_name', [
815        (CustomContextType, 'CustomContextType'),
816        (structures.Context, 'Context'),
817    ])
818    def test_dict_interface(self, context_type, type_name):
819        ctx = context_type()
820
821        ctx['foo'] = 'bar'
822        ctx['details'] = None
823        ctx[1] = 'one'
824        ctx[2] = 'two'
825
826        assert ctx == {'foo': 'bar', 'details': None, 1: 'one', 2: 'two'}
827        assert ctx != {'bar': 'foo', 'details': None, 1: 'one', 2: 'two'}
828        assert ctx != {}
829
830        copy = ctx.copy()
831        assert isinstance(copy, context_type)
832        assert copy == ctx
833        assert copy == {'foo': 'bar', 'details': None, 1: 'one', 2: 'two'}
834        copy.pop('foo')
835        assert copy != ctx
836
837        assert set(key for key in ctx) == {'foo', 'details', 1, 2}
838
839        assert ctx.get('foo') == 'bar'
840        assert ctx.get('bar') is None
841        assert ctx.get('bar', frozenset('hello')) == frozenset('hello')
842        false = ctx.get('bar', False)
843        assert isinstance(false, bool)
844        assert not false
845
846        assert len(ctx) == 4
847        assert ctx.pop(3) is None
848        assert ctx.pop(3, 'not found') == 'not found'
849        assert ctx.pop('foo') == 'bar'
850        assert ctx.pop(1) == 'one'
851        assert ctx.pop(2) == 'two'
852        assert len(ctx) == 1
853
854        assert repr(ctx) == type_name + "({'details': None})"
855        assert str(ctx) == type_name + "({'details': None})"
856        assert '{}'.format(ctx) == type_name + "({'details': None})"
857
858        with pytest.raises(TypeError):
859            {ctx: ctx}
860
861        ctx.clear()
862        assert ctx == {}
863        assert len(ctx) == 0
864
865        ctx['key'] = 'value'
866        assert ctx.popitem() == ('key', 'value')
867
868        ctx.setdefault('numbers', []).append(1)
869        ctx.setdefault('numbers', []).append(2)
870        ctx.setdefault('numbers', []).append(3)
871        assert ctx['numbers'] == [1, 2, 3]
872
873    @pytest.mark.parametrize('context_type', [
874        CustomContextType,
875        structures.Context,
876    ])
877    def test_keys_and_values(self, context_type):
878        ctx = context_type()
879        ctx.update((number, number ** 2) for number in range(1, 5))
880
881        assert set(ctx.keys()) == {1, 2, 3, 4}
882        assert set(ctx.values()) == {1, 4, 9, 16}
883        assert set(ctx.items()) == {(1, 1), (2, 4), (3, 9), (4, 16)}
884
885    @pytest.mark.skipif(compat.PY3, reason='python2-specific dict methods')
886    def test_python2_dict_methods(self):
887        ctx = structures.Context()
888        ctx.update((number, number ** 2) for number in range(1, 5))
889
890        assert set(ctx.keys()) == set(ctx.iterkeys()) == set(ctx.viewkeys())
891        assert set(ctx.values()) == set(ctx.itervalues()) == set(ctx.viewvalues())
892        assert set(ctx.items()) == set(ctx.iteritems()) == set(ctx.viewitems())
893
894        assert ctx.has_key(2)  # noqa
895