forked from enviPath/enviPy
## Major Changes - Implement a REST style API app in epapi - Currently implements a GET method for all entity types in the browse menu (both package level and global) - Provides paginated results per default with query style filtering for reviewed vs unreviewed. - Provides new paginated templates with thin wrappers per entity types for easier maintainability - Implements e2e tests for the API ## Minor changes - Added more comprehensive gitignore to cover coverage reports and other test/node.js etc. data. - Add additional CI file for API tests that only gets triggered on API relevant changes. ## ⚠️ Currently only works with session-based authentication. Token based will be added in new PR. Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Co-authored-by: jebus <lorsbach@envipath.com> Reviewed-on: enviPath/enviPy#243 Co-authored-by: Tobias O <tobias.olenyi@envipath.com> Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
533 lines
20 KiB
Python
533 lines
20 KiB
Python
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)
|