Files
orion/tests/unit/services/test_messaging_service.py
Samir Boulahtit 4adc35a674 refactor: update test imports to use module locations
Update all test files to import from canonical module locations:
- Integration tests: orders, inventory, messages, invoices
- Unit tests: services and models
- Fixtures: customer, vendor, message fixtures

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 09:02:50 +01:00

588 lines
21 KiB
Python

# tests/unit/services/test_messaging_service.py
"""Unit tests for MessagingService."""
import pytest
from app.services.messaging_service import MessagingService
from app.modules.messaging.models import (
Conversation,
ConversationParticipant,
ConversationType,
Message,
ParticipantType,
)
@pytest.fixture
def messaging_service():
"""Create a MessagingService instance."""
return MessagingService()
@pytest.mark.unit
class TestMessagingServiceCreateConversation:
"""Test conversation creation."""
def test_create_conversation_admin_vendor(
self, db, messaging_service, test_admin, test_vendor_user, test_vendor
):
"""Test creating an admin-vendor conversation."""
conversation = messaging_service.create_conversation(
db=db,
conversation_type=ConversationType.ADMIN_VENDOR,
subject="Test Subject",
initiator_type=ParticipantType.ADMIN,
initiator_id=test_admin.id,
recipient_type=ParticipantType.VENDOR,
recipient_id=test_vendor_user.id,
vendor_id=test_vendor.id,
)
db.commit()
assert conversation.id is not None
assert conversation.conversation_type == ConversationType.ADMIN_VENDOR
assert conversation.subject == "Test Subject"
assert conversation.vendor_id == test_vendor.id
assert conversation.is_closed is False
assert len(conversation.participants) == 2
def test_create_conversation_vendor_customer(
self, db, messaging_service, test_vendor_user, test_customer, test_vendor
):
"""Test creating a vendor-customer conversation."""
conversation = messaging_service.create_conversation(
db=db,
conversation_type=ConversationType.VENDOR_CUSTOMER,
subject="Customer Support",
initiator_type=ParticipantType.VENDOR,
initiator_id=test_vendor_user.id,
recipient_type=ParticipantType.CUSTOMER,
recipient_id=test_customer.id,
vendor_id=test_vendor.id,
)
db.commit()
assert conversation.id is not None
assert conversation.conversation_type == ConversationType.VENDOR_CUSTOMER
assert len(conversation.participants) == 2
# Verify participants
participant_types = [p.participant_type for p in conversation.participants]
assert ParticipantType.VENDOR in participant_types
assert ParticipantType.CUSTOMER in participant_types
def test_create_conversation_admin_customer(
self, db, messaging_service, test_admin, test_customer, test_vendor
):
"""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,
vendor_id=test_vendor.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_vendor_user, test_vendor
):
"""Test creating a conversation with an initial message."""
conversation = messaging_service.create_conversation(
db=db,
conversation_type=ConversationType.ADMIN_VENDOR,
subject="With Message",
initiator_type=ParticipantType.ADMIN,
initiator_id=test_admin.id,
recipient_type=ParticipantType.VENDOR,
recipient_id=test_vendor_user.id,
vendor_id=test_vendor.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_vendor_customer_without_vendor_id_fails(
self, db, messaging_service, test_vendor_user, test_customer
):
"""Test that vendor_customer conversation requires vendor_id."""
with pytest.raises(ValueError) as exc_info:
messaging_service.create_conversation(
db=db,
conversation_type=ConversationType.VENDOR_CUSTOMER,
subject="No Vendor",
initiator_type=ParticipantType.VENDOR,
initiator_id=test_vendor_user.id,
recipient_type=ParticipantType.CUSTOMER,
recipient_id=test_customer.id,
vendor_id=None,
)
assert "vendor_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_vendor, test_admin
):
"""Test getting a conversation by ID."""
conversation = messaging_service.get_conversation(
db=db,
conversation_id=test_conversation_admin_vendor.id,
participant_type=ParticipantType.ADMIN,
participant_id=test_admin.id,
)
assert conversation is not None
assert conversation.id == test_conversation_admin_vendor.id
assert conversation.subject == "Test Admin-Vendor 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_vendor, test_customer
):
"""Test getting a conversation without access."""
# Customer is not a participant in admin-vendor conversation
conversation = messaging_service.get_conversation(
db=db,
conversation_id=test_conversation_admin_vendor.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-vendor 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_vendor_user, test_vendor
):
"""Test filtering conversations by type."""
# Vendor should see admin-vendor (3) + vendor-customer (2) = 5
# Filter to vendor-customer only
conversations, total, _ = messaging_service.list_conversations(
db=db,
participant_type=ParticipantType.VENDOR,
participant_id=test_vendor_user.id,
vendor_id=test_vendor.id,
conversation_type=ConversationType.VENDOR_CUSTOMER,
)
assert total == 2
for conv in conversations:
assert conv.conversation_type == ConversationType.VENDOR_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_vendor, 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_vendor, test_admin
):
"""Test sending a message."""
message = messaging_service.send_message(
db=db,
conversation_id=test_conversation_admin_vendor.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_vendor.id
# Verify conversation was updated
db.refresh(test_conversation_admin_vendor)
assert test_conversation_admin_vendor.message_count == 1
assert test_conversation_admin_vendor.last_message_at is not None
def test_send_message_with_attachments(
self, db, messaging_service, test_conversation_admin_vendor, 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_vendor.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_vendor, test_admin, test_vendor_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_vendor.id,
sender_type=ParticipantType.ADMIN,
sender_id=test_admin.id,
content="Test message",
)
db.commit()
# Check that vendor user has unread count increased
vendor_participant = (
db.query(ConversationParticipant)
.filter(
ConversationParticipant.conversation_id == test_conversation_admin_vendor.id,
ConversationParticipant.participant_type == ParticipantType.VENDOR,
ConversationParticipant.participant_id == test_vendor_user.id,
)
.first()
)
assert vendor_participant.unread_count == 1
# Admin's unread count should be 0
admin_participant = (
db.query(ConversationParticipant)
.filter(
ConversationParticipant.conversation_id == test_conversation_admin_vendor.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_vendor, test_admin
):
"""Test sending a system message."""
message = messaging_service.send_message(
db=db,
conversation_id=test_conversation_admin_vendor.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_vendor, test_admin, test_vendor_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_vendor.id,
sender_type=ParticipantType.ADMIN,
sender_id=test_admin.id,
content="Test message",
)
db.commit()
# Mark as read for vendor
result = messaging_service.mark_conversation_read(
db=db,
conversation_id=test_conversation_admin_vendor.id,
reader_type=ParticipantType.VENDOR,
reader_id=test_vendor_user.id,
)
db.commit()
assert result is True
# Verify unread count is reset
vendor_participant = (
db.query(ConversationParticipant)
.filter(
ConversationParticipant.conversation_id == test_conversation_admin_vendor.id,
ConversationParticipant.participant_type == ParticipantType.VENDOR,
)
.first()
)
assert vendor_participant.unread_count == 0
assert vendor_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_vendor_user
):
"""Test getting total unread count for a participant."""
# Send messages in multiple conversations (first 2 are admin-vendor)
for conv in multiple_conversations[:2]:
messaging_service.send_message(
db=db,
conversation_id=conv.id,
sender_type=ParticipantType.VENDOR,
sender_id=test_vendor_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_vendor, test_admin
):
"""Test closing a conversation."""
conversation = messaging_service.close_conversation(
db=db,
conversation_id=test_conversation_admin_vendor.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_vendor, test_admin
):
"""Test updating notification preferences."""
result = messaging_service.update_notification_preferences(
db=db,
conversation_id=test_conversation_admin_vendor.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_vendor.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_vendor, test_admin
):
"""Test updating with no changes."""
result = messaging_service.update_notification_preferences(
db=db,
conversation_id=test_conversation_admin_vendor.id,
participant_type=ParticipantType.ADMIN,
participant_id=test_admin.id,
)
assert result is False