Commit 02c60ad0 by christopher lee Committed by Christopher Lee

Fix caching of ApiAccessRequest

Service users do not return any results for ApiAccessRequest. This
caused an index error which then prevented that result from being
cached.
parent 45b26d08
...@@ -161,6 +161,9 @@ def get_utm_source_for_user(partner, user): ...@@ -161,6 +161,9 @@ def get_utm_source_for_user(partner, user):
# use company name from API Access Request as utm_source. # use company name from API Access Request as utm_source.
if waffle.switch_is_active('use_company_name_as_utm_source_value') and partner.lms_url: if waffle.switch_is_active('use_company_name_as_utm_source_value') and partner.lms_url:
lms = LMSAPIClient(partner.site) lms = LMSAPIClient(partner.site)
# This result is not being used to determine access. It is only being
# used to create an alternative UTM code parsed from the result.
api_access_request = lms.get_api_access_request(user) api_access_request = lms.get_api_access_request(user)
if api_access_request: if api_access_request:
......
...@@ -12,6 +12,10 @@ from course_discovery.apps.api.utils import get_cache_key ...@@ -12,6 +12,10 @@ from course_discovery.apps.api.utils import get_cache_key
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SENTINEL_NO_RESULT = ()
ONE_HOUR = 60 * 60
ONE_MINUTE = 60
class LMSAPIClient(object): class LMSAPIClient(object):
""" """
...@@ -23,13 +27,13 @@ class LMSAPIClient(object): ...@@ -23,13 +27,13 @@ class LMSAPIClient(object):
def get_api_access_request(self, user): def get_api_access_request(self, user):
""" """
Get API Access Requests made by the given user. Get ApiAccessRequests made by the given user.
Arguments: Arguments:
user (User): Django User. user (User): Django User.
Returns: Returns:
(dict): API Access requests made by the given user. (dict): ApiAccessRequests for the given user.
Examples: Examples:
>> user = User.objects.get(username='staff') >> user = User.objects.get(username='staff')
...@@ -54,23 +58,32 @@ class LMSAPIClient(object): ...@@ -54,23 +58,32 @@ class LMSAPIClient(object):
} }
cache_key = get_cache_key(username=user.username, resource=resource) cache_key = get_cache_key(username=user.username, resource=resource)
api_access_request = cache.get(cache_key) cached_api_access_request = cache.get(cache_key)
if cached_api_access_request is SENTINEL_NO_RESULT:
return None
if not api_access_request: if cached_api_access_request:
try: return cached_api_access_request
results = getattr(self.client, resource).get(**query_parameters)['results']
api_access_request = None
try:
results = getattr(self.client, resource).get(**query_parameters)['results']
if results:
if len(results) > 1: if len(results) > 1:
logger.warning( logger.warning(
'Multiple APIAccessRequest models returned from LMS API for user [%s].', 'Multiple ApiAccessRequest models returned from LMS API for user [%s].',
user.username, user.username,
) )
api_access_request = results[0] api_access_request = results[0]
cache.set(cache_key, api_access_request, 60 * 60) cache.set(cache_key, api_access_request, ONE_HOUR)
except (SlumberBaseException, ConnectionError, Timeout): else:
logger.exception('Failed to fetch API Access Request from LMS for user "%s".', user.username) cache.set(cache_key, SENTINEL_NO_RESULT, ONE_HOUR)
except (IndexError, KeyError): logger.info('No results for ApiAccessRequest for user [%s].', user.username)
logger.info('APIAccessRequest model not found for user [%s].', user.username)
except (SlumberBaseException, ConnectionError, Timeout, KeyError) as exception:
cache.set(cache_key, SENTINEL_NO_RESULT, ONE_MINUTE)
logger.exception('%s: Failed to fetch ApiAccessRequest from LMS for user [%s].',
exception.__class__.__name__, user.username)
return api_access_request return api_access_request
...@@ -82,6 +82,28 @@ class LMSAPIClientMixin(object): ...@@ -82,6 +82,28 @@ class LMSAPIClientMixin(object):
status=status status=status
) )
def mock_api_access_request_with_configurable_results(self, lms_url, user, status=200, results=None):
"""
Mock the api access requests endpoint response of the LMS.
"""
data = {
'count': len(results),
'num_pages': 1,
'current_page': 1,
'results': results,
'next': None,
'start': 0,
'previous': None
}
responses.add(
responses.GET,
lms_url.rstrip('/') + '/api-admin/api/v1/api_access_request/?user__username={}'.format(user.username),
body=json.dumps(data),
content_type='application/json',
status=status
)
def mock_api_access_request_with_invalid_data(self, lms_url, user, status=200, response_overrides=None): def mock_api_access_request_with_invalid_data(self, lms_url, user, status=200, response_overrides=None):
""" """
Mock the api access requests endpoint response of the LMS. Mock the api access requests endpoint response of the LMS.
......
...@@ -2,6 +2,7 @@ import logging ...@@ -2,6 +2,7 @@ import logging
import mock import mock
import responses import responses
from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
from course_discovery.apps.core.api_client import lms from course_discovery.apps.core.api_client import lms
...@@ -12,7 +13,6 @@ from course_discovery.apps.core.tests.utils import MockLoggingHandler ...@@ -12,7 +13,6 @@ from course_discovery.apps.core.tests.utils import MockLoggingHandler
class TestLMSAPIClient(LMSAPIClientMixin, TestCase): class TestLMSAPIClient(LMSAPIClientMixin, TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(TestLMSAPIClient, cls).setUpClass() super(TestLMSAPIClient, cls).setUpClass()
...@@ -24,7 +24,6 @@ class TestLMSAPIClient(LMSAPIClientMixin, TestCase): ...@@ -24,7 +24,6 @@ class TestLMSAPIClient(LMSAPIClientMixin, TestCase):
@mock.patch.object(Partner, 'access_token', return_value='JWT fake') @mock.patch.object(Partner, 'access_token', return_value='JWT fake')
def setUp(self, mock_access_token): # pylint: disable=unused-argument def setUp(self, mock_access_token): # pylint: disable=unused-argument
super(TestLMSAPIClient, self).setUp() super(TestLMSAPIClient, self).setUp()
# Reset mock logger for each test. # Reset mock logger for each test.
self.log_handler.reset() self.log_handler.reset()
...@@ -44,6 +43,7 @@ class TestLMSAPIClient(LMSAPIClientMixin, TestCase): ...@@ -44,6 +43,7 @@ class TestLMSAPIClient(LMSAPIClientMixin, TestCase):
'site': 1, 'site': 1,
'contacted': True 'contacted': True
} }
cache.clear()
@responses.activate @responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake') @mock.patch.object(Partner, 'access_token', return_value='JWT fake')
...@@ -63,12 +63,9 @@ class TestLMSAPIClient(LMSAPIClientMixin, TestCase): ...@@ -63,12 +63,9 @@ class TestLMSAPIClient(LMSAPIClientMixin, TestCase):
Verify that `get_api_access_request` returns None when api_access_request Verify that `get_api_access_request` returns None when api_access_request
API endpoint is not available. API endpoint is not available.
""" """
self.mock_api_access_request( self.mock_api_access_request(self.partner.lms_url, self.user, status=404)
self.partner.lms_url, self.user, status=404
)
assert self.lms.get_api_access_request(self.user) is None assert self.lms.get_api_access_request(self.user) is None
assert 'Failed to fetch API Access Request from LMS for user "%s".' % self.user.username in \ assert 'HttpNotFoundError' in self.log_messages['error'][0]
self.log_messages['error']
@responses.activate @responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake') @mock.patch.object(Partner, 'access_token', return_value='JWT fake')
...@@ -81,15 +78,14 @@ class TestLMSAPIClient(LMSAPIClientMixin, TestCase): ...@@ -81,15 +78,14 @@ class TestLMSAPIClient(LMSAPIClientMixin, TestCase):
self.partner.lms_url, self.user self.partner.lms_url, self.user
) )
assert self.lms.get_api_access_request(self.user) is None assert self.lms.get_api_access_request(self.user) is None
assert 'APIAccessRequest model not found for user [%s].' % self.user.username in \ assert 'KeyError' in self.log_messages['error'][0]
self.log_messages['info']
@responses.activate @responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake') @mock.patch.object(Partner, 'access_token', return_value='JWT fake')
def test_get_api_access_request_with_invalid_response(self, mock_access_token): # pylint: disable=unused-argument def test_get_api_access_request_with_invalid_response(self, mock_access_token): # pylint: disable=unused-argument
""" """
Verify that `get_api_access_request` returns None when api_access_request Verify that `get_api_access_request` returns None when api_access_request
API endpoint is not available. returns an invalid response.
""" """
# API response without proper paginated structure. # API response without proper paginated structure.
# Following is an invalid response. # Following is an invalid response.
...@@ -111,8 +107,53 @@ class TestLMSAPIClient(LMSAPIClientMixin, TestCase): ...@@ -111,8 +107,53 @@ class TestLMSAPIClient(LMSAPIClientMixin, TestCase):
self.partner.lms_url, self.user, response_overrides=sample_invalid_response self.partner.lms_url, self.user, response_overrides=sample_invalid_response
) )
assert self.lms.get_api_access_request(self.user) is None assert self.lms.get_api_access_request(self.user) is None
assert 'APIAccessRequest model not found for user [%s].' % self.user.username in \ assert 'KeyError' in self.log_messages['error'][0]
self.log_messages['info']
@responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
def test_get_api_access_request_with_no_results(self, mock_access_token): # pylint: disable=unused-argument
"""
Verify that `get_api_access_request` returns None when api_access_request
API returns no results.
"""
self.mock_api_access_request_with_configurable_results(
self.partner.lms_url, self.user, results=[]
)
assert self.lms.get_api_access_request(self.user) is None
assert 'No results for ApiAccessRequest for user [%s].' % self.user.username in self.log_messages['info']
@responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
def test_get_api_access_request_cache_for_user_with_no_results(self,
mock_access_token): # pylint: disable=unused-argument
"""
Verify that `get_api_access_request` returns None when api_access_request
API returns no results and returns the cached result on another call with
the same user.
"""
self.mock_api_access_request_with_configurable_results(
self.partner.lms_url, self.user, results=[]
)
assert self.lms.get_api_access_request(self.user) is None
assert 'No results for ApiAccessRequest for user [%s].' % self.user.username in self.log_messages['info']
assert self.lms.get_api_access_request(self.user) is None
assert len(responses.calls) == 1
@responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
def test_get_api_access_request_cache_hit(self,
mock_access_token): # pylint: disable=unused-argument
"""
Verify that `get_api_access_request` returns the correct value and then
returns the cached results on another call with the same user.
"""
self.mock_api_access_request(
self.partner.lms_url, self.user, api_access_request_overrides=self.response
)
assert self.lms.get_api_access_request(self.user) == self.response
assert self.lms.get_api_access_request(self.user) == self.response
assert len(responses.calls) == 1
@responses.activate @responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake') @mock.patch.object(Partner, 'access_token', return_value='JWT fake')
...@@ -121,50 +162,39 @@ class TestLMSAPIClient(LMSAPIClientMixin, TestCase): ...@@ -121,50 +162,39 @@ class TestLMSAPIClient(LMSAPIClientMixin, TestCase):
Verify that `get_api_access_request` logs a warning message and returns the first result Verify that `get_api_access_request` logs a warning message and returns the first result
if endpoint returns multiple api-access-requests for a user. if endpoint returns multiple api-access-requests for a user.
""" """
# API response without proper paginated structure. results = [
# Following is an invalid response. {
sample_response_with_multiple_users = { 'id': 1,
'count': 2, 'created': '2017-09-25T08:37:05.872566Z',
'num_pages': 1, 'modified': '2017-09-25T08:37:47.412496Z',
'current_page': 1, 'user': 1,
'results': 'status': 'declined',
[ 'website': 'https://example.com/',
{ 'reason': 'Example Reason',
'id': 1, 'company_name': 'Test Company',
'created': '2017-09-25T08:37:05.872566Z', 'company_address': 'Example Address',
'modified': '2017-09-25T08:37:47.412496Z', 'site': 1,
'user': 1, 'contacted': True
'status': 'declined', },
'website': 'https://example.com/', {
'reason': 'Example Reason', 'id': 2,
'company_name': 'Test Company', 'created': '2017-10-25T08:37:05.872566Z',
'company_address': 'Example Address', 'modified': '2017-10-25T08:37:47.412496Z',
'site': 1, 'user': 1,
'contacted': True 'status': 'approved',
}, 'website': 'https://example.com/',
{ 'reason': 'Example Reason',
'id': 2, 'company_name': 'Test Company',
'created': '2017-10-25T08:37:05.872566Z', 'company_address': 'Example Address',
'modified': '2017-10-25T08:37:47.412496Z', 'site': 1,
'user': 1, 'contacted': True
'status': 'approved', },
'website': 'https://example.com/', ]
'reason': 'Example Reason',
'company_name': 'Test Company', self.mock_api_access_request_with_configurable_results(
'company_address': 'Example Address', self.partner.lms_url, self.user, results=results
'site': 1,
'contacted': True
},
],
'next': None,
'start': 0,
'previous': None
}
self.mock_api_access_request_with_invalid_data(
self.partner.lms_url, self.user, response_overrides=sample_response_with_multiple_users
) )
assert self.lms.get_api_access_request(self.user)['company_name'] == 'Test Company' assert self.lms.get_api_access_request(self.user)['company_name'] == 'Test Company'
assert 'Multiple APIAccessRequest models returned from LMS API for user [%s].' % self.user.username in \ assert 'Multiple ApiAccessRequest models returned from LMS API for user [%s].' % self.user.username in \
self.log_messages['warning'] self.log_messages['warning']
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment