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", [], )