from django.test import TestCase, tag from epdb.logic import GroupManager, PackageManager, UserManager from epdb.models import ( Compound, GroupPackagePermission, Permission, UserPackagePermission, ) @tag("api", "end2end") class APIPermissionTestBase(TestCase): """ Base class for API permission tests. Sets up common test data: - user1: Owner of packages - user2: User with various permissions - user3: User with no permissions - reviewed_package: Public package (reviewed=True) - unreviewed_package_owned: Unreviewed package owned by user1 - unreviewed_package_read: Unreviewed package with READ permission for user2 - unreviewed_package_write: Unreviewed package with WRITE permission for user2 - unreviewed_package_all: Unreviewed package with ALL permission for user2 - unreviewed_package_no_access: Unreviewed package with no permissions for user2/user3 - group_package: Unreviewed package accessible via group permission - test_group: Group containing user2 """ @classmethod def setUpTestData(cls): # Create users cls.user1 = UserManager.create_user( "permission-user1", "permission-user1@envipath.com", "SuperSafe", set_setting=False, add_to_group=False, is_active=True, ) cls.user2 = UserManager.create_user( "permission-user2", "permission-user2@envipath.com", "SuperSafe", set_setting=False, add_to_group=False, is_active=True, ) cls.user3 = UserManager.create_user( "permission-user3", "permission-user3@envipath.com", "SuperSafe", set_setting=False, add_to_group=False, is_active=True, ) # Delete default packages to ensure clean test data for user in [cls.user1, cls.user2, cls.user3]: default_pkg = user.default_package user.default_package = None user.save() if default_pkg: default_pkg.delete() # Create reviewed package (public) cls.reviewed_package = PackageManager.create_package( cls.user1, "Reviewed Package", "Public package" ) cls.reviewed_package.reviewed = True cls.reviewed_package.save() # Create unreviewed packages with various permissions cls.unreviewed_package_owned = PackageManager.create_package( cls.user1, "User1 Owned Package", "Owned by user1" ) cls.unreviewed_package_read = PackageManager.create_package( cls.user1, "User2 Read Package", "User2 has READ permission" ) UserPackagePermission.objects.create( user=cls.user2, package=cls.unreviewed_package_read, permission=Permission.READ[0] ) cls.unreviewed_package_write = PackageManager.create_package( cls.user1, "User2 Write Package", "User2 has WRITE permission" ) UserPackagePermission.objects.create( user=cls.user2, package=cls.unreviewed_package_write, permission=Permission.WRITE[0] ) cls.unreviewed_package_all = PackageManager.create_package( cls.user1, "User2 All Package", "User2 has ALL permission" ) UserPackagePermission.objects.create( user=cls.user2, package=cls.unreviewed_package_all, permission=Permission.ALL[0] ) cls.unreviewed_package_no_access = PackageManager.create_package( cls.user1, "No Access Package", "No permissions for user2/user3" ) # Create group and group package cls.test_group = GroupManager.create_group( cls.user1, "Test Group", "Group for permission testing" ) cls.test_group.user_member.add(cls.user2) cls.test_group.save() cls.group_package = PackageManager.create_package( cls.user1, "Group Package", "Accessible via group permission" ) GroupPackagePermission.objects.create( group=cls.test_group, package=cls.group_package, permission=Permission.READ[0] ) # Create test compounds in each package cls.reviewed_compound = Compound.create( cls.reviewed_package, "C", "Reviewed Compound", "Test compound" ) cls.owned_compound = Compound.create( cls.unreviewed_package_owned, "CC", "Owned Compound", "Test compound" ) cls.read_compound = Compound.create( cls.unreviewed_package_read, "CCC", "Read Compound", "Test compound" ) cls.write_compound = Compound.create( cls.unreviewed_package_write, "CCCC", "Write Compound", "Test compound" ) cls.all_compound = Compound.create( cls.unreviewed_package_all, "CCCCC", "All Compound", "Test compound" ) cls.no_access_compound = Compound.create( cls.unreviewed_package_no_access, "CCCCCC", "No Access Compound", "Test compound" ) cls.group_compound = Compound.create( cls.group_package, "CCCCCCC", "Group Compound", "Test compound" ) @tag("api", "end2end") class PackageListPermissionTest(APIPermissionTestBase): """ Test permissions for /api/v1/packages/ endpoint. Special case: This endpoint allows anonymous access (auth=None) """ ENDPOINT = "/api/v1/packages/" def test_anonymous_user_sees_only_reviewed_packages(self): """Anonymous users should only see reviewed packages.""" self.client.logout() response = self.client.get(self.ENDPOINT) self.assertEqual(response.status_code, 200) payload = response.json() # Should only see reviewed package self.assertEqual(payload["total_items"], 1) self.assertEqual(payload["items"][0]["uuid"], str(self.reviewed_package.uuid)) self.assertEqual(payload["items"][0]["review_status"], "reviewed") def test_authenticated_user_sees_all_readable_packages(self): """Authenticated users see reviewed + packages they have access to.""" self.client.force_login(self.user2) response = self.client.get(self.ENDPOINT) self.assertEqual(response.status_code, 200) payload = response.json() # user2 should see: # - reviewed_package (public) # - unreviewed_package_read (READ permission) # - unreviewed_package_write (WRITE permission) # - unreviewed_package_all (ALL permission) # - group_package (via group membership) # Total: 5 packages self.assertEqual(payload["total_items"], 5) visible_uuids = {item["uuid"] for item in payload["items"]} expected_uuids = { str(self.reviewed_package.uuid), str(self.unreviewed_package_read.uuid), str(self.unreviewed_package_write.uuid), str(self.unreviewed_package_all.uuid), str(self.group_package.uuid), } self.assertEqual(visible_uuids, expected_uuids) def test_owner_sees_all_owned_packages(self): """Package owner sees all packages they created.""" self.client.force_login(self.user1) response = self.client.get(self.ENDPOINT) self.assertEqual(response.status_code, 200) payload = response.json() # user1 owns all packages # Total: 7 packages (all packages created in setUpTestData) self.assertEqual(payload["total_items"], 7) def test_filter_by_review_status_true(self): """Filter to show only reviewed packages.""" self.client.force_login(self.user2) response = self.client.get(self.ENDPOINT, {"review_status": True}) self.assertEqual(response.status_code, 200) payload = response.json() # Only reviewed_package self.assertEqual(payload["total_items"], 1) self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"])) def test_filter_by_review_status_false(self): """Filter to show only unreviewed packages.""" self.client.force_login(self.user2) response = self.client.get(self.ENDPOINT, {"review_status": False}) self.assertEqual(response.status_code, 200) payload = response.json() # user2's accessible unreviewed packages: 4 self.assertEqual(payload["total_items"], 4) self.assertTrue(all(item["review_status"] == "unreviewed" for item in payload["items"])) def test_anonymous_filter_unreviewed_returns_empty(self): """Anonymous users get no results when filtering for unreviewed.""" self.client.logout() response = self.client.get(self.ENDPOINT, {"review_status": False}) self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload["total_items"], 0) @tag("api", "end2end") class GlobalCompoundListPermissionTest(APIPermissionTestBase): """ Test permissions for /api/v1/compounds/ endpoint. This endpoint requires authentication. """ ENDPOINT = "/api/v1/compounds/" def test_anonymous_user_cannot_access(self): """Anonymous users should get 401 Unauthorized.""" self.client.logout() response = self.client.get(self.ENDPOINT) self.assertEqual(response.status_code, 401) def test_authenticated_user_sees_compounds_from_readable_packages(self): """Authenticated users see compounds from packages they can read.""" self.client.force_login(self.user2) response = self.client.get(self.ENDPOINT) self.assertEqual(response.status_code, 200) payload = response.json() # user2 should see compounds from: # - reviewed_package (public) # - unreviewed_package_read (READ permission) # - unreviewed_package_write (WRITE permission) # - unreviewed_package_all (ALL permission) # - group_package (via group membership) # Total: 5 compounds self.assertEqual(payload["total_items"], 5) visible_uuids = {item["uuid"] for item in payload["items"]} expected_uuids = { str(self.reviewed_compound.uuid), str(self.read_compound.uuid), str(self.write_compound.uuid), str(self.all_compound.uuid), str(self.group_compound.uuid), } self.assertEqual(visible_uuids, expected_uuids) def test_user_without_permission_cannot_see_compound(self): """User without permission to package cannot see its compounds.""" self.client.force_login(self.user3) response = self.client.get(self.ENDPOINT) self.assertEqual(response.status_code, 200) payload = response.json() # user3 should only see compounds from reviewed_package self.assertEqual(payload["total_items"], 1) self.assertEqual(payload["items"][0]["uuid"], str(self.reviewed_compound.uuid)) def test_owner_sees_all_compounds(self): """Package owner sees all compounds they created.""" self.client.force_login(self.user1) response = self.client.get(self.ENDPOINT) self.assertEqual(response.status_code, 200) payload = response.json() # user1 owns all packages, so sees all compounds self.assertEqual(payload["total_items"], 7) def test_read_permission_allows_viewing(self): """READ permission allows viewing compounds.""" self.client.force_login(self.user2) response = self.client.get(self.ENDPOINT) self.assertEqual(response.status_code, 200) payload = response.json() # Check that read_compound is included uuids = [item["uuid"] for item in payload["items"]] self.assertIn(str(self.read_compound.uuid), uuids) def test_write_permission_allows_viewing(self): """WRITE permission also allows viewing compounds.""" self.client.force_login(self.user2) response = self.client.get(self.ENDPOINT) self.assertEqual(response.status_code, 200) payload = response.json() # Check that write_compound is included uuids = [item["uuid"] for item in payload["items"]] self.assertIn(str(self.write_compound.uuid), uuids) def test_all_permission_allows_viewing(self): """ALL permission allows viewing compounds.""" self.client.force_login(self.user2) response = self.client.get(self.ENDPOINT) self.assertEqual(response.status_code, 200) payload = response.json() # Check that all_compound is included uuids = [item["uuid"] for item in payload["items"]] self.assertIn(str(self.all_compound.uuid), uuids) def test_group_permission_allows_viewing(self): """Group membership grants access to group-permitted packages.""" self.client.force_login(self.user2) response = self.client.get(self.ENDPOINT) self.assertEqual(response.status_code, 200) payload = response.json() # Check that group_compound is included uuids = [item["uuid"] for item in payload["items"]] self.assertIn(str(self.group_compound.uuid), uuids) @tag("api", "end2end") class PackageScopedCompoundListPermissionTest(APIPermissionTestBase): """ Test permissions for /api/v1/package/{uuid}/compound/ endpoint. This endpoint requires authentication AND package access. """ def test_anonymous_user_cannot_access_reviewed_package(self): """Anonymous users should get 401 even for reviewed packages.""" self.client.logout() endpoint = f"/api/v1/package/{self.reviewed_package.uuid}/compound/" response = self.client.get(endpoint) self.assertEqual(response.status_code, 401) def test_authenticated_user_can_access_reviewed_package(self): """Authenticated users can access reviewed packages.""" self.client.force_login(self.user3) endpoint = f"/api/v1/package/{self.reviewed_package.uuid}/compound/" response = self.client.get(endpoint) self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload["total_items"], 1) self.assertEqual(payload["items"][0]["uuid"], str(self.reviewed_compound.uuid)) def test_user_can_access_package_with_read_permission(self): """User with READ permission can access package-scoped endpoint.""" self.client.force_login(self.user2) endpoint = f"/api/v1/package/{self.unreviewed_package_read.uuid}/compound/" response = self.client.get(endpoint) self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload["total_items"], 1) self.assertEqual(payload["items"][0]["uuid"], str(self.read_compound.uuid)) def test_user_can_access_package_with_write_permission(self): """User with WRITE permission can access package-scoped endpoint.""" self.client.force_login(self.user2) endpoint = f"/api/v1/package/{self.unreviewed_package_write.uuid}/compound/" response = self.client.get(endpoint) self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload["total_items"], 1) self.assertEqual(payload["items"][0]["uuid"], str(self.write_compound.uuid)) def test_user_can_access_package_with_all_permission(self): """User with ALL permission can access package-scoped endpoint.""" self.client.force_login(self.user2) endpoint = f"/api/v1/package/{self.unreviewed_package_all.uuid}/compound/" response = self.client.get(endpoint) self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload["total_items"], 1) self.assertEqual(payload["items"][0]["uuid"], str(self.all_compound.uuid)) def test_user_cannot_access_package_without_permission(self): """User without permission gets 403 Forbidden.""" self.client.force_login(self.user2) endpoint = f"/api/v1/package/{self.unreviewed_package_no_access.uuid}/compound/" response = self.client.get(endpoint) self.assertEqual(response.status_code, 403) def test_nonexistent_package_returns_404(self): """Request for non-existent package returns 404.""" self.client.force_login(self.user2) fake_uuid = "00000000-0000-0000-0000-000000000000" endpoint = f"/api/v1/package/{fake_uuid}/compound/" response = self.client.get(endpoint) self.assertEqual(response.status_code, 404) def test_owner_can_access_owned_package(self): """Package owner can access their package.""" self.client.force_login(self.user1) endpoint = f"/api/v1/package/{self.unreviewed_package_owned.uuid}/compound/" response = self.client.get(endpoint) self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload["total_items"], 1) self.assertEqual(payload["items"][0]["uuid"], str(self.owned_compound.uuid)) def test_group_member_can_access_group_package(self): """Group member can access package via group permission.""" self.client.force_login(self.user2) endpoint = f"/api/v1/package/{self.group_package.uuid}/compound/" response = self.client.get(endpoint) self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload["total_items"], 1) self.assertEqual(payload["items"][0]["uuid"], str(self.group_compound.uuid)) def test_non_group_member_cannot_access_group_package(self): """Non-group member cannot access package with only group permission.""" self.client.force_login(self.user3) endpoint = f"/api/v1/package/{self.group_package.uuid}/compound/" response = self.client.get(endpoint) self.assertEqual(response.status_code, 403) @tag("api", "end2end") class MultiResourcePermissionTest(APIPermissionTestBase): """ Test that permission system works consistently across all resource types. Tests a sample of other endpoints to ensure permission logic is consistent. """ def test_rules_endpoint_respects_permissions(self): """Rules endpoint uses same permission logic.""" from epdb.models import SimpleAmbitRule # Create rule in no-access package rule = SimpleAmbitRule.create( self.unreviewed_package_no_access, "Test Rule", "Test", "[C:1]>>[C:1]O" ) self.client.force_login(self.user2) response = self.client.get("/api/v1/rules/") self.assertEqual(response.status_code, 200) payload = response.json() # user2 should not see the rule from no_access_package rule_uuids = [item["uuid"] for item in payload["items"]] self.assertNotIn(str(rule.uuid), rule_uuids) def test_reactions_endpoint_respects_permissions(self): """Reactions endpoint uses same permission logic.""" from epdb.models import Reaction # Create reaction in no-access package reaction = Reaction.create( self.unreviewed_package_no_access, "Test Reaction", "Test", ["C"], ["CO"] ) self.client.force_login(self.user2) response = self.client.get("/api/v1/reactions/") self.assertEqual(response.status_code, 200) payload = response.json() # user2 should not see the reaction from no_access_package reaction_uuids = [item["uuid"] for item in payload["items"]] self.assertNotIn(str(reaction.uuid), reaction_uuids) def test_pathways_endpoint_respects_permissions(self): """Pathways endpoint uses same permission logic.""" from epdb.models import Pathway # Create pathway in no-access package pathway = Pathway.objects.create( package=self.unreviewed_package_no_access, name="Test Pathway", description="Test" ) self.client.force_login(self.user2) response = self.client.get("/api/v1/pathways/") self.assertEqual(response.status_code, 200) payload = response.json() # user2 should not see the pathway from no_access_package pathway_uuids = [item["uuid"] for item in payload["items"]] self.assertNotIn(str(pathway.uuid), pathway_uuids)