-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
2u/add view tests to optimizer #36114
Open
jesperhodge
wants to merge
4
commits into
2u/course-optimizer
Choose a base branch
from
2u/add-view-tests-to-optimizer
base: 2u/course-optimizer
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+127
−16
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
60 changes: 60 additions & 0 deletions
60
cms/djangoapps/contentstore/docs/how-tos/test_course_related_view_auth.rst
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
============================================== | ||
How to test View Auth for course-related views | ||
============================================== | ||
|
||
What to test | ||
------------ | ||
Each view endpoint that exposes an internal API endpoint - like in files in the rest_api folder - must | ||
be tested for the following. | ||
|
||
- Only authenticated users can access the endpoint. | ||
- Only users with the correct permissions (authorization) can access the endpoint. | ||
- All data and params that are part of the request are properly validated. | ||
|
||
How to test | ||
----------- | ||
A lot of these tests can be easily implemented by inheriting from the `AuthorizeStaffTestCase`. | ||
This parent class assumes that the view is for a specific course and that only users who have access | ||
to the course can access the view. (They are either staff or instructors for the course, or global admin). | ||
|
||
Here is an example of how to test a view that requires a user to be authenticated and have access to a course. | ||
|
||
.. code-block:: python | ||
|
||
from cms.djangoapps.contentstore.tests.test_utils import AuthorizeStaffTestCase | ||
from django.test import TestCase | ||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase | ||
from django.urls import reverse | ||
|
||
class TestMyGetView(AuthorizeStaffTestCase, ModuleStoreTestCase, TestCase): | ||
def make_request(self, course_id=None, data=None): | ||
url = self.get_url(self.course.id) | ||
response = self.client.get(url, data) | ||
return response | ||
|
||
def get_url(self, course_key): | ||
url = reverse( | ||
'cms.djangoapps.contentstore:v0:my_get_view', | ||
kwargs={'course_id': self.course.id} | ||
) | ||
return url | ||
|
||
As you can see, you need to inherit from `AuthorizeStaffTestCase` and `ModuleStoreTestCase`, and then either | ||
`TestCase` or `APITestCase` depending on the type of view you are testing. For cookie-based | ||
authentication, `TestCase` is sufficient, for Oauth2 use `ApiTestCase`. | ||
|
||
The only two methods you need to implement are `make_request` and `get_url`. The `make_request` method | ||
should make the request to the view and return the response. The `get_url` method should return the URL | ||
for the view you are testing. | ||
|
||
Overwriting Tests | ||
----------------- | ||
If you need different behavior you can overwrite the tests from the parent class. | ||
For example, if students should have access to the view, simply implement the | ||
`test_student` method in your test class. | ||
|
||
Adding other tests | ||
------------------ | ||
If you want to test other things in the view - let's say validation - | ||
it's easy to just add another `test_...` function to your test class | ||
and you can use the `make_request` method to make the request. |
76 changes: 64 additions & 12 deletions
76
cms/djangoapps/contentstore/rest_api/v0/tests/test_course_optimizer.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,74 @@ | ||
from cms.djangoapps.contentstore.tests.test_utils import AuthorizeStaffTestCase | ||
from rest_framework import status | ||
from django.test import TestCase | ||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase | ||
from django.urls import reverse | ||
|
||
class TestCourseOptimizer(AuthorizeStaffTestCase, ModuleStoreTestCase, TestCase): | ||
from cms.djangoapps.contentstore.tests.test_utils import AuthorizeStaffTestCase | ||
|
||
class TestGetLinkCheckStatus(AuthorizeStaffTestCase, ModuleStoreTestCase, TestCase): | ||
''' | ||
Tests for CourseOptimizer | ||
Authentication and Authorization Tests for CourseOptimizer. | ||
For concrete tests that are run, check `AuthorizeStaffTestCase`. | ||
''' | ||
def test_inherited(self): | ||
# This method ensures that pytest recognizes this class as containing tests | ||
pass | ||
|
||
def make_request(self, course_id=None, data=None): | ||
return self.client.get(self.get_url(course_id), data) | ||
def make_request(self, course_id=None, data=None, **kwargs): | ||
url = self.get_url(self.course.id) | ||
response = self.client.get(url, data) | ||
return response | ||
|
||
def get_url(self, course_key): | ||
return reverse( | ||
url = reverse( | ||
'cms.djangoapps.contentstore:v0:link_check_status', | ||
kwargs={'course_id': 'course-v1:someOrg+someCourse+someRun'} | ||
kwargs={'course_id': self.course.id} | ||
) | ||
return url | ||
|
||
def test_produces_4xx_when_invalid_course_id(self): | ||
''' | ||
Test course_id validation | ||
''' | ||
response = self.make_request(course_id='invalid_course_id') | ||
self.assertIn(response.status_code, range(400, 500)) | ||
|
||
def test_produces_4xx_when_additional_kwargs(self): | ||
''' | ||
Test additional kwargs validation | ||
''' | ||
response = self.make_request(course_id=self.course.id, malicious_kwarg='malicious_kwarg') | ||
self.assertIn(response.status_code, range(400, 500)) | ||
|
||
class TestPostLinkCheck(AuthorizeStaffTestCase, ModuleStoreTestCase, TestCase): | ||
''' | ||
Authentication and Authorization Tests for CourseOptimizer. | ||
For concrete tests that are run, check `AuthorizeStaffTestCase`. | ||
''' | ||
def make_request(self, course_id=None, data=None, **kwargs): | ||
url = self.get_url(self.course.id) | ||
response = self.client.post(url, data) | ||
return response | ||
|
||
def get_url(self, course_key): | ||
url = reverse( | ||
'cms.djangoapps.contentstore:v0:link_check', | ||
kwargs={'course_id': self.course.id} | ||
) | ||
return url | ||
|
||
def test_produces_4xx_when_invalid_course_id(self): | ||
''' | ||
Test course_id validation | ||
''' | ||
response = self.make_request(course_id='invalid_course_id') | ||
self.assertIn(response.status_code, range(400, 500)) | ||
|
||
def test_produces_4xx_when_additional_kwargs(self): | ||
''' | ||
Test additional kwargs validation | ||
''' | ||
response = self.make_request(course_id=self.course.id, malicious_kwarg='malicious_kwarg') | ||
self.assertIn(response.status_code, range(400, 500)) | ||
|
||
def test_produces_4xx_when_unexpected_data(self): | ||
''' | ||
Test validation when request contains unexpected data | ||
''' | ||
response = self.make_request(course_id=self.course.id, data={'unexpected_data': 'unexpected_data'}) | ||
self.assertIn(response.status_code, range(400, 500)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -125,6 +125,7 @@ def _decorator(func_or_class): | |
SessionAuthenticationAllowInactiveUser | ||
) | ||
func_or_class.permission_classes = () | ||
print('is_authenticated', is_authenticated) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove print statement |
||
if is_authenticated: | ||
func_or_class.permission_classes += (IsAuthenticated,) | ||
if is_user: | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is self.course set up through ModuleStoreTestCase here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either that or AuthorizeStaffTestCase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's in
AuthorizeStaffTestCase
.As you can see here, I defined a whole bunch of objects in there:
All of which can and should be used by child classes.
Now the tests that are running with this that are automatically included in child classes are:
So they cover some interesting cases like: if there's a course instructor, but he has access to a different course and not the current one, we make sure that he gets an Unauthorized 403.
But you could easily overwrite that expectation in your child class by just doing:
Because now you're calling the parent method with 200 as expect status instead, which changes the assertion it runs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I noticed additional tests were running when I ran pytest that are coming from
AuthorizeStaffTestCase
. I'll be utilizing this in my tests