I'm trying to write a unit test that posts a mock event to my stripe webhook.
I went and pulled an event from my logs and tried sending that with the test mode enabled, but I (somewhat predictably) got an error:
a similar object exists in live mode, but a test mode key was used to make this request.
Fair enough. So how do I create a mock event that I can actually send to my webhook and get it processed correctly?
Here's my current test:
class StripeTest(TestCase):
def setUp(self):
self.client = Client()
def test_receiving_a_callback(self):
with open('donate/test_assets/stripe_event.json', 'r') as f:
stripe_event = simplejson.load(f)
self.client.post('/donate/callbacks/stripe/',
data=simplejson.dumps(stripe_event),
content_type='application/json')
The solution is to create your own mock data. In the code below we create a test payment by creating a stripe token, then submitting it via the front end (at the /donate/ endpoint).
Once the front end has worked properly, you can get the event from stripe and then send it to your development machine's webhook endpoint.
This is more work than I expected, and I don't love that my tests are hitting the network, but it seems to be a decent solution. I feel a lot more confident about my payments than before.
def test_making_a_donation_and_getting_the_callback(self):
"""These two tests must live together because they need to be done sequentially.
First, we place a donation using the client. Then we send a mock callback to our
webhook, to make sure it accepts it properly.
"""
stripe.api_key = settings.STRIPE_SECRET_KEY
# Create a stripe token (this would normally be done via javascript in the front
# end when the submit button was pressed)
token = stripe.Token.create(
card={
'number': '4242424242424242',
'exp_month': '6',
'exp_year': str(datetime.today().year + 1),
'cvc': '123',
}
)
# Place a donation as an anonymous (not logged in) person using the
# token we just got
r = self.client.post('/donate/', data={
'amount': '25',
'payment_provider': 'cc',
'first_name': 'Barack',
'last_name': 'Obama',
'address1': '1600 Pennsylvania Ave.',
'address2': 'The Whitehouse',
'city': 'DC',
'state': 'DC',
'zip_code': '20500',
'email': 'barack#freelawproject.org',
'referrer': 'footer',
'stripeToken': token.id,
})
self.assertEqual(r.status_code, 302) # 302 because we redirect after a post.
# Get the stripe event so we can post it to the webhook
# We don't know the event ID, so we have to get the latest ones, then filter...
events = stripe.Event.all()
event = None
for obj in events.data:
if obj.data.object.card.fingerprint == token.card.fingerprint:
event = obj
break
self.assertIsNotNone(event, msg="Unable to find correct event for token: %s" % token.card.fingerprint)
# Finally, we can test the webhook!
r = self.client.post('/donate/callbacks/stripe/',
data=simplejson.dumps(event),
content_type='application/json')
# Does it return properly?
self.assertEqual(r.status_code, 200)
Related
I am trying to use data saved in django session variables to run a function once the webhook has confirmed that 'checkout.session.completed' but I always get a key error. I am 100% sure the keys exist in the session variables.
Here is my webhook:
#csrf_exempt
def stripe_webhook(request):
# You can find your endpoint's secret in your webhook settings
endpoint_secret = 'secret'
payload = request.body
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
event = None
try:
event = stripe.Webhook.construct_event(
payload, sig_header, endpoint_secret
)
except ValueError as e:
# Invalid payload
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return HttpResponse(status=400)
# Handle the checkout.session.completed event
if event['type'] == 'checkout.session.completed':
session = event['data']['object']
fulfull_order(session)
return HttpResponse(status=200)
Here is my fulfill order function:
def fulfull_order(session):
generator = PlanMaker(goal=request.session['goal'], gender=request.session['gender'])
/// send email code.
This line generator = PlanMaker(goal=request.session['goal'], gender=request.session['gender'])
Always gives a key error on request.session['goal'] The key definitely exists, it just seems it is inaccessible from the webhook view.
How to solve?
You should save the information you want to the metadata field when creating the checkout.Session.
def checkout(request):
session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=[{
'price': 'price_key',
'quantity': 1,
}],
mode='payment',
success_url=request.build_absolute_uri(reverse('success_url')) + '?session_id={CHECKOUT_SESSION_ID}',
cancel_url=request.build_absolute_uri(reverse('cancel_url')),
metadata={'someKeyHere': 'your session variable data'}
)
return JsonResponse({
'session_id' : session.id,
'stripe_public_key' : settings.STRIPE_PUBLISHABLE_KEY
})
then you can access the information like session['metadata']['someKeyHere']
The webhook event is a separate request coming directly from Stripe that would not be related to any Django session and so this lack of session data would seem expected. As #Anthony suggests you can store this information in the Checkout Session metadata when you create the session. The metadata will be included in the webhook object.
Update: Solved my own problem: I've learnt that Django creates its own test database, and as such, it needs to be populated with data. Ran my importer in my test cases and it all worked. So, if you're also wondering why you're tests don't work, check that you've got some data in the test db!
End Update
I am writing tests for my Django Rest Framework API but I am struggling to get my code to return a 200 OK. At the moment, my test case continually returns a 404 Not Found.
I'm in the early stages of writing tests, and have a lot to learn. I'm currently following https://www.django-rest-framework.org/api-guide/testing/
I'm trying to test an endpoint at the following URL
# Not shown here, is that all URLs here will be prepended with /api/v1
path('case/<int:pk>/', EntireCaseView.as_view(), name='case'),
I have an object in my database with an ID (primary key) of 1. I can successful query the API by going to http://localhost:8000/api/v1/case/1/
I receive a valid JSON response (Trampe is a rabbit)
{
"id": 1,
"total_points": 5000,
"passing_points": 3700,
"budget": 5000,
"description": "Saving Trampe from Trauma",
"name": "Trampe",
"signalment": "8yr, intact male, mixed breed.",
"problem": "Respiratory difficulty",
"image": {
"id": 1,
"file": "http://localhost:8000/media/images/trampe.jpg",
"description": "A lovely picture of Trampe"
},
My API requires authentication, and as such I am providing authentication in my test case.
class CaseTests(APITestCase):
def test_status_code(self):
"""
ensure that case/1 returns 200 OK
"""
# Create a test user
test_user = User(username='jim', password='monkey123', email='jim#jim.com')
test_user.save()
# build a factory and get our user Jim
factory = APIRequestFactory()
user = User.objects.get(username='jim')
# Get our view to test and the url, too
view = EntireCaseView.as_view()
url = reverse('case', kwargs={'pk': '1'})
print(url.__str__())
# Make an authenticated request to the view...
request = factory.get(url)
print(request.get_full_path())
force_authenticate(request, user=user)
response = view(request, "1")
print(response.data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
Of interest here (at least to me) are the lines
url = reverse('case', kwargs={'pk': '1'})
and
response = view(request, "1")
If I leave out either the kwargs argument in url =r everse('case', kwargs={'pk': '1'}) or the "1" in response = view(request, "1") I will receive an error saying that the get() method requires 2 positional arguments but only given.
Here is the signature of the get() method in my view.
class EntireCaseView(APIView):
def get(self, request, pk):
If I run my test, Django reports that it fails because of a 404.
self.assertEqual(response.status_code, status.HTTP_200_OK)
AssertionError: 404 != 200
What I am trying to work out is why this is the case. Printing print(url.__str__()) outputs /api/v1/case/1/ as does print(request.get_full_path())
So in summary, I am trying to understand why I'm receiving this 404, and ultimately, how I can test this, and other endpoints.
Any and all help is appreciated.
Cheers,
C
I am trying to unit test a function:
#shared_task()
def push_notification(message=None, message_type=None, user_id=None, data={}):
# Get the aws arn from token table
aws_token_data = AwsDeviceToken.objects.filter(user_id=user_id).latest("id")
client = boto3.client('sns', **aws.AWS_CREDENTIAL)
message = {
'default': message,
more stuff here
'data': data})
}
message = json.dumps(message, ensure_ascii=False)
response = client.publish(
TargetArn=str(aws_token_data.aws_PLATFORM_endpoint_arn),
Message=message,
MessageStructure='json',
MessageAttributes={}
)
return response
When users register for our service they get a topic arn based on their device type.
I have tried:
def test_push_notification(self):
with mock.patch('boto3.client') as mock_client:
data = {'Some data': "to be sent"}
push_notification(
message="your invitation has been accepted",
message_type='b2g_accepted',
user=self.user,
data=data
)
self.assertEqual(mock_client.call_count, 1)
Where self.user is a user registered in the setUp method of TestCase. This fails, the call_count is 0
I am scratching my head trying to figure out a way to test this function but mostly coming up with third party modules or examples for S3.
Any help is appreciated
You need to mock where it is imported (unless it is a class method)
So if your push_notification function is in a module called my_module, you should write:
With mock.patch('my_module.boto3.client') as mock_client
I am having an issue with automated testing in web py framework.
I am going through the last exercise of learn python the hard way. In this exercise we make a web application "engine" that runs a map of rooms.
I want to be able to automate test every single room, but there is one problem, is that the engine depends on the previous room to decide which room to go to next (and user input).
if web.config.get("_session") is None:
store = web.session.DiskStore("sessions")
session = web.session.Session(app, store, initializer={"room":None})
web.config._session = session
else:
session = web.config._session
This class handles GET request sent to /
class Index(object):
def GET(self):
session.room = map.START
web.seeother("/game")
This class handles GET and POST requests to /game
class GameEngine(object):
def GET(self):
if session.room:
return render.show_room(room=session.room)
else:
return render.you_died()
def POST(self):
form = web.input(action=None)
if session.room and form.action:
session.room = session.room.go(form.action)
web.seeother("/game")
In my automated testing I use two things: first I use the app.request API:
app.request(localpart='/', method='GET',data=None,
host='0.0.0.0:8080', headers=None, https=False)
create a response object, something like:
resp = app.request("/game", method = "GET")
Second I pass the resp object to this function to check for certain things:
from nose.tools import *
import re
def assert_response(resp, contains=None, matches=None, headers=None,
status="200"):
assert status in resp.status, "Expected response %r not in %r" %
(status, resp.status)
if status == "200":
assert resp.data, "Response data is empty"
if contains:
assert contains in resp.data, "Response does not contain %r" %
contains
if matches:
reg = re.compile(matches)
assert reg.matces(resp.data), "Response does not match %r" %
matches
if headers:
assert_equal(resp.headers, headers)
We can pass variables as a dictionary to the keyword argument data in the API app.request to modify the web.input().
my question is: in my automated test module how do we "pass" a value that overwrite the room value in the initializer dictionary in our session:
session = web.session.Session(app, store, initializer={"room":None})
In the app module its done by setting
session.room = map.START
and then session.room updates using:
if session.room and form.action:
session.room = session.room.go(form.action)
Thanks for taking the time to read this, and any insights would be appreciated!
Alright I finally found it! The main issue here was that every time I make a http request through app.request it gives me a new session ID.
The trick that I found thanks to this post:
How to initialize session data in automated test? (python 2.7, webpy, nosetests)
is to record the session ID of the request to reuse that ID in my automated tests by passing it to the headers keyword argument in the request!
record the session ID using this function (which I placed as suggested in the post in tests/tools.py):
def get_session_id(resp):
cookies_str = resp.headers['Set-Cookie']
if cookies_str:
for kv in cookies_str.split(';'):
if 'webpy_session_id=' in kv:
return kv
then in the automated tests something like:
def test_session():
resp = app.request('/')
session_id = get_session_id(resp)
resp1 = app.request('/game', headers={'Cookie':session_id})
assert_response(resp1, status='200', contains='Central Corridor')
I hope that helps in the future for programmers who get stuck on the same issue!
I have a small web app with AngularJS front-end and Django ReST in the back. There's a strange hitch going on when I make POST request to the web service: the browser console clearly shows 3 parameters being sent, but the backend logging reports only 2 params received. The result is that the server throws a code 500 error due to a bad database lookup.
Here's the code:
Client
var b = newQuesForm.username.value;
$http.post('/myapp/questions/new', {username:b,title:q.title,description:q.description}).
success(function(data, status, headers, config) {
$http.get('/myapp/questions').success(function(data){
$scope.questions = data;
q = null;
$scope.newQuesForm.$setPristine();
}).error(function(data, status, headers, config) {
console.log(headers+data);
});
}).
error(function(data, status, headers, config) {
console.log(headers+data);
});
Both my manual logging and the dev console show a string like:
{"username":"admin","description":"What's your name?","title":"question 1"}
Server
class CreateQuestionSerializer(serializers.Serializer):
author = UserSerializer(required=False)
title = serializers.CharField(max_length=150)
description = serializers.CharField(max_length=350)
def create(self, data):
q= Question()
d = data
q.title = d.get('title')
q.description = d.get("description")
q.author = User.objects.get(username=d.get('username'))
q.save()
return q
Server-side logging shows the username parameter never succeeds in making the trip, and thus I end up with code 500 and error message:
User matching query does not exist. (No user with id=none)
What's causing some of the data to get lost?
So it turns out the problem was really with the serialization of fields, as #nikhiln began to point out. I followed his lead to refactor the code, moving the create() method to api.py, rather than serializers.py, and stopped relying altogether on the client-side data for the user's identity, something that was a bit silly in the first place (passing User to a hidden input in the view, and then harvesting the username from there and passing it back to the server in the AJAX params). Here's the new code, that works perfectly:
class QuestionCreate(generics.CreateAPIView):
model = Question
serializer_class = CreateQuestionSerializer
def create(self, request,*args,**kwargs):
q= Question()
d = request.data
q.title = d.get('title')
q.description = d.get("description")
q.author = request.user
q.save()
if q.pk:
return Response({'id':q.pk,'author':q.author.username}, status=status.HTTP_201_CREATED)
return Response({'error':'record not created'}, status=status.HTTP_400_BAD_REQUEST)
So here, I do it the right way: pull the User from the request param directly in the backend.