import pytest from unittest.mock import Mock, AsyncMock, patch import re from backend.services.template_message_service import TemplateMessageService from backend.services.email_sender_service import EmailSenderService from backend.models.models import MessageTemplateDoc, EmailSenderDoc class TestTemplateMessageServiceUnit: """Unit tests for TemplateMessageService""" def test_service_initialization(self): """Test service can be initialized.""" service = TemplateMessageService() assert service is not None @pytest.mark.asyncio async def test_update_global_template_success(self): """Test successful global template update.""" service = TemplateMessageService() # Mock template mock_template = Mock() mock_template.tenant_id = None # Global template mock_template.set = AsyncMock() # Mock MessageTemplateDoc.get with patch('backend.services.template_message_service.MessageTemplateDoc.get', new_callable=AsyncMock, return_value=mock_template): result = await service.update_global_template("template_id", {"subject": "New Subject"}) mock_template.set.assert_called_once_with({"subject": "New Subject"}) assert result == {"success": True} @pytest.mark.asyncio async def test_create_global_template(self): """Test creating global template.""" service = TemplateMessageService() # Mock template mock_template = Mock() mock_template.tenant_id = "some_tenant" mock_template.create = AsyncMock(return_value=mock_template) # Test global template creation result = await service.create_global_template(mock_template) # Verify tenant_id is set to None assert mock_template.tenant_id is None mock_template.create.assert_called_once() assert result == mock_template @pytest.mark.asyncio async def test_update_global_template_not_found(self): """Test updating non-existent global template.""" service = TemplateMessageService() # Mock MessageTemplateDoc.get returning None with patch('backend.services.template_message_service.MessageTemplateDoc.get', new_callable=AsyncMock, return_value=None): with pytest.raises(PermissionError, match="Not a global template"): await service.update_global_template("template_id", {"subject": "New Subject"}) @pytest.mark.asyncio async def test_update_global_template_not_global(self): """Test updating tenant template as global template.""" service = TemplateMessageService() # Mock template (tenant template) mock_template = Mock() mock_template.tenant_id = "tenant_123" # Not global # Mock MessageTemplateDoc.get with patch('backend.services.template_message_service.MessageTemplateDoc.get', new_callable=AsyncMock, return_value=mock_template): with pytest.raises(PermissionError, match="Not a global template"): await service.update_global_template("template_id", {"subject": "New Subject"}) @pytest.mark.asyncio async def test_delete_global_template_success(self): """Test successful global template deletion.""" service = TemplateMessageService() # Mock template mock_template = Mock() mock_template.tenant_id = None # Global template mock_template.delete = AsyncMock() # Mock MessageTemplateDoc.get with patch('backend.services.template_message_service.MessageTemplateDoc.get', new_callable=AsyncMock, return_value=mock_template): result = await service.delete_global_template("template_id") mock_template.delete.assert_called_once() assert result == {"success": True} @pytest.mark.asyncio async def test_delete_global_template_not_found(self): """Test deleting non-existent global template.""" service = TemplateMessageService() # Mock MessageTemplateDoc.get returning None with patch('backend.services.template_message_service.MessageTemplateDoc.get', new_callable=AsyncMock, return_value=None): with pytest.raises(PermissionError, match="Not a global template"): await service.delete_global_template("template_id") @pytest.mark.asyncio async def test_delete_global_template_not_global(self): """Test deleting tenant template as global template.""" service = TemplateMessageService() # Mock template (tenant template) mock_template = Mock() mock_template.tenant_id = "tenant_123" # Not global # Mock MessageTemplateDoc.get with patch('backend.services.template_message_service.MessageTemplateDoc.get', new_callable=AsyncMock, return_value=mock_template): with pytest.raises(PermissionError, match="Not a global template"): await service.delete_global_template("template_id") @pytest.mark.asyncio async def test_get_template(self): """Test getting template by template_id, tenant_id, and region.""" service = TemplateMessageService() # Mock template mock_template = Mock() # Mock MessageTemplateDoc.find_one with patch('backend.services.template_message_service.MessageTemplateDoc.find_one', new_callable=AsyncMock, return_value=mock_template): result = await service.get_template("template_id", "tenant_123", 1) assert result == mock_template @pytest.mark.asyncio async def test_list_global_templates(self): """Test listing global templates.""" service = TemplateMessageService() # Mock templates list mock_templates = [Mock(), Mock()] mock_query = Mock() mock_query.to_list = AsyncMock(return_value=mock_templates) # Mock MessageTemplateDoc.find with patch('backend.services.template_message_service.MessageTemplateDoc.find', return_value=mock_query): result = await service.list_global_templates(1) assert result == mock_templates @pytest.mark.asyncio async def test_list_tenant_templates(self): """Test listing tenant templates.""" service = TemplateMessageService() # Mock templates list mock_templates = [Mock(), Mock()] mock_query = Mock() mock_query.to_list = AsyncMock(return_value=mock_templates) # Mock MessageTemplateDoc.find with patch('backend.services.template_message_service.MessageTemplateDoc.find', return_value=mock_query): result = await service.list_tenant_templates("tenant_123", 1) assert result == mock_templates @pytest.mark.asyncio async def test_assign_template_to_tenant_already_assigned(self): """Test assigning template that tenant already has.""" service = TemplateMessageService() # Mock global template mock_global_template = Mock() mock_global_template.template_id = "template_1" mock_global_template.region = 1 mock_global_template.subject = "Test Subject" mock_global_template.body = "Test Body" # Mock existing tenant template mock_existing_template = Mock() # Create a mock that returns different values based on the query async def mock_find_one(query): if query.get("tenant_id") is None: # Global template query return mock_global_template else: # Tenant template query return mock_existing_template # Template already exists # Mock MessageTemplateDoc.find_one with patch('backend.services.template_message_service.MessageTemplateDoc.find_one', side_effect=mock_find_one): result = await service.assign_template_to_tenant(["template_1"], 1, "tenant_123") assert len(result) == 1 assert result[0]["template_id"] == "template_1" assert result[0]["success"] is False assert "Template already assigned" in result[0]["msg"] @pytest.mark.asyncio async def test_assign_template_to_tenant_not_found(self): """Test assigning non-existent template to tenant.""" service = TemplateMessageService() # Mock MessageTemplateDoc.find_one returning None for global template with patch('backend.services.template_message_service.MessageTemplateDoc.find_one', new_callable=AsyncMock, return_value=None): result = await service.assign_template_to_tenant(["non_existent_template"], 1, "tenant_123") assert len(result) == 1 assert result[0]["template_id"] == "non_existent_template" assert result[0]["success"] is False assert "Template not found" in result[0]["msg"] @pytest.mark.asyncio async def test_assign_template_to_tenant_success_complex(self): """Test successful template assignment to tenant with complex mocking.""" service = TemplateMessageService() # Mock global template mock_global_template = Mock() mock_global_template.template_id = "template_1" mock_global_template.region = 1 mock_global_template.subject = "Test Subject" mock_global_template.body = "Test Body" # Mock new template mock_new_template = Mock() mock_new_template.id = "new_id" mock_new_template.create = AsyncMock() # Create a simple async mock function async def mock_find_one(query): if query.get("tenant_id") is None: # Global template query return mock_global_template else: # Tenant template query return None # No existing template # Mock the entire MessageTemplateDoc class with patch('backend.services.template_message_service.MessageTemplateDoc') as mock_doc_class: # Set up the find_one method mock_doc_class.find_one = mock_find_one # Set up the constructor mock_doc_class.return_value = mock_new_template result = await service.assign_template_to_tenant(["template_1"], 1, "tenant_123") assert len(result) == 1 assert result[0]["template_id"] == "template_1" assert result[0]["success"] is True assert "template_db_id" in result[0] @pytest.mark.asyncio async def test_create_template_tenant_success(self): """Test creating tenant template.""" service = TemplateMessageService() # Mock template mock_template = Mock() mock_template.tenant_id = None mock_template.create = AsyncMock(return_value=mock_template) result = await service.create_template(mock_template, "tenant_123") # Verify tenant_id is set assert mock_template.tenant_id == "tenant_123" mock_template.create.assert_called_once() assert result == mock_template @pytest.mark.asyncio async def test_update_template_tenant_success(self): """Test successful template update.""" service = TemplateMessageService() # Mock template mock_template = Mock() mock_template.tenant_id = "tenant_123" mock_template.set = AsyncMock() # Mock MessageTemplateDoc.get with patch('backend.services.template_message_service.MessageTemplateDoc.get', new_callable=AsyncMock, return_value=mock_template): result = await service.update_template("template_id", "tenant_123", {"subject": "New Subject"}) mock_template.set.assert_called_once_with({"subject": "New Subject"}) assert result == {"success": True} @pytest.mark.asyncio async def test_update_template_tenant_not_found(self): """Test updating non-existent template.""" service = TemplateMessageService() # Mock MessageTemplateDoc.get returning None with patch('backend.services.template_message_service.MessageTemplateDoc.get', new_callable=AsyncMock, return_value=None): with pytest.raises(PermissionError, match="Forbidden"): await service.update_template("template_id", "tenant_123", {"subject": "New Subject"}) @pytest.mark.asyncio async def test_update_template_tenant_forbidden(self): """Test template update with wrong tenant.""" service = TemplateMessageService() # Mock template with different tenant mock_template = Mock() mock_template.tenant_id = "tenant_456" # Different tenant # Mock MessageTemplateDoc.get with patch('backend.services.template_message_service.MessageTemplateDoc.get', new_callable=AsyncMock, return_value=mock_template): with pytest.raises(PermissionError, match="Forbidden"): await service.update_template("template_id", "tenant_123", {"subject": "New Subject"}) @pytest.mark.asyncio async def test_delete_template_tenant_success(self): """Test successful template deletion.""" service = TemplateMessageService() # Mock template mock_template = Mock() mock_template.tenant_id = "tenant_123" mock_template.delete = AsyncMock() # Mock MessageTemplateDoc.get with patch('backend.services.template_message_service.MessageTemplateDoc.get', new_callable=AsyncMock, return_value=mock_template): result = await service.delete_template("template_id", "tenant_123") mock_template.delete.assert_called_once() assert result == {"success": True} @pytest.mark.asyncio async def test_delete_template_tenant_not_found(self): """Test deleting non-existent template.""" service = TemplateMessageService() # Mock MessageTemplateDoc.get returning None with patch('backend.services.template_message_service.MessageTemplateDoc.get', new_callable=AsyncMock, return_value=None): with pytest.raises(PermissionError, match="Forbidden"): await service.delete_template("template_id", "tenant_123") @pytest.mark.asyncio async def test_delete_template_tenant_forbidden(self): """Test deleting template with wrong tenant.""" service = TemplateMessageService() # Mock template with different tenant mock_template = Mock() mock_template.tenant_id = "tenant_456" # Different tenant # Mock MessageTemplateDoc.get with patch('backend.services.template_message_service.MessageTemplateDoc.get', new_callable=AsyncMock, return_value=mock_template): with pytest.raises(PermissionError, match="Forbidden"): await service.delete_template("template_id", "tenant_123") def test_template_rendering_logic(self): """Test template rendering logic without database.""" # Mock template data (simulating what would come from database) mock_template = Mock() mock_template.subject = "Hello {name}, your code is {code}" mock_template.body = "Welcome {name}! Your verification code is {code}. Please use it within {expiry} minutes." # Extract placeholders with regex subject_placeholders = re.findall(r'\{(\w+)\}', mock_template.subject) body_placeholders = re.findall(r'\{(\w+)\}', mock_template.body) # Analyze count and type of placeholders print(f"Subject placeholders: {subject_placeholders}") # ['name', 'code'] print(f"Body placeholders: {body_placeholders}") # ['name', 'code', 'expiry'] # Count placeholders subject_count = len(subject_placeholders) body_count = len(body_placeholders) total_unique = len(set(subject_placeholders + body_placeholders)) print(f"Subject placeholder count: {subject_count}") print(f"Body placeholder count: {body_count}") print(f"Total unique placeholders: {total_unique}") # Validate placeholder types assert "name" in subject_placeholders assert "code" in subject_placeholders assert "name" in body_placeholders assert "code" in body_placeholders assert "expiry" in body_placeholders # Get all required properties required_properties = set(subject_placeholders + body_placeholders) print(f"Required properties: {required_properties}") # {'name', 'code', 'expiry'} # Test properties properties = { "name": "John Doe", "code": "123456", "expiry": "10" } # Validate all required properties are provided missing_properties = required_properties - set(properties.keys()) assert len(missing_properties) == 0, f"Missing properties: {missing_properties}" # Test rendering logic subject = mock_template.subject.format(**properties) body = mock_template.body.format(**properties) assert subject == "Hello John Doe, your code is 123456" assert body == "Welcome John Doe! Your verification code is 123456. Please use it within 10 minutes." # Test error handling when missing properties incomplete_properties = {"name": "John Doe"} # Missing code and expiry missing_properties = required_properties - set(incomplete_properties.keys()) assert len(missing_properties) == 2 assert "code" in missing_properties assert "expiry" in missing_properties # Validate that KeyError is raised when missing properties with pytest.raises(KeyError): mock_template.subject.format(**incomplete_properties) @pytest.mark.asyncio async def test_render_template_success(self): """Test successful template rendering.""" service = TemplateMessageService() # Mock template mock_template = Mock() mock_template.subject = "Hello {name}" mock_template.body = "Welcome {name}!" properties = {"name": "John"} # Test successful rendering subject, body = await service.render_template(mock_template, properties) assert subject == "Hello John" assert body == "Welcome John!" @pytest.mark.asyncio async def test_render_template_missing_property(self): """Test template rendering with missing property.""" service = TemplateMessageService() # Mock template mock_template = Mock() mock_template.subject = "Hello {name}" mock_template.body = "Welcome {name}!" properties = {} # Missing "name" # Test error handling with pytest.raises(ValueError, match="Missing template parameter"): await service.render_template(mock_template, properties) def test_complex_template_placeholder_analysis(self): """Test analyzing complex templates with multiple placeholders.""" # Complex template with various types of placeholders complex_template = Mock() complex_template.subject = "Interview {type} for {position} - {company}" complex_template.body = """ Dear {name}, Your {type} interview for the {position} position at {company} has been scheduled. Details: - Date: {date} - Time: {time} - Duration: {duration} minutes - Interviewer: {interviewer} - Location: {location} Please confirm your attendance by replying to this email. Best regards, {company} HR Team """ # Extract placeholders subject_placeholders = re.findall(r'\{(\w+)\}', complex_template.subject) body_placeholders = re.findall(r'\{(\w+)\}', complex_template.body) # Analyze placeholder statistics all_placeholders = subject_placeholders + body_placeholders unique_placeholders = list(set(all_placeholders)) # Test counts - update based on actual count assert len(subject_placeholders) == 3 assert len(body_placeholders) == 10 # Updated: actually 10 placeholders assert len(unique_placeholders) == 9 # Total unique placeholders # Test specific placeholders expected_placeholders = { "type", "position", "company", "name", "date", "time", "duration", "interviewer", "location" } assert set(unique_placeholders) == expected_placeholders # Test placeholder frequency placeholder_frequency = {} for placeholder in all_placeholders: placeholder_frequency[placeholder] = placeholder_frequency.get(placeholder, 0) + 1 # Check which placeholders appear multiple times repeated_placeholders = {k: v for k, v in placeholder_frequency.items() if v > 1} assert "company" in repeated_placeholders assert repeated_placeholders["company"] == 3 # Appears in subject and twice in body class TestEmailSenderServiceUnit: """Unit tests for EmailSenderService """ def test_service_initialization(self): """Test service can be initialized.""" service = EmailSenderService() assert service is not None @pytest.mark.asyncio async def test_get_email_sender_with_doc(self): """Test getting email senders when document exists.""" service = EmailSenderService() # Mock document mock_doc = Mock() mock_doc.email_senders = ["test@example.com", "admin@example.com"] # Mock EmailSenderDoc.find_one with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=mock_doc): result = await service.get_email_sender("tenant_123") assert result == ["test@example.com", "admin@example.com"] @pytest.mark.asyncio async def test_get_email_sender_no_doc(self): """Test getting email senders when document doesn't exist.""" service = EmailSenderService() # Mock EmailSenderDoc.find_one returning None with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=None): result = await service.get_email_sender("tenant_123") assert result == [] @pytest.mark.asyncio async def test_set_email_sender_existing_doc(self): """Test setting email senders for existing document.""" service = EmailSenderService() # Mock document mock_doc = Mock() mock_doc.email_senders = ["new@example.com"] mock_doc.set = AsyncMock() # Mock EmailSenderDoc.find_one with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=mock_doc): result = await service.set_email_sender("tenant_123", ["new@example.com"]) mock_doc.set.assert_called_once_with({"email_senders": ["new@example.com"]}) assert result["success"] is True assert result["email_senders"] == ["new@example.com"] @pytest.mark.asyncio async def test_set_email_sender_new_doc(self): """Test setting email senders for new document.""" service = EmailSenderService() # Mock document that will be returned by the constructor mock_doc = Mock() mock_doc.email_senders = ["new@example.com"] mock_doc.create = AsyncMock() # Create a simple async mock function async def mock_find_one(query): return None # No existing document # Mock the entire EmailSenderDoc class with patch('backend.services.email_sender_service.EmailSenderDoc') as mock_doc_class: # Set up the find_one method mock_doc_class.find_one = mock_find_one # Set up the constructor mock_doc_class.return_value = mock_doc result = await service.set_email_sender("tenant_123", ["new@example.com"]) # Verify the constructor was called mock_doc_class.assert_called_once_with(tenant_id="tenant_123", email_senders=["new@example.com"]) mock_doc.create.assert_called_once() assert result["success"] is True @pytest.mark.asyncio async def test_add_email_senders_no_senders(self): """Test adding email senders with no senders provided.""" service = EmailSenderService() result = await service.add_email_senders("tenant_123", None) assert result["success"] is False assert "No sender provided" in result["msg"] @pytest.mark.asyncio async def test_add_email_senders_not_list(self): """Test adding email senders with non-list input.""" service = EmailSenderService() result = await service.add_email_senders("tenant_123", "not_a_list") assert result["success"] is False assert "No sender provided" in result["msg"] @pytest.mark.asyncio async def test_add_email_senders_existing_doc(self): """Test adding email senders to existing document.""" service = EmailSenderService() # Mock document mock_doc = Mock() mock_doc.email_senders = ["existing@example.com"] mock_doc.set = AsyncMock() # Mock EmailSenderDoc.find_one with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=mock_doc): result = await service.add_email_senders("tenant_123", ["new@example.com"]) # Verify the set was called with the correct email_senders (order doesn't matter) mock_doc.set.assert_called_once() call_args = mock_doc.set.call_args[0][0] assert set(call_args["email_senders"]) == {"existing@example.com", "new@example.com"} assert result["success"] is True assert "new@example.com" in result["email_senders"] @pytest.mark.asyncio async def test_add_email_senders_all_exist(self): """Test adding email senders when all already exist.""" service = EmailSenderService() # Mock document mock_doc = Mock() mock_doc.email_senders = ["existing@example.com"] # Mock EmailSenderDoc.find_one with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=mock_doc): result = await service.add_email_senders("tenant_123", ["existing@example.com"]) assert result["success"] is False assert "All senders already exist" in result["msg"] @pytest.mark.asyncio async def test_add_email_senders_empty_list(self): """Test adding empty list of email senders.""" service = EmailSenderService() result = await service.add_email_senders("tenant_123", []) assert result["success"] is False assert "No sender provided" in result["msg"] @pytest.mark.asyncio async def test_add_email_senders_new_doc_simple(self): """Test adding email senders when document doesn't exist with simple mocking.""" service = EmailSenderService() # Mock document that will be returned by the constructor mock_doc = Mock() mock_doc.email_senders = ["new@example.com"] mock_doc.create = AsyncMock() # Create a simple async mock function async def mock_find_one(query): return None # No existing document # Mock the entire EmailSenderDoc class with patch('backend.services.email_sender_service.EmailSenderDoc') as mock_doc_class: # Set up the find_one method mock_doc_class.find_one = mock_find_one # Set up the constructor mock_doc_class.return_value = mock_doc result = await service.add_email_senders("tenant_123", ["new@example.com"]) # Verify the constructor was called mock_doc_class.assert_called_once_with(tenant_id="tenant_123", email_senders=["new@example.com"]) mock_doc.create.assert_called_once() assert result["success"] is True @pytest.mark.asyncio async def test_remove_email_senders_no_doc(self): """Test removing email senders when document doesn't exist.""" service = EmailSenderService() # Mock EmailSenderDoc.find_one returning None with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=None): result = await service.remove_email_senders("tenant_123", ["test@example.com"]) assert result["success"] is False assert "No sender found" in result["msg"] @pytest.mark.asyncio async def test_remove_email_senders_no_senders(self): """Test removing email senders when document has no senders.""" service = EmailSenderService() # Mock document with no senders mock_doc = Mock() mock_doc.email_senders = [] # Mock EmailSenderDoc.find_one with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=mock_doc): result = await service.remove_email_senders("tenant_123", ["test@example.com"]) assert result["success"] is False assert "No sender found" in result["msg"] @pytest.mark.asyncio async def test_remove_email_senders_success(self): """Test successful email sender removal.""" service = EmailSenderService() # Mock document mock_doc = Mock() mock_doc.email_senders = ["test@example.com", "admin@example.com"] mock_doc.set = AsyncMock() # Mock EmailSenderDoc.find_one with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=mock_doc): result = await service.remove_email_senders("tenant_123", ["test@example.com"]) mock_doc.set.assert_called_once_with({"email_senders": ["admin@example.com"]}) assert result["success"] is True assert result["remaining"] == ["admin@example.com"] @pytest.mark.asyncio async def test_remove_email_senders_no_match(self): """Test removing email senders when none match.""" service = EmailSenderService() # Mock document mock_doc = Mock() mock_doc.email_senders = ["admin@example.com"] # Mock EmailSenderDoc.find_one with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=mock_doc): result = await service.remove_email_senders("tenant_123", ["test@example.com"]) assert result["success"] is False assert "No sender matched for removal" in result["msg"] @pytest.mark.asyncio async def test_clear_email_senders_success(self): """Test successful email sender clearing.""" service = EmailSenderService() # Mock document mock_doc = Mock() mock_doc.set = AsyncMock() # Mock EmailSenderDoc.find_one with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=mock_doc): result = await service.clear_email_senders("tenant_123") mock_doc.set.assert_called_once_with({"email_senders": []}) assert result["success"] is True @pytest.mark.asyncio async def test_clear_email_senders_no_doc(self): """Test clearing email senders when document doesn't exist.""" service = EmailSenderService() # Mock EmailSenderDoc.find_one returning None with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=None): result = await service.clear_email_senders("tenant_123") assert result["success"] is False assert "No sender config found" in result["msg"] @pytest.mark.asyncio async def test_delete_email_sender_success(self): """Test successful email sender deletion.""" service = EmailSenderService() # Mock document mock_doc = Mock() mock_doc.delete = AsyncMock() # Mock EmailSenderDoc.find_one with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=mock_doc): result = await service.delete_email_sender("tenant_123") mock_doc.delete.assert_called_once() assert result["success"] is True @pytest.mark.asyncio async def test_delete_email_sender_no_doc(self): """Test deleting email sender when document doesn't exist.""" service = EmailSenderService() # Mock EmailSenderDoc.find_one returning None with patch('backend.services.email_sender_service.EmailSenderDoc.find_one', new_callable=AsyncMock, return_value=None): result = await service.delete_email_sender("tenant_123") assert result["success"] is False assert "No sender config found" in result["msg"]