diff --git a/README.md b/README.md index 30c8a0ae..b027321f 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Following the lead of other Jupyter services, it is a `tornado` application. ## Compatibility -This version is compatible with `nbgrader` 0.5 +This version is compatible with `nbgrader` >= 0.6.2 # Documentation @@ -72,19 +72,19 @@ Nbexchange can be installed as a Helm chart ## Configuration -| Parameter | Description | Default | -| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | -| `replicaCount` | Replica count | 1 | -| `image.repository` | Image repository | `quay.io/noteable/nbexchange` | -| `image.tag` | Image tag | `latest` | -| `image.pullPolicy` | Image pull policy | `IfNotPresent` | -| `environment` | Environment variables for the application | `{}` | -| `service.type` | Type of Service | `ClusterIP` | -| `service.port` | Port to expose service under | `9000` | -| `resources.requests.cpu` | CPU resource requests | `200m` | -| `resources.requests.memory` | Memory resource requests | `256Mi` | -| `tolerations` | Pod taint tolerations for deployment | `[]` | -| `nodeSelector` | Pod node selector for deployment | `{}` | +| Parameter | Description | Default | +| ---------- | -------------- | ------- | +| `replicaCount` | Replica count | 1 | +| `image.repository` | Image repository | `quay.io/noteable/nbexchange` | +| `image.tag` | Image tag | `latest` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `environment` | Environment variables for the application | `{}` | +| `service.type` | Type of Service | `ClusterIP` | +| `service.port` | Port to expose service under | `9000` | +| `resources.requests.cpu` | CPU resource requests | `200m` | +| `resources.requests.memory` | Memory resource requests | `256Mi` | +| `tolerations` | Pod taint tolerations for deployment| `[]` | +| `nodeSelector` | Pod node selector for deployment | `{}` | ## How to diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..738fce48 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,18 @@ +Documentation relating to the NBExchange service +================================================ + +NbExchange API +-------------- + +The documentation for the various API calls are given in ` `_ + +It gives the URL calls, the HTTP Methods, and the expected responses + + +Exchange API +------------ + +Th `Exchange API calls `_ documentation describes how +nbgrader expects to interact with an exchange service. + +This is the original documentation for `nbgrader and its exchange service `_ page \ No newline at end of file diff --git a/nbexchange/plugin/fetch_feedback.py b/nbexchange/plugin/fetch_feedback.py index bb80d8bd..a3ad4a07 100644 --- a/nbexchange/plugin/fetch_feedback.py +++ b/nbexchange/plugin/fetch_feedback.py @@ -20,7 +20,7 @@ class ExchangeFetchFeedback(abc.ExchangeFetchFeedback, Exchange): # where the downloaded files are placed def init_src(self): - self.src_path = "" + pass # where in the user tree def init_dest(self): @@ -74,17 +74,8 @@ def download(self): else: self.fail(content.get("note", "could not get feedback")) - def do_copy(self, src, dest): - """Copy the src dir to the dest dir omitting the self.coursedir.ignore globs.""" - self.download() - # shutil.copy(src, dest) - # # clear tmp having downloaded file - # os.remove(self.src_path) - def copy_files(self): - self.log.debug(f"Source: {self.src_path}") self.log.debug(f"Destination: {self.dest_path}") - # self.do_copy(self.src_path, self.dest_path) self.download() self.log.debug(f"Fetched as: {self.coursedir.notebook_id}") diff --git a/nbexchange/plugin/release_feedback.py b/nbexchange/plugin/release_feedback.py index 9ba1aaec..1f69c05d 100644 --- a/nbexchange/plugin/release_feedback.py +++ b/nbexchange/plugin/release_feedback.py @@ -18,8 +18,6 @@ class ExchangeReleaseFeedback(abc.ExchangeReleaseFeedback, Exchange): src_path = None - dest_path = None - notebooks = None # where the downloaded files are placed def init_src(self): diff --git a/nbexchange/tests/test_handlers_base.py b/nbexchange/tests/test_handlers_base.py index 3f00708a..45feb5d8 100644 --- a/nbexchange/tests/test_handlers_base.py +++ b/nbexchange/tests/test_handlers_base.py @@ -22,3 +22,7 @@ def test_main_page(self, app): r = yield async_requests.get(app.url + "/") assert r.status_code == 200 assert re.search(r"NbExchange", r.text) + + def test_base_location_story(self, app): + # Not "/services/nbexchange/", the tests move it + assert app.base_storage_location in ["/tmp/exchange/", "/tmp/courses"] diff --git a/nbexchange/tests/test_handlers_collection.py b/nbexchange/tests/test_handlers_collection.py index 4d3ffe5f..8f70ae4f 100644 --- a/nbexchange/tests/test_handlers_collection.py +++ b/nbexchange/tests/test_handlers_collection.py @@ -8,6 +8,7 @@ from nbexchange.handlers.base import BaseHandler from nbexchange.tests.utils import ( async_requests, + clear_database, get_files_dict, user_brobbere_student, user_kiz_instructor, @@ -31,7 +32,7 @@ def test_post_collection_is_501(app): # subscribed user makes no difference (501, because we've hard-coded it) @pytest.mark.gen_test -def test_post_collection_is_501_even_authenticaated(app): +def test_post_collection_is_501_even_authenticaated(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -43,7 +44,7 @@ def test_post_collection_is_501_even_authenticaated(app): # require authenticated user @pytest.mark.gen_test -def test_get_collection_requires_authentication(app): +def test_get_collection_requires_authentication(app, clear_database): r = yield async_requests.get(app.url + "/collection") assert r.status_code == 403 @@ -69,7 +70,7 @@ def test_get_collection_requires_parameters(app): # (needs to be fetched before it can be submitted ) # (needs to be released before it can be fetched ) @pytest.mark.gen_test -def test_get_collection_catches_missing_path(app): +def test_get_collection_catches_missing_path(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -113,7 +114,7 @@ def test_get_collection_catches_missing_path(app): # (needs to be fetched before it can be submitted ) # (needs to be released before it can be fetched ) @pytest.mark.gen_test -def test_get_collection_catches_missing_assignment(app): +def test_get_collection_catches_missing_assignment(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -157,7 +158,7 @@ def test_get_collection_catches_missing_assignment(app): # (needs to be fetched before it can be submitted ) # (needs to be released before it can be fetched ) @pytest.mark.gen_test -def test_get_collection_catches_missing_course(app): +def test_get_collection_catches_missing_course(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -201,7 +202,7 @@ def test_get_collection_catches_missing_course(app): # (needs to be fetched before it can be submitted ) # (needs to be released before it can be fetched ) @pytest.mark.gen_test -def test_get_collection_checks_for_user_subscription(app): +def test_get_collection_checks_for_user_subscription(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -240,7 +241,7 @@ def test_get_collection_checks_for_user_subscription(app): # Has all three params, student can't collect (note this is hard-coded params, as students can list items available for collection) # (needs to be released to register the assignment ) @pytest.mark.gen_test -def test_get_collection_check_catches_student_role(app): +def test_get_collection_check_catches_student_role(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -266,7 +267,7 @@ def test_get_collection_check_catches_student_role(app): # (needs to be fetched before it can be submitted ) # (needs to be released before it can be fetched ) @pytest.mark.gen_test -def test_get_collection_confirm_instructor_does_download(app): +def test_get_collection_confirm_instructor_does_download(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -303,7 +304,7 @@ def test_get_collection_confirm_instructor_does_download(app): # Confirm that multiple submissions are listed @pytest.mark.gen_test -async def test_collection_actions_show_correctly(app): +async def test_collection_actions_show_correctly(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -409,7 +410,7 @@ async def test_collection_actions_show_correctly(app): # (needs to be fetched before it can be submitted ) # (needs to be released before it can be fetched ) @pytest.mark.gen_test -def test_get_collection_path_is_incorrect(app): +def test_get_collection_path_is_incorrect(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -449,7 +450,7 @@ def test_get_collection_path_is_incorrect(app): # (needs to be fetched before it can be submitted ) # (needs to be released before it can be fetched ) @pytest.mark.gen_test -def test_get_collection_with_a_blank_feedback_path_injected(app): +def test_get_collection_with_a_blank_feedback_path_injected(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): diff --git a/nbexchange/tests/test_handlers_delete.py b/nbexchange/tests/test_handlers_delete.py index 7b6f9a09..f5e612bd 100644 --- a/nbexchange/tests/test_handlers_delete.py +++ b/nbexchange/tests/test_handlers_delete.py @@ -7,6 +7,7 @@ from nbexchange.handlers.base import BaseHandler from nbexchange.tests.utils import ( async_requests, + clear_database, get_files_dict, user_kiz_instructor, user_kiz_student, @@ -16,6 +17,18 @@ logger = logging.getLogger(__file__) logger.setLevel(logging.ERROR) +################################# +# +# Very Important Note +# +# The `clear_database` fixture removed all database records. +# In this suite of tests, we do that FOR EVERY TEST +# This means that every single test is run in isolation, and therefore will need to have the full Release, Fetch, +# Submit steps done before the collection can be tested. +# (On the plus side, adding or changing a test will no longer affect those below) +# +################################# + ##### DELETE /assignment (delete or purge assignment) ###### # require authenticated user (404 because the bounce to login fails) @@ -31,7 +44,7 @@ def test_delete_needs_user(app): # Requires both params (none) @pytest.mark.gen_test -def test_delete_needs_both_params(app): +def test_delete_needs_both_params(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -46,7 +59,7 @@ def test_delete_needs_both_params(app): # Requires both params (just course) @pytest.mark.gen_test -def test_delete_needs_assignment(app): +def test_delete_needs_assignment(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -62,7 +75,7 @@ def test_delete_needs_assignment(app): # Requires both params (just assignment) @pytest.mark.gen_test -def test_delete_needs_course(app): +def test_delete_needs_course(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -79,7 +92,7 @@ def test_delete_needs_course(app): # Student cannot release # Note we have to use a user who's NEVER been an instructor on the course @pytest.mark.gen_test -def test_delete_student_blocked(app): +def test_delete_student_blocked(app, clear_database): with patch.object(BaseHandler, "get_current_user", return_value=user_zik_student): r = yield async_requests.get(app.url + "/assignments?course_id=course_2") r = yield async_requests.delete( @@ -93,7 +106,7 @@ def test_delete_student_blocked(app): # Instructor, wrong course, cannot release @pytest.mark.gen_test -def test_delete_wrong_course_blocked(app): +def test_delete_wrong_course_blocked(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -108,7 +121,7 @@ def test_delete_wrong_course_blocked(app): # instructor can delete @pytest.mark.gen_test -def test_delete_instructor_delete(app): +def test_delete_instructor_delete(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -131,7 +144,7 @@ def test_delete_instructor_delete(app): # instructor can purge @pytest.mark.gen_test -def test_delete_instructor_purge(app): +def test_delete_instructor_purge(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -155,7 +168,7 @@ def test_delete_instructor_purge(app): # Instructor, wrong course, cannot delete @pytest.mark.gen_test -def test_delete_wrong_course_blocked(app): +def test_delete_wrong_course_blocked(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -174,7 +187,7 @@ def test_delete_wrong_course_blocked(app): # instructor releasing - Picks up the first attribute if more than 1 (wrong course) @pytest.mark.gen_test -def test_delete_multiple_courses_listed_first_wrong_blocked(app): +def test_delete_multiple_courses_listed_first_wrong_blocked(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -195,7 +208,7 @@ def test_delete_multiple_courses_listed_first_wrong_blocked(app): # instructor releasing - Picks up the first attribute if more than 1 (wrong course) @pytest.mark.gen_test -def test_delete_multiple_courses_listed_first_right_passes(app): +def test_delete_multiple_courses_listed_first_right_passes(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -218,10 +231,8 @@ def test_delete_multiple_courses_listed_first_right_passes(app): # confirm unreleased does not show in list -# Skipping because it fails in the group test, but fine when just the 1 file is run -@pytest.mark.skip @pytest.mark.gen_test -def test_delete_assignment10(app): +def test_delete_assignment10(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): diff --git a/nbexchange/tests/test_handlers_feedback.py b/nbexchange/tests/test_handlers_feedback.py index 6ffa104c..f7ae2cc4 100644 --- a/nbexchange/tests/test_handlers_feedback.py +++ b/nbexchange/tests/test_handlers_feedback.py @@ -10,6 +10,7 @@ from nbexchange.tests.utils import ( AsyncSession, async_requests, + clear_database, get_feedback_dict, get_files_dict, user_brobbere_student, @@ -35,7 +36,7 @@ def test_feedback_unauthenticated(app): @pytest.mark.gen_test -def test_feedback_authenticated_no_params(app): +def test_feedback_authenticated_no_params(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -49,7 +50,7 @@ def test_feedback_authenticated_no_params(app): @pytest.mark.gen_test -def test_feedback_authenticated_with_params(app): +def test_feedback_authenticated_with_params(app, clear_database): assignment_id = "my_assignment" course_id = "my_course" @@ -64,7 +65,7 @@ def test_feedback_authenticated_with_params(app): @pytest.mark.gen_test -def test_feedback_post_unauthenticated(app): +def test_feedback_post_unauthenticated(app, clear_database): """ Require authenticated user for posting """ @@ -73,7 +74,7 @@ def test_feedback_post_unauthenticated(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_no_params(app): +def test_feedback_post_authenticated_no_params(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -87,7 +88,7 @@ def test_feedback_post_authenticated_no_params(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_no_assignment_id(app): +def test_feedback_post_authenticated_no_assignment_id(app, clear_database): url = f"/feedback?course_id=faked¬ebook=faked&student=faked×tamp=faked&checksum=faked" with patch.object( @@ -103,7 +104,7 @@ def test_feedback_post_authenticated_no_assignment_id(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_no_course_id(app): +def test_feedback_post_authenticated_no_course_id(app, clear_database): assignment_id = "my_assignment" url = f"/feedback?assignment_id={assignment_id}¬ebook=faked&student=faked×tamp=faked&checksum=faked" @@ -120,7 +121,7 @@ def test_feedback_post_authenticated_no_course_id(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_no_notebook(app): +def test_feedback_post_authenticated_no_notebook(app, clear_database): assignment_id = "my_assignment" url = f"/feedback?assignment_id={assignment_id}&course_id=faked&student=faked×tamp=faked&checksum=faked" @@ -137,7 +138,7 @@ def test_feedback_post_authenticated_no_notebook(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_no_student(app): +def test_feedback_post_authenticated_no_student(app, clear_database): assignment_id = "my_assignment" url = f"/feedback?assignment_id={assignment_id}&course_id=faked¬ebook=faked×tamp=faked&checksum=faked" @@ -154,7 +155,7 @@ def test_feedback_post_authenticated_no_student(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_no_timestamp(app): +def test_feedback_post_authenticated_no_timestamp(app, clear_database): assignment_id = "my_assignment" url = f"/feedback?assignment_id={assignment_id}&course_id=faked¬ebook=faked&student=faked&checksum=faked" @@ -171,7 +172,7 @@ def test_feedback_post_authenticated_no_timestamp(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_no_checksum(app): +def test_feedback_post_authenticated_no_checksum(app, clear_database): assignment_id = "my_assignment" url = f"/feedback?assignment_id={assignment_id}&course_id=faked¬ebook=faked&student=faked×tamp=faked" @@ -188,7 +189,7 @@ def test_feedback_post_authenticated_no_checksum(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_with_params(app): +def test_feedback_post_authenticated_with_params(app, clear_database): assignment_id = "my_assignment" url = ( @@ -209,7 +210,7 @@ def test_feedback_post_authenticated_with_params(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_with_incorrect_assignment_id(app): +def test_feedback_post_authenticated_with_incorrect_assignment_id(app, clear_database): assignment_id = "assign_a" course_id = "course_2" notebook = "notebook" @@ -258,7 +259,7 @@ def test_feedback_post_authenticated_with_incorrect_assignment_id(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_with_incorrect_notebook_id(app): +def test_feedback_post_authenticated_with_incorrect_notebook_id(app, clear_database): assignment_id = "assign_a" course_id = "course_2" notebook = "notebook" @@ -308,7 +309,7 @@ def test_feedback_post_authenticated_with_incorrect_notebook_id(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_with_incorrect_student_id(app): +def test_feedback_post_authenticated_with_incorrect_student_id(app, clear_database): assignment_id = "assign_a" course_id = "course_2" notebook = "notebook" @@ -359,7 +360,8 @@ def test_feedback_post_authenticated_with_incorrect_student_id(app): # Not yet implemented on exchange server... @pytest.mark.skip -def test_feedback_post_authenticated_with_incorrect_checksum(app): +@pytest.mark.gen_test +def test_feedback_post_authenticated_with_incorrect_checksum(app, clear_database): assignment_id = "assign_a" course_id = "course_2" notebook = "notebook" @@ -409,7 +411,7 @@ def test_feedback_post_authenticated_with_incorrect_checksum(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_with_correct_params(app): +def test_feedback_post_authenticated_with_correct_params(app, clear_database): assignment_id = "assign_a" course_id = "course_2" notebook = "notebook" @@ -461,7 +463,9 @@ def test_feedback_post_authenticated_with_correct_params(app): @pytest.mark.gen_test -def test_feedback_post_authenticated_with_correct_params_incorrect_instructor(app): +def test_feedback_post_authenticated_with_correct_params_incorrect_instructor( + app, clear_database +): assignment_id = "assign_a" course_id = "course_2" notebook = "notebook" @@ -514,7 +518,9 @@ def test_feedback_post_authenticated_with_correct_params_incorrect_instructor(ap @pytest.mark.gen_test -def test_feedback_post_authenticated_with_correct_params_student_submitter(app): +def test_feedback_post_authenticated_with_correct_params_student_submitter( + app, clear_database +): assignment_id = "assign_a" course_id = "course_2" notebook = "notebook" @@ -565,7 +571,7 @@ def test_feedback_post_authenticated_with_correct_params_student_submitter(app): @pytest.mark.gen_test -def test_feedback_get_authenticated_with_incorrect_student(app): +def test_feedback_get_authenticated_with_incorrect_student(app, clear_database): assignment_id = "assign_a" course_id = "course_2" notebook = "notebook" @@ -627,7 +633,7 @@ def test_feedback_get_authenticated_with_incorrect_student(app): @pytest.mark.gen_test -def test_feedback_get_authenticated_with_correct_params(app): +def test_feedback_get_authenticated_with_correct_params(app, clear_database): assignment_id = "assign_a" course_id = "course_2" notebook = "notebook" @@ -690,7 +696,7 @@ def test_feedback_get_authenticated_with_correct_params(app): # test feedback picks up the correct assignment when two courses have the same assignment name # Should pick up course 2. @pytest.mark.gen_test -def test_feedback_get_correct_assignment_across_courses(app): +def test_feedback_get_correct_assignment_across_courses(app, clear_database): pass # set up the situation assignment_id = "assign_a" diff --git a/nbexchange/tests/test_handlers_fetch.py b/nbexchange/tests/test_handlers_fetch.py index 2a365a09..e5227f83 100644 --- a/nbexchange/tests/test_handlers_fetch.py +++ b/nbexchange/tests/test_handlers_fetch.py @@ -7,6 +7,7 @@ from nbexchange.handlers.base import BaseHandler from nbexchange.tests.utils import ( async_requests, + clear_database, get_files_dict, user_brobbere_instructor, user_brobbere_student, @@ -20,18 +21,30 @@ # set up the file to be uploaded as part of the testing later files = get_files_dict(sys.argv[0]) # ourself :) +################################# +# +# Very Important Note +# +# The `clear_database` fixture removed all database records. +# In this suite of tests, we do that FOR EVERY TEST +# This means that every single test is run in isolation, and therefore will need to have the full Release, Fetch, +# Submit steps done before the collection can be tested. +# (On the plus side, adding or changing a test will no longer affect those below) +# +################################# + ##### GET /assignment (download/fetch assignment) ###### # require authenticated user (404 because the bounce to login fails) @pytest.mark.gen_test -def test_assignment0(app): +def test_fetch_requires_auth_user(app): r = yield async_requests.get(app.url + "/assignment") assert r.status_code == 403 # Requires both params (none) @pytest.mark.gen_test -def test_assignment1(app): +def test_fetch_fails_no_parama(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -46,7 +59,7 @@ def test_assignment1(app): # Requires both params (just course) @pytest.mark.gen_test -def test_assignment2(app): +def test_fetch_fails_missing_assignment(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -62,7 +75,7 @@ def test_assignment2(app): # Requires both params (just assignment) @pytest.mark.gen_test -def test_assignment3(app): +def test_fetch_fails_missing_course(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -78,7 +91,7 @@ def test_assignment3(app): # both params, incorrect course @pytest.mark.gen_test -def test_assignment4(app): +def test_fetch_fails_user_not_subscribed(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -93,7 +106,7 @@ def test_assignment4(app): # both params, correct course, assignment does not exist @pytest.mark.gen_test -def test_assignment5(app): +def test_fetch_fails_assignment_not_exists(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -107,71 +120,9 @@ def test_assignment5(app): assert response_data["note"] == "Assignment assign_does_not_exist does not exist" -# both params, correct course, assignment does not exist - differnet user, same role -@pytest.mark.gen_test -def test_assignment6(app): - with patch.object( - BaseHandler, "get_current_user", return_value=user_brobbere_instructor - ): - r = yield async_requests.get( - app.url - + "/assignment?course_id=course_2&assignment_id=assign_does_not_exist" - ) - assert r.status_code == 200 - response_data = r.json() - assert response_data["success"] == False - assert response_data["note"] == "Assignment assign_does_not_exist does not exist" - - -# both params, correct course, assignment does not exist - same user, different role -@pytest.mark.gen_test -def test_assignment7(app): - with patch.object(BaseHandler, "get_current_user", return_value=user_kiz_student): - r = yield async_requests.get( - app.url - + "/assignment?course_id=course_2&assignment_id=assign_does_not_exist" - ) - assert r.status_code == 200 - response_data = r.json() - assert response_data["success"] == False - assert response_data["note"] == "Assignment assign_does_not_exist does not exist" - - -# both params, correct course, assignment does not exist - different user, different role -@pytest.mark.gen_test -def test_assignment8(app): - with patch.object( - BaseHandler, "get_current_user", return_value=user_brobbere_student - ): - r = yield async_requests.get( - app.url - + "/assignment?course_id=course_2&assignment_id=assign_does_not_exist" - ) - assert r.status_code == 200 - response_data = r.json() - assert response_data["success"] == False - assert response_data["note"] == "Assignment assign_does_not_exist does not exist" - - -# additional param makes no difference -@pytest.mark.gen_test -def test_assignment9(app): - with patch.object( - BaseHandler, "get_current_user", return_value=user_brobbere_student - ): - r = yield async_requests.get( - app.url - + "/assignment?course_id=course_2&assignment_id=assign_does_not_exist&foo=bar" - ) - assert r.status_code == 200 - response_data = r.json() - assert response_data["success"] == False - assert response_data["note"] == "Assignment assign_does_not_exist does not exist" - - # Picks up the first attribute if more than 1 (wrong course) @pytest.mark.gen_test -def test_assignment10(app): +def test_fetch_duplicate_param_first_is_wrong(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_brobbere_student ): @@ -187,7 +138,7 @@ def test_assignment10(app): # Picks up the first attribute if more than 1 (right course) @pytest.mark.gen_test -def test_assignment11(app): +def test_fetch_duplicate_param_first_is_right(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_brobbere_student ): @@ -204,7 +155,7 @@ def test_assignment11(app): # fetch assignment, correct details, same user as releaser # (needs to be released before it can be fetched ) @pytest.mark.gen_test -def test_assignment13(app): +def test_instructor_can_fetch(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -226,7 +177,7 @@ def test_assignment13(app): # fetch assignment, correct details, different user, different role # (needs to be released before it can be fetched ) @pytest.mark.gen_test -def test_assignment14(app): +def test_student_can_fetch(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -245,27 +196,9 @@ def test_assignment14(app): assert int(r.headers["Content-Length"]) > 0 -# fetch assignment, correct details, different user, different role - Picks up the first attribute if more than 1 (wrong course) -@pytest.mark.gen_test -def test_assignment15(app): - with patch.object( - BaseHandler, "get_current_user", return_value=user_brobbere_student - ): - r = yield async_requests.get( - app.url - + "/assignment?course_id=course_1&course_id=course_2&assignment_id=assign_a" - ) - assert r.status_code == 200 - response_data = r.json() - assert response_data["success"] == False - assert response_data["note"] == "User not subscribed to course course_1" - - # Confirm that a fetch always matches the last release -### This is skipped because it's database is cumulitive with earlier tests - which we don't waht! -@pytest.mark.skip @pytest.mark.gen_test -def test_post_assignment16(app): +def test_fetch_after_rerelease_gets_different_file(app, clear_database): with patch.object( BaseHandler, "get_current_user", return_value=user_kiz_instructor ): @@ -308,7 +241,7 @@ def test_post_assignment16(app): assert "note" not in response_data # just that it's missing paths = list(map(lambda assignment: assignment["path"], response_data["value"])) actions = list(map(lambda assignment: assignment["status"], response_data["value"])) - assert len(paths) == 7 # 6 + assert len(paths) == 6 assert actions == [ "released", "released", diff --git a/nbexchange/tests/test_plugin_collect.py b/nbexchange/tests/test_plugin_collect.py index 3138839b..49629036 100644 --- a/nbexchange/tests/test_plugin_collect.py +++ b/nbexchange/tests/test_plugin_collect.py @@ -34,6 +34,74 @@ ass_1_5 = "assign_1_5" +@pytest.mark.gen_test +def test_collect_methods(plugin_config, tmpdir): + plugin_config.CourseDirectory.course_id = "no_course" + plugin_config.CourseDirectory.assignment_id = ass_1_3 + plugin_config.CourseDirectory.submitted_directory = str( + tmpdir.mkdir("submitted").realpath() + ) + plugin = ExchangeCollect( + coursedir=CourseDirectory(config=plugin_config), config=plugin_config + ) + + plugin.init_src() + with pytest.raises(AttributeError) as e_info: + foo = plugin.src_path + assert ( + str(e_info.value) == "'ExchangeCollect' object has no attribute 'src_path'" + ) + plugin.init_dest() + with pytest.raises(AttributeError) as e_info: + foo = plugin.dest_path + assert ( + str(e_info.value) == "'ExchangeCollect' object has no attribute 'dest_path'" + ) + + def api_request_good(*args, **kwargs): + tar_file = io.BytesIO() + + assert "method" not in kwargs or kwargs.get("method").lower() == "get" + with tarfile.open(fileobj=tar_file, mode="w:gz") as tar_handle: + tar_handle.add( + notebook1_filename, arcname=os.path.basename(notebook1_filename) + ) + # tar_handle.add(notebook2_filename, arcname=os.path.basename(notebook2_filename)) + tar_file.seek(0) + + return type( + "Response", + (object,), + { + "status_code": 200, + "headers": {"content-type": "application/x-tar"}, + "content": tar_file.read(), + }, + ) + + def api_request_bad(*args, **kwargs): + return type( + "Response", + (object,), + { + "status_code": 200, + "headers": {"content-type": "application/x-tar"}, + "content": b"", + }, + ) + + with patch.object(Exchange, "api_request", side_effect=api_request_bad): + submission = { + "student_id": student_id, + "path": f"/submitted/no_course/{ass_1_3}/1/", + "timestamp": "2020-01-01 00:00:00.0 UTC", + } + dest_path = f"{plugin_config.CourseDirectory.submitted_directory}/123/{ass_1_3}" + with pytest.raises(Exception) as e_info: + plugin.download(submission, dest_path) + assert str(e_info.value) == "file could not be opened successfully" + + @pytest.mark.gen_test def test_collect_normal(plugin_config, tmpdir): plugin_config.CourseDirectory.course_id = "no_course" diff --git a/nbexchange/tests/test_plugin_fetch_assignment.py b/nbexchange/tests/test_plugin_fetch_assignment.py index 648b2e26..83785102 100644 --- a/nbexchange/tests/test_plugin_fetch_assignment.py +++ b/nbexchange/tests/test_plugin_fetch_assignment.py @@ -1,6 +1,7 @@ import io import logging import os +import re import shutil import tarfile from shutil import copyfile @@ -27,6 +28,94 @@ notebook2_file = get_feedback_file(notebook2_filename) +@pytest.mark.gen_test +def test_fetch_assignment_methods_init_dest(plugin_config, tmpdir): + plugin_config.CourseDirectory.course_id = "no_course" + plugin_config.CourseDirectory.assignment_id = "assign_1_2" + + plugin = ExchangeFetchAssignment( + coursedir=CourseDirectory(config=plugin_config), config=plugin_config + ) + + # we're good if the dir doesn't exist + plugin.init_dest() + assert re.search(r"assign_1_2$", plugin.dest_path) + assert os.path.isdir(plugin.dest_path) + + # we're good if the dir exists and is empty + plugin.init_dest() + assert re.search(r"assign_1_2$", plugin.dest_path) + + # we're good if the dir exists, and has something OTHER than an ipynb file in it + with open(f"{plugin.dest_path}/random.txt", "w") as fp: + fp.write("Hello world") + plugin.init_dest() + assert re.search(r"assign_1_2$", plugin.dest_path) + + # FAILS if the dir exists AND there's an ipynb file in it + with open(f"{plugin.dest_path}/random.ipynb", "w") as fp: + fp.write("Hello world") + with pytest.raises(ExchangeError) as e_info: + plugin.init_dest() + assert ( + f"You already have notebook documents in directory: {plugin_config.CourseDirectory.assignment_id}. Please remove them before fetching again" + in str(e_info.value) + ) + shutil.rmtree(plugin.dest_path) + + +@pytest.mark.gen_test +def test_fetch_assignment_methods_rest(plugin_config, tmpdir): + plugin_config.CourseDirectory.course_id = "no_course" + plugin_config.CourseDirectory.assignment_id = "assign_1_2" + + plugin = ExchangeFetchAssignment( + coursedir=CourseDirectory(config=plugin_config), config=plugin_config + ) + + plugin.init_src() + assert re.search(r"no_course/assign_1_2/assignment.tar.gz$", plugin.src_path) + plugin.init_dest() + + try: + + def api_request(*args, **kwargs): + tar_file = io.BytesIO() + + with tarfile.open(fileobj=tar_file, mode="w:gz") as tar_handle: + tar_handle.add( + notebook1_filename, arcname=os.path.basename(notebook1_filename) + ) + tar_file.seek(0) + + assert args[0] == ( + f"assignment?course_id=no_course&assignment_id=assign_1_2" + ) + assert "method" not in kwargs or kwargs.get("method").lower() == "get" + return type( + "Response", + (object,), + { + "status_code": 200, + "headers": {"content-type": "application/x-tar"}, + "content": tar_file.read(), + }, + ) + + with patch.object(Exchange, "api_request", side_effect=api_request): + plugin.download() + assert os.path.exists(os.path.join(plugin.src_path, "assignment-0.6.ipynb")) + shutil.rmtree(plugin.dest_path) + + # do_copy includes a download() + plugin.do_copy(plugin.src_path, plugin.dest_path) + assert os.path.exists( + os.path.join(plugin.dest_path, "assignment-0.6.ipynb") + ) + finally: + shutil.rmtree(plugin.dest_path) + + @pytest.mark.gen_test def test_fetch_assignment_fetch_normal(plugin_config, tmpdir): plugin_config.CourseDirectory.course_id = "no_course" @@ -62,7 +151,7 @@ def api_request(*args, **kwargs): ) with patch.object(Exchange, "api_request", side_effect=api_request): - called = plugin.start() + plugin.start() assert os.path.exists( os.path.join(plugin.dest_path, "assignment-0.6.ipynb") ) @@ -106,7 +195,7 @@ def api_request(*args, **kwargs): ) with patch.object(Exchange, "api_request", side_effect=api_request): - called = plugin.start() + plugin.start() assert os.path.exists( os.path.join(plugin.dest_path, "assignment-0.6.ipynb") ) @@ -151,7 +240,7 @@ def api_request(*args, **kwargs): ) with patch.object(Exchange, "api_request", side_effect=api_request): - called = plugin.start() + plugin.start() assert os.path.exists( os.path.join(plugin.dest_path, "assignment-0.6.ipynb") ) @@ -200,8 +289,7 @@ def api_request(*args, **kwargs): ) with patch.object(Exchange, "api_request", side_effect=api_request): - called = plugin.start() - print(f"dest_path {plugin.dest_path}") + plugin.start() assert os.path.exists( os.path.join(plugin.dest_path, "assignment-0.6.ipynb") ) @@ -253,7 +341,7 @@ def api_request(*args, **kwargs): with patch.object(Exchange, "api_request", side_effect=api_request): with pytest.raises(ExchangeError) as e_info: - called = plugin.start() + plugin.start() assert ( str(e_info.value) == "You already have notebook documents in directory: assign_1_3. Please remove them before fetching again" @@ -302,8 +390,7 @@ def api_request(*args, **kwargs): ) with patch.object(Exchange, "api_request", side_effect=api_request): - called = plugin.start() - print(f"dest_path {plugin.dest_path}") + plugin.start() assert os.path.exists( os.path.join(plugin.dest_path, "assignment-0.6.ipynb") ) diff --git a/nbexchange/tests/test_plugin_fetch_feedback.py b/nbexchange/tests/test_plugin_fetch_feedback.py index 245b3691..674fea27 100644 --- a/nbexchange/tests/test_plugin_fetch_feedback.py +++ b/nbexchange/tests/test_plugin_fetch_feedback.py @@ -1,5 +1,6 @@ import logging import os +import re import sys import pytest @@ -24,6 +25,35 @@ """ +@pytest.mark.gen_test +def test_fetch_feedback_methods(plugin_config, tmpdir): + plugin_config.Exchange.assignment_dir = str( + tmpdir.mkdir("feedback_test").realpath() + ) + plugin_config.CourseDirectory.course_id = "no_course" + plugin_config.CourseDirectory.assignment_id = assignment_id + + plugin = ExchangeFetchFeedback( + coursedir=CourseDirectory(config=plugin_config), config=plugin_config + ) + + plugin.init_src() + with pytest.raises(AttributeError) as e_info: + foo = plugin.src_path + assert ( + str(e_info.value) + == "'ExchangeFetchFeedback' object has no attribute 'src_path'" + ) + + plugin.init_dest() + print(f"plugin.dest_path:{plugin.dest_path}") + assert re.search( + r"test_fetch_feedback_methods0/feedback_test/assign_1/feedback$", + plugin.dest_path, + ) + assert os.path.isdir(plugin.dest_path) + + @pytest.mark.gen_test def test_fetch_feedback_dir_created(plugin_config, tmpdir): plugin_config.Exchange.assignment_dir = str( diff --git a/nbexchange/tests/test_plugin_release_assignment.py b/nbexchange/tests/test_plugin_release_assignment.py index 135fc098..2cd1a516 100644 --- a/nbexchange/tests/test_plugin_release_assignment.py +++ b/nbexchange/tests/test_plugin_release_assignment.py @@ -1,7 +1,8 @@ import datetime import logging import os -import sys +import re +import shutil from shutil import copyfile import pytest @@ -17,7 +18,8 @@ logger = logging.getLogger(__file__) logger.setLevel(logging.ERROR) - +release_dir = "release_test" +source_dir = "source_test" notebook1_filename = os.path.join( os.path.dirname(__file__), "data", "assignment-0.6.ipynb" ) @@ -28,12 +30,120 @@ notebook2_file = get_feedback_file(notebook2_filename) +def test_release_assignment_methods_init_src(plugin_config, tmpdir, caplog): + plugin_config.CourseDirectory.root = "/" + + plugin_config.CourseDirectory.source_directory = str( + tmpdir.mkdir(source_dir).realpath() + ) + plugin_config.CourseDirectory.release_directory = str( + tmpdir.mkdir(release_dir).realpath() + ) + plugin_config.CourseDirectory.assignment_id = "assign_1" + + plugin = ExchangeReleaseAssignment( + coursedir=CourseDirectory(config=plugin_config), config=plugin_config + ) + + # No release file, no source file + with pytest.raises(ExchangeError) as e_info: + plugin.init_src() + assert "Assignment not found at:" in str(e_info.value) + + # No release, source file exists + os.makedirs( + os.path.join(plugin_config.CourseDirectory.source_directory, "assign_1"), + exist_ok=True, + ) + copyfile( + notebook1_filename, + os.path.join( + plugin_config.CourseDirectory.source_directory, "assign_1", "release.ipynb" + ), + ) + with pytest.raises(ExchangeError) as e_info: + plugin.init_src() + assert re.match( + r"Assignment found in '.+' but not '.+', run `nbgrader assign` first.", + str(e_info.value), + ) + + # release file exists + os.makedirs( + os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1"), + exist_ok=True, + ) + copyfile( + notebook1_filename, + os.path.join( + plugin_config.CourseDirectory.release_directory, "assign_1", "release.ipynb" + ), + ) + with open( + os.path.join( + plugin_config.CourseDirectory.release_directory, "assign_1", "timestamp.txt" + ), + "w", + ) as fp: + fp.write("2020-01-01 00:00:00.0 UTC") + plugin.init_src() + assert re.search( + r"test_release_assignment_method0/release_test/./assign_1$", plugin.src_path + ) + assert os.path.isdir(plugin.src_path) + + +@pytest.mark.gen_test +def test_release_assignment_methods_the_rest(plugin_config, tmpdir, caplog): + plugin_config.CourseDirectory.root = "/" + + plugin_config.CourseDirectory.release_directory = str( + tmpdir.mkdir(release_dir).realpath() + ) + plugin_config.CourseDirectory.assignment_id = "assign_1" + + plugin = ExchangeReleaseAssignment( + coursedir=CourseDirectory(config=plugin_config), config=plugin_config + ) + os.makedirs( + os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1"), + exist_ok=True, + ) + copyfile( + notebook1_filename, + os.path.join( + plugin_config.CourseDirectory.release_directory, "assign_1", "release.ipynb" + ), + ) + with open( + os.path.join( + plugin_config.CourseDirectory.release_directory, "assign_1", "timestamp.txt" + ), + "w", + ) as fp: + fp.write("2020-01-01 00:00:00.0 UTC") + + plugin.init_src() + plugin.init_dest() + with pytest.raises(AttributeError) as e_info: + foo = plugin.dest_path + assert ( + str(e_info.value) + == "'ExchangeReleaseAssignment' object has no attribute 'dest_path'" + ) + + file = plugin.tar_source() + assert len(file) > 1000 + plugin.get_notebooks() + assert plugin.notebooks == ["release"] + + @pytest.mark.gen_test def test_release_assignment_normal(plugin_config, tmpdir): plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.release_directory = str( - tmpdir.mkdir("submitted_test").realpath() + tmpdir.mkdir(release_dir).realpath() ) plugin_config.CourseDirectory.assignment_id = "assign_1" os.makedirs( @@ -73,7 +183,11 @@ def api_request(*args, **kwargs): ) with patch.object(Exchange, "api_request", side_effect=api_request): - called = plugin.start() + plugin.start() + print(f"plugin.src_path: {plugin.src_path}") + assert re.search( + r"test_release_assignment_normal0/release_test/./assign_1$", plugin.src_path + ) @pytest.mark.gen_test @@ -81,7 +195,7 @@ def test_release_assignment_several_normal(plugin_config, tmpdir): plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.release_directory = str( - tmpdir.mkdir("submitted_test").realpath() + tmpdir.mkdir(release_dir).realpath() ) plugin_config.CourseDirectory.assignment_id = "assign_1" os.makedirs( @@ -141,7 +255,7 @@ def api_request(*args, **kwargs): ) with patch.object(Exchange, "api_request", side_effect=api_request): - called = plugin.start() + plugin.start() @pytest.mark.gen_test @@ -149,7 +263,7 @@ def test_release_assignment_fail(plugin_config, tmpdir): plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.release_directory = str( - tmpdir.mkdir("submitted_test").realpath() + tmpdir.mkdir(release_dir).realpath() ) plugin_config.CourseDirectory.assignment_id = "assign_1" os.makedirs( @@ -188,7 +302,7 @@ def api_request(*args, **kwargs): with patch.object(Exchange, "api_request", side_effect=api_request): with pytest.raises(ExchangeError) as e_info: - called = plugin.start() + plugin.start() assert str(e_info.value) == "failure note" @@ -197,7 +311,7 @@ def test_release_oversize_blocked(plugin_config, tmpdir): plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.release_directory = str( - tmpdir.mkdir("submitted_test").realpath() + tmpdir.mkdir(release_dir).realpath() ) plugin_config.CourseDirectory.assignment_id = "assign_1" os.makedirs( @@ -241,7 +355,7 @@ def api_request(*args, **kwargs): with patch.object(Exchange, "api_request", side_effect=api_request): with pytest.raises(ExchangeError) as e_info: - called = plugin.start() + plugin.start() assert ( str(e_info.value) == "Assignment assign_1 not released. The contents of your assignment are too large:\nYou may have data files, temporary files, and/or working files that should not be included - try deleting them." diff --git a/nbexchange/tests/test_plugin_release_feedback.py b/nbexchange/tests/test_plugin_release_feedback.py index 41b06308..1d90e75c 100644 --- a/nbexchange/tests/test_plugin_release_feedback.py +++ b/nbexchange/tests/test_plugin_release_feedback.py @@ -1,5 +1,6 @@ import logging import os +import re from shutil import copyfile import pytest @@ -37,6 +38,37 @@ assignment_id = "assign_1" +@pytest.mark.gen_test +def test_release_feedback_methods(plugin_config, tmpdir): + plugin_config.CourseDirectory.root = "/" + plugin_config.CourseDirectory.feedback_directory = str( + tmpdir.mkdir("feedback_test").realpath() + ) + plugin_config.CourseDirectory.assignment_id = assignment_id + + plugin = ExchangeReleaseFeedback( + coursedir=CourseDirectory(config=plugin_config), config=plugin_config + ) + plugin.init_src() + print(f"asserting plugin.src_path: {plugin.src_path}") + assert re.search( + r"test_release_feedback_methods0/feedback_test/\*/assign_1$", plugin.src_path + ) + plugin.coursedir.student_id = student_id + plugin.init_src() + assert re.search( + r"test_release_feedback_methods0/feedback_test/1/assign_1$", plugin.src_path + ) + + plugin.init_dest() + with pytest.raises(AttributeError) as e_info: + foo = plugin.dest_path + assert ( + str(e_info.value) + == "'ExchangeReleaseFeedback' object has no attribute 'dest_path'" + ) + + @pytest.mark.gen_test def test_release_feedback_fetch_normal(plugin_config, tmpdir): plugin_config.CourseDirectory.root = "/" diff --git a/nbexchange/tests/test_plugin_submit.py b/nbexchange/tests/test_plugin_submit.py index 96f04ac3..e8c1af40 100644 --- a/nbexchange/tests/test_plugin_submit.py +++ b/nbexchange/tests/test_plugin_submit.py @@ -1,6 +1,7 @@ import io import logging import os +import re import shutil import tarfile from os.path import basename @@ -42,6 +43,107 @@ assignment_id2 = "assign_1_2" assignment_id3 = "assign_1_3" + +@pytest.mark.gen_test +def test_submit_methods(plugin_config, tmpdir, caplog): + plugin_config.CourseDirectory.course_id = course_id + plugin_config.CourseDirectory.assignment_id = assignment_id1 + + os.makedirs(assignment_id1, exist_ok=True) + copyfile( + notebook1_filename, + os.path.join(assignment_id1, basename(notebook1_filename)), + ) + + plugin = ExchangeSubmit( + coursedir=CourseDirectory(config=plugin_config), config=plugin_config + ) + plugin.init_src() + assert re.search(r"nbexchange/assign_1_1$", plugin.src_path) + plugin.init_dest() + with pytest.raises(AttributeError) as e_info: + foo = plugin.dest_path + assert ( + str(e_info.value) + == "'ExchangeReleaseAssignment' object has no attribute 'dest_path'" + ) + file = plugin.tar_source() + assert len(file) > 1000 + + def api_request_wrong_nb(*args, **kwargs): + return type( + "Request", + (object,), + { + "status_code": 200, + "json": ( + lambda: { + "success": True, + "value": [ + { + "assignment_id": assignment_id1, + "student_id": "1", + "course_id": course_id, + "status": "released", + "path": "", + "notebooks": [ + { + "notebook_id": "assignment-0.6.1", + "has_exchange_feedback": False, + "feedback_updated": False, + "feedback_timestamp": False, + } + ], + "timestamp": "2020-01-01 00:00:00.0 UTC", + } + ], + } + ), + }, + ) + + def api_request_right_nb(*args, **kwargs): + return type( + "Request", + (object,), + { + "status_code": 200, + "json": ( + lambda: { + "success": True, + "value": [ + { + "assignment_id": assignment_id1, + "student_id": "1", + "course_id": course_id, + "status": "released", + "path": "", + "notebooks": [ + { + "notebook_id": "assignment-0.6", + "has_exchange_feedback": False, + "feedback_updated": False, + "feedback_timestamp": False, + } + ], + "timestamp": "2020-01-01 00:00:00.0 UTC", + } + ], + } + ), + }, + ) + + with patch.object(Exchange, "api_request", side_effect=api_request_wrong_nb): + plugin.check_filename_diff() + assert "assignment-0.6.1.ipynb: MISSING" in caplog.text + assert "assignment-0.6.ipynb: EXTRA" in caplog.text + caplog.clear() # clears the capture from above + with patch.object(Exchange, "api_request", side_effect=api_request_right_nb): + plugin.check_filename_diff() + assert caplog.text == "" + + # Straight simple submission works @pytest.mark.gen_test def test_submit_single_item(plugin_config, tmpdir):