How to write clean pytest chain api tests - python

New-ish to python and I'm stumped on this. I literally cannot seem to find a clear and correct answer anywhere online. I honestly feel stupid because this is a pretty basic python skill.
My issue:
I've been tasked with writing a python test suite of asyncronous api requests chained together. I need to fire the requests, save values from the response json, then pass those values into subsequent requests.
Below is a list of what I WANT the test to do, and what I would expect it to do based on how I've written the test file:
Send GET to /users endpoint
Save first customerId from response payload as customer_id
Send POST to /customer/{customer_id}/addresses endpoint to create new customer address
Save addressId from response payload as address_id
Send PUT to /orders/{customer_id} endpoint to update order with the address_id from step #3.
Example Code:
(This is the same code I have on my machine, but I've sanitized the file with fake info.
import requests
import json
def first_request_get(self):
url = 'https://api.example.net/users'
headers = {'Content-Type': 'application/json',
'Authorization': 'Bearer asdfasdfasdfasdfasdfasdfasdf'
}
payload = {}
resp = requests.get(url, headers=headers, data=json.dumps(payload))
json_str = json.dumps(payload)
resp_body = resp.json()
customer_id = resp_body['customer'][0]['id']
assert resp_body['customer'][0]['id'] is not None
assert resp.status_code == 200
return customer_id
def second_request_post(self):
customer_id = self.first_request_get()
url = 'https://api.example.net/customers/{}/addresses'.format(customer_id)
headers = {'Content-Type': 'application/json',
'Authorization': 'Bearer asdfasdfasdfasdfasdfasdfasdf'
}
payload = {"address1": "285 Beandip Ln",
"city": "Lincoln",
"province": "Nebraska",
"zip": "68510",
}
resp = requests.post(url, headers=headers, data=json.dumps(payload))
resp_body = resp.json()
address_id = resp_body['address']['id']
print('address_id == ')
print(address_id)
assert resp.status_code == 200
assert resp_body['address']['id'] is not None
return address_id
def third_request_put(self):
address_id = self.second_request_post()
customer_id = self.first_request_get()
url = 'https://api.example.net/orders/{}'.format(customer_id)
headers = {'Content-Type': 'application/json',
'Authorization': 'Bearer asdfasdfasdfasdfasdfasdfasdf'
}
payload = {"address_id": address_id,
"next_charge_scheduled_at": "2022-01-31T00:00:00",
"price": 6.85,
"quantity": 2,
}
resp = requests.put(url, headers=headers, data=json.dumps(payload))
resp_body = resp.json()
subscription_id = resp_body['order']['id']
print('order_id == ')
print(subscription_id)
assert resp_body['order']['id'] is not None
assert resp.status_code == 200
return subscription_id
The result of executing this test file ends up looping through the file in a weird order rather than doing what I would expect it to do. Why do I have to call an earlier function just to access the variable I've already extracted from the json and assigned to a variable in an earlier function?

Unfortunately, it's not advised to design your tests this way.
Testcases should be completely independent of one another and have no side effects. The order of execution should not matter. The Pytest documentation among other sources makes reference to this (and takes steps to disallow violating this principle).
I can also recommend the book "The Art of Software Testing" by Glenford J Myers.
As such, to make this test work, you're going to have to place it all in one method...
If you'd like to be more explicit about the point of failure (even though pytest does a pretty good job already), you'll want to break this test up into multiple cases, but mock the data you're not explicitly testing at each stage.

Related

Xero : OAuth2: Python3: Example Code to get the refresh token

Xero has changed its API to require OAuth2 connections instead of OAuth1.
I had a working solution in OAuth1, but the examples for Oauth2 are scarce at best, but mainly for web sites.
I'm another developer who managed to create an Oauth1 solution, that successfully worked as a machine to machine solution with no web server involved.
Xero has some examples that run in Postman, that easily work in my test environment.
I'm attempting to reproduce the postman action of refreshing the token in Python3.
My basic code below is where I currently am:
#client_id = "xxxxx"
#client_secret = "xxxxx"
callback_url = "https://api.xero.com/connections"
re_directURI = "https://developer.xero.com"
scopes = "offline_access accounting.contacts accounting.transactions"
refresh_url = "https://identity.xero.com/connect/token"
access_token = open('AccessToken.txt').read()
old_refresh_token = open('RefreshToken.txt','r').read()
# Refresh Token code...
import requests
#def refresh_xero_token(refresh_token):
headers = {
'grant_type': 'refresh_token',
'Content-Type': 'application/json',
}
data = {
'grant_type': 'refresh_token',
'refresh_token': old_refresh_token,
'client_id': client_id,
'client_secret': client_secret
}
print(data,headers)
response = requests.post(refresh_url, headers=headers, data=data)
#return response.json()
print(response.text)
I have so far failed to find an example that works without a web server, just using python to communicate with the Xero servers to transfer local data into the Zero API.
Using xoauth,.exe (windows) to get the access_token, and then in postman I can run the examples for refresh token, connections, invoices etc to the demo company.
and I believe just being able to replicate these examples will provide me with what I need to get a working solution.
currently with this python code I only get
{"error":"invalid_request"}
So, clearly I am missing something.
I'll class myself as a newbie with Python or Oauth2, but have chosen this path due to my previous success with an Oauth1 connected solution.
I would ask the Xero developer community, but I'm writing this for a user of our software to push data into their Xero accounts, and so for testing I only have a trial account, which does not give me access to the Xero developer community.
Which by itself is really annoying.
Xero support seems of little use also, I tried.
If there is anyone out there able to assist, that would be fabulous.
Thank you in advance for any help given.
After using the xoauth application and setting up the connection, I have since found that with the refresh token, running this function keeps the connection up and running.
def refresh_xero_token():
refresh_url = "https://identity.xero.com/connect/token"
old_refresh_token = open('RefreshToken.txt','r').read()
tokenb4 = f"{client_id}:{client_secret}"
basic_token = base64.urlsafe_b64encode(tokenb4.encode()).decode()
headers = {
'Authorization': f"Basic {basic_token}",
'Content-Type': 'application/x-www-form-urlencoded',
}
data = {
'grant_type': 'refresh_token',
'refresh_token': old_refresh_token
}
try:
response = requests.post(refresh_url, headers=headers, data=data)
results = response.json()
open('RefreshToken.txt','w').write(results["refresh_token"])
open('AccessToken.txt','w').write(results["access_token"])
except Exception as err:
print("ERROR ! Refreshing token error?")
print(response.text)
As additional information, I can then also use this connection to, for example create a contact in Xero:
In this example the irContact is a SQLAlchemy row of data from a SQL table.
def create_contact( connection, irContact, access_token):
#Setup new contact
address1 = {"AddressType": "POBOX"}
if irContact['addressline1'] is not None: address1.update({"AddressLine1": irContact['addressline1']})
if irContact['addressline2'] is not None: address1.update({"AddressLine2": irContact['addressline2']})
if irContact['addressline3'] is not None: address1.update({"AddressLine3": irContact['addressline3']})
if irContact['addressline4'] is not None: address1.update({"AddressLine4": irContact['addressline4']})
if irContact['city'] is not None: address1.update({"City": irContact['city']})
if irContact['region'] is not None: address1.update({"Region": irContact['region']})
if irContact['postalcode'] is not None: address1.update({"PostalCode": irContact['postalcode']})
if irContact['country'] is not None: address1.update({"Country": irContact['country']})
if irContact['attentionto'] is not None: address1.update({"AttentionTo": irContact['attentionto']})
#print (address1.values())
addresses = []
addresses.append(address1)
phones = []
if irContact['phonenumber'] is not None:
phone1 = {"PhoneType": "DEFAULT"}
#phone1.update({"PhoneType": "DEFAULT"})
if irContact['phonenumber'] is not None: phone1.update({"PhoneNumber": irContact['phonenumber']})
if irContact['phoneareacode'] is not None: phone1.update({"PhoneAreaCode": irContact['phoneareacode']})
if irContact['phonecountrycode'] is not None: phone1.update({"PhoneCountryCode": irContact['phonecountrycode']})
phones.append(phone1)
#print (phone1.values())
if irContact['mobilenumber'] is not None:
phone2 = {"PhoneType": "MOBILE"}
if irContact['phonenumber'] is not None: phone2.update({"PhoneNumber": irContact['mobilenumber']})
if irContact['phoneareacode'] is not None: phone2.update({"PhoneAreaCode": irContact['mobileareacode']})
if irContact['phonecountrycode'] is not None: phone2.update({"PhoneCountryCode": irContact['mobilecountrycode']})
phones.append(phone2)
#print (phone2.values())
contact = { "Name": irContact['name'],
"ContactNumber": irContact['contactnumber'],
"AccountNumber": irContact['accountnumber'],
#"ContactStatus": "ACTIVE",
"FirstName": irContact['firstname'],
"LastName": irContact['lastname'],
#"EmailAddress": irContact['emailaddress'],
"Addresses": addresses,
#"Phones":phones
}
contacts = [contact]
#print(contacts)
contacts_url = "https://api.xero.com/api.xro/2.0/Contacts"
headers = {
'Authorization': f"Bearer {access_token}",
'Accept': 'application/json',
'Content-Type': 'application/json',
'xero-tenant-id': tenant_id,
}
data = {
'Contacts': [contact],
}
#print(data)
try:
response = requests.post(contacts_url, headers=headers, json=data)
except Exception as err:
print("ERROR! Contact: %s" % (str(err) ))
print(response.text)
return 0
#print(response.text)
results = response.json()
if 'Contacts' in results:
newcontacts = results['Contacts']
for newcontact in newcontacts: break
query = "update xero_contact set errortext='', ContactID='%s' where id=%d" % (newcontact["ContactID"], irContact['id'])
connection.execute(query)
query = "update xero_invoice_header set ContactID='%s' where OurContactID=%d and (InvoiceID='' or InvoiceID is null ) " % ( newcontact["ContactID"], irContact['id'] )
connection.execute(query)
I believe, with this amount of information, anyone can be capable of creating their own Xero machine to machine interface...
Realising that other records can be read and created using minimal tweaks to the header and or data element of the requests call.
I found the lack of this information so very frustrating, if people can find this, it may help them in future.

What is the best method to return smmry api request with block of text in json instead of url?

I am trying to write a function in python that returns the json from a request to the smmry API. I was able to get it working with the SM_URL request like this:
def summry():
API_ENDPOINT = "https://api.smmry.com"
API_KEY = "B..."
params = {
"SM_API_KEY":API_KEY,
"SM_URL":"https:..."
}
r = requests.get(url=API_ENDPOINT, params=params)
return r.json()
However, I am not sure how you would do this for passing in a block of text instead of a URL. I have tried making the request with sm_api_input=my_input but that returned an error of insufficient variables. I have also tried it with a POST request and got the same error.
If anyone is curious, this is how I solved the problem. Turns out I needed an Expect: 100-continue header and the sm_api_input is a separate post field instead of a get query.
def summry(text):
API_KEY = "B..."
API_ENDPOINT = "https://api.smmry.com"
data = {
"sm_api_input":text
}
params = {
"SM_API_KEY":API_KEY
}
header_params = {"Expect":"100-continue"}
r = requests.post(url=API_ENDPOINT, params=params, data=data, headers=header_params)
return r.json()

Keep getting a 401 error when trying to pull data

I'm fairly new to using web APIs and pulling data and i'm also pretty new with python. My goal is to make a stat-tracker app but I keep getting a 401 when I try and pull the data.
I've printed out the entire url just to make sure I didn't get it wrong. I copied and pasted the API key exactly so that shouldn't be a problem
api_token = 'api key in python file'
api_url_base = 'https://public-api.tracker.gg/v2/apex/standard/'
headers = {'Content-Type' : 'application/json',
'Authorization' : 'Bearer {}'.format(api_token)}
def get_player_profile():
api_url = '{}profile/psn/Daltoosh'.format(api_url_base)
response = requests.get(api_url, headers=headers)
if response.status_code == 200:
return json.loads(response.content.decode('utf-8'))
else:
return response.status_code, api_url
print(get_player_profile())
#player_profile = get_player_profile()
#if player_profile is not None:
# print("Career Stats:")
# for k, v in player_profile['profile/psn/Daltoosh'].items():
# print('{0}:{1}.format(k, v)')
#else:
# print('[!] Data Request Failed [!]')
I expected a status code of 200 but there seems to be a problem authenticating.
I'm not too well versed in the web API that you are using, but I think you might be using the API token incorrectly. I don't think that specific API requires a Bearer token, but instead a separate header called TRN-Api-Key.
So maybe write something like this:
headers = {'Content-Type' : 'application/json', 'TRN-Api-Key' : api_token}
If you look here, you should be able to read up on how to set up authentication.

Using JSON data from API GET to POST to another API via python script

So, I'm new to python and am struggling, self taught, and still learning to code. So be easy on me :)
I am using a script to get data from one source (Jira's API) and trying to use those results to post to another (PowerBi).
I've managed to successfully get the data, I just don't know how to pass the data to this other API.
I know how to use the GET and POST calls, it's just using the data from one to another than I can't seem to find anything about. Assuming since what I'm asking for is very specific?
edit: I also want to mention that while my get is asking for specific data, I'm getting more than I actually need. So I need a way to specify (hopefully) what data is actually being sent to PowerBi's API
import json
import requests
url = 'https://mydomain.atlassian.net/rest/api/2/search'
headers = { 'Content-Type' : 'application/json',
'Authorization' : 'Basic 123456789' }
params = {
'jql' : 'project IN (, PY, CH, NW, RP, DP, KR, DA, RE, SS, CR, CD, AB) AND issueType=incident AND statusCategory!=Done',
'startAt': 0,
'maxResults' : 50,
}
requestget = requests.get(url, headers=headers, params=params)
if requestget.status_code == 200:
print(json.dumps(json.loads(requestget.text), sort_keys=True, indent=4, separators=(",", ": ")))
else:
print("None")
Apologies if I miss understood what you were asking help on, but you could use this to send a POST request as json.
request = urllib.request.Request()#Put the powerbi api here
request.add_header('Content-Type', 'application/json; charset=utf-8')
jsondata = #your json data
jsonBytes = jsondata.encode('utf-8')
#Has to be bytes
request.add_header('Content-Length', len(jsonBytes))
response = urllib.request.urlopen(request, jsonBytes)
You could go with a requests.post instead.
jsondata = #put json data here
headers = {'content-type': 'application/json'}
response = requests.post(url, data=json.dumps(jsondata), headers=headers)
Requests documentation

Requests package and API documentation

I'm having trouble understanding where to add parameters defined by API documentation. Take BeeBole's documentation for example, which specifies that to get an absence by ID, the following request is required:
{
"service": "absence.get",
"id": "absence_id"
}
They provide only one URL in the documentation:
BeeBole is accepting HTTP POST resquests in a json-doc format to the following URL:
https://beebole-apps.com/api/v2
How would this be implemented in the context of Python requests? The following code I've tried returns 404:
import requests
payload = {
"service": "absence.get",
"id": "absence_id"
}
auth = {
"username": "API_token",
"password": "x"
}
url = "https://beebole-apps.com/api/v2"
req = requests.get(url, params=payload, auth=auth).json()
BeeBole is accepting HTTP POST resquests in a json-doc format to the following URL: https://beebole-apps.com/api/v2
The JSON document format here is the part you missed; you need to pass the information as a JSON encoded body of the request. The params argument you used only sets the URL query string (the ?... part in a URL).
Use
import requests
payload = {
"service": "absence.get",
"id": "absence_id"
}
auth = ("API_token", "x")
url = "https://beebole-apps.com/api/v2"
req = requests.get(url, json=payload, auth=auth).json()
The json= part ensures that the payload dictionary is encoded to JSON and sent as a POST body. This also sets the Content-Type header of the request.
I've also updated the API authentication, all that the auth keyword needs here is a tuple of the username and password. See the Basic Authentication section.
You may want to wait with calling .json() on the response; check if the response was successful first:
req = requests.get(url, json=payload, auth=auth)
if not req.ok:
print('Request not OK, status:', req.status_code, req.reason)
if req.content:
print(req.text)
else:
data = req.json()
if data['status'] == 'error':
print('Request error:', data['message'])
This uses the documented error responses.
From the site documentation it would appear that this particular vendor has chosen an unusual API. Most people use different endpoints to implement different operations, but BeeBole appears to implement everything off the one endpoint, and then selects the operation by examining the "service" key in the request data.
Try
response - request.post('https://beebole-apps.com/api/v2',
json={"service": "company.list"},
headers={"authorization": TOKEN)
From the documentation I can't guarantee that will put the request in the right format, but at least if it doesn't work it should give you some clue as to how to proceed. Establishing the correct value of TOKEN is described under "Authorization" in the BeeBole documentation.
It's an unusual way to offer an API, but it seems workable.

Categories