Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added V2 functionality to handle queries against a program_uuid inste… #136

Merged
merged 1 commit into from
Jan 9, 2017

Conversation

davec-edx
Copy link
Contributor

@davec-edx davec-edx commented Dec 20, 2016

Part of the conversion from program_id to program_uuid... Added a V2 endpoint for credentials that would function using program_uuid instead of a 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
@edx/ecommerce

@@ -5,6 +5,17 @@
from credentials.apps.credentials.models import UserCredential


class ProgramFilterByUuid(django_filters.FilterSet):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be UserCredentialFilter going forward since it is a filter for UserCredential objects. Sub-class it, add the program_id filter, call it something else, and use the sub-class for the v1 API. You could also move the sub-class to /api/v1/filters.py.

'program_id': self.program_id}, expected={
'error': 'A program_uuid query string parameter should not appear in V1 queries.'
})

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1

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_program_invalid_id_filter(self):
""" Verify the list endpoint supports filter data by program_id."""
program_cert = factories.ProgramCertificateFactory(program_id=001)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1

@@ -54,6 +54,10 @@ def list(self, request, *args, **kwargs):
raise ValidationError(
{'error': 'A program_id query string parameter is required for filtering program credentials.'})

if self.request.query_params.get('program_uuid'):
raise ValidationError(
{'error': 'A program_uuid query string parameter should not appear in V1 queries.'})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not? We can be forwards-compatible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be injecting additional query parameters, the consumer is either calling a V1 or a V2, if they have the uuid parameter it should be V2. If we do this, then what's the point of versioning the api?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave the v1 API alone. It is already setup properly to respond to requests that don't include the program_id parameter. If a client passes the program_uuid parameter, the v1 API shouldn't care. It's the same as if the client passed the foobar parameter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a helper for the upgrade to V2. You are correct, the parameter will be ignored, but it will result in the wrong type of error (incorrect parameter vs missing). When we deprecate it it will go away.
Removing it now.

@@ -0,0 +1 @@
# Create your tests in sub-packages prefixed with "test_" (e.g. test_views).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Useless comment is useless. Also, where are the tests for ProgramsCredentialsViewSet?

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also need to include the other resources from API v1.

@@ -0,0 +1,37 @@
"""
Credentials service API views (v1).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These docstrings are rarely useful. Remove them.

@@ -0,0 +1,11 @@
""" API v2 URLs. """
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove.

{'error': 'A UUID query string parameter is required for filtering program credentials.'})

# Confirmation that we are not supplying both parameters. We should only be providing the program_uuid in V2
if self.request.query_params.get('program_id'):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is not useful. Just ignore the parameter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would treat this as any other arbitrary parameter. We don't explicitly block calls made with other parameters. Why block this one? If they aren't using UUID, we'll error out. If they are, they will get the expected result. The odds of anyone using both are low. This is unnecessary defensive coding.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be removed, along with associated tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about this on Tuesday and thought the resolution was different. API security dictates that we should A) Validate that all parameters/options that are given are of the correct type, and B) Validate that extra parameters/options are not given. This is to prevent injections that could be potentially harmful. Additionally, in a versioned API we can add helper methods for deprecation of an older version so that consumers of the API are checked to ensure they are not passing "old" parameters. We should add a subtask to the deprecation of the old version to remove this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The query string parameters have nothing to do with security. Yes, we will validate parameters we do care about. When we get parameters we don't care about, we just ignore them. This is the case for all of our APIs. The only place where we drop requests with unexpected parameters are for APIs behind Amazon's API Gateway, and that is only due to a (now lifted) restriction of API Gateway.

We (edx.org) are the only consumers of this API. There is no need for the helpers. The work to modify edx-platform to use this new version of the API is already in the review state: https://github.com/edx/edx-platform/pull/14259.

If this were an API with a larger user base, such as the Catalog API, I would agree that we should be more careful. Since we have exactly one consumer, all you're doing here is adding debt.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davec-edx I'd like to second @clintonb here. If a parameter we don't expect or otherwise need is provided, it'll be ignored. I don't see the value in including this.


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 ProgramCredentialViewSetTestsV2(CredentialViewSetTests):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This belongs in the v2 package.

@davec-edx davec-edx force-pushed the ecom6482 branch 5 times, most recently from 1ec47e3 to ea3868a Compare December 21, 2016 17:52
Copy link
Contributor

@clintonb clintonb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall direction is great; however, I would like to remove the notion of program_id completely from v2 (including tests). Feel free to disregard my comments on test cleanup. We'll be back in this repo for ECOM-4639, and can take care of that then.

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


class CredentialViewSetTests(APITestCase):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a mixin—inherit object—to avoid executing setUp unnecessarily. The test runner considers anything that inherits TestCase to be a test class. Also, rename to CredentialViewSetTestMixin.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be addressed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@@ -0,0 +1,91 @@
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docstring can go away.

"""
Tests for credentials service views.
"""
# pylint: disable=no-member
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally discourage file-wide disabling of PyLint warnings. Does this need to be disabled for anything outside of setUp (which uses factories)?



class ProgramCredentialViewSetTests(CredentialViewSetTests):
""" Tests for ProgramCredentialViewSetTests. """
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docstring doesn't add any value. It can go.

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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single quotes.

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

def assert_list_with_status_filter(self, data, should_exist=True):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is weird given that it hides the status filter in the data argument. Find a way to combine this with the method above. Both seem to do almost the same thing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be ignored. We'll fix it later.


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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserCredential.REVOKED

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in v1 and v2

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_uuid': self.program_uuid, 'status': UserCredential.AWARDED})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow this test, which is a side-effect of these helper methods being used. It would actually be somewhat clearer to explicitly create the data, make the request, and validate the response in each test. The reliance on the helper methods was good in theory to de-dupe code. However, we seem to have taken that to an extreme that complicates understanding some of the tests.

from credentials.apps.api.permissions import UserCredentialViewSetPermissions
from credentials.apps.api.serializers import UserCredentialCreationSerializer, UserCredentialSerializer
from credentials.apps.credentials.models import UserCredential

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this new line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed v1 and v2


from rest_framework import mixins, viewsets
from rest_framework.exceptions import ValidationError
from credentials.apps.api.filters import CourseFilter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be grouped below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in v1 and v2

@davec-edx davec-edx force-pushed the ecom6482 branch 3 times, most recently from 0e05c22 to 8e6cc63 Compare January 3, 2017 21:01
@clintonb
Copy link
Contributor

clintonb commented Jan 4, 2017

@davec-edx please rebase to prepare for merging.

from credentials.apps.api.permissions import UserCredentialViewSetPermissions
from credentials.apps.api.serializers import UserCredentialCreationSerializer, UserCredentialSerializer
from credentials.apps.api.v2.filters import UserCredentialFilter

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this newline

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

{'error': 'A UUID query string parameter is required for filtering program credentials.'})

# Confirmation that we are not supplying both parameters. We should only be providing the program_uuid in V2
if self.request.query_params.get('program_id'):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be removed, along with associated tests.

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

from credentials.apps.api.tests.test_views import (CredentialViewSetTests, BaseUserCredentialViewSetTests,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the contents within the parentheses to ease indentation issues:

from credentials.apps.api.tests.test_views import (
  CredentialViewSetTests, BaseUserCredentialViewSetTests, BaseUserCredentialViewSetPermissionsTests,
  BaseCourseCredentialViewSetTests
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh, I see what you're looking for -- fixed

@@ -54,6 +54,10 @@ def list(self, request, *args, **kwargs):
raise ValidationError(
{'error': 'A program_id query string parameter is required for filtering program credentials.'})

if self.request.query_params.get('program_uuid'):
raise ValidationError(
{'error': 'A program_uuid query string parameter should not appear in V1 queries.'})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be removed.

from credentials.apps.api.permissions import UserCredentialViewSetPermissions
from credentials.apps.api.serializers import UserCredentialCreationSerializer, UserCredentialSerializer
from credentials.apps.credentials.models import UserCredential
from credentials.apps.api.v1.filters import UserCredentialFilter

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the newline.

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


class CredentialViewSetTests(APITestCase):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be addressed.

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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be ignored. We'll fix it later.

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

def assert_list_with_id_filter(self, data=None, should_exist=True):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be ignored. We'll fix it later.

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

def assert_list_with_status_filter(self, data, should_exist=True):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be ignored. We'll fix it later.

@davec-edx davec-edx force-pushed the ecom6482 branch 3 times, most recently from 5803017 to 97f1cf9 Compare January 5, 2017 16:04


class ProgramsCredentialsViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
"""It will return the all credentials for programs based on the program_uuid."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clean up this docstring?

{'error': 'A UUID query string parameter is required for filtering program credentials.'})

# Confirmation that we are not supplying both parameters. We should only be providing the program_uuid in V2
if self.request.query_params.get('program_id'):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davec-edx I'd like to second @clintonb here. If a parameter we don't expect or otherwise need is provided, it'll be ignored. I don't see the value in including this.

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants