Skip to content

Commit cd0e609

Browse files
committed
add stray test suite for request parameters parsing
1 parent 05dccc3 commit cd0e609

1 file changed

Lines changed: 364 additions & 0 deletions

File tree

tests/test_request_params.py

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
# -*- coding: utf-8 -*-
2+
import json
3+
4+
import pytest
5+
from webtest import TestApp
6+
7+
import tg
8+
from tg import MinimalApplicationConfigurator, TGController, expose
9+
from tg.configuration import config
10+
from tg.controllers import RestController
11+
from tg.decorators import before_call, before_validate, decode_params, paginate, validate
12+
from tg.support.converters import asint
13+
from tg.validation import Convert
14+
15+
IntValidator = Convert(asint, msg="Please enter an integer value")
16+
17+
18+
def _copy_x_to_copied_x(remainder, params):
19+
params.setdefault('copied_x', params['x'])
20+
21+
22+
class TestRequestParameterEquivalence(object):
23+
@pytest.mark.parametrize('source', ['query', 'positional', 'multipart', 'json'])
24+
def test_required_action_argument_is_satisfied_by_equivalent_parameter_sources(
25+
self, app, source
26+
):
27+
resp = _request_with_x(app, '/echo', source)
28+
29+
assert resp.json_body == {'x': '5'}
30+
31+
@pytest.mark.parametrize('source', ['query', 'positional', 'multipart', 'json'])
32+
def test_validation_sees_equivalent_parameter_sources(self, app, source):
33+
resp = _request_with_x(app, '/validated', source)
34+
35+
assert resp.json_body == {'x': 5, 'type': 'int'}
36+
37+
def test_validation_accepts_json_native_types(self, app):
38+
resp = app.post_json('/validated', {'x': 5})
39+
40+
assert resp.json_body == {'x': 5, 'type': 'int'}
41+
42+
def test_json_content_type_media_type_is_case_insensitive(self, app):
43+
resp = app.post(
44+
'/echo',
45+
params=json.dumps({'x': '5'}),
46+
content_type='Application/JSON',
47+
)
48+
49+
assert resp.json_body == {'x': '5'}
50+
51+
@pytest.mark.parametrize('source', ['query', 'multipart', 'json'])
52+
def test_before_validate_hook_sees_equivalent_parameter_sources(self, app, source):
53+
resp = _request_with_x(app, '/before_validate_hooked', source)
54+
55+
assert resp.json_body == {'x': '5', 'copied_x': '5'}
56+
57+
@pytest.mark.parametrize('source', ['query', 'multipart', 'json'])
58+
def test_before_call_hook_sees_equivalent_parameter_sources(self, app, source):
59+
resp = _request_with_x(app, '/before_call_hooked', source)
60+
61+
assert resp.json_body == {'x': '5', 'copied_x': '5'}
62+
63+
@pytest.mark.parametrize('source', ['query', 'positional', 'multipart', 'json'])
64+
def test_controller_before_hook_sees_equivalent_parameter_sources(
65+
self, app, source
66+
):
67+
resp = _request_with_x(app, '/controller_before_hooked', source)
68+
69+
assert resp.json_body == {'x': '5'}
70+
71+
@pytest.mark.parametrize('source', ['query', 'multipart', 'json'])
72+
def test_paginate_sees_equivalent_parameter_sources(self, app, source):
73+
resp = _request_with_page(app, source)
74+
75+
assert resp.json_body == {'page': 3}
76+
77+
@pytest.mark.parametrize('source', ['query', 'multipart', 'json'])
78+
def test_rest_method_override_uses_equivalent_parameter_sources(
79+
self, rest_app, source
80+
):
81+
resp = _request_with_method_override(rest_app, source)
82+
83+
assert resp.json_body == {'action': 'put', 'x': '5'}
84+
85+
def test_multipart_body_combines_with_query_params_on_collision(self, app):
86+
resp = app.post(
87+
'/echo?x=query',
88+
params={'x': 'multipart'},
89+
upload_files=[_empty_file()],
90+
)
91+
92+
assert resp.json_body == {'x': ['query', 'multipart']}
93+
94+
def test_json_body_overrides_query_params_on_collision(self, app):
95+
resp = app.post_json('/echo?x=query', {'x': 'json'})
96+
97+
assert resp.json_body == {'x': 'json'}
98+
99+
def test_decode_params_decorator_can_coexist_with_json_params(self, app):
100+
resp = app.post_json('/decode_decorated?x=query', {'x': 'json'})
101+
102+
assert resp.json_body == {'x': 'json'}
103+
104+
def test_empty_json_body_is_bad_request(self, app):
105+
app.post('/optional', params='', content_type='application/json', status=400)
106+
107+
def test_invalid_json_body_is_bad_request(self, app):
108+
app.post('/optional', params='{', content_type='application/json', status=400)
109+
110+
@pytest.mark.parametrize('json_body', [
111+
['not', 'an', 'object'],
112+
[['x', '5']],
113+
])
114+
def test_non_object_json_body_is_bad_request(self, app, json_body):
115+
app.post(
116+
'/optional',
117+
params=json.dumps(json_body),
118+
content_type='application/json',
119+
status=400,
120+
)
121+
122+
def test_json_params_are_disabled_by_default(self, default_config_app):
123+
resp = default_config_app.post_json('/optional', {'x': '5'})
124+
125+
assert resp.json_body == {'x': 'default'}
126+
127+
def test_json_params_can_be_explicitly_disabled(self, json_disabled_app):
128+
resp = json_disabled_app.post_json('/optional', {'x': '5'})
129+
130+
assert resp.json_body == {'x': 'default'}
131+
132+
def test_json_params_respect_ignore_parameters(self, ignored_params_app):
133+
resp = ignored_params_app.post_json('/filtered_args_params', {
134+
'ignored': 'removed',
135+
'kept': 'kept',
136+
})
137+
138+
assert resp.json_body == {
139+
'x': None,
140+
'y': None,
141+
'ignored': None,
142+
'kept': 'kept',
143+
}
144+
145+
def test_json_params_are_available_in_routing_args(self, routing_args_app):
146+
resp = routing_args_app.post_json('/routing_args', {'x': '5'})
147+
148+
assert resp.json_body == {'x': '5'}
149+
150+
151+
class TestRequestParameterEquivalenceWithUrlArgs(object):
152+
@pytest.mark.parametrize('source', ['query', 'multipart', 'json'])
153+
def test_url_positional_args_win_when_mixed_with_parameter_sources(
154+
self, app, source
155+
):
156+
resp = _request_with_positional_x_and_y(app, source)
157+
158+
assert resp.json_body == {'x': '5', 'y': '6'}
159+
160+
def test_request_args_params_uses_url_arg_over_json_param(self, app):
161+
resp = app.post_json('/filtered_args_params/5', {'x': 'json', 'y': '6'})
162+
163+
assert resp.json_body == {
164+
'x': '5',
165+
'y': '6',
166+
'ignored': None,
167+
'kept': None,
168+
}
169+
170+
171+
class ParameterRestController(RestController):
172+
@expose('json:')
173+
def post(self, x=None, **kw):
174+
return dict(action='post', x=x)
175+
176+
@expose('json:')
177+
def put(self, x=None, **kw):
178+
return dict(action='put', x=x)
179+
180+
181+
class RequestParameterController(TGController):
182+
def _before(self, *args, **kw):
183+
self._before_params = kw.copy()
184+
if args:
185+
self._before_params['x'] = args[0]
186+
187+
@expose()
188+
def index(self):
189+
return 'request params tests'
190+
191+
@expose('json:')
192+
def echo(self, x):
193+
return dict(x=x)
194+
195+
@expose('json:')
196+
@validate(validators=dict(x=IntValidator))
197+
def validated(self, x):
198+
return dict(x=x, type=type(x).__name__)
199+
200+
@expose('json:')
201+
@decode_params()
202+
def decode_decorated(self, x=None):
203+
return dict(x=x)
204+
205+
@expose('json:')
206+
@before_validate(_copy_x_to_copied_x)
207+
def before_validate_hooked(self, x, copied_x=None):
208+
return dict(x=x, copied_x=copied_x)
209+
210+
@expose('json:')
211+
@before_call(_copy_x_to_copied_x)
212+
def before_call_hooked(self, **kw):
213+
return dict(x=kw.get('x'), copied_x=kw.get('copied_x'))
214+
215+
@expose('json:')
216+
def controller_before_hooked(self, x=None):
217+
return dict(x=self._before_params.get('x'))
218+
219+
@expose('json:')
220+
@paginate('items')
221+
def paginated(self, **kw):
222+
return dict(page=tg.request.paginators['items'].paginate_page)
223+
224+
@expose('json:')
225+
def mixed(self, x, y):
226+
return dict(x=x, y=y)
227+
228+
@expose('json:')
229+
def filtered_args_params(self, x=None, y=None, **kw):
230+
params = tg.request.args_params
231+
return dict(
232+
x=params.get('x'),
233+
y=params.get('y'),
234+
ignored=params.get('ignored'),
235+
kept=params.get('kept'),
236+
)
237+
238+
@expose('json:')
239+
def routing_args(self, **kw):
240+
return dict(x=tg.request.dispatch_state.routing_args.get('x'))
241+
242+
@expose('json:')
243+
def optional(self, x='default'):
244+
return dict(x=x)
245+
246+
247+
class RestParameterRootController(TGController):
248+
@expose()
249+
def index(self):
250+
return 'request params rest tests'
251+
252+
rest = ParameterRestController()
253+
254+
255+
@pytest.fixture
256+
def app():
257+
yield _make_app(RequestParameterController())
258+
_reset_global_config()
259+
260+
261+
@pytest.fixture
262+
def rest_app():
263+
yield _make_app(RestParameterRootController())
264+
_reset_global_config()
265+
266+
267+
@pytest.fixture
268+
def default_config_app():
269+
_reset_global_config()
270+
cfg = MinimalApplicationConfigurator()
271+
cfg.update_blueprint({'root_controller': RequestParameterController()})
272+
yield TestApp(cfg.make_wsgi_app({}, {}))
273+
_reset_global_config()
274+
275+
276+
@pytest.fixture
277+
def json_disabled_app():
278+
yield _make_app(RequestParameterController(), decode_json_params=False)
279+
_reset_global_config()
280+
281+
282+
@pytest.fixture
283+
def ignored_params_app():
284+
yield _make_app(RequestParameterController(), ignore_parameters=['ignored'])
285+
_reset_global_config()
286+
287+
288+
@pytest.fixture
289+
def routing_args_app():
290+
yield _make_app(RequestParameterController(), enable_routing_args=True)
291+
_reset_global_config()
292+
293+
294+
def _request_with_x(app, path, source):
295+
if source == 'query':
296+
return app.get('%s?x=5' % path)
297+
if source == 'positional':
298+
return app.get('%s/5' % path)
299+
if source == 'multipart':
300+
return app.post(path, params={'x': '5'}, upload_files=[_empty_file()])
301+
if source == 'json':
302+
return app.post_json(path, {'x': '5'})
303+
raise AssertionError('unknown parameter source %s' % source)
304+
305+
306+
def _request_with_page(app, source):
307+
if source == 'query':
308+
return app.get('/paginated?page=3')
309+
if source == 'multipart':
310+
return app.post('/paginated', params={'page': '3'}, upload_files=[_empty_file()])
311+
if source == 'json':
312+
return app.post_json('/paginated', {'page': '3'})
313+
raise AssertionError('unknown parameter source %s' % source)
314+
315+
316+
def _request_with_method_override(app, source):
317+
if source == 'query':
318+
return app.post('/rest?_method=PUT&x=5')
319+
if source == 'multipart':
320+
return app.post(
321+
'/rest',
322+
params={'_method': 'PUT', 'x': '5'},
323+
upload_files=[_empty_file()],
324+
)
325+
if source == 'json':
326+
return app.post_json('/rest', {'_method': 'PUT', 'x': '5'})
327+
raise AssertionError('unknown parameter source %s' % source)
328+
329+
330+
def _request_with_positional_x_and_y(app, source):
331+
if source == 'query':
332+
return app.get('/mixed/5?x=from_params&y=6')
333+
if source == 'multipart':
334+
return app.post(
335+
'/mixed/5',
336+
params={'x': 'from_params', 'y': '6'},
337+
upload_files=[_empty_file()],
338+
)
339+
if source == 'json':
340+
return app.post_json('/mixed/5', {'x': 'from_params', 'y': '6'})
341+
raise AssertionError('unknown parameter source %s' % source)
342+
343+
344+
def _empty_file():
345+
return ('attachment', 'empty.txt', b'')
346+
347+
348+
def _make_app(root_controller, **config_options):
349+
_reset_global_config()
350+
cfg = MinimalApplicationConfigurator()
351+
app_config = {
352+
'root_controller': root_controller,
353+
'decode_json_params': True,
354+
}
355+
app_config.update(config_options)
356+
cfg.update_blueprint(app_config)
357+
return TestApp(cfg.make_wsgi_app({}, {}))
358+
359+
360+
def _reset_global_config():
361+
try:
362+
config.config_proxy.pop_process_config()
363+
except IndexError:
364+
pass

0 commit comments

Comments
 (0)