Skip to content

Commit cd177a5

Browse files
committed
add helper class to analyze response.location
1 parent 2bce698 commit cd177a5

4 files changed

Lines changed: 199 additions & 0 deletions

File tree

tests/test_response.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,15 @@ def test_content_dezips(self):
416416
resp = app.get('/')
417417
self.assertEqual(resp.body, b'test')
418418

419+
def test_location(self):
420+
app = webtest.TestApp(debug_app)
421+
res = app.post('/')
422+
res.location = 'http://pylons.org'
423+
res.content_location = 'https://example.org/a/b/c'
424+
self.assertTrue(res.location.match('http://'))
425+
self.assertTrue(res.content_location.match('https://example.org/a/b/c'))
426+
self.assertTrue(res.url.match('http://localhost/'))
427+
419428

420429
class TestFollow(unittest.TestCase):
421430

tests/test_utils.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,69 @@ def test_json_method_doc(self):
117117

118118
def test_json_method_name(self):
119119
self.assertEqual(self.mock.foo_json.__name__, 'foo_json')
120+
121+
class URLTest(unittest.TestCase):
122+
def test_attributes(self):
123+
url = utils.URL('https://example.com/a/b/c?foo=bar&foo=barbar&bar=foo')
124+
self.assertEqual(str(url), 'https://example.com/a/b/c?foo=bar&foo=barbar&bar=foo')
125+
self.assertEqual(repr(url), "<URL 'https://example.com/a/b/c?foo=bar&foo=barbar&bar=foo'>")
126+
self.assertEqual(url.scheme, 'https')
127+
self.assertEqual(url.domain, 'example.com')
128+
# webob.Response is wrong in environ_from_url(), here it should be example.com
129+
self.assertEqual(url.host, 'example.com:443')
130+
# so netloc was implemented to replace it
131+
self.assertEqual(url.netloc, 'example.com')
132+
self.assertEqual(url.host_url, 'https://example.com')
133+
self.assertEqual(url.path, '/a/b/c')
134+
self.assertEqual(url.path_url, 'https://example.com/a/b/c')
135+
self.assertEqual(url.path_qs, '/a/b/c?foo=bar&foo=barbar&bar=foo')
136+
self.assertEqual(list(url.params.items()), [('foo', 'bar'), ('foo', 'barbar'), ('bar', 'foo')])
137+
self.assertEqual(url.params['foo'], 'barbar')
138+
self.assertEqual(url.query_string, 'foo=bar&foo=barbar&bar=foo')
139+
140+
def test_join(self):
141+
url = utils.URL('https://example.com/a/b/c?foo=bar&foo=barbar&bar=foo')
142+
new_url = url.join('x/y?foofo=bar')
143+
144+
self.assertEqual(new_url, 'https://example.com/a/b/x/y?foofo=bar')
145+
146+
def test_match(self):
147+
url = utils.URL('https://example.com/a/b/c?foo=bar&foo=barbar&bar=foo')
148+
self.assertTrue(url.match('https://'))
149+
self.assertFalse(url.match('http://'))
150+
self.assertEqual(repr(url.match('http://')), 'Error -- scheme differs https != http')
151+
self.assertTrue(url.match('//example.com'))
152+
self.assertFalse(url.match('//a.example.com'))
153+
self.assertEqual(repr(url.match('//a.example.com')), 'Error -- netloc differs example.com != a.example.com')
154+
self.assertTrue(url.match('https://example.com'))
155+
156+
self.assertTrue(url.match('/a/b/c'))
157+
self.assertFalse(url.match('/a/b/c/'))
158+
self.assertEqual(repr(url.match('/a/b/c/')), 'Error -- path differs /a/b/c != /a/b/c/')
159+
self.assertFalse(url.match('/x'))
160+
161+
self.assertTrue(url.match('https://example.com/a/b/c'))
162+
self.assertFalse(url.match('https://example.com/a/b/c/'))
163+
self.assertFalse(url.match('https://example.com/x'))
164+
165+
self.assertTrue(url.match(url))
166+
self.assertTrue(url.match('https://example.com/a/b/c?foo=bar&foo=barbar&bar=foo'))
167+
self.assertTrue(url.match('https://example.com/a/b/c?foo=bar'))
168+
self.assertTrue(url.match('https://example.com/a/b/c?foo=barbar'))
169+
self.assertFalse(url.match('https://example.com/a/b/c?foo=bar', strict=True))
170+
self.assertEqual(repr(url.match('https://example.com/a/b/c?foo=bar&bar=foo', strict=True)), 'Error -- ?foo=barbar was not expected.')
171+
172+
# multiple errors
173+
self.assertEqual(repr(url.match('https://example.com/a/b/c?foo=bar', strict=True)), 'Error -- ?foo=barbar was not expected. ?bar=foo was not expected.')
174+
175+
self.assertTrue(url.match('?!foobar'))
176+
self.assertTrue(url.match('?bar=?'))
177+
self.assertTrue(url.match('?bar=?&foo=*', strict=True))
178+
self.assertEqual(repr(url.match('?!foo')), 'Error -- foo should be absent, but ?foo=bar&foo=barbar found.')
179+
self.assertEqual(repr(url.match('?foo=?')), 'Error -- foo should have only one value but ?foo=bar&foo=barbar found.')
180+
self.assertEqual(repr(url.match('?foobar=?')), 'Error -- foobar should have only one value but is absent.')
181+
182+
self.assertEqual(repr(url.match('?foo=barfoo')), 'Error -- foo should have value \'barfoo\' but ?foo=bar&foo=barbar found.')
183+
self.assertEqual(repr(url.match('?foobar=barfoo')), 'Error -- foobar should have value \'barfoo\' but is absent.')
184+
185+

webtest/response.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,3 +545,19 @@ def showbrowser(self):
545545
else:
546546
url = 'file://' + name
547547
webbrowser.open_new(url)
548+
549+
@webob.Response.location.getter
550+
def location(self):
551+
value = webob.Response.location.fget(self)
552+
if value is not None:
553+
return utils.URL(value)
554+
555+
@webob.Response.content_location.getter
556+
def content_location(self):
557+
value = webob.Response.content_location.fget(self)
558+
if value is not None:
559+
return utils.URL(value)
560+
561+
@property
562+
def url(self):
563+
return utils.URL(self.request.url)

webtest/utils.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import functools
12
import re
23
from json import dumps
4+
import urllib.parse
5+
6+
import webob
37

48
from webtest.compat import urlencode
59

@@ -167,3 +171,107 @@ def getheaders(self, header):
167171
def get_all(self, headers, default): # NOQA
168172
# This is undocumented method that Python 3 cookielib uses
169173
return self._response.headers.getall(headers)
174+
175+
176+
class Error:
177+
def __init__(self, message):
178+
self.message = message
179+
180+
def __str__(self):
181+
return self.message
182+
183+
def __bool__(self):
184+
return False
185+
186+
def __repr__(self):
187+
return f'Error -- {self.message}'
188+
189+
190+
class URL(str):
191+
@functools.cached_property
192+
def request(self):
193+
parsed = urllib.parse.urlparse(self)
194+
request = webob.Request.blank(self)
195+
# XXX: webob force the http: scheme if parsed.scheme is absent
196+
# XXX: webob does not understand //{host}/ it interprets it as if //{host}/ is a path
197+
if not parsed.scheme:
198+
request.environ['PATH_INFO'] = parsed.path
199+
request.environ['HTTP_HOST'] = parsed.netloc
200+
request.environ['wsgi.url_scheme'] = None
201+
return request
202+
203+
def __getattr__(self, attr):
204+
return getattr(self.request, attr)
205+
206+
def join(self, other):
207+
return URL(urllib.parse.urljoin(str(self), str(other) if other else ''))
208+
209+
@property
210+
def netloc(self):
211+
return urllib.parse.urlparse(self).netloc
212+
213+
@property
214+
def host_url(self):
215+
return URL(self.request.host_url)
216+
217+
@property
218+
def path(self):
219+
return URL(self.request.path)
220+
221+
@property
222+
def path_url(self):
223+
return URL(self.request.path_url)
224+
225+
@property
226+
def path_qs(self):
227+
return URL(self.request.path_qs)
228+
229+
def __repr__(self):
230+
return f'<{self.__class__.__name__} {str(self)!r}>'
231+
232+
def match(self, other, strict=False):
233+
if not isinstance(other, URL):
234+
other = URL(other)
235+
errors = []
236+
if other.scheme and other.scheme != self.scheme:
237+
errors.append(f'scheme differs {self.scheme} != {other.scheme}')
238+
if other.netloc and self.netloc != other.netloc:
239+
errors.append(f'netloc differs {self.netloc} != {other.netloc}')
240+
if other.path and other.path != '/*/' and other.path != self.path:
241+
errors.append(f'path differs {self.path} != {other.path}')
242+
if other.params or strict:
243+
expected = set()
244+
for key, value in other.params.items():
245+
# &!key forbids key in query string
246+
if key.startswith('!'):
247+
if key[1:] in self.params:
248+
qs = urllib.parse.urlencode([(key[1:], v) for v in self.params.getall(key[1:])])
249+
errors.append(f'{key[1:]} should be absent, but ?{qs} found.')
250+
elif value == '?':
251+
values = self.params.getall(key)
252+
if len(values) == 0 or (len(values) == 1 and not values[0]):
253+
errors.append(f'{key} should have only one value but is absent.')
254+
elif len(values) > 1:
255+
qs = urllib.parse.urlencode([(key, v) for v in self.params.getall(key)])
256+
errors.append(f'{key} should have only one value but ?{qs} found.')
257+
else:
258+
expected.add((key, self.params[key]))
259+
elif value == '*':
260+
for v in self.params.getall(key):
261+
expected.add((key, v))
262+
else:
263+
if key not in self.params:
264+
errors.append(f'{key} should have value {value!r} but is absent.')
265+
elif value not in self.params.getall(key):
266+
qs = urllib.parse.urlencode([(key, v) for v in self.params.getall(key)])
267+
errors.append(f'{key} should have value {value!r} but ?{qs} found.')
268+
else:
269+
expected.add((key, value))
270+
271+
if strict:
272+
for key, value in self.params.items():
273+
if (key, value) not in expected:
274+
errors.append(f'?{key}={value} was not expected.')
275+
if errors:
276+
return Error(' '.join(errors))
277+
return True

0 commit comments

Comments
 (0)