1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 import urllib
22 from urlparse import urlparse, urlunparse, urlsplit
23 import sys
24 import os
25 import re
26 import mimetypes
27 import warnings
28 try:
29 from cStringIO import StringIO
30 except ImportError:
31 from StringIO import StringIO
32
33 from django.conf import settings
34 from django.contrib.auth import authenticate, login
35 from django.core.handlers.base import BaseHandler
36 from django.core.handlers.wsgi import WSGIRequest
37 from django.core.signals import got_request_exception
38 from django.http import SimpleCookie, HttpRequest, QueryDict
39 from django.template import TemplateDoesNotExist
40 from django.test import signals
41 from django.utils.functional import curry
42 from django.utils.encoding import smart_str
43 from django.utils.http import urlencode
44 from django.utils.importlib import import_module
45 from django.utils.itercompat import is_iterable
46 from django.db import transaction, close_connection
47 from django.test.utils import ContextList
48
49 from omeroweb.webgateway import views as webgateway_views
50
51 __all__ = ('Client', 'RequestFactory', 'encode_file', 'encode_multipart')
52
53
54 BOUNDARY = 'BoUnDaRyStRiNg'
55 MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY
56 CONTENT_TYPE_RE = re.compile('.*; charset=([\w\d-]+);?')
57
58 -def fakeRequest (method, path="/", params={}, **kwargs):
59 def bogus_request(self, **request):
60 """
61 Usage:
62 rf = RequestFactory()
63 get_request = rf.get('/hello/')
64 post_request = rf.post('/submit/', {'foo': 'bar'})
65 """
66 if not method.lower() in ('post', 'get'):
67 raise AttributeError("Method must be 'get' or 'post'")
68 if not isinstance(params, dict):
69 raise AttributeError("Params must be a dictionary")
70
71 rf = RequestFactory()
72 r = getattr(rf, method.lower())(path, params)
73 if 'django.contrib.sessions' in settings.INSTALLED_APPS:
74 engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
75 r.session = engine.SessionStore()
76 return r
77 Client.bogus_request = bogus_request
78 c = Client()
79 return c.bogus_request(**kwargs)
80
81
83 """
84 A wrapper around StringIO that restricts what can be read since data from
85 the network can't be seeked and cannot be read outside of its content
86 length. This makes sure that views can't do anything under the test client
87 that wouldn't work in Real Life.
88 """
92
93 - def read(self, num_bytes=None):
94 if num_bytes is None:
95 num_bytes = self.__len or 1
96 assert self.__len >= num_bytes, "Cannot read more than the available bytes from the HTTP incoming data."
97 content = self.__content.read(num_bytes)
98 self.__len -= num_bytes
99 return content
100
101
103 """
104 A HTTP Handler that can be used for testing purposes.
105 Uses the WSGI interface to compose requests, but returns
106 the raw HttpResponse object
107 """
108 - def __init__(self, enforce_csrf_checks=True, *args, **kwargs):
109 self.enforce_csrf_checks = enforce_csrf_checks
110 super(ClientHandler, self).__init__(*args, **kwargs)
111
113 from django.conf import settings
114 from django.core import signals
115
116
117
118 if self._request_middleware is None:
119 self.load_middleware()
120
121 signals.request_started.send(sender=self.__class__)
122 try:
123 request = WSGIRequest(environ)
124
125
126
127
128 request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
129 response = self.get_response(request)
130 finally:
131 signals.request_finished.disconnect(close_connection)
132 signals.request_finished.send(sender=self.__class__)
133 signals.request_finished.connect(close_connection)
134
135 return response
136
138 """
139 Stores templates and contexts that are rendered.
140 """
141 store.setdefault('templates', []).append(template)
142 store.setdefault('context', ContextList()).append(context)
143
145 """
146 Encodes multipart POST data from a dictionary of form values.
147
148 The key will be used as the form data name; the value will be transmitted
149 as content. If the value is a file, the contents of the file will be sent
150 as an application/octet-stream; otherwise, str(value) will be sent.
151 """
152 lines = []
153 to_str = lambda s: smart_str(s, settings.DEFAULT_CHARSET)
154
155
156 is_file = lambda thing: hasattr(thing, "read") and callable(thing.read)
157
158
159
160
161 for (key, value) in data.items():
162 if is_file(value):
163 lines.extend(encode_file(boundary, key, value))
164 elif not isinstance(value, basestring) and is_iterable(value):
165 for item in value:
166 if is_file(item):
167 lines.extend(encode_file(boundary, key, item))
168 else:
169 lines.extend([
170 '--' + boundary,
171 'Content-Disposition: form-data; name="%s"' % to_str(key),
172 '',
173 to_str(item)
174 ])
175 else:
176 lines.extend([
177 '--' + boundary,
178 'Content-Disposition: form-data; name="%s"' % to_str(key),
179 '',
180 to_str(value)
181 ])
182
183 lines.extend([
184 '--' + boundary + '--',
185 '',
186 ])
187 return '\r\n'.join(lines)
188
190 to_str = lambda s: smart_str(s, settings.DEFAULT_CHARSET)
191 content_type = mimetypes.guess_type(file.name)[0]
192 if content_type is None:
193 content_type = 'application/octet-stream'
194 return [
195 '--' + boundary,
196 'Content-Disposition: form-data; name="%s"; filename="%s"' \
197 % (to_str(key), to_str(os.path.basename(file.name))),
198 'Content-Type: %s' % content_type,
199 '',
200 file.read()
201 ]
202
203
204
206 """
207 Class that lets you create mock Request objects for use in testing.
208
209 Usage:
210
211 rf = RequestFactory()
212 get_request = rf.get('/hello/')
213 post_request = rf.post('/submit/', {'foo': 'bar'})
214
215 Once you have a request object you can pass it to any view function,
216 just as if that view had been hooked up using a URLconf.
217 """
219 self.defaults = defaults
220 self.cookies = SimpleCookie()
221 self.errors = StringIO()
222
224 """
225 The base environment for a request.
226 """
227 environ = {
228 'HTTP_COOKIE': self.cookies.output(header='', sep='; '),
229 'PATH_INFO': '/',
230 'QUERY_STRING': '',
231 'REMOTE_ADDR': '127.0.0.1',
232 'REQUEST_METHOD': 'GET',
233 'SCRIPT_NAME': '',
234 'SERVER_NAME': 'testserver',
235 'SERVER_PORT': '80',
236 'SERVER_PROTOCOL': 'HTTP/1.1',
237 'wsgi.version': (1,0),
238 'wsgi.url_scheme': 'http',
239 'wsgi.errors': self.errors,
240 'wsgi.multiprocess': True,
241 'wsgi.multithread': False,
242 'wsgi.run_once': False,
243 }
244 environ.update(self.defaults)
245 environ.update(request)
246 return environ
247
249 "Construct a generic request object with session."
250 return WSGIRequest(self._base_environ(**request))
251
252 - def get(self, path, data={}, **extra):
253 "Construct a GET request"
254
255 parsed = urlparse(path)
256 r = {
257 'CONTENT_TYPE': 'text/html; charset=utf-8',
258 'PATH_INFO': urllib.unquote(parsed[2]),
259 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
260 'REQUEST_METHOD': 'GET',
261 'wsgi.input': FakePayload('')
262 }
263 r.update(extra)
264 return self.request(**r)
265
266 - def post(self, path, data={}, content_type=MULTIPART_CONTENT,
267 **extra):
268 "Construct a POST request."
269
270 if content_type is MULTIPART_CONTENT:
271 post_data = encode_multipart(BOUNDARY, data)
272 else:
273
274 match = CONTENT_TYPE_RE.match(content_type)
275 if match:
276 charset = match.group(1)
277 else:
278 charset = settings.DEFAULT_CHARSET
279 post_data = smart_str(data, encoding=charset)
280
281 parsed = urlparse(path)
282 r = {
283 'CONTENT_LENGTH': len(post_data),
284 'CONTENT_TYPE': content_type,
285 'PATH_INFO': urllib.unquote(parsed[2]),
286 'QUERY_STRING': parsed[4],
287 'REQUEST_METHOD': 'POST',
288 'wsgi.input': FakePayload(post_data),
289 }
290 r.update(extra)
291 return self.request(**r)
292
293 - def head(self, path, data={}, **extra):
294 "Construct a HEAD request."
295
296 parsed = urlparse(path)
297 r = {
298 'CONTENT_TYPE': 'text/html; charset=utf-8',
299 'PATH_INFO': urllib.unquote(parsed[2]),
300 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
301 'REQUEST_METHOD': 'HEAD',
302 'wsgi.input': FakePayload('')
303 }
304 r.update(extra)
305 return self.request(**r)
306
307 - def options(self, path, data={}, **extra):
308 "Constrict an OPTIONS request"
309
310 parsed = urlparse(path)
311 r = {
312 'PATH_INFO': urllib.unquote(parsed[2]),
313 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
314 'REQUEST_METHOD': 'OPTIONS',
315 'wsgi.input': FakePayload('')
316 }
317 r.update(extra)
318 return self.request(**r)
319
322 "Construct a PUT request."
323
324 if content_type is MULTIPART_CONTENT:
325 post_data = encode_multipart(BOUNDARY, data)
326 else:
327 post_data = data
328
329
330
331 query_string = None
332 if not isinstance(data, basestring):
333 query_string = urlencode(data, doseq=True)
334
335 parsed = urlparse(path)
336 r = {
337 'CONTENT_LENGTH': len(post_data),
338 'CONTENT_TYPE': content_type,
339 'PATH_INFO': urllib.unquote(parsed[2]),
340 'QUERY_STRING': query_string or parsed[4],
341 'REQUEST_METHOD': 'PUT',
342 'wsgi.input': FakePayload(post_data),
343 }
344 r.update(extra)
345 return self.request(**r)
346
347 - def delete(self, path, data={}, **extra):
348 "Construct a DELETE request."
349
350 parsed = urlparse(path)
351 r = {
352 'PATH_INFO': urllib.unquote(parsed[2]),
353 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
354 'REQUEST_METHOD': 'DELETE',
355 'wsgi.input': FakePayload('')
356 }
357 r.update(extra)
358 return self.request(**r)
359
360
362 """
363 A class that can act as a client for testing purposes.
364
365 It allows the user to compose GET and POST requests, and
366 obtain the response that the server gave to those requests.
367 The server Response objects are annotated with the details
368 of the contexts and templates that were rendered during the
369 process of serving the request.
370
371 Client objects are stateful - they will retain cookie (and
372 thus session) details for the lifetime of the Client instance.
373
374 This is not intended as a replacement for Twill/Selenium or
375 the like - it is here to allow testing against the
376 contexts and templates produced by a view, rather than the
377 HTML rendered to the end-user.
378 """
379 - def __init__(self, enforce_csrf_checks=False, **defaults):
383
385 """
386 Stores exceptions when they are generated by a view.
387 """
388 self.exc_info = sys.exc_info()
389
391 """
392 Obtains the current session variables.
393 """
394 if 'django.contrib.sessions' in settings.INSTALLED_APPS:
395 engine = import_module(settings.SESSION_ENGINE)
396 cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
397 if cookie:
398 return engine.SessionStore(cookie.value)
399 return {}
400 session = property(_session)
401
402
404 """
405 The master request method. Composes the environment dictionary
406 and passes to the handler, returning the result of the handler.
407 Assumes defaults for the query environment, which can be overridden
408 using the arguments to the request.
409 """
410 environ = self._base_environ(**request)
411
412
413
414 data = {}
415 on_template_render = curry(store_rendered_templates, data)
416 signals.template_rendered.connect(on_template_render, dispatch_uid="template-render")
417
418 got_request_exception.connect(self.store_exc_info, dispatch_uid="request-exception")
419 try:
420
421 try:
422 response = self.handler(environ)
423 except TemplateDoesNotExist, e:
424
425
426
427
428
429
430 if e.args != ('500.html',):
431 raise
432
433
434
435
436
437 if self.exc_info:
438 exc_info = self.exc_info
439 self.exc_info = None
440 raise exc_info[1], None, exc_info[2]
441
442
443 response.client = self
444 response.request = request
445
446
447 response.templates = data.get("templates", [])
448 response.context = data.get("context")
449
450
451
452
453 if response.context and len(response.context) == 1:
454 response.context = response.context[0]
455
456
457 def _get_template(self):
458 warnings.warn("response.template is deprecated; use response.templates instead (which is always a list)",
459 PendingDeprecationWarning, stacklevel=2)
460 if not self.templates:
461 return None
462 elif len(self.templates) == 1:
463 return self.templates[0]
464 return self.templates
465 response.__class__.template = property(_get_template)
466
467
468 if response.cookies:
469 self.cookies.update(response.cookies)
470
471 return response
472 finally:
473 signals.template_rendered.disconnect(dispatch_uid="template-render")
474 got_request_exception.disconnect(dispatch_uid="request-exception")
475
476 - def get(self, path, data={}, follow=False, **extra):
477 """
478 Requests a response from the server using GET.
479 """
480 response = super(Client, self).get(path, data=data, **extra)
481 if follow:
482 response = self._handle_redirects(response, **extra)
483 return response
484
485 - def post(self, path, data={}, content_type=MULTIPART_CONTENT,
486 follow=False, **extra):
487 """
488 Requests a response from the server using POST.
489 """
490 response = super(Client, self).post(path, data=data, content_type=content_type, **extra)
491 if follow:
492 response = self._handle_redirects(response, **extra)
493 return response
494
495 - def head(self, path, data={}, follow=False, **extra):
496 """
497 Request a response from the server using HEAD.
498 """
499 response = super(Client, self).head(path, data=data, **extra)
500 if follow:
501 response = self._handle_redirects(response, **extra)
502 return response
503
504 - def options(self, path, data={}, follow=False, **extra):
505 """
506 Request a response from the server using OPTIONS.
507 """
508 response = super(Client, self).options(path, data=data, **extra)
509 if follow:
510 response = self._handle_redirects(response, **extra)
511 return response
512
515 """
516 Send a resource to the server using PUT.
517 """
518 response = super(Client, self).put(path, data=data, content_type=content_type, **extra)
519 if follow:
520 response = self._handle_redirects(response, **extra)
521 return response
522
523 - def delete(self, path, data={}, follow=False, **extra):
524 """
525 Send a DELETE request to the server.
526 """
527 response = super(Client, self).delete(path, data=data, **extra)
528 if follow:
529 response = self._handle_redirects(response, **extra)
530 return response
531
532 - def login(self, login,password, server_id=1, secure=True):
533 """
534 Sets the Factory to appear as if it has successfully logged into a site.
535
536 Returns True if login is possible; False if the provided credentials
537 are incorrect, or the user is inactive, or if the sessions framework is
538 not available.
539 """
540 engine = import_module(settings.SESSION_ENGINE)
541
542 params = {
543 'username': login,
544 'password': password,
545 'server':server_id,
546 'ssl':'on'
547 }
548 request = fakeRequest(method="post", path="/webadmin/login/", params=params)
549
550 if self.session:
551 request.session = self.session
552 else:
553 request.session = engine.SessionStore()
554
555
556 session_cookie = settings.SESSION_COOKIE_NAME
557 self.cookies[session_cookie] = request.session.session_key
558 cookie_data = {
559 'max-age': None,
560 'path': '/',
561 'domain': settings.SESSION_COOKIE_DOMAIN,
562 'secure': settings.SESSION_COOKIE_SECURE or None,
563 'expires': None,
564 }
565 self.cookies[session_cookie].update(cookie_data)
566
567 blitz = settings.SERVER_LIST.get(pk=request.REQUEST.get('server'))
568 request.session['server'] = blitz.id
569 request.session['host'] = blitz.host
570 request.session['port'] = blitz.port
571 request.session['username'] = request.REQUEST.get('username').encode('utf-8').strip()
572 request.session['password'] = request.REQUEST.get('password').encode('utf-8').strip()
573 request.session['ssl'] = (True, False)[request.REQUEST.get('ssl') is None]
574
575 conn = webgateway_views.getBlitzConnection(request, useragent="TEST.webadmin")
576 if conn is not None and conn.isConnected() and conn.keepAlive():
577 request.session.save()
578 return True
579 else:
580 webgateway_views._session_logout(request, request.session.get('server'))
581 return False
582
584 """
585 Removes the authenticated user's cookies and session object.
586
587 Causes the authenticated user to be logged out.
588 """
589 request = fakeRequest(method="get", path="/webadmin/logout/")
590 webgateway_views._session_logout(request, request.session.get('server'))
591
592 session = import_module(settings.SESSION_ENGINE).SessionStore()
593 session_cookie = self.cookies.get(settings.SESSION_COOKIE_NAME)
594 if session_cookie:
595 session.delete(session_key=session_cookie.value)
596 self.cookies = SimpleCookie()
597
599 "Follows any redirects by requesting responses from the server using GET."
600
601 response.redirect_chain = []
602 while response.status_code in (301, 302, 303, 307):
603 url = response['Location']
604 scheme, netloc, path, query, fragment = urlsplit(url)
605
606 redirect_chain = response.redirect_chain
607 redirect_chain.append((url, response.status_code))
608
609 if scheme:
610 extra['wsgi.url_scheme'] = scheme
611
612
613
614
615
616 response = self.get(path, QueryDict(query), follow=False, **extra)
617 response.redirect_chain = redirect_chain
618
619
620 if response.redirect_chain[-1] in response.redirect_chain[0:-1]:
621 break
622 return response
623