Django testing - fail on sending email - django

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.

Related

Google API Scope Changed

edit: I solved it easily by adding "https://www.googleapis.com/auth/plus.me" to my scopes, but I wanted to start a discussion on this topic and see if anyone else experienced the same issue.
I have a service running on GCP, an app engine that uses Google API. This morning, I've received this "warning" message which threw an 500 error.
It has been working fine for the past month and only threw this error today (5 hours prior to this post).
Does anyone know why Google returned an additional scope at the oauth2callback? Any additional insight is very much appreciated. Please let me know if you've seen this before or not. I couldn't find it anywhere.
Exception Type: Warning at /oauth2callback
Exception Value:
Scope has changed from
"https://www.googleapis.com/auth/userinfo.email" to
"https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/plus.me".
This line threw the error:
flow.fetch_token(
authorization_response=authorization_response,
code=request.session["code"])
The return url is https://my_website.com/oauth2callback?state=SECRET_STATE&scope=https://www.googleapis.com/auth/userinfo.email+https://www.googleapis.com/auth/plus.me#
instead of the usual https://my_website.com/oauth2callback?state=SECRET_STATE&scope=https://www.googleapis.com/auth/userinfo.email#
edit: sample code
import the required things
SCOPES = ['https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/calendar',
# 'https://www.googleapis.com/auth/plus.me' <-- without this, it throws the error stated above. adding it, fixes the problem. Google returns an additional scope (.../plus.me) which causes an error.
]
def auth(request):
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
CLIENT_SECRETS_FILE, scopes=SCOPES)
flow.redirect_uri = website_url + '/oauth2callback'
authorization_url, state = flow.authorization_url(
access_type='offline', include_granted_scopes='true',
prompt='consent')
request.session["state"] = state
return redirect(authorization_url)
def oauth2callback(request):
...
# request.session["code"] = code in url
authorization_response = website_url + '/oauth2callback' + parsed.query
flow.fetch_token(
authorization_response=authorization_response,
code=request.session["code"])
...
We discovered the same issue today. Our solution has been working without any hiccups for the last couple of months.
We solved the issue by updating our original scopes 'profile email' to https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile and by doing some minor changes to the code.
When initiating the google_auth_oauthlib.flow client, we previously passed in the scopes in a list with only one item which contained a string in which the scopes were separated by spaces.
google_scopes = 'email profile'
self.flow = Flow.from_client_secrets_file(secret_file, scopes=[google_scopes], state=state)
Now, with the updated scopes, we send in a list where each element is a separate scope.
google_scopes = 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile'
self.flow = Flow.from_client_secrets_file(secret_file, scopes=google_scopes.split(' '), state=state)
Hope it helps, good luck!
I am using requests_oauthlib extension and I had the same error. I fix the issue by adding OAUTHLIB_RELAX_TOKEN_SCOPE: '1' to environment variables. So my app.yaml file is similar to this:
#...
env_variables:
OAUTHLIB_RELAX_TOKEN_SCOPE: '1'
For my case I added the following line in that function that the authentication is happening in
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
flow = InstalledAppFlow.from_client_config(client_config, scopes=SCOPES)
At a guess from your error it looks like you're using a depreciated scope. See:
https://developers.google.com/+/web/api/rest/oauth#deprecated-scopes
I'm also guessing that you may be using the Google+ Platform Web library and maybe the People:Get method. Perhaps try using one of the following scopes instead:
https://www.googleapis.com/auth/plus.login
or
https://www.googleapis.com/auth/plus.me
Given the timing, you might be effected by this change by Google:
"Starting July 18, 2017, Google OAuth clients that request certain sensitive OAuth scopes will be subject to review by Google."
https://developers.google.com/apps-script/guides/client-verification

How to follow Django redirect using django-pytest?

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).

Django can not sent email with attachments

I am trying to sent email by using this code. But when I was trying. Either html or email attachment in sent. But not both. If I omit attachment email body is sent. but if I add attachment email body is not sent . Please someone provide you suggestion.
msg = EmailMultiAlternatives(subject=self.mail_subject, from_email=from_mail, to=(to,),
connection=connection)
if isinstance(attachment, list):
for attach_path in attachment:
msg.attach_file(attach_path)
msg.attach_alternative(html_with_context_data, "text/html")
try:
msg.send(fail_silently=True)
except:
pass
With code like this what do you expect?
try:
msg.send(fail_silently=True)
except:
pass
You are telling django to fail silently which means it will not tell you if something goes wrong. And even if you switched off the silent mode, your exception handling is going to make sure that you never ever hear about it!!
Never do this
except:
pass
Always catch specific extensions, and on the rare cases that you need to catch generic exceptions like this, log it.
import traceback
except:
traceback.print_exc()
Then you will see why exactly your attachment is not being sent.
(perhaps this should be a comment but it's far too long for that)
After spending 2 hours I have added only one line. Now I am getting both HTML and attachment.
msg.content_subtype = 'html'

Django forms EmailValidation not working

I have been researching on this issue but it seems there's not a lot of explanation around there covering this.
...
class RangerRegistrationForm(RegistrationFormUniqueEmail):
email = forms.EmailField(label=_("Email Address"), validators=[EmailValidator(whitelist=['gmail.com'])])
...
Here's the part of my script where I check if the user supplies a gmail account. Unfortunately, as long as it's a valid email it will always pass the check.
What am I doing wrong here?
This is NOT a bug in Django (re-read the source code link posted in #catavaran's answer).
A whitelist in this case is not a "block everything except for this domain part" solution. Rather, the whitelist is a domain part that would otherwise be flagged as invalid by Django's EmailValidator.
For example, the default whitelist is set to domain_whitelist = ['localhost']...an otherwise invalid domain_part that is being flagged as being OK for this use case.
To validate the domain part of an email field, you are going to need to write your own clean function. Something like:
class RangerRegistrationForm(forms.Form):
email = forms.EmailField(label=_("Email Address"))
def clean_email(self):
submitted_data = self.cleaned_data['email']
if '#gmail.com' not in submitted_data:
raise forms.ValidationError('You must register using a Gmail address')
return submitted_data
Congratulations! You had found a bug in Django.
Look at this code from the EmailValidator:
if (domain_part not in self.domain_whitelist and
not self.validate_domain_part(domain_part)):
...
If the domain part of the e-mail is valid then checking against the self.domain_whitelist just ignored.

django-paypal ipn works fine but signal is not received

I have this code at the end of my models.py file
from paypal.standard.ipn.signals import payment_was_successful
def confirm_payment(sender, **kwargs):
# it's important to check that the product exists
logging.debug('** CONFIRMED PAYMENT ***') #never reached this point
try:
bfeat = BuyingFeature.objects.get(slug=sender.item_number)
except BuyingFeature.DoesNotExist:
return
# And that someone didn't tamper with the price
if int(bfeat.price) != int(sender.mc_gross):
return
# Check to see if it's an existing customer
try:
customer = User.objects.get(email=sender.payer_email)
except User.DoesNotExist:
customer = User.objects.create(
email=sender.payer_email,
first_name=sender.first_name,
last_name=sender.last_name
)
# Add a new order
CustomerOrder.objects.create(customer=customer, feature=bfeat, quantity=1, paypal_email=sender.payer_email, invoice=sender.invoice, remarks='')
payment_was_successful.connect(confirm_payment)
The whole process runs ok. Payment is complete. return_url and cancel_url work fine. notify_url was tested from the paypal sandbox's test tools and works ok. However, signal is never received.
Signal code is placed at the end of the models.py and django-paypal code is placed inside my project's directory.
(code was 'stolen' from here)
I must be doing something completely wrong. Any help would be appreciated!
In django-paypal there are two signals for basic transactions:
payment_was_successful
payment_was_flagged
You must handle both signals.
I had this problem - and having chased around a few similar questions have found a resolution for my specific case. I mention it here in case anyone else is hitting this wall.
I've not researched it thoroughly, but it looks as though it's highly dependent on which version/repository you source your copy of django-paypal from. Specifically, the version I downloaded wasn't updated to accommodate the {% csrf_token %} idiom. To get this to work, I had to add the #csrf_exempt decorator to two views:
the ipn view in paypal.standard.views
the view loaded by the return url in my django paypal dictionary (... this one flags a highly accurate error if you have debug on).
Is django-paypal there in the settings.INSTALLED_APPS?
I don't see any other reason why the signal wouldn't be fired, otherwise.