Fixed #27398 -- Added an assertion to compare URLs, ignoring the order of their query strings.

This commit is contained in:
Jan Pieter Waagmeester 2017-12-19 20:05:10 +01:00 committed by Tim Graham
parent 4249076844
commit 24959e48d9
6 changed files with 95 additions and 25 deletions

View File

@ -9,7 +9,9 @@ from contextlib import contextmanager
from copy import copy from copy import copy
from functools import wraps from functools import wraps
from unittest.util import safe_repr from unittest.util import safe_repr
from urllib.parse import unquote, urljoin, urlparse, urlsplit from urllib.parse import (
parse_qsl, unquote, urlencode, urljoin, urlparse, urlsplit, urlunparse,
)
from urllib.request import url2pathname from urllib.request import url2pathname
from django.apps import apps from django.apps import apps
@ -313,11 +315,30 @@ class SimpleTestCase(unittest.TestCase):
% (path, redirect_response.status_code, target_status_code) % (path, redirect_response.status_code, target_status_code)
) )
self.assertEqual( self.assertURLEqual(
url, expected_url, url, expected_url,
msg_prefix + "Response redirected to '%s', expected '%s'" % (url, expected_url) msg_prefix + "Response redirected to '%s', expected '%s'" % (url, expected_url)
) )
def assertURLEqual(self, url1, url2, msg_prefix=''):
"""
Assert that two URLs are the same, ignoring the order of query string
parameters except for parameters with the same name.
For example, /path/?x=1&y=2 is equal to /path/?y=2&x=1, but
/path/?a=1&a=2 isn't equal to /path/?a=2&a=1.
"""
def normalize(url):
"""Sort the URL's query string parameters."""
scheme, netloc, path, params, query, fragment = urlparse(url)
query_parts = sorted(parse_qsl(query))
return urlunparse((scheme, netloc, path, params, urlencode(query_parts), fragment))
self.assertEqual(
normalize(url1), normalize(url2),
msg_prefix + "Expected '%s' to equal '%s'." % (url1, url2)
)
def _assert_contains(self, response, text, status_code, msg_prefix, html): def _assert_contains(self, response, text, status_code, msg_prefix, html):
# If the response supports deferred rendering and hasn't been rendered # If the response supports deferred rendering and hasn't been rendered
# yet, then ensure that it does get rendered before proceeding further. # yet, then ensure that it does get rendered before proceeding further.

View File

@ -183,7 +183,9 @@ Templates
Tests Tests
~~~~~ ~~~~~
* ... * The new :meth:`.SimpleTestCase.assertURLEqual` assertion checks for a given
URL, ignoring the ordering of the query string.
:meth:`~.SimpleTestCase.assertRedirects` uses the new assertion.
URLs URLs
~~~~ ~~~~

View File

@ -700,6 +700,7 @@ A subclass of :class:`unittest.TestCase` that adds this functionality:
<SimpleTestCase.assertContains>`. <SimpleTestCase.assertContains>`.
* Verifying that a template :meth:`has/hasn't been used to generate a given * Verifying that a template :meth:`has/hasn't been used to generate a given
response content <SimpleTestCase.assertTemplateUsed>`. response content <SimpleTestCase.assertTemplateUsed>`.
* Verifying that two :meth:`URLs <SimpleTestCase.assertURLEqual>` are equal.
* Verifying a HTTP :meth:`redirect <SimpleTestCase.assertRedirects>` is * Verifying a HTTP :meth:`redirect <SimpleTestCase.assertRedirects>` is
performed by the app. performed by the app.
* Robustly testing two :meth:`HTML fragments <SimpleTestCase.assertHTMLEqual>` * Robustly testing two :meth:`HTML fragments <SimpleTestCase.assertHTMLEqual>`
@ -1477,6 +1478,15 @@ your test suite.
You can use this as a context manager in the same way as You can use this as a context manager in the same way as
:meth:`~SimpleTestCase.assertTemplateUsed`. :meth:`~SimpleTestCase.assertTemplateUsed`.
.. method:: SimpleTestCase.assertURLEqual(url1, url2, msg_prefix='')
.. versionadded:: 2.2
Asserts that two URLs are the same, ignoring the order of query string
parameters except for parameters with the same name. For example,
``/path/?x=1&y=2`` is equal to ``/path/?y=2&x=1``, but
``/path/?a=1&a=2`` isn't equal to ``/path/?a=2&a=1``.
.. method:: SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='', fetch_redirect_response=True) .. method:: SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='', fetch_redirect_response=True)
Asserts that the response returned a ``status_code`` redirect status, Asserts that the response returned a ``status_code`` redirect status,

View File

@ -3,7 +3,7 @@ import itertools
import os import os
import re import re
from importlib import import_module from importlib import import_module
from urllib.parse import ParseResult, quote, urlparse from urllib.parse import quote
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
@ -23,7 +23,7 @@ from django.contrib.sessions.middleware import SessionMiddleware
from django.contrib.sites.requests import RequestSite from django.contrib.sites.requests import RequestSite
from django.core import mail from django.core import mail
from django.db import connection from django.db import connection
from django.http import HttpRequest, QueryDict from django.http import HttpRequest
from django.middleware.csrf import CsrfViewMiddleware, get_token from django.middleware.csrf import CsrfViewMiddleware, get_token
from django.test import Client, TestCase, override_settings from django.test import Client, TestCase, override_settings
from django.test.client import RedirectCycleError from django.test.client import RedirectCycleError
@ -70,23 +70,6 @@ class AuthViewsTestCase(TestCase):
form_errors = list(itertools.chain(*response.context['form'].errors.values())) form_errors = list(itertools.chain(*response.context['form'].errors.values()))
self.assertIn(str(error), form_errors) self.assertIn(str(error), form_errors)
def assertURLEqual(self, url, expected, parse_qs=False):
"""
Given two URLs, make sure all their components (the ones given by
urlparse) are equal, only comparing components that are present in both
URLs.
If `parse_qs` is True, then the querystrings are parsed with QueryDict.
This is useful if you don't want the order of parameters to matter.
Otherwise, the query strings are compared as-is.
"""
fields = ParseResult._fields
for attr, x, y in zip(fields, urlparse(url), urlparse(expected)):
if parse_qs and attr == 'query':
x, y = QueryDict(x), QueryDict(y)
if x and y and x != y:
self.fail("%r != %r (%s doesn't match)" % (url, expected, attr))
@override_settings(ROOT_URLCONF='django.contrib.auth.urls') @override_settings(ROOT_URLCONF='django.contrib.auth.urls')
class AuthViewNamedURLTests(AuthViewsTestCase): class AuthViewNamedURLTests(AuthViewsTestCase):
@ -724,10 +707,10 @@ class LoginTest(AuthViewsTestCase):
class LoginURLSettings(AuthViewsTestCase): class LoginURLSettings(AuthViewsTestCase):
"""Tests for settings.LOGIN_URL.""" """Tests for settings.LOGIN_URL."""
def assertLoginURLEquals(self, url, parse_qs=False): def assertLoginURLEquals(self, url):
response = self.client.get('/login_required/') response = self.client.get('/login_required/')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, url, parse_qs=parse_qs) self.assertURLEqual(response.url, url)
@override_settings(LOGIN_URL='/login/') @override_settings(LOGIN_URL='/login/')
def test_standard_login_url(self): def test_standard_login_url(self):
@ -751,7 +734,7 @@ class LoginURLSettings(AuthViewsTestCase):
@override_settings(LOGIN_URL='/login/?pretty=1') @override_settings(LOGIN_URL='/login/?pretty=1')
def test_login_url_with_querystring(self): def test_login_url_with_querystring(self):
self.assertLoginURLEquals('/login/?pretty=1&next=/login_required/', parse_qs=True) self.assertLoginURLEquals('/login/?pretty=1&next=/login_required/')
@override_settings(LOGIN_URL='http://remote.example.com/login/?next=/default/') @override_settings(LOGIN_URL='http://remote.example.com/login/?next=/default/')
def test_remote_login_url_with_next_querystring(self): def test_remote_login_url_with_next_querystring(self):

View File

@ -205,6 +205,12 @@ class ClientTest(TestCase):
response = self.client.get('/redirect_view/', {'var': 'value'}) response = self.client.get('/redirect_view/', {'var': 'value'})
self.assertRedirects(response, '/get_view/?var=value') self.assertRedirects(response, '/get_view/?var=value')
def test_redirect_with_query_ordering(self):
"""assertRedirects() ignores the order of query string parameters."""
response = self.client.get('/redirect_view/', {'var': 'value', 'foo': 'bar'})
self.assertRedirects(response, '/get_view/?var=value&foo=bar')
self.assertRedirects(response, '/get_view/?foo=bar&var=value')
def test_permanent_redirect(self): def test_permanent_redirect(self):
"GET a URL that redirects permanently elsewhere" "GET a URL that redirects permanently elsewhere"
response = self.client.get('/permanent_redirect_view/') response = self.client.get('/permanent_redirect_view/')

View File

@ -911,6 +911,54 @@ class AssertFieldOutputTests(SimpleTestCase):
self.assertFieldOutput(MyCustomField, {}, {}, empty_value=None) self.assertFieldOutput(MyCustomField, {}, {}, empty_value=None)
class AssertURLEqualTests(SimpleTestCase):
def test_equal(self):
valid_tests = (
('http://example.com/?', 'http://example.com/'),
('http://example.com/?x=1&', 'http://example.com/?x=1'),
('http://example.com/?x=1&y=2', 'http://example.com/?y=2&x=1'),
('http://example.com/?x=1&y=2', 'http://example.com/?y=2&x=1'),
('http://example.com/?x=1&y=2&a=1&a=2', 'http://example.com/?a=1&a=2&y=2&x=1'),
('/path/to/?x=1&y=2&z=3', '/path/to/?z=3&y=2&x=1'),
('?x=1&y=2&z=3', '?z=3&y=2&x=1'),
)
for url1, url2 in valid_tests:
with self.subTest(url=url1):
self.assertURLEqual(url1, url2)
def test_not_equal(self):
invalid_tests = (
# Protocol must be the same.
('http://example.com/', 'https://example.com/'),
('http://example.com/?x=1&x=2', 'https://example.com/?x=2&x=1'),
('http://example.com/?x=1&y=bar&x=2', 'https://example.com/?y=bar&x=2&x=1'),
# Parameters of the same name must be in the same order.
('/path/to?a=1&a=2', '/path/to/?a=2&a=1')
)
for url1, url2 in invalid_tests:
with self.subTest(url=url1), self.assertRaises(AssertionError):
self.assertURLEqual(url1, url2)
def test_message(self):
msg = (
"Expected 'http://example.com/?x=1&x=2' to equal "
"'https://example.com/?x=2&x=1'"
)
with self.assertRaisesMessage(AssertionError, msg):
self.assertURLEqual('http://example.com/?x=1&x=2', 'https://example.com/?x=2&x=1')
def test_msg_prefix(self):
msg = (
"Prefix: Expected 'http://example.com/?x=1&x=2' to equal "
"'https://example.com/?x=2&x=1'"
)
with self.assertRaisesMessage(AssertionError, msg):
self.assertURLEqual(
'http://example.com/?x=1&x=2', 'https://example.com/?x=2&x=1',
msg_prefix='Prefix: ',
)
class FirstUrls: class FirstUrls:
urlpatterns = [url(r'first/$', empty_response, name='first')] urlpatterns = [url(r'first/$', empty_response, name='first')]