1import pytest
2
3import json
4
5from webob.request import Request
6from webob.dec import wsgify
7from webob import exc as webob_exc
8
9@wsgify
10def method_not_allowed_app(req):
11    if req.method != 'GET':
12        raise webob_exc.HTTPMethodNotAllowed()
13    return 'hello!'
14
15def test_noescape_null():
16    assert webob_exc.no_escape(None) == ''
17
18def test_noescape_not_basestring():
19    assert webob_exc.no_escape(42) == '42'
20
21def test_noescape_unicode():
22    class DummyUnicodeObject(object):
23        def __unicode__(self):
24            return '42'
25    duo = DummyUnicodeObject()
26    assert webob_exc.no_escape(duo) == '42'
27
28def test_strip_tags_empty():
29    assert webob_exc.strip_tags('') == ''
30
31def test_strip_tags_newline_to_space():
32    assert webob_exc.strip_tags('a\nb') == 'a b'
33
34def test_strip_tags_zaps_carriage_return():
35    assert webob_exc.strip_tags('a\rb') == 'ab'
36
37def test_strip_tags_br_to_newline():
38    assert webob_exc.strip_tags('a<br/>b') == 'a\nb'
39
40def test_strip_tags_zaps_comments():
41    assert webob_exc.strip_tags('a<!--b-->') == 'ab'
42
43def test_strip_tags_zaps_tags():
44    assert webob_exc.strip_tags('foo<bar>baz</bar>') == 'foobaz'
45
46def test_HTTPException():
47    _called = []
48    _result = object()
49    def _response(environ, start_response):
50        _called.append((environ, start_response))
51        return _result
52    environ = {}
53    start_response = object()
54    exc = webob_exc.HTTPException('testing', _response)
55    assert exc.wsgi_response is _response
56    result = exc(environ, start_response)
57    assert result is result
58    assert _called == [(environ, start_response)]
59
60def test_exception_with_unicode_data():
61    req = Request.blank('/', method='POST')
62    res = req.get_response(method_not_allowed_app)
63    assert res.status_code == 405
64
65def test_WSGIHTTPException_headers():
66    exc = webob_exc.WSGIHTTPException(headers=[('Set-Cookie', 'a=1'),
67                                     ('Set-Cookie', 'a=2')])
68    mixed = exc.headers.mixed()
69    assert mixed['set-cookie'] == ['a=1', 'a=2']
70
71def test_WSGIHTTPException_w_body_template():
72    from string import Template
73    TEMPLATE = '$foo: $bar'
74    exc = webob_exc.WSGIHTTPException(body_template=TEMPLATE)
75    assert exc.body_template == TEMPLATE
76    assert isinstance(exc.body_template_obj, Template)
77    assert exc.body_template_obj.substitute({'foo': 'FOO', 'bar': 'BAR'}) == 'FOO: BAR'
78
79def test_WSGIHTTPException_w_empty_body():
80    class EmptyOnly(webob_exc.WSGIHTTPException):
81        empty_body = True
82    exc = EmptyOnly(content_type='text/plain', content_length=234)
83    assert 'content_type' not in exc.__dict__
84    assert 'content_length' not in exc.__dict__
85
86def test_WSGIHTTPException___str__():
87    exc1 = webob_exc.WSGIHTTPException(detail='Detail')
88    assert str(exc1) == 'Detail'
89    class Explain(webob_exc.WSGIHTTPException):
90        explanation = 'Explanation'
91    assert str(Explain()) == 'Explanation'
92
93def test_WSGIHTTPException_plain_body_no_comment():
94    class Explain(webob_exc.WSGIHTTPException):
95        code = '999'
96        title = 'Testing'
97        explanation = 'Explanation'
98    exc = Explain(detail='Detail')
99    assert exc.plain_body({}) == '999 Testing\n\nExplanation\n\n Detail  '
100
101def test_WSGIHTTPException_html_body_w_comment():
102    class Explain(webob_exc.WSGIHTTPException):
103        code = '999'
104        title = 'Testing'
105        explanation = 'Explanation'
106    exc = Explain(detail='Detail', comment='Comment')
107    assert exc.html_body({}) == (
108        '<html>\n'
109        ' <head>\n'
110        '  <title>999 Testing</title>\n'
111        ' </head>\n'
112        ' <body>\n'
113        '  <h1>999 Testing</h1>\n'
114        '  Explanation<br /><br />\n'
115        'Detail\n'
116        '<!-- Comment -->\n\n'
117        ' </body>\n'
118        '</html>'
119        )
120
121def test_WSGIHTTPException_json_body_no_comment():
122    class ValidationError(webob_exc.WSGIHTTPException):
123        code = '422'
124        title = 'Validation Failed'
125        explanation = 'Validation of an attribute failed.'
126
127    exc = ValidationError(detail='Attribute "xyz" is invalid.')
128    body = exc.json_body({})
129    assert json.loads(body) == {
130        "code": "422 Validation Failed",
131        "title": "Validation Failed",
132        "message": "Validation of an attribute failed.<br /><br />\nAttribute"
133                   ' "xyz" is invalid.\n\n',
134    }
135
136def test_WSGIHTTPException_respects_application_json():
137    class ValidationError(webob_exc.WSGIHTTPException):
138        code = '422'
139        title = 'Validation Failed'
140        explanation = 'Validation of an attribute failed.'
141    def start_response(status, headers, exc_info=None):
142        # check that json doesn't contain a charset
143        assert ('Content-Type', 'application/json') in headers
144        pass
145
146    exc = ValidationError(detail='Attribute "xyz" is invalid.')
147    resp = exc.generate_response(environ={
148        'wsgi.url_scheme': 'HTTP',
149        'SERVER_NAME': 'localhost',
150        'SERVER_PORT': '80',
151        'REQUEST_METHOD': 'PUT',
152        'HTTP_ACCEPT': 'application/json',
153    }, start_response=start_response)
154    assert json.loads(resp[0].decode('utf-8')) == {
155        "code": "422 Validation Failed",
156        "title": "Validation Failed",
157        "message": "Validation of an attribute failed.<br /><br />\nAttribute"
158                   ' "xyz" is invalid.\n\n',
159    }
160
161def test_WSGIHTTPException_respects_accept_text_html():
162    def start_response(status, headers, exc_info=None):
163        for header in headers:
164            if header[0] == 'Content-Type':
165                assert header[1].startswith('text/html')
166
167    exc = webob_exc.WSGIHTTPException()
168    resp = exc.generate_response(environ={
169        'wsgi.url_scheme': 'HTTP',
170        'SERVER_NAME': 'localhost',
171        'SERVER_PORT': '80',
172        'REQUEST_METHOD': 'GET',
173        'HTTP_ACCEPT': 'text/html',
174    }, start_response=start_response)
175
176def test_WSGIHTTPException_respects_accept_text_plain():
177    def start_response(status, headers, exc_info=None):
178        for header in headers:
179            if header[0] == 'Content-Type':
180                assert header[1].startswith('text/plain')
181
182    exc = webob_exc.WSGIHTTPException()
183    resp = exc.generate_response(environ={
184        'wsgi.url_scheme': 'HTTP',
185        'SERVER_NAME': 'localhost',
186        'SERVER_PORT': '80',
187        'REQUEST_METHOD': 'GET',
188        'HTTP_ACCEPT': 'text/plain',
189    }, start_response=start_response)
190
191def test_WSGIHTTPException_respects_accept_star_star():
192    def start_response(status, headers, exc_info=None):
193        for header in headers:
194            if header[0] == 'Content-Type':
195                assert header[1].startswith('text/html')
196
197    exc = webob_exc.WSGIHTTPException()
198    resp = exc.generate_response(environ={
199        'wsgi.url_scheme': 'HTTP',
200        'SERVER_NAME': 'localhost',
201        'SERVER_PORT': '80',
202        'REQUEST_METHOD': 'GET',
203        'HTTP_ACCEPT': '*/*',
204    }, start_response=start_response)
205
206def test_WSGIHTTPException_allows_custom_json_formatter():
207    def json_formatter(body, status, title, environ):
208        return {"fake": True}
209    class ValidationError(webob_exc.WSGIHTTPException):
210        code = '422'
211        title = 'Validation Failed'
212        explanation = 'Validation of an attribute failed.'
213
214    exc = ValidationError(detail='Attribute "xyz" is invalid.',
215                          json_formatter=json_formatter)
216    body = exc.json_body({})
217    assert json.loads(body) == {"fake": True}
218
219def test_WSGIHTTPException_generate_response():
220    def start_response(status, headers, exc_info=None):
221        assert ('Content-Type', 'text/html; charset=UTF-8') in headers
222        pass
223    environ = {
224       'wsgi.url_scheme': 'HTTP',
225       'SERVER_NAME': 'localhost',
226       'SERVER_PORT': '80',
227       'REQUEST_METHOD': 'PUT',
228       'HTTP_ACCEPT': 'text/html'
229    }
230    excep = webob_exc.WSGIHTTPException()
231    assert excep(environ, start_response) == [
232        b'<html>\n'
233        b' <head>\n'
234        b'  <title>500 Internal Server Error</title>\n'
235        b' </head>\n'
236        b' <body>\n'
237        b'  <h1>500 Internal Server Error</h1>\n'
238        b'  <br /><br />\n'
239        b'\n'
240        b'\n\n'
241        b' </body>\n'
242        b'</html>']
243
244def test_WSGIHTTPException_call_w_body():
245    def start_response(status, headers, exc_info=None):
246        pass
247    environ = {
248       'wsgi.url_scheme': 'HTTP',
249       'SERVER_NAME': 'localhost',
250       'SERVER_PORT': '80',
251       'REQUEST_METHOD': 'PUT'
252    }
253    excep = webob_exc.WSGIHTTPException()
254    excep.body = b'test'
255    assert  excep(environ,start_response) == [b'test']
256
257
258def test_WSGIHTTPException_wsgi_response():
259    def start_response(status, headers, exc_info=None):
260        pass
261    environ = {
262       'wsgi.url_scheme': 'HTTP',
263       'SERVER_NAME': 'localhost',
264       'SERVER_PORT': '80',
265       'REQUEST_METHOD': 'HEAD'
266    }
267    excep = webob_exc.WSGIHTTPException()
268    assert  excep.wsgi_response(environ,start_response) == []
269
270def test_WSGIHTTPException_exception_newstyle():
271    def start_response(status, headers, exc_info=None):
272        pass
273    environ = {
274       'wsgi.url_scheme': 'HTTP',
275       'SERVER_NAME': 'localhost',
276       'SERVER_PORT': '80',
277       'REQUEST_METHOD': 'HEAD'
278    }
279    excep = webob_exc.WSGIHTTPException()
280    webob_exc.newstyle_exceptions = True
281    assert  excep(environ,start_response) == []
282
283def test_WSGIHTTPException_exception_no_newstyle():
284    def start_response(status, headers, exc_info=None):
285        pass
286    environ = {
287       'wsgi.url_scheme': 'HTTP',
288       'SERVER_NAME': 'localhost',
289       'SERVER_PORT': '80',
290       'REQUEST_METHOD': 'HEAD'
291    }
292    excep = webob_exc.WSGIHTTPException()
293    webob_exc.newstyle_exceptions = False
294    assert  excep(environ,start_response) == []
295
296def test_HTTPOk_head_of_proxied_head():
297    # first set up a response to a HEAD request
298    HELLO_WORLD = "Hi!\n"
299    CONTENT_TYPE = "application/hello"
300    def head_app(environ, start_response):
301        """An application object that understands HEAD"""
302        status = '200 OK'
303        response_headers = [('Content-Type', CONTENT_TYPE),
304                            ('Content-Length', len(HELLO_WORLD))]
305        start_response(status, response_headers)
306
307        if environ['REQUEST_METHOD'] == 'HEAD':
308            return []
309        else:
310            return [HELLO_WORLD]
311
312    def verify_response(resp, description):
313        assert resp.content_type == CONTENT_TYPE, description
314        assert resp.content_length == len(HELLO_WORLD), description
315        assert resp.body == b'', description
316
317    req = Request.blank('/', method='HEAD')
318    resp1 = req.get_response(head_app)
319    verify_response(resp1, "first response")
320
321    # Copy the response like a proxy server would.
322    # Copying an empty body has set content_length
323    # so copy the headers only afterwards.
324    resp2 = webob_exc.status_map[resp1.status_int](request=req)
325    resp2.body = resp1.body
326    resp2.headerlist = resp1.headerlist
327    verify_response(resp2, "copied response")
328
329    # evaluate it again
330    resp3 = req.get_response(resp2)
331    verify_response(resp3, "evaluated copy")
332
333def test_HTTPMove():
334    def start_response(status, headers, exc_info=None):
335        pass
336    environ = {
337       'wsgi.url_scheme': 'HTTP',
338       'SERVER_NAME': 'localhost',
339       'SERVER_PORT': '80',
340       'REQUEST_METHOD': 'HEAD',
341       'PATH_INFO': '/',
342    }
343    m = webob_exc._HTTPMove()
344    assert  m( environ, start_response ) == []
345
346def test_HTTPMove_location_not_none():
347    def start_response(status, headers, exc_info=None):
348        pass
349    environ = {
350       'wsgi.url_scheme': 'HTTP',
351       'SERVER_NAME': 'localhost',
352       'SERVER_PORT': '80',
353       'REQUEST_METHOD': 'HEAD',
354       'PATH_INFO': '/',
355    }
356    m = webob_exc._HTTPMove(location='http://example.com')
357    assert  m( environ, start_response ) == []
358
359def test_HTTPMove_location_newlines():
360    environ = {
361       'wsgi.url_scheme': 'HTTP',
362       'SERVER_NAME': 'localhost',
363       'SERVER_PORT': '80',
364       'REQUEST_METHOD': 'HEAD',
365       'PATH_INFO': '/',
366    }
367    with pytest.raises(ValueError):
368        webob_exc._HTTPMove(location='http://example.com\r\nX-Test: false')
369
370def test_HTTPMove_add_slash_and_location():
371    def start_response(status, headers, exc_info=None):
372        pass
373    with pytest.raises(TypeError):
374        webob_exc._HTTPMove(location='http://example.com', add_slash=True)
375
376def test_HTTPMove_call_add_slash():
377    def start_response(status, headers, exc_info=None):
378        pass
379    environ = {
380       'wsgi.url_scheme': 'HTTP',
381       'SERVER_NAME': 'localhost',
382       'SERVER_PORT': '80',
383       'REQUEST_METHOD': 'HEAD',
384       'PATH_INFO': '/',
385    }
386    m = webob_exc._HTTPMove()
387    m.add_slash = True
388    assert  m( environ, start_response ) == []
389
390def test_HTTPMove_call_query_string():
391    def start_response(status, headers, exc_info=None):
392        pass
393    environ = {
394       'wsgi.url_scheme': 'HTTP',
395       'SERVER_NAME': 'localhost',
396       'SERVER_PORT': '80',
397       'REQUEST_METHOD': 'HEAD'
398    }
399    m = webob_exc._HTTPMove()
400    m.add_slash = True
401    environ[ 'QUERY_STRING' ] = 'querystring'
402    environ['PATH_INFO'] = '/'
403    assert  m( environ, start_response ) == []
404
405def test_HTTPFound_unused_environ_variable():
406    class Crashy(object):
407        def __str__(self):
408            raise Exception('I crashed!')
409
410    def start_response(status, headers, exc_info=None):
411        pass
412    environ = {
413       'wsgi.url_scheme': 'HTTP',
414       'SERVER_NAME': 'localhost',
415       'SERVER_PORT': '80',
416       'REQUEST_METHOD': 'GET',
417       'PATH_INFO': '/',
418       'HTTP_ACCEPT': 'text/html',
419       'crashy': Crashy()
420    }
421
422    m = webob_exc._HTTPMove(location='http://www.example.com')
423    assert m(environ, start_response) == [
424        b'<html>\n'
425        b' <head>\n'
426        b'  <title>500 Internal Server Error</title>\n'
427        b' </head>\n'
428        b' <body>\n'
429        b'  <h1>500 Internal Server Error</h1>\n'
430        b'  The resource has been moved to '
431        b'<a href="http://www.example.com">'
432        b'http://www.example.com</a>;\n'
433        b'you should be redirected automatically.\n'
434        b'\n\n'
435        b' </body>\n'
436        b'</html>']
437
438def test_HTTPExceptionMiddleware_ok():
439    def app(environ, start_response):
440        return '123'
441    application = app
442    m = webob_exc.HTTPExceptionMiddleware(application)
443    environ = {}
444    start_response = None
445    res = m(environ, start_response)
446    assert res == '123'
447
448def test_HTTPExceptionMiddleware_exception():
449    def wsgi_response(environ, start_response):
450        return '123'
451    def app(environ, start_response):
452        raise webob_exc.HTTPException(None, wsgi_response)
453    application = app
454    m = webob_exc.HTTPExceptionMiddleware(application)
455    environ = {}
456    start_response = None
457    res = m(environ, start_response)
458    assert res == '123'
459
460def test_HTTPExceptionMiddleware_exception_exc_info_none():
461    class DummySys:
462        def exc_info(self):
463            return None
464    def wsgi_response(environ, start_response):
465        return start_response('200 OK', [], exc_info=None)
466    def app(environ, start_response):
467        raise webob_exc.HTTPException(None, wsgi_response)
468    application = app
469    m = webob_exc.HTTPExceptionMiddleware(application)
470    environ = {}
471    def start_response(status, headers, exc_info):
472        pass
473    try:
474        old_sys = webob_exc.sys
475        sys = DummySys()
476        res = m(environ, start_response)
477        assert res is None
478    finally:
479        webob_exc.sys = old_sys
480
481def test_status_map_is_deterministic():
482    for code, cls in (
483        (200, webob_exc.HTTPOk),
484        (201, webob_exc.HTTPCreated),
485        (202, webob_exc.HTTPAccepted),
486        (203, webob_exc.HTTPNonAuthoritativeInformation),
487        (204, webob_exc.HTTPNoContent),
488        (205, webob_exc.HTTPResetContent),
489        (206, webob_exc.HTTPPartialContent),
490        (300, webob_exc.HTTPMultipleChoices),
491        (301, webob_exc.HTTPMovedPermanently),
492        (302, webob_exc.HTTPFound),
493        (303, webob_exc.HTTPSeeOther),
494        (304, webob_exc.HTTPNotModified),
495        (305, webob_exc.HTTPUseProxy),
496        (307, webob_exc.HTTPTemporaryRedirect),
497        (308, webob_exc.HTTPPermanentRedirect),
498        (400, webob_exc.HTTPBadRequest),
499        (401, webob_exc.HTTPUnauthorized),
500        (402, webob_exc.HTTPPaymentRequired),
501        (403, webob_exc.HTTPForbidden),
502        (404, webob_exc.HTTPNotFound),
503        (405, webob_exc.HTTPMethodNotAllowed),
504        (406, webob_exc.HTTPNotAcceptable),
505        (407, webob_exc.HTTPProxyAuthenticationRequired),
506        (408, webob_exc.HTTPRequestTimeout),
507        (409, webob_exc.HTTPConflict),
508        (410, webob_exc.HTTPGone),
509        (411, webob_exc.HTTPLengthRequired),
510        (412, webob_exc.HTTPPreconditionFailed),
511        (413, webob_exc.HTTPRequestEntityTooLarge),
512        (414, webob_exc.HTTPRequestURITooLong),
513        (415, webob_exc.HTTPUnsupportedMediaType),
514        (416, webob_exc.HTTPRequestRangeNotSatisfiable),
515        (417, webob_exc.HTTPExpectationFailed),
516        (422, webob_exc.HTTPUnprocessableEntity),
517        (423, webob_exc.HTTPLocked),
518        (424, webob_exc.HTTPFailedDependency),
519        (428, webob_exc.HTTPPreconditionRequired),
520        (429, webob_exc.HTTPTooManyRequests),
521        (431, webob_exc.HTTPRequestHeaderFieldsTooLarge),
522        (451, webob_exc.HTTPUnavailableForLegalReasons),
523        (500, webob_exc.HTTPInternalServerError),
524        (501, webob_exc.HTTPNotImplemented),
525        (502, webob_exc.HTTPBadGateway),
526        (503, webob_exc.HTTPServiceUnavailable),
527        (504, webob_exc.HTTPGatewayTimeout),
528        (505, webob_exc.HTTPVersionNotSupported),
529        (511, webob_exc.HTTPNetworkAuthenticationRequired),
530    ):
531        assert webob_exc.status_map[code] == cls
532