I want to test an authenticated post request on an API using Pytest. This is what I am doing so far:
def test_auth_user_can_create(self, client):
url = api_reverse('crud-simulation_api')
data = {
"project": "testproject",
....
}
response = client.post(url, json=data)
assert response.status_code == 200
This doesn't work because it gives me back a 401 (Unauthorized) instead of a 200. That makes sense since the fixture is a client and not an admin client.
Yet if I pass in admin_client instead of client it gives me a Bad Request. The data that I send should be fine though.
I also tried to pass in the headers like so (since I use JWT authorization):
token = "bigassstringwhichismytoken"
headers = {
"Authorization": "JWT " + token
}
Finally I tried to log in before which gives me a 403 (Forbidden):
def test_auth_user_can_create_simulation_api(self, client, django_user_model):
username = "Jack"
password = "password"
django_user_model.objects.create_user(username=username, password=password)
client.login(username=username, password=password)
url = api_reverse('crud-simulation_api')
data = {
"project": "testproject",
...
}
response = client.post(url, json=data)
assert response.status_code == 200
If someone could point me into the right direction that would be fantastic! Thanks a lot in advance
To provide headers for client.{request} pass them individually as keyword agruments:
client.post(url, data, HTTP_FIRST_HEADER='...', HTTP_SECOND_HEADER='...')
Although you're unlikely to collide with any reserved parameter names in post call chain, better collect all headers you need in a dictionary:
headers = {
'HTTP_FIRST_HEADER': '...',
'HTTP_SECOND_HEADER': '...',
}
And pass them to request as arbitrary number of keyword arguments:
client.post(url, data, **headers)
In this case ** arguments are treated as extra information and are automatically added as headers.
You can hit the login url with username and password and get the token.
creade a header dictionary like headers = {'Authorization': 'JWT <token>'}
and use the header when using post.
client.post(url, json=data, headers=headers)
I would suggest installing the pytest-django package. Based on its docs, the easiest answer would be just using the admin_client fixture. As admin_client has the type of django.test.Client, it can be used for both get and post requests.
def test_sth_with_auth(admin_client):
response = admin_client.get('/private')
assert response.status_code == 200
Also if you want to use a specific user, you can try sth like this:
#pytest.fixture
def my_user(django_user_model):
return django_user_model.objects.create_user(username=username, password=password)
#pytest.fixture
def logged_in_client(client, my_user):
return client.force_login(my_user)
def test_sth_with_auth(logged_in_client):
response = logged_in_client.get('/private')
assert response.status_code == 200
this part of the doc can be helpful to write your desired logged_in_client().
Related
def queue_song(session_id):
song_uri='spotify:track:5RwV8BvLfX5injfqYodke9'
tokens = get_user_tokens(session_id)
headers = {'Content-Type': 'application/json',
'Authorization': "Bearer " + tokens.access_token,
}
url = BASE_URL +'player/queue'
data={
'uri':song_uri
}
response = requests.post(url,headers=headers,data=data).json()
print(response)
Output:
{'error': {'status': 400, 'message': 'Required parameter uri missing'}}
https://developer.spotify.com/documentation/web-api/reference/#/operations/add-to-queue
I dont thing there is any problem with auth tokens... coz 'GET' requests are working fine
By default, using data= in requests.post() sets the content type to application/x-www-form-urlencoded which makes the body a akin to a HTTP form request.
Spotify's API is JSON based, so your data needs to be a valid json data.
You can do it in 2 ways:
response = requests.post(url,headers=headers,data=json.dumps(data)).json()
Or, more simply:
response = requests.post(url,headers=headers,json=data).json()
and in this way you don't need to manually set the application/json header as well.
Edit:
After going through the API docs you linked, there's more wrong with the call you're making.
You're sending the parameters in data - which is the body of the request. But Spotify API specifies the parameters need to be put in the Query i.e. the query string of the URI. Which means your request should be:
response = requests.post(url,headers=headers,params=data).json() # set query string not body
What I've Done
I've written an authentication class for obtaining an application's bearer token from Twitter using the application's API Key and its API key secret as demonstrated in the Twitter developer docs.
I've mocked the appropriate endpoint using requests_mock this way:
#pytest.fixture
def mock_post_bearer_token_endpoint(
requests_mock, basic_auth_string, bearer_token
):
requests_mock.post(
"https://api.twitter.com/oauth2/token",
request_headers={
"Authorization": f"Basic {basic_auth_string}",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
json={"token_type": "bearer", "access_token": f"{bearer_token}"},
)
And my test method is :
#pytest.mark.usefixtures("mock_post_bearer_token_endpoint")
def test_basic_auth(api_key, api_key_secret, bearer_token):
response = requests.post(
'https://api.twitter.com/oauth2/token',
data={"grant_type": "client_credentials"},
auth=TwitterBasicAuth(api_key, api_key_secret),
)
assert response.json()['access_token'] == bearer_token
(Where TwitterBasicAuth is the authentication class I wrote, and the fixture basic_auth_string is a hardcoded string that would be obtained from transforming the fixtures api_key and api_key_secret appropriately).
And it works.
The Problem
But I'm really bothered by the fact that the mocked endpoint doesn't check the payload. In this particular case, the payload is vital to obtain a bearer token.
I've combed through the documentation for requests_mock (and responses, too) but haven't figured out how to make the endpoint respond with a bearer token only when the correct payload is POSTed.
Please help.
I think the misconception here is that you need to put everything in the matcher and let NoMatchException be the thing to tell you if you got it right.
The matcher can be the simplest thing it needs to be in order to return the right response and then you can do all the request/response checking as part of your normal unit test handling.
additional_matchers is useful if you need to switch the response value based on the body of the request for example, and typically true/false is sufficient there.
eg, and i made no attempt to look up twitter auth for this:
import requests
import requests_mock
class TwitterBasicAuth(requests.auth.AuthBase):
def __init__(self, api_key, api_key_secret):
self.api_key = api_key
self.api_key_secret = api_key_secret
def __call__(self, r):
r.headers['x-api-key'] = self.api_key
r.headers['x-api-key-secret'] = self.api_key_secret
return r
with requests_mock.mock() as m:
api_key = 'test'
api_key_secret = 'val'
m.post(
"https://api.twitter.com/oauth2/token",
json={"token_type": "bearer", "access_token": "token"},
)
response = requests.post(
'https://api.twitter.com/oauth2/token',
data={"grant_type": "client_credentials"},
auth=TwitterBasicAuth(api_key, api_key_secret),
)
assert response.json()['token_type'] == "bearer"
assert response.json()['access_token'] == "token"
assert m.last_request.headers['x-api-key'] == api_key
assert m.last_request.headers['x-api-key-secret'] == api_key_secret
https://requests-mock.readthedocs.io/en/latest/history.html
Updated Answer
I went with gold_cy's comment and wrote a custom matcher that takes a request and returns an appropriately crafted OK response if the request has the correct url path, headers and json payload. It returns a 403 response otherwise, as I'd expect from the Twitter API.
#pytest.fixture
def mock_post_bearer_token_endpoint(
requests_mock, basic_auth_string, bearer_token
):
def matcher(req):
if req.path != "/oauth2/token":
# no mock address
return None
if req.headers.get("Authorization") != f"Basic {basic_auth_string}":
return create_forbidden_response()
if (
req.headers.get("Content-Type")
!= "application/x-www-form-urlencoded;charset=UTF-8"
):
return create_forbidden_response()
if req.json().get("grant_type") != "client_credentials":
return create_forbidden_response()
resp = requests.Response()
resp._content = json.dumps(
{"token_type": "bearer", "access_token": f"{bearer_token}"}
).encode()
resp.status_code = 200
return resp
requests_mock._adapter.add_matcher(matcher)
yield
def create_forbidden_response():
resp = requests.Response()
resp.status_code = 403
return resp
Older Answer
I went with gold_cy's comment and wrote an additional matcher that takes the request and checks for the presence of the data of interest in the payload.
#pytest.fixture(name="mock_post_bearer_token_endpoint")
def fixture_mock_post_bearer_token_endpoint(
requests_mock, basic_auth_string, bearer_token
):
def match_grant_type_in_payload(request):
if request.json().get("grant_type") == "client_credentials":
return True
resp = Response()
resp.status_code = 403
resp.raise_for_status()
requests_mock.post(
"https://api.twitter.com/oauth2/token",
request_headers={
"Authorization": f"Basic {basic_auth_string}",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
json={"token_type": "bearer", "access_token": f"{bearer_token}"},
additional_matcher=match_grant_type_in_payload,
)
I opted to raise an Http403 error (instead of just returning False) in order to reduce the cognitive load of determining the reason exceptions are raised — returning False would lead to a requests_mock.exceptions.NoMockAddress being raised, which I don't think is descriptive enough in this case.
I still think there's a better way around this, and I'll keep searching for it.
I have gone through number of similar posts related to firing GET requests with Basic Auth (eg: Python, HTTPS GET with basic authentication), still can't figure out the problem. I keep getting the error requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url
With the same credentials, headers tried the same in postman it works as expected. Verified that base64encoded value for the api_key, password is exactly same as the value used in postman, so I don't think its encoding or resource access permission problem.
python -V
Python 3.6.4 :: Anaconda, Inc.
Approach 1
api_key = 'some_api_key'
password = 'some_password'
headers = {'accept': 'application/json'}
url = 'https://test.access.com/this/url'
api_key_password = "%s:%s" % (api_key, password)
b64_encoded = b64encode(bytes(api_key_password, 'utf-8')).decode("ascii")
headers['authorization'] = 'Basic %s' % b64_encoded
response = requests.get(url,
headers=headers)
if (response.ok):
json_data = json.loads(response.content)
print (json_data)
else:
print (response)
response.raise_for_status()
Approach 2
api_key = 'some_api_key'
password = 'some_password'
url = 'https://test.access.com/this/url'
headers = {
'accept': 'application/json',
}
response = requests.get(url, headers=headers, auth=(api_key, password))
print (response.ok)
if (response.ok):
json_data = json.loads(response.content)
print (json_data)
else:
print (response)
response.raise_for_status()
Can you please provide some pointers?
I had a similar issue (although in .NET Framework).
In my case the reason was that I was using the url without a forward slash in the end and the API apparently does not support that.
So https://test.access.com/this/url
Throws 401 error Unauthorized
but
https://test.access.com/this/url/
Returns 200 OK.
Older post but I had a similar issue. Postman will cache your JSESSIONID. Be sure you are clearing out that cookie while testing. If you are hitting an API that requires a login API call to establish a session before you can make subsequent API calls, this Postman behavior can produce a false sense of security.
In this situation with Python requests, it can be handled with code similar to what I've provided below:
import requests,json
loginAPI = "https://myapi.myco.comv/someuri/someuri/users/login"
someHTTPGetAPI = "https://myapi.myco.com/someuri/someuri/someservice"
username = "myuser"
password = "mypass"
headers = {
"Content-Type": "application/json",
"login": username,
"password": password
}
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
verify=False
session = requests.Session()
sessionResponse = session.get(url=loginURL,headers=headers, verify=verify)
if sessionResponse.status_code == 200:
getResponse = session.get(url=someHTTPGetAPI)
if getResponse.status_code == 200:
responseJSON = agentStatus.json()
I'm trying to make a post request to Quizlet following their OAuth flow from these instructions https://quizlet.com/api/2.0/docs/authorization-code-flow. I'm running into a problem where on Step 2, I have to make a post request with a token I generated from their server, but I'm not having success passing in the token to the url. I know it was generated correctly, but I'm having trouble passing it in and not getting a 400 response.
More directly, my question is, is there another way of including the grant_type and code parameters that I'm trying to pass in through the url in the post request such as passing them in through the header of the post request? I've looked at the documentation for requests but I've had no luck.
#app.route('/')
#app.route('/index')
def index():
code = request.args.get('code')
state = request.args.get('state')
print("code is " + code)
r = requests.post("https://api.quizlet.com/oauth/token?grant_type=authorization_code&code=" + code)
return render_template('index.html')
You must specify the required headers Authorization, Content-Type.
import requests
from requests.auth import _basic_auth_str
client_id = 'YOUR CLIENT ID'
secret = 'YOUR CLIENT SECRET'
code = 'CODE FROM STEP 1'
headers = {
'Authorization': _basic_auth_str(client_id, secret),
'Content-Type': 'application/x-www-form-urlencoded'
}
r = requests.post('https://api.quizlet.com/oauth/token?grant_type=authorization_code&code={0}'.format(
code), headers=headers)
print r.status_code
print r.content
I have a request URI and a token. If I use:
curl -s "<MY_URI>" -H "Authorization: TOK:<MY_TOKEN>"
etc., I get a 200 and view the corresponding JSON data.
So, I installed requests and when I attempt to access this resource I get a 403 probably because I do not know the correct syntax to pass that token. Can anyone help me figure it out?
This is what I have:
import sys,socket
import requests
r = requests.get('<MY_URI>','<MY_TOKEN>')
r. status_code
I already tried:
r = requests.get('<MY_URI>',auth=('<MY_TOKEN>'))
r = requests.get('<MY_URI>',auth=('TOK','<MY_TOKEN>'))
r = requests.get('<MY_URI>',headers=('Authorization: TOK:<MY_TOKEN>'))
But none of these work.
In python:
('<MY_TOKEN>')
is equivalent to
'<MY_TOKEN>'
And requests interprets
('TOK', '<MY_TOKEN>')
As you wanting requests to use Basic Authentication and craft an authorization header like so:
'VE9LOjxNWV9UT0tFTj4K'
Which is the base64 representation of 'TOK:<MY_TOKEN>'
To pass your own header you pass in a dictionary like so:
r = requests.get('<MY_URI>', headers={'Authorization': 'TOK:<MY_TOKEN>'})
I was looking for something similar and came across this. It looks like in the first option you mentioned
r = requests.get('<MY_URI>', auth=('<MY_TOKEN>'))
"auth" takes two parameters: username and password, so the actual statement should be
r=requests.get('<MY_URI>', auth=('<YOUR_USERNAME>', '<YOUR_PASSWORD>'))
In my case, there was no password, so I left the second parameter in auth field empty as shown below:
r=requests.get('<MY_URI', auth=('MY_USERNAME', ''))
Hope this helps somebody :)
This worked for me:
access_token = #yourAccessTokenHere#
result = requests.post(url,
headers={'Content-Type':'application/json',
'Authorization': 'Bearer {}'.format(access_token)})
You can also set headers for the entire session:
TOKEN = 'abcd0123'
HEADERS = {'Authorization': 'token {}'.format(TOKEN)}
with requests.Session() as s:
s.headers.update(HEADERS)
resp = s.get('http://example.com/')
I found it here, it's working for me with Linkedin:
https://auth0.com/docs/flows/guides/auth-code/call-api-auth-code
The code I used with Linkedin login is:
ref = 'https://api.linkedin.com/v2/me'
headers = {"content-type": "application/json; charset=UTF-8",'Authorization':'Bearer {}'.format(access_token)}
Linkedin_user_info = requests.get(ref1, headers=headers).json()
Requests natively supports basic auth only with user-pass params, not with tokens.
You could, if you wanted, add the following class to have requests support token based basic authentication:
import requests
from base64 import b64encode
class BasicAuthToken(requests.auth.AuthBase):
def __init__(self, token):
self.token = token
def __call__(self, r):
authstr = 'Basic ' + b64encode(('token:' + self.token).encode('utf-8')).decode('utf-8')
r.headers['Authorization'] = authstr
return r
Then, to use it run the following request :
r = requests.get(url, auth=BasicAuthToken(api_token))
An alternative would be to formulate a custom header instead, just as was suggested by other users here.
You can try something like this
r = requests.get(ENDPOINT, params=params, headers={'Authorization': 'Basic %s' % API_KEY})
This worked for me:
r = requests.get('http://127.0.0.1:8000/api/ray/musics/', headers={'Authorization': 'Token 22ec0cc4207ebead1f51dea06ff149342082b190'})
My code uses user generated token.
You have a request needing an authorization maybe you have a result 401.
Suppose your request is like this :
REQ ='https://api.asite.com/something/else/else'
You have your token :
TOKEN = 'fliuzabuvdgfnsuczkncsq12454632'
build your header like this :
HEADER = {'Authorization': f'{TOKEN}'}
and use it like this :
req.get(REQ, headers=HEADER)
display your result like this :
req.get(COACH, headers=HEADER).json()