diff --git a/eox_core/api/v1/tests/integration/test_views.py b/eox_core/api/v1/tests/integration/test_views.py index 4f8d3335..1e59b5bd 100644 --- a/eox_core/api/v1/tests/integration/test_views.py +++ b/eox_core/api/v1/tests/integration/test_views.py @@ -183,6 +183,25 @@ def delete_pre_enrollment(self, tenant: dict, data: dict | None = None) -> reque return make_request(tenant, "DELETE", url=self.PRE_ENROLLMENT_URL, data=data) +class GradeAPIRequestMixin: + """Mixin class for the Grade API request methods.""" + + GRADE_URL = f"{settings['EOX_CORE_API_BASE']}{reverse('eox-api:eox-api:edxapp-grade')}" + + def get_grade_info(self, tenant: dict, params: dict | None = None) -> requests.Response: + """ + Get an grade in a tenant. + + Args: + tenant (dict): The tenant data. + params (dict, optional): The query parameters for the request. + + Returns: + requests.Response: The response object. + """ + return make_request(tenant, "GET", url=self.GRADE_URL, params=params) + + @ddt.ddt class TestUsersAPIIntegration(BaseIntegrationTest, UsersAPIRequestMixin): """Integration test suite for the Users API""" @@ -1445,3 +1464,163 @@ def test_info_view_success(self) -> None: self.assertIn("version", response_data) self.assertIn("name", response_data) self.assertIn("git", response_data) + + +@ddt.ddt +# pylint: disable=too-many-ancestors +class TestGradeAPIIntegration( + BaseIntegrationTest, + UsersAPIRequestMixin, + EnrollmentAPIRequestMixin, + GradeAPIRequestMixin +): + """Integration test suite for the Grade API""" + + def setUp(self) -> None: + """Set up the test suite""" + super().setUp() + self.mode = "audit" + self.user = next(FAKE_USER_DATA) + self.create_user(self.tenant_x, self.user) + self.create_enrollment_data = { + "email": self.user["email"], + "course_id": self.demo_course_id, + "mode": self.mode, + "is_active": True, + "force": True, + } + self.params = { + "username": self.user["username"], + "course_id": self.demo_course_id, + } + + @ddt.data( + {"detailed": False, "grading_policy": False}, + {"detailed": True, "grading_policy": False}, + {"detailed": True, "grading_policy": True}, + ) + def test_get_grades_for_user_successfully(self, extra_params: dict): + """ + Get Grade info about the user who belongs to tenant where the course is hosted + + Open edX definitions tested: + - `get_edxapp_user` + - `get_enrollment` + - `get_course_grade_factory` + - `get_valid_course_key` + - `get_courseware_courses` + - `grade_factory` + + Expected result: + - The status code is 200. + - The response includes the 'earned_grade'. + - The response includes the 'section_breakdown' if 'detailed' params is True, otherwise, it is not included. + - The response includes the 'grading_policy' if 'grading_policy' params is True, otherwise, it is not included. + """ + self.create_enrollment(self.tenant_x, self.create_enrollment_data) + params = self.params.copy() + params.update(extra_params) + + response = self.get_grade_info(self.tenant_x, params) + response_content = response.json() + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("earned_grade", response_content) + if extra_params["detailed"]: + self.assertIn("section_breakdown", response_content) + for detail_grade in response_content["section_breakdown"]: + self.assertGreaterEqual(detail_grade["percent"], 0.0) + self.assertGreaterEqual(detail_grade["score_earned"], 0.0) + self.assertGreaterEqual(detail_grade["score_possible"], 0.0) + self.assertFalse(detail_grade["attempted"]) + self.assertIn("assignment_type", detail_grade) + self.assertIn("subsection_name", detail_grade) + else: + self.assertNotIn("section_breakdown", response_content) + + if extra_params["grading_policy"]: + self.assertIn("grading_policy", response_content) + self.assertIn("grader", response_content["grading_policy"]) + self.assertIn("grade_cutoffs", response_content["grading_policy"]) + for detail_grade in response_content["grading_policy"]["grader"]: + self.assertGreaterEqual(detail_grade["count"], 0) + self.assertGreaterEqual(detail_grade["dropped"], 0) + self.assertIn("assignment_type", detail_grade) + self.assertIn("weight", detail_grade) + else: + self.assertNotIn("grading_policy", response_content) + + def test_get_grade_for_user_not_found(self): + """ + Test get grade info for a user and course from another site. + + Open edX definitions tested: + - `get_edxapp_user` + + Expected result: + - The status code is 404. + - The response contains an error message. + """ + params = self.params.copy() + params.update({ + "detailed": True, + "grading_policy": True, + }) + + response = self.get_grade_info(self.tenant_y, params) + response_data = response.json() + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertIn("detail", response_data) + self.assertEqual( + response_data["detail"], + f"No user found by {{'username': '{params['username']}'}} on site {self.tenant_y['domain']}.", + ) + + def test_get_grade_for_enrollment_not_found(self): + """ + Test grade info for a enrollment not found. + + Open edX definitions tested: + - `get_enrollment` + + Expected result: + - The status code is 404. + - The response contains an error message. + """ + response = self.get_grade_info(self.tenant_x, self.params) + response_data = response.json() + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual( + response_data, [ + f"No enrollment found for user:`{self.user['username']}`" + ], + ) + + def test_get_grade_for_course_not_found(self): + """ + Test grade info for a course not found. + + Open edX definitions tested: + - `get_enrollment` + + Expected result: + - The status code is 404. + - The response contains an error message. + """ + response = self.get_grade_info( + self.tenant_x, + { + "username": self.user["username"], + "course_id": "course-not-exist" + } + ) + response_data = response.json() + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual( + response_data, [ + "No course found for course_id `course-not-exist`" + ], + ) diff --git a/eox_core/tests/integration/README.rst b/eox_core/tests/integration/README.rst deleted file mode 100644 index 8c9c399d..00000000 --- a/eox_core/tests/integration/README.rst +++ /dev/null @@ -1,429 +0,0 @@ -Integration tests -================= - -.. contents:: - -Enrollments API -+++++++++++++++ - -Running -------- - -You can run the tests using the make target - -.. code-block:: console - - $ make python-test - -This test make several assumptions about the current state of the database -in case your setup differs you will have to modify ``test_data`` accordingly. - -Data requirements ------------------ -The test_data file includes the data necessary to run each test. It's content -must reflect the current database configuration of the platform you -are running the tests. The provided test are meant to be run on a devstack -environment with the following requirements: - -1. There should be a DOT application with client_id ``apiclient`` and - client_secret ``apisecret`` -2. There should be two sites available with Domain Name ``site1.localhost`` and - ``site2.localhost`` -3. Each site should have one user with username ``user_site1`` and email - ``user_site1@example.com`` for ``site1`` and ``user_site2`` and - ``user_site2@example.com`` for ``site2``. -4. ``site1`` should have a ``site1_course`` with id - ``course-v1:edX+DemoX+Demo_Course`` this course should not be available on - ``site2``. You must enable the ``audit`` and ``honor`` modes for ``site1_course`` - -``test_data`` layout -~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: json - - { - "site1_data": { - "fake_user": "fakeuser", - "user_id": "user_site1", - "user_email": "user_site1@example.com", - "host": "site1.localhost", - "course": { - "id": "course-v1:edX+DemoX+Demo_Course", - "mode": "audit" - }, - "base_url": "http://site1.localhost:18000", - "client_id": "apiclient", - "client_secret": "apisecret" - }, - "site2_data": { - "user_id": "user_site2", - "user_email": "user_site2@example.com", - "host": "site2.localhost", - "base_url": "http://site2.localhost:18000", - "client_id": "apiclient", - "client_secret": "apisecret" - } - } - -Current Tests -------------- - -Each test from this suite performs an http request to guarantee -that all CRUD operations are handled correctly. Each test case -is detailed below, including arguments used in each request -with data from ``test_data``. - -Create -~~~~~~ - - .. code:: - - SetUp 1-7: Delete previous enrollments - - 1. Create a valid enrollment ✔ - 2. Create a valid enrollment using force ✔ - 3. Create an enrollment with invalid user ❌ - 4. Create an enrollment with user from another site ❌ - 5. Create an enrollment with invalid course ❌ - 6. Create an enrollment with course from another site ❌ - 7. Create an enrollment with invalid mode ❌ - - -**Requests arguments** - -.. list-table:: - - - * - Nº - - Method - - User - - Course - - Mode - - Site - - Force - - * - 1 - - POST - - ``user_site1`` - - ``site1_course`` - - ``audit`` - - ``site1`` - - ``False`` - - * - 2 - - POST - - ``user_site1`` - - ``site1_course`` - - ``audit`` - - ``site1`` - - ``True`` - - * - 3 - - POST - - ``site1_fakeuser`` - - ``site1_course`` - - ``audit`` - - ``site1`` - - ``False`` - - * - 4 - - POST - - ``user_site2`` - - ``site1_course`` - - ``audit`` - - ``site1`` - - ``False`` - - * - 5 - - POST - - ``user_site1`` - - ``site1_fakecourse`` - - ``audit`` - - ``site1`` - - ``True`` - - * - 6 - - POST - - ``user_site2`` - - ``site1_course`` - - ``audit`` - - ``site2`` - - ``True`` - - * - 7 - - POST - - ``user_site1`` - - ``site1_course`` - - ``Masters`` - - ``site1`` - - ``True`` - -READ -~~~~ - - .. code:: - - SetUp 1,3: Create default enrollment - SetUp 2: Delete previous enrollments - - 1. Read a valid enrollment ✔ - 2. Read a non-existent enrollment ❌ - 3. Read an existing enrollment from another site ❌ - -**Requests arguments** - -.. list-table:: - - * - Nº - - Method - - User - - Course - - Site - - * - 1 - - GET - - ``user_site1`` - - ``site1_course`` - - ``site1`` - - * - 2 - - GET - - ``user_site1`` - - ``site1_course`` - - ``site1`` - - * - 3 - - GET - - ``user_site2`` - - ``site1_course`` - - ``site2`` - -UPDATE -~~~~~~ - - .. code:: - - SetUp 1-3, 6-7: Create default enrollment - SetUp 4-5: Delete previous enrollments - - 1. Change ``is_active`` ✔ - 2. Change mode ✔ - 3. Change to invalid mode ❌ - 4. Change ``is_active`` from invalid enrollment ❌ - 5. Change mode from invalid enrollment ❌ - 6. Change ``is_active`` with POST force ✔ - 7. Change mode with POST force ✔ - -**Requests arguments** - -.. list-table:: - - * - Nº - - Method - - User - - Course - - Mode - - Site - - ``is_active`` - - * - 1 - - PUT - - ``user_site1`` - - ``site1_course`` - - ``audit`` - - ``site1`` - - ``False`` - - * - 2 - - PUT - - ``user_site1`` - - ``site1_course`` - - ``honor`` - - ``site1`` - - ``True`` - - * - 3 - - PUT - - ``user_site1`` - - ``site1_course`` - - ``masters`` - - ``site1`` - - ``True`` - - * - 4 - - PUT - - ``user_site1`` - - ``site1_course`` - - ``honor`` - - ``site1`` - - ``True`` - - * - 5 - - PUT - - ``user_site1`` - - ``site1_course`` - - ``audit`` - - ``site1`` - - ``False`` - -.. list-table:: - - * - Nº - - Method - - User - - Course - - Mode - - Site - - ``is_active`` - - Force - - * - 6 - - POST - - ``user_site1`` - - ``site1_course`` - - ``audit`` - - ``site2`` - - ``False`` - - ``True`` - - * - 7 - - POST - - ``user_site1`` - - ``site1_course`` - - ``masters`` - - ``site1`` - - ``True`` - - ``True`` - -DELETE -~~~~~~ - - .. code:: - - SetUp 1,3: Create default enrollment - SetUp 2: Delete previous enrollments - - 1. Delete a valid enrollment ✔ - 2. Delete a non-existent enrollment ❌ - 3. Delete an existing enrollment from another site ❌ - -**Requests arguments** - -.. list-table:: - - * - Nº - - Method - - User - - Course - - Site - - * - 1 - - DELETE - - ``user_site1`` - - ``site1_course`` - - ``site1`` - - * - 2 - - DELETE - - ``user_site1`` - - ``site1_course`` - - ``site1`` - - * - 3 - - DELETE - - ``user_site2`` - - ``site1_course`` - - ``site2`` - -Testing on stage ----------------- -In case you want to run the test suite on a staging server, first you must -alter the ``test_data`` file. -The prerequisites mentioned on `Data requirements`_ still apply; - -1. You must have access to 2 different sites. Change: - ``site1_data['base_url']``, ``site1_data['host']``, - ``site2_data['base_url']``, ``site2_data['host']`` - depending on their domain name. -2. You must have an client_id and client_secret for each site. Change: - ``site1_data['client_id']``, ``site1_data['client_secret']``, - ``site2_data['client_id']``, ``site2_data['client_secret']`` -3. You must have one user for each site. Change: - ``site1_data['user_id']``, ``site1_data['user_email']``, - ``site2_data['user_id']``, ``site2_data['user_email']`` -4. You must have a course on site 1 that is **not** available on site 2 - with audit and honor as available modes. Change: - ``site1_data['course']['id']`` - -Grades API -+++++++++++++++ - -The info about Running, Data requirements and ``test_data`` layout is the same -as in `Enrollments Api`_. - -Current Tests -------------- - -The Grades API only supports the read operation in consequence those are the -only tests present. - -READ -~~~~ - - .. code:: - - SetUp : Create default enrollment - - 1. Read a user's final grade in a course ✔ - 2. Read a user's final grade and by subsection in a course ✔ - 3. Read a user's final grade, by subsection and course grading policy ✔ - 4. Read a user's grade, with user and course belonging to another site. ❌ - -**Requests arguments** - -.. list-table:: - - * - Nº - - Method - - User - - Course - - Site - - ``detailed`` - - ``grading_policy`` - - * - 1 - - GET - - ``user_site1`` - - ``site1_course`` - - ``site1`` - - ``false`` - - ``false`` - - * - 2 - - GET - - ``user_site1`` - - ``site1_course`` - - ``site1`` - - ``true`` - - ``false`` - - * - 3 - - GET - - ``user_site1`` - - ``site1_course`` - - ``site1`` - - ``true`` - - ``true`` - - * - 3 - - GET - - ``user_site1`` - - ``site1_course`` - - ``site2`` - - ``true`` - - ``true`` - -Testing on stage ----------------- - -Follow steps 1-3 from the `Enrollments API`_ *Testing on stage* instructions. diff --git a/eox_core/tests/integration/__init__.py b/eox_core/tests/integration/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/eox_core/tests/integration/data/fake_users.py b/eox_core/tests/integration/data/fake_users.py index 73142f94..80f57eef 100644 --- a/eox_core/tests/integration/data/fake_users.py +++ b/eox_core/tests/integration/data/fake_users.py @@ -1238,5 +1238,83 @@ "city": "New York", "goals": "Integer tincidunt. Cras dapibus.", }, + { + "username": "privera29", + "email": "privera29@protonmail.com", + "fullname": "Pedro Rivera", + "password": "pR7&2Ab#qZ@", + "activate_user": True, + "mailing_address": "305 Elm Street", + "year_of_birth": 1995, + "gender": "m", + "level_of_education": "p", + "city": "Miami", + "goals": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + }, + { + "username": "llopez17", + "email": "llopez17@protonmail.com", + "fullname": "Luisa Lopez", + "password": "lL3%8Mp@kF#", + "activate_user": True, + "mailing_address": "500 Oak Drive", + "year_of_birth": 1990, + "gender": "f", + "level_of_education": "p", + "city": "Los Angeles", + "goals": "Sed eget justo nec erat dignissim suscipit.", + }, + { + "username": "jrodriguez42", + "email": "jrodriguez42@protonmail.com", + "fullname": "Juan Rodriguez", + "password": "jR9!5Eq*gT@", + "activate_user": True, + "mailing_address": "702 Maple Avenue", + "year_of_birth": 1983, + "gender": "m", + "level_of_education": "p", + "city": "New York", + "goals": "Vivamus ut sem consectetur, dignissim elit nec, placerat purus.", + }, + { + "username": "smartinez23", + "email": "smartinez23@protonmail.com", + "fullname": "Sofia Martinez", + "password": "sM2@4Dl!hB#", + "activate_user": True, + "mailing_address": "810 Pine Road", + "year_of_birth": 1987, + "gender": "f", + "level_of_education": "p", + "city": "Chicago", + "goals": "Donec eget ante eu dolor sodales molestie.", + }, + { + "username": "jmendoza35", + "email": "jmendoza35@protonmail.com", + "fullname": "Jorge Mendoza", + "password": "jM6@3Rw!nZ#", + "activate_user": True, + "mailing_address": "409 Cedar Street", + "year_of_birth": 1992, + "gender": "m", + "level_of_education": "p", + "city": "Houston", + "goals": "Pellentesque maximus ante nec sem malesuada, et interdum nisl iaculis.", + }, + { + "username": "ggonzalez19", + "email": "ggonzalez19@protonmail.com", + "fullname": "Gabriela Gonzalez", + "password": "gG1@7Sw$pC#", + "activate_user": True, + "mailing_address": "201 Walnut Boulevard", + "year_of_birth": 1985, + "gender": "f", + "level_of_education": "p", + "city": "San Francisco", + "goals": "In ultrices sem nec turpis pretium, non consequat quam venenatis.", + }, ] ) diff --git a/eox_core/tests/integration/test_data b/eox_core/tests/integration/test_data deleted file mode 100644 index cb0cc0eb..00000000 --- a/eox_core/tests/integration/test_data +++ /dev/null @@ -1,23 +0,0 @@ -{ - "site1_data": { - "fake_user": "fakeuser", - "user_id": "user_site1", - "user_email": "user_site1@example.com", - "host": "site1.localhost", - "course": { - "id": "course-v1:edX+DemoX+Demo_Course", - "mode": "audit" - }, - "base_url": "http://site1.localhost:18000", - "client_id": "apiclient", - "client_secret": "apisecret" - }, - "site2_data": { - "user_id": "user_site2", - "user_email": "user_site2@example.com", - "host": "site2.localhost", - "base_url": "http://site2.localhost:18000", - "client_id": "apiclient", - "client_secret": "apisecret" - } -} diff --git a/eox_core/tests/integration/test_grade_integration.py b/eox_core/tests/integration/test_grade_integration.py deleted file mode 100644 index 4813acbb..00000000 --- a/eox_core/tests/integration/test_grade_integration.py +++ /dev/null @@ -1,173 +0,0 @@ -""" -Integration test suite. - -This suite performs multiple http requests to guarantee -that the Grade API is behaving as expected on a live server. -""" -import json -from os import environ - -import pytest -import requests -from django.test import TestCase -from rest_framework import status - - -@pytest.mark.skipif(not environ.get("TEST_INTEGRATION"), reason="Run only explicitly") -class TesteGradeIntegration(TestCase): # pragma: no cover - """Test suite""" - - data = {} - - @classmethod - def setUpClass(cls): - with open("eox_core/tests/integration/test_data", encoding="utf-8") as file_obj: - cls.data = json.load(file_obj) - cls.data["endpoint"] = "eox-core/api/v1/grade/" - site1_data = { - "client_id": cls.data["site1_data"]["client_id"], - "client_secret": cls.data["site1_data"]["client_secret"], - "grant_type": "client_credentials", - } - site2_data = { - "client_id": cls.data["site2_data"]["client_id"], - "client_secret": cls.data["site2_data"]["client_secret"], - "grant_type": "client_credentials", - } - request_url = f"{cls.data['site1_data']['base_url']}/oauth2/access_token/" - response_site1 = requests.post(request_url, data=site1_data, timeout=10) - response_site1.raise_for_status() - cls.data["site1_data"]["token"] = response_site1.json()["access_token"] - request_url = f"{cls.data['site2_data']['base_url']}/oauth2/access_token/" - response_site2 = requests.post(request_url, data=site2_data, timeout=10) - response_site2.raise_for_status() - cls.data["site1_data"]["token"] = response_site1.json()["access_token"] - cls.data["site2_data"]["token"] = response_site2.json()["access_token"] - - create_enrollment(cls.data) - - @classmethod - def tearDownClass(cls): - pass - - def test_read_valid_default_params(self): - # pylint: disable=invalid-name - """ - Get grades info from a user enrolled on a course without the optional - fields. - """ - site1_data = self.data["site1_data"] - data = { - "email": site1_data["user_email"], - "course_id": site1_data["course"]["id"], - } - headers = { - "Authorization": f"Bearer {site1_data['token']}", - "Host": site1_data["host"], - } - request_url = f"{site1_data['base_url']}/{self.data['endpoint']}" - - response = requests.get(request_url, data=data, headers=headers, timeout=10) - response_content = response.json() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn("earned_grade", response_content) - self.assertNotIn("grading_policy", response_content) - self.assertNotIn("section_breakdown", response_content) - - def test_read_detail_no_policy(self): - # pylint: disable=invalid-name - """ - Get grades info from a user enrolled on a course. Include detailed info - for each graded subsection. - """ - site1_data = self.data["site1_data"] - data = { - "email": site1_data["user_email"], - "course_id": site1_data["course"]["id"], - "detailed": True, - } - headers = { - "Authorization": f"Bearer {site1_data['token']}", - "Host": site1_data["host"], - } - request_url = f"{site1_data['base_url']}/{self.data['endpoint']}" - - response = requests.get(request_url, data=data, headers=headers, timeout=10) - response_content = response.json() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn("earned_grade", response_content) - self.assertIn("section_breakdown", response_content) - self.assertNotIn("grading_policy", response_content) - - def test_read_policy_detail(self): - # pylint: disable=invalid-name - """ - Get grades info from a user enrolled on a course. Include all extra info - (subsection details and grading policy) - """ - site1_data = self.data["site1_data"] - data = { - "email": site1_data["user_email"], - "course_id": site1_data["course"]["id"], - "detailed": True, - "grading_policy": True, - } - headers = { - "Authorization": f"Bearer {site1_data['token']}", - "Host": site1_data["host"], - } - request_url = f"{site1_data['base_url']}/{self.data['endpoint']}" - - response = requests.get(request_url, data=data, headers=headers, timeout=10) - response_content = response.json() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn("earned_grade", response_content) - self.assertIn("section_breakdown", response_content) - self.assertIn("grading_policy", response_content) - self.assertIn("grader", response_content["grading_policy"]) - self.assertIn("grade_cutoffs", response_content["grading_policy"]) - - def test_read_invalid_enrollment_for_site(self): - # pylint: disable=invalid-name - """ - Get grade info for a user and course from another site. - """ - site1_data = self.data["site1_data"] - site2_data = self.data["site2_data"] - data = { - "email": site1_data["user_email"], - "course_id": site1_data["course"]["id"], - "detailed": True, - "grading_policy": True, - } - headers = { - "Authorization": f"Bearer {site2_data['token']}", - "Host": site2_data["host"], - } - request_url = f"{site2_data['base_url']}/{self.data['endpoint']}" - - response = requests.get(request_url, data=data, headers=headers, timeout=10) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - -def create_enrollment(data): - """ - Auxiliary function to setUp test fixtures. Creates/enables a new enrollment. - - :param data: dictionary with all the parameters needed to create an enrollment. - """ - req_data = { - "email": data["site1_data"]["user_email"], - "course_id": data["site1_data"]["course"]["id"], - "mode": data["site1_data"]["course"]["mode"], - } - headers = { - "Authorization": f"Bearer {data['site1_data']['token']}", - "Host": data["site1_data"]["host"], - } - request_url = f"{data['site1_data']['base_url']}/eox-core/api/v1/enrollment/" - response = requests.post(request_url, data=req_data, headers=headers, timeout=10) - response.raise_for_status()