Commit fa74b9f1 by Adam

Merge pull request #11339 from edx/merge-release-into-master

Merge release into master
parents 40ff300e 46638d7f
......@@ -29,7 +29,7 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule
@httpretty.activate
def test_programs_config_disabled(self):
"""Verify that the programs tab and creation button aren't rendered when config is disabled."""
self.create_programs_config(enable_studio_tab=False)
self.create_config(enable_studio_tab=False)
self.mock_programs_api()
response = self.client.get(self.studio_home)
......@@ -48,7 +48,7 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule
student = UserFactory(is_staff=False)
self.client.login(username=student.username, password='test')
self.create_programs_config()
self.create_config()
self.mock_programs_api()
response = self.client.get(self.studio_home)
......@@ -57,9 +57,9 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule
@httpretty.activate
def test_programs_displayed(self):
"""Verify that the programs tab and creation button can be rendered when config is enabled."""
self.create_config()
# When no data is provided, expect creation prompt.
self.create_programs_config()
self.mock_programs_api(data={'results': []})
response = self.client.get(self.studio_home)
......@@ -102,7 +102,7 @@ class TestProgramAuthoringView(ProgramsApiConfigMixin, SharedModuleStoreTestCase
def test_authoring_header(self):
"""Verify that the header contains the expected text."""
self.client.login(username=self.staff.username, password='test')
self.create_programs_config()
self.create_config()
response = self._assert_status(200)
self.assertIn("Program Administration", response.content)
......@@ -116,7 +116,7 @@ class TestProgramAuthoringView(ProgramsApiConfigMixin, SharedModuleStoreTestCase
self._assert_status(404)
# Enable Programs authoring interface
self.create_programs_config()
self.create_config()
student = UserFactory(is_staff=False)
self.client.login(username=student.username, password='test')
......@@ -134,13 +134,13 @@ class TestProgramsIdTokenView(ProgramsApiConfigMixin, SharedModuleStoreTestCase)
def test_config_disabled(self):
"""Ensure the endpoint returns 404 when Programs authoring is disabled."""
self.create_programs_config(enable_studio_tab=False)
self.create_config(enable_studio_tab=False)
response = self.client.get(self.path)
self.assertEqual(response.status_code, 404)
def test_not_logged_in(self):
"""Ensure the endpoint denies access to unauthenticated users."""
self.create_programs_config()
self.create_config()
self.client.logout()
response = self.client.get(self.path)
self.assertEqual(response.status_code, 302)
......@@ -152,7 +152,7 @@ class TestProgramsIdTokenView(ProgramsApiConfigMixin, SharedModuleStoreTestCase)
Ensure the endpoint responds with a valid JSON payload when authoring
is enabled.
"""
self.create_programs_config()
self.create_config()
response = self.client.get(self.path)
self.assertEqual(response.status_code, 200)
payload = json.loads(response.content)
......
......@@ -834,8 +834,6 @@ INSTALLED_APPS = (
# Microsite configuration application
'microsite_configuration',
# Credentials support
'openedx.core.djangoapps.credentials',
)
......
......@@ -132,7 +132,6 @@ DATABASES = {
# This hack disables migrations during tests. We want to create tables directly from the models for speed.
# See https://groups.google.com/d/msg/django-developers/PWPj3etj3-U/kCl6pMsQYYoJ.
MIGRATION_MODULES = {app: "app.migrations_not_used_in_tests" for app in INSTALLED_APPS}
MIGRATION_MODULES["credentials"] = "app.migrations_not_used_in_tests"
LMS_BASE = "localhost:8000"
FEATURES['PREVIEW_LMS_BASE'] = "preview"
......
......@@ -1035,7 +1035,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
CourseEnrollment.enroll(self.user, self.course_2.id, mode=course_mode)
self.client.login(username="jack", password="test")
self.create_programs_config()
self.create_config()
with patch('student.views.get_programs_for_dashboard') as mock_data:
mock_data.return_value = self._create_program_data(
......@@ -1068,7 +1068,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
CourseEnrollment.enroll(self.user, self.course_1.id, mode='verified')
self.client.login(username="jack", password="test")
self.create_programs_config()
self.create_config()
with patch(
'student.views.get_programs_for_dashboard',
......@@ -1098,7 +1098,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
CourseEnrollment.enroll(self.user, self.course_3.id, mode='honor')
self.client.login(username="jack", password="test")
self.create_programs_config()
self.create_config()
with patch('student.views.get_programs_for_dashboard') as mock_data:
mock_data.return_value = self._create_program_data(
......@@ -1119,7 +1119,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
CourseEnrollment.enroll(self.user, self.course_1.id)
self.client.login(username="jack", password="test")
self.create_programs_config()
self.create_config()
program_data = self._create_program_data([(self.course_1.id, 'active')])
if key_remove and key_remove in program_data[unicode(self.course_1.id)]:
......
......@@ -123,7 +123,6 @@ from eventtracking import tracker
from notification_prefs.views import enable_notifications
# Note that this lives in openedx, so this dependency should be refactored.
from openedx.core.djangoapps.credentials.utils import get_user_program_credentials
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
from openedx.core.djangoapps.programs.utils import get_programs_for_dashboard
......@@ -610,7 +609,6 @@ def dashboard(request):
# This is passed along in the template context to allow rendering of
# program-related information on the dashboard.
course_programs = _get_course_programs(user, [enrollment.course_id for enrollment in course_enrollments])
xseries_credentials = _get_xseries_credentials(user)
# Construct a dictionary of course mode information
# used to render the course list. We re-use the course modes dict
......@@ -734,7 +732,6 @@ def dashboard(request):
'nav_hidden': True,
'course_programs': course_programs,
'disable_courseware_js': True,
'xseries_credentials': xseries_credentials,
}
return render_to_response('dashboard.html', context)
......@@ -2413,34 +2410,3 @@ def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invali
log.warning('Program structure is invalid, skipping display: %r', program)
return programs_data
def _get_xseries_credentials(user):
"""Return program credentials data required for display on
the learner dashboard.
Given a user, find all programs for which certificates have been earned
and return list of dictionaries of required program data.
Arguments:
user (User): user object for getting programs credentials.
Returns:
list of dict, containing data corresponding to the programs for which
the user has been awarded a credential.
"""
programs_credentials = get_user_program_credentials(user)
credentials_data = []
for program in programs_credentials:
if program.get('category') == 'xseries':
try:
program_data = {
'display_name': program['name'],
'subtitle': program['subtitle'],
'credential_url': program['credential_url'],
}
credentials_data.append(program_data)
except KeyError:
log.warning('Program structure is invalid: %r', program)
return credentials_data
......@@ -734,7 +734,6 @@ CREDIT_HELP_LINK_URL = ENV_TOKENS.get('CREDIT_HELP_LINK_URL', CREDIT_HELP_LINK_U
#### JWT configuration ####
JWT_ISSUER = ENV_TOKENS.get('JWT_ISSUER', JWT_ISSUER)
JWT_EXPIRATION = ENV_TOKENS.get('JWT_EXPIRATION', JWT_EXPIRATION)
JWT_AUTH.update(ENV_TOKENS.get('JWT_AUTH', {}))
################# PROCTORING CONFIGURATION ##################
......
......@@ -1922,9 +1922,6 @@ INSTALLED_APPS = (
'openedx.core.djangoapps.self_paced',
'sorl.thumbnail',
# Credentials support
'openedx.core.djangoapps.credentials',
)
# Migrations which are not in the standard module "migrations"
......@@ -2001,18 +1998,6 @@ SOCIAL_MEDIA_FOOTER_NAMES = [
"reddit",
]
# JWT Settings
JWT_AUTH = {
'JWT_SECRET_KEY': None,
'JWT_ALGORITHM': 'HS256',
'JWT_VERIFY_EXPIRATION': True,
'JWT_ISSUER': None,
'JWT_PAYLOAD_GET_USERNAME_HANDLER': lambda d: d.get('username'),
'JWT_AUDIENCE': None,
'JWT_LEEWAY': 1,
'JWT_DECODE_HANDLER': 'openedx.core.lib.api.jwt_decode_handler.decode',
}
# The footer URLs dictionary maps social footer names
# to URLs defined in configuration.
SOCIAL_MEDIA_FOOTER_URLS = {}
......@@ -2567,9 +2552,6 @@ ECOMMERCE_API_SIGNING_KEY = None
ECOMMERCE_API_TIMEOUT = 5
ECOMMERCE_SERVICE_WORKER_USERNAME = 'ecommerce_worker'
# Credentials configuration
CREDENTIALS_SERVICE_USERNAME = 'credentials_service_user'
# Reverification checkpoint name pattern
CHECKPOINT_PATTERN = r'(?P<checkpoint_name>[^/]+)'
......
......@@ -224,13 +224,6 @@ CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_WHITELIST = ()
CORS_ORIGIN_ALLOW_ALL = True
# JWT settings for devstack
JWT_AUTH.update({
'JWT_ALGORITHM': 'HS256',
'JWT_SECRET_KEY': 'lms-secret',
'JWT_ISSUER': 'http://127.0.0.1:8000/oauth2',
'JWT_AUDIENCE': 'lms-key',
})
#####################################################################
# See if the developer has any local overrides.
......
......@@ -190,7 +190,6 @@ DATABASES = {
# This hack disables migrations during tests. We want to create tables directly from the models for speed.
# See https://groups.google.com/d/msg/django-developers/PWPj3etj3-U/kCl6pMsQYYoJ.
MIGRATION_MODULES = {app: "app.migrations_not_used_in_tests" for app in INSTALLED_APPS}
MIGRATION_MODULES["credentials"] = "app.migrations_not_used_in_tests"
CACHES = {
# This is the cache used for most things.
......@@ -563,9 +562,3 @@ FEATURES['ORGANIZATIONS_APP'] = True
# Financial assistance page
FEATURES['ENABLE_FINANCIAL_ASSISTANCE_FORM'] = True
JWT_AUTH.update({
'JWT_SECRET_KEY': 'test-secret',
'JWT_ISSUER': 'https://test-provider/oauth2',
'JWT_AUDIENCE': 'test-key',
})
......@@ -32,32 +32,6 @@
}
}
.wrapper-xseries-certificates{
@include float(right);
@include margin-left(flex-gutter());
width: flex-grid(3);
.title{
@extend %t-title7;
@extend %t-weight4;
}
ul{
@include padding-left(0);
margin-top: ($baseline/2);
}
li{
@include line-height(20);
list-style-type: none;
}
.copy {
@extend %t-copy-sub1;
margin-top: ($baseline/2);
}
}
.profile-sidebar {
background: transparent;
@include float(right);
......
......@@ -172,19 +172,6 @@ import json
</ul>
</section>
</section>
% if xseries_credentials:
<div class="wrapper-xseries-certificates">
<p class="title">${_("XSeries Program Certificates")}</p>
<p class="copy">${_("You have received a certificate for the following XSeries programs:")}</p>
<ul>
% for xseries_credential in xseries_credentials:
<li>
<a class="copy" href="${xseries_credential['credential_url']}">${xseries_credential['display_name']}</a>
</li>
% endfor
</ul>
</div>
% endif
</section>
<section id="email-settings-modal" class="modal" aria-hidden="true">
......
......@@ -102,7 +102,6 @@ urlpatterns = (
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'^rss_proxy/', include('rss_proxy.urls', namespace='rss_proxy')),
url(r'^api/organizations/', include('organizations.urls', namespace='organizations')),
)
if settings.FEATURES["ENABLE_COMBINED_LOGIN_REGISTRATION"]:
......
"""
edX Platform support for credentials.
This package will be used as a wrapper for interacting with the credentials
service.
"""
"""
Django admin pages for credentials support models.
"""
from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
class CredentialsApiConfigAdmin(ConfigurationModelAdmin): # pylint: disable=missing-docstring
pass
admin.site.register(CredentialsApiConfig, CredentialsApiConfigAdmin)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CredentialsApiConfig',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('internal_service_url', models.URLField(verbose_name='Internal Service URL')),
('public_service_url', models.URLField(verbose_name='Public Service URL')),
('enable_learner_issuance', models.BooleanField(default=False, help_text='Enable issuance of credentials via Credential Service.', verbose_name='Enable Learner Issuance')),
('enable_studio_authoring', models.BooleanField(default=False, help_text='Enable authoring of Credential Service credentials in Studio.', verbose_name='Enable Authoring of Credential in Studio')),
('cache_ttl', 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')),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
options={
'ordering': ('-change_date',),
'abstract': False,
},
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
from django.contrib.auth.models import User
def add_service_user(apps, schema_editor):
"""Add service user."""
user, created = User.objects.get_or_create(username=settings.CREDENTIALS_SERVICE_USERNAME)
if created:
user.is_staff = True
user.set_unusable_password()
user.save()
def remove_service_user(apps, schema_editor):
"""Remove service user."""
try:
User.objects.get(username=settings.CREDENTIALS_SERVICE_USERNAME).delete()
except User.DoesNotExist:
return
class Migration(migrations.Migration):
dependencies = [
('credentials', '0001_initial'),
]
operations = [
migrations.RunPython(add_service_user, remove_service_user),
]
"""
Models for credentials support for the LMS and Studio.
"""
from urlparse import urljoin
from django.utils.translation import ugettext_lazy as _
from django.db import models
from config_models.models import ConfigurationModel
class CredentialsApiConfig(ConfigurationModel):
"""
Manages configuration for connecting to the Credential service and using its
API.
"""
OAUTH2_CLIENT_NAME = 'credentials'
API_NAME = 'credentials'
CACHE_KEY = 'credentials.api.data'
internal_service_url = models.URLField(verbose_name=_("Internal Service URL"))
public_service_url = models.URLField(verbose_name=_("Public Service URL"))
enable_learner_issuance = models.BooleanField(
verbose_name=_("Enable Learner Issuance"),
default=False,
help_text=_(
"Enable issuance of credentials via Credential Service."
)
)
enable_studio_authoring = models.BooleanField(
verbose_name=_("Enable Authoring of Credential in Studio"),
default=False,
help_text=_(
"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):
return self.public_api_url
@property
def internal_api_url(self):
"""
Generate a URL based on internal service URL and API version number.
"""
return urljoin(self.internal_service_url, '/api/v1/')
@property
def public_api_url(self):
"""
Generate a URL based on public service URL and API version number.
"""
return urljoin(self.public_service_url, '/api/v1/')
@property
def is_learner_issuance_enabled(self):
"""
Indicates whether the learner credential should be enabled or not.
"""
return self.enabled and self.enable_learner_issuance
@property
def is_studio_authoring_enabled(self):
"""
Indicates whether Studio functionality related to Credential should
be enabled or not.
"""
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
"""Mixins for use during testing."""
import json
import httpretty
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
class CredentialsApiConfigMixin(object):
""" Utilities for working with Credentials configuration during testing."""
CREDENTIALS_DEFAULTS = {
'enabled': True,
'internal_service_url': 'http://internal.credentials.org/',
'public_service_url': 'http://public.credentials.org/',
'enable_learner_issuance': True,
'enable_studio_authoring': True,
'cache_ttl': 0,
}
def create_credentials_config(self, **kwargs):
""" Creates a new CredentialsApiConfig with DEFAULTS, updated with any
provided overrides.
"""
fields = dict(self.CREDENTIALS_DEFAULTS, **kwargs)
CredentialsApiConfig(**fields).save()
return CredentialsApiConfig.current()
class CredentialsDataMixin(object):
"""Mixin mocking Credentials API URLs and providing fake data for testing."""
CREDENTIALS_API_RESPONSE = {
"next": None,
"results": [
{
"id": 1,
"username": "test",
"credential": {
"credential_id": 1,
"program_id": 1
},
"status": "awarded",
"uuid": "dummy-uuid-1",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-1/"
},
{
"id": 2,
"username": "test",
"credential": {
"credential_id": 2,
"program_id": 2
},
"status": "awarded",
"uuid": "dummy-uuid-2",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-2/"
},
{
"id": 3,
"username": "test",
"credential": {
"credential_id": 3,
"program_id": 3
},
"status": "revoked",
"uuid": "dummy-uuid-3",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-3/"
},
{
"id": 4,
"username": "test",
"credential": {
"course_id": "edx/test01/2015",
"credential_id": 4,
"certificate_type": "honor"
},
"status": "awarded",
"uuid": "dummy-uuid-4",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-4/"
},
{
"id": 5,
"username": "test",
"credential": {
"course_id": "edx/test02/2015",
"credential_id": 5,
"certificate_type": "verified"
},
"status": "awarded",
"uuid": "dummy-uuid-5",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-5/"
},
{
"id": 6,
"username": "test",
"credential": {
"course_id": "edx/test03/2015",
"credential_id": 6,
"certificate_type": "honor"
},
"status": "revoked",
"uuid": "dummy-uuid-6",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-6/"
}
]
}
CREDENTIALS_NEXT_API_RESPONSE = {
"next": None,
"results": [
{
"id": 7,
"username": "test",
"credential": {
"credential_id": 7,
"program_id": 7
},
"status": "awarded",
"uuid": "dummy-uuid-7",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-7"
},
{
"id": 8,
"username": "test",
"credential": {
"credential_id": 8,
"program_id": 8
},
"status": "awarded",
"uuid": "dummy-uuid-8",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-8/"
}
]
}
def mock_credentials_api(self, user, data=None, status_code=200, reset_url=True, is_next_page=False):
"""Utility for mocking out Credentials API URLs."""
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Credentials API calls.')
internal_api_url = CredentialsApiConfig.current().internal_api_url.strip('/')
url = internal_api_url + '/user_credentials/?username=' + user.username
if reset_url:
httpretty.reset()
if data is None:
data = self.CREDENTIALS_API_RESPONSE
body = json.dumps(data)
if is_next_page:
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.GET, next_page_url, body=body, content_type='application/json', status=status_code
)
httpretty.register_uri(
httpretty.GET, url, body=next_page_body, content_type='application/json', status=status_code
)
else:
httpretty.register_uri(
httpretty.GET, url, body=body, content_type='application/json', status=status_code
)
"""Tests for models supporting Credentials-related functionality."""
from django.test import TestCase
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
class TestCredentialsApiConfig(CredentialsApiConfigMixin, TestCase):
"""Tests covering the CredentialsApiConfig model."""
def test_url_construction(self):
"""Verify that URLs returned by the model are constructed correctly."""
credentials_config = self.create_credentials_config()
self.assertEqual(
credentials_config.internal_api_url,
credentials_config.internal_service_url.strip('/') + '/api/v1/')
self.assertEqual(
credentials_config.public_api_url,
credentials_config.public_service_url.strip('/') + '/api/v1/')
def test_is_learner_issuance_enabled(self):
"""
Verify that the property controlling display on the student dashboard is only True
when configuration is enabled and all required configuration is provided.
"""
credentials_config = self.create_credentials_config(enabled=False)
self.assertFalse(credentials_config.is_learner_issuance_enabled)
credentials_config = self.create_credentials_config(enable_learner_issuance=False)
self.assertFalse(credentials_config.is_learner_issuance_enabled)
credentials_config = self.create_credentials_config()
self.assertTrue(credentials_config.is_learner_issuance_enabled)
def test_is_studio_authoring_enabled(self):
"""
Verify that the property controlling display in the Studio authoring is only True
when configuration is enabled and all required configuration is provided.
"""
credentials_config = self.create_credentials_config(enabled=False)
self.assertFalse(credentials_config.is_studio_authoring_enabled)
credentials_config = self.create_credentials_config(enable_studio_authoring=False)
self.assertFalse(credentials_config.is_studio_authoring_enabled)
credentials_config = self.create_credentials_config()
self.assertTrue(credentials_config.is_studio_authoring_enabled)
"""Tests covering Credentials utilities."""
from django.core.cache import cache
from django.test import TestCase
import httpretty
from oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.utils import (
get_user_credentials, get_user_program_credentials
)
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from student.tests.factories import UserFactory
class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin, CredentialsDataMixin,
ProgramsDataMixin, TestCase):
""" Tests covering the retrieval of user credentials from the Credentials
service.
"""
def setUp(self):
super(TestCredentialsRetrieval, self).setUp()
ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.user = UserFactory()
cache.clear()
@httpretty.activate
def test_get_user_credentials(self):
"""Verify user credentials data can be retrieve."""
self.create_credentials_config()
self.mock_credentials_api(self.user)
actual = get_user_credentials(self.user)
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):
"""Verify that user program credentials cannot be retrieved if issuance is disabled."""
self.create_credentials_config(enable_learner_issuance=False)
actual = get_user_program_credentials(self.user)
self.assertEqual(actual, [])
@httpretty.activate
def test_get_user_program_credentials_no_credential(self):
"""Verify behavior if no credential exist."""
self.create_credentials_config()
self.mock_credentials_api(self.user, data={'results': []})
actual = get_user_program_credentials(self.user)
self.assertEqual(actual, [])
@httpretty.activate
def test_get_user_programs_credentials(self):
"""Verify program credentials data can be retrieved and parsed correctly."""
# create credentials and program configuration
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'] = self.PROGRAMS_CREDENTIALS_DATA[0]['certificate_url']
expected[1]['credential_url'] = self.PROGRAMS_CREDENTIALS_DATA[1]['certificate_url']
# 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):
"""Verify behavior if credential revoked."""
self.create_credentials_config()
credential_data = {"results": [
{
"id": 1,
"username": "test",
"credential": {
"credential_id": 1,
"program_id": 1
},
"status": "revoked",
"uuid": "dummy-uuid-1"
}
]}
self.mock_credentials_api(self.user, data=credential_data)
actual = get_user_program_credentials(self.user)
self.assertEqual(actual, [])
"""Helper functions for working with Credentials."""
from __future__ import unicode_literals
import logging
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.programs.utils import get_programs_for_credentials
from openedx.core.lib.edx_api_utils import get_edx_api_data
log = logging.getLogger(__name__)
def get_user_credentials(user):
"""Given a user, get credentials earned from the Credentials service.
Arguments:
user (User): The user to authenticate as when requesting credentials.
Returns:
list of dict, representing credentials returned by the Credentials
service.
"""
credential_configuration = CredentialsApiConfig.current()
user_query = {'username': user.username}
# Bypass caching for staff users, who may be generating credentials and
# 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
def get_user_program_credentials(user):
"""Given a user, get the list of all program credentials earned and returns
list of dictionaries containing related programs data.
Arguments:
user (User): The user object for getting programs credentials.
Returns:
list, containing programs dictionaries.
"""
programs_credentials_data = []
credential_configuration = CredentialsApiConfig.current()
if not credential_configuration.is_learner_issuance_enabled:
log.debug('Display of certificates for programs is disabled.')
return programs_credentials_data
credentials = get_user_credentials(user)
if not credentials:
log.info('No credential earned by the given user.')
return programs_credentials_data
programs_credentials = []
for credential in credentials:
try:
if 'program_id' in credential['credential'] and credential['status'] == 'awarded':
programs_credentials.append(credential)
except KeyError:
log.exception('Invalid credential structure: %r', credential)
if programs_credentials:
programs_credentials_data = get_programs_for_credentials(user, programs_credentials)
return programs_credentials_data
......@@ -18,7 +18,6 @@ class ProgramsApiConfig(ConfigurationModel):
"""
OAUTH2_CLIENT_NAME = 'programs'
CACHE_KEY = 'programs.api.data'
API_NAME = 'programs'
api_version_number = models.IntegerField(verbose_name=_("API Version"))
......
......@@ -22,7 +22,7 @@ class ProgramsApiConfigMixin(object):
'enable_certification': True,
}
def create_programs_config(self, **kwargs):
def create_config(self, **kwargs):
"""Creates a new ProgramsApiConfig with DEFAULTS, updated with any provided overrides."""
fields = dict(self.DEFAULTS, **kwargs)
ProgramsApiConfig(**fields).save()
......@@ -185,31 +185,6 @@ class ProgramsDataMixin(object):
]
}
PROGRAMS_CREDENTIALS_DATA = [
{
"id": 1,
"username": "test",
"credential": {
"credential_id": 1,
"program_id": 1
},
"status": "awarded",
"uuid": "dummy-uuid-1",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-1/"
},
{
"id": 2,
"username": "test",
"credential": {
"credential_id": 2,
"program_id": 2
},
"status": "awarded",
"uuid": "dummy-uuid-2",
"certificate_url": "http://credentials.edx.org/credentials/dummy-uuid-2/"
}
]
def mock_programs_api(self, data=None, status_code=200):
"""Utility for mocking out Programs API URLs."""
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.')
......
......@@ -14,7 +14,7 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase):
"""Tests covering the ProgramsApiConfig model."""
def test_url_construction(self, _mock_cache):
"""Verify that URLs returned by the model are constructed correctly."""
programs_config = self.create_programs_config()
programs_config = self.create_config()
self.assertEqual(
programs_config.internal_api_url,
......@@ -43,7 +43,7 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase):
@ddt.unpack
def test_cache_control(self, cache_ttl, is_cache_enabled, _mock_cache):
"""Verify the behavior of the property controlling whether API responses are cached."""
programs_config = self.create_programs_config(cache_ttl=cache_ttl)
programs_config = self.create_config(cache_ttl=cache_ttl)
self.assertEqual(programs_config.is_cache_enabled, is_cache_enabled)
def test_is_student_dashboard_enabled(self, _mock_cache):
......@@ -51,13 +51,13 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase):
Verify that the property controlling display on the student dashboard is only True
when configuration is enabled and all required configuration is provided.
"""
programs_config = self.create_programs_config(enabled=False)
programs_config = self.create_config(enabled=False)
self.assertFalse(programs_config.is_student_dashboard_enabled)
programs_config = self.create_programs_config(enable_student_dashboard=False)
programs_config = self.create_config(enable_student_dashboard=False)
self.assertFalse(programs_config.is_student_dashboard_enabled)
programs_config = self.create_programs_config()
programs_config = self.create_config()
self.assertTrue(programs_config.is_student_dashboard_enabled)
def test_is_studio_tab_enabled(self, _mock_cache):
......@@ -65,16 +65,16 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase):
Verify that the property controlling display of the Studio tab is only True
when configuration is enabled and all required configuration is provided.
"""
programs_config = self.create_programs_config(enabled=False)
programs_config = self.create_config(enabled=False)
self.assertFalse(programs_config.is_studio_tab_enabled)
programs_config = self.create_programs_config(enable_studio_tab=False)
programs_config = self.create_config(enable_studio_tab=False)
self.assertFalse(programs_config.is_studio_tab_enabled)
programs_config = self.create_programs_config(authoring_app_js_path='', authoring_app_css_path='')
programs_config = self.create_config(authoring_app_js_path='', authoring_app_css_path='')
self.assertFalse(programs_config.is_studio_tab_enabled)
programs_config = self.create_programs_config()
programs_config = self.create_config()
self.assertTrue(programs_config.is_studio_tab_enabled)
def test_is_certification_enabled(self, _mock_cache):
......@@ -82,11 +82,11 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase):
Verify that the property controlling certification-related functionality
for Programs behaves as expected.
"""
programs_config = self.create_programs_config(enabled=False)
programs_config = self.create_config(enabled=False)
self.assertFalse(programs_config.is_certification_enabled)
programs_config = self.create_programs_config(enable_certification=False)
programs_config = self.create_config(enable_certification=False)
self.assertFalse(programs_config.is_certification_enabled)
programs_config = self.create_programs_config()
programs_config = self.create_config()
self.assertTrue(programs_config.is_certification_enabled)
......@@ -6,17 +6,13 @@ import mock
from oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
from openedx.core.djangoapps.programs.utils import (
get_programs, get_programs_for_credentials, get_programs_for_dashboard
)
from openedx.core.djangoapps.programs.utils import get_programs, get_programs_for_dashboard
from student.tests.factories import UserFactory
class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
CredentialsApiConfigMixin, TestCase):
class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, TestCase):
"""Tests covering the retrieval of programs from the Programs service."""
def setUp(self):
super(TestProgramRetrieval, self).setUp()
......@@ -29,7 +25,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
@httpretty.activate
def test_get_programs(self):
"""Verify programs data can be retrieved."""
self.create_programs_config()
self.create_config()
self.mock_programs_api()
actual = get_programs(self.user)
......@@ -44,7 +40,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
@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.create_config(cache_ttl=1)
self.mock_programs_api()
# Warm up the cache.
......@@ -67,7 +63,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
def test_get_programs_programs_disabled(self):
"""Verify behavior when programs is disabled."""
self.create_programs_config(enabled=False)
self.create_config(enabled=False)
actual = get_programs(self.user)
self.assertEqual(actual, [])
......@@ -75,7 +71,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
@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()
self.create_config()
mock_init.side_effect = Exception
actual = get_programs(self.user)
......@@ -85,7 +81,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
@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.create_config()
self.mock_programs_api(status_code=500)
actual = get_programs(self.user)
......@@ -94,7 +90,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
@httpretty.activate
def test_get_programs_for_dashboard(self):
"""Verify programs data can be retrieved and parsed correctly."""
self.create_programs_config()
self.create_config()
self.mock_programs_api()
actual = get_programs_for_dashboard(self.user, self.COURSE_KEYS)
......@@ -109,7 +105,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
def test_get_programs_for_dashboard_dashboard_display_disabled(self):
"""Verify behavior when student dashboard display is disabled."""
self.create_programs_config(enable_student_dashboard=False)
self.create_config(enable_student_dashboard=False)
actual = get_programs_for_dashboard(self.user, self.COURSE_KEYS)
self.assertEqual(actual, {})
......@@ -117,7 +113,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
@httpretty.activate
def test_get_programs_for_dashboard_no_data(self):
"""Verify behavior when no programs data is found for the user."""
self.create_programs_config()
self.create_config()
self.mock_programs_api(data={'results': []})
actual = get_programs_for_dashboard(self.user, self.COURSE_KEYS)
......@@ -126,56 +122,10 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
@httpretty.activate
def test_get_programs_for_dashboard_invalid_data(self):
"""Verify behavior when the Programs API returns invalid data and parsing fails."""
self.create_programs_config()
self.create_config()
invalid_program = {'invalid_key': 'invalid_data'}
self.mock_programs_api(data={'results': [invalid_program]})
actual = get_programs_for_dashboard(self.user, self.COURSE_KEYS)
self.assertEqual(actual, {})
@httpretty.activate
def test_get_program_for_certificates(self):
"""Verify programs data can be retrieved and parsed correctly for certificates."""
self.create_programs_config()
self.mock_programs_api()
actual = get_programs_for_credentials(self.user, self.PROGRAMS_CREDENTIALS_DATA)
expected = self.PROGRAMS_API_RESPONSE['results']
expected[0]['credential_url'] = self.PROGRAMS_CREDENTIALS_DATA[0]['certificate_url']
expected[1]['credential_url'] = self.PROGRAMS_CREDENTIALS_DATA[1]['certificate_url']
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
@httpretty.activate
def test_get_program_for_certificates_no_data(self):
"""Verify behavior when no programs data is found for the user."""
self.create_programs_config()
self.create_credentials_config()
self.mock_programs_api(data={'results': []})
actual = get_programs_for_credentials(self.user, self.PROGRAMS_CREDENTIALS_DATA)
self.assertEqual(actual, [])
@httpretty.activate
def test_get_program_for_certificates_id_not_exist(self):
"""Verify behavior when no program with the given program_id in
credentials exists.
"""
self.create_programs_config()
self.create_credentials_config()
self.mock_programs_api()
credential_data = [
{
"id": 1,
"username": "test",
"credential": {
"credential_id": 1,
"program_id": 100
},
"status": "awarded",
"credential_url": "www.example.com"
}
]
actual = get_programs_for_credentials(self.user, credential_data)
self.assertEqual(actual, [])
"""Helper functions for working with Programs."""
import logging
from django.core.cache import cache
from edx_rest_api_client.client import EdxRestApiClient
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.lib.edx_api_utils import get_edx_api_data
from openedx.core.lib.token_utils import get_id_token
log = logging.getLogger(__name__)
......@@ -10,6 +13,7 @@ log = logging.getLogger(__name__)
def get_programs(user):
"""Given a user, get programs from the Programs service.
Returned value is cached depending on user permissions. Staff users making requests
against Programs will receive unpublished programs, while regular users will only receive
published programs.
......@@ -21,12 +25,39 @@ def get_programs(user):
list of dict, representing programs returned by the Programs service.
"""
programs_config = ProgramsApiConfig.current()
no_programs = []
# Bypass caching for staff users, who may be creating Programs and want to see them displayed immediately.
use_cache = programs_config.is_cache_enabled and not user.is_staff
if not programs_config.enabled:
log.warning('Programs configuration is disabled.')
return no_programs
if use_cache:
cached = cache.get(programs_config.CACHE_KEY)
if cached is not None:
return cached
try:
jwt = get_id_token(user, programs_config.OAUTH2_CLIENT_NAME)
api = EdxRestApiClient(programs_config.internal_api_url, jwt=jwt)
except Exception: # pylint: disable=broad-except
log.exception('Failed to initialize the Programs API client.')
return no_programs
# Bypass caching for staff users, who may be creating Programs and want
# to see them displayed immediately.
cache_key = programs_config.CACHE_KEY if programs_config.is_cache_enabled and not user.is_staff else None
try:
response = api.programs.get()
except Exception: # pylint: disable=broad-except
log.exception('Failed to retrieve programs from the Programs API.')
return no_programs
return get_edx_api_data(programs_config, user, 'programs', cache_key=cache_key)
results = response.get('results', no_programs)
if use_cache:
cache.set(programs_config.CACHE_KEY, results, programs_config.cache_ttl)
return results
def get_programs_for_dashboard(user, course_keys):
......@@ -74,31 +105,3 @@ def get_programs_for_dashboard(user, course_keys):
log.exception('Unable to parse Programs API response: %r', program)
return course_programs
def get_programs_for_credentials(user, programs_credentials):
""" Given a user and an iterable of credentials, get corresponding programs
data and return it as a list of dictionaries.
Arguments:
user (User): The user to authenticate as for requesting programs.
programs_credentials (list): List of credentials awarded to the user
for completion of a program.
Returns:
list, containing programs dictionaries.
"""
certificate_programs = []
programs = get_programs(user)
if not programs:
log.debug('No programs for user %d.', user.id)
return certificate_programs
for program in programs:
for credential in programs_credentials:
if program['id'] == credential['credential']['program_id']:
program['credential_url'] = credential['certificate_url']
certificate_programs.append(program)
return certificate_programs
......@@ -5,10 +5,10 @@ For more information, see:
https://openedx.atlassian.net/wiki/display/TNL/User+API
"""
from django.db import transaction
from rest_framework import status, permissions
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework import permissions
from openedx.core.lib.api.authentication import (
SessionAuthenticationAllowInactiveUser,
......@@ -137,9 +137,7 @@ class AccountView(APIView):
If the update is successful, updated user account data is returned.
"""
authentication_classes = (
OAuth2AuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser, JSONWebTokenAuthentication
)
authentication_classes = (OAuth2AuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser)
permission_classes = (permissions.IsAuthenticated,)
parser_classes = (MergePatchParser,)
......
""" Common Authentication Handlers used across projects. """
"""
Common Authentication Handlers used across projects.
"""
import logging
from rest_framework.authentication import SessionAuthentication
from rest_framework import exceptions as drf_exceptions
from rest_framework_oauth.authentication import OAuth2Authentication
from .exceptions import AuthenticationFailed
from rest_framework_oauth.compat import oauth2_provider, provider_now
from openedx.core.lib.api.exceptions import AuthenticationFailed
OAUTH2_TOKEN_ERROR = u'token_error'
OAUTH2_TOKEN_ERROR_EXPIRED = u'token_expired'
OAUTH2_TOKEN_ERROR_MALFORMED = u'token_malformed'
......@@ -17,9 +16,6 @@ OAUTH2_TOKEN_ERROR_NONEXISTENT = u'token_nonexistent'
OAUTH2_TOKEN_ERROR_NOT_PROVIDED = u'token_not_provided'
log = logging.getLogger(__name__)
class SessionAuthenticationAllowInactiveUser(SessionAuthentication):
"""Ensure that the user is logged in, but do not require the account to be active.
......
"""
Custom JWT decoding function for django_rest_framework jwt package.
Adds logging to facilitate debugging of InvalidTokenErrors. Also
requires "exp" and "iat" claims to be present - the base package
doesn't expose settings to enforce this.
"""
import logging
import jwt
from rest_framework_jwt.settings import api_settings
log = logging.getLogger(__name__)
def decode(token):
"""
Ensure InvalidTokenErrors are logged for diagnostic purposes, before
failing authentication.
"""
options = {
'verify_exp': api_settings.JWT_VERIFY_EXPIRATION,
'require_exp': True,
'require_iat': True,
}
try:
return jwt.decode(
token,
api_settings.JWT_SECRET_KEY,
api_settings.JWT_VERIFY,
options=options,
leeway=api_settings.JWT_LEEWAY,
audience=api_settings.JWT_AUDIENCE,
issuer=api_settings.JWT_ISSUER,
algorithms=[api_settings.JWT_ALGORITHM]
)
except jwt.InvalidTokenError as exc:
exc_type = u'{}.{}'.format(exc.__class__.__module__, exc.__class__.__name__)
log.exception("raised_invalid_token: exc_type=%r, exc_detail=%r", exc_type, exc.message)
raise
......@@ -10,6 +10,7 @@ import itertools
import json
import ddt
from django.conf.urls import patterns, url, include
from django.contrib.auth.models import User
from django.http import HttpResponse
......@@ -25,8 +26,8 @@ from rest_framework.test import APIRequestFactory, APIClient
from rest_framework.views import APIView
from provider import scope, constants
from openedx.core.lib.api import authentication
from .. import authentication
factory = APIRequestFactory() # pylint: disable=invalid-name
......
"""Helper functions to get data from APIs"""
from __future__ import unicode_literals
import logging
from django.core.cache import cache
from edx_rest_api_client.client import EdxRestApiClient
from openedx.core.lib.token_utils import get_id_token
log = logging.getLogger(__name__)
def get_edx_api_data(api_config, user, resource, querystring=None, cache_key=None):
"""Fetch data from an API using provided API configuration and resource
name.
Arguments:
api_config (ConfigurationModel): The configuration model governing
interaction with the API.
user (User): The user to authenticate as when requesting data.
resource(str): Name of the API resource for which data is being
requested.
querystring(dict): Querystring parameters that might be required to
request data.
cache_key(str): Where to cache retrieved data. Omitting this will cause the
cache to be bypassed.
Returns:
list of dict, representing data returned by the API.
"""
no_data = []
if not api_config.enabled:
log.warning('%s configuration is disabled.', api_config.API_NAME)
return no_data
if cache_key:
cached = cache.get(cache_key)
if cached is not None:
return cached
try:
jwt = get_id_token(user, api_config.OAUTH2_CLIENT_NAME)
api = EdxRestApiClient(api_config.internal_api_url, jwt=jwt)
except Exception: # pylint: disable=broad-except
log.exception('Failed to initialize the %s API client.', api_config.API_NAME)
return no_data
try:
querystring = {} if not querystring else querystring
response = getattr(api, resource).get(**querystring)
results = response.get('results', no_data)
page = 1
next_page = response.get('next', None)
while next_page:
page += 1
querystring['page'] = page
response = getattr(api, resource).get(**querystring)
results += response.get('results', no_data)
next_page = response.get('next', None)
except Exception: # pylint: disable=broad-except
log.exception('Failed to retrieve data from the %s API.', api_config.API_NAME)
return no_data
if cache_key:
cache.set(cache_key, results, api_config.cache_ttl)
return results
"""Tests covering Api utils."""
from django.core.cache import cache
from django.test import TestCase
import httpretty
import mock
from oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from testfixtures import LogCapture
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
from openedx.core.lib.edx_api_utils import get_edx_api_data
from student.tests.factories import UserFactory
LOGGER_NAME = 'openedx.core.lib.edx_api_utils'
class TestApiDataRetrieval(CredentialsApiConfigMixin, CredentialsDataMixin, ProgramsApiConfigMixin, ProgramsDataMixin,
TestCase):
"""Test utility for API data retrieval."""
def setUp(self):
super(TestApiDataRetrieval, self).setUp()
ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.user = UserFactory()
cache.clear()
@httpretty.activate
def test_get_edx_api_data_programs(self):
"""Verify programs data can be retrieved using get_edx_api_data."""
program_config = self.create_programs_config()
self.mock_programs_api()
actual = get_edx_api_data(program_config, self.user, 'programs')
self.assertEqual(
actual,
self.PROGRAMS_API_RESPONSE['results']
)
# Verify the API was actually hit (not the cache).
self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
def test_get_edx_api_data_disable_config(self):
"""Verify no data is retrieved if configuration is disabled."""
program_config = self.create_programs_config(enabled=False)
actual = get_edx_api_data(program_config, self.user, 'programs')
self.assertEqual(actual, [])
@httpretty.activate
def test_get_edx_api_data_cache(self):
"""Verify that when enabled, the cache is used."""
program_config = self.create_programs_config(cache_ttl=1)
self.mock_programs_api()
# Warm up the cache.
get_edx_api_data(program_config, self.user, 'programs', cache_key='test.key')
# Hit the cache.
get_edx_api_data(program_config, self.user, 'programs', cache_key='test.key')
# Verify only one request was made.
self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
@mock.patch('edx_rest_api_client.client.EdxRestApiClient.__init__')
def test_get_edx_api_data_client_initialization_failure(self, mock_init):
"""Verify no data is retrieved and exception logged when API client
fails to initialize.
"""
program_config = self.create_programs_config()
mock_init.side_effect = Exception
with LogCapture(LOGGER_NAME) as logger:
actual = get_edx_api_data(program_config, self.user, 'programs')
logger.check(
(LOGGER_NAME, 'ERROR', u'Failed to initialize the programs API client.')
)
self.assertEqual(actual, [])
self.assertTrue(mock_init.called)
@httpretty.activate
def test_get_edx_api_data_retrieval_failure(self):
"""Verify exception is logged when data can't be retrieved from API."""
program_config = self.create_programs_config()
self.mock_programs_api(status_code=500)
with LogCapture(LOGGER_NAME) as logger:
actual = get_edx_api_data(program_config, self.user, 'programs')
logger.check(
(LOGGER_NAME, 'ERROR', u'Failed to retrieve data from the programs API.')
)
self.assertEqual(actual, [])
@httpretty.activate
def test_get_edx_api_data_multiple_page(self):
"""Verify that all data is retrieve for multiple page response."""
credentials_config = self.create_credentials_config()
self.mock_credentials_api(self.user, is_next_page=True)
querystring = {'username': self.user.username}
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']
self.assertEqual(actual, expected_data)
......@@ -32,7 +32,6 @@ django-method-override==0.1.0
#djangorestframework>=3.1,<3.2
git+https://github.com/edx/django-rest-framework.git@3c72cb5ee5baebc4328947371195eae2077197b0#egg=djangorestframework==3.2.3
django==1.8.7
djangorestframework-jwt==1.7.2
edx-rest-api-client==1.2.1
edx-search==0.1.1
facebook-sdk==0.4.0
......@@ -61,7 +60,7 @@ polib==1.0.3
pycrypto>=2.6
pygments==2.0.1
pygraphviz==1.1
PyJWT==1.4.0
PyJWT==1.0.1
pymongo==2.9.1
pyparsing==2.0.1
python-memcached==1.48
......@@ -154,7 +153,6 @@ rednose==0.4.3
selenium==2.42.1
splinter==0.5.4
testtools==0.9.34
testfixtures==4.5.0
zendesk==1.1.1
# Used for Segment analytics
......
......@@ -92,7 +92,7 @@ git+https://github.com/edx/xblock-utils.git@v1.0.2#egg=xblock-utils==1.0.2
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
-e git+https://github.com/edx/edx-reverification-block.git@0.0.5#egg=edx-reverification-block==0.0.5
-e git+https://github.com/edx/edx-user-state-client.git@30c0ad4b9f57f8d48d6943eb585ec8a9205f4469#egg=edx-user-state-client
git+https://github.com/edx/edx-organizations.git@release-2016-01-22#egg=edx-organizations==0.3.0
git+https://github.com/edx/edx-organizations.git@release-2015-12-08#egg=edx-organizations==0.2.0
git+https://github.com/edx/edx-proctoring.git@0.12.6#egg=edx-proctoring==0.12.6
git+https://github.com/edx/xblock-lti-consumer.git@v1.0.2#egg=xblock-lti-consumer==1.0.2
......
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