[Feature] Server pagination implementation (#243)

## 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>
This commit is contained in:
2025-12-15 11:34:53 +13:00
committed by jebus
parent d2d475b990
commit 8adb93012a
59 changed files with 3101 additions and 620 deletions

View File

@ -0,0 +1 @@
# Tests for epapi v1 API

View File

@ -0,0 +1,532 @@
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)

View File

@ -0,0 +1,477 @@
from django.test import TestCase, tag
from epdb.logic import PackageManager, UserManager
from epdb.models import Compound, Reaction, Pathway, EPModel, SimpleAmbitRule, Scenario
class BaseTestAPIGetPaginated:
"""
Mixin class for API pagination tests.
Subclasses must inherit from both this class and TestCase, e.g.:
class MyTest(BaseTestAPIGetPaginated, TestCase):
...
Subclasses must define:
- resource_name: Singular name (e.g., "compound")
- resource_name_plural: Plural name (e.g., "compounds")
- global_endpoint: Global listing endpoint (e.g., "/api/v1/compounds/")
- package_endpoint_template: Template for package-scoped endpoint or None
- total_reviewed: Number of reviewed items to create
- total_unreviewed: Number of unreviewed items to create
- create_reviewed_resource(cls, package, idx): Factory method
- create_unreviewed_resource(cls, package, idx): Factory method
"""
# Configuration to be overridden by subclasses
resource_name = None
resource_name_plural = None
global_endpoint = None
package_endpoint_template = None
total_reviewed = 50
total_unreviewed = 20
default_page_size = 50
max_page_size = 100
@classmethod
def setUpTestData(cls):
# Create test user
cls.user = UserManager.create_user(
f"{cls.resource_name}-user",
f"{cls.resource_name}-user@envipath.com",
"SuperSafe",
set_setting=False,
add_to_group=False,
is_active=True,
)
# Delete the auto-created default package to ensure clean test data
default_pkg = cls.user.default_package
cls.user.default_package = None
cls.user.save()
default_pkg.delete()
# Create reviewed package
cls.reviewed_package = PackageManager.create_package(
cls.user, "Reviewed Package", f"Reviewed package for {cls.resource_name} tests"
)
cls.reviewed_package.reviewed = True
cls.reviewed_package.save()
# Create unreviewed package
cls.unreviewed_package = PackageManager.create_package(
cls.user, "Draft Package", f"Unreviewed package for {cls.resource_name} tests"
)
# Create reviewed resources
for idx in range(cls.total_reviewed):
cls.create_reviewed_resource(cls.reviewed_package, idx)
# Create unreviewed resources
for idx in range(cls.total_unreviewed):
cls.create_unreviewed_resource(cls.unreviewed_package, idx)
# Set up package-scoped endpoints if applicable
if cls.package_endpoint_template:
cls.reviewed_package_endpoint = cls.package_endpoint_template.format(
uuid=cls.reviewed_package.uuid
)
cls.unreviewed_package_endpoint = cls.package_endpoint_template.format(
uuid=cls.unreviewed_package.uuid
)
@classmethod
def create_reviewed_resource(cls, package, idx):
"""
Create a single reviewed resource.
Must be implemented by subclass.
Args:
package: The package to create the resource in
idx: Index of the resource (0-based)
"""
raise NotImplementedError(f"{cls.__name__} must implement create_reviewed_resource()")
@classmethod
def create_unreviewed_resource(cls, package, idx):
"""
Create a single unreviewed resource.
Must be implemented by subclass.
Args:
package: The package to create the resource in
idx: Index of the resource (0-based)
"""
raise NotImplementedError(f"{cls.__name__} must implement create_unreviewed_resource()")
def setUp(self):
self.client.force_login(self.user)
def test_requires_session_authentication(self):
"""Test that the global endpoint requires authentication."""
self.client.logout()
response = self.client.get(self.global_endpoint)
self.assertEqual(response.status_code, 401)
def test_global_listing_uses_default_page_size(self):
"""Test that the global endpoint uses default pagination settings."""
response = self.client.get(self.global_endpoint, {"review_status": True})
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["page"], 1)
self.assertEqual(payload["page_size"], self.default_page_size)
self.assertEqual(payload["total_items"], self.total_reviewed)
# Verify only reviewed items are returned
self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))
def test_can_request_later_page(self):
"""Test that pagination works for later pages."""
if self.total_reviewed <= self.default_page_size:
self.skipTest(
f"Not enough items to test pagination "
f"({self.total_reviewed} <= {self.default_page_size})"
)
response = self.client.get(self.global_endpoint, {"page": 2})
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["page"], 2)
# Calculate expected items on page 2
expected_items = min(self.default_page_size, self.total_reviewed - self.default_page_size)
self.assertEqual(len(payload["items"]), expected_items)
# Verify only reviewed items are returned
self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))
def test_page_size_is_capped(self):
"""Test that page size is capped at the maximum."""
if self.total_reviewed <= self.max_page_size:
self.skipTest(
f"Not enough items to test page size cap "
f"({self.total_reviewed} <= {self.max_page_size})"
)
response = self.client.get(self.global_endpoint, {"page_size": 150})
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["page_size"], self.max_page_size)
self.assertEqual(len(payload["items"]), self.max_page_size)
def test_package_endpoint_for_reviewed_package(self):
"""Test the package-scoped endpoint for reviewed packages."""
if not self.package_endpoint_template:
self.skipTest("No package endpoint for this resource")
response = self.client.get(self.reviewed_package_endpoint)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["total_items"], self.total_reviewed)
# Verify only reviewed items are returned
self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))
def test_package_endpoint_for_unreviewed_package(self):
"""Test the package-scoped endpoint for unreviewed packages."""
if not self.package_endpoint_template:
self.skipTest("No package endpoint for this resource")
response = self.client.get(self.unreviewed_package_endpoint)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["total_items"], self.total_unreviewed)
# Verify only unreviewed items are returned
self.assertTrue(all(item["review_status"] == "unreviewed" for item in payload["items"]))
@tag("api", "end2end")
class PackagePaginationAPITest(TestCase):
ENDPOINT = "/api/v1/packages/"
@classmethod
def setUpTestData(cls):
cls.user = UserManager.create_user(
"package-user",
"package-user@envipath.com",
"SuperSafe",
set_setting=False,
add_to_group=False,
is_active=True,
)
# Delete the auto-created default package to ensure clean test data
default_pkg = cls.user.default_package
cls.user.default_package = None
cls.user.save()
default_pkg.delete()
# Create reviewed packages
cls.total_reviewed = 25
for idx in range(cls.total_reviewed):
package = PackageManager.create_package(
cls.user, f"Reviewed Package {idx:03d}", "Reviewed package for tests"
)
package.reviewed = True
package.save()
# Create unreviewed packages
cls.total_unreviewed = 15
for idx in range(cls.total_unreviewed):
PackageManager.create_package(
cls.user, f"Draft Package {idx:03d}", "Unreviewed package for tests"
)
def setUp(self):
self.client.force_login(self.user)
def test_anonymous_can_access_reviewed_packages(self):
self.client.logout()
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, 200)
payload = response.json()
# Anonymous users can only see reviewed packages
self.assertEqual(payload["total_items"], self.total_reviewed)
self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))
def test_listing_uses_default_page_size(self):
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["page"], 1)
self.assertEqual(payload["page_size"], 50)
self.assertEqual(payload["total_items"], self.total_reviewed + self.total_unreviewed)
def test_reviewed_filter_true(self):
response = self.client.get(self.ENDPOINT, {"review_status": True})
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["total_items"], self.total_reviewed)
self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))
def test_reviewed_filter_false(self):
response = self.client.get(self.ENDPOINT, {"review_status": False})
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["total_items"], self.total_unreviewed)
self.assertTrue(all(item["review_status"] == "unreviewed" for item in payload["items"]))
def test_reviewed_filter_false_anonymous(self):
self.client.logout()
response = self.client.get(self.ENDPOINT, {"review_status": False})
self.assertEqual(response.status_code, 200)
payload = response.json()
# Anonymous users cannot access unreviewed packages
self.assertEqual(payload["total_items"], 0)
@tag("api", "end2end")
class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
"""Compound pagination tests using base class."""
resource_name = "compound"
resource_name_plural = "compounds"
global_endpoint = "/api/v1/compounds/"
package_endpoint_template = "/api/v1/package/{uuid}/compound/"
total_reviewed = 125
total_unreviewed = 35
@classmethod
def create_reviewed_resource(cls, package, idx):
simple_smiles = ["C", "CC", "CCC", "CCCC", "CCCCC"]
smiles = simple_smiles[idx % len(simple_smiles)] + ("O" * (idx // len(simple_smiles)))
return Compound.create(
package,
smiles,
f"Reviewed Compound {idx:03d}",
"Compound for pagination tests",
)
@classmethod
def create_unreviewed_resource(cls, package, idx):
simple_smiles = ["C", "CC", "CCC", "CCCC", "CCCCC"]
smiles = simple_smiles[idx % len(simple_smiles)] + ("N" * (idx // len(simple_smiles)))
return Compound.create(
package,
smiles,
f"Draft Compound {idx:03d}",
"Compound for pagination tests",
)
@tag("api", "end2end")
class RulePaginationAPITest(BaseTestAPIGetPaginated, TestCase):
"""Rule pagination tests using base class."""
resource_name = "rule"
resource_name_plural = "rules"
global_endpoint = "/api/v1/rules/"
package_endpoint_template = "/api/v1/package/{uuid}/rule/"
total_reviewed = 125
total_unreviewed = 35
@classmethod
def create_reviewed_resource(cls, package, idx):
# Create unique SMIRKS by combining chain length and functional group variations
# This ensures each idx gets a truly unique SMIRKS pattern
carbon_chain = "C" * (idx + 1) # C, CC, CCC, ... (grows with idx)
smirks = f"[{carbon_chain}:1]>>[{carbon_chain}:1]O"
return SimpleAmbitRule.create(
package,
f"Reviewed Rule {idx:03d}",
f"Rule {idx} for pagination tests",
smirks,
)
@classmethod
def create_unreviewed_resource(cls, package, idx):
# Create unique SMIRKS by varying the carbon chain length
carbon_chain = "C" * (idx + 1) # C, CC, CCC, ... (grows with idx)
smirks = f"[{carbon_chain}:1]>>[{carbon_chain}:1]N"
return SimpleAmbitRule.create(
package,
f"Draft Rule {idx:03d}",
f"Rule {idx} for pagination tests",
smirks,
)
@tag("api", "end2end")
class ReactionPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
"""Reaction pagination tests using base class."""
resource_name = "reaction"
resource_name_plural = "reactions"
global_endpoint = "/api/v1/reactions/"
package_endpoint_template = "/api/v1/package/{uuid}/reaction/"
total_reviewed = 125
total_unreviewed = 35
@classmethod
def create_reviewed_resource(cls, package, idx):
# Generate unique SMILES with growing chain lengths to avoid duplicates
# Each idx gets a unique chain length
educt_smiles = "C" * (idx + 1) # C, CC, CCC, ... (grows with idx)
product_smiles = educt_smiles + "O"
return Reaction.create(
package=package,
name=f"Reviewed Reaction {idx:03d}",
description="Reaction for pagination tests",
educts=[educt_smiles],
products=[product_smiles],
)
@classmethod
def create_unreviewed_resource(cls, package, idx):
# Generate unique SMILES with growing chain lengths to avoid duplicates
# Each idx gets a unique chain length
educt_smiles = "C" * (idx + 1) # C, CC, CCC, ... (grows with idx)
product_smiles = educt_smiles + "N"
return Reaction.create(
package=package,
name=f"Draft Reaction {idx:03d}",
description="Reaction for pagination tests",
educts=[educt_smiles],
products=[product_smiles],
)
@tag("api", "end2end")
class PathwayPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
"""Pathway pagination tests using base class."""
resource_name = "pathway"
resource_name_plural = "pathways"
global_endpoint = "/api/v1/pathways/"
package_endpoint_template = "/api/v1/package/{uuid}/pathway/"
total_reviewed = 125
total_unreviewed = 35
@classmethod
def create_reviewed_resource(cls, package, idx):
return Pathway.objects.create(
package=package,
name=f"Reviewed Pathway {idx:03d}",
description="Pathway for pagination tests",
)
@classmethod
def create_unreviewed_resource(cls, package, idx):
return Pathway.objects.create(
package=package,
name=f"Draft Pathway {idx:03d}",
description="Pathway for pagination tests",
)
@tag("api", "end2end")
class ModelPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
"""Model pagination tests using base class."""
resource_name = "model"
resource_name_plural = "models"
global_endpoint = "/api/v1/models/"
package_endpoint_template = "/api/v1/package/{uuid}/model/"
total_reviewed = 125
total_unreviewed = 35
@classmethod
def create_reviewed_resource(cls, package, idx):
return EPModel.objects.create(
package=package,
name=f"Reviewed Model {idx:03d}",
description="Model for pagination tests",
)
@classmethod
def create_unreviewed_resource(cls, package, idx):
return EPModel.objects.create(
package=package,
name=f"Draft Model {idx:03d}",
description="Model for pagination tests",
)
@tag("api", "end2end")
class ScenarioPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
"""Scenario pagination tests using base class."""
resource_name = "scenario"
resource_name_plural = "scenarios"
global_endpoint = "/api/v1/scenarios/"
package_endpoint_template = "/api/v1/package/{uuid}/scenario/"
total_reviewed = 125
total_unreviewed = 35
@classmethod
def create_reviewed_resource(cls, package, idx):
return Scenario.create(
package,
f"Reviewed Scenario {idx:03d}",
"Scenario for pagination tests",
"2025-01-01",
"lab",
[],
)
@classmethod
def create_unreviewed_resource(cls, package, idx):
return Scenario.create(
package,
f"Draft Scenario {idx:03d}",
"Scenario for pagination tests",
"2025-01-01",
"field",
[],
)