I've been stuck on this problem for a couple hours. I'm new to django and automated testing, and I've been playing around with Selenium and django's StaticLiveServerTestCase. I've been trying to test my login form (which works fine when I use runserver and test it myself, by the way.)
Everything is working great, except I can't seem to successfully login my test user. I've narrowed down the break point to django's User.authenticate method.
The User object is created successfully in my setUp and I can confirm that by accessing it's attributes in my test method. However, authenticate fails.
I've looked at the following for help but they didn't get me very far:
Django Unittests Client Login: fails in test suite, but not in Shell
Authentication failed in Django test
Any idea why authenticate fails? Do I need to add something to my settings?
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
class AccountTestCase(StaticLiveServerTestCase):
def setUp(self):
self.selenium = webdriver.Chrome()
super().setUp()
User.objects.create(username='test', email='test#test.com', password='Test1234', is_active=True)
def tearDownClass(self):
self.selenium.quit()
super().tearDown()
def test_register(self):
user = authenticate(username='test', password='Test1234')
if user is not None: # prints Backend login failed
print("Backend login successful")
else:
print("Backend login failed")
user = User.objects.get(username='test')
print(user)
print(user.username) # prints test
print(user.password) # prints Test1234
I found the issue. The User.authenticate() method hashes the password provided. However, I set the password directly when creating the user, which means it was stored as Test1234, so the hashed password provided during authentication did not match 'Test1234', hence the failure.
To properly store a hashed password, you need to use the set_password() method.
Updated setUp code:
def setUp(self):
self.selenium = webdriver.Chrome()
super().setUp()
user = User.objects.create(username='test', email='test#test.com', is_active=True)
user.set_password('Test1234')
user.save()
Using create_superuser resolved the issue. Below code resolves it.
from django.contrib.auth.models import User
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.test import Client
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver
import time
from django.contrib.auth import authenticate
#from apps.digital.models import User
class MyTests(StaticLiveServerTestCase):
port = 0
host = 'my host'
def setUp(self):
super(MyTests, self).setUp()
self.selenium = WebDriver()
self.client = Client()
self.user = User.objects.create_superuser(username='test', password='Test1234', email='test#test.com', is_active=True)
self.user.save()
def tearDown(self):
self.selenium.quit()
super(MyTests, self).tearDown()
def test_login(self):
self.user = authenticate(username='test', password='Test1234')
if self.user is not None: # prints Backend login failed
self.user = User.objects.get(username='test')
print(self.user.username) # prints test
print(self.user.password) # prints Test1234
self.login = self.client.login(username='test', password='Test1234')
self.assertEqual(self.login, True)
print("Backend login successful")
self.selenium.get('%s%s' % (self.live_server_url, '/admin/'))
username_input = self.selenium.find_element_by_name("username")
username_input.send_keys(self.user.username)
password_input = self.selenium.find_element_by_name("password")
password_input.send_keys('Test1234')
self.selenium.find_element_by_xpath('//input[#value="Log in"]').click()
time.sleep(1)
else:
print("Backend login failed")
User Authentication is getting passed but when I try to login with the same credentials, the login fails.
from django.contrib.auth.models import User
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.test import Client
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver
import time
from django.contrib.auth import authenticate
#from apps.digital.models import User
class MyTests(StaticLiveServerTestCase):
port = 0
host = '<my host>'
def setUp(self):
super(MyTests, self).setUp()
self.selenium = WebDriver()
self.client = Client()
self.user = User.objects.create(username='test', email='test#test.com', is_active=True)
self.user.set_password('Test1234')
self.user.save()
def tearDown(self):
self.selenium.quit()
super(MyTests, self).tearDown()
def test_login(self):
self.user = authenticate(username='test', password='Test1234')
if self.user is not None: # prints Backend login failed
self.user = User.objects.get(username='test')
print(self.user.username) # prints test
print(self.user.password) # prints Test1234
self.login = self.client.login(username='test', password='Test1234')
self.assertEqual(self.login, True)
print("Backend login successful")
self.selenium.get('%s%s' % (self.live_server_url, '/admin/'))
username_input = self.selenium.find_element_by_name("username")
username_input.send_keys(self.user.username)
password_input = self.selenium.find_element_by_name("password")
password_input.send_keys('Test1234')
self.selenium.find_element_by_xpath('//input[#value="Log in"]').click()
time.sleep(1)
else:
print("Backend login failed")
Related
I would like to test a View in Django using Sellenium that has a decorator, that requires being logged in:
#method_decorator(login_required, name='dispatch')
This is how mu code looks like:
class TestAddOrder(LiveServerTestCase):
def setUp(self):
self.selenium = webdriver.Firefox()
super(TestAddOrder, self).setUp()
def tearDown(self):
self.selenium.quit()
super(TestAddOrder, self).tearDown()
def test_add_order(self):
selenium = self.selenium
selenium.get('http://127.0.0.1:8000/orders/create/')
date = selenium.find_element_by_name('date').send_keys('01/31/2019')
hours = selenium.find_element_by_id('id_hour').send_keys('18')
submit = selenium.find_element_by_name('submit').send_keys(Keys.RETURN).send_keys(Keys.RETURN)
And the error:
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [name="date"]
How can I keep the session of logged in user when trying to automise my test with Sellenium?
In your setUp() method you should create a user, log in the user and set the session cookie so that it's sent with every subsequent request:
def setUp(self):
self.selenium = webdriver.Firefox()
super().setup()
user = User.objects.create_user(...)
self.client.force_login(user) # TestCase client login method
session_key = self.client.cookies[settings.SESSION_COOKIE_NAME].value
self.selenium.get('http://127.0.0.1/') # load any page
self.selenium.add_cookie({'name': settings.SESSION_COOKIE_NAME, 'value': session_key, 'path': '/'})
I'm currently trying to write my unittest for a successfull password reset.
TLDR: I can't figure out how to handle the injection of the form data in the post request in the set-password view.
For context, let me give you my code first, since a line of code speaks more than 1000 words.
accounts/api/views.py:
class PasswordResetView(generics.GenericAPIView):
"""(POST) Expects email. Sends a password reset mail to email."""
permission_classes = (permissions.AllowAny,)
serializer_class = PasswordResetSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid(raise_exception=True):
user = User.objects.get(email=serializer.data['email'])
if user.is_active and user.has_usable_password():
send_password_reset_email(user, request)
return Response(
{"detail": "A password reset email has been send."},
status=status.HTTP_204_NO_CONTENT
)
else:
return Response(
{"detail": "This users password can't be reset."},
status=status.HTTP_406_NOT_ACCEPTABLE
)
accounts/api/urls.py:
url(r'^password_reset/$', views.PasswordResetView.as_view(),
name="password_reset"),
accounts/api/serializers.py:
class PasswordResetSerializer(serializers.Serializer):
email = serializers.EmailField()
def validate_email(self, value):
if User.objects.filter(email__iexact=value).exists():
return value
else:
raise serializers.ValidationError("Email not found.")
accounts/utils.py:
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.contrib.sites.shortcuts import get_current_site
from django.utils import six
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from templated_email import send_templated_mail
password_reset_token = PasswordResetTokenGenerator()
def build_email_context(user_obj, request):
current_site = get_current_site(request)
context = {
"name": "",
"domain": current_site.domain,
"uid": urlsafe_base64_encode(force_bytes(user_obj.pk)),
'token': password_reset_token.make_token(user_obj)
}
if user_obj.name != "":
context["name"]: " " + user_obj.name
return context
def send_password_reset_email(user_obj, request):
context = build_email_context(user_obj, request)
send_templated_mail(
template_name="password_reset",
from_email="noreply#ordersome.com",
recipient_list=[user_obj.email],
context=context
)
The views that are linked to the route in the email are the standard CBV PasswordResetConfirmView and PasswordResetDoneView from django.contrib.auth.
And here is the test I wrote for this password reset sequence:
api/tests/test_views.py:
from django.contrib.auth import get_user, get_user_model
from django.core import mail
from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.reverse import reverse as api_reverse
from rest_framework.test import APITestCase
User = get_user_model()
class UserAPIViewsTestCase(APITestCase):
def setUp(self):
User.objects.create_user(
email="testuser#test.com", password="test1234")
self.login_url = api_reverse("api:auth:login")
self.register_url = api_reverse("api:auth:register")
self.password_reset_url = api_reverse("api:auth:password_reset")
self.password_change_url = api_reverse("api:auth:password_change")
self.user_delete_url = api_reverse("api:auth:user_delete")
def test_api_auth_password_reset_success(self):
"""Test if a password reset successfully resets password."""
pre_user = User.objects.get(email="testuser#test.com")
self.assertEqual(pre_user.check_password("test1234"), True)
email_data = {
"email": "testuser#test.com",
}
email_response = self.client.post(
self.password_reset_url, email_data, format="json")
self.assertEqual(email_response.status_code,
status.HTTP_204_NO_CONTENT)
password_reset_data = {
"new_password1": "newtest1234",
"new_password2": "newtest1234"
}
password_reset_response = self.client.get(
mail.outbox[0].body[-94:-37], format="json", follow=True)
path = password_reset_response.request["PATH_INFO"]
done_url = "http://testserver" + path
password_reset_done_response = self.client.post(
done_url, password_reset_data, format="json")
post_user = User.objects.get(email="testuser#test.com")
self.assertEqual(post_user.check_password("newtest1234"), True)
The last self.assertEqual fails. But when I test the view manually, it works, so the code should be correct. How should I modify the test?
When I log out done_url it gives me http://testserver/auth/reset/MQ/set-password/, which should be the correct path.
And when I log out password_reset_done_response it says, that it is a TemplateResponse and the status_code is 200 (which should mean successful, but it's not). Also this response still seems to have the path /auth/reset/MQ/set-password/ which should not be the case anymore.
password_reset_data is not complete - there's no info to which user the new password should be applied.
from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
...
password_reset_data = {
'uid': urlsafe_base64_encode(force_bytes(pre_user.pk)),
'token': default_token_generator.make_token(pre_user),
'new_password1': 'newtest1234',
'new_password2': 'newtest1234',
}
Other suggestions
setUp(self) should call super().setUp()
Use self.assertTrue instead of self.assertEqual
post_user is not needed, use pre_user.refresh_from_db() before last assert
This is my DRF view:
#api_view(['GET'])
#permission_classes([IsAuthenticated])
def check_user(request):
user = request.user
# use user object here
return JSONResponse({})
And this is my unit test for said view:
class CheckUserViewTest(TestCase):
def test_check_user(self):
user = User.objects.create_user('username', 'Pas$w0rd')
self.client.login(username='username', password='Pas$w0rd')
response = self.client.get(reverse('check_user'))
self.assertEqual(response.status_code, httplib.OK)
But I always get a 401 UNAUTHORIZED response from my view. I have logged in the user in my test. What am I doing wrong?
Since you are using Django REST Framework you have to also use DRF's test client called APIClient instead of Django's test client. This happens automagically if you inherit from DRF's APITestCase instead of Django's TestCase.
Complete example:
class CheckUserViewTest(APITestCase):
def test_check_user(self):
user = User.objects.create_user('username', 'Pas$w0rd')
self.assertTrue(self.client.login(username='username', password='Pas$w0rd'))
response = self.client.get(reverse('check_user'))
self.assertEqual(response.status_code, httplib.OK)
An alternative is to use force_authenticate:
class CheckUserViewTest(APITestCase):
def test_check_user(self):
user = User.objects.create_user('username', 'Pas$w0rd')
self.client.force_authenticate(user)
response = self.client.get(reverse('check_user'))
self.assertEqual(response.status_code, httplib.OK)
If your are using djangorestframework you must have to use APITestCase here.
An complete example is just below
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password
class TestLogin(APITestCase):
'''
This will handle login testcases
'''
def setUp(self):
self.url = reverse('check_user')
def test_login(self):
'''
This will test successfull login
'''
data = {
"full_name" : "full name",
'email' : "email#gmail.com",
'password' : "password"
}
User.objects.create(
full_name = data.get('full_name'),
email = data.get('email'),
password = make_password(data.get('password'))
)
response = self.client.get(self.url, data=data)
self.assertEqual(response.status_code,status.HTTP_200_OK)
I have successfully followed the examples in the documentation of Flask-Dance to add Oauth from GitHub, and manage the users with SQLAlchemy. However, I am unable to add more providers. I tried to simply add another blueprint for Twitter and registering it, but I get various errors when trying to login with Twitter. Also, I end up with lots of duplicated code, so this is not a good approach.
Is there a better way to add another provider?
import sys
from flask import Flask, redirect, url_for, flash, render_template
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound
from flask_dance.contrib.github import make_github_blueprint, github
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin, SQLAlchemyBackend
from flask_dance.consumer import oauth_authorized, oauth_error
from flask_login import (
LoginManager, UserMixin, current_user,
login_required, login_user, logout_user
)
# setup Flask application
app = Flask(__name__)
app.secret_key = "supersekrit"
blueprint = make_github_blueprint(
client_id="my-key-here",
client_secret="my-secret-here",
)
app.register_blueprint(blueprint, url_prefix="/login")
# setup database models
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///multi.db"
db = SQLAlchemy()
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(256), unique=True)
# ... other columns as needed
class OAuth(db.Model, OAuthConsumerMixin):
user_id = db.Column(db.Integer, db.ForeignKey(User.id))
user = db.relationship(User)
# setup login manager
login_manager = LoginManager()
login_manager.login_view = 'github.login'
#login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# setup SQLAlchemy backend
blueprint.backend = SQLAlchemyBackend(OAuth, db.session, user=current_user)
# create/login local user on successful OAuth login
#oauth_authorized.connect_via(blueprint)
def github_logged_in(blueprint, token):
if not token:
flash("Failed to log in with {name}".format(name=blueprint.name))
return
# figure out who the user is
resp = blueprint.session.get("/user")
if resp.ok:
username = resp.json()["login"]
query = User.query.filter_by(username=username)
try:
user = query.one()
except NoResultFound:
# create a user
user = User(username=username)
db.session.add(user)
db.session.commit()
login_user(user)
flash("Successfully signed in with GitHub")
else:
msg = "Failed to fetch user info from {name}".format(name=blueprint.name)
flash(msg, category="error")
# notify on OAuth provider error
#oauth_error.connect_via(blueprint)
def github_error(blueprint, error, error_description=None, error_uri=None):
msg = (
"OAuth error from {name}! "
"error={error} description={description} uri={uri}"
).format(
name=blueprint.name,
error=error,
description=error_description,
uri=error_uri,
)
flash(msg, category="error")
#app.route("/logout")
#login_required
def logout():
logout_user()
flash("You have logged out")
return redirect(url_for("index"))
#app.route("/")
def index():
return render_template("home.html")
# hook up extensions to app
db.init_app(app)
login_manager.init_app(app)
if __name__ == "__main__":
if "--setup" in sys.argv:
with app.app_context():
db.create_all()
db.session.commit()
print("Database tables created")
else:
app.run(debug=True)
Here is a sample project that uses multiple providers simultaneously: flask-dance-multi-provider. You can read through the code to see how it works. Does that help?
The following test code does not pass even though manually submitting the form on my web interface actually does work.
import os
from flask.ext.testing import TestCase
from flask import url_for
from config import _basedir
from app import app, db
from app.users.models import User
class TestUser(TestCase):
def create_app(self):
"""
Required method. Always implement this so that app is returned with context.
"""
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(_basedir, 'test.db')
app.config['WTF_CSRF_ENABLED'] = False # This must be disabled for post to succeed during tests
self.client = app.test_client()
ctx = app.app_context()
ctx.push()
return app
def setUp(self):
db.create_all()
#pass
#app.teardown_appcontext
def tearDown(self):
db.session.remove()
db.drop_all()
#pass
def test_admin_home(self):
# url is the admin home page
url = url_for('admin.index')
resp = self.client.get(url)
self.assertTrue(resp.status_code == 200)
def test_admin_registration(self):
url = url_for('admin.register_view')
data = {'username': 'admin', 'email': 'admin#example.com', 'password': 'admin'}
resp = self.client.post(url, data)
self.assertTrue(resp.status_code == 200)
u = User.query.filter_by(username=u'admin').first()
self.assertTrue(u.username == 'admin') # <----- This fails. Why?
After the test client has post to the register_view url and returns a 200 OK response, I fail to retrieve the 'admin' user from the test database. Why is this so?
Here's the view code (this is a flask admin view)
from flask import request
from flask.ext.admin import expose, AdminIndexView, helpers
from app.auth.forms import LoginForm, RegistrationForm
from app.users.models import User
from app import db
class MyAdminIndexView(AdminIndexView):
#expose('/', methods=('GET', 'POST'))
def index(self):
# handle user login
form = LoginForm(request.form)
self._template_args['form'] = form
return super(MyAdminIndexView, self).index()
#expose('/register/', methods=('GET', 'POST'))
def register_view(self):
# handle user registration
form = RegistrationForm(request.form)
if helpers.validate_form_on_submit(form):
user = User()
form.populate_obj(user)
db.session.add(user)
db.session.commit()
self._template_args['form'] = form
return super(MyAdminIndexView, self).index()
Dumbest mistake ever.
The offending line in my test code is
resp = self.client.post(url, data)
It should be
resp = self.client.post(url, data=data)
I managed to track it down by painstakingly walking through the logic and inserting ipdb.set_trace() step by step until I found the bad POST request made by my client.