import { advanceDirectMessageStatus, createDirectConversation, createGroupConversation, directMessageEventIncludesUser, directMessageSyncIncludesUser, directMessageConversationIncludesUser, createDirectCallStartedMessage, getDirectConversationId, isGroupDirectConversation, updateMessageStatusInConversation, upsertDirectMessage } from '../../domain/logic/direct-message.logic'; import type { DirectMessage, DirectMessageParticipant } from '../../domain/models/direct-message.model'; const alice: DirectMessageParticipant = { userId: 'alice', username: 'alice', displayName: 'Alice' }; const bob: DirectMessageParticipant = { userId: 'bob', username: 'bob', displayName: 'Bob' }; const charlie: DirectMessageParticipant = { userId: 'charlie', username: 'charlie', displayName: 'Charlie' }; describe('DirectMessageService domain flow', () => { it('should create conversation', () => { const conversation = createDirectConversation(alice, bob, 10); expect(conversation.id).toBe(getDirectConversationId('alice', 'bob')); expect(conversation.participants).toEqual(['alice', 'bob']); expect(conversation.unreadCount).toBe(0); }); it('should send message', () => { const conversation = createDirectConversation(alice, bob, 10); const queuedMessage = createMessage('message-1', 'QUEUED'); const withQueuedMessage = upsertDirectMessage(conversation, queuedMessage, false); const withSentMessage = updateMessageStatusInConversation(withQueuedMessage, queuedMessage.id, 'SENT'); expect(withSentMessage.messages[0].status).toBe('SENT'); }); it('should queue message when offline', () => { const conversation = createDirectConversation(alice, bob, 10); const queuedMessage = createMessage('message-1', 'QUEUED'); const updatedConversation = upsertDirectMessage(conversation, queuedMessage, false); expect(updatedConversation.messages[0].status).toBe('QUEUED'); }); it('should create empty group conversation without direct-message history', () => { const directConversation = upsertDirectMessage(createDirectConversation(alice, bob, 10), createMessage('message-1', 'SENT'), false); const groupConversation = createGroupConversation('dm-group-test', [ alice, bob, charlie ], 30, 'Alice, Bob, Charlie'); expect(isGroupDirectConversation(groupConversation)).toBe(true); expect(groupConversation.id).toBe('dm-group-test'); expect(groupConversation.title).toBe('Alice, Bob, Charlie'); expect(groupConversation.participants).toEqual([ 'alice', 'bob', 'charlie' ]); expect(groupConversation.messages).toEqual([]); expect(directConversation.messages).toHaveLength(1); }); it('should preserve group message recipient metadata', () => { const conversation = createGroupConversation('dm-group-test', [ alice, bob, charlie ], 10); const recipientIds = ['bob', 'charlie']; const message = createMessage('message-1', 'QUEUED', conversation.id, recipientIds); const updatedConversation = upsertDirectMessage(conversation, message, false); expect(updatedConversation.messages[0].recipientId).toBe('bob'); expect(updatedConversation.messages[0].recipientIds).toEqual(recipientIds); }); it('does not increment unread when an existing message is upserted again', () => { const conversation = createDirectConversation(alice, bob, 10); const message = createMessage('message-1', 'SENT'); const withUnreadMessage = upsertDirectMessage(conversation, message, true); const withDuplicateMessage = upsertDirectMessage(withUnreadMessage, { ...message, status: 'DELIVERED' }, true); expect(withDuplicateMessage.messages).toHaveLength(1); expect(withDuplicateMessage.unreadCount).toBe(1); expect(withDuplicateMessage.messages[0].status).toBe('DELIVERED'); }); it('does not increment unread for call-started system messages', () => { const conversation = createDirectConversation(alice, bob, 0); const message = createDirectCallStartedMessage(conversation.id, alice, ['bob'], 123); const withSystemMessage = upsertDirectMessage(conversation, message, true); expect(withSystemMessage.unreadCount).toBe(0); expect(withSystemMessage.messages).toHaveLength(1); }); it('creates call-started system messages that do not read like normal text', () => { const message = createDirectCallStartedMessage(getDirectConversationId('alice', 'bob'), alice, ['bob'], 123); expect(message.id).toBe(`dm-call-started-${getDirectConversationId('alice', 'bob')}-123`); expect(message.kind).toBe('system'); expect(message.systemEvent).toBe('call-started'); expect(message.content).toBe('Alice started a call'); expect(message.status).toBe('DELIVERED'); }); it('should update status correctly', () => { expect(advanceDirectMessageStatus('QUEUED', 'SENT')).toBe('SENT'); expect(advanceDirectMessageStatus('SENT', 'DELIVERED')).toBe('DELIVERED'); expect(advanceDirectMessageStatus('DELIVERED', 'SENT')).toBe('DELIVERED'); expect(advanceDirectMessageStatus('DELIVERED', 'ACKNOWLEDGED')).toBe('ACKNOWLEDGED'); }); it('recognises only declared direct-message recipients and participants', () => { const payload = { message: createMessage('message-1', 'SENT', 'dm-group-test', ['bob']), participants: [alice, bob], sender: alice }; expect(directMessageEventIncludesUser(payload, 'bob')).toBe(true); expect(directMessageEventIncludesUser(payload, 'charlie')).toBe(false); }); it('recognises direct-message recipients across identity aliases', () => { const payload = { message: createMessage('message-1', 'SENT', getDirectConversationId('alice', 'bob-foreign'), ['bob-foreign']), participants: [alice, { ...bob, userId: 'bob-foreign' }], sender: alice }; const bobIds = new Set(['bob', 'bob-foreign']); expect(directMessageEventIncludesUser(payload, bobIds)).toBe(true); expect(directMessageEventIncludesUser(payload, 'bob')).toBe(false); }); it('recognises conversation participants across identity aliases', () => { const conversation = { ...createDirectConversation(alice, bob, 10), participants: ['alice', 'bob-foreign'], participantProfiles: { alice, 'bob-foreign': { ...bob, userId: 'bob-foreign' } } }; expect(directMessageConversationIncludesUser(conversation, new Set(['bob', 'bob-foreign']))).toBe(true); expect(directMessageConversationIncludesUser(conversation, 'bob')).toBe(false); }); it('recognises only declared sync participants', () => { const payload = { conversationId: 'dm-group-test', messages: [], participants: [alice, bob], sender: alice, syncedAt: 30 }; expect(directMessageSyncIncludesUser(payload, 'alice')).toBe(true); expect(directMessageSyncIncludesUser(payload, 'charlie')).toBe(false); }); }); function createMessage( id: string, status: DirectMessage['status'], conversationId = getDirectConversationId('alice', 'bob'), recipientIds = ['bob'] ): DirectMessage { return { id, conversationId, senderId: 'alice', recipientId: recipientIds[0], recipientIds, content: 'Hello', timestamp: 20, status }; }