Django - Turn settings.DEBUG to True for testing Swagger endpint - python

I have this test for checking if I can ping the swagger endpoint
from django.test import SimpleTestCase
from django.test.utils import override_settings
from django.urls import reverse
from rest_framework import status
class SwaggerTest(SimpleTestCase):
#override_settings(DEBUG=True)
def test_successful_swagger_ping(self):
"""
Test to ensure that Swagger can be reached successfully. If this test
throws 5XX that means there's an error in swagger annotation on some view function.
"""
response = self.client.get(reverse('swagger'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
So this test fails, because I only have swagger added to url when settings.DEBUG=True. I thought #override_settings(DEBUG=TRUE) would fix that, but it doesn't. My test passes when I manually set settings.DEBUG=True under my settings.py file, otherwise it throws an error because it can't find the endpoint. I've tried decorating both the class and the function with #override_settings and in both cases it threw the error because it couldn't find the endpoint. I'm not particularly attached to this way of testing to see if Swagger is working. This test is just to check if Swagger isn't returning an error regarding annotation. If anyone knows a better way to test this I'm open to that.
I've also tried
from django.conf import settings
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
class SwaggerTest(TestCase):
#staticmethod
def setUpClass() -> None:
settings.DEBUG = True
super(SwaggerTest, SwaggerTest).setUpClass()
def test_successful_swagger_ping(self):
"""
Test to ensure that Swagger can be reached successfully. If this test
throws 5XX that means there's an error in swagger annotation on some view function.
"""
response = self.client.get(reverse('swagger'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
but that also gives django.urls.exceptions.NoReverseMatch: Reverse for 'swagger' not found. 'swagger' is not a valid view function or pattern name. it only works when I set
test/__init__.py
settings.DEBUG = True

Is it ok to change settings?
Short answer: No, unless you do it during the startup.
Long answer: You should not modify settings at runtime. This means, no settings modifications after the app has been started, like changing configuration in views.py, serializers.py, models.py or other modules you add during the development. But it is OK to modify settings if they depend on local variables if you do it at startup and you are fully aware of what happens.
But this is how I usually override settings in tests:
from django.conf import settings
from django.test import TestCase
class MyTestCase(TestCase): # noqa
#staticmethod
def setUpClass(): # noqa
settings.DEBUG = True
super(ExampleTestCase, ExampleTestCase).setUpClass()
But maybe you should run your tests twice for that?

Related

Django: Can we enforce POST request in DRF without requiring csrftoken? [duplicate]

I know that there are answers regarding Django Rest Framework, but I couldn't find a solution to my problem.
I have an application which has authentication and some functionality.
I added a new app to it, which uses Django Rest Framework. I want to use the library only in this app. Also I want to make POST request, and I always receive this response:
{
"detail": "CSRF Failed: CSRF token missing or incorrect."
}
I have the following code:
# urls.py
from django.conf.urls import patterns, url
urlpatterns = patterns(
'api.views',
url(r'^object/$', views.Object.as_view()),
)
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt
class Object(APIView):
#csrf_exempt
def post(self, request, format=None):
return Response({'received data': request.data})
I want add the API without affecting the current application.
So my questions is how can I disable CSRF only for this app ?
Note: Disabling CSRF is unsafe from security point of view. Please use your judgement to use the below method.
Why this error is happening?
This is happening because of the default SessionAuthentication scheme used by DRF. DRF's SessionAuthentication uses Django's session framework for authentication which requires CSRF to be checked.
When you don't define any authentication_classes in your view/viewset, DRF uses this authentication classes as the default.
'DEFAULT_AUTHENTICATION_CLASSES'= (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication'
),
Since DRF needs to support both session and non-session based authentication to the same views, it enforces CSRF check for only authenticated users. This means that only authenticated requests require CSRF tokens and anonymous requests may be sent without CSRF tokens.
If you're using an AJAX style API with SessionAuthentication, you'll need to include a valid CSRF token for any "unsafe" HTTP method calls, such as PUT, PATCH, POST or DELETE requests.
What to do then?
Now to disable csrf check, you can create a custom authentication class CsrfExemptSessionAuthentication which extends from the default SessionAuthentication class. In this authentication class, we will override the enforce_csrf() check which was happening inside the actual SessionAuthentication.
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
class CsrfExemptSessionAuthentication(SessionAuthentication):
def enforce_csrf(self, request):
return # To not perform the csrf check previously happening
In your view, then you can define the authentication_classes to be:
authentication_classes = (CsrfExemptSessionAuthentication, BasicAuthentication)
This should handle the csrf error.
Easier solution:
In views.py, use django-braces' CsrfExemptMixin and authentication_classes:
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt
from braces.views import CsrfExemptMixin
class Object(CsrfExemptMixin, APIView):
authentication_classes = []
def post(self, request, format=None):
return Response({'received data': request.data})
Modify urls.py
If you manage your routes in urls.py, you can wrap your desired routes with csrf_exempt() to exclude them from the CSRF verification middleware.
import views
from django.conf.urls import patterns, url
from django.views.decorators.csrf import csrf_exempt
urlpatterns = patterns('',
url(r'^object/$', csrf_exempt(views.ObjectView.as_view())),
...
)
Alternatively, as a Decorator
Some may find the use of the #csrf_exempt decorator more suitable for their needs
for instance,
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
#csrf_exempt
def my_view(request):
return HttpResponse('Hello world')
should get the Job Done!
For all who did not find a helpful answer. Yes DRF automatically removes CSRF protection if you do not use SessionAuthentication AUTHENTICATION CLASS, for example, many developers use only JWT:
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
But issue CSRF not set may be occurred from some another reason, for exmple you not correctly added path to you view:
url(r'^api/signup/', CreateUserView), # <= error! DRF cant remove CSRF because it is not as_view that does it!
instead of
url(r'^api/signup/', CreateUserView.as_view()),
I tried a few of the answers above and felt creating a separate class was a little overboard.
For reference, I ran into this problem when trying to update a function based view method to a class based view method for user registration.
When using class-based-views (CBVs) and Django Rest Framework (DRF), Inherit from the ApiView class and set permission_classes and authentication_classes to an empty tuple. Find an example below.
class UserRegistrationView(APIView):
permission_classes = ()
authentication_classes = ()
def post(self, request, *args, **kwargs):
# rest of your code here
If you do not want to use session based authentication, you can remove Session Authentication from REST_AUTHENTICATION_CLASSES and that would automatically remove all csrf based issues. But in that case Browseable apis might not work.
Besides this error should not come even with session authentication. You should use custom authentication like TokenAuthentication for your apis and make sure to send Accept:application/json and Content-Type:application/json(provided you are using json) in your requests along with authentication token.
You need to add this to prevent default session authentication: (settings.py)
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
Then: (views.py)
from rest_framework.permissions import AllowAny
class Abc(APIView):
permission_classes = (AllowAny,)
def ...():
You need to be absolutely sure, that you want to switch off CSRF protection.
Create file authentication.py and place it wherever you want in your project. For example, in folder session_utils.
Place this code in the file:
from rest_framework.authentication import SessionAuthentication
class SessionCsrfExemptAuthentication(SessionAuthentication):
def enforce_csrf(self, request):
pass
When you want to make POST, PUT, PATCH or DELETE requests to your view be sure that you've changed SessionAuthentication to SessionCsrfExemptAuthentication from the new file. View example:
#api_view(["POST"])
#authentication_classes([SessionCsrfExemptAuthentication])
#permission_classes([IsAuthenticated])
def some_view(request) -> "Response":
# some logic here
return Response({})
This trick allow you to override method (pass) enforce_csrf and the new session authentication class will skip CSRF check.
✌️
I am struck with the same problem. I followed this reference and it worked.
Solution is to create a middleware
Add disable.py file in one of your apps (in my case it is 'myapp')
class DisableCSRF(object):
def process_request(self, request):
setattr(request, '_dont_enforce_csrf_checks', True)
And add the middileware to the MIDDLEWARE_CLASSES
MIDDLEWARE_CLASSES = (
myapp.disable.DisableCSRF,
)
My Solution is shown blow. Just decorate my class.
from django.views.decorators.csrf import csrf_exempt
#method_decorator(csrf_exempt, name='dispatch')
#method_decorator(basic_auth_required(
target_test=lambda request: not request.user.is_authenticated
), name='dispatch')
class GenPedigreeView(View):
pass
When using REST API POSTs, absence of X-CSRFToken request header may cause that error.
Django docs provide a sample code on getting and setting the CSRF token value from JS.
As pointed in answers above, CSRF check happens when the SessionAuthentication is used. Another approach is to use TokenAuthentication, but keep in mind that it should be placed first in the list of DEFAULT_AUTHENTICATION_CLASSES of REST_FRAMEWORK setting.
If you are using an exclusive virtual environment for your application, you can use the following approach without effective any other applications.
What you observed happens because rest_framework/authentication.py has this code in the authenticate method of SessionAuthentication class:
self.enforce_csrf(request)
You can modify the Request class to have a property called csrf_exempt and initialize it inside your respective View class to True if you do not want CSRF checks. For example:
Next, modify the above code as follows:
if not request.csrf_exempt:
self.enforce_csrf(request)
There are some related changes you'd have to do it in the Request class
This could also be a problem during a DNS Rebinding attack.
In between DNS changes, this can also be a factor. Waiting till DNS is fully flushed will resolve this if it was working before DNS problems/changes.
For me, using django 3.1.5 and django rest framework 3.12 the solution was way easier.
It happened to me that on a views.py file I had defined this two methods:
#api_view(['POST'])
#permission_classes((IsAuthenticated, ))
def create_transaction(request):
return Response(status=status.HTTP_200_OK)
def create_transaction(initial_data):
pass
On my urls.py:
urlpatterns = [
path('transaction', views.create_transaction, name='transaction'),
]
Django was picking the latest and throwing the error. Renaming one of the two solved the issue.
Code bellow would remove demand for CSRF. Even anon user would be able to send request.
from typing import List, Any
class Object(APIView):
authentication_classes: List = []
permission_classes: List[Any] = [AllowAny]
...
...
Removing CSRF check is not always the only (or best) solution. Actually, it's an important security mechanism for SessionAuthentication.
I was having the same issue when trying to authenticate with JWT and doing a POST request.
My initial setup looked like this:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
"django_cognito_jwt.JSONWebTokenAuthentication",
),
...
}
As SessionAuthentication was checked first in the list, the CSRF error was raised. My solution was as simple as changing the order to always check JWT auth first. Like this:
"DEFAULT_AUTHENTICATION_CLASSES": (
"django_cognito_jwt.JSONWebTokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
At the end, SessionAuthentication for me is only used in the django admin panel and 99% of the requests goes to the API that uses JWT auth.

Flask returns 404 in views

I am running unittests in a Flask app and I keep getting 404 when views.py file is not imported even though it is not used. I have such tests.py package:
import unittest
from presence_analyzer import main, utils
from presence_analyzer import views
class PresenceAnalyzerViewsTestCase(unittest.TestCase):
def setUp(self):
self.client = main.app.test_client()
def test_mainpage(self):
resp = self.client.get('/')
self.assertEqual(resp.status_code, 302)
When I delete views import the described problem occurs. Views are organized in a similar way to this:
from presence_analyzer.main import app
#app.route('/')
def mainpage():
return redirect('/static/presence_weekday.html')
And the main.py file:
import os.path
from flask import Flask
app = Flask(__name__) # pylint: disable=invalid-name
app.config.update(
DEBUG=True,
)
I guess it's something similar to what happened in this case, so I'm trying to change the application so that I don't have to make this dumb imports while testing. I've been trying to make use of the answer from above, but still can't make it work and these docs don't seem helpful. What am I doing wrong? main.py:
from flask.blueprints import Blueprint
PROJECT_NAME = 'presence_analyzer'
blue_print = Blueprint(PROJECT_NAME, __name__)
def create_app():
app_to_create = Flask(__name__) # pylint: disable=invalid-name
app_to_create.register_blueprint(blue_print)
return app_to_create
app = create_app()
views.py:
from presence_analyzer.main import app, blue_print
#blue_print.route('/')
def mainpage():
return redirect('/static/presence_weekday.html')
And tests.py has remained unchanged.
You must import views, or the route will not be registered. No, you are not executing the views directly, but importing executes code all module-level code. Executing code calls route. route registers the view function. You cannot get around needing to import a module in order to use the module.

Pytest and Django settings runtime changes

I have a receiver that needs to know whether DEBUG set to True in my settings.py.
from django.conf import settings
...
#receiver(post_save, sender=User)
def create_fake_firebaseUID(sender, instance, created=False, **kwargs):
# Fake firebaseUID if in DEBUG mode for development purposes
if created and settings.DEBUG:
try:
instance.userprofile
except ObjectDoesNotExist:
UserProfile.objects.create(user=instance, firebaseUID=str(uuid.uuid4()))
The problem is that when I create a user using manage.py shell everything works as expected. However, if I run my tests via py.test, the value of settings.DEBUG changes to False. If I check it in conftest.py in pytest_configure, DEBUG is set to True. It changes somewhere later and I have no idea where.
What can cause this? I am sure that I do not change it anywhere in my code.
Edit.
conftest.py
import uuid
import pytest
import tempfile
from django.conf import settings
from django.contrib.auth.models import User
#pytest.fixture(scope='session', autouse=True)
def set_media_temp_folder():
with tempfile.TemporaryDirectory() as temp_dir:
settings.MEDIA_ROOT = temp_dir
yield None
def create_normal_user() -> User:
username = str(uuid.uuid4())[:30]
user = User.objects.create(username=username)
user.set_password('12345')
user.save()
return user
#pytest.fixture
def normal_user() -> User:
return create_normal_user()
#pytest.fixture
def normal_user2() -> User:
return create_normal_user()
myapp/tests/conftest.py
# encoding: utf-8
import os
import pytest
from django.core.files.uploadedfile import SimpleUploadedFile
from userprofile.models import ProfilePicture
#pytest.fixture
def test_image() -> bytes:
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(DIR_PATH, 'test_image.jpg'), 'rb') as f:
yield f
#pytest.fixture
def profile_picture(test_image, normal_user) -> ProfilePicture:
picture = SimpleUploadedFile(name='test_image.jpg',
content=test_image.read(),
content_type='image/png')
profile_picture = ProfilePicture.objects.get(userprofile__user=normal_user)
profile_picture.picture = picture
profile_picture.save()
return profile_picture
pytest.ini
[pytest]
addopts = --reuse-db
DJANGO_SETTINGS_MODULE=mysite.settings
Apparently pytest-django explicitly sets DEBUG to False (source code link).
Diving through the git history of pytest-django a bit, this was done to match Django's default behavior (pytest commit link).
From the Django docs:
Regardless of the value of the DEBUG setting in your configuration file, all Django tests run with DEBUG=False. This is to ensure that
the observed output of your code matches what will be seen in a
production setting.
As a workaround you can use pytest-django's settings fixture to override so DEBUG=True if you need it to be. For example,
def test_my_thing(settings):
settings.DEBUG = True
# ... do your test ...
For anyone who is having similar problem. I found the reason. I downloaded source files of pytest-django and found out that it sets DEBUG to False in pytest-django/pytest_django/plugin.py:338. I do not know why tho.
Add the following line in the pytest.ini file:
django_debug_mode = True

Given a URL how to find a view callable in pyramid?

I want to write unit test, that proves that correct url will resolve to my view callable.
In this question Get Pyramid View callable by it's path (request context) I found the way to do it, but my test fails, probably that is because self.testing.scan() hasn't been run?
How can I reliably test that application will find correct view callable?
import unittest
from pyramid import testing
class ViewTests(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
def tearDown(self):
testing.tearDown()
# This one fails
def test_root_path_points_to_correct_view_callable(self):
from ..views import my_view
from pyramid.scripts.pviews import PViewsCommand
views_command = PViewsCommand([])
view = views_command._find_view(testing.DummyRequest(path='/'))
assert my_view is view
# This one passes
def test_my_view(self):
from ..views import my_view
request = testing.DummyRequest()
info = my_view(request)
self.assertEqual(info['project'], 'my_project')

Django unit test; Login using python-social-auth

I would like to write unit tests for my Django app that is using python-social-auth.
It all works great when running Django and using a browser, thanks python-social-auth!
However, I can't seem to write unit tests because I can't create an authenticated client to test with.
Has anyone done so successfully?
How did you get an authenticated client()?
I have tried this (the login returns false and does not work):
self.c = Client()
self.u = User.objects.create(username="testuser", password="password", is_staff=True, is_active=True, is_superuser=True)
self.u.save()
self.auth = UserSocialAuth(user=self.u, provider="Facebook")
self.auth.save()
self.c.login(username=self.u.username, password=self.u.password)
Got it:
My mistake was thinking that it mattered how that Client got authenticated, for unit testing the views/endpoints oauth really doesn't need to come into play at all.
this worked for me:
self.user = User.objects.create(username='testuser', password='12345', is_active=True, is_staff=True, is_superuser=True)
self.user.set_password('hello')
self.user.save()
self.user = authenticate(username='testuser', password='hello')
login = self.c.login(username='testuser', password='hello')
self.assertTrue(login)
I have found a workaround to the issue by using the django.test.Client.force_login() method instead. With it, you need to fetch a user from the database, whose data is probably stored in a fixture, and specify the authentication backend in the second argument.
Here's the code I've used:
from random import sample
class SubscribeTestCase(TestCase):
fixtures = (
"auth.User.json", "social_django.UserSocialAuth.json",
"<myapp>.CustomProfileUser.json", "<myapp>.SubscriptionPlan.json"
)
def test_user_logged_in(self):
users = User.objects.all()
user = sample(list(users), 1)[0]
# This isn't actually used inside this method
social_user = user.social_auth.get(provider="auth0")
self.client.force_login(
user, "django.contrib.auth.backends.ModelBackend"
)
response = self.client.get(
reverse("<myappnamespace>:subscribe")
)
print(response.content)
# Looking for a way to fetch the user after a
# response was returned? Seems a little hard, see below
I am not sure how you can access a user in a Django unit test scenario after having received a Response object, which as the documentation observes is not the same as the usual HttpResponse used in production environments. I have done a quick research and it does look like developers aren't intended to do that. In my case I didn't need that so I didn't dig deeper.
If one checks Python Social Auth - Django source code, one will see a file in social-app-django/tests/test_views.py that contains an example to test authenticating the user with Facebook.
from unittest import mock
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractBaseUser
from django.test import TestCase, override_settings
from django.urls import reverse
from social_django.models import UserSocialAuth
from social_django.views import get_session_timeout
#override_settings(SOCIAL_AUTH_FACEBOOK_KEY='1',
SOCIAL_AUTH_FACEBOOK_SECRET='2')
class TestViews(TestCase):
def setUp(self):
session = self.client.session
session['facebook_state'] = '1'
session.save()
def test_begin_view(self):
response = self.client.get(reverse('social:begin', kwargs={'backend': 'facebook'}))
self.assertEqual(response.status_code, 302)
url = reverse('social:begin', kwargs={'backend': 'blabla'})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
#mock.patch('social_core.backends.base.BaseAuth.request')
def test_complete(self, mock_request):
url = reverse('social:complete', kwargs={'backend': 'facebook'})
url += '?code=2&state=1'
mock_request.return_value.json.return_value = {'access_token': '123'}
with mock.patch('django.contrib.sessions.backends.base.SessionBase'
'.set_expiry', side_effect=[OverflowError, None]):
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/accounts/profile/')
To use with another social backend is relatively straightforward; simply substitute facebook with the one one is using.
Also, note that the example doesn't consider partial pipelines but the code can be adjusted to consider them too.
Note:
Agree that their docs could be improved.

Categories

Resources