Wrong Behaviour doing tests on Django - python

I'm having problems to do test on Django. I've been reading the documentation of the responses and I can't do the same as they explain on the documentation.
When I get the response, I only have access to response.status_code and can't access to context or redirect_chain when I write response.(and now PyCharm shows all available options).
I've checked on settings.py and I've 'BACKEND': 'django.template.backends.django.DjangoTemplates' to be sure that I'm using Django templates so I don't know why don't work the test. I need configure something?
The code of the test I'm trying to do it's:
from django.test import TestCase
from django.test.client import Client
class Test(TestCase):
def testLogin(self):
client = Client()
headers = {'X-OpenAM-Username': 'user', 'X-OpenAM-Password': 'password', 'Content-Type': 'application/json'}
data = {}
response = self.client.post('/login/', headers=headers, data=data, secure=True, follow=True)
assert (response.status_code == 200)
# self.assertRedirects(response, '/menu/', status_code=301, target_status_code=200)
I'm not using Django authentication, the login form sends the data to an IDP and if the IDP sends with a correct answer, the "login" it's successful:
def login(request):
logout(request)
message = None
if request.method == "POST":
form = LoginForm(request.POST)
if form.is_valid():
username = request.POST['username']
password = request.POST['password']
headers = {'X-OpenAM-Username': username, 'X-OpenAM-Password': password, 'Content-Type': 'application/json'}
data = {}
req = requests.post('http://openam.idp.com:8090/openamIDP/json/authenticate', headers=headers, params=data)
if req.status_code == 200:
respJson = json.loads(req.content)
tokenIdJson = respJson['tokenId']
request.session['tokenId'] = tokenIdJson
return render_to_response('menu/menu.html', request)
elif req.status_code == 401:
message = "Invalid username and/or password. Please, try again"
else:
form = LoginForm()
return render_to_response('registration/login.html', {'message': message, 'form': form},
context_instance=RequestContext(request))
The redirect assert it's commented because now it fails, when I do the debug I see an empty redirect_chain. I don't understand why happens this because running the web everything works, all views redirect as expected.
Why I only can check status_code? I'm doing something wrong when I redirect after a successful login that on a normal use it works but on the test not?
Thanks.

The remote authentication url expects the credentials as headers, but your local login view expects them as POST data. Your test passes the credentials as headers to your local view.
As a result, the form is passed an empty dictionary (request.POST contains no actual data), and the form is invalid. You get an empty form as a response, without any redirects.
You have to simply pass the credentials as post data to your local view:
def testLogin(self):
client = Client()
data = {'username': 'user', 'password': 'password'}
response = self.client.post('/login/', data=data, secure=True, follow=True)
assert (response.status_code == 200)
self.assertRedirects(response, '/menu/', status_code=301, target_status_code=200)

Related

Python/Django Test Framework: Response code was 200 (expected 200)

So I am using Django's test framework, and in this case im testing update_password_view that I ve created on top of the built-in PasswordChangeForm.
Could someone please help me with the error from below?
After I run tests I get the following error:
AssertionError: [] is not true : Response didn't redirect as expected: Response code was 200(expected 200)
Here is the code:
#views.py
class UpdatePassword(PasswordChangeView):
form_class = PasswordChangeForm
success_url = reverse_lazy('posts:home')
template_name = 'accounts/password.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# get the number of unseen messages
context['inbox_count'] = Message.objects.filter(
~Q(sender=self.request.user), Q(seen=False),
(Q(chat__user1=self.request.user) |\
Q(chat__user2=self.request.user))).count()
return context
#tests.py
def test_update_password_view(self):
credentials = {
'old_password': '123secret',
'password1': '321secret',
'password2': '321secret',
}
response = self.client.post('http://127.0.0.1:8000/users/change-password/',
credentials, follow=True)
self.assertRedirects(response, '/posts/', status_code=200,
target_status_code=200)
status_code inside assertRedirects has to be redirect status code, which should be 3XX. In your occasion it has to be 302. See more at docs.
For the proper behaviour you should replace this:
self.assertRedirects(response, '/posts/', status_code=200, target_status_code=200)
With this:
self.assertRedirects(response, '/posts/', status_code=302, target_status_code=200)
By the way, it's similar to this:
self.assertRedirects(response, '/posts/')
This is not tested, but perhaps the issue is that it is not redirecting because the user is not created, or logged in with the old password. Try creating a user, logging them in, then running the test.
#tests.py
def test_update_password_view(self):
credentials = {
'old_password': '123secret',
'password1': '321secret',
'password2': '321secret',
}
# Create and login the user
self.client.user = User.objects.create(username="testuser", password="123secret")
c = Client()
logged_in = c.login(username='testuser', password='123secret')
response = self.client.post('http://127.0.0.1:8000/users/change-password/',
credentials, follow=True)
self.assertRedirects(response, '/posts/', status_code=302, target_status_code=200)

Django server 403 (CSRF token missing or incorrect)

I have a basic Django server with an python command line client for posting to this service. I have a login function and a post function. Even though the cookie for CSRF is being set from the login function the server is saying forbidden when I try to access the post_product endpoint after logging in.
I've been troubleshooting this for days and haven't had any luck.
/api/login/ function:
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth import authenticate, login, logout
from django.http import HttpResponse
#csrf_exempt
def handle_login(request):
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
user = authenticate(request, username=username, password=password)
if user is not None:
if user.is_active:
login(request, user)
if user.is_authenticated:
return HttpResponse(author.name + ' is logged in, Welcome!', status=201, content_type='text/plain')
return HttpResponse(data)
else:
return HttpResponse('disabled account', status=400, content_type='text/plain')
else:
return HttpResponse('invalid login', status=400, content_type='text/plain')
else:
return HttpResponse('request method invalid ' + request.method, status=400, content_type='text/plain')
/api/postproduct/ function:
def post_story(request):
if request.method == 'POST' and request.user.is_authenticated:
# Pull product details from request.
# Validate product details.
# Create model and save.
Python terminal client
FAILURE_MESSAGE = "The server responded with an unsuccessful code: "
def run():
url ='http://127.0.0.1:8000' # For debugging
logged_in = false
with requests.session() as session:
while True:
command = input("""Enter one of the following commands:
login
post \n""")
# login case.
if command == "login":
url = user_inputs[1]
logged_in = login(session, url)
continue
# post case.
elif command == "post" and logged_in:
post(session, url)
continue
else:
print('incorrect command')
continue
def login(session, url):
username = input("Enter your username: \n")
password = input("Enter your password: \n")
response = session.post(url + "/api/login/", data={'username': username, 'password': password})
# If any response but success notify user.
if response.status_code != 201:
print(FAILURE_MESSAGE + str(response.status_code))
return False
else:
print("Successfully logged in!")
return True
def post(session, url):
# Check session is authenticated
if 'csrftoken' not in session.cookies:
print("Not authenticated, have you logged in to a service?")
return
# Omitted : prompt user for productname, category, price and details.
data = {'productname': productname, 'category': category, 'price': price, 'details': details}
data_json = json.dumps(data)
payload = {'json_payload': data_json}
if not session_is_active():
print("You aren't logged into any services")
return
else:
response = session.post(url + "/api/postproduct/", data=payload)
print(6)
if response.status_code != 201:
print(FAILURE_MESSAGE + str(response.status_code))
return
print("Post was successful")
When I run the client, login works fine and on inspection does set the csrf cookie. However when I then try and post the server responds with 403 forbidden. From the server's output:
[15/Aug/2019 15:45:23] "POST /api/login/ HTTP/1.1" 201 42
Forbidden (CSRF token missing or incorrect.): /api/postproduct/
Django's CSRF protection requires that you post the CSRF cookie and a token hidden in a form field. For AJAX requests, you can set a header instead of the form field.
Try something like the following (untested):
headers = {'X-CSRFToken': session.cookies['csrftoken']}
response = session.post(url + "/api/postproduct/", data=payload, headers=headers)
Error = csrf Forbidden (CSRF token missing or incorrect.) when submitting a request to a Django backend:
in a form, include {% csrf_token %}, which generates an input tag with the csrf token value,
and in the request, have the headers include ‘X-CSRFTOKEN
headers: {
content_type: 'application/json',
'X-CSRFToken': "{{ csrf_token }}"
},

Pytest Flask Request Referrer

I'm currently testing my Flask application using Pytest and ran into a problem with a POST request and a redirect. Let me explain a bit more.
A user wants to register for our new site, but must confirm they have an account with a different site. Once they confirm the credentials of the other account, they are taken to the register page. They can only hit the register page if coming from the confirmation page else they are redirected back to the home page.
I want to test this functionality and can successfully make a POST request to the confirmation page. If I don't specify follow_redirects=True and print the response data, I get the following HTML:
Redirecting...
Redirecting...
You should be redirected automatically to target URL: /register?emp_id=1. If not click the link.
Great! Exactly what I'm looking for! I want to be redirected to the registration page.
Now when I do specify follow_redirects=True and print out the response data, I expected the register page HTML to return. The response data instead returns the home page HTML.
I further investigated where the problem was. As I mentioned before, the only way you can hit the registration page is from the confirmation page. I took a look at the request.referrer attribute in the view during the test and it will return None. I attempted setting the Referrer header content in the test's POST request, but to no luck.
Here is the code I'm working with:
views.py
#app.route('/confirm', methods=['GET', 'POST'])
def confirm_bpm():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = BPMLoginForm()
if form.validate_on_submit():
bpm_user = BPMUser.query\
.filter(and_(BPMUser.group_name == form.group_name.data,
BPMUser.user_name == form.username.data,
BPMUser.password == encrypt(form.password.data)))\
.first()
if not bpm_user:
flash('BPM user account does not exist!')
url = url_for('confirm_bpm')
return redirect(url)
if bpm_user.user_level != 3:
flash('Only L3 Users can register through this portal.')
url = url_for('confirm_bpm')
return redirect(url)
url = url_for('register', emp_id=bpm_user.emp_id)
return redirect(url)
return render_template('login/confirm_bpm.html', form=form)
#app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
if request.method == 'GET' and\
request.referrer != request.url_root + 'confirm':
return redirect(url_for('index'))
emp_id = request.args.get('emp_id')
emp_id_exists = User.query.filter_by(emp_id=emp_id).first()
if emp_id_exists:
flash('User is already registered!')
return redirect(url_for('login'))
form = RegistrationForm()
if form.validate_on_submit():
new_user = User(login_type=form.login_type.data, login=form.login.data,
emp_id=emp_id)
new_user.set_password(form.password.data)
db.session.add(new_user)
db.session.commit()
flash('Registration successful!')
return redirect(url_for('login'))
return render_template('login/register.html', form=form)
TESTS
base.py
from config import TestConfig
from app import app, db
#pytest.fixture
def client():
"""
Initializes test requests for each individual test. The test request
keeps track of cookies.
"""
app.config.from_object(TestConfig)
client = app.test_client()
ctx = app.app_context()
ctx.push()
yield client
ctx.pop()
def confirm_bpm_login(client, group_name, username, password):
"""
POST to /confirm
"""
return client.post('/confirm', data=dict(
group_name=group_name,
username=username,
password=password,
submit=True
), follow_redirects=True)
test_auth.py
from app import db
from app.models import BPMCompany, BPMEmployee, User, BPMUser
from tests.base import client, db_data, login, confirm_bpm_login
def test_registration_page_from_confirm(client, db_data):
"""
Test registration page by HTTP GET request from "/confirm" url.
Should cause redirect to registration page.
!!! FAILING !!!
Reason: The POST to /confirm will redirect us to /register?emp_id=1,
but it will return the index.html because in the register view,
request.referrer does not recognize the POST is coming from /confirm_bpm
"""
bpm_user = BPMUser.query.filter_by(id=1).first()
rv = confirm_bpm_login(client, bpm_user.group_name,
bpm_user.user_name, 'jsmith01')
assert b'Register' in rv.data
The db_data parameter is a fixture in base.py that just populates the DB with the necessary data for the registration flow to work.
My goal is to test a complete registration flow without having to separate the confirmation and registration into two tests.
The Flask test client does not appear to add the Referer header when it follows redirects.
What you can do is implement your own version of following redirects. Maybe something like this:
def confirm_bpm_login(client, group_name, username, password):
"""
POST to /confirm
"""
response = client.post('/confirm', data=dict(
group_name=group_name,
username=username,
password=password,
submit=True
), follow_redirects=False)
if 300 <= response.status_code < 400:
response = client.get(response.headers['Location'], headers={
"Referer": 'http://localhost/confirm'
})
return response
Please test this, I wrote it from memory and may need some minor adjustments.

Django Rest Framework JWT Unit Test

I am using DRF with the JWT package for authentication. Now, I'm trying to write a unit test that authenticates itself with a JWT token. No matter how I try it, I can't get the test API client to authenticate itself via JWT. If I do the same with an API client (in my case, Postman), everything works.
This is the test case:
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework_jwt.settings import api_settings
from backend.factories import member_factory
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
class MemberTests(APITestCase):
def test_get_member(self):
member = member_factory()
payload = jwt_payload_handler(member.user)
token = jwt_encode_handler(payload)
self.client.credentials(Authorization='JWT {0}'.format(token))
response = self.client.get(reverse('member-detail', kwargs={'pk': member.pk}))
assert response.status_code == 200
But I always get a 401 Authentication credentials were not provided.
In response.request I see the token is there, it's just not being applied I guess.
If I rewrite the test to use rest_framework.test.RequestsClient and actually send it to the live_server URL, it works.
Any help on this?
P.S.: I am aware of force_authenticate() and login, but I would like my unit tests to access the API the same as the API client will in production.
Try setting up a new APIClient for this test. This is how my own test looks like
def test_api_jwt(self):
url = reverse('api-jwt-auth')
u = user_model.objects.create_user(username='user', email='user#foo.com', password='pass')
u.is_active = False
u.save()
resp = self.client.post(url, {'email':'user#foo.com', 'password':'pass'}, format='json')
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
u.is_active = True
u.save()
resp = self.client.post(url, {'username':'user#foo.com', 'password':'pass'}, format='json')
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertTrue('token' in resp.data)
token = resp.data['token']
#print(token)
verification_url = reverse('api-jwt-verify')
resp = self.client.post(verification_url, {'token': token}, format='json')
self.assertEqual(resp.status_code, status.HTTP_200_OK)
resp = self.client.post(verification_url, {'token': 'abc'}, format='json')
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='JWT ' + 'abc')
resp = client.get('/api/v1/account/', data={'format': 'json'})
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
client.credentials(HTTP_AUTHORIZATION='JWT ' + token)
resp = client.get('/api/v1/account/', data={'format': 'json'})
self.assertEqual(resp.status_code, status.HTTP_200_OK)
The following answer applies if you are using Simple JWT and pytest, and Python 3.6+. You need to create a fixture, I have called it api_client, and you need to get the token for an existing user.
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
import pytest
#pytest.fixture
def api_client():
user = User.objects.create_user(username='john', email='js#js.com', password='js.sj')
client = APIClient()
refresh = RefreshToken.for_user(user)
client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}')
return client
Notice that in the fixture above, the user is created there, but you can use another fixture to create the user and pass it to this one. The key element is the following line:
refresh = RefreshToken.for_user(user)
This line allows you to create tokens manually as explained in the docs. Once you have that token, you can use the method credentials in order to set headers that will then be included on all subsequent requests by the test client. Notice that refresh.access_token contains the access token.
This fixture has to be used in your tests that you require the user to be authenticated as in the following example:
#pytest.mark.django_db
def test_name_of_your_test(api_client):
# Add your logic here
url = reverse('your-url')
response = api_client.get(url)
data = response.data
assert response.status_code == HTTP_200_OK
# your asserts
I had similar issue, enclosed I send you my solution just to have more code to compare (tests.py).
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User
class AuthViewsTests(APITestCase):
def setUp(self):
self.username = 'usuario'
self.password = 'contrasegna'
self.data = {
'username': self.username,
'password': self.password
}
def test_current_user(self):
# URL using path name
url = reverse('tokenAuth')
# Create a user is a workaround in order to authentication works
user = User.objects.create_user(username='usuario', email='usuario#mail.com', password='contrasegna')
self.assertEqual(user.is_active, 1, 'Active User')
# First post to get token
response = self.client.post(url, self.data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK, response.content)
token = response.data['token']
# Next post/get's will require the token to connect
self.client.credentials(HTTP_AUTHORIZATION='JWT {0}'.format(token))
response = self.client.get(reverse('currentUser'), data={'format': 'json'})
self.assertEqual(response.status_code, status.HTTP_200_OK, response.content)
Well, since i was using django unit test client, i just created a simple base test class with a bearer token property:
import json
from django.test import TestCase
from django.contrib.auth import User
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
class TestCaseBase(TestCase):
#property
def bearer_token(self):
# assuming there is a user in User model
user = User.objects.get(id=1)
refresh = RefreshToken.for_user(user)
return {"HTTP_AUTHORIZATION":f'Bearer {refresh.access_token}'}
In my django unit tests:
class SomeTestClass(TestCaseBase):
url = "someurl"
def test_get_something(self):
self.client.get(self.url, **self.bearer_token)
def test_post_something(self):
self.client.post(self.url, data={"key":"value"}, **self.bearer_token)
Inspired by #dkarchmer, this is my code working.
I am using a custom user model which the email is used for authentication.
Pay attention to using email field for authentication requests.
If I use username, the response is 400_BAD_REQUEST.
The 401_UNAUTHORIZED usually means the credentials are not correct or the user is not activated.
def test_unusual(self):
User = get_user_model()
email = 'user#test.com'
password = 'userpass1'
username = 'user'
user = User.objects.create_user(
username=username, email=email, password=password)
user.is_active = False
user.save()
obtain_url = reverse('token_obtain_pair')
resp = self.client.post(
obtain_url, {'email': email, 'password': password}, format='json')
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
user.is_active = True
user.save()
resp = self.client.post(
obtain_url, {'email': email, 'password': password}, format='json')
self.assertEqual(resp.status_code, status.HTTP_200_OK)
Postman interacts with your actual database. Django uses separate database for it's test case running. Therefore a new user record needs to be created again inside your test definition before authentication testing. Hope this helps.
I'm using DRF and simple-jwt and I had to use Bearer instead of JWT in the http auth header: HTTP_AUTHORIZATION=f'Bearer {token}'
Full code:
def setUp(self):
username = "tim#me.com"
password = "strongP#assword!"
self.user = User.objects.create_user(username, username, password)
jwt_fetch_data = {
'username':username,
'password':password
}
url = reverse('token_obtain_pair')
response = self.client.post(url, jwt_fetch_data, format='json')
token = response.data['access']
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
def test_post(self):
response = self.client.get('/some-url/',
data={'format': 'json'}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
from rest_framework.test import APITestCase
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework import status
from rest_framework_simplejwt.tokens import RefreshToken
User = get_user_model()
class TestCaseBase(APITestCase):
#property
def bearer_token(self):
# assuming there is a user in User model
user = User.objects.create_user(
email='test#user.me', password='12345678'
)
refresh = RefreshToken.for_user(user)
return {"HTTP_AUTHORIZATION": f'Bearer {refresh.access_token}'}
class CategoriesTestClass(TestCaseBase):
url = reverse('categories-list')
def test_get_list_no_auth(self):
response = self.client.get(self.url)
self.assertEqual(
response.status_code, status.HTTP_401_UNAUTHORIZED, response.data
)
def test_get_list(self):
response = self.client.get(self.url, **self.bearer_token)
self.assertEqual(response.status_code, status.HTTP_200_OK)`enter code here`

Flask oauth 2 resource owner password flow

I'm using flask-oauthlib module to develop both oauth 2 client and provider
When using resource owner password flow, the provider won't redirect to client's redirect url.
Here is my client code for sending post to provider:
#app.route('/signin', methods=['POST', 'GET'])
def signin():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
f = {'client_id': 'jCce40zAaHXLxP0prU*************',
'client_secret': 'vzf7U219hrAjIYN70NcFo3VBQzott******',
'grant_type': 'password', 'scope': 'email',
'redirect_uri': 'http://localhost:8000/authorized', 'response_type': 'token'}
data = {'username': username, 'password': password}
encoded_url = 'http://127.0.0.1:5000/oauth/authorize?' + parse.urlencode(f)
headers = {"Content-Type": "application/json"}
requests.post(encoded_url, data=json.dumps(data), headers=headers)
return render_template('signin.html')
And here is provider authorize_handler
#app.route('/oauth/authorize', methods=['GET', 'POST'])
#oauth.authorize_handler
def authorize(*args, **kwargs):
if request.method == 'POST':
details = json.loads(request.data)
username = details['username']
password = details['password']
user = User.query.filter_by(user_name=username).first()
if user:
if user.check_password(password):
session['id'] = user.id
return True
return False
return False
if request.method == 'GET':
user = current_user()
if not user:
session['redirect_after_login'] = request.url
return redirect('/home')
client_id = kwargs.get('client_id')
client = Client.query.filter_by(client_id=client_id).first()
kwargs['client'] = client
kwargs['user'] = user
return render_template('authorize.html', **kwargs)
confirm = request.form.get('confirm', 'no')
return confirm == 'yes'
Also Flask-oauthlib oauth 2 provider logging
Fetched credentials from request {'response_type': 'token', 'state': None, 'client_id': 'jCce40zAaHXLxP0prU************', 'redirect_uri': 'http://localhost:8000/authorized'}.
Found redirect_uri http://localhost:8000/authorized.
Validate client 'jCce40zAaHXLxP0prU***********'
Save bearer token {'scope': 'email', 'access_token': 'y08hkm594YbLe2*****', 'expires_in': 180, 'token_type': 'Bearer'}
Authorization successful.
127.0.0.1 - - [20/Sep/2015 17:40:53] "POST /oauth/authorize?client_id=jCce40zAaHXLxP0prU*********&client_secret=vzf7U219hrAjIYN70NcFo3VBQzot**********&response_type=token&grant_type=password&scope=email&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fauthorized HTTP/1.1" 302 -
The way I see it, the token is being saved but:-
Redirection does not occur
It cause the client to load like forever until I restart it (Even if I tried to access other routes, the client does not respond)
What am I missing ?
NB:
I've implemented server side flow and client side flow and they worked fine
I'm still new to flask
I think you are mixing different grant types of OAuth2. With the Resource Owner Password Credentials grant, the authorization server does not do a redirect, instead it provides a token response to the client.
https://www.rfc-editor.org/rfc/rfc6749#section-4.3
redirect_uris are associated with the Authorization Code grant.

Categories