Commit a2104634 by Jonathan Piacenti

Implement Badging API views.

parent 61c76771
"""
Serializers for Badges
"""
from rest_framework import serializers
from badges.models import BadgeClass, BadgeAssertion
class BadgeClassSerializer(serializers.ModelSerializer):
"""
Serializer for BadgeClass model.
"""
image_url = serializers.ImageField(source='image')
class Meta(object):
model = BadgeClass
fields = ('slug', 'issuing_component', 'display_name', 'course_id', 'description', 'criteria', 'image_url')
class BadgeAssertionSerializer(serializers.ModelSerializer):
"""
Serializer for the BadgeAssertion model.
"""
badge_class = BadgeClassSerializer(read_only=True)
class Meta(object):
model = BadgeAssertion
fields = ('badge_class', 'image_url', 'assertion_url')
"""
Tests for the badges API views.
"""
from django.conf import settings
from django.test.utils import override_settings
from badges.tests.factories import BadgeAssertionFactory, BadgeClassFactory, RandomBadgeClassFactory
from openedx.core.lib.api.test_utils import ApiTestCase
from student.tests.factories import UserFactory
from util.testing import UrlResetMixin
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
FEATURES_WITH_BADGES_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_BADGES_ENABLED['ENABLE_OPENBADGES'] = True
@override_settings(FEATURES=FEATURES_WITH_BADGES_ENABLED)
class UserAssertionTestCase(UrlResetMixin, ModuleStoreTestCase, ApiTestCase):
"""
Mixin for badge API tests.
"""
WILDCARD = False
CHECK_COURSE = False
def setUp(self, *args, **kwargs):
super(UserAssertionTestCase, self).setUp(*args, **kwargs)
self.course = CourseFactory.create()
self.user = UserFactory.create()
# Password defined by factory.
self.client.login(username=self.user.username, password='test')
def url(self):
"""
Return the URL to look up the current user's assertions.
"""
return '/api/badges/v1/assertions/user/{}/'.format(self.user.username)
def check_class_structure(self, badge_class, json_class):
"""
Check a JSON response against a known badge class.
"""
self.assertEqual(badge_class.issuing_component, json_class['issuing_component'])
self.assertEqual(badge_class.slug, json_class['slug'])
self.assertIn(badge_class.image.url, json_class['image_url'])
self.assertEqual(badge_class.description, json_class['description'])
self.assertEqual(badge_class.criteria, json_class['criteria'])
self.assertEqual(badge_class.course_id and unicode(badge_class.course_id), json_class['course_id'])
def check_assertion_structure(self, assertion, json_assertion):
"""
Check a JSON response against a known assertion object.
"""
self.assertEqual(assertion.image_url, json_assertion['image_url'])
self.assertEqual(assertion.assertion_url, json_assertion['assertion_url'])
self.check_class_structure(assertion.badge_class, json_assertion['badge_class'])
def get_course_id(self, badge_class):
"""
Used for tests which may need to test for a course_id or a wildcard.
"""
if self.WILDCARD:
return '*'
else:
return unicode(badge_class.course_id)
def create_badge_class(self, **kwargs):
"""
Create a badge class, using a course id if it's relevant to the URL pattern.
"""
if self.CHECK_COURSE:
return RandomBadgeClassFactory.create(course_id=self.course.location.course_key, **kwargs)
return RandomBadgeClassFactory.create(**kwargs)
def get_qs_args(self, badge_class):
"""
Get a dictionary to be serialized into querystring params based on class settings.
"""
qs_args = {
'issuing_component': badge_class.issuing_component,
'slug': badge_class.slug,
}
if self.CHECK_COURSE:
qs_args['course_id'] = self.get_course_id(badge_class)
return qs_args
class TestUserBadgeAssertions(UserAssertionTestCase):
"""
Test the general badge assertions retrieval view.
"""
def test_get_assertions(self):
"""
Verify we can get all of a user's badge assertions.
"""
for dummy in range(3):
BadgeAssertionFactory(user=self.user)
# Add in a course scoped badge-- these should not be excluded from the full listing.
BadgeAssertionFactory(user=self.user, badge_class=BadgeClassFactory(course_id=self.course.location.course_key))
# Should not be included.
for dummy in range(3):
self.create_badge_class()
response = self.get_json(self.url())
# pylint: disable=no-member
self.assertEqual(len(response['results']), 4)
def test_assertion_structure(self):
badge_class = self.create_badge_class()
assertion = BadgeAssertionFactory.create(user=self.user, badge_class=badge_class)
response = self.get_json(self.url())
# pylint: disable=no-member
self.check_assertion_structure(assertion, response['results'][0])
class TestUserCourseBadgeAssertions(UserAssertionTestCase):
"""
Test the Badge Assertions view with the course_id filter.
"""
CHECK_COURSE = True
def test_get_assertions(self):
"""
Verify we can get assertions via the course_id and username.
"""
course_key = self.course.location.course_key
badge_class = BadgeClassFactory.create(course_id=course_key)
for dummy in range(3):
BadgeAssertionFactory.create(user=self.user, badge_class=badge_class)
# Should not be included.
for dummy in range(3):
BadgeAssertionFactory.create(user=self.user)
# Also should not be included
for dummy in range(6):
BadgeAssertionFactory.create(badge_class=badge_class)
response = self.get_json(self.url(), data={'course_id': course_key})
# pylint: disable=no-member
self.assertEqual(len(response['results']), 3)
unused_course = CourseFactory.create()
response = self.get_json(self.url(), data={'course_id': unused_course.location.course_key})
# pylint: disable=no-member
self.assertEqual(len(response['results']), 0)
def test_assertion_structure(self):
"""
Verify the badge assertion structure is not mangled in this mode.
"""
course_key = self.course.location.course_key
badge_class = BadgeClassFactory.create(course_id=course_key)
assertion = BadgeAssertionFactory.create(badge_class=badge_class, user=self.user)
response = self.get_json(self.url())
# pylint: disable=no-member
self.check_assertion_structure(assertion, response['results'][0])
class TestUserBadgeAssertionsByClass(UserAssertionTestCase):
"""
Test the Badge Assertions view with the badge class filter.
"""
def test_get_assertions(self):
"""
Verify we can get assertions via the badge class and username.
"""
badge_class = self.create_badge_class()
for dummy in range(3):
BadgeAssertionFactory.create(user=self.user, badge_class=badge_class)
if badge_class.course_id:
# Also create a version of this badge under a different course.
alt_class = BadgeClassFactory.create(
slug=badge_class.slug, issuing_component=badge_class.issuing_component,
course_id=CourseFactory.create().location.course_key
)
BadgeAssertionFactory.create(user=self.user, badge_class=alt_class)
# Should not be in list.
for dummy in range(5):
BadgeAssertionFactory.create(badge_class=badge_class)
# Also should not be in list.
for dummy in range(6):
BadgeAssertionFactory.create()
response = self.get_json(
self.url(),
data=self.get_qs_args(badge_class),
)
if self.WILDCARD:
expected_length = 4
else:
expected_length = 3
# pylint: disable=no-member
self.assertEqual(len(response['results']), expected_length)
unused_class = self.create_badge_class(slug='unused_slug', issuing_component='unused_component')
response = self.get_json(
self.url(),
data=self.get_qs_args(unused_class),
)
# pylint: disable=no-member
self.assertEqual(len(response['results']), 0)
def check_badge_class_assertion(self, badge_class):
"""
Given a badge class, create an assertion for the current user and fetch it, checking the structure.
"""
assertion = BadgeAssertionFactory.create(badge_class=badge_class, user=self.user)
response = self.get_json(
self.url(),
data=self.get_qs_args(badge_class),
)
# pylint: disable=no-member
self.check_assertion_structure(assertion, response['results'][0])
def test_assertion_structure(self):
self.check_badge_class_assertion(self.create_badge_class())
def test_empty_issuing_component(self):
self.check_badge_class_assertion(self.create_badge_class(issuing_component=''))
# pylint: disable=test-inherits-tests
class TestUserBadgeAssertionsByClassCourse(TestUserBadgeAssertionsByClass):
"""
Test searching all assertions for a user with a course bound badge class.
"""
CHECK_COURSE = True
# pylint: disable=test-inherits-tests
class TestUserBadgeAssertionsByClassWildCard(TestUserBadgeAssertionsByClassCourse):
"""
Test searching slugs/issuing_components across all course IDs.
"""
WILDCARD = True
"""
URLs for badges API
"""
from django.conf import settings
from django.conf.urls import patterns, url
from .views import UserBadgeAssertions
urlpatterns = patterns(
'badges.api',
url('^assertions/user/' + settings.USERNAME_PATTERN + '/$', UserBadgeAssertions.as_view(), name='user_assertions'),
)
"""
API views for badges
"""
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from rest_framework import generics
from rest_framework.exceptions import APIException
from badges.models import BadgeAssertion
from openedx.core.lib.api.view_utils import view_auth_classes
from .serializers import BadgeAssertionSerializer
from xmodule_django.models import CourseKeyField
class CourseKeyError(APIException):
"""
Raised the course key given isn't valid.
"""
status_code = 400
default_detail = "The course key provided could not be parsed."
@view_auth_classes(is_user=True)
class UserBadgeAssertions(generics.ListAPIView):
"""
** Use cases **
Request a list of assertions for a user, optionally constrained to a course.
** Example Requests **
GET /api/badges/v1/assertions/user/{username}/
** Response Values **
Body comprised of a list of objects with the following fields:
* badge_class: The badge class the assertion was awarded for. Represented as an object
with the following fields:
* slug: The identifier for the badge class
* issuing_component: The software component responsible for issuing this badge.
* display_name: The display name of the badge.
* course_id: The course key of the course this badge is scoped to, or null if it isn't scoped to a course.
* description: A description of the award and its significance.
* criteria: A description of what is needed to obtain this award.
* image_url: A URL to the icon image used to represent this award.
* image_url: The baked assertion image derived from the badge_class icon-- contains metadata about the award
in its headers.
* assertion_url: The URL to the OpenBadges BadgeAssertion object, for verification by compatible tools
and software.
** Params **
* slug (optional): The identifier for a particular badge class to filter by.
* issuing_component (optional): The issuing component for a particular badge class to filter by
(requires slug to have been specified, or this will be ignored.) If slug is provided and this is not,
assumes the issuing_component should be empty.
* course_id (optional): Returns assertions that were awarded as part of a particular course. If slug is
provided, and this field is not specified, assumes that the target badge has an empty course_id field.
'*' may be used to get all badges with the specified slug, issuing_component combination across all courses.
** Returns **
* 200 on success, with a list of Badge Assertion objects.
* 403 if a user who does not have permission to masquerade as
another user specifies a username other than their own.
* 404 if the specified user does not exist
{
"count": 7,
"previous": null,
"num_pages": 1,
"results": [
{
"badge_class": {
"slug": "special_award",
"issuing_component": "edx__course",
"display_name": "Very Special Award",
"course_id": "course-v1:edX+DemoX+Demo_Course",
"description": "Awarded for people who did something incredibly special",
"criteria": "Do something incredibly special.",
"image": "http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png"
},
"image_url": "http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png",
"assertion_url": "http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6"
},
...
]
}
"""
serializer_class = BadgeAssertionSerializer
def get_queryset(self):
"""
Get all badges for the username specified.
"""
queryset = BadgeAssertion.objects.filter(user__username=self.kwargs['username'])
provided_course_id = self.request.query_params.get('course_id')
if provided_course_id == '*':
# We might want to get all the matching course scoped badges to see how many courses
# a user managed to get a specific award on.
course_id = None
elif provided_course_id:
try:
course_id = CourseKey.from_string(provided_course_id)
except InvalidKeyError:
raise CourseKeyError
elif 'slug' not in self.request.query_params:
# Need to get all badges for the user.
course_id = None
else:
course_id = CourseKeyField.Empty
if course_id is not None:
queryset = queryset.filter(badge_class__course_id=course_id)
if self.request.query_params.get('slug'):
queryset = queryset.filter(
badge_class__slug=self.request.query_params['slug'],
badge_class__issuing_component=self.request.query_params.get('issuing_component', '')
)
return queryset
...@@ -32,7 +32,7 @@ def validate_lowercase(string): ...@@ -32,7 +32,7 @@ def validate_lowercase(string):
""" """
Validates that a string is lowercase. Validates that a string is lowercase.
""" """
if not string == string.lower(): if not string.islower():
raise ValidationError(_(u"This value must be all lowercase.")) raise ValidationError(_(u"This value must be all lowercase."))
......
...@@ -54,7 +54,7 @@ class RandomBadgeClassFactory(BadgeClassFactory): ...@@ -54,7 +54,7 @@ class RandomBadgeClassFactory(BadgeClassFactory):
""" """
Same as BadgeClassFactory, but randomize the slug. Same as BadgeClassFactory, but randomize the slug.
""" """
slug = factory.lazy_attribute(lambda _: 'test_slug_' + str(random())) slug = factory.lazy_attribute(lambda _: 'test_slug_' + str(random()).replace('.', '_'))
class BadgeAssertionFactory(DjangoModelFactory): class BadgeAssertionFactory(DjangoModelFactory):
......
...@@ -136,6 +136,11 @@ if settings.FEATURES["ENABLE_MOBILE_REST_API"]: ...@@ -136,6 +136,11 @@ if settings.FEATURES["ENABLE_MOBILE_REST_API"]:
url(r'^api/mobile/v0.5/', include('mobile_api.urls')), url(r'^api/mobile/v0.5/', include('mobile_api.urls')),
) )
if settings.FEATURES["ENABLE_OPENBADGES"]:
urlpatterns += (
url(r'^api/badges/v1/', include('badges.api.urls')),
)
js_info_dict = { js_info_dict = {
'domain': 'djangojs', 'domain': 'djangojs',
# We need to explicitly include external Django apps that are not in LOCALE_PATHS. # We need to explicitly include external Django apps that are not in LOCALE_PATHS.
......
"""Tests for the user API at the HTTP request level. """ """Tests for the user API at the HTTP request level. """
import datetime import datetime
import base64
import json import json
import re
from unittest import skipUnless, SkipTest from unittest import skipUnless, SkipTest
import ddt import ddt
import httpretty import httpretty
from pytz import UTC
import mock import mock
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from django.core import mail
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase from django.core import mail
from django.core.urlresolvers import reverse
from django.test.client import RequestFactory
from django.test.testcases import TransactionTestCase from django.test.testcases import TransactionTestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.test.client import RequestFactory
from social.apps.django_app.default.models import UserSocialAuth
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from pytz import UTC
from social.apps.django_app.default.models import UserSocialAuth
from django_comment_common import models from django_comment_common import models
from openedx.core.lib.api.test_utils import ApiTestCase, TEST_API_KEY
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin
from third_party_auth.tests.utils import ( from third_party_auth.tests.utils import (
ThirdPartyOAuthTestMixin, ThirdPartyOAuthTestMixinFacebook, ThirdPartyOAuthTestMixinGoogle ThirdPartyOAuthTestMixin, ThirdPartyOAuthTestMixinFacebook, ThirdPartyOAuthTestMixinGoogle
) )
from .test_helpers import TestCaseForm from .test_helpers import TestCaseForm
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from ..accounts.api import get_account_settings from xmodule.modulestore.tests.factories import CourseFactory
from ..accounts import ( from ..accounts import (
NAME_MAX_LENGTH, EMAIL_MIN_LENGTH, EMAIL_MAX_LENGTH, PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH, NAME_MAX_LENGTH, EMAIL_MIN_LENGTH, EMAIL_MAX_LENGTH, PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH,
USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH
) )
from ..accounts.api import get_account_settings
from ..models import UserOrgTag from ..models import UserOrgTag
from ..tests.factories import UserPreferenceFactory from ..tests.factories import UserPreferenceFactory
from ..tests.test_constants import SORTED_COUNTRIES from ..tests.test_constants import SORTED_COUNTRIES
TEST_API_KEY = "test_api_key"
USER_LIST_URI = "/user_api/v1/users/" USER_LIST_URI = "/user_api/v1/users/"
USER_PREFERENCE_LIST_URI = "/user_api/v1/user_prefs/" USER_PREFERENCE_LIST_URI = "/user_api/v1/user_prefs/"
ROLE_LIST_URI = "/user_api/v1/forum_roles/Moderator/users/" ROLE_LIST_URI = "/user_api/v1/forum_roles/Moderator/users/"
@override_settings(EDX_API_KEY=TEST_API_KEY) class UserAPITestCase(ApiTestCase):
class ApiTestCase(TestCase):
""" """
Parent test case for API workflow coverage Parent test case for User API workflow coverage
""" """
LIST_URI = USER_LIST_URI LIST_URI = USER_LIST_URI
def basic_auth(self, username, password):
"""
Returns a dictionary containing the http auth header with encoded username+password
"""
return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('%s:%s' % (username, password))}
def request_with_auth(self, method, *args, **kwargs):
"""Issue a get request to the given URI with the API key header"""
return getattr(self.client, method)(*args, HTTP_X_EDX_API_KEY=TEST_API_KEY, **kwargs)
def get_json(self, *args, **kwargs):
"""Make a request with the given args and return the parsed JSON repsonse"""
resp = self.request_with_auth("get", *args, **kwargs)
self.assertHttpOK(resp)
self.assertTrue(resp["Content-Type"].startswith("application/json"))
return json.loads(resp.content)
def get_uri_for_user(self, target_user): def get_uri_for_user(self, target_user):
"""Given a user object, get the URI for the corresponding resource""" """Given a user object, get the URI for the corresponding resource"""
users = self.get_json(USER_LIST_URI)["results"] users = self.get_json(USER_LIST_URI)["results"]
...@@ -90,20 +64,6 @@ class ApiTestCase(TestCase): ...@@ -90,20 +64,6 @@ class ApiTestCase(TestCase):
return pref["url"] return pref["url"]
self.fail() self.fail()
def assertAllowedMethods(self, uri, expected_methods):
"""Assert that the allowed methods for the given URI match the expected list"""
resp = self.request_with_auth("options", uri)
self.assertHttpOK(resp)
allow_header = resp.get("Allow")
self.assertIsNotNone(allow_header)
allowed_methods = re.split('[^A-Z]+', allow_header)
self.assertItemsEqual(allowed_methods, expected_methods)
def assertSelfReferential(self, obj):
"""Assert that accessing the "url" entry in the given object returns the same object"""
copy = self.get_json(obj["url"])
self.assertEqual(obj, copy)
def assertUserIsValid(self, user): def assertUserIsValid(self, user):
"""Assert that the given user result is valid""" """Assert that the given user result is valid"""
self.assertItemsEqual(user.keys(), ["email", "id", "name", "username", "preferences", "url"]) self.assertItemsEqual(user.keys(), ["email", "id", "name", "username", "preferences", "url"])
...@@ -121,37 +81,8 @@ class ApiTestCase(TestCase): ...@@ -121,37 +81,8 @@ class ApiTestCase(TestCase):
self.assertSelfReferential(pref) self.assertSelfReferential(pref)
self.assertUserIsValid(pref["user"]) self.assertUserIsValid(pref["user"])
def assertHttpOK(self, response):
"""Assert that the given response has the status code 200"""
self.assertEqual(response.status_code, 200)
def assertHttpForbidden(self, response):
"""Assert that the given response has the status code 403"""
self.assertEqual(response.status_code, 403)
def assertHttpBadRequest(self, response):
"""Assert that the given response has the status code 400"""
self.assertEqual(response.status_code, 400)
def assertHttpMethodNotAllowed(self, response):
"""Assert that the given response has the status code 405"""
self.assertEqual(response.status_code, 405)
def assertAuthDisabled(self, method, uri):
"""
Assert that the Django rest framework does not interpret basic auth
headers for views exposed to anonymous users as an attempt to authenticate.
"""
# Django rest framework interprets basic auth headers
# as an attempt to authenticate with the API.
# We don't want this for views available to anonymous users.
basic_auth_header = "Basic " + base64.b64encode('username:password')
response = getattr(self.client, method)(uri, HTTP_AUTHORIZATION=basic_auth_header)
self.assertNotEqual(response.status_code, 403)
class EmptyUserTestCase(ApiTestCase): class EmptyUserTestCase(UserAPITestCase):
""" """
Test that the endpoint supports empty user result sets Test that the endpoint supports empty user result sets
""" """
...@@ -163,7 +94,7 @@ class EmptyUserTestCase(ApiTestCase): ...@@ -163,7 +94,7 @@ class EmptyUserTestCase(ApiTestCase):
self.assertEqual(result["results"], []) self.assertEqual(result["results"], [])
class EmptyRoleTestCase(ApiTestCase): class EmptyRoleTestCase(UserAPITestCase):
"""Test that the endpoint supports empty result sets""" """Test that the endpoint supports empty result sets"""
course_id = SlashSeparatedCourseKey.from_deprecated_string("org/course/run") course_id = SlashSeparatedCourseKey.from_deprecated_string("org/course/run")
LIST_URI = ROLE_LIST_URI + "?course_id=" + course_id.to_deprecated_string() LIST_URI = ROLE_LIST_URI + "?course_id=" + course_id.to_deprecated_string()
...@@ -177,7 +108,7 @@ class EmptyRoleTestCase(ApiTestCase): ...@@ -177,7 +108,7 @@ class EmptyRoleTestCase(ApiTestCase):
self.assertEqual(result["results"], []) self.assertEqual(result["results"], [])
class UserApiTestCase(ApiTestCase): class UserApiTestCase(UserAPITestCase):
""" """
Generalized test case class for specific implementations below Generalized test case class for specific implementations below
""" """
...@@ -607,7 +538,7 @@ class PreferenceUsersListViewTest(UserApiTestCase): ...@@ -607,7 +538,7 @@ class PreferenceUsersListViewTest(UserApiTestCase):
@ddt.ddt @ddt.ddt
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class LoginSessionViewTest(ApiTestCase): class LoginSessionViewTest(UserAPITestCase):
"""Tests for the login end-points of the user API. """ """Tests for the login end-points of the user API. """
USERNAME = "bob" USERNAME = "bob"
...@@ -773,7 +704,7 @@ class LoginSessionViewTest(ApiTestCase): ...@@ -773,7 +704,7 @@ class LoginSessionViewTest(ApiTestCase):
@ddt.ddt @ddt.ddt
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class PasswordResetViewTest(ApiTestCase): class PasswordResetViewTest(UserAPITestCase):
"""Tests of the user API's password reset endpoint. """ """Tests of the user API's password reset endpoint. """
def setUp(self): def setUp(self):
...@@ -829,7 +760,7 @@ class PasswordResetViewTest(ApiTestCase): ...@@ -829,7 +760,7 @@ class PasswordResetViewTest(ApiTestCase):
@ddt.ddt @ddt.ddt
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class RegistrationViewTest(ThirdPartyAuthTestMixin, ApiTestCase): class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
"""Tests for the registration end-points of the User API. """ """Tests for the registration end-points of the User API. """
maxDiff = None maxDiff = None
...@@ -1925,7 +1856,7 @@ class TestGoogleRegistrationView( ...@@ -1925,7 +1856,7 @@ class TestGoogleRegistrationView(
@ddt.ddt @ddt.ddt
class UpdateEmailOptInTestCase(ApiTestCase, SharedModuleStoreTestCase): class UpdateEmailOptInTestCase(UserAPITestCase, SharedModuleStoreTestCase):
"""Tests the UpdateEmailOptInPreference view. """ """Tests the UpdateEmailOptInPreference view. """
USERNAME = "steve" USERNAME = "steve"
......
"""
Helpers for API tests.
"""
import base64
import json
import re
from django.test import TestCase
from django.test.utils import override_settings
TEST_API_KEY = "test_api_key"
@override_settings(EDX_API_KEY=TEST_API_KEY)
class ApiTestCase(TestCase):
"""
Parent test case for API workflow coverage
"""
def basic_auth(self, username, password):
"""
Returns a dictionary containing the http auth header with encoded username+password
"""
return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('%s:%s' % (username, password))}
def request_with_auth(self, method, *args, **kwargs):
"""Issue a get request to the given URI with the API key header"""
return getattr(self.client, method)(*args, HTTP_X_EDX_API_KEY=TEST_API_KEY, **kwargs)
def get_json(self, *args, **kwargs):
"""Make a request with the given args and return the parsed JSON repsonse"""
resp = self.request_with_auth("get", *args, **kwargs)
self.assertHttpOK(resp)
self.assertTrue(resp["Content-Type"].startswith("application/json"))
return json.loads(resp.content)
def assertAllowedMethods(self, uri, expected_methods):
"""Assert that the allowed methods for the given URI match the expected list"""
resp = self.request_with_auth("options", uri)
self.assertHttpOK(resp)
allow_header = resp.get("Allow")
self.assertIsNotNone(allow_header)
allowed_methods = re.split('[^A-Z]+', allow_header)
self.assertItemsEqual(allowed_methods, expected_methods)
def assertSelfReferential(self, obj):
"""Assert that accessing the "url" entry in the given object returns the same object"""
copy = self.get_json(obj["url"])
self.assertEqual(obj, copy)
def assertHttpOK(self, response):
"""Assert that the given response has the status code 200"""
self.assertEqual(response.status_code, 200)
def assertHttpForbidden(self, response):
"""Assert that the given response has the status code 403"""
self.assertEqual(response.status_code, 403)
def assertHttpBadRequest(self, response):
"""Assert that the given response has the status code 400"""
self.assertEqual(response.status_code, 400)
def assertHttpMethodNotAllowed(self, response):
"""Assert that the given response has the status code 405"""
self.assertEqual(response.status_code, 405)
def assertAuthDisabled(self, method, uri):
"""
Assert that the Django rest framework does not interpret basic auth
headers for views exposed to anonymous users as an attempt to authenticate.
"""
# Django rest framework interprets basic auth headers
# as an attempt to authenticate with the API.
# We don't want this for views available to anonymous users.
basic_auth_header = "Basic " + base64.b64encode('username:password')
response = getattr(self.client, method)(uri, HTTP_AUTHORIZATION=basic_auth_header)
self.assertNotEqual(response.status_code, 403)
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