Match direct messages against every local identity alias (home id and provisioned signal-server actor ids) so recipients accept traffic addressed to their per-server presence id instead of silently dropping it. Co-authored-by: Cursor <cursoragent@cursor.com>
198 lines
7.2 KiB
TypeScript
198 lines
7.2 KiB
TypeScript
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
|
|
};
|
|
}
|