Scheme for final page in `assertRedirects(response, …)` with `response.url = "/some/relative/url/"`

Hello,

I recently started using SECURE_SSL_REDIRECT = True. This however broke most tests that use SimpleTestCase.client.get("/some/relative/url/"), because the secure parameter of get() is by default False:

SimpleTestCase.client.get("/some/relative/url/", secure=False)

I fixed this problem by using a custom test client that makes HTTPS requests by default:

from django.test import Client, TestCase


class SecureTestClient(Client):
    """
    A test client that uses HTTPS requests by default (secure=True).
    This is required when SECURE_SSL_REDIRECT=True is used.
    """

    def get(self, path, data=None, secure=True, **extra):
        return super().get(path, data=data, secure=secure, **extra)

    # Similar for post(), put(), ...
    ...


class MyTestCase(TestCase):
    client_class = SecureTestClient

    ...

This helps with most tests, but tests like this still don’t work:

class AdminTest(TestCase):

    def test_admin_login(self):
        response = self.client.get("/admin/")
        self.assertRedirects(response, "/admin/login/?next=/admin/")

This is because assertRedirects() loads the final page by default [docs]:

assertRedirects(response, …, fetch_redirect_response=True)

However, this fails: assertRedirects() makes an attempt [source] to use the same scheme for loading the final page as was used in request.url – which however is not "https" if request.url is a relative URL like "/some/relative/url/".

As a result, we get:

AssertionError: 301 != 200 : Couldn't retrieve redirection page '/admin/login/': response code was 301 (expected 200)

The problem is that, apart from using fetch_redirect_response=False, I can neither see a workaround for this nor a proper fix. :thinking:

Any ideas?

I would try a custom TestCase base class and override assertRedirects to ensure the final request is always secure.

Sure, but how? If I understand the source code correctly, the relevant line is the one linked above:

secure=(scheme == "https"),

which had to evaluate to True for this to work.

The way I tend to solve this is to not set SECURE_SSL_REDIRECT to True during tests. In reality, you don’t want SSL redirects in tests, so it’s easier to run your tests without it. If you have behaviour which changes based on a secure connection, you can always use override_settings for those specific cases.

That said, I could definitely see some value in having the inner assertRedirects use the same scheme for its redirect check. Sounds like a bug to me worth fixing.

1 Like

Call the original assertRedirects with fetch_redirect_response=False to handle the initial checks, and then manually fetch the final response using your client with an explicit secure=True.

from django.test import TestCase

class SecureRedirectTestCase(TestCase):

# Use the custom client class by default for all inheriting tests
client_class = SecureTestClient

def assertRedirects(self, response, expected_url, status_code=302,
                    target_status_code=200, msg_prefix='',
                    fetch_redirect_response=True):

    # --- Step 1: Standard Checks (Redirect URL and Status) ---
    # Call the parent method, but tell it NOT to fetch the final response yet.
    super().assertRedirects(response, expected_url, status_code=status_code,
                            target_status_code=target_status_code,
                            msg_prefix=msg_prefix,
                            fetch_redirect_response=False)

    # If the user explicitly set fetch_redirect_response=False, we are done.
    if not fetch_redirect_response:
        return

    # --- Step 2: Manually Fetch the Final Target Securely ---
    # Get the URL that the response redirected to.
    # This will be the relative or absolute URL in the 'Location' header.
    response_url = response.url

    # Make the final request using the test client, explicitly forcing secure=True.
    # This overrides the logic that was causing the failure.
    try:
        # By explicitly setting secure=True, we guarantee HTTPS for the final request.
        final_response = self.client.get(response_url, secure=True)

    except Exception as e:
        raise AssertionError(
            msg_prefix + "Couldn't retrieve redirection page '%s': %s" % (
                response_url, e
            )
        )

    # --- Step 3: Check the Final Response's Status Code ---
    self.assertEqual(
        final_response.status_code,
        target_status_code,
        msg_prefix + (
            "Couldn't retrieve redirection page '%s': response code was %d (expected %d)"
            % (response_url, final_response.status_code, target_status_code)
        )
    )


Yes, that would work, but it feels like we’re just re-implementing assertRedirects():thinking:

For now, I’m going with @theorangeone’s suggestion to just use SECURE_SSL_REDIRECT = False in tests. That’s straightforward, and it’s useful not only for assertRedirects(), but also in a bunch of other places, like LiveServerTestCase.