How to follow Django redirect using django-pytest? - django

In setting up a ArchiveIndexView in Django I am able to successfully display a list of items in a model by navigating to the page myself.
When going to write the test in pytest to verify navigating to the page "checklist_GTD/archive/" succeeds, the test fails with the message:
> assert response.status_code == 200
E assert 301 == 200
E + where 301 = <HttpResponsePermanentRedirect status_code=301, "text/html; charset=utf-8", url="/checklist_GTD/archive/">.status_code
test_archive.py:4: AssertionError
I understand there is a way to follow the request to get the final status_code. Can someone help me with how this done in pytest-django, similar to this question? The documentation on pytest-django does not have anything on redirects. Thanks.

pytest-django provides both an unauthenticated client and a logged-in admin_client as fixtures. Really simplifies this sort of thing. Assuming for the moment that you're using admin_client because you just want to test the redirect as easily as possible, without having to log in manually:
def test_something(admin_client):
response = admin_client.get(url, follow=True)
assert response.status_code == 200
If you want to log in a standard user:
def test_something(client):
# Create user here, then:
client.login(username="foo", password="bar")
response = client.get(url, follow=True)
assert response.status_code == 200
By using follow=True in either of these, the response.status_code will equal the return code of the page after the redirect, rather than the access to the original URL. Therefore, it should resolve to 200, not 301.
I think it's not documented in pytest-django because the option is inherited from the Django test client it subclasses from (making requests).

UPDATE:
I'm getting downvoted into oblivion but I still think my answer is better so let me explain.
I still think there is a problem with Shacker's answer, where you can set follow=True and get a response code of 200 but not at the URL you expect. For example, you could get redirected unexpectedly to the login page, follow and get a response code of 200.
I understand that I asked a question on how to accomplish something with pytest and the reason I'm getting downvoted is because I provided an answer using Django's built-in TestCase class. However, the correct answer for the test is/was more important to me at the time than exclusively using pytest. As noted below, my answer still works with pytest's test discovery so I think the answer is still valid. After all, pytest is built upon Django's built-in TestCase. And my answer asserts the response code of 200 came from where I expected it to come from.
The best solution would be to modify pytest to include the expected_url as a parameter. If anyone is up for doing this I think it would be a big improvement. Thanks for reading.
ORIGINAL CONTENT:
Answering my own question here. I decided to include final expected URL using the built-in Django testing framework's assertRedirects and verify that it (1) gets redirected initially with 302 response and (2) eventually succeeds with a code 200 at the expected URL.
from django.test import TestCase, Client
def test_pytest_works():
assert 1==1
class Test(TestCase):
def test_redirect(self):
client = Client()
response = client.get("/checklist_GTD/archive/")
self.assertRedirects(response, "/expected_redirect/url", 302, 200)
Hat tip to #tdsymonds for pointing me in the right direction. I appreciated Shacker's answer but I have seen in some scenarios the redirect result being 200 when the page is redirected to an undesirable URL. With the solution above I am able to enforce the redirect URL, which pytest-django does not currently support.
Please note: This answer is compliant with the auto-discover feature of pytest-django and is thus not incompatible (it will auto-discover both pytest-django and Django TestCase tests).

Related

How to do a class based delete view that allows DELETE method in django 3.1?

In Django 3.1, the typical DeleteView accepts GET and POST.
See https://docs.djangoproject.com/en/3.1/ref/class-based-views/generic-editing/#deleteview
and I reproduce below:
A view that displays a confirmation page and deletes an existing object. The given object will only be deleted if the request method is POST. If this view is fetched via GET, it will display a confirmation page that should contain a form that POSTs to the same URL.
How do I do a DELETEView that's class based view and also accepts DELETE method?
Tldr; I chose to use 303 at the server side so that it can correct redirect to the list view
Long story short is here https://stackoverflow.com/a/24375475/80353
In this SO answer which applies to Spring (a Java Framework), the question had the same issue as me.
Send a DELETE
then server side want to redirect using 302
302 will use precedent method and list typically don't accept DELETE as precedent method. Only POST, GET, and HEAD as precedent method
This seems like a web framework issue. But it's not. It appears to a convention most sensible web frameworks adopt.
There are 3 solutions with drawbacks:
1. override the convention
Allow the backend web framework to accept DELETE as precedent method for 302.
Con: Not nice by convention
2. Let client handle redirection
send back a 200 then client will redirect back to list view
Con: This results in two requests and htmx-delete doesn't work that way. It will send a DELETE method request and then take whatever comes back and immediately swap. I like this so I want to keep this. One request to settle this rather than two.
3. Use 303 for the redirection
After successful delete, do a 303 redirect to list view (I chose this)
Con: 303 doesn't work with HTTP/1.0 and older browsers. But that's not a problem in the year 2021 and will continue to be less of a problem going forward.
In the end I wrote my own deleteview
from django.views.generic.detail import BaseDetailView
class DeleteThingView(BaseDetailView):
http_method_names = ["delete"]
model = Thing
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.delete()
response = redirect(reverse_lazy("things-list"))
response.status_code = 303
return response

request issue when unit-testing webapp2 with mock and patch

I'm building unit tests for this webapp2 handler (built for GAE)
class PushNotificationHandler(webapp2.RequestHandler):
def post(self):
UserNotification.parse_from_queue(self.request)
return
app = webapp2.WSGIApplication([
(r'/push/notification', PushNotificationHandler),
], debug=True)
One test is
#patch.object(UserNotification, 'parse_from_queue')
def test_post_webapp(self, p_parse_from_queue):
response = webtest.TestApp(app).post('/push/notification')
eq_(response.status_int, 200)
p_parse_from_queue.assert_called_once_with(response.request)
The HTTP reply is OK, but the mock assertion fails:
Expected call: parse_from_queue(<TestRequest at 0x105a89850 POST http://localhost/push/notification>)
Actual call: parse_from_queue(<Request at 0x105a89950 POST http://localhost/push/notification>)
I can't understand why the request is not the correct one (looks like a deep copy). Is there anything wrong with the unit-test, or is that the way webapp2 handle requests. In the second case, is there a way to test it, without creating a separate test to test PushNotificationHandler.post()
Thanks
I've used mock's call_args in a similar situation. You can do something like this:
request = p_parse_from_queue.call_args[0][0]
self.assertEqual(request.url, "foo")
self.assertEqual(request.*, *)
The [0][0] gives you the first passed argument assuming that you are using ordered arguments and not keyword arguments.
You can then proceed to check other relevant attributes of the request object to make sure it is behaving as expected.

Setup cookies in flask request unittest without client

I am writing unit-tests for my flask application. I have couple of method which not aimed to return response, but it is actively using cookies. For example:
def get_timezone():
try:
return int(request.cookies.get('timezone_offset', 0))
except (RuntimeError, ValueError):
pass
return 0
My test looks like:
from my_main import my_flask_app
with my_flask_app.test_Request_context(**request_environ_params):
tz_value = 4
# HERE. How to setup cookie value to `tz_value`?
tz = myapp.utils.get_timezone()
self.assertEqual(tz, tz_value)
So, I want to setup cookies (how to setup other request parameters I am already figured out) for testing purposes. How it can be done?
The test client has a set_cookie method, does something like this work?
with app.test_client() as c:
c.set_cookie('localhost', 'COOKIE_NAME', 'cookie_value')
resp = c.get('/my_url')
I think this will be useful:
with app.app_context():
with app.test_request_context():
resp = make_response(redirect('your_url'))
resp.set_cookie('Cookie_name', 'Cookie_Value')
I think this can help you.
The cookie should in headers which is a key of **request_environ_params
cookie = http.dump_cookie("Cookie_name",'Cookie_value')
with app.test_request_context(path="/", method="GET", headers={"COOKIE":cookie}):
# do_something...
I think you can know my answer.
:P(I'm not good at English)

Django testing, no 404 response when expected

I am writing some tests in Django to check the apps which I have written. I am more or less working through what is suggested on here (http://toastdriven.com/blog/2011/apr/10/guide-to-testing-in-django/).
However, when I put something like
resp = self.client.get('made_up_url')
self.assertEqual(resp.status_code, 404)
which doesn't exist (and hence I'm hoping for a 404 response, i.e. the test will pass)...but the test fails saying 200!=400, i.e. it seems to think that the made_up_url is a valid page.
Any ideas what could be wrong?
You are trying to self.client.get an unexistent URL: made_up_url. As dan-klasson told you, you need to get /made_up_url.
I suggest that, instead of hardcoding the URL in the self.client.get arguments, first name the related URL in your urls.py file. So you can resolve it via the reverse function.
Then, once you have a name for it, you can do this in your unit tests:
from django.core.urlresolvers import reverse
url = reverse('made_up_url')
resp = self.client.get(url)
self.assertEqual(resp.status_code, 404)

Django testing - fail on sending email

I have a simple function in Django 1.4 that results in a mail being sent. This is put in a try/except, just in case the mailing service might be down (which is an external dependency).
Now, I would like to test this exception. I thought this would be simple, by overriding some email settings (like settings.EMAIL_HOST or settings.EMAIL_BACKEND) but Django test framework does not cause send_mail() to throw an error even if the backend is configured with jibberish...
So the question is: How do I make send_mail() throw an error in my test case?
Thanks!
Answer:
import mock
class MyTestCase(TestCase):
#mock.patch('path.to.your.project.views.send_mail', mock.Mock(side_effect=Exception('Boom!')))
def test_changed_send_mail(self):
I'm not a testing expert, but I think you should use a send_mail mock that raise the exception you want to test.
Probably you could take a look at this stackoverflow question to know more about mocking in Django.
Yes, the test suite does not set up the email system. Unfortunately, I don't know of any way t o test the email system.
You shouldn't really be testing send_mail functions as it is a built in. That said, you can validate the data being passed into send_mail by another function. If you know the domain of expected inputs, you can verify and throw (raise) an exception of your own.
https://docs.djangoproject.com/en/1.4/topics/email/#the-emailmessage-class
This is the more django'y way to send email:
# attempt to send out a welcome email
try :
t = loader.get_template('email_templates/membership/register.html')
c = Context({
'user' : user,
'site' : Site.objects.get(id=settings.SITE_ID)
})
msg = EmailMessage('Welcome to Site', t.render(c), settings.EMAIL_HOST_USER, to=[user.email,])
msg.content_subtype = "html"
msg.send()
except :
messages.info(request, _(u'Our email servers are encountering technical issues, you may not recieve a welcome email.'))
in my settings.py:
import os
EMAIL_HOST_USER = os.environ['SENDGRID_USERNAME']
EMAIL_HOST= 'smtp.sendgrid.net'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_PASSWORD = os.environ['SENDGRID_PASSWORD']
note: SENDGRID_USERNAME and SENDGRID_PASSWORD are added as env variables by a heroku addon, you may have the actual credentials embedded in your settings file which is fine.
so why isnt your email throwing exceptions? https://docs.djangoproject.com/en/1.4/topics/email/#django.core.mail.get_connection
The fail_silently argument controls how the backend should handle errors. If fail_silently is True, exceptions during the email sending process will be silently ignored.
The patch documentation at http://www.voidspace.org.uk/python/mock/patch.html#where-to-patch explains in detail how to go about this.
I noted that the answer indeed was in the question but to help others understand where the catch is, the link above will prove to be quite insightful.
If you want to patch an object reference in class b.py, ensure that your patch call mocks the object reference in b.py rather than in a.py from where the object is imported. This can be a stumbling block for java developers who are getting used to the whole idea of functions being 1st class citizens.