From 95d8c0619a961ebf55208bebe5cc686b8247655f Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 12 Nov 2008 11:22:05 +0000 Subject: [PATCH] Fixed #9351 -- Modified the test client to pass on URL encoded parameters to the underlying views. Thanks to sime for the suggestion. git-svn-id: http://code.djangoproject.com/svn/django/trunk@9398 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/test/client.py | 31 ++++++---- docs/topics/testing.txt | 24 ++++++++ .../test_client_regress/models.py | 61 +++++++++++++++++-- .../test_client_regress/urls.py | 1 + .../test_client_regress/views.py | 12 ++++ 5 files changed, 113 insertions(+), 16 deletions(-) diff --git a/django/test/client.py b/django/test/client.py index 84513a1479..1e18c11b76 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -1,4 +1,5 @@ import urllib +from urlparse import urlparse, urlunparse import sys import os try: @@ -260,10 +261,11 @@ class Client(object): """ Requests a response from the server using GET. """ + parsed = urlparse(path) r = { 'CONTENT_TYPE': 'text/html; charset=utf-8', - 'PATH_INFO': urllib.unquote(path), - 'QUERY_STRING': urlencode(data, doseq=True), + 'PATH_INFO': urllib.unquote(parsed.path), + 'QUERY_STRING': urlencode(data, doseq=True) or parsed.query, 'REQUEST_METHOD': 'GET', 'wsgi.input': FakePayload('') } @@ -280,10 +282,12 @@ class Client(object): else: post_data = data + parsed = urlparse(path) r = { 'CONTENT_LENGTH': len(post_data), 'CONTENT_TYPE': content_type, - 'PATH_INFO': urllib.unquote(path), + 'PATH_INFO': urllib.unquote(parsed.path), + 'QUERY_STRING': parsed.query, 'REQUEST_METHOD': 'POST', 'wsgi.input': FakePayload(post_data), } @@ -295,10 +299,11 @@ class Client(object): """ Request a response from the server using HEAD. """ + parsed = urlparse(path) r = { 'CONTENT_TYPE': 'text/html; charset=utf-8', - 'PATH_INFO': urllib.unquote(path), - 'QUERY_STRING': urlencode(data, doseq=True), + 'PATH_INFO': urllib.unquote(parsed.path), + 'QUERY_STRING': urlencode(data, doseq=True) or parsed.query, 'REQUEST_METHOD': 'HEAD', 'wsgi.input': FakePayload('') } @@ -310,9 +315,10 @@ class Client(object): """ Request a response from the server using OPTIONS. """ + parsed = urlparse(path) r = { - 'PATH_INFO': urllib.unquote(path), - 'QUERY_STRING': urlencode(data, doseq=True), + 'PATH_INFO': urllib.unquote(parsed.path), + 'QUERY_STRING': urlencode(data, doseq=True) or parsed.query, 'REQUEST_METHOD': 'OPTIONS', 'wsgi.input': FakePayload('') } @@ -328,11 +334,13 @@ class Client(object): post_data = encode_multipart(BOUNDARY, data) else: post_data = data + + parsed = urlparse(path) r = { 'CONTENT_LENGTH': len(post_data), 'CONTENT_TYPE': content_type, - 'PATH_INFO': urllib.unquote(path), - 'QUERY_STRING': urlencode(data, doseq=True), + 'PATH_INFO': urllib.unquote(parsed.path), + 'QUERY_STRING': urlencode(data, doseq=True) or parsed.query, 'REQUEST_METHOD': 'PUT', 'wsgi.input': FakePayload(post_data), } @@ -344,9 +352,10 @@ class Client(object): """ Send a DELETE request to the server. """ + parsed = urlparse(path) r = { - 'PATH_INFO': urllib.unquote(path), - 'QUERY_STRING': urlencode(data, doseq=True), + 'PATH_INFO': urllib.unquote(parsed.path), + 'QUERY_STRING': urlencode(data, doseq=True) or parsed.query, 'REQUEST_METHOD': 'DELETE', 'wsgi.input': FakePayload('') } diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index ec9ee8a014..91a78a9c33 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -493,6 +493,18 @@ arguments at time of construction: /customers/details/?name=fred&age=7 + .. versionadded:: development + + If you already have the GET arguments in URL-encoded form, you can + use that encoding instead of using the data argument. For example, + the previous GET request could also be posed as:: + + >>> c = Client() + >>> c.get('/customers/details/?name=fred&age=7') + + If you provide URL both an encoded GET data and a data argument, + the data argument will take precedence. + .. method:: Client.post(path, data={}, content_type=MULTIPART_CONTENT) Makes a POST request on the provided ``path`` and returns a @@ -544,6 +556,18 @@ arguments at time of construction: Note that you should manually close the file after it has been provided to ``post()``. + .. versionadded:: development + + If the URL you request with a POST contains encoded parameters, these + parameters will be made available in the request.GET data. For example, + if you were to make the request:: + + >>> c.post('/login/?vistor=true', {'name': 'fred', 'passwd': 'secret'}) + + ... the view handling this request could interrogate request.POST + to retrieve the username and password, and could interrogate request.GET + to determine if the user was a visitor. + .. method:: Client.head(path, data={}) .. versionadded:: development diff --git a/tests/regressiontests/test_client_regress/models.py b/tests/regressiontests/test_client_regress/models.py index 5a3489040a..7015f71e00 100644 --- a/tests/regressiontests/test_client_regress/models.py +++ b/tests/regressiontests/test_client_regress/models.py @@ -309,17 +309,17 @@ class ExceptionTests(TestCase): self.client.get("/test_client_regress/staff_only/") except SuspiciousOperation: self.fail("Staff should be able to visit this page") - + class TemplateExceptionTests(TestCase): def setUp(self): self.old_templates = settings.TEMPLATE_DIRS settings.TEMPLATE_DIRS = () - + def tearDown(self): settings.TEMPLATE_DIRS = self.old_templates - + def test_no_404_template(self): - "Missing templates are correctly reported by test client" + "Missing templates are correctly reported by test client" try: response = self.client.get("/no_such_view/") self.fail("Should get error about missing template") @@ -334,7 +334,7 @@ class TemplateExceptionTests(TestCase): self.fail("Should get error about syntax error in template") except TemplateSyntaxError: pass - + # We need two different tests to check URLconf substitution - one to check # it was changed, and another one (without self.urls) to check it was reverted on # teardown. This pair of tests relies upon the alphabetical ordering of test execution. @@ -421,3 +421,54 @@ class RequestMethodTests(TestCase): response = self.client.delete('/test_client_regress/request_methods/') self.assertEqual(response.status_code, 200) self.assertEqual(response.content, 'request method: DELETE') + +class QueryStringTests(TestCase): + def test_get_like_requests(self): + for method_name in ('get','head','options','put','delete'): + # A GET-like request can pass a query string as data + method = getattr(self.client, method_name) + response = method("/test_client_regress/request_data/", data={'foo':'whiz'}) + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['request-foo'], 'whiz') + + # A GET-like request can pass a query string as part of the URL + response = method("/test_client_regress/request_data/?foo=whiz") + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['request-foo'], 'whiz') + + # Data provided in the URL to a GET-like request is overridden by actual form data + response = method("/test_client_regress/request_data/?foo=whiz", data={'foo':'bang'}) + self.assertEqual(response.context['get-foo'], 'bang') + self.assertEqual(response.context['request-foo'], 'bang') + + response = method("/test_client_regress/request_data/?foo=whiz", data={'bar':'bang'}) + self.assertEqual(response.context['get-foo'], None) + self.assertEqual(response.context['get-bar'], 'bang') + self.assertEqual(response.context['request-foo'], None) + self.assertEqual(response.context['request-bar'], 'bang') + + def test_post_like_requests(self): + # A POST-like request can pass a query string as data + response = self.client.post("/test_client_regress/request_data/", data={'foo':'whiz'}) + self.assertEqual(response.context['get-foo'], None) + self.assertEqual(response.context['post-foo'], 'whiz') + + # A POST-like request can pass a query string as part of the URL + response = self.client.post("/test_client_regress/request_data/?foo=whiz") + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['post-foo'], None) + self.assertEqual(response.context['request-foo'], 'whiz') + + # POST data provided in the URL augments actual form data + response = self.client.post("/test_client_regress/request_data/?foo=whiz", data={'foo':'bang'}) + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['post-foo'], 'bang') + self.assertEqual(response.context['request-foo'], 'bang') + + response = self.client.post("/test_client_regress/request_data/?foo=whiz", data={'bar':'bang'}) + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['get-bar'], None) + self.assertEqual(response.context['post-foo'], None) + self.assertEqual(response.context['post-bar'], 'bang') + self.assertEqual(response.context['request-foo'], 'whiz') + self.assertEqual(response.context['request-bar'], 'bang') diff --git a/tests/regressiontests/test_client_regress/urls.py b/tests/regressiontests/test_client_regress/urls.py index 016a95170e..e7db15a46b 100644 --- a/tests/regressiontests/test_client_regress/urls.py +++ b/tests/regressiontests/test_client_regress/urls.py @@ -5,6 +5,7 @@ urlpatterns = patterns('', (r'^no_template_view/$', views.no_template_view), (r'^staff_only/$', views.staff_only_view), (r'^get_view/$', views.get_view), + (r'^request_data/$', views.request_data), url(r'^arg_view/(?P.+)/$', views.view_with_argument, name='arg_view'), (r'^login_protected_redirect_view/$', views.login_protected_redirect_view), (r'^set_session/$', views.set_session_view), diff --git a/tests/regressiontests/test_client_regress/views.py b/tests/regressiontests/test_client_regress/views.py index b3e5499d98..e62f6a17c5 100644 --- a/tests/regressiontests/test_client_regress/views.py +++ b/tests/regressiontests/test_client_regress/views.py @@ -1,6 +1,7 @@ from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseRedirect from django.core.exceptions import SuspiciousOperation +from django.shortcuts import render_to_response def no_template_view(request): "A simple view that expects a GET request, and returns a rendered template" @@ -18,6 +19,17 @@ def get_view(request): return HttpResponse("Hello world") get_view = login_required(get_view) +def request_data(request): + "A simple view that returns the request data in the context" + return render_to_response('base.html', { + 'get-foo':request.GET.get('foo',None), + 'get-bar':request.GET.get('bar',None), + 'post-foo':request.POST.get('foo',None), + 'post-bar':request.POST.get('bar',None), + 'request-foo':request.REQUEST.get('foo',None), + 'request-bar':request.REQUEST.get('bar',None), + }) + def view_with_argument(request, name): """A view that takes a string argument