from unittest.mock import patch, MagicMock, PropertyMock from django.test import TestCase from epdb.logic import PackageManager from epdb.models import User, SimpleAmbitRule 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('epdb.models.Package.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')