Commit 70eaf189 by Ahsan Ulhaq

caching for requests to credentials service

ECOM-3278
parent 51815136
...@@ -21,8 +21,6 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule ...@@ -21,8 +21,6 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.create_programs_config()
self.staff = UserFactory(is_staff=True) self.staff = UserFactory(is_staff=True)
self.client.login(username=self.staff.username, password='test') self.client.login(username=self.staff.username, password='test')
...@@ -50,6 +48,7 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule ...@@ -50,6 +48,7 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule
student = UserFactory(is_staff=False) student = UserFactory(is_staff=False)
self.client.login(username=student.username, password='test') self.client.login(username=student.username, password='test')
self.create_programs_config()
self.mock_programs_api() self.mock_programs_api()
response = self.client.get(self.studio_home) response = self.client.get(self.studio_home)
...@@ -60,6 +59,7 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule ...@@ -60,6 +59,7 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule
"""Verify that the programs tab and creation button can be rendered when config is enabled.""" """Verify that the programs tab and creation button can be rendered when config is enabled."""
# When no data is provided, expect creation prompt. # When no data is provided, expect creation prompt.
self.create_programs_config()
self.mock_programs_api(data={'results': []}) self.mock_programs_api(data={'results': []})
response = self.client.get(self.studio_home) response = self.client.get(self.studio_home)
......
...@@ -2432,7 +2432,7 @@ def _get_xseries_credentials(user): ...@@ -2432,7 +2432,7 @@ def _get_xseries_credentials(user):
programs_credentials = get_user_program_credentials(user) programs_credentials = get_user_program_credentials(user)
credentials_data = [] credentials_data = []
for program in programs_credentials: for program in programs_credentials:
if program.get('status') == 'active': if program.get('category') == 'xseries':
try: try:
program_data = { program_data = {
'display_name': program['name'], 'display_name': program['name'],
......
...@@ -102,6 +102,8 @@ urlpatterns = ( ...@@ -102,6 +102,8 @@ urlpatterns = (
url(r'^api/commerce/', include('commerce.api.urls', namespace='commerce_api')), url(r'^api/commerce/', include('commerce.api.urls', namespace='commerce_api')),
url(r'^api/credit/', include('openedx.core.djangoapps.credit.urls', app_name="credit", namespace='credit')), url(r'^api/credit/', include('openedx.core.djangoapps.credit.urls', app_name="credit", namespace='credit')),
url(r'^rss_proxy/', include('rss_proxy.urls', namespace='rss_proxy')), url(r'^rss_proxy/', include('rss_proxy.urls', namespace='rss_proxy')),
url(r'^api/organizations/', include(
'openedx.core.djangoapps.organization_api.urls', app_name='organization_api', namespace='organization_api')),
) )
if settings.FEATURES["ENABLE_COMBINED_LOGIN_REGISTRATION"]: if settings.FEATURES["ENABLE_COMBINED_LOGIN_REGISTRATION"]:
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('credentials', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='credentialsapiconfig',
name='cache_ttl',
field=models.PositiveIntegerField(default=0, help_text='Specified in seconds. Enable caching by setting this to a value greater than 0.', verbose_name='Cache Time To Live'),
),
]
...@@ -17,6 +17,7 @@ class CredentialsApiConfig(ConfigurationModel): ...@@ -17,6 +17,7 @@ class CredentialsApiConfig(ConfigurationModel):
""" """
OAUTH2_CLIENT_NAME = 'credentials' OAUTH2_CLIENT_NAME = 'credentials'
API_NAME = 'credentials' API_NAME = 'credentials'
CACHE_KEY = 'credentials.api.data'
internal_service_url = models.URLField(verbose_name=_("Internal Service URL")) internal_service_url = models.URLField(verbose_name=_("Internal Service URL"))
public_service_url = models.URLField(verbose_name=_("Public Service URL")) public_service_url = models.URLField(verbose_name=_("Public Service URL"))
...@@ -35,6 +36,13 @@ class CredentialsApiConfig(ConfigurationModel): ...@@ -35,6 +36,13 @@ class CredentialsApiConfig(ConfigurationModel):
"Enable authoring of Credential Service credentials in Studio." "Enable authoring of Credential Service credentials in Studio."
) )
) )
cache_ttl = models.PositiveIntegerField(
verbose_name=_("Cache Time To Live"),
default=0,
help_text=_(
"Specified in seconds. Enable caching by setting this to a value greater than 0."
)
)
def __unicode__(self): def __unicode__(self):
return self.public_api_url return self.public_api_url
...@@ -67,3 +75,8 @@ class CredentialsApiConfig(ConfigurationModel): ...@@ -67,3 +75,8 @@ class CredentialsApiConfig(ConfigurationModel):
be enabled or not. be enabled or not.
""" """
return self.enabled and self.enable_studio_authoring return self.enabled and self.enable_studio_authoring
@property
def is_cache_enabled(self):
"""Whether responses from the Credentials API will be cached."""
return self.cache_ttl > 0
...@@ -15,6 +15,7 @@ class CredentialsApiConfigMixin(object): ...@@ -15,6 +15,7 @@ class CredentialsApiConfigMixin(object):
'public_service_url': 'http://public.credentials.org/', 'public_service_url': 'http://public.credentials.org/',
'enable_learner_issuance': True, 'enable_learner_issuance': True,
'enable_studio_authoring': True, 'enable_studio_authoring': True,
'cache_ttl': 0,
} }
def create_credentials_config(self, **kwargs): def create_credentials_config(self, **kwargs):
...@@ -99,7 +100,7 @@ class CredentialsDataMixin(object): ...@@ -99,7 +100,7 @@ class CredentialsDataMixin(object):
} }
CREDENTIALS_NEXT_API_RESPONSE = { CREDENTIALS_NEXT_API_RESPONSE = {
"next": 'next_page_url', "next": None,
"results": [ "results": [
{ {
"id": 7, "id": 7,
...@@ -140,9 +141,9 @@ class CredentialsDataMixin(object): ...@@ -140,9 +141,9 @@ class CredentialsDataMixin(object):
body = json.dumps(data) body = json.dumps(data)
if is_next_page: if is_next_page:
next_page_data = self.CREDENTIALS_NEXT_API_RESPONSE
next_page_body = json.dumps(next_page_data)
next_page_url = internal_api_url + '/user_credentials/?page=2&username=' + user.username next_page_url = internal_api_url + '/user_credentials/?page=2&username=' + user.username
self.CREDENTIALS_NEXT_API_RESPONSE['next'] = next_page_url
next_page_body = json.dumps(self.CREDENTIALS_NEXT_API_RESPONSE)
httpretty.register_uri( httpretty.register_uri(
httpretty.GET, next_page_url, body=body, content_type='application/json', status=status_code httpretty.GET, next_page_url, body=body, content_type='application/json', status=status_code
) )
......
"""Tests covering Credentials utilities.""" """Tests covering Credentials utilities."""
from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
import httpretty import httpretty
from oauth2_provider.tests.factories import ClientFactory from oauth2_provider.tests.factories import ClientFactory
...@@ -26,6 +27,8 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin ...@@ -26,6 +27,8 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.user = UserFactory() self.user = UserFactory()
cache.clear()
@httpretty.activate @httpretty.activate
def test_get_user_credentials(self): def test_get_user_credentials(self):
"""Verify user credentials data can be retrieve.""" """Verify user credentials data can be retrieve."""
...@@ -35,6 +38,30 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin ...@@ -35,6 +38,30 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin
actual = get_user_credentials(self.user) actual = get_user_credentials(self.user)
self.assertEqual(actual, self.CREDENTIALS_API_RESPONSE['results']) self.assertEqual(actual, self.CREDENTIALS_API_RESPONSE['results'])
@httpretty.activate
def test_get_user_credentials_caching(self):
"""Verify that when enabled, the cache is used for non-staff users."""
self.create_credentials_config(cache_ttl=1)
self.mock_credentials_api(self.user)
# Warm up the cache.
get_user_credentials(self.user)
# Hit the cache.
get_user_credentials(self.user)
# Verify only one request was made.
self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
staff_user = UserFactory(is_staff=True)
# Hit the Credentials API twice.
for _ in range(2):
get_user_credentials(staff_user)
# Verify that three requests have been made (one for student, two for staff).
self.assertEqual(len(httpretty.httpretty.latest_requests), 3)
def test_get_user_program_credentials_issuance_disable(self): def test_get_user_program_credentials_issuance_disable(self):
"""Verify that user program credentials cannot be retrieved if issuance is disabled.""" """Verify that user program credentials cannot be retrieved if issuance is disabled."""
self.create_credentials_config(enable_learner_issuance=False) self.create_credentials_config(enable_learner_issuance=False)
...@@ -50,6 +77,28 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin ...@@ -50,6 +77,28 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin
self.assertEqual(actual, []) self.assertEqual(actual, [])
@httpretty.activate @httpretty.activate
def test_get_user_programs_credentials(self):
"""Verify program credentials data can be retrieved and parsed correctly."""
# create credentials and program configuration
credentials_config = self.create_credentials_config()
self.create_programs_config()
# Mocking the API responses from programs and credentials
self.mock_programs_api()
self.mock_credentials_api(self.user, reset_url=False)
actual = get_user_program_credentials(self.user)
expected = self.PROGRAMS_API_RESPONSE['results']
expected[0]['credential_url'] = \
credentials_config.public_service_url + 'credentials/' + self.PROGRAMS_CREDENTIALS_DATA[0]['uuid']
expected[1]['credential_url'] = \
credentials_config.public_service_url + 'credentials/' + self.PROGRAMS_CREDENTIALS_DATA[1]['uuid']
# checking response from API is as expected
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
@httpretty.activate
def test_get_user_program_credentials_revoked(self): def test_get_user_program_credentials_revoked(self):
"""Verify behavior if credential revoked.""" """Verify behavior if credential revoked."""
self.create_credentials_config() self.create_credentials_config()
...@@ -68,21 +117,3 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin ...@@ -68,21 +117,3 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin
self.mock_credentials_api(self.user, data=credential_data) self.mock_credentials_api(self.user, data=credential_data)
actual = get_user_program_credentials(self.user) actual = get_user_program_credentials(self.user)
self.assertEqual(actual, []) self.assertEqual(actual, [])
@httpretty.activate
def test_get_user_programs_credentials(self):
"""Verify program credentials data can be retrieved and parsed correctly."""
credentials_config = self.create_credentials_config()
self.create_programs_config()
self.mock_programs_api()
self.mock_credentials_api(self.user, reset_url=False)
actual = get_user_program_credentials(self.user)
expected = self.PROGRAMS_API_RESPONSE['results']
expected[0]['credential_url'] = \
credentials_config.public_service_url + 'credentials/' + self.PROGRAMS_CREDENTIALS_DATA[0]['uuid']
expected[1]['credential_url'] = \
credentials_config.public_service_url + 'credentials/' + self.PROGRAMS_CREDENTIALS_DATA[1]['uuid']
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
httpretty.reset()
...@@ -4,7 +4,7 @@ import logging ...@@ -4,7 +4,7 @@ import logging
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.programs.utils import get_programs_for_credentials from openedx.core.djangoapps.programs.utils import get_programs_for_credentials
from openedx.core.lib.api_utils import get_api_data from openedx.core.lib.edx_api_utils import get_edx_api_data
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -20,8 +20,13 @@ def get_user_credentials(user): ...@@ -20,8 +20,13 @@ def get_user_credentials(user):
""" """
credential_configuration = CredentialsApiConfig.current() credential_configuration = CredentialsApiConfig.current()
user_query = {'username': user.username} user_query = {'username': user.username}
credentials = get_api_data( # Bypass caching for staff users, who may be generating credentials and
credential_configuration, user, credential_configuration.API_NAME, 'user_credentials', querystring=user_query # want to see them displayed immediately.
use_cache = credential_configuration.is_cache_enabled and not user.is_staff
cache_key = credential_configuration.CACHE_KEY + '.' + user.username if use_cache else None
credentials = get_edx_api_data(
credential_configuration, user, 'user_credentials', querystring=user_query, cache_key=cache_key
) )
return credentials return credentials
......
"""
Organization API.
WARNING: This API is intended to move into the 'edx-organizations' repo.
https://github.com/edx/edx-organizations
"""
"""
Organizations API views.
"""
from rest_framework import permissions
from rest_framework.authentication import SessionAuthentication
from rest_framework.response import Response
from rest_framework.status import HTTP_404_NOT_FOUND
from rest_framework.views import APIView
from rest_framework_oauth.authentication import OAuth2Authentication
from util.organizations_helpers import get_organization_by_short_name
class OrganizationsView(APIView):
"""
View to get organization information.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, organization_key):
"""
Return organization information related to provided organization
key/short_name.
"""
organization = get_organization_by_short_name(organization_key)
if organization:
logo = organization.get('logo')
organization_data = {
'name': organization.get('name', ''),
'short_name': organization.get('short_name', ''),
'description': organization.get('description', ''),
'logo': request.build_absolute_uri(logo.url) if logo else ''
}
return Response(organization_data)
return Response(status=HTTP_404_NOT_FOUND)
"""
Tests for organization API.
"""
import ddt
import json
import unittest
from django.conf import settings
from django.core.urlresolvers import reverse, NoReverseMatch
from django.test import TestCase
from oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory
from student.tests.factories import UserFactory
from util import organizations_helpers
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class OrganizationsAPITests(TestCase):
"""
Tests for the organizations API endpoints.
GET /api/organizations/v1/organization/:org_key/
"""
def setUp(self):
"""
Test setup for the organizations API.
"""
super(OrganizationsAPITests, self).setUp()
self.user_password = 'password'
self.user = UserFactory(password=self.user_password)
self.test_org_key = 'test_organization'
self.test_org_url = self._generate_org_url(self.test_org_key)
def _create_test_organization(self, org_key=None):
"""
Helper method to create a test organization with the provide 'org_key' and
returns the url to access it.
"""
if org_key is None:
org_key = self.test_org_key
test_organization_data = {
'name': 'Test Organization',
'short_name': org_key,
'description': 'Test Organization Description',
'logo': '/test_logo.png/'
}
organizations_helpers.add_organization(organization_data=test_organization_data)
return self._generate_org_url(org_key)
def _generate_org_url(self, org_key):
"""
Helper method to generate the url to get organization data for a
specific organization key.
"""
return reverse(
'organization_api:get_organization', kwargs={'organization_key': org_key}
)
def test_authentication_required(self):
"""
Verify that the endpoint requires authentication.
"""
response = self.client.get(self.test_org_url)
self.assertEqual(response.status_code, 401)
def test_session_auth(self):
"""
Verify that the endpoint supports session authentication.
"""
self.client.login(username=self.user.username, password=self.user_password)
response = self.client.get(self.test_org_url)
# verify that the test org does not exist
self.assertEqual(response.status_code, 404)
# add a test organization
self._create_test_organization()
# verify that the organization api return data in correct format
response = self.client.get(self.test_org_url)
self.assertEqual(response.status_code, 200)
expected_output = {
'name': 'Test Organization',
'short_name': 'test_organization',
'description': 'Test Organization Description',
'logo': 'http://testserver/test_logo.png/'
}
self.assertEqual(json.loads(response.content), expected_output)
def test_oauth(self):
"""
Verify that the organization API supports OAuth.
"""
oauth_client = ClientFactory.create()
access_token = AccessTokenFactory.create(user=self.user, client=oauth_client).token
headers = {
'HTTP_AUTHORIZATION': 'Bearer ' + access_token
}
response = self.client.get(self.test_org_url, **headers)
# verify that the test org does not exist
self.assertEqual(response.status_code, 404)
# add a test organization
self._create_test_organization()
# verify that the organization api return data in correct format
response = self.client.get(self.test_org_url, **headers)
self.assertEqual(response.status_code, 200)
expected_output = {
'name': 'Test Organization',
'short_name': 'test_organization',
'description': 'Test Organization Description',
'logo': 'http://testserver/test_logo.png/'
}
self.assertEqual(json.loads(response.content), expected_output)
@ddt.data("test_org's", "test_org*", "test(org)", "!test", "test org")
def test_with_invalid_org_key(self, invalid_org_key):
"""
Verify that organization url does not match for invalid org key.
"""
with self.assertRaises(NoReverseMatch):
self._generate_org_url(invalid_org_key)
"""
URLs for the organization app.
"""
from django.conf.urls import patterns, url
from openedx.core.djangoapps.organization_api.api import views
ORGANIZATION_KEY_PATTERN = r"(?P<organization_key>((?![\^'\!\(\)\*\s]).)*)"
urlpatterns = patterns(
'',
url(
r'^v0/organization/{}/$'.format(ORGANIZATION_KEY_PATTERN),
views.OrganizationsView.as_view(),
name='get_organization'
),
)
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
import httpretty import httpretty
import mock
from oauth2_provider.tests.factories import ClientFactory from oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL from provider.constants import CONFIDENTIAL
...@@ -41,6 +42,56 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, ...@@ -41,6 +42,56 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
self.assertEqual(len(httpretty.httpretty.latest_requests), 1) self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
@httpretty.activate @httpretty.activate
def test_get_programs_caching(self):
"""Verify that when enabled, the cache is used for non-staff users."""
self.create_programs_config(cache_ttl=1)
self.mock_programs_api()
# Warm up the cache.
get_programs(self.user)
# Hit the cache.
get_programs(self.user)
# Verify only one request was made.
self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
staff_user = UserFactory(is_staff=True)
# Hit the Programs API twice.
for _ in range(2):
get_programs(staff_user)
# Verify that three requests have been made (one for student, two for staff).
self.assertEqual(len(httpretty.httpretty.latest_requests), 3)
def test_get_programs_programs_disabled(self):
"""Verify behavior when programs is disabled."""
self.create_programs_config(enabled=False)
actual = get_programs(self.user)
self.assertEqual(actual, [])
@mock.patch('edx_rest_api_client.client.EdxRestApiClient.__init__')
def test_get_programs_client_initialization_failure(self, mock_init):
"""Verify behavior when API client fails to initialize."""
self.create_programs_config()
mock_init.side_effect = Exception
actual = get_programs(self.user)
self.assertEqual(actual, [])
self.assertTrue(mock_init.called)
@httpretty.activate
def test_get_programs_data_retrieval_failure(self):
"""Verify behavior when data can't be retrieved from Programs."""
self.create_programs_config()
self.mock_programs_api(status_code=500)
actual = get_programs(self.user)
self.assertEqual(actual, [])
@httpretty.activate
def test_get_programs_for_dashboard(self): def test_get_programs_for_dashboard(self):
"""Verify programs data can be retrieved and parsed correctly.""" """Verify programs data can be retrieved and parsed correctly."""
self.create_programs_config() self.create_programs_config()
......
...@@ -4,7 +4,7 @@ from urlparse import urljoin ...@@ -4,7 +4,7 @@ from urlparse import urljoin
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.lib.api_utils import get_api_data from openedx.core.lib.edx_api_utils import get_edx_api_data
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -24,9 +24,11 @@ def get_programs(user): ...@@ -24,9 +24,11 @@ def get_programs(user):
""" """
programs_config = ProgramsApiConfig.current() programs_config = ProgramsApiConfig.current()
# Bypass caching for staff users, who may be creating Programs and want to see them displayed immediately. # Bypass caching for staff users, who may be creating Programs and want
use_cache = programs_config.is_cache_enabled and not user.is_staff # to see them displayed immediately.
return get_api_data(programs_config, user, programs_config.API_NAME, 'programs', use_cache=use_cache) cache_key = programs_config.CACHE_KEY if programs_config.is_cache_enabled and not user.is_staff else None
return get_edx_api_data(programs_config, user, 'programs', cache_key=cache_key)
def get_programs_for_dashboard(user, course_keys): def get_programs_for_dashboard(user, course_keys):
......
...@@ -11,19 +11,20 @@ from openedx.core.lib.token_utils import get_id_token ...@@ -11,19 +11,20 @@ from openedx.core.lib.token_utils import get_id_token
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def get_api_data(api_config, user, api_name, resource, querystring=None, use_cache=False): def get_edx_api_data(api_config, user, resource, querystring=None, cache_key=None):
"""Fetch the data from the API using provided API Configuration and """Fetch data from an API using provided API configuration and resource
resource. name.
Arguments: Arguments:
api_config: The configuration which will be user for requesting data. api_config (ConfigurationModel): The configuration model governing
interaction with the API.
user (User): The user to authenticate as when requesting data. user (User): The user to authenticate as when requesting data.
api_name: Name fo the api to be use for logging. resource(str): Name of the API resource for which data is being
resource: API resource to from where data will be requested. requested.
querystring: Querystring parameters that might be required to request querystring(dict): Querystring parameters that might be required to
data. request data.
use_cache: Will be used to decide whether to cache the response data cache_key(str): Where to cache retrieved data. Omitting this will cause the
or not. cache to be bypassed.
Returns: Returns:
list of dict, representing data returned by the API. list of dict, representing data returned by the API.
...@@ -31,23 +32,19 @@ def get_api_data(api_config, user, api_name, resource, querystring=None, use_cac ...@@ -31,23 +32,19 @@ def get_api_data(api_config, user, api_name, resource, querystring=None, use_cac
no_data = [] no_data = []
if not api_config.enabled: if not api_config.enabled:
log.warning('%s configuration is disabled.', api_name) log.warning('%s configuration is disabled.', api_config.API_NAME)
return no_data return no_data
if use_cache: if cache_key:
if api_config.CACHE_KEY: cached = cache.get(cache_key)
cached = cache.get(api_config.CACHE_KEY) if cached is not None:
if cached is not None: return cached
return cached
else:
log.warning('No cache key available for %s configuration.', api_name)
return no_data
try: try:
jwt = get_id_token(user, api_config.OAUTH2_CLIENT_NAME) jwt = get_id_token(user, api_config.OAUTH2_CLIENT_NAME)
api = EdxRestApiClient(api_config.internal_api_url, jwt=jwt) api = EdxRestApiClient(api_config.internal_api_url, jwt=jwt)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
log.exception('Failed to initialize the %s API client.', api_name) log.exception('Failed to initialize the %s API client.', api_config.API_NAME)
return no_data return no_data
try: try:
...@@ -63,10 +60,10 @@ def get_api_data(api_config, user, api_name, resource, querystring=None, use_cac ...@@ -63,10 +60,10 @@ def get_api_data(api_config, user, api_name, resource, querystring=None, use_cac
results += response.get('results', no_data) results += response.get('results', no_data)
next_page = response.get('next', None) next_page = response.get('next', None)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
log.exception('Failed to retrieve data from the %s API.', api_name) log.exception('Failed to retrieve data from the %s API.', api_config.API_NAME)
return no_data return no_data
if use_cache: if cache_key:
cache.set(api_config.CACHE_KEY, results, api_config.cache_ttl) cache.set(cache_key, results, api_config.cache_ttl)
return results return results
...@@ -10,13 +10,13 @@ from openedx.core.djangoapps.credentials.models import CredentialsApiConfig ...@@ -10,13 +10,13 @@ from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
from openedx.core.lib.api_utils import get_api_data from openedx.core.lib.edx_api_utils import get_edx_api_data
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
class TestApiDataRetrieval(CredentialsApiConfigMixin, CredentialsDataMixin, ProgramsApiConfigMixin, ProgramsDataMixin, class TestApiDataRetrieval(CredentialsApiConfigMixin, CredentialsDataMixin, ProgramsApiConfigMixin, ProgramsDataMixin,
TestCase): TestCase):
"""Test data retrieval from the api util function.""" """Test utility for API data retrieval."""
def setUp(self): def setUp(self):
super(TestApiDataRetrieval, self).setUp() super(TestApiDataRetrieval, self).setUp()
ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
...@@ -26,12 +26,12 @@ class TestApiDataRetrieval(CredentialsApiConfigMixin, CredentialsDataMixin, Prog ...@@ -26,12 +26,12 @@ class TestApiDataRetrieval(CredentialsApiConfigMixin, CredentialsDataMixin, Prog
cache.clear() cache.clear()
@httpretty.activate @httpretty.activate
def test_get_api_data_programs(self): def test_get_edx_api_data_programs(self):
"""Verify programs data can be retrieve using get_api_data.""" """Verify programs data can be retrieved using get_edx_api_data."""
program_config = self.create_programs_config() program_config = self.create_programs_config()
self.mock_programs_api() self.mock_programs_api()
actual = get_api_data(program_config, self.user, 'programs', 'programs') actual = get_edx_api_data(program_config, self.user, 'programs')
self.assertEqual( self.assertEqual(
actual, actual,
self.PROGRAMS_API_RESPONSE['results'] self.PROGRAMS_API_RESPONSE['results']
...@@ -40,75 +40,54 @@ class TestApiDataRetrieval(CredentialsApiConfigMixin, CredentialsDataMixin, Prog ...@@ -40,75 +40,54 @@ class TestApiDataRetrieval(CredentialsApiConfigMixin, CredentialsDataMixin, Prog
# Verify the API was actually hit (not the cache). # Verify the API was actually hit (not the cache).
self.assertEqual(len(httpretty.httpretty.latest_requests), 1) self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
@httpretty.activate def test_get_edx_api_data_disable_config(self):
def test_get_api_data_credentials(self): """Verify no data is retrieved if configuration is disabled."""
"""Verify credentials data can be retrieve using get_api_data."""
credentials_config = self.create_credentials_config()
self.mock_credentials_api(self.user)
querystring = {'username': self.user.username}
actual = get_api_data(credentials_config, self.user, 'credentials', 'user_credentials', querystring=querystring)
self.assertEqual(
actual,
self.CREDENTIALS_API_RESPONSE['results']
)
def test_get_api_data_disable_config(self):
"""Verify no data is retrieve if configuration is disabled."""
program_config = self.create_programs_config(enabled=False) program_config = self.create_programs_config(enabled=False)
actual = get_api_data(program_config, self.user, 'programs', 'programs') actual = get_edx_api_data(program_config, self.user, 'programs')
self.assertEqual(actual, []) self.assertEqual(actual, [])
@httpretty.activate @httpretty.activate
def test_get_api_data_cache(self): def test_get_edx_api_data_cache(self):
"""Verify that when enabled, the cache is used.""" """Verify that when enabled, the cache is used."""
program_config = self.create_programs_config(cache_ttl=1) program_config = self.create_programs_config(cache_ttl=1)
self.mock_programs_api() self.mock_programs_api()
# Warm up the cache. # Warm up the cache.
get_api_data(program_config, self.user, 'programs', 'programs', use_cache=True) get_edx_api_data(program_config, self.user, 'programs', cache_key='test.key')
# Hit the cache. # Hit the cache.
get_api_data(program_config, self.user, 'programs', 'programs', use_cache=True) get_edx_api_data(program_config, self.user, 'programs', cache_key='test.key')
# Verify only one request was made. # Verify only one request was made.
self.assertEqual(len(httpretty.httpretty.latest_requests), 1) self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
def test_get_api_data_without_cache_key(self):
"""Verify that when cache enabled without cache key then no data is retrieved."""
ProgramsApiConfig.CACHE_KEY = None
program_config = self.create_programs_config(cache_ttl=1)
actual = get_api_data(program_config, self.user, 'programs', 'programs', use_cache=True)
self.assertEqual(actual, [])
@mock.patch('edx_rest_api_client.client.EdxRestApiClient.__init__') @mock.patch('edx_rest_api_client.client.EdxRestApiClient.__init__')
def test_get_api_data_client_initialization_failure(self, mock_init): def test_get_edx_api_data_client_initialization_failure(self, mock_init):
"""Verify behavior when API client fails to initialize.""" """Verify behavior when API client fails to initialize."""
program_config = self.create_programs_config() program_config = self.create_programs_config()
mock_init.side_effect = Exception mock_init.side_effect = Exception
actual = get_api_data(program_config, self.user, 'programs', 'programs') actual = get_edx_api_data(program_config, self.user, 'programs')
self.assertEqual(actual, []) self.assertEqual(actual, [])
self.assertTrue(mock_init.called) self.assertTrue(mock_init.called)
@httpretty.activate @httpretty.activate
def test_get_api_data_retrieval_failure(self): def test_get_edx_api_data_retrieval_failure(self):
"""Verify behavior when data can't be retrieved from API.""" """Verify behavior when data can't be retrieved from API."""
program_config = self.create_programs_config() program_config = self.create_programs_config()
self.mock_programs_api(status_code=500) self.mock_programs_api(status_code=500)
actual = get_api_data(program_config, self.user, 'programs', 'programs') actual = get_edx_api_data(program_config, self.user, 'programs')
self.assertEqual(actual, []) self.assertEqual(actual, [])
@httpretty.activate @httpretty.activate
def test_get_api_data_multiple_page(self): def test_get_edx_api_data_multiple_page(self):
"""Verify that all data is retrieve for multiple page response.""" """Verify that all data is retrieve for multiple page response."""
credentials_config = self.create_credentials_config() credentials_config = self.create_credentials_config()
self.mock_credentials_api(self.user, is_next_page=True) self.mock_credentials_api(self.user, is_next_page=True)
querystring = {'username': self.user.username} querystring = {'username': self.user.username}
actual = get_api_data(credentials_config, self.user, 'credentials', 'user_credentials', querystring=querystring) actual = get_edx_api_data(credentials_config, self.user, 'user_credentials', querystring=querystring)
expected_data = self.CREDENTIALS_NEXT_API_RESPONSE['results'] + self.CREDENTIALS_API_RESPONSE['results'] expected_data = self.CREDENTIALS_NEXT_API_RESPONSE['results'] + self.CREDENTIALS_API_RESPONSE['results']
self.assertEqual(actual, expected_data) self.assertEqual(actual, expected_data)
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