How to check with pytest if one function was called inside Django form save method? - django

What i Have
Within the save method of the form are called two functions: generate_random_password and send_email, I need to know if these functions were called because generate_random_password assigns a password randomly to each new created user and send_email sends a email notification to the user with the credentials, the password generated and the user to login. It is important to know if these functions were executed correctly within save.
class UserAdminCreationForm(forms.ModelForm):
class Meta:
model = User
fields = ()
def save(self, commit: bool = True) -> U:
user = super(UserAdminCreationForm, self).save(commit=False)
data = self.cleaned_data
# Set random password for every new user
random_password = generate_random_password()
user.set_password(random_password)
# Send email confirmation with credentials to login
email = data.get("email")
html_message = render_to_string(
template_name="mails/user_creation_notification.html",
context={"email": email, "password": random_password},
)
# Strip the html tag. So people can see the pure text at least.
plain_message = strip_tags(html_message)
send_mail(
subject="Bienvenido a TodoTránsito",
message=plain_message,
recipient_list=[email],
html_message=html_message,
)
# Save into the DB the new user
if commit:
user.save()
return user
The problem
I'm using pytest to test my Django code, but I don't know how to assert if these functions were called.

From what you wrote in the comments, I would guess mocking both functions could make sense, as you probably don't really need the random password in your test. You could do something like:
from unittest import mock
#mock.patch('some_module.generate_random_password', return_value='swordfish')
#mock.patch('some_module.send_mail')
def test_save_user(mocked_send_mail, mocked_generate_password):
user_form = create_user_form() # whatever you do to create the form in the test
user_from.save()
mocked_generate_password.assert_called_once()
mocked_send_mail.assert_called_once()
# or mocked_send_mail.assert_called_once_with(...) if you want to check the parameters it was called with
Note that you have to make sure to mock the correct module, e.g. the one used in the tested code (see where to patch).
In this case generate_random_password is replaced with a mock that always returns the same password, and send_mail is replaced by a mock that does nothing except recording calls. Both mocks can be accessed via the arguments in the test, that are injected by the patch decorators (last decorator first, the argument names are arbitrary).
If you install pytest-mock, you get the mocker fixture, that gives you the same functionality and more. The same code would like like this:
def test_save_user(mocker):
mocked_generate_password = mocker.patch('some_module.generate_random_password', return_value='swordfish')
mocked_send_mail = mocker.patch('some_module.send_mail')
user_form = create_user_form() # whatever you do to create the form in the test
user_from.save()
mocked_generate_password.assert_called_once()
mocked_send_mail.assert_called_once()
If you now want to use the real generate_random_password, but still want to see if it was called, you can use mocker.spy instead:
def test_save_user(mocker):
mocked_generate_password = mocker.spy(some_module, 'generate_random_password')
mocked_send_mail = mocker.patch('some_module.send_mail')
user_form = create_user_form() # whatever you do to create the form in the test
user_from.save()
mocked_generate_password.assert_called_once()
mocked_send_mail.assert_called_once()
Note that you can achieve the same with unittest.mock.patch.object, but less convenient, in my opinion.

Related

model_mommy - user to assignment relationship

I'm finally setting up testing for my Django app, but I'm having difficulties getting started. I'm using model_mommy to create dynamic data for my tests, but have the following problem:
The view I'm testing is supposed to show me all the assignments a particular user has to complete. To test this, I want to create 500 assignments, log into the app and check if they are shown. So far I have the following test cases:
class TestLogin(TestCase):
def setUp(self):
self.client = Client()
user = User.objects.create(username='sam')
user.set_password('samspassword')
user.save()
def test_login(self):
self.client.login(username='sam', password='samspassword')
response = self.client.get('/')
print (response.content)
self.assertEqual(response.status_code, 200)
and
class TestShowAssignments(TestCase):
def setUp(self):
user_recipe = Recipe(User, username='sam', password="samspassword")
self.assignment = Recipe(Assignment,
coders = related(user_recipe))
self.assignments = self.assignment.make(_quantity=500)
def test_assignments(self):
self.assertIsInstance(self.assignments[0],Assignment)
self.assertEqual(len(self.assignments),500)
The first test passes fine and does what it should: TestLogin logs the user in and shows his account page.
The trouble starts with TestShowAssignments, which creates 500 assignments but if I look at the assignments with print (self.assignments[0].coders), I get auth.User.None. So it doesn't add the user I defined as a relation to the assignments. What might be important here is that the coders field in the model is a m2m field, which I tried to address by using related, but that doesn't seem to work.
What also doesn't work is logging in: if I use the same code I use for logging in during TestLogin in TestShowAssignments, I can't log in and see the user page.
So, my question: How do I use model_mommy to create Assignments and add them to a specific user, so that I can log in as that user and see if the assignments are displayed properly?
Do you want 500 Assignments that all have User "sam" as a single entry in the 'coders' field? If so, try:
from model_mommy import mommy
...
class TestShowAssignments(TestCase):
def setUp(self):
self.user = mommy.make(User, username='sam', password='samspassword')
self.assignments = mommy.make(Assigment, coders=[self.user], _quantity=500)

Only lists and tuples may be used in a list field Validation Error

Hi I am implementing test cases for my models.
I am using Mongoengine0.9.0 + Django 1.8
My models.py
class Project(Document):
# commented waiting for org-group to get finalize
project_name = StringField()
org_group = ListField(ReferenceField(OrganizationGroup, required=False))
My Serializers.py
class ProjectSerializer(DocumentSerializer):
class Meta:
model = Project
depth = 1
test.py file
def setUp(self):
# Every test needs access to the request factory.
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='jacob', email='jacob#jacob.com', password='top_secret')
def test_post_put_project(self):
"""
Ensure we can create new clients in mongo database.
"""
org_group = str((test_utility.create_organization_group(self)).id)
url = '/project-management/project/'
data = {
"project_name": "googer",
"org_group": [org_group],
}
##import pdb; pdb.set_trace()
factory = APIRequestFactory()
user = User.objects.get(username='jacob')
view = views.ProjectList.as_view()
# Make an authenticated request to the view...
request = factory.post(url, data=data,)
force_authenticate(request, user=user)
response = view(request)
self.assertEqual(response.status_code, 200)
When I am running test cases I am getting this error
(Only lists and tuples may be used in a list field: ['org_group'])
The complete Stack Trace is
ValidationError: Got a ValidationError when calling Project.objects.create().
This may be because request data satisfies serializer validations but not Mongoengine`s.
You may need to check consistency between Project and ProjectSerializer.
If that is not the case, please open a ticket regarding this issue on https://github.com/umutbozkurt/django-rest-framework-mongoengine/issues
Original exception was: ValidationError (Project:None) (Only lists and tuples may be used in a list field: ['org_group'])
Not getting why we cant pass object like this.
Same thing when I am posting as an request to same method It is working for me but test cases it is failing
The tests should be running using multipart/form-data, which means that they don't support lists or nested data.
You can override this with the format argument, which I'm guessing you probably want to set to json. Most likely your front-end is using JSON, or a parser which supports lists, which explains why you are not seeing this.

Django unit test for function

I need to test if a user is active. I have that function:
Service.py
#validate_input({
'code': {'required': True}
})
def signup_complete(self, data):
try:
code = VerificationCode.objects.select_related('user').get(code=data["code"],
code_type="registration",
expiration_date__gt=timezone.now())
except VerificationCode.DoesNotExist:
raise NotFound(_(u"Неверный код восстановленыя"), 1001)
user = code.user
user.is_active = True
user.save()
code.delete()
and I try to write a test for this function, but I don't know what argument I need to send.
Test.py
def test_signup_complete(self):
user = SiteUser.objects.get(email="Test#gmail.com")
code = VerificationCode.objects.get_or_create(user=user, code_type="registration", code=user.code)
UserService(user).signup_complete()
self.assertEqual(user.is_active, True)
You can reproduce all the steps needed for user registration and activation. Accessing registration page, sending filled form, checking emails with activation code etc. With this approach (which is much more reliable in comparison to your current code) you can get the confirmation code in the same way as real user gets it (for example, from profile activation email).

Flask Sqlalchemy model working in python shell but not in web interface

I am writing a simple web application in flask(python) which will ask students to fill experimental data in a form and that data will be submitted on a server.
My user class looks like:
class User(db.Model):
id=db.Column(db.Integer,primary_key=True)
email=db.Column(db.String,unique=True)
name=db.Column(db.String)
password=db.Column(db.String)
data = db.relationship('ExpData', backref='student', lazy='dynamic')
and ExpData class, which will store data looks like:
class ExpData(db.Model):
id=db.Column(db.Integer,primary_key=True)
exp_1_data=db.Column(db.String)
submission_date=db.Column(db.DateTime)
user_id=db.Column(db.Integer, db.ForeignKey('user.id'))
Now User model works perfectly, whide ExpData models does not put anything in database. I used the following lines to achieve it:
u=models.User(email=usern,name=nameu,password=userp,rollno=rollno)
db.session.add(u)
db.session.commit()
Then in required function:
user=g.user
dat=models.ExpData(exp_1_data='dataStr',submit=1,submission_date=datetime.now(),student=user)
db.session.add(dat)
db.session.commit()
in Python shell it works perfectly. But for some reason through web interface its not working.
Other details:
Users are logged in via a login_user() function provided by Flask-Login
Then user is assigned to g.user through another decorator
#app.before_request
def before_request():
g.user = current_user
Then Database entry is done whenever following function is executed upon visiting the url
#app.route('/exp1echo', methods=['GET','POST'])
#login_required
def exp1echo():
exp1_data={}
exp1_data["ans"]=float(request.args.get("ans")) #getting some data through ajax 'get' call
user=g.user
filename = user.name+" simplePrint.pdf" # save file with user name in file name
pdf=render_template('exp1_post.html',exp1_data=exp1_data)
pisa.CreatePDF(pdf.encode("ISO-8859-1"), file(filename, "wb"))
pdf.close() # Just fyi this call does return an error
#"No handlers could be found for logger "xhtml2pdf"" but all works fine
abc={'a':1,'b':2,'c':3}
dataStr=str(abc)
dat=ExpData(exp_1_data='dataStr',submit=1,submission_date=datetime.now(),student=g.user)
db.session.add(dat)
db.session.commit()
return jsonify({'base_vl':1}) # return some value to ajax call
User loader is defined as:
lm = LoginManager()
lm.init_app(app)
#lm.user_loader
def load_user(id):
return models.User.query.get(int(id))
Users are registered through following URL:
#app.route('/register', methods=['GET','POST'])
def register():
if request.method == 'POST':
user_pass=request.form["password"]
user_name=request.form["username"]
name_=request.form["name"]
u=models.User(email=user_name,name=name_,password=user_pass)
db.session.add(u)
db.session.commit()
return redirect(url_for('login'))
return render_template('register_form.html')
Well this is embarrassing! Found out the error. For some reason pdf.close() function in exp1echo() gives an error (as I mentioned earlier). That error does not show any message during debugging nor it exits the programme. Hence I ignored it. But it terminates the execution of that function. So it was not sending any call to data base. So I commented it out and voila! It worked.

Convert POST to PUT with Tastypie

Full Disclosure: Cross posted to Tastypie Google Group
I have a situation where I have limited control over what is being sent to my api. Essentially there are two webservices that I need to be able to accept POST data from. Both use plain POST actions with urlencoded data (basic form submission essentially).
Thinking about it in "curl" terms it's like:
curl --data "id=1&foo=2" http://path/to/api
My problem is that I can't update records using POST. So I need to adjust the model resource (I believe) such that if an ID is specified, the POST acts as a PUT instead of a POST.
api.py
class urlencodeSerializer(Serializer):
formats = ['json', 'jsonp', 'xml', 'yaml', 'html', 'plist', 'urlencoded']
content_types = {
'json': 'application/json',
'jsonp': 'text/javascript',
'xml': 'application/xml',
'yaml': 'text/yaml',
'html': 'text/html',
'plist': 'application/x-plist',
'urlencoded': 'application/x-www-form-urlencoded',
}
# cheating
def to_urlencoded(self,content):
pass
# this comes from an old patch on github, it was never implemented
def from_urlencoded(self, data,options=None):
""" handles basic formencoded url posts """
qs = dict((k, v if len(v)>1 else v[0] )
for k, v in urlparse.parse_qs(data).iteritems())
return qs
class FooResource(ModelResource):
class Meta:
queryset = Foo.objects.all() # "id" = models.AutoField(primary_key=True)
resource_name = 'foo'
authorization = Authorization() # only temporary, I know.
serializer = urlencodeSerializer()
urls.py
foo_resource = FooResource
...
url(r'^api/',include(foo_resource.urls)),
)
In #tastypie on Freenode, Ghost[], suggested that I overwrite post_list() by creating a function in the model resource like so, however, I have not been successful in using this as yet.
def post_list(self, request, **kwargs):
if request.POST.get('id'):
return self.put_detail(request,**kwargs)
else:
return super(YourResource, self).post_list(request,**kwargs)
Unfortunately this method isn't working for me. I'm hoping the larger community could provide some guidance or a solution for this problem.
Note: I cannot overwrite the headers that come from the client (as per: http://django-tastypie.readthedocs.org/en/latest/resources.html#using-put-delete-patch-in-unsupported-places)
I had a similar problem on user creation where I wasn't able to check if the record already existed. I ended up creating a custom validation method which validated if the user didn't exist in which case post would work fine. If the user did exist I updated the record from the validation method. The api still returns a 400 response but the record is updated. It feels a bit hacky but...
from tastypie.validation import Validation
class MyValidation(Validation):
def is_valid(self, bundle, request=None):
errors = {}
#if this dict is empty validation passes.
my_foo = foo.objects.filter(id=1)
if not len(my_foo) == 0: #if object exists
foo[0].foo = 'bar' #so existing object updated
errors['status'] = 'object updated' #this will be returned in the api response
return errors
#so errors is empty if object does not exist and validation passes. Otherwise object
#updated and response notifies you of this
class FooResource(ModelResource):
class Meta:
queryset = Foo.objects.all() # "id" = models.AutoField(primary_key=True)
validation = MyValidation()
With Cathal's recommendation I was able to utilize a validation function to update the records I needed. While this does not return a valid code... it works.
from tastypie.validation import Validation
import string # wrapping in int() doesn't work
class Validator(Validation):
def __init__(self,**kwargs):
pass
def is_valid(self,bundle,request=None):
if string.atoi(bundle.data['id']) in Foo.objects.values_list('id',flat=True):
# ... update code here
else:
return {}
Make sure you specify the validation = Validator() in the ModelResource meta.