|
19 | 19 | from opaque_keys.edx.asides import AsideUsageKeyV2 |
20 | 20 | from opaque_keys.edx.keys import CourseKey, UsageKey |
21 | 21 | from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator |
| 22 | +from openedx_authz.constants.roles import COURSE_ADMIN, COURSE_AUDITOR, COURSE_EDITOR, COURSE_STAFF |
22 | 23 | from openedx_events.content_authoring.data import DuplicatedXBlockData |
23 | 24 | from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED |
24 | 25 | from openedx_events.testing import OpenEdxEventsTestMixin |
|
54 | 55 | from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService |
55 | 56 | from common.test.utils import assert_dict_contains_subset |
56 | 57 | from lms.djangoapps.lms_xblock.mixin import NONSENSICAL_ACCESS_RESTRICTION |
| 58 | +from openedx.core.djangoapps.authz.tests.mixins import CourseAuthoringAuthzTestMixin |
57 | 59 | from openedx.core.djangoapps.content_tagging import api as tagging_api |
58 | 60 | from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration |
59 | 61 | from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE |
@@ -3486,6 +3488,145 @@ def validate_xblock_info_consistency( |
3486 | 3488 | self.assertIsNone(xblock_info.get("child_info", None)) # noqa: PT009 |
3487 | 3489 |
|
3488 | 3490 |
|
| 3491 | +@ddt.ddt |
| 3492 | +class TestXBlockOutlineHandlerAuthz(CourseAuthoringAuthzTestMixin, ItemTest): |
| 3493 | + """ |
| 3494 | + Unit tests for xblock_outline_handler authorization functionality. |
| 3495 | + """ |
| 3496 | + |
| 3497 | + def setUp(self): |
| 3498 | + super().setUp() |
| 3499 | + user_id = self.user.id |
| 3500 | + self.chapter = BlockFactory.create( |
| 3501 | + parent_location=self.course.location, |
| 3502 | + category="chapter", |
| 3503 | + display_name="Week 1", |
| 3504 | + user_id=user_id, |
| 3505 | + ) |
| 3506 | + self.sequential = BlockFactory.create( |
| 3507 | + parent_location=self.chapter.location, |
| 3508 | + category="sequential", |
| 3509 | + display_name="Lesson 1", |
| 3510 | + user_id=user_id, |
| 3511 | + ) |
| 3512 | + self.vertical = BlockFactory.create( |
| 3513 | + parent_location=self.sequential.location, |
| 3514 | + category="vertical", |
| 3515 | + display_name="Unit 1", |
| 3516 | + user_id=user_id, |
| 3517 | + ) |
| 3518 | + # Assign COURSE_STAFF role to authorized_user for the course |
| 3519 | + self.add_user_to_role_in_course( |
| 3520 | + self.authorized_user, |
| 3521 | + COURSE_STAFF.external_key, |
| 3522 | + self.course.id |
| 3523 | + ) |
| 3524 | + |
| 3525 | + def test_authorized_user_gets_json_response(self): |
| 3526 | + """ |
| 3527 | + Test that authorized user gets JSON response from xblock_outline_handler. |
| 3528 | + """ |
| 3529 | + outline_url = reverse_usage_url("xblock_outline_handler", self.usage_key) |
| 3530 | + |
| 3531 | + self.client.login(username=self.authorized_user.username, password=self.password) |
| 3532 | + resp = self.client.get(outline_url, HTTP_ACCEPT="application/json") |
| 3533 | + |
| 3534 | + assert resp.status_code == 200 |
| 3535 | + json_response = json.loads(resp.content.decode("utf-8")) |
| 3536 | + assert "id" in json_response |
| 3537 | + assert "display_name" in json_response |
| 3538 | + assert "child_info" in json_response |
| 3539 | + |
| 3540 | + @ddt.data( |
| 3541 | + COURSE_ADMIN.external_key, |
| 3542 | + COURSE_AUDITOR.external_key, |
| 3543 | + COURSE_EDITOR.external_key, |
| 3544 | + ) |
| 3545 | + def test_other_course_roles_can_view_outline(self, role_key): |
| 3546 | + """ |
| 3547 | + Test that course_admin, course_auditor, and course_editor roles |
| 3548 | + can access the outline (all have COURSES_VIEW_COURSE). |
| 3549 | + """ |
| 3550 | + role_user = UserFactory(password=self.password) |
| 3551 | + self.add_user_to_role_in_course(role_user, role_key, self.course.id) |
| 3552 | + |
| 3553 | + outline_url = reverse_usage_url("xblock_outline_handler", self.usage_key) |
| 3554 | + self.client.login(username=role_user.username, password=self.password) |
| 3555 | + resp = self.client.get(outline_url, HTTP_ACCEPT="application/json") |
| 3556 | + |
| 3557 | + assert resp.status_code == 200 |
| 3558 | + |
| 3559 | + def test_unauthorized_user_gets_permission_denied(self): |
| 3560 | + """ |
| 3561 | + Test that unauthorized user gets 403 response from xblock_outline_handler. |
| 3562 | + """ |
| 3563 | + outline_url = reverse_usage_url("xblock_outline_handler", self.usage_key) |
| 3564 | + |
| 3565 | + self.client.login(username=self.unauthorized_user.username, password=self.password) |
| 3566 | + resp = self.client.get(outline_url, HTTP_ACCEPT="application/json") |
| 3567 | + |
| 3568 | + assert resp.status_code == 403 |
| 3569 | + |
| 3570 | + def test_superuser_gets_json_response(self): |
| 3571 | + """ |
| 3572 | + Test that superuser gets JSON response from xblock_outline_handler. |
| 3573 | + """ |
| 3574 | + outline_url = reverse_usage_url("xblock_outline_handler", self.usage_key) |
| 3575 | + |
| 3576 | + self.client.login(username=self.super_user.username, password=self.password) |
| 3577 | + resp = self.client.get(outline_url, HTTP_ACCEPT="application/json") |
| 3578 | + |
| 3579 | + assert resp.status_code == 200 |
| 3580 | + json_response = json.loads(resp.content.decode("utf-8")) |
| 3581 | + assert "id" in json_response |
| 3582 | + assert "display_name" in json_response |
| 3583 | + assert "child_info" in json_response |
| 3584 | + |
| 3585 | + def test_staff_user_gets_json_response(self): |
| 3586 | + """ |
| 3587 | + Test that staff user gets JSON response from xblock_outline_handler. |
| 3588 | + """ |
| 3589 | + outline_url = reverse_usage_url("xblock_outline_handler", self.usage_key) |
| 3590 | + |
| 3591 | + self.client.login(username=self.staff_user.username, password=self.password) |
| 3592 | + resp = self.client.get(outline_url, HTTP_ACCEPT="application/json") |
| 3593 | + |
| 3594 | + assert resp.status_code == 200 |
| 3595 | + json_response = json.loads(resp.content.decode("utf-8")) |
| 3596 | + assert "id" in json_response |
| 3597 | + assert "display_name" in json_response |
| 3598 | + assert "child_info" in json_response |
| 3599 | + |
| 3600 | + def test_authorized_chapter_outline(self): |
| 3601 | + """ |
| 3602 | + Test that authorized user can access chapter-level outline. |
| 3603 | + """ |
| 3604 | + outline_url = reverse_usage_url("xblock_outline_handler", self.chapter.location) |
| 3605 | + |
| 3606 | + self.client.login(username=self.authorized_user.username, password=self.password) |
| 3607 | + resp = self.client.get(outline_url, HTTP_ACCEPT="application/json") |
| 3608 | + |
| 3609 | + assert resp.status_code == 200 |
| 3610 | + json_response = json.loads(resp.content.decode("utf-8")) |
| 3611 | + assert json_response["display_name"] == "Week 1" |
| 3612 | + assert "child_info" in json_response |
| 3613 | + # Verify that children are included (should have the sequential) |
| 3614 | + children = json_response["child_info"]["children"] |
| 3615 | + assert len(children) > 0 |
| 3616 | + assert children[0]["display_name"] == "Lesson 1" |
| 3617 | + |
| 3618 | + def test_unauthorized_chapter_outline(self): |
| 3619 | + """ |
| 3620 | + Test that unauthorized user cannot access chapter-level outline. |
| 3621 | + """ |
| 3622 | + outline_url = reverse_usage_url("xblock_outline_handler", self.chapter.location) |
| 3623 | + |
| 3624 | + self.client.login(username=self.unauthorized_user.username, password=self.password) |
| 3625 | + resp = self.client.get(outline_url, HTTP_ACCEPT="application/json") |
| 3626 | + |
| 3627 | + assert resp.status_code == 403 |
| 3628 | + |
| 3629 | + |
3489 | 3630 | class TestGetMetadataWithProblemDefaults(ModuleStoreTestCase): |
3490 | 3631 | """ |
3491 | 3632 | Unit tests for _get_metadata_with_problem_defaults. |
|
0 commit comments