testing stripe on-boarding django with mock - django

i am having trouble trying to mock test the on-boarding process of stripe connect. I am just learning how to use mock and i am struggling with the StripeAuthorizeCallbackView. the process is as follows: A user reaches the StripeAuthorizeView which sends them to the stripe api to sign up for an account. Once they successfully sign up for an account their redirected back to my platform and stripe sends a temporary code which i then send back to stripe with my api keys. Once i have sent the information back to stripe they then return me credentials for the user being the stripe_user_id.
Here is the two views in question:
import urllib
import requests
class StripeAuthorizeView(LoginRequiredMixin, View):
def get(self, request):
url = 'https://connect.stripe.com/express/oauth/authorize?'
user = self.request.user
if user.account_type == 'Business':
business_type = 'company'
else:
business_type = 'individual'
params = {
'response_type': 'code',
'scope': 'read_write',
'client_id': settings.STRIPE_CONNECT_CLIENT_ID,
'redirect_uri': f'http://127.0.0.1:8000/accounts/stripe/oauth/callback',
'stripe_user[email]' : user.email,
'stripe_user[business_type]' : business_type,
'stripe_user[url]' : 'http://127.0.0.1:8000/accounts/user/%s/' %user.pk,
}
url = f'{url}?{urllib.parse.urlencode(params)}'
return redirect(url)
lass StripeAuthorizeCallbackView(LoginRequiredMixin, View):
def get(self, request):
code = request.GET.get('code')
if code:
data = {
'client_secret': settings.STRIPE_SECRET_KEY,
'grant_type': 'authorization_code',
'client_id': settings.STRIPE_CONNECT_CLIENT_ID,
'code': code
}
url = 'https://connect.stripe.com/oauth/token'
resp = requests.post(url, params=data)
stripe_user_id = resp.json()['stripe_user_id']
stripe_access_token = resp.json()['access_token']
stripe_refresh_token = resp.json()['refresh_token']
user = self.request.user
user.stripe_access_token = stripe_access_token
user.stripe_user_id = stripe_user_id
user.stripe_refresh_token = stripe_refresh_token
user.save()
notify.send(sender=user, recipient=user,
verb='You have succesfully linked a stripe account. You can now take payments for sales.',
level='info')
redirect_url = reverse('account', kwargs={'pk': user.pk})
response = redirect(redirect_url)
return response
else:
user = self.request.user
notify.send(sender=user, recipient=user,
verb='Your attempt to link a stripe account failed. Please contact customer support.',
level='warning')
url = reverse('account', kwargs={'pk': user.pk})
response = redirect(url)
return response
I am not very worried about testing the StripeAuthorizeView a lot. I am more trying to figure out how to test the StripeAuthorizeCallbackView. All i can figure out is that i will need to mock both the code returned and then mock the following requests.post. This test is important to confirm my platform is linking the users credentials after the on-boarding process. Any help on this will be greatly appricated.
edit:
So far i have the following :
#classmethod
def setUpTestData(cls):
cls.test_user = User.objects.create_user(
password='test',
full_name='test name',
email='test#test.com',
address='1 test st',
suburb='test',
state='NSW',
post_code='2000',
contact_number='0433335333' )
#patch('requests.get')
def test_authorizecallback_creates_stripe_details(self, get_mock):
code = requests.get('code')
user = self.test_user
self.client.login(email='test#test.com', password='test')
mocked = ({'stripe_user_id' : '4444','stripe_access_token' : '2222',
'stripe_refresh_token' : '1111' })
with mock.patch('requests.post', mock.Mock(return_value=mocked)):
response = self.client.get('/accounts/stripe/oauth/callback/',
{'code' : '1234'})
self.assertEqual(user.stripe_access_token, '222')
message = list(response.context.get('messages'))[0]
however i keep getting:
File "C:\Users\typef\Desktop\Projects\python_env\fox-listed\Fox-Listed\fox-listed\user\views.py", line 142, in get
stripe_user_id = resp.json()['stripe_user_id']
AttributeError: 'dict' object has no attribute 'json'
the actual response that the StripeAuthorizeCallBackView gives is:
{'access_token': 'sk_test_1KyTG74Ouw65KYTR1O03WjNA00viNjcIfO', 'livemode': False, 'refresh_token': 'rt_H3Vrhd0XbSH7zbmqfDyMNwolgt1Gd7r4ESBDBr5a4VkCzTRT', 'token_type': 'bearer', 'stripe_publishable_key': 'pk_test_**********', 'stripe_user_id': 'acct_1GVOpAF7ag87i2I6', 'scope': 'express'}

Looks like i got it, if there is a flaw here let me know but here is what i have:
class TestStripeAuthorizeCallbackView:
#patch('user.views.requests')
def test_authorizecallback_creates_stripe_details(self, requests_mock):
json = { 'stripe_user_id' : '4444', 'access_token' : '2222', 'refresh_token' : '1111'}
requests_mock.post.return_value.json.return_value = json
user = mixer.blend('user.CustomUser', stripe_user_id=None, access_token=None, refresh_token=None)
req = RequestFactory().get('/', data={'code' : '1234'})
middleware = SessionMiddleware()
middleware.process_request(req)
req.session.save()
messages = FallbackStorage(req)
setattr(req, '_messages', messages)
req.user = user
resp = StripeAuthorizeCallbackView.as_view()(req)
assert resp.status_code == 302 ,'should redirect to success url'
assert user.stripe_user_id == '4444', 'should assign stripe_user_id to user'
assert user.stripe_access_token == '2222', 'should assign an access_token'
assert user.stripe_refresh_token == '1111', 'should assign a refresh_token'

What you're describing isn't mocking so much as it is end-to-end testing, connecting actual test accounts, which you can do.
As long as you're using a test client_id then when you are redirected to Stripe to create the account you can skip the form via a link and get directed back to your site with a real (test mode) oauth code.
Essentially you can set this up and actually go through the flow to create & connect new disposable test Stripe accounts.

Related

Access a view which is for logged in users in django testing

Im trying to create some tests for my django project. I get multiple errors when trying to do the views tests. Most of my views depend on a user to be logged in and I cant find the way to log in . Im using the deafult django built-in AUTH system.
VIEW :
#login_required
def fields(request):
if request.user.profile.user_package == "Livestock":
raise PermissionDenied()
field_list = Field.objects.filter(user = request.user)
context = {
"title": "Fields",
"field_list" : field_list,
}
template = 'agriculture/fields.html'
return render(request, template, context)
TetCase:
class TestViews(TestCase):
#classmethod
#factory.django.mute_signals(signals.pre_save, signals.post_save, signals.pre_delete, signals.post_delete)
def setUpTestData(cls):
# Create new user
test_user = User.objects.create(username='test_user',password='1XISRUkwtuK')
test_user.save()
c = Client()
profile = Profile.objects.get_or_create(user = test_user, user_package = 'hybrid')
c.login(username = test_user.username, password = test_user.password)
Field.objects.create(user=test_user,friendly_name='Arnissa')
def test_logged_in_user(self):
login = self.client.login(username='test_user', password='1XISRUkwtuK')
response = self.client.get(reverse('agriculture:fields'))
# Check our user is logged in
self.assertEqual(str(response.context['user']), 'test_user')
# Check that we got a response "success"
self.assertEqual(response.status_code, 200)
path: path('fields', views.fields, name='fields')
and settings if they provide any help :
LOGIN_REDIRECT_URL = 'dashboard:index'
LOGOUT_REDIRECT_URL = 'login'
LOGIN_URL = 'login'
On my tests i get the error TypeError: 'NoneType' object is not subscriptable when im checking if user is logged in. If i try to get the response I get AssertionError: 302 != 200.
When creating User in Django you should not use create method of user manager, because that sets the password to plain text instead of enrypting it. Try creating user using create_user method, like that:
test_user = User.objects.create_user(username='test_user',password='1XISRUkwtuK')
And if you don't want to use this method, you can always use force_login client method to login user without having to specify login nor password like that:
c.force_login(test_user)

PyTest: How to add X-BULK-OPERATION with HTTP_AUTHORIZATION for session credentials (headers)

I'm writing test for Django Rest Framework with PyTest. There was a problem with headers X-BULK-OPERATION because I can't add X-BULK-OPERATION with HTTP_AUTHORIZATION on header.
my code
#pytest.fixture(scope="module")
def session(django_db_setup, django_db_blocker):
with django_db_blocker.unblock():
user = User.objects.get(email=TEST_EMAIL)
user.set_password(TEST_EMAIL)
user.save(update_fields=["password"])
client = APIClient()
url = reverse("login_sys:login")
response = client.post(url, {
"email": TEST_EMAIL,
"password": TEST_EMAIL
})
client.credentials(**{"X-CSRFToken": response.cookies['csrftoken'].value})
yield client
client.logout()
#pytest.fixture(scope='module')
def choose_crm(session):
crm = Crm.objects.get()
url = reverse("crm:set-jwt", kwargs={"pk": crm.id})
response = session.get(url)
session.credentials(HTTP_AUTHORIZATION=response.data.get("token", None),
**{"X-BULK-OPERATION": True})
yield session
session.logout()
error
{'detail': "Header 'X-BULK-OPERATION' should be provided for bulk operation."}
Debug:print request, as you can see I have Bulk-Operation in header but still gives the error no BULK-OPERATION in header
'HTTP_AUTHORIZATION': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjcm1fdXNlciI6MSwiY3JtX2lkIjoxLCJncm91cF9pZCI6MSwiZGVhbF9wZXJtIjp7ImNyZWF0ZSI6MSwiZGlzcGxheSI6MSwiZWRpdCI6MSwiZGVsZXRlIjoxLCJleHBvcnQiOjF9LCJjb250YWN0X3Blcm0iOnsiY3JlYXRlIjoxLCJkaXNwbGF5IjoxLCJlZGl0IjoxLCJkZWxldGUiOjEsImV4cG9ydCI6MX0sImxlYWRfcGVybSI6eyJjcmVhdGUiOjEsImRpc3BsYXkiOjEsImVkaXQiOjEsImRlbGV0ZSI6MSwiZXhwb3J0IjoxfSwidGFza19wZXJtIjp7ImNyZWF0ZSI6MSwiZGlzcGxheSI6MSwiZWRpdCI6MSwiZGVsZXRlIjoxLCJleHBvcnQiOjF9fQ.Wrd89a8jpRnNu1XhNsOKFAkNQR6v_Y_X0nKySocMtLM', 'HTTP_X-BULK-OPERATION': True}

Why HttpResponseRedirect.set_cookie is not working when i use in django project?

When I use Google OAuth to verify my user, After verify is passed, I want to redirect to the page which user visit before authority, So I want to save the page path to user's cookie, so I implementation like this:
def get_login_resp(request, redirect):
print(redirect)
auth_url = "https://accounts.google.com/o/oauth2/auth?" + urlencode({
"client_id": GOOGLE_CLIENT_ID,
"response_type": "code",
"redirect_uri": make_redirect_url(request, redirect),
"scope": "profile email",
"max_auth_age": 0
})
resp = HttpResponseRedirect(auth_url)
max_age = 3600 * 24
expires = datetime.strftime(datetime.utcnow() + timedelta(seconds=max_age), "%a, %d-%b-%Y %H:%M:%S GMT")
print(expires)
resp.set_cookie('google_auth_redirect', redirect, max_age=max_age, expires=expires,
domain=LOGIN_COOKIE_DOMAIN, secure=True, httponly=True)
print(resp._headers)
print(resp.cookies)
return resp
ps: redirect is the page path which I want to save
But when request the login url with Postman, I can only see this headers:
response headers
And these cookies:
Cookies
So how can i do with this problem? There is not any error info for me.
Try every methods to find out what's wrong, But still failed.
So I try to run server on an other machine(a Linux server), it works!!!
BTW: My develop PC is Macbook Pro 15-inch, 2017 with macOS High Sierra 10.13.1
Update at 14/Jan/2020:
Didn't find the root cause, but I solved this issue by saving redirect_url to session data, in this solution you should check auth valid by using another request, then call google auth to reauth again, code like below:
class GoogleAuthView(RedirectView):
# google auth view
def get(self, request, *args, **kwargs):
# get redirect url from url params, frontend code should pass the param in request url
redirect_url = request.GET.get('redirect_url', None)
if redirect_url:
redirect_url = parse.unquote(redirect_url)
credentials = request.session.get("credentials", None)
if (not credentials) or ('expire_time' not in credentials) or (credentials['expire_time'] < time.time()):
request.session['redirect_url'] = redirect_url # if need google auth, save redirect url to session first
else:
if redirect_url:
return HttpResponseRedirect(redirect_url)
flow = google_auth_oauthlib.flow.Flow.from_client_config(
client_config=settings.GOOGLE_AUTH_CONFIG,
scopes=settings.GOOGLE_AUTH_SCOPES
)
flow.redirect_uri = settings.GOOGLE_AUTH_CONFIG['web']['redirect_uris'][0]
authorization_url, state = flow.authorization_url(
access_type='offline',
include_granted_scopes='true'
)
request.session['state'] = state
return HttpResponseRedirect(authorization_url)
class GoogleAuthCallBackView(BasicView):
# google callback view
def get(self, request, *args, **kwargs):
state = request.session.get('state')
flow = google_auth_oauthlib.flow.Flow.from_client_config(
client_config=settings.GOOGLE_AUTH_CONFIG,
scopes=settings.GOOGLE_AUTH_SCOPES,
state=state
)
flow.redirect_uri = settings.GOOGLE_AUTH_CONFIG['web']['redirect_uris'][0]
# get redirect url from session data if exists
redirect_url = request.session.get('redirect_url') or settings.ADMIN_LOGIN_REDIRECT_URL
response = HttpResponseRedirect(redirect_url)
try:
del request.session['redirect_url']
except KeyError:
logger.info('Delete `redirect_url` in session get KeyError.')
pass
try:
flow.fetch_token(authorization_response=request.build_absolute_uri())
except Exception as e:
logger.error(e.message)
return response
# save credentials to session
credentials = flow.credentials
request.session["credentials"] = {
'token': credentials.token,
'refresh_token': credentials.refresh_token,
'token_uri': credentials.token_uri,
'client_id': credentials.client_id,
'client_secret': credentials.client_secret,
'scopes': credentials.scopes,
'expire_time': time.time() + TOKEN_EXPIRE_TIME,
}
profile_client = googleapiclient.discovery.build(
serviceName='oauth2',
version='v2',
credentials=credentials
)
profile = profile_client.userinfo().v2().me().get().execute()
email = profile['email']
user = user_manager.get_user_by_email(email)
if user:
user.username = profile['name'] # sync username from google
user.picture = profile['picture'] # sync avatar from google
user.save()
request.session["user"] = user.to_dict()
else:
return HttpResponseRedirect("/api/non_existent_user/") # show non-existent user
return response

Why i am getting 400 Bad Request error when sending json data in Flask?

I am trying to write a small restful api application, i am using Chrome Postman extension for sending requests to the app .
I believe that my code does not have mistakes but every time i am sending post request a 400 Bad Request error raising , here is my code:
#api_route.route('/api', methods=['GET'])
def api():
return jsonify({'message':'Api v1.0'})
#api_route.route('/api', methods=['POST'])
def create_user():
data = request.get_json()
if data:
hashed_password = generate_password_hash(data['password'], method='sha256')
api = Api(email=data['email'], password=hashed_password)
db.session.add(api)
db.session.commit()
return jsonify({'message', 'New User Created!'})
The json data that i am sending looks like this:
{"email" : "Test", "password" : "123123123"}
Why i am getting the 400 error ??
Update:
Screenshots for the requests using Postman:
GET Request
POST Request
Here i am initiating api route inside api controller :
from flask import Blueprint
api_route = Blueprint(
'api',
__name__
)
from . import views
then i am registering it inside def create_app() function :
from .api import api_route
app.register_blueprint(api_route)
Here are the extensions that i am using in my application:
toolbar = DebugToolbarExtension()
assets_env = Environment()
cache = Cache()
moment = Moment()
htmlminify = HTMLMIN()
csrf = CSRFProtect()
jac = JAC()
googlemap = GoogleMaps()
session = Session()
principal = Principal()
I solved the problem, i've initiated CSRFProtect with app so i need to include X-CSRFToken in all my requests, so i have two choices:
1 - To include the csrf_token in request.headers for all the requests
2 - Using #csrf.exempt decorator that coming with flask_wtf.csrf
For now i am using #csrf.exempt, so it become like this:
#api_route.route('/api', methods=['GET','POST'])
#csrf.exempt
def create_user():
if request.method == 'GET':
return jsonify({'message' : 'API v1.0'})
elif request.method == 'POST':
data = request.get_json()
hashed_password = generate_password_hash(data['password'], method='sha256')
new_user_api = Api(email=data['email'], password=hashed_password)
db.session.add(new_user_api)
db.session.commit()
return jsonify({'message' : 'New user created!'})
return return jsonify({'message' : 'No user has been added!'})
Thanks for #MrPyCharm for his interests , salute :) .
A good approach would be to structure your views as follows:
Instead of creating view with same route for different request methods, you can handle the request methods in the same view:
#api_route.route('/api', methods=['GET', 'POST'])
def api():
if request.method == 'GET':
return jsonify({'message':'Api v1.0'})
else:
data = request.get_json(force=True)
if data:
hashed_password = generate_password_hash(data['password'], method='sha256')
api = Api(email=data['email'], password=hashed_password)
db.session.add(api)
db.session.commit()
return jsonify({'message': 'New User Created!'})
# Just in case the if condition didn't satisfy
return None
A note for anyone else experiencing this with PostMan and Flask - you will also hit a HTTP 404 if your URL in PostMan is HTTPS but your Flask app only handles HTTP.

Testing Django 1-5 Reset Password Form - how to generate the token for the test?

With the following test, the token is not recognised as valid. In my manual test, it's working so I'm missing something in the way the password is generated I guess.
def test_actual_reset_password(self):
new_password = "myNewPassword012*"
token_generator = PasswordResetTokenGenerator()
user = UserFactory.create()
token = token_generator.make_token(user=user)
response = self.assert_page_loading(path="/forgot-password/reset/{0}/".format(token))
print response
# That loads the page with the error message mentioning that the token was already used
# So I cannot carry on:
form = response.form
form['new_password1'] = new_password
form['new_password2'] = new_password
response = form.submit()
In the django source code, in the PasswordResetForm, I've found this code; I can't see what the difference is:
def save(self, ..., token_generator=default_token_generator, ...):
"""
Generates a one-use only link for resetting password and sends to the
user.
"""
...
for user in self.users_cache:
...
c = {
...
'token': token_generator.make_token(user),
...
}
...
send_mail(subject, email, from_email, [user.email])
Ok, I was just searching for info on how to do this and your question prompted me to figure it out myself. I'm not sure if you're still working on this, but here's how I got it to work:
from django.core import mail
# First we get the initial password reset form.
# This is not strictly necessary, but I included it for completeness
response = self.c.get(reverse('password_reset'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template_name, 'authentication/password_reset_form.html')
# Then we post the response with our "email address"
response = self.c.post(reverse('password_reset'),{'email':'fred#home.com'})
self.assertEqual(response.status_code, 302)
# At this point the system will "send" us an email. We can "check" it thusly:
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'Password reset on example.com')
# Now, here's the kicker: we get the token and userid from the response
token = response.context[0]['token']
uid = response.context[0]['uid']
# Now we can use the token to get the password change form
response = self.c.get(reverse('password_reset_confirm', kwargs={'token':token,'uidb64':uid}))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template_name, 'authentication/password_reset_confirm.html')
# Now we post to the same url with our new password:
response = self.c.post(reverse('password_reset_confirm',
kwargs={'token':token,'uidb36':uid}), {'new_password1':'pass','new_password2':'pass'})
self.assertEqual(response.status_code, 302)
And that's it! Not so hard after all.
This is how I did it for a functional test:
def test_password_reset_from_key(self):
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import base36_to_int, int_to_base36
user = User.objects.all()[:1].get()
token = default_token_generator.make_token(user)
self.get("/accounts/password/reset/key/%s-%s/" % (int_to_base36(user.id), token))
self.selenium.find_element_by_name("password1").send_keys("password")
self.selenium.find_element_by_name("password2").send_keys("password")
self.selenium.find_element_by_name("action").submit()
alert = self.selenium.find_element_by_css_selector(".alert-success")
self.assertIn('Password successfully changed.', alert.text)