Getting 403 response in test script using Django REST and OAuth2 - python

I am getting a 403 response in my test script which uses Django REST and OAuth2. I am using force_authenticate.
In urls.py:
urlpatterns = [
url(r'^user-id/$', views.UserIDView.as_view(), name='user-id'),
...
In views.py:
from oauth2_provider.ext.rest_framework import TokenHasReadWriteScope
class StdPerm(TokenHasReadWriteScope):
pass
StdPermClasses = (IsAuthenticated, StdPerm)
class UserIDView(APIView):
permission_classes = StdPermClasses
renderer_classes = (JSONRenderer,)
def get(self, request, format=None):
return Response({'id': request.user.id})
In tests.py:
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from rest_framework import status
from rest_framework.test import APITestCase
class CreateUserTest(APITestCase):
def setUp(self):
self.user = User.objects.create_user('daniel', 'daniel#test.com',
password='daniel')
self.user.save()
def test_get_user_id(self):
self.client.login(username='daniel', password='daniel')
self.client.force_authenticate(user=self.user)
response = self.client.get(reverse('user-id'))
self.assertEqual(response.status_code, status.HTTP_200_OK)
Usually I use curl which works no problem:
curl -X GET "http://127.0.0.1:8000/user-id/" -H "Authorization: Bearer b3SlzXlpRSxURyh2BltwdHmhrlqNyt"
Update I changed some lines in test_get_user_id:
token = Token.objects.create(user=self.user)
self.client.force_authenticate(user=self.user, token=token)
Now I get the error:
assert False, ('TokenHasScope requires either the' AssertionError:
TokenHasScope requires either the`oauth2_provider.rest_framework
.OAuth2Authentication` authentication class to be used.

I found a solution to this problem. Basically my code was missing two things, namely the OAuth2 application record and an access token specific to OAuth2. I added the following to setUp:
app = Application(
client_type='confidential',
authorization_grant_type='password',
name='MyAppTest',
user_id=1
)
app.save()
...for generating a suitable access token:
app = Application.objects.get(name='MyAppTest')
token = generate_token()
expires = now() + timedelta(seconds=oauth2_settings. \
ACCESS_TOKEN_EXPIRE_SECONDS)
scope = 'read write'
access_token = AccessToken.objects.create(
user=self.user,
application=app,
expires=expires,
token=token,
scope=scope
)
...and then to use the token:
self.client.force_authenticate(user=self.user, token=access_token)
The import section ended up like so:
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from oauth2_provider.settings import oauth2_settings
from oauthlib.common import generate_token
from oauth2_provider.models import AccessToken, Application
from django.utils.timezone import now, timedelta

This worked for me
from oauth2_provider.settings import oauth2_settings
from oauth2_provider.models import get_access_token_model,
get_application_model
from django.contrib.auth import get_user_model
from django.utils import timezone
from rest_framework.test import APITestCase
Application = get_application_model()
AccessToken = get_access_token_model()
UserModel = get_user_model()
class Test_mytest(APITestCase):
def setUp(self):
oauth2_settings._SCOPES = ["read", "write", "scope1", "scope2", "resource1"]
self.test_user = UserModel.objects.create_user("test_user", "test#example.com", "123456")
self.application = Application.objects.create(
name="Test Application",
redirect_uris="http://localhost http://example.com http://example.org",
user=self.test_user,
client_type=Application.CLIENT_CONFIDENTIAL,
authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
)
self.access_token = AccessToken.objects.create(
user=self.test_user,
scope="read write",
expires=timezone.now() + timezone.timedelta(seconds=300),
token="secret-access-token-key",
application=self.application
)
# read or write as per your choice
self.access_token.scope = "read"
self.access_token.save()
# correct token and correct scope
self.auth = "Bearer {0}".format(self.access_token.token)
def test_success_response(self):
url = reverse('my_url',)
# Obtaining the POST response for the input data
response = self.client.get(url, HTTP_AUTHORIZATION=self.auth)
# checking wether the response is success
self.assertEqual(response.status_code, status.HTTP_200_OK)
Now everything will work as expected. Thanks

Related

Shopify webhook request verification for `orders/create` topic returning False in python/django project [duplicate]

How can I verify the incoming webhook from Shopify? Shopify provides a python implementation (of Flask), but how can I do it in Django/DRF?
Set these two variables in the settings.py file
# settings.py
SHOPIFY_HMAC_HEADER = "HTTP_X_SHOPIFY_HMAC_SHA256"
SHOPIFY_API_SECRET = "5f6b6_my_secret"
Then, create a verify webhook function that accepts the Django request as it's parameter
# utils.py
import base64
import hashlib
import hmac
from django.conf import settings
from django.core.handlers.wsgi import WSGIRequest
def verify_shopify_webhook(request: WSGIRequest):
shopify_hmac_header = request.META.get(settings.SHOPIFY_HMAC_HEADER)
encoded_secret = settings.SHOPIFY_API_SECRET.encode("utf-8")
digest = hmac.new(
encoded_secret,
request.body,
digestmod=hashlib.sha256,
).digest()
computed_hmac = base64.b64encode(digest)
return hmac.compare_digest(computed_hmac, shopify_hmac_header.encode("utf-8"))
Then, create a view that accepts the incoming webhook and use the verify_shopify_webhook(...) function to verify the request.
# views.py
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from .utils import verify_shopify_webhook
#method_decorator(csrf_exempt, name="dispatch")
class ShopifyWebhookView(View):
def post(self, request, *args, **kwargs):
verified = verify_shopify_webhook(request=request)
return HttpResponse(status=200 if verified else 403)
If you're using Django REST Framework, you can also use APIView as
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .utils import verify_shopify_webhook
class ShopifyWebhookView(APIView):
def post(self, request, *args, **kwargs):
verified = verify_shopify_webhook(request=request)
return Response(status=200 if verified else 403)

How can I pass in url arguments to an APIRequestFactory put request?

I have been trying for hours but cannot figure out how to pass a url argument through an APIRequestFactory put request. I have tried it through Postman when running my server and the url variable is passed just fine, but when I run it in my tests it stops working.
What I mean is that when I send a Postman PUT request to '/litter/1/' it will successfully take in the 1 as the variable litterId since my url is setup like this
path('litter/', include('apps.litter.urls')),
and
path('<int:litterId>/', LitterView.as_view(), name='litter-with-id')
But when I try and send an APIRequestFactory put request to that same url, for some reason the 1 will not go through as the litterId anymore.
Some relevant pieces of code...
My top level url.py
from rest_framework.authtoken import views
from apps.litter.views import LitterView
urlpatterns = [
path('admin/', admin.site.urls),
path('auth/', include('apps.my_auth.urls')),
path('litter/', include('apps.litter.urls')),
]
This is my app specific urls.py
from .views import LitterView
urlpatterns = [
path('', LitterView.as_view(), name='standard-litter'),
path('<int:litterId>/', LitterView.as_view(), name='litter-with-id'),
]
Here is my views.py
import json
from django.contrib.auth.models import User
from django.db import IntegrityError
from django.views.decorators.csrf import csrf_exempt
from rest_framework import authentication, permissions
from rest_framework.parsers import JSONParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
from django.db import models
from .models import Litter
from .serializers import LitterSerializer
##csrf_exempt
class LitterView(APIView):
"""
View for litter related requests
* Requres token auth
"""
permission_classes = (IsAuthenticated,)
authentication_classes = [authentication.TokenAuthentication]
renderer_classes = [JSONRenderer]
def put(self, request, litterId=0):
"""
Updates an old litter
"""
try:
litterModel = Litter.objects.get(user=request.user, id=litterId)
except Litter.DoesNotExist:
returnData = {'status': 'fail',
'error': 'Could not find object with that id.'}
return Response(returnData)
serializer_class = LitterSerializer
serialized = LitterSerializer(litterModel, data=request.data)
if serialized.is_valid():
litterModel = serialized.save()
returnData = {'status': 'okay',
'litter': [serialized.data]}
return Response(returnData)
else:
return Response(serialized.errors, status=400)
And here is the relevant test.
def test_easy_successful_put_type(self):
"""
Testing a simple put
"""
user = UserFactory()
amount = 40
amountChange = 20
litter = LitterFactory(user=user, amount=amount)
data = {'typeOfLitter': litter.typeOfLitter,
'amount': litter.amount + amountChange,
'timeCollected': litter.timeCollected}
url = '/litter/' + str(litter.id) + '/'
request = self.factory.put(url, data, format='json')
force_authenticate(request, user=user)
view = LitterView.as_view()
response = view(request).render()
responseData = json.loads(response.content)
No matter what I do, I cannot get the int:litterId to get passed in, the put function always has the default value of 0. Any help would be greatly appreciated.
Your problem is here:
response = view(request).render()
You are manually passing the request to the view, also not passing the kwarg litterId, instead use APIClient and make a put request to the url. First import the required modules:
from django.urls import reverse
from rest_framework.test import APIClient
then:
user = UserFactory()
amount = 40
amountChange = 20
litter = LitterFactory(user=user, amount=amount)
data = {
'typeOfLitter': litter.typeOfLitter,
'amount': litter.amount + amountChange,
'timeCollected': litter.timeCollected
}
url = reverse('litter-with-id', kwargs={'litterId': litter.id})
client = APIClient()
client.force_authenticate(user=user)
response = client.put(url, data, format='json')

Mock a test with an external api rest in Django Rest Framework

Context
I am developing an API Rest with Django Rest Framework, and some of my endpoints consume an external API Rest to create a model instance, the code works but right now I am trying to test my code but I have very struggled with that. I am a little beginner developing tests and maybe the question is very obvious but, I tried so many things, with the below links but I couldn't achieve it.
Mocking external API for testing with Python
How do I mock a third party library inside a Django Rest Framework endpoint while testing?
https://realpython.com/testing-third-party-apis-with-mocks/
Codes
This is my view
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from app.lnd_client import new_address
from app.models import Wallet
from api.serializers import WalletSerializer
class WalletViewSet(viewsets.ModelViewSet):
queryset = Wallet.objects.all()
serializer_class = WalletSerializer
lookup_field = "address"
#action(methods=["post"], detail=False)
def new_address(self, request):
response = new_address()
if "error" in response.keys():
return Response(
data={"error": response["error"]}, status=response["status_code"]
)
else:
return Response(response)
def create(self, request, *args, **kwargs):
response = self.new_address(self)
if response.status_code >= 400:
return response
serializer = self.get_serializer(data=response.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
)
This is my client with the call to the external api
import requests
from django.conf import settings
from typing import Dict
endpoint = settings.LND_REST["ENDPOINT"]
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
endpoint = (
"https://" + endpoint if not endpoint.startswith("http") else endpoint
)
endpoint = endpoint
auth = {"Grpc-Metadata-macaroon": settings.LND_REST["MACAROON"]}
cert = settings.LND_REST["CERT"]
def new_address() -> Dict:
try:
r = requests.get(
f"{endpoint}/v1/newaddress", headers=auth, verify=cert
)
r.raise_for_status()
except requests.exceptions.HTTPError as errh:
return {
"error": errh.response.text,
"status_code": errh.response.status_code,
}
except requests.exceptions.RequestException:
return {
"error": f"Unable to connect to {endpoint}",
"status_code": 500,
}
return r.json()
My test is this
from django.urls import reverse
from nose.tools import eq_
from rest_framework.test import APITestCase
from unittest.mock import patch
from rest_framework import status
from .factories import WalletFactory
from app.lnd_client import new_address
class TestWalletListTestCase(APITestCase):
def setUp(self):
WalletFactory.create_batch(5)
self.url = reverse("wallet-list")
self.wallet_data = WalletFactory.generate_address()
def test_get_all_wallets(self):
response = self.client.get(self.url)
eq_(response.status_code, status.HTTP_200_OK)
#This is the problem test
#patch('app.lnd_client.requests.post')
def test_create_wallet(self,mock_method):
mock_method.return_value.ok = True
response = self.client.post(self.url)
eq_(response.status_code, status.HTTP_201_CREATED)
Take a look at httmock. It should do what you're looking for. The example from their docs is:
from httmock import urlmatch, HTTMock
import requests
#urlmatch(netloc=r'(.*\.)?google\.com$')
def google_mock(url, request):
return 'Feeling lucky, punk?'
with HTTMock(google_mock):
r = requests.get('http://google.com/')
print(r.content) # 'Feeling lucky, punk?'
You'll want to use something like:
#urlmatch(path=r"(.*)/newaddress$")
def new_address_url(url, request):
return {...} # stuff
with HTTMock(new_address_url)
# make request to DRF api here.

APIRequestFactory with force_authenticate: GET returns a 404

I'm running an APITestCase using Rest Framework for Django and its APIRequestFactory.
I am able to return a status 200 when I request this api with APIClient, but I get a 404 with APIRequestFactory. I'd like to use APIRequestFactory for it's "black box" effect in testing over APIClient.
I've modeled my test after the REST Framework docs for force_authenticate. It doesn't seem to be related to the issue of a APIRequestFactory returning an HTTPRequest instead of a Request as in this question.
Imports:
from rest_framework.test import APIClient, APITestCase, APIRequestFactory
from myapp.factories import UserFactory
from .views import internalPostListView
Test Using APIClient that successfully GET's (returns status 200):
class PostTests(APITestCase):
#classmethod
def setUpTestData(cls):
cls.user = UserFactory()
cls.client = APIClient()
def test_successful_get_internal_post_list(self):
self.user.is_superuser = True
self.client.force_authenticate(user=self.user)
response = self.client.get('/internal/posts/')
self.assertEquals(200, response.status_code)
Test using APIRequestFactory that can't find the api (returns status 404):
class PostTests(APITestCase):
#classmethod
def setUpTestData(cls):
cls.factory = APIRequestFactory()
cls.user = UserFactory()
def test_successful_get_internal_post_list(self):
self.user.is_superuser = True
view = internalPostListView #Note: internalPostListView = InternalPostListView.as_view()
request = self.factory.get('/internal/posts/')
force_authenticate(request, user = self.user)
response = view(request)
self.assertEquals(200, response.status_code)
It's failing to GET the api, and is not an issue of authentication or permissions. It seems to match the docs to me- what am I missing?

Access GA API with Django: /accounts/login/ redirect issue

I'm working on Django app that will provide to users the information about their Google Analytics accounts (Account ID, Property, View ID).
But I'm stacked on the very beginning trying to resolve the problem with authorization of the user (handling oAuth 2 server response).
I followed the Google documentation on it.
MODELS.PY
from django.db import models
from django.contrib.auth.models import User
from oauth2client.django_orm import FlowField, CredentialsField
class FlowModel(models.Model):
id = models.ForeignKey(User, primary_key=True)
flow = FlowField()
class CredentialsModel(models.Model):
id = models.ForeignKey(User, primary_key=True)
credential = CredentialsField()
VIEWS.PY
import os
import httplib2
from oauth2client import xsrfutil
from oauth2client.client import flow_from_clientsecrets
from oauth2client.django_orm import Storage
from apiclient.discovery import build
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.contrib.sites.models import get_current_site
from .models import CredentialsModel, FlowModel
CLIENT_SECRETS = os.path.join(
os.path.dirname(__file__), 'client_secrets.json')
def get_accounts_ids(service):
accounts = service.management().accounts().list().execute()
ids = []
if accounts.get('items'):
for account in accounts['items']:
ids.append(account['id'])
return ids
#login_required
def index(request):
# use the first REDIRECT_URI if you are developing your app
# locally, and the second in production
REDIRECT_URI = 'http://localhost:8080/oauth2/oauth2callback'
#REDIRECT_URI = "https://%s%s" % (
# get_current_site(request).domain, reverse("oauth2:return"))
FLOW = flow_from_clientsecrets(
CLIENT_SECRETS,
scope='https://www.googleapis.com/auth/analytics.readonly',
redirect_uri=REDIRECT_URI
)
user = request.user
storage = Storage(CredentialsModel, 'id', user, 'credential')
credential = storage.get()
if credential is None or credential.invalid is True:
FLOW.params['state'] = xsrfutil.generate_token(
settings.SECRET_KEY, user)
authorize_url = FLOW.step1_get_authorize_url()
webbrowser.open_new(authorize_url)
f = FlowModel(id=user, flow=FLOW)
f.save()
return HttpResponseRedirect(authorize_url)
else:
http = httplib2.Http()
http = credential.authorize(http)
service = build('analytics', 'v3', http=http)
ids = get_account_ids(service)
return render(
request, 'ga_auth2/main.html', {'ids':ids})
#login_required
def auth_return(request):
user = request.user
if not xsrfutil.validate_token(
settings.SECRET_KEY, request.REQUEST['state'], user):
return HttpResponseBadRequest()
FLOW = FlowModel.objects.get(id=user).flow
credential = FLOW.step2_exchange(request.REQUEST)
storage = Storage(CredentialsModel, 'id', user, 'credential')
storage.put(credential)
return HttpResponseRedirect("/oauth2")
URLS.PY
from django.conf.urls import include, url
from django.contrib import admin
from . import views
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'oauth2callback', views.auth_return, name='return'),
]
The problem is that when I go to http://localhost:8080/oauth2
it's being redirected to http://localhost:8080/accounts/login/?next=/oauth2/
and I see 404 error Page not found.
I defined the API project in Google Developpers Console:
Javascript origins: localhost:8080
Redirect uri: localhost:8080/oauth2/oauth2callback
I imagine the with this redirect I'm pushed to login in my Google account.
But when I execute my Python code from prompt, the browser is being opened automatically asking me login and/or authorize the API access.
How could I handle this issue?
Thanks a lot in advance!
You've protected your index view with login_required, so it's going to redirect you to the login page. If you don't want that, don't use that decorator.

Categories