# tests/unit/services/test_messaging_service.py """Unit tests for MessagingService.""" import pytest from app.modules.messaging.models import ( ConversationParticipant, ConversationType, ParticipantType, ) from app.modules.messaging.services.messaging_service import MessagingService @pytest.fixture def messaging_service(): """Create a MessagingService instance.""" return MessagingService() @pytest.mark.unit class TestMessagingServiceCreateConversation: """Test conversation creation.""" def test_create_conversation_admin_store( self, db, messaging_service, test_admin, test_store_user, test_store ): """Test creating an admin-store conversation.""" conversation = messaging_service.create_conversation( db=db, conversation_type=ConversationType.ADMIN_STORE, subject="Test Subject", initiator_type=ParticipantType.ADMIN, initiator_id=test_admin.id, recipient_type=ParticipantType.STORE, recipient_id=test_store_user.id, store_id=test_store.id, ) db.commit() assert conversation.id is not None assert conversation.conversation_type == ConversationType.ADMIN_STORE assert conversation.subject == "Test Subject" assert conversation.store_id == test_store.id assert conversation.is_closed is False assert len(conversation.participants) == 2 def test_create_conversation_store_customer( self, db, messaging_service, test_store_user, test_customer, test_store ): """Test creating a store-customer conversation.""" conversation = messaging_service.create_conversation( db=db, conversation_type=ConversationType.STORE_CUSTOMER, subject="Customer Support", initiator_type=ParticipantType.STORE, initiator_id=test_store_user.id, recipient_type=ParticipantType.CUSTOMER, recipient_id=test_customer.id, store_id=test_store.id, ) db.commit() assert conversation.id is not None assert conversation.conversation_type == ConversationType.STORE_CUSTOMER assert len(conversation.participants) == 2 # Verify participants participant_types = [p.participant_type for p in conversation.participants] assert ParticipantType.STORE in participant_types assert ParticipantType.CUSTOMER in participant_types def test_create_conversation_admin_customer( self, db, messaging_service, test_admin, test_customer, test_store ): """Test creating an admin-customer conversation.""" conversation = messaging_service.create_conversation( db=db, conversation_type=ConversationType.ADMIN_CUSTOMER, subject="Platform Support", initiator_type=ParticipantType.ADMIN, initiator_id=test_admin.id, recipient_type=ParticipantType.CUSTOMER, recipient_id=test_customer.id, store_id=test_store.id, ) db.commit() assert conversation.conversation_type == ConversationType.ADMIN_CUSTOMER assert len(conversation.participants) == 2 def test_create_conversation_with_initial_message( self, db, messaging_service, test_admin, test_store_user, test_store ): """Test creating a conversation with an initial message.""" conversation = messaging_service.create_conversation( db=db, conversation_type=ConversationType.ADMIN_STORE, subject="With Message", initiator_type=ParticipantType.ADMIN, initiator_id=test_admin.id, recipient_type=ParticipantType.STORE, recipient_id=test_store_user.id, store_id=test_store.id, initial_message="Hello, this is the first message!", ) db.commit() db.refresh(conversation) assert conversation.message_count == 1 assert len(conversation.messages) == 1 assert conversation.messages[0].content == "Hello, this is the first message!" def test_create_store_customer_without_store_id_fails( self, db, messaging_service, test_store_user, test_customer ): """Test that store_customer conversation requires store_id.""" with pytest.raises(ValueError) as exc_info: messaging_service.create_conversation( db=db, conversation_type=ConversationType.STORE_CUSTOMER, subject="No Store", initiator_type=ParticipantType.STORE, initiator_id=test_store_user.id, recipient_type=ParticipantType.CUSTOMER, recipient_id=test_customer.id, store_id=None, ) assert "store_id required" in str(exc_info.value) @pytest.mark.unit class TestMessagingServiceGetConversation: """Test conversation retrieval.""" def test_get_conversation_success( self, db, messaging_service, test_conversation_admin_store, test_admin ): """Test getting a conversation by ID.""" conversation = messaging_service.get_conversation( db=db, conversation_id=test_conversation_admin_store.id, participant_type=ParticipantType.ADMIN, participant_id=test_admin.id, ) assert conversation is not None assert conversation.id == test_conversation_admin_store.id assert conversation.subject == "Test Admin-Store Conversation" def test_get_conversation_not_found(self, db, messaging_service, test_admin): """Test getting a non-existent conversation.""" conversation = messaging_service.get_conversation( db=db, conversation_id=99999, participant_type=ParticipantType.ADMIN, participant_id=test_admin.id, ) assert conversation is None def test_get_conversation_unauthorized( self, db, messaging_service, test_conversation_admin_store, test_customer ): """Test getting a conversation without access.""" # Customer is not a participant in admin-store conversation conversation = messaging_service.get_conversation( db=db, conversation_id=test_conversation_admin_store.id, participant_type=ParticipantType.CUSTOMER, participant_id=test_customer.id, ) assert conversation is None @pytest.mark.unit class TestMessagingServiceListConversations: """Test conversation listing.""" def test_list_conversations_success( self, db, messaging_service, multiple_conversations, test_admin ): """Test listing conversations for a participant.""" conversations, total, total_unread = messaging_service.list_conversations( db=db, participant_type=ParticipantType.ADMIN, participant_id=test_admin.id, ) # Admin should see all admin-store conversations (3 of them) assert total == 3 assert len(conversations) == 3 def test_list_conversations_with_type_filter( self, db, messaging_service, multiple_conversations, test_store_user, test_store ): """Test filtering conversations by type.""" # Store should see admin-store (3) + store-customer (2) = 5 # Filter to store-customer only conversations, total, _ = messaging_service.list_conversations( db=db, participant_type=ParticipantType.STORE, participant_id=test_store_user.id, store_id=test_store.id, conversation_type=ConversationType.STORE_CUSTOMER, ) assert total == 2 for conv in conversations: assert conv.conversation_type == ConversationType.STORE_CUSTOMER def test_list_conversations_pagination( self, db, messaging_service, multiple_conversations, test_admin ): """Test pagination of conversations.""" # First page conversations, total, _ = messaging_service.list_conversations( db=db, participant_type=ParticipantType.ADMIN, participant_id=test_admin.id, skip=0, limit=2, ) assert total == 3 assert len(conversations) == 2 # Second page conversations, total, _ = messaging_service.list_conversations( db=db, participant_type=ParticipantType.ADMIN, participant_id=test_admin.id, skip=2, limit=2, ) assert total == 3 assert len(conversations) == 1 def test_list_conversations_with_closed_filter( self, db, messaging_service, test_conversation_admin_store, closed_conversation, test_admin ): """Test filtering by open/closed status.""" # Only open conversations, total, _ = messaging_service.list_conversations( db=db, participant_type=ParticipantType.ADMIN, participant_id=test_admin.id, is_closed=False, ) assert total == 1 assert all(not conv.is_closed for conv in conversations) # Only closed conversations, total, _ = messaging_service.list_conversations( db=db, participant_type=ParticipantType.ADMIN, participant_id=test_admin.id, is_closed=True, ) assert total == 1 assert all(conv.is_closed for conv in conversations) @pytest.mark.unit class TestMessagingServiceSendMessage: """Test message sending.""" def test_send_message_success( self, db, messaging_service, test_conversation_admin_store, test_admin ): """Test sending a message.""" message = messaging_service.send_message( db=db, conversation_id=test_conversation_admin_store.id, sender_type=ParticipantType.ADMIN, sender_id=test_admin.id, content="Hello, this is a test message!", ) db.commit() assert message.id is not None assert message.content == "Hello, this is a test message!" assert message.sender_type == ParticipantType.ADMIN assert message.sender_id == test_admin.id assert message.conversation_id == test_conversation_admin_store.id # Verify conversation was updated db.refresh(test_conversation_admin_store) assert test_conversation_admin_store.message_count == 1 assert test_conversation_admin_store.last_message_at is not None def test_send_message_with_attachments( self, db, messaging_service, test_conversation_admin_store, test_admin ): """Test sending a message with attachments.""" attachments = [ { "filename": "doc1.pdf", "original_filename": "document.pdf", "file_path": "/uploads/messages/2025/01/1/doc1.pdf", "file_size": 12345, "mime_type": "application/pdf", "is_image": False, } ] message = messaging_service.send_message( db=db, conversation_id=test_conversation_admin_store.id, sender_type=ParticipantType.ADMIN, sender_id=test_admin.id, content="See attached document.", attachments=attachments, ) db.commit() db.refresh(message) assert len(message.attachments) == 1 assert message.attachments[0].original_filename == "document.pdf" def test_send_message_updates_unread_count( self, db, messaging_service, test_conversation_admin_store, test_admin, test_store_user ): """Test that sending a message updates unread count for other participants.""" # Send message as admin messaging_service.send_message( db=db, conversation_id=test_conversation_admin_store.id, sender_type=ParticipantType.ADMIN, sender_id=test_admin.id, content="Test message", ) db.commit() # Check that store user has unread count increased store_participant = ( db.query(ConversationParticipant) .filter( ConversationParticipant.conversation_id == test_conversation_admin_store.id, ConversationParticipant.participant_type == ParticipantType.STORE, ConversationParticipant.participant_id == test_store_user.id, ) .first() ) assert store_participant.unread_count == 1 # Admin's unread count should be 0 admin_participant = ( db.query(ConversationParticipant) .filter( ConversationParticipant.conversation_id == test_conversation_admin_store.id, ConversationParticipant.participant_type == ParticipantType.ADMIN, ConversationParticipant.participant_id == test_admin.id, ) .first() ) assert admin_participant.unread_count == 0 def test_send_system_message( self, db, messaging_service, test_conversation_admin_store, test_admin ): """Test sending a system message.""" message = messaging_service.send_message( db=db, conversation_id=test_conversation_admin_store.id, sender_type=ParticipantType.ADMIN, sender_id=test_admin.id, content="Conversation closed", is_system_message=True, ) db.commit() assert message.is_system_message is True @pytest.mark.unit class TestMessagingServiceMarkRead: """Test marking conversations as read.""" def test_mark_conversation_read( self, db, messaging_service, test_conversation_admin_store, test_admin, test_store_user ): """Test marking a conversation as read.""" # Send a message to create unread count messaging_service.send_message( db=db, conversation_id=test_conversation_admin_store.id, sender_type=ParticipantType.ADMIN, sender_id=test_admin.id, content="Test message", ) db.commit() # Mark as read for store result = messaging_service.mark_conversation_read( db=db, conversation_id=test_conversation_admin_store.id, reader_type=ParticipantType.STORE, reader_id=test_store_user.id, ) db.commit() assert result is True # Verify unread count is reset store_participant = ( db.query(ConversationParticipant) .filter( ConversationParticipant.conversation_id == test_conversation_admin_store.id, ConversationParticipant.participant_type == ParticipantType.STORE, ) .first() ) assert store_participant.unread_count == 0 assert store_participant.last_read_at is not None @pytest.mark.unit class TestMessagingServiceUnreadCount: """Test unread count retrieval.""" def test_get_unread_count( self, db, messaging_service, multiple_conversations, test_admin, test_store_user ): """Test getting total unread count for a participant.""" # Send messages in multiple conversations (first 2 are admin-store) for conv in multiple_conversations[:2]: messaging_service.send_message( db=db, conversation_id=conv.id, sender_type=ParticipantType.STORE, sender_id=test_store_user.id, content="Test message", ) db.commit() # Admin should have 2 unread messages unread_count = messaging_service.get_unread_count( db=db, participant_type=ParticipantType.ADMIN, participant_id=test_admin.id, ) assert unread_count == 2 def test_get_unread_count_zero(self, db, messaging_service, test_admin): """Test unread count when no messages.""" unread_count = messaging_service.get_unread_count( db=db, participant_type=ParticipantType.ADMIN, participant_id=test_admin.id, ) assert unread_count == 0 @pytest.mark.unit class TestMessagingServiceCloseReopen: """Test conversation close/reopen.""" def test_close_conversation( self, db, messaging_service, test_conversation_admin_store, test_admin ): """Test closing a conversation.""" conversation = messaging_service.close_conversation( db=db, conversation_id=test_conversation_admin_store.id, closer_type=ParticipantType.ADMIN, closer_id=test_admin.id, ) db.commit() assert conversation is not None assert conversation.is_closed is True assert conversation.closed_at is not None assert conversation.closed_by_type == ParticipantType.ADMIN assert conversation.closed_by_id == test_admin.id # Should have system message db.refresh(conversation) assert any(m.is_system_message and "closed" in m.content for m in conversation.messages) def test_reopen_conversation( self, db, messaging_service, closed_conversation, test_admin ): """Test reopening a closed conversation.""" conversation = messaging_service.reopen_conversation( db=db, conversation_id=closed_conversation.id, opener_type=ParticipantType.ADMIN, opener_id=test_admin.id, ) db.commit() assert conversation is not None assert conversation.is_closed is False assert conversation.closed_at is None assert conversation.closed_by_type is None assert conversation.closed_by_id is None @pytest.mark.unit class TestMessagingServiceParticipantInfo: """Test participant info retrieval.""" def test_get_participant_info_admin(self, db, messaging_service, test_admin): """Test getting admin participant info.""" info = messaging_service.get_participant_info( db=db, participant_type=ParticipantType.ADMIN, participant_id=test_admin.id, ) assert info is not None assert info["id"] == test_admin.id assert info["type"] == "admin" assert "email" in info def test_get_participant_info_customer(self, db, messaging_service, test_customer): """Test getting customer participant info.""" info = messaging_service.get_participant_info( db=db, participant_type=ParticipantType.CUSTOMER, participant_id=test_customer.id, ) assert info is not None assert info["id"] == test_customer.id assert info["type"] == "customer" assert info["name"] == "John Doe" def test_get_participant_info_not_found(self, db, messaging_service): """Test getting info for non-existent participant.""" info = messaging_service.get_participant_info( db=db, participant_type=ParticipantType.ADMIN, participant_id=99999, ) assert info is None @pytest.mark.unit class TestMessagingServiceNotificationPreferences: """Test notification preference updates.""" def test_update_notification_preferences( self, db, messaging_service, test_conversation_admin_store, test_admin ): """Test updating notification preferences.""" result = messaging_service.update_notification_preferences( db=db, conversation_id=test_conversation_admin_store.id, participant_type=ParticipantType.ADMIN, participant_id=test_admin.id, email_notifications=False, muted=True, ) db.commit() assert result is True # Verify preferences updated participant = ( db.query(ConversationParticipant) .filter( ConversationParticipant.conversation_id == test_conversation_admin_store.id, ConversationParticipant.participant_type == ParticipantType.ADMIN, ) .first() ) assert participant.email_notifications is False assert participant.muted is True def test_update_notification_preferences_no_changes( self, db, messaging_service, test_conversation_admin_store, test_admin ): """Test updating with no changes.""" result = messaging_service.update_notification_preferences( db=db, conversation_id=test_conversation_admin_store.id, participant_type=ParticipantType.ADMIN, participant_id=test_admin.id, ) assert result is False