from unittest.mock import MagicMock, PropertyMock, patch from django.conf import settings as s from django.test import TestCase from epdb.logic import PackageManager from epdb.models import SimpleAmbitRule, User class SimpleAmbitRuleTest(TestCase): fixtures = ["test_fixtures.jsonl.gz"] @classmethod def setUpClass(cls): super(SimpleAmbitRuleTest, cls).setUpClass() cls.user = User.objects.get(username="anonymous") cls.package = PackageManager.create_package( cls.user, "Simple Ambit Rule Test Package", "Test Package for SimpleAmbitRule" ) def test_create_basic_rule(self): """Test creating a basic SimpleAmbitRule with minimal parameters.""" smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]" rule = SimpleAmbitRule.create(package=self.package, smirks=smirks) self.assertIsInstance(rule, SimpleAmbitRule) self.assertEqual(rule.smirks, smirks) self.assertEqual(rule.package, self.package) self.assertRegex(rule.name, r"Rule \d+") self.assertEqual(rule.description, "no description") self.assertIsNone(rule.reactant_filter_smarts) self.assertIsNone(rule.product_filter_smarts) def test_create_with_all_parameters(self): """Test creating SimpleAmbitRule with all parameters.""" smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]" name = "Test Rule" description = "A test biotransformation rule" reactant_filter = "[CH2X4]" product_filter = "[OH]" rule = SimpleAmbitRule.create( package=self.package, name=name, description=description, smirks=smirks, reactant_filter_smarts=reactant_filter, product_filter_smarts=product_filter, ) self.assertEqual(rule.name, name) self.assertEqual(rule.description, description) self.assertEqual(rule.smirks, smirks) self.assertEqual(rule.reactant_filter_smarts, reactant_filter) self.assertEqual(rule.product_filter_smarts, product_filter) def test_smirks_required(self): """Test that SMIRKS is required for rule creation.""" with self.assertRaises(ValueError) as cm: SimpleAmbitRule.create(package=self.package, smirks=None) self.assertIn("SMIRKS is required", str(cm.exception)) with self.assertRaises(ValueError) as cm: SimpleAmbitRule.create(package=self.package, smirks="") self.assertIn("SMIRKS is required", str(cm.exception)) with self.assertRaises(ValueError) as cm: SimpleAmbitRule.create(package=self.package, smirks=" ") self.assertIn("SMIRKS is required", str(cm.exception)) @patch("epdb.models.FormatConverter.is_valid_smirks") def test_invalid_smirks_validation(self, mock_is_valid): """Test validation of SMIRKS format.""" mock_is_valid.return_value = False invalid_smirks = "invalid_smirks_string" with self.assertRaises(ValueError) as cm: SimpleAmbitRule.create(package=self.package, smirks=invalid_smirks) self.assertIn(f'SMIRKS "{invalid_smirks}" is invalid', str(cm.exception)) mock_is_valid.assert_called_once_with(invalid_smirks) def test_smirks_trimming(self): """Test that SMIRKS strings are trimmed during creation.""" smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]" smirks_with_whitespace = f" {smirks} " rule = SimpleAmbitRule.create(package=self.package, smirks=smirks_with_whitespace) self.assertEqual(rule.smirks, smirks) def test_empty_name_and_description_handling(self): """Test that empty name and description are handled appropriately.""" smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]" rule = SimpleAmbitRule.create( package=self.package, smirks=smirks, name="", description=" " ) self.assertRegex(rule.name, r"Rule \d+") self.assertEqual(rule.description, "no description") def test_deduplication_basic(self): """Test that identical rules are deduplicated.""" smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]" rule1 = SimpleAmbitRule.create(package=self.package, smirks=smirks, name="Rule 1") rule2 = SimpleAmbitRule.create( package=self.package, smirks=smirks, name="Rule 2", # Different name, but same SMIRKS ) self.assertEqual(rule1.pk, rule2.pk) self.assertEqual( SimpleAmbitRule.objects.filter(package=self.package, smirks=smirks).count(), 1 ) def test_deduplication_with_filters(self): """Test deduplication with filter SMARTS.""" smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]" reactant_filter = "[CH2X4]" product_filter = "[OH]" rule1 = SimpleAmbitRule.create( package=self.package, smirks=smirks, reactant_filter_smarts=reactant_filter, product_filter_smarts=product_filter, ) rule2 = SimpleAmbitRule.create( package=self.package, smirks=smirks, reactant_filter_smarts=reactant_filter, product_filter_smarts=product_filter, ) self.assertEqual(rule1.pk, rule2.pk) def test_no_deduplication_different_filters(self): """Test that rules with different filters are not deduplicated.""" smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]" rule1 = SimpleAmbitRule.create( package=self.package, smirks=smirks, reactant_filter_smarts="[CH2X4]" ) rule2 = SimpleAmbitRule.create( package=self.package, smirks=smirks, reactant_filter_smarts="[CH3X4]" ) self.assertNotEqual(rule1.pk, rule2.pk) self.assertEqual( SimpleAmbitRule.objects.filter(package=self.package, smirks=smirks).count(), 2 ) def test_filter_smarts_trimming(self): """Test that filter SMARTS are trimmed and handled correctly.""" smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]" # Test with whitespace-only filters (should be treated as None) rule = SimpleAmbitRule.create( package=self.package, smirks=smirks, reactant_filter_smarts=" ", product_filter_smarts=" ", ) self.assertIsNone(rule.reactant_filter_smarts) self.assertIsNone(rule.product_filter_smarts) def test_url_property(self): """Test the URL property generation.""" rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]") expected_url = f"{self.package.url}/simple-ambit-rule/{rule.uuid}" self.assertEqual(rule.url, expected_url) @patch("epdb.models.FormatConverter.apply") def test_apply_method(self, mock_apply): """Test the apply method delegates to FormatConverter.""" mock_apply.return_value = ["product1", "product2"] rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]") test_smiles = "CCO" result = rule.apply(test_smiles) mock_apply.assert_called_once_with(test_smiles, rule.smirks) self.assertEqual(result, ["product1", "product2"]) def test_reactants_smarts_property(self): """Test reactants_smarts property extracts correct part of SMIRKS.""" smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]" expected_reactants = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]" rule = SimpleAmbitRule.create(package=self.package, smirks=smirks) self.assertEqual(rule.reactants_smarts, expected_reactants) def test_products_smarts_property(self): """Test products_smarts property extracts correct part of SMIRKS.""" smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]" expected_products = "[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]" rule = SimpleAmbitRule.create(package=self.package, smirks=smirks) self.assertEqual(rule.products_smarts, expected_products) @patch(f"{s.EPDB_PACKAGE_MODEL.replace('.', '.models.')}.objects") def test_related_reactions_property(self, mock_package_objects): """Test related_reactions property returns correct queryset.""" mock_qs = MagicMock() mock_package_objects.filter.return_value = mock_qs rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]") # Instead of directly assigning, patch the property or use with patch.object with patch.object( type(rule), "reaction_rule", new_callable=PropertyMock ) as mock_reaction_rule: mock_reaction_rule.return_value.filter.return_value.order_by.return_value = [ "reaction1", "reaction2", ] result = rule.related_reactions mock_package_objects.filter.assert_called_once_with(reviewed=True) mock_reaction_rule.return_value.filter.assert_called_once_with(package__in=mock_qs) mock_reaction_rule.return_value.filter.return_value.order_by.assert_called_once_with( "name" ) self.assertEqual(result, ["reaction1", "reaction2"]) @patch("epdb.models.Pathway.objects") @patch("epdb.models.Edge.objects") def test_related_pathways_property(self, mock_edge_objects, mock_pathway_objects): """Test related_pathways property returns correct queryset.""" mock_related_reactions = ["reaction1", "reaction2"] with patch.object( SimpleAmbitRule, "related_reactions", new_callable=PropertyMock ) as mock_prop: mock_prop.return_value = mock_related_reactions rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]") # Mock Edge objects query mock_edge_values = MagicMock() mock_edge_values.values.return_value = ["pathway_id1", "pathway_id2"] mock_edge_objects.filter.return_value = mock_edge_values # Mock Pathway objects query mock_pathway_qs = MagicMock() mock_pathway_objects.filter.return_value.order_by.return_value = mock_pathway_qs result = rule.related_pathways mock_edge_objects.filter.assert_called_once_with(edge_label__in=mock_related_reactions) mock_edge_values.values.assert_called_once_with("pathway_id") mock_pathway_objects.filter.assert_called_once() self.assertEqual(result, mock_pathway_qs) @patch("epdb.models.IndigoUtils.smirks_to_svg") def test_as_svg_property(self, mock_smirks_to_svg): """Test as_svg property calls IndigoUtils correctly.""" mock_smirks_to_svg.return_value = "test_svg" rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]") result = rule.as_svg mock_smirks_to_svg.assert_called_once_with(rule.smirks, True, width=800, height=400) self.assertEqual(result, "test_svg") def test_atomic_transaction(self): """Test that rule creation is atomic.""" smirks = "[H:1][C:2]>>[H:1][O:2]" # This should work normally rule = SimpleAmbitRule.create(package=self.package, smirks=smirks) self.assertIsInstance(rule, SimpleAmbitRule) # Test transaction rollback on error with patch("epdb.models.SimpleAmbitRule.save", side_effect=Exception("Database error")): with self.assertRaises(Exception): SimpleAmbitRule.create(package=self.package, smirks="[H:3][C:4]>>[H:3][O:4]") # Verify no partial data was saved self.assertEqual(SimpleAmbitRule.objects.filter(package=self.package).count(), 1) def test_multiple_duplicate_warning(self): """Test logging when multiple duplicates are found.""" smirks = "[H:1][C:2]>>[H:1][O:2]" # Create first rule rule1 = SimpleAmbitRule.create(package=self.package, smirks=smirks) # Manually create a duplicate to simulate the error condition rule2 = SimpleAmbitRule(package=self.package, smirks=smirks, name="Manual Rule") rule2.save() with patch("epdb.models.logger") as mock_logger: # This should find the existing rule and log an error about multiple matches result = SimpleAmbitRule.create(package=self.package, smirks=smirks) # Should return the first matching rule self.assertEqual(result.pk, rule1.pk) # Should log an error about multiple matches mock_logger.error.assert_called() self.assertIn("More than one rule matched", mock_logger.error.call_args[0][0]) def test_model_fields(self): """Test model field properties.""" rule = SimpleAmbitRule.create( package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]", reactant_filter_smarts="[CH3]", product_filter_smarts="[OH]", ) # Test field properties self.assertFalse(rule._meta.get_field("smirks").blank) self.assertFalse(rule._meta.get_field("smirks").null) self.assertTrue(rule._meta.get_field("reactant_filter_smarts").null) self.assertTrue(rule._meta.get_field("product_filter_smarts").null) # Test verbose names self.assertEqual(rule._meta.get_field("smirks").verbose_name, "SMIRKS") self.assertEqual( rule._meta.get_field("reactant_filter_smarts").verbose_name, "Reactant Filter SMARTS" ) self.assertEqual( rule._meta.get_field("product_filter_smarts").verbose_name, "Product Filter SMARTS" )