Pytest - Mock and Testing Django url with call to external package - python

HI I need to test my endpoint, it is expecting a json from another call to external library
url
path("check-services", views.check_services, name="check_services"),
views
from api.services import Services
services = Services()
def check_services(request):
payload = json.loads(request.GET.get("the_params"))
error, message, response = services.check_services(payload)
if error:
return JsonResponse({"res": [], 'error': True, 'message': message})
return JsonResponse({"res": response, 'error': False, "message": "Success"})
tests
import pytest
from api.services import Services
#patch('api.services.Services', side_effect=[True, 'Success', []])
def test_check_services(client):
payload = {}
url = reverse('check_services')
res = client.get(url)
//really dont know how to mock the external service here
error,message, response = Services().check_services(payload)
print(response)
//assertion
assert error ==True
Got error
AssertionError: assert <MagicMock name='Services.get().status_code' id='140222676793616'> == 200
E + where <MagicMock name='Services.get().status_code' id='140222676793616'> = <MagicMock name='Services.get()' id='140222676785840'>.status_code
But it is still actually calling the external package when I commented the line that says `res=client.get(url).
Inspecting the package Service class
class Services:
def __init__(self):
//
def check_services(payload: dict):
try
response = //some manipulation here ex calling another api
return False, 'success', response
except Exception as e:
return True,e.message,[]
Update ,
Ok so I updated my test using unittest.mock
class SimpleTestCase(TestCase):
def setUp(self):
self.client = Client()
#patch('Courier.views.check_services')
def test_mocked_test_case_should_succeed(self, mocked_check_services):
"""
This is a simple testcase using mock feature.
1 - Decorate the method with path.
2 - Reference the mock in test method as param (mocked_check_services).
3 - Define a value of the return of method (sub method or property) to mock.
4 - Call the mocked method and compare.
"""
mocked_check_services.get.content.return_value = b'{"res": [], "error": true, "messages": "Invalid service"}'
test_address = '{"name": "37 Jamaica Drive"}'
url = f'{reverse("check_services")}?the_params={test_address}'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.content, b'{"res": [], "error": true, "message": "Invalid service"}')
Bu still it keeps calling the Services class directly.
How to test in this scenario without actually running the imported external method from a library?

Related

How to call a normal python function along with header information without using requests

Details of application:
UI: Angular
Backend: Python Flask (using Swagger)
Database: MongoDB
We have a few backend python methods which will be called from the UI side to do CURD operations on the database.
Each of the methods has a decorator which will check the header information to ensure that only a genuine person can call the methods.
From the UI side when these API's are called, this authorization decorator is not creating any problem and a proper response is returned to the UI (as we are passing the header information also to the request)
But now we are writing unit test cases for the API's. Here each test case will call the backend method and because of the authorization decorator, I am getting errors and not able to proceed. How can I handle this issue?
backend_api.py
--------------
from commonlib.auth import require_auth
#require_auth
def get_records(record_id):
try:
record_details = records_coll.find_one({"_id": ObjectId(str(record_id))})
if record_details is not None:
resp = jsonify({"msg": "Found Record", "data": str(record_details)})
resp.status_code = 200
return resp
else:
resp = jsonify({"msg": "Record not found"})
resp.status_code = 404
return resp
except Exception as ex:
resp = jsonify({"msg": "Exception Occured",'Exception Details': ex}))
resp.status_code = 500
return resp
commonlib/auth.py
-----------------
### some lines of code here
def require_auth(func):
"""
Decorator that can be added to a function to check for authorization
"""
def wrapper(*args, **kwargs):
print(*args,**kwargs)
username = get_username()
security_log = {
'loginId': username,
'securityProtocol': _get_auth_type(),
}
try:
if username is None:
raise SecurityException('Authorization header or cookie not found')
if not is_auth_valid():
raise SecurityException('Authorization header or cookie is invalid')
except SecurityException as ex:
log_security(result='DENIED', message=str(ex))
unauthorized(str(ex))
return func(*args, **kwargs)
return wrapper
test_backend_api.py
-------------------
class TestBackendApi(unittest.TestCase):
### some lines of code here
#mock.patch("pymongo.collection.Collection.find_one", side_effect=[projects_json])
def test_get_records(self, mock_call):
from backend_api import get_records
ret_resp = get_records('61729c18afe7a83268c6c9b8')
final_response = ret_resp.get_json()
message1 = "return response status code is not 200"
self.assertEqual(ret_resp.status_code, 200, message1)
Error snippet :
---------------
E RuntimeError: Working outside of request context.
E
E This typically means that you attempted to use functionality that needed
E an active HTTP request. Consult the documentation on testing for
E information about how to avoid this problem.

requests.auth.AuthBase TypeError on call

From the docs at https://docs.python-requests.org/en/master/user/authentication/
I gathered that the __call__ function in my own Auth Class should have the r argument,
However when i go to call this class in requests.get(auth=MyClass), I get the error TypeError: __call__() missing 1 required positional argument: 'r'
The code for my class can be found here https://pastebin.com/YDZ2DeaT
import requests
import time
import base64
from requests.auth import AuthBase
class TokenAuth(AuthBase):
"""Refreshes SkyKick token, for use with all Skykick requests"""
def __init__(self, Username: str, SubKey: str):
self.Username = Username
self.SubKey = SubKey
# Initialise with no token and instant expiry
self.Token = None
self.TokenExpiry = time.time()
self.Headers = {
# Request headers
'Content-Type' : 'application/x-www-form-urlencoded',
'Ocp-Apim-Subscription-Key': self.SubKey,
}
self.Body = {
# Request body
'grant_type': 'client_credentials',
'scope' : 'Partner'
}
def regenToken(self):
# Sends request to regenerate token
try:
# Get key from API
response = requests.post("https://apis.skykick.com/auth/token",
headers=self.Headers,
auth=(self.Username, self.SubKey),
data=self.Body,
).json()
except:
raise Exception("Sending request failed, check connection.")
# API errors are inconsistent, easiest way to catch them
if "error" in response or "statusCode" in response:
raise Exception(
"Token requesting failed, cannot proceed with any Skykick actions, exiting.\n"
f"Error raised was {response}")
# Get token from response and set expiry
self.Token = response["access_token"]
self.TokenExpiry = time.time() + 82800
def __call__(self, r):
# If token expiry is now or in past, call regenToken
if self.TokenExpiry <= time.time():
self.regenToken()
# Set headers and return complete requests.Request object
r.headers["Authorization"] = f"Bearer {self.Token}"
return r
# Initialise our token class, so it is ready to call
TokenClass = TokenAuth("test", "1234")
#Send request with class as auth method.
requests.get("https://apis.skykick.com/whoami", auth=TokenClass())
I've tried using the example code, which works, but I can't figure out why mine won't work.
python-requests version is 2.25.1
I think I know what is going on.
This line instantiates an object, called TokenClass
TokenClass = TokenAuth("test", "1234")
then here,
requests.get("https://apis.skykick.com/whoami", auth=TokenClass())
you are calling that object like a function
when you call an object like a function, python looks for the __call__ method of the object.
And you are not calling in any arguments here. What you have is roughly the same as this I think
requests.get("https://apis.skykick.com/whoami", auth=TokenClass.__call__())
and so it complains that you are missing the r argument
This is their example:
import requests
class MyAuth(requests.auth.AuthBase):
def __call__(self, r):
# Implement my authentication
return r
url = 'https://httpbin.org/get'
requests.get(url, auth=MyAuth())
MyAuth is a class that they define, and then MyAuth() creates an instance of it that they pass in to get.
Yours is more like this
import requests
class MyAuth(requests.auth.AuthBase):
def __call__(self, r):
# Implement my authentication
return r
url = 'https://httpbin.org/get'
myAuth = MyAuth() # create an instance of the class
requests.get(url, auth=myAuth()) # call the instance and pass in result
It could also be written like this
import requests
class MyAuth(requests.auth.AuthBase):
def __call__(self, r):
# Implement my authentication
return r
url = 'https://httpbin.org/get'
requests.get(url, auth=MyAuth()())
This program with produce the same error you are getting
import requests
class MyAuth(requests.auth.AuthBase):
def __call__(self, r):
# Implement my authentication
return r
url = 'https://httpbin.org/get'
MyAuth()()
because when you put () after a class, you get an instance, and when you put () after an instance, you call the __call__ method

Conditionally mock httputil.get method on specific url with path json

I have http get method mocked so to get the response from the url without actually sending the url:
def get(url, retries=None, back_off_factor=None, max_back_off=None, timeout=None, response_encoding=None,
retry_on_timeout=None, retry_codes=None, **kwargs):
return _make_request("GET", url,
retries=retries, back_off_factor=back_off_factor,
max_back_off=max_back_off,
timeout=timeout,
response_encoding=response_encoding,
retry_on_timeout=retry_on_timeout,
retry_codes=retry_codes,
**kwargs)
#patch('lib.httputil.get')
def test_harvest(self, mock_get):
articles = json.load(json_file)
# Configure the mock to return a response with an OK status code. Also, the mock should have
# a `json()` method that returns a list of todos.
mock_get.return_value = Mock(ok=True)
mock_get.return_value.json.return_value = articles
mock_get.return_value.status_code = 200
the_rest_of_the_test()
But I realized I need to mock it only if the URL is specific. I know I could use new keyword and do:
def mock_get(self, url):
if url == MY_SPECIFIC_URL:
{...}
else:
self.old_get(url)
{...}
with mock.patch('portality.lib.httputil.get', new=self.mock_get):
the_rest_of_the_test()
but I don't really know how to mock the Response object so that it returns the correct status code and gives the correct result to .json() method.
How can I use both of these approaches altogether so that on one hand I can use the conditional but on the other mock the Response in easy way?
I suggest that you use the requests library, along with responses which is specifically meant for returning the desired HTTP responses.
You can mock specific urls:
import responses
import requests
#responses.activate
def test_simple():
responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
json={'error': 'not found'}, status=404)
resp = requests.get('http://twitter.com/api/1/foobar')
assert resp.json() == {"error": "not found"}
assert len(responses.calls) == 1
assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar'
assert responses.calls[0].response.text == '{"error": "not found"}'
And you can exclude other urls:
responses.add_passthru(re.compile('https://percy.io/\\w+'))

Flask test client using GET request instead of POST

I have a route for only POST request and it returns json response if conditions are met. It's something like this:
#app.route('/panel', methods=['POST'])
def post_panel():
# Check for conditions and database operations
return jsonify({"message": "Panel added to database!"
"success": 1})
I am using flask-sslify to force http requests to https.
I am testing this route with Flask test client and unittest. The test function is similar to following:
class TestAPI2_0(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
create_fake_data(db)
self.client = self.app.test_client()
def tearDown(self):
....
def test_post_panel_with_good_data(self):
# data
r = self.client.post('/panel',
data=json.dumps(data),
follow_redirects=True)
print(r.data)
self.assertEqual(r.status_code, 200)
Output is exactly below:
test_post_panel_with_good_data (tests.test_api_2_0.TestAPI2_0) ... b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<title>405 Method Not Allowed</title>\n<h1>Method Not Allowed</h1>\n<p>The method is not allowed for the requested URL.</p>\n'
======================================================================
FAIL: test_post_panel_with_good_data (tests.test_api_2_0.TestAPI2_0)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/tanjibpa/work/craftr-master/tests/test_api_2_0.py", line 110, in test_post_panel_with_good_data
self.assertEqual(r.status_code, 200)
AssertionError: 405 != 200
I am getting an error that Method is not allowed in that route.
If I specify GET as a method (methods=['GET', 'POST']) for the route test seems to work. But why test client is making a GET request? Is there any way around rather than specifying a GET request for the route?
Update:
If do it like this:
#app.route('/panel', methods=['GET', 'POST'])
def post_panel():
if request.method == 'POST':
# Check for conditions and database operations
return jsonify({"message": "Panel added to database!"
"success": 1})
return jsonify({"message": "GET request"})
I get output like this:
test_post_panel_with_good_data (tests.test_api_2_0.TestAPI2_0) ... b'{\n "message": "GET request"\n}\n'
I found out what was causing the GET request within flask test client.
I am using flask-sslify to force http requests to https.
Somehow flask-sslify is enforcing a GET request although test client is specified with other kind of requests (POST, PUT, DELETE...).
So, If I disable sslify during testing flask test client works as it should.

Python unittest mock an API key

I'm writing unit tests for the Client class of client.py, which queries an API. Each test instantiates the client with c = client.Client("apikey"). Running one test at a time works fine, but running them all (e.g. with py.test) I get a 401: "Exception: Response 401: Unauthorized Access. Requests must contain a valid api-key."
I have a valid API key but this should not be included in the unit tests. I would appreciate an explanation of why "apikey" works for only one query. More specifically, how can I mock out the calls to the API? Below is an example unit test:
def testGetContextReturnFields(self):
c = client.Client("apikey")
contexts = c.getContext("foo")
assert(isinstance(contexts[0]["context_label"], str))
assert(contexts[0]["context_id"] == 0)
Separate out the tests for API calls and for the Client.getContext() method. For explicitly testing the API calls, patch a request object...
import client
import httpretty
import requests
from mock import Mock, patch
...
def testGetQueryToAPI(self):
"""
Tests the client can send a 'GET' query to the API, asserting we receive
an HTTP status code reflecting successful operation.
"""
# Arrange: patch the request in client.Client._queryAPI().
with patch.object(requests, 'get') as mock_get:
mock_get.return_value = mock_response = Mock()
mock_response.status_code = 200
# Act:
c = client.Client()
response = c._queryAPI("path", 'GET', {}, None, {})
# Assert:
self.assertEqual(response.status_code, 200)
# Repeat the same test for 'POST' queries.
And for testing getContext(), mock out the HTTP with httpretty...
#httpretty.activate
def testGetContextReturnFields(self):
"""
Tests client.getContext() for a sample term.
Asserts the returned object contains the corrcet fields and have contents as
expected.
"""
# Arrange: mock JSON response from API, mock out the API endpoint we expect
# to be called.
mockResponseString = getMockApiData("context_foo.json")
httpretty.register_uri(httpretty.GET,
"http://this.is.the.url/query",
body=mockResponseString,
content_type="application/json")
# Act: create the client object we'll be testing.
c = client.Client()
contexts = c.getContext("foo")
# Assert: check the result object.
self.assertTrue(isinstance(contexts, list),
"Returned object is not of type list as expected.")
self.assertTrue(("context_label" and "context_id") in contexts[0],
"Data structure returned by getContext() does not contain"
" the required fields.")
self.assertTrue(isinstance(contexts[0]["context_label"], str),
"The \'context_label\' field is not of type string.")
self.assertEqual(contexts[0]["context_id"], 0,
"The top context does not have ID of zero.")

Categories

Resources