Skip to content

Commit

Permalink
Added V2 functionality to handle queries against a program_uuid inste…
Browse files Browse the repository at this point in the history
…ad of program_id

Added unit tests for this V2 functionality
Adjusted V1 unit tests to confirm it is a V1 request not a V2
ECOM-6482
  • Loading branch information
davec-edx committed Dec 21, 2016
1 parent 69ec92c commit 1ec47e3
Show file tree
Hide file tree
Showing 16 changed files with 381 additions and 119 deletions.
11 changes: 0 additions & 11 deletions credentials/apps/api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,6 @@
from credentials.apps.credentials.models import UserCredential


class ProgramFilter(django_filters.FilterSet):
""" Allows for filtering program credentials by their program_id and status
using a query string argument.
"""
program_id = django_filters.NumberFilter(name="program_credentials__program_id")

class Meta:
model = UserCredential
fields = ['program_id', 'status']


class CourseFilter(django_filters.FilterSet):
""" Allows for filtering course credentials by their course_id, status and
certificate_type using a query string argument.
Expand Down
4 changes: 2 additions & 2 deletions credentials/apps/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ def to_internal_value(self, data):
try:
if 'program_id' in data and data.get('program_id'):
credential_id = data['program_id']
return ProgramCertificate.objects.get(program_id=data['program_id'], is_active=True)
return ProgramCertificate.objects.get(program_id=credential_id, is_active=True)
elif 'course_id' in data and data.get('course_id') and data.get('certificate_type'):
credential_id = data['course_id']
return CourseCertificate.objects.get(
course_id=data['course_id'],
course_id=credential_id,
certificate_type=data['certificate_type'],
is_active=True
)
Expand Down
159 changes: 62 additions & 97 deletions credentials/apps/api/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
"""
Tests for credentials service views.
"""
# pylint: disable=no-member
from __future__ import unicode_literals
import json
Expand All @@ -17,20 +14,71 @@
from credentials.apps.credentials.models import UserCredential
from credentials.apps.credentials.tests import factories


JSON_CONTENT_TYPE = 'application/json'
LOGGER_NAME = 'credentials.apps.credentials.issuers'
LOGGER_NAME_SERIALIZER = 'credentials.apps.api.serializers'


class CredentialViewSetTests(APITestCase):
""" Base Class for ProgramCredentialViewSetTests and CourseCredentialViewSetTests. """

list_path = None
user_credential = None

def setUp(self):
super(CredentialViewSetTests, self).setUp()

self.user = UserFactory()
self.user.groups.add(Group.objects.get(name=Role.ADMINS))
self.client.force_authenticate(self.user)
self.request = APIRequestFactory().get('/')

def assert_permission_required(self, data):
"""
Ensure access to these APIs is restricted to those with explicit model
permissions.
"""
self.client.force_authenticate(user=UserFactory())
response = self.client.get(self.list_path, data)
self.assertEqual(response.status_code, 403)

def assert_list_without_id_filter(self, path, expected, data=None):
"""Helper method used for making request and assertions. """
response = self.client.get(path, data)

self.assertEqual(response.status_code, 400)
self.assertEqual(response.data, expected)

def assert_list_with_id_filter(self, data=None, should_exist=True):
"""Helper method used for making request and assertions. """
expected = self._generate_results(should_exist)
response = self.client.get(self.list_path, data)

self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, expected)

def assert_list_with_status_filter(self, data, should_exist=True):
"""Helper method for making request and assertions. """
expected = self._generate_results(should_exist)
response = self.client.get(self.list_path, data, expected)
self.assertEqual(json.loads(response.content), expected)

def _generate_results(self, exists=True):
if exists:
return {'count': 1, 'next': None, 'previous': None,
'results': [UserCredentialSerializer(self.user_credential, context={'request': self.request}).data]}

return {'count': 0, 'next': None, 'previous': None, 'results': []}


@ddt.ddt
class UserCredentialViewSetTests(APITestCase):
class BaseUserCredentialViewSetTests(object):
""" Tests for GenerateCredentialView. """

list_path = reverse("api:v1:usercredential-list")
list_path = None

def setUp(self):
super(UserCredentialViewSetTests, self).setUp()
super(BaseUserCredentialViewSetTests, self).setUp()

self.user = UserFactory()
self.client.force_authenticate(self.user)
Expand Down Expand Up @@ -449,10 +497,11 @@ def test_users_lists_access_by_authenticated_users(self):


@ddt.ddt
class UserCredentialViewSetPermissionsTests(APITestCase):
class BaseUserCredentialViewSetPermissionsTests(object):
"""
Thoroughly exercise the custom view- and object-level permissions for this viewset.
"""
list_path = None

def make_user(self, group=None, perm=None, **kwargs):
""" DRY helper to create users with specific groups and/or permissions. """
Expand All @@ -478,10 +527,9 @@ def test_list(self, user_kwargs, expected_status):
The list method (GET) requires either 'view' permission, or for the
'username' query parameter to match that of the requesting user.
"""
list_path = reverse("api:v1:usercredential-list")

self.client.force_authenticate(self.make_user(**user_kwargs))
response = self.client.get(list_path, {'username': 'test-user'})
response = self.client.get(self.list_path, {'username': 'test-user'})
self.assertEqual(response.status_code, expected_status)

@ddt.data(
Expand All @@ -497,7 +545,6 @@ def test_create(self, user_kwargs, expected_status):
"""
The creation (POST) method requires the 'add' permission.
"""
list_path = reverse('api:v1:usercredential-list')
program_certificate = factories.ProgramCertificateFactory()
post_data = {
'username': 'test-user',
Expand All @@ -508,7 +555,7 @@ def test_create(self, user_kwargs, expected_status):
}

self.client.force_authenticate(self.make_user(**user_kwargs))
response = self.client.post(list_path, data=json.dumps(post_data), content_type=JSON_CONTENT_TYPE)
response = self.client.post(self.list_path, data=json.dumps(post_data), content_type=JSON_CONTENT_TYPE)
self.assertEqual(response.status_code, expected_status)

@ddt.data(
Expand Down Expand Up @@ -563,95 +610,13 @@ def test_partial_update(self, user_kwargs, expected_status):
self.assertEqual(response.status_code, expected_status)


class CredentialViewSetTests(APITestCase):
""" Base Class for ProgramCredentialViewSetTests and CourseCredentialViewSetTests. """

list_path = None
user_credential = None

def setUp(self):
super(CredentialViewSetTests, self).setUp()

self.user = UserFactory()
self.user.groups.add(Group.objects.get(name=Role.ADMINS))
self.client.force_authenticate(self.user)
self.request = APIRequestFactory().get('/')

def assert_permission_required(self, data):
"""
Ensure access to these APIs is restricted to those with explicit model
permissions.
"""
self.client.force_authenticate(user=UserFactory())
response = self.client.get(self.list_path, data)
self.assertEqual(response.status_code, 403)

def assert_list_without_id_filter(self, path, expected):
"""Helper method used for making request and assertions. """
response = self.client.get(path)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.data, expected)

def assert_list_with_id_filter(self, data):
"""Helper method used for making request and assertions. """
expected = {'count': 1, 'next': None, 'previous': None,
'results': [UserCredentialSerializer(self.user_credential, context={'request': self.request}).data]}
response = self.client.get(self.list_path, data)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, expected)

def assert_list_with_status_filter(self, data):
"""Helper method for making request and assertions. """
expected = {'count': 1, 'next': None, 'previous': None,
'results': [UserCredentialSerializer(self.user_credential, context={'request': self.request}).data]}
response = self.client.get(self.list_path, data, expected)
self.assertEqual(json.loads(response.content), expected)


class ProgramCredentialViewSetTests(CredentialViewSetTests):
""" Tests for ProgramCredentialViewSetTests. """

list_path = reverse("api:v1:programcredential-list")

def setUp(self):
super(ProgramCredentialViewSetTests, self).setUp()

self.program_certificate = factories.ProgramCertificateFactory()
self.program_id = self.program_certificate.program_id
self.user_credential = factories.UserCredentialFactory.create(credential=self.program_certificate)
self.request = APIRequestFactory().get('/')

def test_list_without_program_id(self):
""" Verify a list end point of program credentials will work only with
program_id filter.
"""
self.assert_list_without_id_filter(path=self.list_path, expected={
'error': 'A program_id query string parameter is required for filtering program credentials.'
})

def test_list_with_program_id_filter(self):
""" Verify the list endpoint supports filter data by program_id."""
program_cert = factories.ProgramCertificateFactory(program_id=001)
factories.UserCredentialFactory.create(credential=program_cert)
self.assert_list_with_id_filter(data={'program_id': self.program_id})

def test_list_with_status_filter(self):
""" Verify the list endpoint supports filtering by status."""
factories.UserCredentialFactory.create_batch(2, status="revoked", username=self.user_credential.username)
self.assert_list_with_status_filter(data={'program_id': self.program_id, 'status': UserCredential.AWARDED}, )

def test_permission_required(self):
""" Verify that requests require explicit model permissions. """
self.assert_permission_required({'program_id': self.program_id, 'status': UserCredential.AWARDED})


class CourseCredentialViewSetTests(CredentialViewSetTests):
class BaseCourseCredentialViewSetTests(object):
""" Tests for CourseCredentialViewSetTests. """

list_path = reverse("api:v1:coursecredential-list")
list_path = None

def setUp(self):
super(CourseCredentialViewSetTests, self).setUp()
super(BaseCourseCredentialViewSetTests, self).setUp()

self.course_certificate = factories.CourseCertificateFactory()
self.course_id = self.course_certificate.course_id
Expand Down
1 change: 1 addition & 0 deletions credentials/apps/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@

urlpatterns = [
url(r'^v1/', include('credentials.apps.api.v1.urls', namespace='v1')),
url(r'^v2/', include('credentials.apps.api.v2.urls', namespace='v2')),
]
16 changes: 16 additions & 0 deletions credentials/apps/api/v1/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import django_filters

from credentials.apps.credentials.models import UserCredential


class UserCredentialFilter(django_filters.FilterSet):
""" Allows for filtering program credentials by their program_id and status
using a query string argument.
"""
program_id = django_filters.NumberFilter(name="program_credentials__program_id")

class Meta:
model = UserCredential
fields = ['program_id', 'status']


1 change: 0 additions & 1 deletion credentials/apps/api/v1/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
# Create your tests in sub-packages prefixed with "test_" (e.g. test_views).
91 changes: 91 additions & 0 deletions credentials/apps/api/v1/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
Tests for credentials service views.
"""
# pylint: disable=no-member
from __future__ import unicode_literals

from django.core.urlresolvers import reverse
from rest_framework.test import APIRequestFactory, APITestCase

from credentials.apps.api.tests.test_views import CredentialViewSetTests, BaseUserCredentialViewSetTests, \
BaseUserCredentialViewSetPermissionsTests, BaseCourseCredentialViewSetTests
from credentials.apps.credentials.models import UserCredential
from credentials.apps.credentials.tests import factories


class ProgramCredentialViewSetTests(CredentialViewSetTests):
""" Tests for ProgramCredentialViewSetTests. """

list_path = reverse("api:v1:programcredential-list")

def setUp(self):
super(ProgramCredentialViewSetTests, self).setUp()

self.program_certificate = factories.ProgramCertificateFactory()
self.program_id = self.program_certificate.program_id
self.user_credential = factories.UserCredentialFactory.create(credential=self.program_certificate)
self.request = APIRequestFactory().get('/')

def test_list_without_program_id(self):
""" Verify a list end point of program credentials will work only with
program_id filter.
"""
self.assert_list_without_id_filter(path=self.list_path, expected={
'error': 'A program_id query string parameter is required for filtering program credentials.'
})

def test_list_with_uuid_but_not_id(self):
""" Verify a list end point of program credentials will work with
program_uuid filter.
"""
self.assert_list_without_id_filter(path=self.list_path, data={'program_uuid': self.program_id}, expected={
'error': 'A program_id query string parameter is required for filtering program credentials.'
})

def test_list_with_both_uuid_and_id(self):
""" Verify a list end point of program credentials will work with
program_uuid filter.
"""
error_message = {'error': 'A program_uuid query string parameter should not appear in V1 queries.'}
self.assert_list_without_id_filter(path=self.list_path,
data={'program_uuid': self.program_id,
'program_id': self.program_id},
expected=error_message)

def test_list_with_program_id_filter(self):
""" Verify the list endpoint supports filter data by program_id."""
program_cert = factories.ProgramCertificateFactory(program_id=1)
factories.UserCredentialFactory.create(credential=program_cert)
self.assert_list_with_id_filter(data={'program_id': self.program_id})

def test_list_with_program_invalid_id_filter(self):
""" Verify the list endpoint supports filter data by program_id."""
program_cert = factories.ProgramCertificateFactory(program_id=1)
factories.UserCredentialFactory.create(credential=program_cert)
self.assert_list_with_id_filter(data={'program_id': 50}, should_exist=False)

def test_list_with_status_filter(self):
""" Verify the list endpoint supports filtering by status."""
factories.UserCredentialFactory.create_batch(2, status="revoked", username=self.user_credential.username)
self.assert_list_with_status_filter(data={'program_id': self.program_id, 'status': UserCredential.AWARDED})

def test_list_with_bad_status_filter(self):
""" Verify the list endpoint supports filtering by status."""
self.assert_list_with_status_filter(data={'program_id': self.program_id, 'status': UserCredential.REVOKED},
should_exist=False)

def test_permission_required(self):
""" Verify that requests require explicit model permissions. """
self.assert_permission_required({'program_id': self.program_id, 'status': UserCredential.AWARDED})


class UserCredentialViewSetTests(BaseUserCredentialViewSetTests, APITestCase):
list_path = reverse("api:v1:usercredential-list")


class UserCredentialViewSetPermissionsTests(BaseUserCredentialViewSetPermissionsTests, APITestCase):
list_path = reverse("api:v1:usercredential-list")


class CourseCredentialViewSetTests(BaseCourseCredentialViewSetTests, CredentialViewSetTests):
list_path = reverse("api:v1:coursecredential-list")
3 changes: 1 addition & 2 deletions credentials/apps/api/v1/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
""" API v1 URLs. """
from rest_framework.routers import DefaultRouter

from credentials.apps.api.v1 import views


router = DefaultRouter() # pylint: disable=invalid-name
# URL can not have hyphen as it is not currently supported by slumber
# URLs can not have hyphen as it is not currently supported by slumber
# as mentioned https://github.com/samgiles/slumber/issues/44
router.register(r'user_credentials', views.UserCredentialViewSet)
router.register(r'program_credentials', views.ProgramsCredentialsViewSet, base_name='programcredential')
Expand Down
Loading

0 comments on commit 1ec47e3

Please sign in to comment.