From 9af620baf302612543590cc13b09ae087bbafa56 Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 14:44:03 -0600 Subject: [PATCH 01/25] chore: add more unit tests for data.ts --- ts/test/data/data_test.ts | 142 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 ts/test/data/data_test.ts diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts new file mode 100644 index 000000000..3db00dbbd --- /dev/null +++ b/ts/test/data/data_test.ts @@ -0,0 +1,142 @@ +import { afterEach, beforeEach, describe } from 'mocha'; +import Sinon from 'sinon'; +import { expect } from 'chai'; +import { Data } from '../../data/data'; +import { channels } from '../../data/channels'; +import * as dataInit from '../../data/dataInit'; +import { GuardNode } from '../../data/types'; +import { Storage } from '../../util/storage'; +import * as cryptoUtils from '../../session/crypto'; + +describe('data', () => { + beforeEach(() => { + channels.close = () => {}; + channels.removeDB = () => {}; + channels.getPasswordHash = () => {}; + channels.getGuardNodes = () => {}; + channels.updateGuardNodes = () => {}; + channels.getItemById = () => {}; + channels.createOrUpdateItem = () => {}; + }); + + afterEach(() => { + Sinon.restore(); + }); + + describe('shutdown', () => { + it('shuts down the data service', async () => { + const shutdownStub = Sinon.stub(dataInit, 'shutdown'); + const closeStub = Sinon.stub(channels, 'close'); + + await Data.shutdown(); + + expect(closeStub.calledOnce).to.be.true; + expect(shutdownStub.calledOnce).to.be.true; + }); + }); + + describe('close', () => { + it('closes the data service', async () => { + const closeStub = Sinon.stub(channels, 'close'); + + await Data.close(); + + expect(closeStub.calledOnce).to.be.true; + }); + }); + + describe('removeDB', () => { + it('removes the database', async () => { + const removeStub = Sinon.stub(channels, 'removeDB'); + + await Data.removeDB(); + + expect(removeStub.calledOnce).to.be.true; + }); + }); + + describe('getPasswordHash', () => { + it('returns the password hash', async () => { + const expectedPasswordHash = 'passwordHash'; + const getPasswordHashStub = Sinon.stub(channels, 'getPasswordHash').resolves( + expectedPasswordHash + ); + + const actualPasswordHash = await Data.getPasswordHash(); + + expect(getPasswordHashStub.calledOnce).to.be.true; + expect(expectedPasswordHash).to.equal(actualPasswordHash); + }); + }); + + describe('getGuardNodes', () => { + it('returns guard nodes', async () => { + const expectedGuardNodes: Array = [ + { + ed25519PubKey: 'foobar', + }, + ]; + + const getGuardNodesStub = Sinon.stub(channels, 'getGuardNodes').resolves(expectedGuardNodes); + const actualGuardNodes = await Data.getGuardNodes(); + + expect(getGuardNodesStub.calledOnce).to.be.true; + expect(expectedGuardNodes).to.deep.equal(actualGuardNodes); + }); + }); + + describe('updateGuardNodes', () => { + it('updates guard nodes', async () => { + const updateGuardNodesStub = Sinon.stub(channels, 'updateGuardNodes'); + const expectedGuardNode = 'foo'; + + await Data.updateGuardNodes([expectedGuardNode]); + + expect(updateGuardNodesStub.calledOnce).to.be.true; + expect(updateGuardNodesStub.calledWith([expectedGuardNode])).to.be.true; + }); + }); + + describe('generateAttachmentKeyIfEmpty', () => { + it('does not generate a new key when one already exists', async () => { + const existingKey = { id: 'local_attachment_encrypted_key', value: 'existing_key' }; + const getItemByIdStub = Sinon.stub(channels, 'getItemById').resolves(existingKey); + const createOrUpdateItemStub = Sinon.stub(channels, 'createOrUpdateItem'); + const storagePutStub = Sinon.stub(Storage, 'put'); + + await Data.generateAttachmentKeyIfEmpty(); + + expect(getItemByIdStub.calledOnce).to.be.true; + expect(getItemByIdStub.calledWith('local_attachment_encrypted_key')).to.be.true; + expect(createOrUpdateItemStub.called).to.be.false; + expect(storagePutStub.called).to.be.false; + }); + + it('generates a new key when none exists', async () => { + const getItemByIdStub = Sinon.stub(channels, 'getItemById').resolves(undefined); + const createOrUpdateItemStub = Sinon.stub(channels, 'createOrUpdateItem'); + const storagePutStub = Sinon.stub(Storage, 'put'); + + const mockSodium = { + to_hex: Sinon.stub().returns('generated_hex_key'), + randombytes_buf: Sinon.stub().returns(new Uint8Array(32)), + } as any; + const getSodiumRendererStub = Sinon.stub(cryptoUtils, 'getSodiumRenderer').resolves(mockSodium); + + await Data.generateAttachmentKeyIfEmpty(); + + expect(getItemByIdStub.calledOnce).to.be.true; + expect(getItemByIdStub.calledWith('local_attachment_encrypted_key')).to.be.true; + expect(getSodiumRendererStub.calledOnce).to.be.true; + expect(mockSodium.randombytes_buf.calledWith(32)).to.be.true; + expect(mockSodium.to_hex.calledOnce).to.be.true; + expect(createOrUpdateItemStub.calledOnce).to.be.true; + expect(createOrUpdateItemStub.calledWith({ + id: 'local_attachment_encrypted_key', + value: 'generated_hex_key', + })).to.be.true; + expect(storagePutStub.calledOnce).to.be.true; + expect(storagePutStub.calledWith('local_attachment_encrypted_key', 'generated_hex_key')).to.be.true; + }); + }); +}); From cf26f661cf66cbe2ba883951fe4df93d66cd2b4f Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 14:49:24 -0600 Subject: [PATCH 02/25] chore: add unit tests for generateAttachmentKeyIfEmpty, add more assertions --- ts/test/data/data_test.ts | 43 ++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index 3db00dbbd..9f860903e 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -17,6 +17,7 @@ describe('data', () => { channels.updateGuardNodes = () => {}; channels.getItemById = () => {}; channels.createOrUpdateItem = () => {}; + channels.getSwarmNodesForPubkey = () => {}; }); afterEach(() => { @@ -88,12 +89,29 @@ describe('data', () => { describe('updateGuardNodes', () => { it('updates guard nodes', async () => { const updateGuardNodesStub = Sinon.stub(channels, 'updateGuardNodes'); - const expectedGuardNode = 'foo'; + const expectedGuardNodes = ['foo']; - await Data.updateGuardNodes([expectedGuardNode]); + const actualGuardNodes = await Data.updateGuardNodes(expectedGuardNodes); expect(updateGuardNodesStub.calledOnce).to.be.true; - expect(updateGuardNodesStub.calledWith([expectedGuardNode])).to.be.true; + expect(updateGuardNodesStub.calledWith(expectedGuardNodes)).to.be.true; + expect(expectedGuardNodes).to.deep.equal(actualGuardNodes); + }); + }); + + describe('getSwarmNodesForPubkey', () => { + it('returns swarm nodes for pubkey', async () => { + const expectedPubkey = 'test_pubkey_123'; + const expectedSwarmNodes = ['node1', 'node2', 'node3']; + + const getSwarmNodesForPubkeyStub = Sinon.stub(channels, 'getSwarmNodesForPubkey').resolves( + expectedSwarmNodes + ); + const actualSwarmNodes = await Data.getSwarmNodesForPubkey(expectedPubkey); + + expect(getSwarmNodesForPubkeyStub.calledOnce).to.be.true; + expect(getSwarmNodesForPubkeyStub.calledWith(expectedPubkey)).to.be.true; + expect(expectedSwarmNodes).to.deep.equal(actualSwarmNodes); }); }); @@ -116,12 +134,14 @@ describe('data', () => { const getItemByIdStub = Sinon.stub(channels, 'getItemById').resolves(undefined); const createOrUpdateItemStub = Sinon.stub(channels, 'createOrUpdateItem'); const storagePutStub = Sinon.stub(Storage, 'put'); - const mockSodium = { to_hex: Sinon.stub().returns('generated_hex_key'), randombytes_buf: Sinon.stub().returns(new Uint8Array(32)), } as any; - const getSodiumRendererStub = Sinon.stub(cryptoUtils, 'getSodiumRenderer').resolves(mockSodium); + + const getSodiumRendererStub = Sinon.stub(cryptoUtils, 'getSodiumRenderer').resolves( + mockSodium + ); await Data.generateAttachmentKeyIfEmpty(); @@ -131,12 +151,15 @@ describe('data', () => { expect(mockSodium.randombytes_buf.calledWith(32)).to.be.true; expect(mockSodium.to_hex.calledOnce).to.be.true; expect(createOrUpdateItemStub.calledOnce).to.be.true; - expect(createOrUpdateItemStub.calledWith({ - id: 'local_attachment_encrypted_key', - value: 'generated_hex_key', - })).to.be.true; + expect( + createOrUpdateItemStub.calledWith({ + id: 'local_attachment_encrypted_key', + value: 'generated_hex_key', + }) + ).to.be.true; expect(storagePutStub.calledOnce).to.be.true; - expect(storagePutStub.calledWith('local_attachment_encrypted_key', 'generated_hex_key')).to.be.true; + expect(storagePutStub.calledWith('local_attachment_encrypted_key', 'generated_hex_key')).to.be + .true; }); }); }); From a39720a5df7d23a1664aafa995fd1269e9a90f9e Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 14:51:30 -0600 Subject: [PATCH 03/25] fix: updateGuardNodes assertion --- ts/test/data/data_test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index 9f860903e..c072dc085 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -91,11 +91,11 @@ describe('data', () => { const updateGuardNodesStub = Sinon.stub(channels, 'updateGuardNodes'); const expectedGuardNodes = ['foo']; - const actualGuardNodes = await Data.updateGuardNodes(expectedGuardNodes); + const result = await Data.updateGuardNodes(expectedGuardNodes); expect(updateGuardNodesStub.calledOnce).to.be.true; expect(updateGuardNodesStub.calledWith(expectedGuardNodes)).to.be.true; - expect(expectedGuardNodes).to.deep.equal(actualGuardNodes); + expect(result).to.be.undefined; }); }); From 6e1762d260bcae4cc0efa3abfbfc807a52a09066 Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 15:00:34 -0600 Subject: [PATCH 04/25] chore: add unit tests for updateSwarmNodesForPubkey, clearOutAllSnodesNotInPool, saveConversation --- ts/test/data/data_test.ts | 91 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index c072dc085..e788d45b8 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -7,6 +7,8 @@ import * as dataInit from '../../data/dataInit'; import { GuardNode } from '../../data/types'; import { Storage } from '../../util/storage'; import * as cryptoUtils from '../../session/crypto'; +import { ConversationAttributes } from '../../models/conversationAttributes'; +import { SaveConversationReturn } from '../../types/sqlSharedTypes'; describe('data', () => { beforeEach(() => { @@ -18,6 +20,9 @@ describe('data', () => { channels.getItemById = () => {}; channels.createOrUpdateItem = () => {}; channels.getSwarmNodesForPubkey = () => {}; + channels.updateSwarmNodesForPubkey = () => {}; + channels.clearOutAllSnodesNotInPool = () => {}; + channels.saveConversation = () => {}; }); afterEach(() => { @@ -115,6 +120,92 @@ describe('data', () => { }); }); + describe('updateSwarmNodesForPubkey', () => { + it('updates swarm nodes for pubkey', async () => { + const updateSwarmNodesForPubkeyStub = Sinon.stub(channels, 'updateSwarmNodesForPubkey'); + const expectedPubkey = 'test_pubkey_123'; + const expectedSnodeEdKeys = ['node1', 'node2', 'node3']; + + const result = await Data.updateSwarmNodesForPubkey(expectedPubkey, expectedSnodeEdKeys); + + expect(updateSwarmNodesForPubkeyStub.calledOnce).to.be.true; + expect(updateSwarmNodesForPubkeyStub.calledWith(expectedPubkey, expectedSnodeEdKeys)).to.be + .true; + expect(result).to.be.undefined; + }); + }); + + describe('clearOutAllSnodesNotInPool', () => { + it('clears out all snodes not in pool', async () => { + const clearOutAllSnodesNotInPoolStub = Sinon.stub(channels, 'clearOutAllSnodesNotInPool'); + const expectedEdKeysOfSnodePool = ['snode1', 'snode2', 'snode3']; + + const result = await Data.clearOutAllSnodesNotInPool(expectedEdKeysOfSnodePool); + + expect(clearOutAllSnodesNotInPoolStub.calledOnce).to.be.true; + expect(clearOutAllSnodesNotInPoolStub.calledWith(expectedEdKeysOfSnodePool)).to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('saveConversation', () => { + it('saves conversation with normal data', async () => { + const conversationData: ConversationAttributes = { + id: 'test_convo_123', + active_at: 1234567890, + type: 'private', + } as ConversationAttributes; + + const expectedReturn: SaveConversationReturn = { + unreadCount: 0, + mentionedUs: false, + lastReadTimestampMessage: null, + }; + + const saveConversationStub = Sinon.stub(channels, 'saveConversation').resolves( + expectedReturn + ); + const result = await Data.saveConversation(conversationData); + + expect(saveConversationStub.calledOnce).to.be.true; + expect(saveConversationStub.calledWith(conversationData)).to.be.true; + expect(result).to.deep.equal(expectedReturn); + }); + + it('updates active_at when it is -Infinity', async () => { + const mockNow = 9876543210; + const dateNowStub = Sinon.stub(Date, 'now').returns(mockNow); + + const conversationData: ConversationAttributes = { + id: 'test_convo_123', + active_at: -Infinity, + type: 'private', + } as ConversationAttributes; + + const expectedCleanedData = { + id: 'test_convo_123', + active_at: mockNow, + type: 'private', + }; + + const expectedReturn: SaveConversationReturn = { + unreadCount: 0, + mentionedUs: false, + lastReadTimestampMessage: null, + }; + + const saveConversationStub = Sinon.stub(channels, 'saveConversation').resolves( + expectedReturn + ); + const result = await Data.saveConversation(conversationData); + + expect(saveConversationStub.calledOnce).to.be.true; + expect(saveConversationStub.calledWith(expectedCleanedData)).to.be.true; + expect(result).to.deep.equal(expectedReturn); + expect(dateNowStub.calledOnce).to.be.true; + }); + }); + describe('generateAttachmentKeyIfEmpty', () => { it('does not generate a new key when one already exists', async () => { const existingKey = { id: 'local_attachment_encrypted_key', value: 'existing_key' }; From 2dfaf6b4557bdb532fd58fde0de876b9a8a217f1 Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 15:10:51 -0600 Subject: [PATCH 05/25] chore: testing fetchConvoMemoryDetails, getConversationById, removeConversation, getAllConversations --- ts/test/data/data_test.ts | 116 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index e788d45b8..92c293c2e 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -8,6 +8,7 @@ import { GuardNode } from '../../data/types'; import { Storage } from '../../util/storage'; import * as cryptoUtils from '../../session/crypto'; import { ConversationAttributes } from '../../models/conversationAttributes'; +import { ConversationModel } from '../../models/conversation'; import { SaveConversationReturn } from '../../types/sqlSharedTypes'; describe('data', () => { @@ -23,6 +24,10 @@ describe('data', () => { channels.updateSwarmNodesForPubkey = () => {}; channels.clearOutAllSnodesNotInPool = () => {}; channels.saveConversation = () => {}; + channels.fetchConvoMemoryDetails = () => {}; + channels.getConversationById = () => {}; + channels.removeConversation = () => {}; + channels.getAllConversations = () => {}; }); afterEach(() => { @@ -253,4 +258,115 @@ describe('data', () => { .true; }); }); + + describe('fetchConvoMemoryDetails', () => { + it('fetches conversation memory details', async () => { + const expectedConvoId = 'test_convo_123'; + const expectedReturn: SaveConversationReturn = { + unreadCount: 5, + mentionedUs: true, + lastReadTimestampMessage: 1234567890, + }; + + const fetchConvoMemoryDetailsStub = Sinon.stub(channels, 'fetchConvoMemoryDetails').resolves(expectedReturn); + const result = await Data.fetchConvoMemoryDetails(expectedConvoId); + + expect(fetchConvoMemoryDetailsStub.calledOnce).to.be.true; + expect(fetchConvoMemoryDetailsStub.calledWith(expectedConvoId)).to.be.true; + expect(result).to.deep.equal(expectedReturn); + }); + }); + + describe('getConversationById', () => { + it('returns conversation model when conversation exists', async () => { + const expectedId = 'test_convo_123'; + const conversationData: ConversationAttributes = { + id: expectedId, + type: 'private', + active_at: 1234567890, + } as ConversationAttributes; + + const getConversationByIdStub = Sinon.stub(channels, 'getConversationById').resolves(conversationData); + const result = await Data.getConversationById(expectedId); + + expect(getConversationByIdStub.calledOnce).to.be.true; + expect(getConversationByIdStub.calledWith(expectedId)).to.be.true; + expect(result).to.be.instanceOf(ConversationModel); + expect(result?.get('id')).to.equal(expectedId); + }); + + it('returns undefined when conversation does not exist', async () => { + const expectedId = 'non_existent_convo'; + + const getConversationByIdStub = Sinon.stub(channels, 'getConversationById').resolves(undefined); + const result = await Data.getConversationById(expectedId); + + expect(getConversationByIdStub.calledOnce).to.be.true; + expect(getConversationByIdStub.calledWith(expectedId)).to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('removeConversation', () => { + it('removes conversation when it exists', async () => { + const expectedId = 'test_convo_123'; + const conversationData: ConversationAttributes = { + id: expectedId, + type: 'private', + active_at: 1234567890, + } as ConversationAttributes; + + const getConversationByIdStub = Sinon.stub(channels, 'getConversationById').resolves(conversationData); + const removeConversationStub = Sinon.stub(channels, 'removeConversation'); + + const result = await Data.removeConversation(expectedId); + + expect(getConversationByIdStub.calledOnce).to.be.true; + expect(getConversationByIdStub.calledWith(expectedId)).to.be.true; + expect(removeConversationStub.calledOnce).to.be.true; + expect(removeConversationStub.calledWith(expectedId)).to.be.true; + expect(result).to.be.undefined; + }); + + it('does nothing when conversation does not exist', async () => { + const expectedId = 'non_existent_convo'; + + const getConversationByIdStub = Sinon.stub(channels, 'getConversationById').resolves(undefined); + const removeConversationStub = Sinon.stub(channels, 'removeConversation'); + + const result = await Data.removeConversation(expectedId); + + expect(getConversationByIdStub.calledOnce).to.be.true; + expect(getConversationByIdStub.calledWith(expectedId)).to.be.true; + expect(removeConversationStub.called).to.be.false; + expect(result).to.be.undefined; + }); + }); + + describe('getAllConversations', () => { + it('returns array of conversation models', async () => { + const conversationsData: Array = [ + { + id: 'convo_1', + type: 'private', + active_at: 1234567890, + } as ConversationAttributes, + { + id: 'convo_2', + type: 'group', + active_at: 1234567891, + } as ConversationAttributes, + ]; + + const getAllConversationsStub = Sinon.stub(channels, 'getAllConversations').resolves(conversationsData); + const result = await Data.getAllConversations(); + + expect(getAllConversationsStub.calledOnce).to.be.true; + expect(result).to.have.length(2); + expect(result[0]).to.be.instanceOf(ConversationModel); + expect(result[1]).to.be.instanceOf(ConversationModel); + expect(result[0].get('id')).to.equal('convo_1'); + expect(result[1].get('id')).to.equal('convo_2'); + }); + }); }); From 061a36c943c487f2ec59dae4f57b1bad4f885257 Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 15:26:34 -0600 Subject: [PATCH 06/25] chore: testing getPubkeysInPublicConversation, searchConversations, searchMessages --- ts/test/data/data_test.ts | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index 92c293c2e..fb493f3bc 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -28,6 +28,9 @@ describe('data', () => { channels.getConversationById = () => {}; channels.removeConversation = () => {}; channels.getAllConversations = () => {}; + channels.getPubkeysInPublicConversation = () => {}; + channels.searchConversations = () => {}; + channels.searchMessages = () => {}; }); afterEach(() => { @@ -369,4 +372,61 @@ describe('data', () => { expect(result[1].get('id')).to.equal('convo_2'); }); }); + + describe('getPubkeysInPublicConversation', () => { + it('returns pubkeys for public conversation', async () => { + const expectedId = 'public_convo_123'; + const expectedPubkeys = ['pubkey1', 'pubkey2', 'pubkey3']; + + const getPubkeysInPublicConversationStub = Sinon.stub(channels, 'getPubkeysInPublicConversation').resolves(expectedPubkeys); + const result = await Data.getPubkeysInPublicConversation(expectedId); + + expect(getPubkeysInPublicConversationStub.calledOnce).to.be.true; + expect(getPubkeysInPublicConversationStub.calledWith(expectedId)).to.be.true; + expect(result).to.deep.equal(expectedPubkeys); + }); + }); + + describe('searchConversations', () => { + it('returns search results for conversations', async () => { + const expectedQuery = 'test search'; + const expectedResults = [ + { id: 'convo_1', name: 'Test Conversation 1' }, + { id: 'convo_2', name: 'Test Search Result' }, + ]; + + const searchConversationsStub = Sinon.stub(channels, 'searchConversations').resolves(expectedResults); + const result = await Data.searchConversations(expectedQuery); + + expect(searchConversationsStub.calledOnce).to.be.true; + expect(searchConversationsStub.calledWith(expectedQuery)).to.be.true; + expect(result).to.deep.equal(expectedResults); + }); + }); + + describe('searchMessages', () => { + it('returns unique search results for messages', async () => { + const expectedQuery = 'test search'; + const expectedLimit = 10; + const messagesWithDuplicates = [ + { id: 'msg_1', content: 'Test message 1' }, + { id: 'msg_2', content: 'Test search result' }, + { id: 'msg_1', content: 'Test message 1' }, // duplicate + { id: 'msg_3', content: 'Another test message' }, + ]; + const expectedUniqueResults = [ + { id: 'msg_1', content: 'Test message 1' }, + { id: 'msg_2', content: 'Test search result' }, + { id: 'msg_3', content: 'Another test message' }, + ]; + + const searchMessagesStub = Sinon.stub(channels, 'searchMessages').resolves(messagesWithDuplicates); + const result = await Data.searchMessages(expectedQuery, expectedLimit); + + expect(searchMessagesStub.calledOnce).to.be.true; + expect(searchMessagesStub.calledWith(expectedQuery, expectedLimit)).to.be.true; + expect(result).to.deep.equal(expectedUniqueResults); + expect(result).to.have.length(3); // Verify duplicates were removed + }); + }); }); From 41b91e0dd81e30d785e274f30a61e6fe1a46b4d0 Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 15:29:07 -0600 Subject: [PATCH 07/25] chore: testing searchMessagesInConversation, cleanSeenMessages, cleanLastHashes --- ts/test/data/data_test.ts | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index fb493f3bc..80fca71f9 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -31,6 +31,9 @@ describe('data', () => { channels.getPubkeysInPublicConversation = () => {}; channels.searchConversations = () => {}; channels.searchMessages = () => {}; + channels.searchMessagesInConversation = () => {}; + channels.cleanSeenMessages = () => {}; + channels.cleanLastHashes = () => {}; }); afterEach(() => { @@ -429,4 +432,45 @@ describe('data', () => { expect(result).to.have.length(3); // Verify duplicates were removed }); }); + + describe('searchMessagesInConversation', () => { + it('returns search results for messages in conversation', async () => { + const expectedQuery = 'test search'; + const expectedConversationId = 'convo_123'; + const expectedLimit = 5; + const expectedMessages = [ + { id: 'msg_1', content: 'Test message in conversation', conversationId: 'convo_123' }, + { id: 'msg_2', content: 'Another test search result', conversationId: 'convo_123' }, + ]; + + const searchMessagesInConversationStub = Sinon.stub(channels, 'searchMessagesInConversation').resolves(expectedMessages); + const result = await Data.searchMessagesInConversation(expectedQuery, expectedConversationId, expectedLimit); + + expect(searchMessagesInConversationStub.calledOnce).to.be.true; + expect(searchMessagesInConversationStub.calledWith(expectedQuery, expectedConversationId, expectedLimit)).to.be.true; + expect(result).to.deep.equal(expectedMessages); + }); + }); + + describe('cleanSeenMessages', () => { + it('cleans seen messages', async () => { + const cleanSeenMessagesStub = Sinon.stub(channels, 'cleanSeenMessages'); + + const result = await Data.cleanSeenMessages(); + + expect(cleanSeenMessagesStub.calledOnce).to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('cleanLastHashes', () => { + it('cleans last hashes', async () => { + const cleanLastHashesStub = Sinon.stub(channels, 'cleanLastHashes'); + + const result = await Data.cleanLastHashes(); + + expect(cleanLastHashesStub.calledOnce).to.be.true; + expect(result).to.be.undefined; + }); + }); }); From 8604fde12db350fe4929f7abdd26d15b61486f1c Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 15:33:47 -0600 Subject: [PATCH 08/25] chore: testing simple channel tests, formatting updates --- ts/test/data/data_test.ts | 131 ++++++++++++++++++++++++++++++++++---- 1 file changed, 118 insertions(+), 13 deletions(-) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index 80fca71f9..d8f2f3da9 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -9,7 +9,11 @@ import { Storage } from '../../util/storage'; import * as cryptoUtils from '../../session/crypto'; import { ConversationAttributes } from '../../models/conversationAttributes'; import { ConversationModel } from '../../models/conversation'; -import { SaveConversationReturn } from '../../types/sqlSharedTypes'; +import { + SaveConversationReturn, + SaveSeenMessageHash, + UpdateLastHashType, +} from '../../types/sqlSharedTypes'; describe('data', () => { beforeEach(() => { @@ -34,6 +38,10 @@ describe('data', () => { channels.searchMessagesInConversation = () => {}; channels.cleanSeenMessages = () => {}; channels.cleanLastHashes = () => {}; + channels.saveSeenMessageHashes = () => {}; + channels.clearLastHashesForConvoId = () => {}; + channels.emptySeenMessageHashesForConversation = () => {}; + channels.updateLastHash = () => {}; }); afterEach(() => { @@ -274,7 +282,9 @@ describe('data', () => { lastReadTimestampMessage: 1234567890, }; - const fetchConvoMemoryDetailsStub = Sinon.stub(channels, 'fetchConvoMemoryDetails').resolves(expectedReturn); + const fetchConvoMemoryDetailsStub = Sinon.stub(channels, 'fetchConvoMemoryDetails').resolves( + expectedReturn + ); const result = await Data.fetchConvoMemoryDetails(expectedConvoId); expect(fetchConvoMemoryDetailsStub.calledOnce).to.be.true; @@ -292,7 +302,9 @@ describe('data', () => { active_at: 1234567890, } as ConversationAttributes; - const getConversationByIdStub = Sinon.stub(channels, 'getConversationById').resolves(conversationData); + const getConversationByIdStub = Sinon.stub(channels, 'getConversationById').resolves( + conversationData + ); const result = await Data.getConversationById(expectedId); expect(getConversationByIdStub.calledOnce).to.be.true; @@ -304,7 +316,9 @@ describe('data', () => { it('returns undefined when conversation does not exist', async () => { const expectedId = 'non_existent_convo'; - const getConversationByIdStub = Sinon.stub(channels, 'getConversationById').resolves(undefined); + const getConversationByIdStub = Sinon.stub(channels, 'getConversationById').resolves( + undefined + ); const result = await Data.getConversationById(expectedId); expect(getConversationByIdStub.calledOnce).to.be.true; @@ -322,7 +336,9 @@ describe('data', () => { active_at: 1234567890, } as ConversationAttributes; - const getConversationByIdStub = Sinon.stub(channels, 'getConversationById').resolves(conversationData); + const getConversationByIdStub = Sinon.stub(channels, 'getConversationById').resolves( + conversationData + ); const removeConversationStub = Sinon.stub(channels, 'removeConversation'); const result = await Data.removeConversation(expectedId); @@ -337,7 +353,9 @@ describe('data', () => { it('does nothing when conversation does not exist', async () => { const expectedId = 'non_existent_convo'; - const getConversationByIdStub = Sinon.stub(channels, 'getConversationById').resolves(undefined); + const getConversationByIdStub = Sinon.stub(channels, 'getConversationById').resolves( + undefined + ); const removeConversationStub = Sinon.stub(channels, 'removeConversation'); const result = await Data.removeConversation(expectedId); @@ -364,7 +382,9 @@ describe('data', () => { } as ConversationAttributes, ]; - const getAllConversationsStub = Sinon.stub(channels, 'getAllConversations').resolves(conversationsData); + const getAllConversationsStub = Sinon.stub(channels, 'getAllConversations').resolves( + conversationsData + ); const result = await Data.getAllConversations(); expect(getAllConversationsStub.calledOnce).to.be.true; @@ -381,7 +401,10 @@ describe('data', () => { const expectedId = 'public_convo_123'; const expectedPubkeys = ['pubkey1', 'pubkey2', 'pubkey3']; - const getPubkeysInPublicConversationStub = Sinon.stub(channels, 'getPubkeysInPublicConversation').resolves(expectedPubkeys); + const getPubkeysInPublicConversationStub = Sinon.stub( + channels, + 'getPubkeysInPublicConversation' + ).resolves(expectedPubkeys); const result = await Data.getPubkeysInPublicConversation(expectedId); expect(getPubkeysInPublicConversationStub.calledOnce).to.be.true; @@ -398,7 +421,9 @@ describe('data', () => { { id: 'convo_2', name: 'Test Search Result' }, ]; - const searchConversationsStub = Sinon.stub(channels, 'searchConversations').resolves(expectedResults); + const searchConversationsStub = Sinon.stub(channels, 'searchConversations').resolves( + expectedResults + ); const result = await Data.searchConversations(expectedQuery); expect(searchConversationsStub.calledOnce).to.be.true; @@ -423,7 +448,9 @@ describe('data', () => { { id: 'msg_3', content: 'Another test message' }, ]; - const searchMessagesStub = Sinon.stub(channels, 'searchMessages').resolves(messagesWithDuplicates); + const searchMessagesStub = Sinon.stub(channels, 'searchMessages').resolves( + messagesWithDuplicates + ); const result = await Data.searchMessages(expectedQuery, expectedLimit); expect(searchMessagesStub.calledOnce).to.be.true; @@ -443,11 +470,24 @@ describe('data', () => { { id: 'msg_2', content: 'Another test search result', conversationId: 'convo_123' }, ]; - const searchMessagesInConversationStub = Sinon.stub(channels, 'searchMessagesInConversation').resolves(expectedMessages); - const result = await Data.searchMessagesInConversation(expectedQuery, expectedConversationId, expectedLimit); + const searchMessagesInConversationStub = Sinon.stub( + channels, + 'searchMessagesInConversation' + ).resolves(expectedMessages); + const result = await Data.searchMessagesInConversation( + expectedQuery, + expectedConversationId, + expectedLimit + ); expect(searchMessagesInConversationStub.calledOnce).to.be.true; - expect(searchMessagesInConversationStub.calledWith(expectedQuery, expectedConversationId, expectedLimit)).to.be.true; + expect( + searchMessagesInConversationStub.calledWith( + expectedQuery, + expectedConversationId, + expectedLimit + ) + ).to.be.true; expect(result).to.deep.equal(expectedMessages); }); }); @@ -473,4 +513,69 @@ describe('data', () => { expect(result).to.be.undefined; }); }); + + describe('saveSeenMessageHashes', () => { + it('saves seen message hashes', async () => { + const expectedData: Array = [ + { hash: 'hash1', conversationId: 'convo1', expiresAt: 123 }, + { hash: 'hash2', conversationId: 'convo2', expiresAt: 123 }, + ]; + + const saveSeenMessageHashesStub = Sinon.stub(channels, 'saveSeenMessageHashes'); + const result = await Data.saveSeenMessageHashes(expectedData); + + expect(saveSeenMessageHashesStub.calledOnce).to.be.true; + expect(saveSeenMessageHashesStub.calledWith(expectedData)).to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('clearLastHashesForConvoId', () => { + it('clears last hashes for conversation id', async () => { + const expectedConversationId = 'test_convo_123'; + + const clearLastHashesForConvoIdStub = Sinon.stub(channels, 'clearLastHashesForConvoId'); + const result = await Data.clearLastHashesForConvoId(expectedConversationId); + + expect(clearLastHashesForConvoIdStub.calledOnce).to.be.true; + expect(clearLastHashesForConvoIdStub.calledWith(expectedConversationId)).to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('emptySeenMessageHashesForConversation', () => { + it('empties seen message hashes for conversation', async () => { + const expectedConversationId = 'test_convo_123'; + + const emptySeenMessageHashesForConversationStub = Sinon.stub( + channels, + 'emptySeenMessageHashesForConversation' + ); + const result = await Data.emptySeenMessageHashesForConversation(expectedConversationId); + + expect(emptySeenMessageHashesForConversationStub.calledOnce).to.be.true; + expect(emptySeenMessageHashesForConversationStub.calledWith(expectedConversationId)).to.be + .true; + expect(result).to.be.undefined; + }); + }); + + describe('updateLastHash', () => { + it('updates last hash', async () => { + const expectedData: UpdateLastHashType = { + convoId: 'test_convo_123', + snode: 'test_snode_ed25519', + hash: 'test_hash_value', + expiresAt: 1234567890, + namespace: 321, + }; + + const updateLastHashStub = Sinon.stub(channels, 'updateLastHash'); + const result = await Data.updateLastHash(expectedData); + + expect(updateLastHashStub.calledOnce).to.be.true; + expect(updateLastHashStub.calledWith(expectedData)).to.be.true; + expect(result).to.be.undefined; + }); + }); }); From 82fff2f92eb6616e7e0791d482b96a4ef2e6d40f Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 15:39:54 -0600 Subject: [PATCH 09/25] chore: add several more unit tests --- ts/test/data/data_test.ts | 267 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index d8f2f3da9..10abea917 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -7,8 +7,11 @@ import * as dataInit from '../../data/dataInit'; import { GuardNode } from '../../data/types'; import { Storage } from '../../util/storage'; import * as cryptoUtils from '../../session/crypto'; +import { DisappearingMessages } from '../../session/disappearing_messages'; import { ConversationAttributes } from '../../models/conversationAttributes'; import { ConversationModel } from '../../models/conversation'; +import { MessageModel } from '../../models/message'; +import { MessageAttributes } from '../../models/messageType'; import { SaveConversationReturn, SaveSeenMessageHash, @@ -42,6 +45,14 @@ describe('data', () => { channels.clearLastHashesForConvoId = () => {}; channels.emptySeenMessageHashesForConversation = () => {}; channels.updateLastHash = () => {}; + channels.saveMessage = () => {}; + channels.saveMessages = () => {}; + channels.cleanUpExpirationTimerUpdateHistory = () => {}; + channels.removeMessage = () => {}; + channels.removeMessagesByIds = () => {}; + channels.removeAllMessagesInConversationSentBefore = () => {}; + channels.getAllMessagesWithAttachmentsInConversationSentBefore = () => {}; + channels.getMessageById = () => {}; }); afterEach(() => { @@ -578,4 +589,260 @@ describe('data', () => { expect(result).to.be.undefined; }); }); + + describe('saveMessage', () => { + it('saves message and updates expiring messages check', async () => { + const expectedMessageId = 'msg_123'; + const messageData: MessageAttributes = { + id: expectedMessageId, + body: 'Test message body', + conversationId: 'convo_123', + sent_at: 1234567890, + } as MessageAttributes; + + const saveMessageStub = Sinon.stub(channels, 'saveMessage').resolves(expectedMessageId); + const updateExpiringMessagesCheckStub = Sinon.stub( + DisappearingMessages, + 'updateExpiringMessagesCheck' + ); + + const result = await Data.saveMessage(messageData); + + expect(saveMessageStub.calledOnce).to.be.true; + expect(saveMessageStub.calledWith(messageData)).to.be.true; + expect(updateExpiringMessagesCheckStub.calledOnce).to.be.true; + expect(result).to.equal(expectedMessageId); + }); + }); + + describe('saveMessages', () => { + it('saves array of messages', async () => { + const messagesData: Array = [ + { + id: 'msg_1', + body: 'First test message', + conversationId: 'convo_123', + sent_at: 1234567890, + } as MessageAttributes, + { + id: 'msg_2', + body: 'Second test message', + conversationId: 'convo_456', + sent_at: 1234567891, + } as MessageAttributes, + ]; + + const saveMessagesStub = Sinon.stub(channels, 'saveMessages'); + const result = await Data.saveMessages(messagesData); + + expect(saveMessagesStub.calledOnce).to.be.true; + expect(saveMessagesStub.calledWith(messagesData)).to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('cleanUpExpirationTimerUpdateHistory', () => { + it('cleans up expiration timer update history for private conversation', async () => { + const expectedConversationId = 'private_convo_123'; + const expectedIsPrivate = true; + const expectedRemovedIds = ['timer_msg_1', 'timer_msg_2']; + + const cleanUpExpirationTimerUpdateHistoryStub = Sinon.stub( + channels, + 'cleanUpExpirationTimerUpdateHistory' + ).resolves(expectedRemovedIds); + + const result = await Data.cleanUpExpirationTimerUpdateHistory( + expectedConversationId, + expectedIsPrivate + ); + + expect(cleanUpExpirationTimerUpdateHistoryStub.calledOnce).to.be.true; + expect( + cleanUpExpirationTimerUpdateHistoryStub.calledWith( + expectedConversationId, + expectedIsPrivate + ) + ).to.be.true; + expect(result).to.deep.equal(expectedRemovedIds); + }); + + it('cleans up expiration timer update history for group conversation', async () => { + const expectedConversationId = 'group_convo_456'; + const expectedIsPrivate = false; + const expectedRemovedIds = ['timer_msg_3']; + + const cleanUpExpirationTimerUpdateHistoryStub = Sinon.stub( + channels, + 'cleanUpExpirationTimerUpdateHistory' + ).resolves(expectedRemovedIds); + + const result = await Data.cleanUpExpirationTimerUpdateHistory( + expectedConversationId, + expectedIsPrivate + ); + + expect(cleanUpExpirationTimerUpdateHistoryStub.calledOnce).to.be.true; + expect( + cleanUpExpirationTimerUpdateHistoryStub.calledWith( + expectedConversationId, + expectedIsPrivate + ) + ).to.be.true; + expect(result).to.deep.equal(expectedRemovedIds); + }); + }); + + describe('removeMessage', () => { + it('removes message when it exists', async () => { + const expectedMessageId = 'msg_123'; + const mockMessage = new MessageModel({ + id: expectedMessageId, + body: 'Test message', + source: 'source', + type: 'incoming', + conversationId: '321', + }); + mockMessage.cleanup = Sinon.stub(); + + const getMessageByIdStub = Sinon.stub(channels, 'getMessageById').resolves({ + id: expectedMessageId, + body: 'Test message', + }); + const removeMessageStub = Sinon.stub(channels, 'removeMessage'); + + const result = await Data.removeMessage(expectedMessageId); + + expect(getMessageByIdStub.calledOnce).to.be.true; + expect(getMessageByIdStub.calledWith(expectedMessageId)).to.be.true; + expect(removeMessageStub.calledOnce).to.be.true; + expect(removeMessageStub.calledWith(expectedMessageId)).to.be.true; + expect(result).to.be.undefined; + }); + + it('does nothing when message does not exist', async () => { + const expectedMessageId = 'non_existent_msg'; + + const getMessageByIdStub = Sinon.stub(channels, 'getMessageById').resolves(null); + const removeMessageStub = Sinon.stub(channels, 'removeMessage'); + + const result = await Data.removeMessage(expectedMessageId); + + expect(getMessageByIdStub.calledOnce).to.be.true; + expect(getMessageByIdStub.calledWith(expectedMessageId)).to.be.true; + expect(removeMessageStub.called).to.be.false; + expect(result).to.be.undefined; + }); + }); + + describe('removeMessagesByIds', () => { + it('removes multiple messages by IDs without cleanup', async () => { + const expectedMessageIds = ['msg_1', 'msg_2', 'msg_3']; + + const removeMessagesByIdsStub = Sinon.stub(channels, 'removeMessagesByIds'); + const result = await Data.removeMessagesByIds(expectedMessageIds); + + expect(removeMessagesByIdsStub.calledOnce).to.be.true; + expect(removeMessagesByIdsStub.calledWith(expectedMessageIds)).to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('removeAllMessagesInConversationSentBefore', () => { + it('removes messages sent before specified timestamp', async () => { + const expectedArgs = { + deleteBeforeSeconds: 1640995200, + conversationId: 'convo_123' as any, + }; + const expectedRemovedIds = ['msg_1', 'msg_2', 'msg_3']; + + const removeAllMessagesInConversationSentBeforeStub = Sinon.stub( + channels, + 'removeAllMessagesInConversationSentBefore' + ).resolves(expectedRemovedIds); + + const result = await Data.removeAllMessagesInConversationSentBefore(expectedArgs); + + expect(removeAllMessagesInConversationSentBeforeStub.calledOnce).to.be.true; + expect(removeAllMessagesInConversationSentBeforeStub.calledWith(expectedArgs)).to.be.true; + expect(result).to.deep.equal(expectedRemovedIds); + }); + }); + + describe('getAllMessagesWithAttachmentsInConversationSentBefore', () => { + it('returns message models with attachments sent before timestamp', async () => { + const expectedArgs = { + deleteAttachBeforeSeconds: 1640995200, + conversationId: 'convo_456' as any, + }; + const mockMessageAttrs = [ + { + id: 'msg_with_attach_1', + body: 'Message with attachment', + conversationId: 'convo_456', + attachments: [{ fileName: 'test.jpg' }], + }, + { + id: 'msg_with_attach_2', + body: 'Another message with attachment', + conversationId: 'convo_456', + attachments: [{ fileName: 'document.pdf' }], + }, + ]; + + const getAllMessagesWithAttachmentsInConversationSentBeforeStub = Sinon.stub( + channels, + 'getAllMessagesWithAttachmentsInConversationSentBefore' + ).resolves(mockMessageAttrs); + + const result = await Data.getAllMessagesWithAttachmentsInConversationSentBefore(expectedArgs); + + expect(getAllMessagesWithAttachmentsInConversationSentBeforeStub.calledOnce).to.be.true; + expect(getAllMessagesWithAttachmentsInConversationSentBeforeStub.calledWith(expectedArgs)).to + .be.true; + expect(result).to.have.length(2); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[1]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('msg_with_attach_1'); + expect(result[1].get('id')).to.equal('msg_with_attach_2'); + }); + + it('returns empty array when no messages found', async () => { + const expectedArgs = { + deleteAttachBeforeSeconds: 1640995200, + conversationId: 'empty_convo' as any, + }; + + const getAllMessagesWithAttachmentsInConversationSentBeforeStub = Sinon.stub( + channels, + 'getAllMessagesWithAttachmentsInConversationSentBefore' + ).resolves(null); + + const result = await Data.getAllMessagesWithAttachmentsInConversationSentBefore(expectedArgs); + + expect(getAllMessagesWithAttachmentsInConversationSentBeforeStub.calledOnce).to.be.true; + expect(getAllMessagesWithAttachmentsInConversationSentBeforeStub.calledWith(expectedArgs)).to + .be.true; + expect(result).to.deep.equal([]); + }); + + it('returns empty array when empty array is returned', async () => { + const expectedArgs = { + deleteAttachBeforeSeconds: 1640995200, + conversationId: 'empty_convo' as any, + }; + + const getAllMessagesWithAttachmentsInConversationSentBeforeStub = Sinon.stub( + channels, + 'getAllMessagesWithAttachmentsInConversationSentBefore' + ).resolves([]); + + const result = await Data.getAllMessagesWithAttachmentsInConversationSentBefore(expectedArgs); + + expect(getAllMessagesWithAttachmentsInConversationSentBeforeStub.calledOnce).to.be.true; + expect(getAllMessagesWithAttachmentsInConversationSentBeforeStub.calledWith(expectedArgs)).to + .be.true; + expect(result).to.deep.equal([]); + }); + }); }); From f49706f8a56f9b5c8c059863ba0267141ae7c317 Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 15:51:48 -0600 Subject: [PATCH 10/25] chore: fix test stubs --- ts/test/data/data_test.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index 10abea917..e822b90a7 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe } from 'mocha'; import Sinon from 'sinon'; import { expect } from 'chai'; +import { PubkeyType } from 'libsession_util_nodejs'; import { Data } from '../../data/data'; import { channels } from '../../data/channels'; import * as dataInit from '../../data/dataInit'; @@ -11,12 +12,13 @@ import { DisappearingMessages } from '../../session/disappearing_messages'; import { ConversationAttributes } from '../../models/conversationAttributes'; import { ConversationModel } from '../../models/conversation'; import { MessageModel } from '../../models/message'; -import { MessageAttributes } from '../../models/messageType'; +import { MessageAttributes, MessageAttributesOptionals } from '../../models/messageType'; import { SaveConversationReturn, SaveSeenMessageHash, UpdateLastHashType, } from '../../types/sqlSharedTypes'; +import { UserUtils } from '../../session/utils'; describe('data', () => { beforeEach(() => { @@ -696,19 +698,18 @@ describe('data', () => { describe('removeMessage', () => { it('removes message when it exists', async () => { const expectedMessageId = 'msg_123'; - const mockMessage = new MessageModel({ + const message: MessageAttributesOptionals = { id: expectedMessageId, body: 'Test message', source: 'source', type: 'incoming', conversationId: '321', - }); + }; + + const mockMessage = new MessageModel(message); mockMessage.cleanup = Sinon.stub(); - const getMessageByIdStub = Sinon.stub(channels, 'getMessageById').resolves({ - id: expectedMessageId, - body: 'Test message', - }); + const getMessageByIdStub = Sinon.stub(channels, 'getMessageById').resolves(message); const removeMessageStub = Sinon.stub(channels, 'removeMessage'); const result = await Data.removeMessage(expectedMessageId); @@ -775,18 +776,22 @@ describe('data', () => { deleteAttachBeforeSeconds: 1640995200, conversationId: 'convo_456' as any, }; - const mockMessageAttrs = [ + const mockMessageAttrs: Array = [ { id: 'msg_with_attach_1', body: 'Message with attachment', conversationId: 'convo_456', attachments: [{ fileName: 'test.jpg' }], + source: 'foo', + type: 'incoming', }, { id: 'msg_with_attach_2', body: 'Another message with attachment', conversationId: 'convo_456', attachments: [{ fileName: 'document.pdf' }], + source: 'bar', + type: 'outgoing', }, ]; @@ -795,6 +800,9 @@ describe('data', () => { 'getAllMessagesWithAttachmentsInConversationSentBefore' ).resolves(mockMessageAttrs); + const pubkey: PubkeyType = '05foo'; + Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(pubkey); + const result = await Data.getAllMessagesWithAttachmentsInConversationSentBefore(expectedArgs); expect(getAllMessagesWithAttachmentsInConversationSentBeforeStub.calledOnce).to.be.true; From 65c497f79ca20d08dbca4179502aaa6a414da32e Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 15:54:23 -0600 Subject: [PATCH 11/25] chore: add channel mocking to its own method --- ts/test/data/data_test.ts | 70 +++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index e822b90a7..ad9376147 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -22,39 +22,7 @@ import { UserUtils } from '../../session/utils'; describe('data', () => { beforeEach(() => { - channels.close = () => {}; - channels.removeDB = () => {}; - channels.getPasswordHash = () => {}; - channels.getGuardNodes = () => {}; - channels.updateGuardNodes = () => {}; - channels.getItemById = () => {}; - channels.createOrUpdateItem = () => {}; - channels.getSwarmNodesForPubkey = () => {}; - channels.updateSwarmNodesForPubkey = () => {}; - channels.clearOutAllSnodesNotInPool = () => {}; - channels.saveConversation = () => {}; - channels.fetchConvoMemoryDetails = () => {}; - channels.getConversationById = () => {}; - channels.removeConversation = () => {}; - channels.getAllConversations = () => {}; - channels.getPubkeysInPublicConversation = () => {}; - channels.searchConversations = () => {}; - channels.searchMessages = () => {}; - channels.searchMessagesInConversation = () => {}; - channels.cleanSeenMessages = () => {}; - channels.cleanLastHashes = () => {}; - channels.saveSeenMessageHashes = () => {}; - channels.clearLastHashesForConvoId = () => {}; - channels.emptySeenMessageHashesForConversation = () => {}; - channels.updateLastHash = () => {}; - channels.saveMessage = () => {}; - channels.saveMessages = () => {}; - channels.cleanUpExpirationTimerUpdateHistory = () => {}; - channels.removeMessage = () => {}; - channels.removeMessagesByIds = () => {}; - channels.removeAllMessagesInConversationSentBefore = () => {}; - channels.getAllMessagesWithAttachmentsInConversationSentBefore = () => {}; - channels.getMessageById = () => {}; + mockChannels(); }); afterEach(() => { @@ -854,3 +822,39 @@ describe('data', () => { }); }); }); + +function mockChannels(): void { + channels.close = () => {}; + channels.removeDB = () => {}; + channels.getPasswordHash = () => {}; + channels.getGuardNodes = () => {}; + channels.updateGuardNodes = () => {}; + channels.getItemById = () => {}; + channels.createOrUpdateItem = () => {}; + channels.getSwarmNodesForPubkey = () => {}; + channels.updateSwarmNodesForPubkey = () => {}; + channels.clearOutAllSnodesNotInPool = () => {}; + channels.saveConversation = () => {}; + channels.fetchConvoMemoryDetails = () => {}; + channels.getConversationById = () => {}; + channels.removeConversation = () => {}; + channels.getAllConversations = () => {}; + channels.getPubkeysInPublicConversation = () => {}; + channels.searchConversations = () => {}; + channels.searchMessages = () => {}; + channels.searchMessagesInConversation = () => {}; + channels.cleanSeenMessages = () => {}; + channels.cleanLastHashes = () => {}; + channels.saveSeenMessageHashes = () => {}; + channels.clearLastHashesForConvoId = () => {}; + channels.emptySeenMessageHashesForConversation = () => {}; + channels.updateLastHash = () => {}; + channels.saveMessage = () => {}; + channels.saveMessages = () => {}; + channels.cleanUpExpirationTimerUpdateHistory = () => {}; + channels.removeMessage = () => {}; + channels.removeMessagesByIds = () => {}; + channels.removeAllMessagesInConversationSentBefore = () => {}; + channels.getAllMessagesWithAttachmentsInConversationSentBefore = () => {}; + channels.getMessageById = () => {}; +} From 7b0761243a883a63f0d9301e78e27444f61a513b Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 16:02:08 -0600 Subject: [PATCH 12/25] chore: add several more unit tests --- ts/test/data/data_test.ts | 379 +++++++++++++++++++++++++++++++++++++- 1 file changed, 376 insertions(+), 3 deletions(-) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index ad9376147..c14e9a792 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -14,6 +14,7 @@ import { ConversationModel } from '../../models/conversation'; import { MessageModel } from '../../models/message'; import { MessageAttributes, MessageAttributesOptionals } from '../../models/messageType'; import { + MsgDuplicateSearchOpenGroup, SaveConversationReturn, SaveSeenMessageHash, UpdateLastHashType, @@ -23,6 +24,9 @@ import { UserUtils } from '../../session/utils'; describe('data', () => { beforeEach(() => { mockChannels(); + + const pubkey: PubkeyType = '05foo'; + Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(pubkey); }); afterEach(() => { @@ -768,9 +772,6 @@ describe('data', () => { 'getAllMessagesWithAttachmentsInConversationSentBefore' ).resolves(mockMessageAttrs); - const pubkey: PubkeyType = '05foo'; - Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(pubkey); - const result = await Data.getAllMessagesWithAttachmentsInConversationSentBefore(expectedArgs); expect(getAllMessagesWithAttachmentsInConversationSentBeforeStub.calledOnce).to.be.true; @@ -821,6 +822,373 @@ describe('data', () => { expect(result).to.deep.equal([]); }); }); + + describe('getMessageIdsFromServerIds', () => { + it('returns message IDs from server IDs', async () => { + const expectedServerIds = ['server_1', 'server_2', 'server_3']; + const expectedConversationId = 'convo_123'; + const expectedMessageIds = ['msg_1', 'msg_2', 'msg_3']; + + const getMessageIdsFromServerIdsStub = Sinon.stub( + channels, + 'getMessageIdsFromServerIds' + ).resolves(expectedMessageIds); + const result = await Data.getMessageIdsFromServerIds( + expectedServerIds, + expectedConversationId + ); + + expect(getMessageIdsFromServerIdsStub.calledOnce).to.be.true; + expect(getMessageIdsFromServerIdsStub.calledWith(expectedServerIds, expectedConversationId)) + .to.be.true; + expect(result).to.deep.equal(expectedMessageIds); + }); + + it('returns undefined when no messages found', async () => { + const expectedServerIds = [123, 456]; + const expectedConversationId = 'empty_convo'; + + const getMessageIdsFromServerIdsStub = Sinon.stub( + channels, + 'getMessageIdsFromServerIds' + ).resolves(undefined); + const result = await Data.getMessageIdsFromServerIds( + expectedServerIds, + expectedConversationId + ); + + expect(getMessageIdsFromServerIdsStub.calledOnce).to.be.true; + expect(getMessageIdsFromServerIdsStub.calledWith(expectedServerIds, expectedConversationId)) + .to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('getMessageById', () => { + it('returns message model when message exists', async () => { + const expectedMessageId = 'msg_123'; + const messageData: MessageAttributesOptionals = { + id: expectedMessageId, + body: 'Test message body', + conversationId: 'convo_123', + source: 'source_123', + type: 'incoming', + }; + + const getMessageByIdStub = Sinon.stub(channels, 'getMessageById').resolves(messageData); + const result = await Data.getMessageById(expectedMessageId); + + expect(getMessageByIdStub.calledOnce).to.be.true; + expect(getMessageByIdStub.calledWith(expectedMessageId)).to.be.true; + expect(result).to.be.instanceOf(MessageModel); + expect(result?.get('id')).to.equal(expectedMessageId); + }); + + it('returns null when message does not exist', async () => { + const expectedMessageId = 'non_existent_msg'; + + const getMessageByIdStub = Sinon.stub(channels, 'getMessageById').resolves(null); + const result = await Data.getMessageById(expectedMessageId); + + expect(getMessageByIdStub.calledOnce).to.be.true; + expect(getMessageByIdStub.calledWith(expectedMessageId)).to.be.true; + expect(result).to.be.null; + }); + + it('sets skipTimerInit when parameter is true', async () => { + const expectedMessageId = 'msg_123'; + const messageData: MessageAttributesOptionals = { + id: expectedMessageId, + body: 'Test message body', + conversationId: 'convo_123', + source: 'source_123', + type: 'incoming', + }; + + const getMessageByIdStub = Sinon.stub(channels, 'getMessageById').resolves(messageData); + const result = await Data.getMessageById(expectedMessageId, true); + + expect(getMessageByIdStub.calledOnce).to.be.true; + expect(getMessageByIdStub.calledWith(expectedMessageId)).to.be.true; + expect(result).to.be.instanceOf(MessageModel); + expect(result?.get('id')).to.equal(expectedMessageId); + }); + }); + + describe('getMessagesById', () => { + it('returns array of message models', async () => { + const expectedMessageIds = ['msg_1', 'msg_2', 'msg_3']; + const messagesData: Array = [ + { + id: 'msg_1', + body: 'First message', + conversationId: 'convo_123', + source: 'source_1', + type: 'incoming', + }, + { + id: 'msg_2', + body: 'Second message', + conversationId: 'convo_123', + source: 'source_2', + type: 'outgoing', + }, + { + id: 'msg_3', + body: 'Third message', + conversationId: 'convo_456', + source: 'source_3', + type: 'incoming', + }, + ]; + + const getMessagesByIdStub = Sinon.stub(channels, 'getMessagesById').resolves(messagesData); + const result = await Data.getMessagesById(expectedMessageIds); + + expect(getMessagesByIdStub.calledOnce).to.be.true; + expect(getMessagesByIdStub.calledWith(expectedMessageIds)).to.be.true; + expect(result).to.have.length(3); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[1]).to.be.instanceOf(MessageModel); + expect(result[2]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('msg_1'); + expect(result[1].get('id')).to.equal('msg_2'); + expect(result[2].get('id')).to.equal('msg_3'); + }); + + it('returns empty array when no messages found', async () => { + const expectedMessageIds = ['non_existent_1', 'non_existent_2']; + + const getMessagesByIdStub = Sinon.stub(channels, 'getMessagesById').resolves(null); + const result = await Data.getMessagesById(expectedMessageIds); + + expect(getMessagesByIdStub.calledOnce).to.be.true; + expect(getMessagesByIdStub.calledWith(expectedMessageIds)).to.be.true; + expect(result).to.deep.equal([]); + }); + + it('returns empty array when empty array is returned', async () => { + const expectedMessageIds = ['msg_1', 'msg_2']; + + const getMessagesByIdStub = Sinon.stub(channels, 'getMessagesById').resolves([]); + const result = await Data.getMessagesById(expectedMessageIds); + + expect(getMessagesByIdStub.calledOnce).to.be.true; + expect(getMessagesByIdStub.calledWith(expectedMessageIds)).to.be.true; + expect(result).to.deep.equal([]); + }); + }); + + describe('getMessageByServerId', () => { + it('returns message model when message exists', async () => { + const expectedConversationId = 'convo_123'; + const expectedServerId = 456; + const messageData: MessageAttributesOptionals = { + id: 'msg_123', + body: 'Message by server ID', + conversationId: expectedConversationId, + serverId: expectedServerId, + source: 'source_123', + type: 'incoming', + }; + + const getMessageByServerIdStub = Sinon.stub(channels, 'getMessageByServerId').resolves( + messageData + ); + const result = await Data.getMessageByServerId(expectedConversationId, expectedServerId); + + expect(getMessageByServerIdStub.calledOnce).to.be.true; + expect(getMessageByServerIdStub.calledWith(expectedConversationId, expectedServerId)).to.be + .true; + expect(result).to.be.instanceOf(MessageModel); + expect(result?.get('id')).to.equal('msg_123'); + expect(result?.get('serverId')).to.equal(expectedServerId); + }); + + it('returns null when message does not exist', async () => { + const expectedConversationId = 'empty_convo'; + const expectedServerId = 999; + + const getMessageByServerIdStub = Sinon.stub(channels, 'getMessageByServerId').resolves(null); + const result = await Data.getMessageByServerId(expectedConversationId, expectedServerId); + + expect(getMessageByServerIdStub.calledOnce).to.be.true; + expect(getMessageByServerIdStub.calledWith(expectedConversationId, expectedServerId)).to.be + .true; + expect(result).to.be.null; + }); + + it('sets skipTimerInit when parameter is true', async () => { + const expectedConversationId = 'convo_123'; + const expectedServerId = 789; + const messageData: MessageAttributesOptionals = { + id: 'msg_456', + body: 'Message with skip timer', + conversationId: expectedConversationId, + serverId: expectedServerId, + source: 'source_456', + type: 'outgoing', + }; + + const getMessageByServerIdStub = Sinon.stub(channels, 'getMessageByServerId').resolves( + messageData + ); + const result = await Data.getMessageByServerId( + expectedConversationId, + expectedServerId, + true + ); + + expect(getMessageByServerIdStub.calledOnce).to.be.true; + expect(getMessageByServerIdStub.calledWith(expectedConversationId, expectedServerId)).to.be + .true; + expect(result).to.be.instanceOf(MessageModel); + expect(result?.get('id')).to.equal('msg_456'); + }); + }); + + describe('filterAlreadyFetchedOpengroupMessage', () => { + it('filters already fetched opengroup messages', async () => { + const inputMsgDetails: MsgDuplicateSearchOpenGroup = [ + { + sender: 'sender_1', + serverTimestamp: 1234567800, + }, + { + sender: 'sender_2', + serverTimestamp: 1234567800, + }, + ]; + + const filteredMsgDetails: MsgDuplicateSearchOpenGroup = [ + { + sender: 'sender_2', + serverTimestamp: 1234567800, + }, + ]; + + const filterAlreadyFetchedOpengroupMessageStub = Sinon.stub( + channels, + 'filterAlreadyFetchedOpengroupMessage' + ).resolves(filteredMsgDetails); + + const result = await Data.filterAlreadyFetchedOpengroupMessage(inputMsgDetails); + + expect(filterAlreadyFetchedOpengroupMessageStub.calledOnce).to.be.true; + expect(filterAlreadyFetchedOpengroupMessageStub.calledWith(inputMsgDetails)).to.be.true; + expect(result).to.deep.equal(filteredMsgDetails); + expect(result).to.have.length(1); + }); + + it('returns empty array when all messages are already fetched', async () => { + const inputMsgDetails: MsgDuplicateSearchOpenGroup = [ + { + sender: 'sender_old', + serverTimestamp: 1234567800, + }, + ]; + + const filterAlreadyFetchedOpengroupMessageStub = Sinon.stub( + channels, + 'filterAlreadyFetchedOpengroupMessage' + ).resolves(null); + + const result = await Data.filterAlreadyFetchedOpengroupMessage(inputMsgDetails); + + expect(filterAlreadyFetchedOpengroupMessageStub.calledOnce).to.be.true; + expect(filterAlreadyFetchedOpengroupMessageStub.calledWith(inputMsgDetails)).to.be.true; + expect(result).to.deep.equal([]); + }); + }); + + describe('getMessagesBySenderAndSentAt', () => { + it('returns message models for sender and timestamp matches', async () => { + const propsList = [ + { + source: 'sender_1', + timestamp: 1234567890, + }, + { + source: 'sender_2', + timestamp: 1234567891, + }, + ]; + + const messagesData: Array = [ + { + id: 'msg_1', + body: 'Message from sender 1', + conversationId: 'convo_123', + source: 'sender_1', + sent_at: 1234567890, + type: 'incoming', + }, + { + id: 'msg_2', + body: 'Message from sender 2', + conversationId: 'convo_456', + source: 'sender_2', + sent_at: 1234567891, + type: 'incoming', + }, + ]; + + const getMessagesBySenderAndSentAtStub = Sinon.stub( + channels, + 'getMessagesBySenderAndSentAt' + ).resolves(messagesData); + + const result = await Data.getMessagesBySenderAndSentAt(propsList); + + expect(getMessagesBySenderAndSentAtStub.calledOnce).to.be.true; + expect(getMessagesBySenderAndSentAtStub.calledWith(propsList)).to.be.true; + expect(result).to.have.length(2); + expect(result?.[0]).to.be.instanceOf(MessageModel); + expect(result?.[1]).to.be.instanceOf(MessageModel); + expect(result?.[0].get('id')).to.equal('msg_1'); + expect(result?.[1].get('id')).to.equal('msg_2'); + }); + + it('returns null when no messages match', async () => { + const propsList = [ + { + source: 'unknown_sender', + timestamp: 9999999999, + }, + ]; + + const getMessagesBySenderAndSentAtStub = Sinon.stub( + channels, + 'getMessagesBySenderAndSentAt' + ).resolves([]); + + const result = await Data.getMessagesBySenderAndSentAt(propsList); + + expect(getMessagesBySenderAndSentAtStub.calledOnce).to.be.true; + expect(getMessagesBySenderAndSentAtStub.calledWith(propsList)).to.be.true; + expect(result).to.be.null; + }); + + it('returns null when result is not an array', async () => { + const propsList = [ + { + source: 'sender_test', + timestamp: 1111111111, + }, + ]; + + const getMessagesBySenderAndSentAtStub = Sinon.stub( + channels, + 'getMessagesBySenderAndSentAt' + ).resolves(null); + + const result = await Data.getMessagesBySenderAndSentAt(propsList); + + expect(getMessagesBySenderAndSentAtStub.calledOnce).to.be.true; + expect(getMessagesBySenderAndSentAtStub.calledWith(propsList)).to.be.true; + expect(result).to.be.null; + }); + }); }); function mockChannels(): void { @@ -857,4 +1225,9 @@ function mockChannels(): void { channels.removeAllMessagesInConversationSentBefore = () => {}; channels.getAllMessagesWithAttachmentsInConversationSentBefore = () => {}; channels.getMessageById = () => {}; + channels.getMessageIdsFromServerIds = () => {}; + channels.getMessagesById = () => {}; + channels.getMessageByServerId = () => {}; + channels.filterAlreadyFetchedOpengroupMessage = () => {}; + channels.getMessagesBySenderAndSentAt = () => {}; } From bc13a4b3a0674394c2fed5e71ab53cac982f6eda Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 16:08:52 -0600 Subject: [PATCH 13/25] fix: improperly typed skipTimerInit variable --- ts/data/data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/data/data.ts b/ts/data/data.ts index 24fbc6c71..9d949d121 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -395,7 +395,7 @@ async function getMessagesByConversation( skipTimerInit = false, returnQuotes = false, messageId = null, - }: { skipTimerInit?: false; returnQuotes?: boolean; messageId: string | null } + }: { skipTimerInit?: boolean; returnQuotes?: boolean; messageId: string | null } ): Promise<{ messages: Array; quotes: Array }> { const { messages, quotes } = await channels.getMessagesByConversation(conversationId, { messageId, From 058d45c9922d8ee241aa9eb8d40a71791aba9872 Mon Sep 17 00:00:00 2001 From: andyo Date: Sat, 9 Aug 2025 16:09:13 -0600 Subject: [PATCH 14/25] chore: add several more unit tests --- ts/test/data/data_test.ts | 327 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index c14e9a792..167d477d7 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -13,6 +13,7 @@ import { ConversationAttributes } from '../../models/conversationAttributes'; import { ConversationModel } from '../../models/conversation'; import { MessageModel } from '../../models/message'; import { MessageAttributes, MessageAttributesOptionals } from '../../models/messageType'; +import { Quote } from '../../receiver/types'; import { MsgDuplicateSearchOpenGroup, SaveConversationReturn, @@ -1189,6 +1190,326 @@ describe('data', () => { expect(result).to.be.null; }); }); + + describe('getUnreadByConversation', () => { + it('returns unread messages for conversation', async () => { + const expectedConversationId = 'convo_123'; + const expectedSentBeforeTimestamp = 1234567890; + const mockMessageAttrs: Array = [ + { + id: 'unread_msg_1', + body: 'First unread message', + conversationId: expectedConversationId, + source: 'sender_1', + type: 'incoming', + read_by: undefined, + }, + { + id: 'unread_msg_2', + body: 'Second unread message', + conversationId: expectedConversationId, + source: 'sender_2', + type: 'incoming', + read_by: ['bobloblaw'], + }, + ]; + + const getUnreadByConversationStub = Sinon.stub(channels, 'getUnreadByConversation').resolves( + mockMessageAttrs + ); + const result = await Data.getUnreadByConversation( + expectedConversationId, + expectedSentBeforeTimestamp + ); + + expect(getUnreadByConversationStub.calledOnce).to.be.true; + expect( + getUnreadByConversationStub.calledWith(expectedConversationId, expectedSentBeforeTimestamp) + ).to.be.true; + expect(result).to.have.length(2); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[1]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('unread_msg_1'); + expect(result[1].get('id')).to.equal('unread_msg_2'); + }); + + it('returns empty array when no unread messages found', async () => { + const expectedConversationId = 'empty_convo'; + const expectedSentBeforeTimestamp = 1234567890; + + const getUnreadByConversationStub = Sinon.stub(channels, 'getUnreadByConversation').resolves( + [] + ); + const result = await Data.getUnreadByConversation( + expectedConversationId, + expectedSentBeforeTimestamp + ); + + expect(getUnreadByConversationStub.calledOnce).to.be.true; + expect( + getUnreadByConversationStub.calledWith(expectedConversationId, expectedSentBeforeTimestamp) + ).to.be.true; + expect(result).to.deep.equal([]); + }); + }); + + describe('getUnreadDisappearingByConversation', () => { + it('returns unread disappearing messages for conversation', async () => { + const expectedConversationId = 'convo_456'; + const expectedSentBeforeTimestamp = 1234567891; + const mockMessageAttrs: Array = [ + { + id: 'disappearing_msg_1', + body: 'First disappearing message', + conversationId: expectedConversationId, + source: 'sender_1', + type: 'incoming', + expireTimer: 300, + }, + { + id: 'disappearing_msg_2', + body: 'Second disappearing message', + conversationId: expectedConversationId, + source: 'sender_2', + type: 'incoming', + expireTimer: 600, + }, + ]; + + const getUnreadDisappearingByConversationStub = Sinon.stub( + channels, + 'getUnreadDisappearingByConversation' + ).resolves(mockMessageAttrs); + const result = await Data.getUnreadDisappearingByConversation( + expectedConversationId, + expectedSentBeforeTimestamp + ); + + expect(getUnreadDisappearingByConversationStub.calledOnce).to.be.true; + expect( + getUnreadDisappearingByConversationStub.calledWith( + expectedConversationId, + expectedSentBeforeTimestamp + ) + ).to.be.true; + expect(result).to.have.length(2); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[1]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('disappearing_msg_1'); + expect(result[1].get('id')).to.equal('disappearing_msg_2'); + }); + }); + + describe('markAllAsReadByConversationNoExpiration', () => { + it('marks all messages as read and returns message IDs', async () => { + const expectedConversationId = 'convo_789'; + const expectedReturnMessagesUpdated = true; + const expectedMessageIds = [123, 456, 789]; + + const markAllAsReadByConversationNoExpirationStub = Sinon.stub( + channels, + 'markAllAsReadByConversationNoExpiration' + ).resolves(expectedMessageIds); + const result = await Data.markAllAsReadByConversationNoExpiration( + expectedConversationId, + expectedReturnMessagesUpdated + ); + + expect(markAllAsReadByConversationNoExpirationStub.calledOnce).to.be.true; + expect( + markAllAsReadByConversationNoExpirationStub.calledWith( + expectedConversationId, + expectedReturnMessagesUpdated + ) + ).to.be.true; + expect(result).to.deep.equal(expectedMessageIds); + }); + + it('marks all messages as read without returning updated messages', async () => { + const expectedConversationId = 'convo_999'; + const expectedReturnMessagesUpdated = false; + const expectedMessageIds: Array = []; + + const markAllAsReadByConversationNoExpirationStub = Sinon.stub( + channels, + 'markAllAsReadByConversationNoExpiration' + ).resolves(expectedMessageIds); + const result = await Data.markAllAsReadByConversationNoExpiration( + expectedConversationId, + expectedReturnMessagesUpdated + ); + + expect(markAllAsReadByConversationNoExpirationStub.calledOnce).to.be.true; + expect( + markAllAsReadByConversationNoExpirationStub.calledWith( + expectedConversationId, + expectedReturnMessagesUpdated + ) + ).to.be.true; + expect(result).to.deep.equal(expectedMessageIds); + }); + }); + + describe('getUnreadCountByConversation', () => { + it('returns unread message count for conversation', async () => { + const expectedConversationId = 'convo_count_123'; + const expectedUnreadCount = 5; + + const getUnreadCountByConversationStub = Sinon.stub( + channels, + 'getUnreadCountByConversation' + ).resolves(expectedUnreadCount); + const result = await Data.getUnreadCountByConversation(expectedConversationId); + + expect(getUnreadCountByConversationStub.calledOnce).to.be.true; + expect(getUnreadCountByConversationStub.calledWith(expectedConversationId)).to.be.true; + expect(result).to.equal(expectedUnreadCount); + }); + + it('returns zero when no unread messages', async () => { + const expectedConversationId = 'read_convo_123'; + const expectedUnreadCount = 0; + + const getUnreadCountByConversationStub = Sinon.stub( + channels, + 'getUnreadCountByConversation' + ).resolves(expectedUnreadCount); + const result = await Data.getUnreadCountByConversation(expectedConversationId); + + expect(getUnreadCountByConversationStub.calledOnce).to.be.true; + expect(getUnreadCountByConversationStub.calledWith(expectedConversationId)).to.be.true; + expect(result).to.equal(expectedUnreadCount); + }); + }); + + describe('getMessageCountByType', () => { + it('returns message count for specific type', async () => { + const expectedConversationId = 'type_convo_123'; + const expectedType = 'incoming' as any; + const expectedCount = 10; + + const getMessageCountByTypeStub = Sinon.stub(channels, 'getMessageCountByType').resolves( + expectedCount + ); + const result = await Data.getMessageCountByType(expectedConversationId, expectedType); + + expect(getMessageCountByTypeStub.calledOnce).to.be.true; + expect(getMessageCountByTypeStub.calledWith(expectedConversationId, expectedType)).to.be.true; + expect(result).to.equal(expectedCount); + }); + + it('returns total message count when no type specified', async () => { + const expectedConversationId = 'all_convo_456'; + const expectedCount = 25; + + const getMessageCountByTypeStub = Sinon.stub(channels, 'getMessageCountByType').resolves( + expectedCount + ); + const result = await Data.getMessageCountByType(expectedConversationId); + + expect(getMessageCountByTypeStub.calledOnce).to.be.true; + expect(getMessageCountByTypeStub.calledWith(expectedConversationId, undefined)).to.be.true; + expect(result).to.equal(expectedCount); + }); + }); + + describe('getMessagesByConversation', () => { + it('returns messages and quotes for conversation with all options', async () => { + const expectedConversationId = 'full_convo_123'; + const expectedOptions = { + skipTimerInit: false, + returnQuotes: true, + messageId: 'anchor_msg_123', + }; + const mockMessages: Array = [ + { + id: 'msg_1', + body: 'First message', + conversationId: expectedConversationId, + source: 'sender_1', + type: 'incoming', + }, + { + id: 'msg_2', + body: 'Second message', + conversationId: expectedConversationId, + source: 'sender_2', + type: 'outgoing', + }, + ]; + const mockQuotes: Array = [ + { + id: 1234567890, + author: 'quote_author_1', + text: 'Quoted message text', + } as Quote, + ]; + + const getMessagesByConversationStub = Sinon.stub( + channels, + 'getMessagesByConversation' + ).resolves({ + messages: mockMessages, + quotes: mockQuotes, + }); + + const result = await Data.getMessagesByConversation(expectedConversationId, expectedOptions); + + expect(getMessagesByConversationStub.calledOnce).to.be.true; + expect( + getMessagesByConversationStub.calledWith(expectedConversationId, { + messageId: expectedOptions.messageId, + returnQuotes: expectedOptions.returnQuotes, + }) + ).to.be.true; + expect(result.messages).to.have.length(2); + expect(result.messages[0]).to.be.instanceOf(MessageModel); + expect(result.messages[1]).to.be.instanceOf(MessageModel); + expect(result.messages[0].get('id')).to.equal('msg_1'); + expect(result.messages[1].get('id')).to.equal('msg_2'); + expect(result.quotes).to.deep.equal(mockQuotes); + }); + + it('returns messages with skipTimerInit when specified', async () => { + const expectedConversationId = 'skip_timer_convo'; + const expectedOptions = { + skipTimerInit: true, + returnQuotes: false, + messageId: null, + }; + const mockMessages: Array = [ + { + id: 'timer_msg_1', + body: 'Message with skip timer', + conversationId: expectedConversationId, + source: 'sender_1', + type: 'incoming', + }, + ]; + + const getMessagesByConversationStub = Sinon.stub( + channels, + 'getMessagesByConversation' + ).resolves({ + messages: mockMessages, + quotes: [], + }); + + const result = await Data.getMessagesByConversation(expectedConversationId, expectedOptions); + + expect(getMessagesByConversationStub.calledOnce).to.be.true; + expect( + getMessagesByConversationStub.calledWith(expectedConversationId, { + messageId: null, + returnQuotes: false, + }) + ).to.be.true; + expect(result.messages).to.have.length(1); + expect(result.messages[0]).to.be.instanceOf(MessageModel); + expect(result.messages[0].get('id')).to.equal('timer_msg_1'); + expect(result.quotes).to.deep.equal([]); + }); + }); }); function mockChannels(): void { @@ -1230,4 +1551,10 @@ function mockChannels(): void { channels.getMessageByServerId = () => {}; channels.filterAlreadyFetchedOpengroupMessage = () => {}; channels.getMessagesBySenderAndSentAt = () => {}; + channels.getUnreadByConversation = () => {}; + channels.getUnreadDisappearingByConversation = () => {}; + channels.markAllAsReadByConversationNoExpiration = () => {}; + channels.getUnreadCountByConversation = () => {}; + channels.getMessageCountByType = () => {}; + channels.getMessagesByConversation = () => {}; } From c6870a123ab6fe4093487d1562bfc48896d3b136 Mon Sep 17 00:00:00 2001 From: andyo Date: Sun, 10 Aug 2025 10:07:46 -0600 Subject: [PATCH 15/25] chore: add unit tests for getLastMessagesByConversation --- ts/test/data/data_test.ts | 76 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index 167d477d7..d6411e40c 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -1510,6 +1510,81 @@ describe('data', () => { expect(result.quotes).to.deep.equal([]); }); }); + + describe('getLastMessagesByConversation', () => { + it('returns last messages for conversation with limit', async () => { + const expectedConversationId = 'last_convo_123'; + const expectedLimit = 2; + const mockMessages: Array = [ + { + id: 'last_msg_1', + body: 'Last message one', + conversationId: expectedConversationId, + source: 'sender_1', + type: 'incoming', + }, + { + id: 'last_msg_2', + body: 'Last message two', + conversationId: expectedConversationId, + source: 'sender_2', + type: 'outgoing', + }, + ]; + + const getLastMessagesByConversationStub = Sinon.stub( + channels, + 'getLastMessagesByConversation' + ).resolves(mockMessages); + + const result = await Data.getLastMessagesByConversation( + expectedConversationId, + expectedLimit, + false + ); + + expect(getLastMessagesByConversationStub.calledOnce).to.be.true; + expect(getLastMessagesByConversationStub.calledWith(expectedConversationId, expectedLimit)).to + .be.true; + expect(result).to.have.length(2); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[1]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('last_msg_1'); + expect(result[1].get('id')).to.equal('last_msg_2'); + }); + + it('returns last messages with skipTimerInit when specified', async () => { + const expectedConversationId = 'last_convo_skip_timer'; + const expectedLimit = 1; + const mockMessages: Array = [ + { + id: 'last_timer_msg_1', + body: 'Last message with skip timer', + conversationId: expectedConversationId, + source: 'sender_1', + type: 'incoming', + }, + ]; + + const getLastMessagesByConversationStub = Sinon.stub( + channels, + 'getLastMessagesByConversation' + ).resolves(mockMessages); + + const result = await Data.getLastMessagesByConversation( + expectedConversationId, + expectedLimit, + true + ); + + expect(getLastMessagesByConversationStub.calledOnce).to.be.true; + expect(getLastMessagesByConversationStub.calledWith(expectedConversationId, expectedLimit)).to + .be.true; + expect(result).to.have.length(1); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('last_timer_msg_1'); + }); + }); }); function mockChannels(): void { @@ -1557,4 +1632,5 @@ function mockChannels(): void { channels.getUnreadCountByConversation = () => {}; channels.getMessageCountByType = () => {}; channels.getMessagesByConversation = () => {}; + channels.getLastMessagesByConversation = () => {}; } From 346c285410012efd116699767304b6d971fb262f Mon Sep 17 00:00:00 2001 From: andyo Date: Sun, 10 Aug 2025 10:13:04 -0600 Subject: [PATCH 16/25] chore: add more unit tests for data class --- ts/test/data/data_test.ts | 132 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index d6411e40c..facc15a98 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -1585,6 +1585,137 @@ describe('data', () => { expect(result[0].get('id')).to.equal('last_timer_msg_1'); }); }); + + describe('getLastMessageIdInConversation', () => { + it('returns the last message id when a message exists', async () => { + const expectedConversationId = 'last_msg_id_convo'; + const mockMessages: Array = [ + { + id: 'last_msg_id_1', + body: 'Only message', + conversationId: expectedConversationId, + source: 'sender_1', + type: 'incoming', + }, + ]; + + const getLastMessagesByConversationStub = Sinon.stub( + channels, + 'getLastMessagesByConversation' + ).resolves(mockMessages); + + const result = await Data.getLastMessageIdInConversation(expectedConversationId); + + expect(getLastMessagesByConversationStub.calledOnce).to.be.true; + expect( + getLastMessagesByConversationStub.calledWith(expectedConversationId, 1) + ).to.be.true; + expect(result).to.equal('last_msg_id_1'); + }); + + it('returns null when no messages exist', async () => { + const expectedConversationId = 'last_msg_id_none'; + + const getLastMessagesByConversationStub = Sinon.stub( + channels, + 'getLastMessagesByConversation' + ).resolves([]); + + const result = await Data.getLastMessageIdInConversation(expectedConversationId); + + expect(getLastMessagesByConversationStub.calledOnce).to.be.true; + expect( + getLastMessagesByConversationStub.calledWith(expectedConversationId, 1) + ).to.be.true; + expect(result).to.equal(null); + }); + }); + + describe('getLastMessageInConversation', () => { + it('returns the last message model when a message exists', async () => { + const expectedConversationId = 'last_msg_convo'; + const mockMessages: Array = [ + { + id: 'last_msg_model_1', + body: 'Only message', + conversationId: expectedConversationId, + source: 'sender_1', + type: 'incoming', + }, + ]; + + const getLastMessagesByConversationStub = Sinon.stub( + channels, + 'getLastMessagesByConversation' + ).resolves(mockMessages); + + const result = await Data.getLastMessageInConversation(expectedConversationId); + + expect(getLastMessagesByConversationStub.calledOnce).to.be.true; + expect( + getLastMessagesByConversationStub.calledWith(expectedConversationId, 1) + ).to.be.true; + expect(result).to.be.instanceOf(MessageModel); + expect((result as MessageModel).get('id')).to.equal('last_msg_model_1'); + }); + + it('returns null when no messages exist', async () => { + const expectedConversationId = 'last_msg_convo_none'; + + const getLastMessagesByConversationStub = Sinon.stub( + channels, + 'getLastMessagesByConversation' + ).resolves([]); + + const result = await Data.getLastMessageInConversation(expectedConversationId); + + expect(getLastMessagesByConversationStub.calledOnce).to.be.true; + expect( + getLastMessagesByConversationStub.calledWith(expectedConversationId, 1) + ).to.be.true; + expect(result).to.equal(null); + }); + }); + + describe('getOldestMessageInConversation', () => { + it('returns the oldest message model when a message exists', async () => { + const expectedConversationId = 'oldest_msg_convo'; + const mockMessages: Array = [ + { + id: 'oldest_msg_1', + body: 'Oldest message', + conversationId: expectedConversationId, + source: 'sender_1', + type: 'incoming', + }, + ]; + + const getOldestMessageInConversationStub = Sinon.stub( + channels, + 'getOldestMessageInConversation' + ).resolves(mockMessages); + + const result = await Data.getOldestMessageInConversation(expectedConversationId); + + expect(getOldestMessageInConversationStub.calledOnce).to.be.true; + expect(result).to.be.instanceOf(MessageModel); + expect((result as MessageModel).get('id')).to.equal('oldest_msg_1'); + }); + + it('returns null when no messages exist', async () => { + const expectedConversationId = 'oldest_msg_convo_none'; + + const getOldestMessageInConversationStub = Sinon.stub( + channels, + 'getOldestMessageInConversation' + ).resolves([]); + + const result = await Data.getOldestMessageInConversation(expectedConversationId); + + expect(getOldestMessageInConversationStub.calledOnce).to.be.true; + expect(result).to.equal(null); + }); + }); }); function mockChannels(): void { @@ -1633,4 +1764,5 @@ function mockChannels(): void { channels.getMessageCountByType = () => {}; channels.getMessagesByConversation = () => {}; channels.getLastMessagesByConversation = () => {}; + channels.getOldestMessageInConversation = () => {}; } From 15a839af11404237e506464b42d7f1d99f704c28 Mon Sep 17 00:00:00 2001 From: andyo Date: Sun, 10 Aug 2025 10:26:37 -0600 Subject: [PATCH 17/25] chore: add more tests and remove usages of any --- ts/test/data/data_test.ts | 230 +++----------------------------------- 1 file changed, 15 insertions(+), 215 deletions(-) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index facc15a98..287dca534 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe } from 'mocha'; import Sinon from 'sinon'; import { expect } from 'chai'; -import { PubkeyType } from 'libsession_util_nodejs'; +import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs'; import { Data } from '../../data/data'; import { channels } from '../../data/channels'; import * as dataInit from '../../data/dataInit'; @@ -12,7 +12,11 @@ import { DisappearingMessages } from '../../session/disappearing_messages'; import { ConversationAttributes } from '../../models/conversationAttributes'; import { ConversationModel } from '../../models/conversation'; import { MessageModel } from '../../models/message'; -import { MessageAttributes, MessageAttributesOptionals } from '../../models/messageType'; +import { + MessageAttributes, + MessageAttributesOptionals, + MessageDirection, +} from '../../models/messageType'; import { Quote } from '../../receiver/types'; import { MsgDuplicateSearchOpenGroup, @@ -724,9 +728,10 @@ describe('data', () => { describe('removeAllMessagesInConversationSentBefore', () => { it('removes messages sent before specified timestamp', async () => { + const conversationId: GroupPubkeyType = '03foo'; const expectedArgs = { deleteBeforeSeconds: 1640995200, - conversationId: 'convo_123' as any, + conversationId, }; const expectedRemovedIds = ['msg_1', 'msg_2', 'msg_3']; @@ -745,9 +750,10 @@ describe('data', () => { describe('getAllMessagesWithAttachmentsInConversationSentBefore', () => { it('returns message models with attachments sent before timestamp', async () => { + const conversationId: GroupPubkeyType = '03convo_456'; const expectedArgs = { deleteAttachBeforeSeconds: 1640995200, - conversationId: 'convo_456' as any, + conversationId, }; const mockMessageAttrs: Array = [ { @@ -786,9 +792,10 @@ describe('data', () => { }); it('returns empty array when no messages found', async () => { + const conversationId: GroupPubkeyType = '03convo_456'; const expectedArgs = { deleteAttachBeforeSeconds: 1640995200, - conversationId: 'empty_convo' as any, + conversationId, }; const getAllMessagesWithAttachmentsInConversationSentBeforeStub = Sinon.stub( @@ -805,9 +812,10 @@ describe('data', () => { }); it('returns empty array when empty array is returned', async () => { + const conversationId: GroupPubkeyType = '03convo_456'; const expectedArgs = { deleteAttachBeforeSeconds: 1640995200, - conversationId: 'empty_convo' as any, + conversationId, }; const getAllMessagesWithAttachmentsInConversationSentBeforeStub = Sinon.stub( @@ -1385,7 +1393,7 @@ describe('data', () => { describe('getMessageCountByType', () => { it('returns message count for specific type', async () => { const expectedConversationId = 'type_convo_123'; - const expectedType = 'incoming' as any; + const expectedType = MessageDirection.incoming; const expectedCount = 10; const getMessageCountByTypeStub = Sinon.stub(channels, 'getMessageCountByType').resolves( @@ -1510,212 +1518,6 @@ describe('data', () => { expect(result.quotes).to.deep.equal([]); }); }); - - describe('getLastMessagesByConversation', () => { - it('returns last messages for conversation with limit', async () => { - const expectedConversationId = 'last_convo_123'; - const expectedLimit = 2; - const mockMessages: Array = [ - { - id: 'last_msg_1', - body: 'Last message one', - conversationId: expectedConversationId, - source: 'sender_1', - type: 'incoming', - }, - { - id: 'last_msg_2', - body: 'Last message two', - conversationId: expectedConversationId, - source: 'sender_2', - type: 'outgoing', - }, - ]; - - const getLastMessagesByConversationStub = Sinon.stub( - channels, - 'getLastMessagesByConversation' - ).resolves(mockMessages); - - const result = await Data.getLastMessagesByConversation( - expectedConversationId, - expectedLimit, - false - ); - - expect(getLastMessagesByConversationStub.calledOnce).to.be.true; - expect(getLastMessagesByConversationStub.calledWith(expectedConversationId, expectedLimit)).to - .be.true; - expect(result).to.have.length(2); - expect(result[0]).to.be.instanceOf(MessageModel); - expect(result[1]).to.be.instanceOf(MessageModel); - expect(result[0].get('id')).to.equal('last_msg_1'); - expect(result[1].get('id')).to.equal('last_msg_2'); - }); - - it('returns last messages with skipTimerInit when specified', async () => { - const expectedConversationId = 'last_convo_skip_timer'; - const expectedLimit = 1; - const mockMessages: Array = [ - { - id: 'last_timer_msg_1', - body: 'Last message with skip timer', - conversationId: expectedConversationId, - source: 'sender_1', - type: 'incoming', - }, - ]; - - const getLastMessagesByConversationStub = Sinon.stub( - channels, - 'getLastMessagesByConversation' - ).resolves(mockMessages); - - const result = await Data.getLastMessagesByConversation( - expectedConversationId, - expectedLimit, - true - ); - - expect(getLastMessagesByConversationStub.calledOnce).to.be.true; - expect(getLastMessagesByConversationStub.calledWith(expectedConversationId, expectedLimit)).to - .be.true; - expect(result).to.have.length(1); - expect(result[0]).to.be.instanceOf(MessageModel); - expect(result[0].get('id')).to.equal('last_timer_msg_1'); - }); - }); - - describe('getLastMessageIdInConversation', () => { - it('returns the last message id when a message exists', async () => { - const expectedConversationId = 'last_msg_id_convo'; - const mockMessages: Array = [ - { - id: 'last_msg_id_1', - body: 'Only message', - conversationId: expectedConversationId, - source: 'sender_1', - type: 'incoming', - }, - ]; - - const getLastMessagesByConversationStub = Sinon.stub( - channels, - 'getLastMessagesByConversation' - ).resolves(mockMessages); - - const result = await Data.getLastMessageIdInConversation(expectedConversationId); - - expect(getLastMessagesByConversationStub.calledOnce).to.be.true; - expect( - getLastMessagesByConversationStub.calledWith(expectedConversationId, 1) - ).to.be.true; - expect(result).to.equal('last_msg_id_1'); - }); - - it('returns null when no messages exist', async () => { - const expectedConversationId = 'last_msg_id_none'; - - const getLastMessagesByConversationStub = Sinon.stub( - channels, - 'getLastMessagesByConversation' - ).resolves([]); - - const result = await Data.getLastMessageIdInConversation(expectedConversationId); - - expect(getLastMessagesByConversationStub.calledOnce).to.be.true; - expect( - getLastMessagesByConversationStub.calledWith(expectedConversationId, 1) - ).to.be.true; - expect(result).to.equal(null); - }); - }); - - describe('getLastMessageInConversation', () => { - it('returns the last message model when a message exists', async () => { - const expectedConversationId = 'last_msg_convo'; - const mockMessages: Array = [ - { - id: 'last_msg_model_1', - body: 'Only message', - conversationId: expectedConversationId, - source: 'sender_1', - type: 'incoming', - }, - ]; - - const getLastMessagesByConversationStub = Sinon.stub( - channels, - 'getLastMessagesByConversation' - ).resolves(mockMessages); - - const result = await Data.getLastMessageInConversation(expectedConversationId); - - expect(getLastMessagesByConversationStub.calledOnce).to.be.true; - expect( - getLastMessagesByConversationStub.calledWith(expectedConversationId, 1) - ).to.be.true; - expect(result).to.be.instanceOf(MessageModel); - expect((result as MessageModel).get('id')).to.equal('last_msg_model_1'); - }); - - it('returns null when no messages exist', async () => { - const expectedConversationId = 'last_msg_convo_none'; - - const getLastMessagesByConversationStub = Sinon.stub( - channels, - 'getLastMessagesByConversation' - ).resolves([]); - - const result = await Data.getLastMessageInConversation(expectedConversationId); - - expect(getLastMessagesByConversationStub.calledOnce).to.be.true; - expect( - getLastMessagesByConversationStub.calledWith(expectedConversationId, 1) - ).to.be.true; - expect(result).to.equal(null); - }); - }); - - describe('getOldestMessageInConversation', () => { - it('returns the oldest message model when a message exists', async () => { - const expectedConversationId = 'oldest_msg_convo'; - const mockMessages: Array = [ - { - id: 'oldest_msg_1', - body: 'Oldest message', - conversationId: expectedConversationId, - source: 'sender_1', - type: 'incoming', - }, - ]; - - const getOldestMessageInConversationStub = Sinon.stub( - channels, - 'getOldestMessageInConversation' - ).resolves(mockMessages); - - const result = await Data.getOldestMessageInConversation(expectedConversationId); - - expect(getOldestMessageInConversationStub.calledOnce).to.be.true; - expect(result).to.be.instanceOf(MessageModel); - expect((result as MessageModel).get('id')).to.equal('oldest_msg_1'); - }); - - it('returns null when no messages exist', async () => { - const expectedConversationId = 'oldest_msg_convo_none'; - - const getOldestMessageInConversationStub = Sinon.stub( - channels, - 'getOldestMessageInConversation' - ).resolves([]); - - const result = await Data.getOldestMessageInConversation(expectedConversationId); - - expect(getOldestMessageInConversationStub.calledOnce).to.be.true; - expect(result).to.equal(null); - }); - }); }); function mockChannels(): void { @@ -1763,6 +1565,4 @@ function mockChannels(): void { channels.getUnreadCountByConversation = () => {}; channels.getMessageCountByType = () => {}; channels.getMessagesByConversation = () => {}; - channels.getLastMessagesByConversation = () => {}; - channels.getOldestMessageInConversation = () => {}; } From 0979d612a7f67c31d64b256c4773fceba9f63780 Mon Sep 17 00:00:00 2001 From: andyo Date: Sun, 10 Aug 2025 10:34:09 -0600 Subject: [PATCH 18/25] chore: add test for getLastMessagesByConversation --- ts/test/data/data_test.ts | 76 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index 287dca534..2d37a930a 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -1518,6 +1518,81 @@ describe('data', () => { expect(result.quotes).to.deep.equal([]); }); }); + + describe('getLastMessagesByConversation', () => { + it('returns last messages for conversation and sets skipTimerInit when true', async () => { + const expectedConversationId = 'last_convo_123'; + const expectedLimit = 2; + + const mockMessages = [ + { + id: 'last_msg_1', + body: 'Recent one', + conversationId: expectedConversationId, + source: 'sender_a', + type: 'incoming', + }, + { + id: 'last_msg_2', + body: 'Recent two', + conversationId: expectedConversationId, + source: 'sender_b', + type: 'outgoing', + }, + ]; + + const getLastMessagesStub = Sinon.stub( + channels, + 'getLastMessagesByConversation' + ).resolves(mockMessages); + + const result = await Data.getLastMessagesByConversation( + expectedConversationId, + expectedLimit, + true + ); + + expect(getLastMessagesStub.calledOnce).to.be.true; + expect(getLastMessagesStub.calledWith(expectedConversationId, expectedLimit)).to.be.true; + expect(result).to.have.length(2); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[1]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('last_msg_1'); + expect(result[1].get('id')).to.equal('last_msg_2'); + }); + + it('returns last messages for conversation when skipTimerInit is false', async () => { + const expectedConversationId = 'last_convo_456'; + const expectedLimit = 1; + + const mockMessages = [ + { + id: 'only_last_msg', + body: 'Most recent', + conversationId: expectedConversationId, + source: 'sender_c', + type: 'incoming', + }, + ]; + + const getLastMessagesStub = Sinon.stub( + channels, + 'getLastMessagesByConversation' + ).resolves(mockMessages); + + const result = await Data.getLastMessagesByConversation( + expectedConversationId, + expectedLimit, + false + ); + + expect(getLastMessagesStub.calledOnce).to.be.true; + expect(getLastMessagesStub.calledWith(expectedConversationId, expectedLimit)).to.be.true; + expect(result).to.have.length(1); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('only_last_msg'); + }); + }); }); function mockChannels(): void { @@ -1565,4 +1640,5 @@ function mockChannels(): void { channels.getUnreadCountByConversation = () => {}; channels.getMessageCountByType = () => {}; channels.getMessagesByConversation = () => {}; + channels.getLastMessagesByConversation = () => {}; } From f15beff6996aead8e1b5c07a46502ca0cbcda33d Mon Sep 17 00:00:00 2001 From: andyo Date: Sun, 10 Aug 2025 10:36:54 -0600 Subject: [PATCH 19/25] fix: formatting for getLastMessagesByConversation --- ts/test/data/data_test.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index 2d37a930a..8f492285e 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -1541,10 +1541,9 @@ describe('data', () => { }, ]; - const getLastMessagesStub = Sinon.stub( - channels, - 'getLastMessagesByConversation' - ).resolves(mockMessages); + const getLastMessagesStub = Sinon.stub(channels, 'getLastMessagesByConversation').resolves( + mockMessages + ); const result = await Data.getLastMessagesByConversation( expectedConversationId, @@ -1575,10 +1574,9 @@ describe('data', () => { }, ]; - const getLastMessagesStub = Sinon.stub( - channels, - 'getLastMessagesByConversation' - ).resolves(mockMessages); + const getLastMessagesStub = Sinon.stub(channels, 'getLastMessagesByConversation').resolves( + mockMessages + ); const result = await Data.getLastMessagesByConversation( expectedConversationId, From 4a44ff9dc342e9ad7c2e23b48b73e91091aa1ac1 Mon Sep 17 00:00:00 2001 From: andyo Date: Sun, 10 Aug 2025 10:44:51 -0600 Subject: [PATCH 20/25] chore: adding more unit tests --- ts/test/data/data_test.ts | 209 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index 8f492285e..bcf965087 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -1591,6 +1591,207 @@ describe('data', () => { expect(result[0].get('id')).to.equal('only_last_msg'); }); }); + + describe('getLastMessageIdInConversation', () => { + it('returns last message id or null when no messages', async () => { + const convo = 'convo_last_id'; + const firstBatch = [ + { id: 'last-id-1', conversationId: convo, type: 'incoming', source: 'a' }, + ]; + const stub = Sinon.stub(channels, 'getLastMessagesByConversation') + .onFirstCall() + .resolves(firstBatch) + .onSecondCall() + .resolves([]); + + const id = await Data.getLastMessageIdInConversation(convo); + expect(stub.calledOnce).to.be.true; + expect(id).to.equal('last-id-1'); + + const idNone = await Data.getLastMessageIdInConversation('empty'); + expect(idNone === null).to.be.true; + }); + }); + + describe('getLastMessageInConversation', () => { + it('returns wrapped last MessageModel or null', async () => { + const convo = 'convo_last_model'; + const msg = { id: 'm-last', conversationId: convo, type: 'incoming', source: 'a' }; + const stub = Sinon.stub(channels, 'getLastMessagesByConversation') + .onFirstCall() + .resolves([msg]) + .onSecondCall() + .resolves([]); + + const model = await Data.getLastMessageInConversation(convo); + expect(stub.calledOnce).to.be.true; + expect(model).to.be.instanceOf(MessageModel); + expect(model?.get('id')).to.equal('m-last'); + + const modelNone = await Data.getLastMessageInConversation('empty'); + expect(modelNone === null).to.be.true; + }); + }); + + describe('getOldestMessageInConversation', () => { + it('returns wrapped oldest MessageModel or null', async () => { + const convo = 'convo_oldest_model'; + const msg = { id: 'm-old', conversationId: convo, type: 'incoming', source: 'a' }; + const stub = Sinon.stub(channels, 'getOldestMessageInConversation') + .onFirstCall() + .resolves([msg]) + .onSecondCall() + .resolves([]); + + const model = await Data.getOldestMessageInConversation(convo); + expect(stub.calledOnce).to.be.true; + expect(model).to.be.instanceOf(MessageModel); + expect(model?.get('id')).to.equal('m-old'); + + const modelNone = await Data.getOldestMessageInConversation('empty'); + expect(modelNone === null).to.be.true; + }); + }); + + describe('getMessageCount', () => { + it('returns total message count', async () => { + const count = 123; + const stub = Sinon.stub(channels, 'getMessageCount').resolves(count); + const result = await Data.getMessageCount(); + expect(stub.calledOnce).to.be.true; + expect(result).to.equal(count); + }); + }); + + describe('getFirstUnreadMessageIdInConversation', () => { + it('returns first unread message id or undefined', async () => { + const convo = 'c-unread'; + const stub = Sinon.stub(channels, 'getFirstUnreadMessageIdInConversation') + .onFirstCall() + .resolves('first-unread') + .onSecondCall() + .resolves(undefined); + + const id = await Data.getFirstUnreadMessageIdInConversation(convo); + expect(id).to.equal('first-unread'); + const none = await Data.getFirstUnreadMessageIdInConversation('empty'); + expect(none).to.equal(undefined); + expect(stub.calledTwice).to.be.true; + expect(stub.firstCall.calledWith(convo)).to.be.true; + expect(stub.secondCall.calledWith('empty')).to.be.true; + }); + }); + + describe('getFirstUnreadMessageWithMention', () => { + it('returns first unread mention id or undefined', async () => { + const stub = Sinon.stub(channels, 'getFirstUnreadMessageWithMention') + .onFirstCall() + .resolves('mention-1') + .onSecondCall() + .resolves(undefined); + + const id = await Data.getFirstUnreadMessageWithMention('c'); + expect(id).to.equal('mention-1'); + const none = await Data.getFirstUnreadMessageWithMention('c2'); + expect(none).to.equal(undefined); + expect(stub.calledTwice).to.be.true; + expect(stub.firstCall.calledWith('c')).to.be.true; + expect(stub.secondCall.calledWith('c2')).to.be.true; + }); + }); + + describe('hasConversationOutgoingMessage', () => { + it('returns whether conversation has outgoing message', async () => { + const stub = Sinon.stub(channels, 'hasConversationOutgoingMessage') + .onFirstCall() + .resolves(true) + .onSecondCall() + .resolves(false); + + const yes = await Data.hasConversationOutgoingMessage('c'); + expect(yes).to.equal(true); + const no = await Data.hasConversationOutgoingMessage('c2'); + expect(no).to.equal(false); + expect(stub.calledTwice).to.be.true; + expect(stub.firstCall.calledWith('c')).to.be.true; + expect(stub.secondCall.calledWith('c2')).to.be.true; + }); + }); + + describe('getLastHashBySnode', () => { + it('returns last hash by snode', async () => { + const convo = 'convo_hash'; + const snode = 'snode-1'; + const ns = 3; + const expected = 'hash123'; + const stub = Sinon.stub(channels, 'getLastHashBySnode').resolves(expected); + const res = await Data.getLastHashBySnode(convo, snode, ns); + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(convo, snode, ns)).to.be.true; + expect(res).to.equal(expected); + }); + }); + + describe('getSeenMessagesByHashList', () => { + it('returns seen messages by hash list', async () => { + const hashes = ['h1', 'h2']; + const expected = [{ hash: 'h1' }, { hash: 'h2' }]; + const stub = Sinon.stub(channels, 'getSeenMessagesByHashList').resolves(expected); + const res = await Data.getSeenMessagesByHashList(hashes); + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(hashes)).to.be.true; + expect(res).to.equal(expected); + }); + }); + + describe('removeAllMessagesInConversation', () => { + it('removes messages in batches and calls cleanup on each', async () => { + const convo = 'convo_del'; + const batch1 = [ + { id: 'm1', conversationId: convo, type: 'incoming', source: 'a' }, + { id: 'm2', conversationId: convo, type: 'outgoing', source: 'b' }, + ]; + const batch2 = [{ id: 'm3', conversationId: convo, type: 'incoming', source: 'c' }]; + + const getLastStub = Sinon.stub(channels, 'getLastMessagesByConversation') + .onFirstCall() + .resolves(batch1) + .onSecondCall() + .resolves(batch2) + .onThirdCall() + .resolves([]); + + const removeIdsStub = Sinon.stub(channels, 'removeMessagesByIds').resolves(); + const finalRemoveStub = Sinon.stub(channels, 'removeAllMessagesInConversation').resolves(); + + const cleanupStub = Sinon.stub(MessageModel.prototype, 'cleanup').resolves(); + + await Data.removeAllMessagesInConversation(convo); + + // getLast called three times (two batches + final empty) + expect(getLastStub.callCount).to.equal(3); + // cleanup called once per message + expect(cleanupStub.callCount).to.equal(3); + // remove by ids called once per non-empty batch with correct ids + expect(removeIdsStub.callCount).to.equal(2); + expect(removeIdsStub.firstCall.args[0]).to.deep.equal(['m1', 'm2']); + expect(removeIdsStub.secondCall.args[0]).to.deep.equal(['m3']); + // Due to current early return on empty batch, the final remove is not called + expect(finalRemoveStub.called).to.equal(false); + }); + + it('returns early when no messages to delete', async () => { + const getLastStub = Sinon.stub(channels, 'getLastMessagesByConversation').resolves([]); + const removeIdsStub = Sinon.stub(channels, 'removeMessagesByIds').resolves(); + const finalRemoveStub = Sinon.stub(channels, 'removeAllMessagesInConversation').resolves(); + + await Data.removeAllMessagesInConversation('convo_empty'); + + expect(getLastStub.calledOnce).to.be.true; + expect(removeIdsStub.called).to.equal(false); + expect(finalRemoveStub.called).to.equal(false); + }); + }); }); function mockChannels(): void { @@ -1639,4 +1840,12 @@ function mockChannels(): void { channels.getMessageCountByType = () => {}; channels.getMessagesByConversation = () => {}; channels.getLastMessagesByConversation = () => {}; + channels.getOldestMessageInConversation = () => {}; + channels.getMessageCount = () => {}; + channels.getFirstUnreadMessageIdInConversation = () => {}; + channels.getFirstUnreadMessageWithMention = () => {}; + channels.hasConversationOutgoingMessage = () => {}; + channels.getLastHashBySnode = () => {}; + channels.getSeenMessagesByHashList = () => {}; + channels.removeAllMessagesInConversation = () => {}; } From d04b8066265ea43244e35f69f52e85a9acab105e Mon Sep 17 00:00:00 2001 From: andyo Date: Sun, 10 Aug 2025 11:05:34 -0600 Subject: [PATCH 21/25] chore: add several more unit tests --- ts/test/data/data_test.ts | 468 +++++++++++++++++++++----------------- 1 file changed, 254 insertions(+), 214 deletions(-) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index bcf965087..7ee46ba4a 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -25,6 +25,11 @@ import { UpdateLastHashType, } from '../../types/sqlSharedTypes'; import { UserUtils } from '../../session/utils'; +import { + FindAllMessageFromSendersInConversationTypeArgs, + FindAllMessageHashesInConversationMatchingAuthorTypeArgs, + FindAllMessageHashesInConversationTypeArgs, +} from '../../data/sharedDataTypes'; describe('data', () => { beforeEach(() => { @@ -1519,277 +1524,313 @@ describe('data', () => { }); }); - describe('getLastMessagesByConversation', () => { - it('returns last messages for conversation and sets skipTimerInit when true', async () => { - const expectedConversationId = 'last_convo_123'; - const expectedLimit = 2; - + describe('findAllMessageFromSendersInConversation', () => { + it('returns wrapped MessageModel array from channel results', async () => { + const args: FindAllMessageFromSendersInConversationTypeArgs = { + toRemove: ['05foo'], + groupPk: '03foo', + signatureTimestamp: 1234567890, + }; const mockMessages = [ { - id: 'last_msg_1', - body: 'Recent one', - conversationId: expectedConversationId, - source: 'sender_a', + id: 'msg_sender_1', + conversationId: 'convo_senders', + source: 'sender1', type: 'incoming', }, { - id: 'last_msg_2', - body: 'Recent two', - conversationId: expectedConversationId, - source: 'sender_b', - type: 'outgoing', + id: 'msg_sender_2', + conversationId: 'convo_senders', + source: 'sender2', + type: 'incoming', }, ]; - const getLastMessagesStub = Sinon.stub(channels, 'getLastMessagesByConversation').resolves( + const stub = Sinon.stub(channels, 'findAllMessageFromSendersInConversation').resolves( mockMessages ); + const result = await Data.findAllMessageFromSendersInConversation(args); - const result = await Data.getLastMessagesByConversation( - expectedConversationId, - expectedLimit, - true + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(args)).to.be.true; + expect(result).to.have.length(2); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[1]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('msg_sender_1'); + expect(result[1].get('id')).to.equal('msg_sender_2'); + }); + + it('returns empty array when no results', async () => { + const args: FindAllMessageFromSendersInConversationTypeArgs = { + toRemove: ['05foo'], + groupPk: '03foo', + signatureTimestamp: 1234567890, + }; + const stub = Sinon.stub(channels, 'findAllMessageFromSendersInConversation').resolves([]); + const result = await Data.findAllMessageFromSendersInConversation(args); + + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(args)).to.be.true; + expect(result).to.deep.equal([]); + }); + }); + + describe('findAllMessageHashesInConversation', () => { + it('returns wrapped MessageModel array from channel results', async () => { + const args: FindAllMessageHashesInConversationTypeArgs = { + messageHashes: ['hash_1', 'hash_2'], + groupPk: '03foo', + signatureTimestamp: 1234567890, + }; + const mockMessages = [ + { id: 'msg_hash_1', conversationId: 'convo_hashes', hash: 'hash1', type: 'incoming' }, + { id: 'msg_hash_2', conversationId: 'convo_hashes', hash: 'hash2', type: 'outgoing' }, + ]; + + const stub = Sinon.stub(channels, 'findAllMessageHashesInConversation').resolves( + mockMessages ); + const result = await Data.findAllMessageHashesInConversation(args); - expect(getLastMessagesStub.calledOnce).to.be.true; - expect(getLastMessagesStub.calledWith(expectedConversationId, expectedLimit)).to.be.true; + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(args)).to.be.true; expect(result).to.have.length(2); expect(result[0]).to.be.instanceOf(MessageModel); expect(result[1]).to.be.instanceOf(MessageModel); - expect(result[0].get('id')).to.equal('last_msg_1'); - expect(result[1].get('id')).to.equal('last_msg_2'); + expect(result[0].get('id')).to.equal('msg_hash_1'); + expect(result[1].get('id')).to.equal('msg_hash_2'); }); - it('returns last messages for conversation when skipTimerInit is false', async () => { - const expectedConversationId = 'last_convo_456'; - const expectedLimit = 1; + it('returns empty array when invalid results', async () => { + const args: FindAllMessageHashesInConversationTypeArgs = { + messageHashes: ['hash_1', 'hash_2'], + groupPk: '03foo', + signatureTimestamp: 1234567890, + }; + const stub = Sinon.stub(channels, 'findAllMessageHashesInConversation').resolves(null); + const result = await Data.findAllMessageHashesInConversation(args); + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(args)).to.be.true; + expect(result).to.deep.equal([]); + }); + }); + + describe('findAllMessageHashesInConversationMatchingAuthor', () => { + it('returns wrapped MessageModel array from channel results', async () => { + const args: FindAllMessageHashesInConversationMatchingAuthorTypeArgs = { + messageHashes: ['hash_1', 'hash_2'], + author: '05foo', + signatureTimestamp: 123456789, + groupPk: '03foo', + }; const mockMessages = [ { - id: 'only_last_msg', - body: 'Most recent', - conversationId: expectedConversationId, - source: 'sender_c', + id: 'msg_author_1', + conversationId: 'convo_author', + source: 'author1', + hash: 'h1', type: 'incoming', }, ]; - const getLastMessagesStub = Sinon.stub(channels, 'getLastMessagesByConversation').resolves( - mockMessages - ); - - const result = await Data.getLastMessagesByConversation( - expectedConversationId, - expectedLimit, - false - ); + const stub = Sinon.stub( + channels, + 'findAllMessageHashesInConversationMatchingAuthor' + ).resolves(mockMessages); + const result = await Data.findAllMessageHashesInConversationMatchingAuthor(args); - expect(getLastMessagesStub.calledOnce).to.be.true; - expect(getLastMessagesStub.calledWith(expectedConversationId, expectedLimit)).to.be.true; + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(args)).to.be.true; expect(result).to.have.length(1); expect(result[0]).to.be.instanceOf(MessageModel); - expect(result[0].get('id')).to.equal('only_last_msg'); + expect(result[0].get('id')).to.equal('msg_author_1'); }); - }); - describe('getLastMessageIdInConversation', () => { - it('returns last message id or null when no messages', async () => { - const convo = 'convo_last_id'; - const firstBatch = [ - { id: 'last-id-1', conversationId: convo, type: 'incoming', source: 'a' }, - ]; - const stub = Sinon.stub(channels, 'getLastMessagesByConversation') - .onFirstCall() - .resolves(firstBatch) - .onSecondCall() - .resolves([]); + it('returns empty array when no matching results', async () => { + const args: FindAllMessageHashesInConversationMatchingAuthorTypeArgs = { + messageHashes: ['hash_1', 'hash_2'], + author: '05foo', + groupPk: '03foo', + signatureTimestamp: 1234567890, + }; + const stub = Sinon.stub( + channels, + 'findAllMessageHashesInConversationMatchingAuthor' + ).resolves(undefined); + const result = await Data.findAllMessageHashesInConversationMatchingAuthor(args); - const id = await Data.getLastMessageIdInConversation(convo); expect(stub.calledOnce).to.be.true; - expect(id).to.equal('last-id-1'); - - const idNone = await Data.getLastMessageIdInConversation('empty'); - expect(idNone === null).to.be.true; + expect(stub.calledWith(args)).to.be.true; + expect(result).to.deep.equal([]); }); }); - describe('getLastMessageInConversation', () => { - it('returns wrapped last MessageModel or null', async () => { - const convo = 'convo_last_model'; - const msg = { id: 'm-last', conversationId: convo, type: 'incoming', source: 'a' }; - const stub = Sinon.stub(channels, 'getLastMessagesByConversation') - .onFirstCall() - .resolves([msg]) - .onSecondCall() - .resolves([]); - - const model = await Data.getLastMessageInConversation(convo); - expect(stub.calledOnce).to.be.true; - expect(model).to.be.instanceOf(MessageModel); - expect(model?.get('id')).to.equal('m-last'); + describe('fetchAllGroupUpdateFailedMessage', () => { + it('returns wrapped MessageModel array from channel results', async () => { + const groupPk = 'group_pk_123' as GroupPubkeyType; + const mockMessages = [ + { + id: 'failed_update_1', + conversationId: groupPk, + type: 'group-update-failed', + source: 'system', + }, + { + id: 'failed_update_2', + conversationId: groupPk, + type: 'group-update-failed', + source: 'system', + }, + ]; - const modelNone = await Data.getLastMessageInConversation('empty'); - expect(modelNone === null).to.be.true; - }); - }); + const stub = Sinon.stub(channels, 'fetchAllGroupUpdateFailedMessage').resolves(mockMessages); + const result = await Data.fetchAllGroupUpdateFailedMessage(groupPk); - describe('getOldestMessageInConversation', () => { - it('returns wrapped oldest MessageModel or null', async () => { - const convo = 'convo_oldest_model'; - const msg = { id: 'm-old', conversationId: convo, type: 'incoming', source: 'a' }; - const stub = Sinon.stub(channels, 'getOldestMessageInConversation') - .onFirstCall() - .resolves([msg]) - .onSecondCall() - .resolves([]); - - const model = await Data.getOldestMessageInConversation(convo); expect(stub.calledOnce).to.be.true; - expect(model).to.be.instanceOf(MessageModel); - expect(model?.get('id')).to.equal('m-old'); - - const modelNone = await Data.getOldestMessageInConversation('empty'); - expect(modelNone === null).to.be.true; + expect(stub.calledWith(groupPk)).to.be.true; + expect(result).to.have.length(2); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[1]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('failed_update_1'); + expect(result[1].get('id')).to.equal('failed_update_2'); }); - }); - describe('getMessageCount', () => { - it('returns total message count', async () => { - const count = 123; - const stub = Sinon.stub(channels, 'getMessageCount').resolves(count); - const result = await Data.getMessageCount(); + it('returns empty array when no failed updates', async () => { + const groupPk = 'empty_group' as GroupPubkeyType; + const stub = Sinon.stub(channels, 'fetchAllGroupUpdateFailedMessage').resolves([]); + const result = await Data.fetchAllGroupUpdateFailedMessage(groupPk); + expect(stub.calledOnce).to.be.true; - expect(result).to.equal(count); + expect(stub.calledWith(groupPk)).to.be.true; + expect(result).to.deep.equal([]); }); }); - describe('getFirstUnreadMessageIdInConversation', () => { - it('returns first unread message id or undefined', async () => { - const convo = 'c-unread'; - const stub = Sinon.stub(channels, 'getFirstUnreadMessageIdInConversation') - .onFirstCall() - .resolves('first-unread') - .onSecondCall() - .resolves(undefined); - - const id = await Data.getFirstUnreadMessageIdInConversation(convo); - expect(id).to.equal('first-unread'); - const none = await Data.getFirstUnreadMessageIdInConversation('empty'); - expect(none).to.equal(undefined); - expect(stub.calledTwice).to.be.true; - expect(stub.firstCall.calledWith(convo)).to.be.true; - expect(stub.secondCall.calledWith('empty')).to.be.true; - }); - }); + describe('getMessagesBySentAt', () => { + it('returns wrapped MessageModel array from channel results', async () => { + const sentAt = 1234567890; + const mockMessages = [ + { id: 'msg_sent_1', sentAt, conversationId: 'convo1', type: 'incoming', source: 'sender1' }, + { id: 'msg_sent_2', sentAt, conversationId: 'convo2', type: 'outgoing', source: 'us' }, + ]; + + const stub = Sinon.stub(channels, 'getMessagesBySentAt').resolves(mockMessages); + const result = await Data.getMessagesBySentAt(sentAt); - describe('getFirstUnreadMessageWithMention', () => { - it('returns first unread mention id or undefined', async () => { - const stub = Sinon.stub(channels, 'getFirstUnreadMessageWithMention') - .onFirstCall() - .resolves('mention-1') - .onSecondCall() - .resolves(undefined); - - const id = await Data.getFirstUnreadMessageWithMention('c'); - expect(id).to.equal('mention-1'); - const none = await Data.getFirstUnreadMessageWithMention('c2'); - expect(none).to.equal(undefined); - expect(stub.calledTwice).to.be.true; - expect(stub.firstCall.calledWith('c')).to.be.true; - expect(stub.secondCall.calledWith('c2')).to.be.true; + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(sentAt)).to.be.true; + expect(result).to.have.length(2); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[1]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('msg_sent_1'); + expect(result[1].get('id')).to.equal('msg_sent_2'); }); - }); - describe('hasConversationOutgoingMessage', () => { - it('returns whether conversation has outgoing message', async () => { - const stub = Sinon.stub(channels, 'hasConversationOutgoingMessage') - .onFirstCall() - .resolves(true) - .onSecondCall() - .resolves(false); - - const yes = await Data.hasConversationOutgoingMessage('c'); - expect(yes).to.equal(true); - const no = await Data.hasConversationOutgoingMessage('c2'); - expect(no).to.equal(false); - expect(stub.calledTwice).to.be.true; - expect(stub.firstCall.calledWith('c')).to.be.true; - expect(stub.secondCall.calledWith('c2')).to.be.true; + it('returns empty array when no messages at that time', async () => { + const sentAt = 9999999999; + const stub = Sinon.stub(channels, 'getMessagesBySentAt').resolves([]); + const result = await Data.getMessagesBySentAt(sentAt); + + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(sentAt)).to.be.true; + expect(result).to.deep.equal([]); }); }); - describe('getLastHashBySnode', () => { - it('returns last hash by snode', async () => { - const convo = 'convo_hash'; - const snode = 'snode-1'; - const ns = 3; - const expected = 'hash123'; - const stub = Sinon.stub(channels, 'getLastHashBySnode').resolves(expected); - const res = await Data.getLastHashBySnode(convo, snode, ns); + describe('getExpiredMessages', () => { + it('returns wrapped MessageModel array from channel results', async () => { + const mockMessages = [ + { + id: 'expired_1', + expiresAt: 1000, + conversationId: 'convo1', + type: 'incoming', + source: 'sender', + }, + { + id: 'expired_2', + expiresAt: 2000, + conversationId: 'convo2', + type: 'outgoing', + source: 'us', + }, + ]; + + const stub = Sinon.stub(channels, 'getExpiredMessages').resolves(mockMessages); + const result = await Data.getExpiredMessages(); + expect(stub.calledOnce).to.be.true; - expect(stub.calledWith(convo, snode, ns)).to.be.true; - expect(res).to.equal(expected); + expect(result).to.have.length(2); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[1]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('expired_1'); + expect(result[1].get('id')).to.equal('expired_2'); }); - }); - describe('getSeenMessagesByHashList', () => { - it('returns seen messages by hash list', async () => { - const hashes = ['h1', 'h2']; - const expected = [{ hash: 'h1' }, { hash: 'h2' }]; - const stub = Sinon.stub(channels, 'getSeenMessagesByHashList').resolves(expected); - const res = await Data.getSeenMessagesByHashList(hashes); + it('returns empty array when no expired messages', async () => { + const stub = Sinon.stub(channels, 'getExpiredMessages').resolves([]); + const result = await Data.getExpiredMessages(); + expect(stub.calledOnce).to.be.true; - expect(stub.calledWith(hashes)).to.be.true; - expect(res).to.equal(expected); + expect(result).to.deep.equal([]); }); }); - describe('removeAllMessagesInConversation', () => { - it('removes messages in batches and calls cleanup on each', async () => { - const convo = 'convo_del'; - const batch1 = [ - { id: 'm1', conversationId: convo, type: 'incoming', source: 'a' }, - { id: 'm2', conversationId: convo, type: 'outgoing', source: 'b' }, + describe('getOutgoingWithoutExpiresAt', () => { + it('returns wrapped MessageModel array from channel results', async () => { + const mockMessages = [ + { id: 'outgoing_1', type: 'outgoing', conversationId: 'convo1', source: 'us' }, + { id: 'outgoing_2', type: 'outgoing', conversationId: 'convo2', source: 'us' }, ]; - const batch2 = [{ id: 'm3', conversationId: convo, type: 'incoming', source: 'c' }]; - - const getLastStub = Sinon.stub(channels, 'getLastMessagesByConversation') - .onFirstCall() - .resolves(batch1) - .onSecondCall() - .resolves(batch2) - .onThirdCall() - .resolves([]); - const removeIdsStub = Sinon.stub(channels, 'removeMessagesByIds').resolves(); - const finalRemoveStub = Sinon.stub(channels, 'removeAllMessagesInConversation').resolves(); + const stub = Sinon.stub(channels, 'getOutgoingWithoutExpiresAt').resolves(mockMessages); + const result = await Data.getOutgoingWithoutExpiresAt(); - const cleanupStub = Sinon.stub(MessageModel.prototype, 'cleanup').resolves(); + expect(stub.calledOnce).to.be.true; + expect(result).to.have.length(2); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[1]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('outgoing_1'); + expect(result[1].get('id')).to.equal('outgoing_2'); + }); - await Data.removeAllMessagesInConversation(convo); + it('handles null/undefined results', async () => { + const stub = Sinon.stub(channels, 'getOutgoingWithoutExpiresAt').resolves(null); + const result = await Data.getOutgoingWithoutExpiresAt(); - // getLast called three times (two batches + final empty) - expect(getLastStub.callCount).to.equal(3); - // cleanup called once per message - expect(cleanupStub.callCount).to.equal(3); - // remove by ids called once per non-empty batch with correct ids - expect(removeIdsStub.callCount).to.equal(2); - expect(removeIdsStub.firstCall.args[0]).to.deep.equal(['m1', 'm2']); - expect(removeIdsStub.secondCall.args[0]).to.deep.equal(['m3']); - // Due to current early return on empty batch, the final remove is not called - expect(finalRemoveStub.called).to.equal(false); + expect(stub.calledOnce).to.be.true; + expect(result).to.deep.equal([]); }); + }); + + describe('getNextExpiringMessage', () => { + it('returns wrapped MessageModel array from channel results', async () => { + const mockMessages = [ + { + id: 'next_expiring_1', + expiresAt: 3000, + conversationId: 'convo1', + type: 'incoming', + source: 'sender', + }, + ]; - it('returns early when no messages to delete', async () => { - const getLastStub = Sinon.stub(channels, 'getLastMessagesByConversation').resolves([]); - const removeIdsStub = Sinon.stub(channels, 'removeMessagesByIds').resolves(); - const finalRemoveStub = Sinon.stub(channels, 'removeAllMessagesInConversation').resolves(); + const stub = Sinon.stub(channels, 'getNextExpiringMessage').resolves(mockMessages); + const result = await Data.getNextExpiringMessage(); - await Data.removeAllMessagesInConversation('convo_empty'); + expect(stub.calledOnce).to.be.true; + expect(result).to.have.length(1); + expect(result[0]).to.be.instanceOf(MessageModel); + expect(result[0].get('id')).to.equal('next_expiring_1'); + }); - expect(getLastStub.calledOnce).to.be.true; - expect(removeIdsStub.called).to.equal(false); - expect(finalRemoveStub.called).to.equal(false); + it('returns empty array when no expiring messages', async () => { + const stub = Sinon.stub(channels, 'getNextExpiringMessage').resolves([]); + const result = await Data.getNextExpiringMessage(); + + expect(stub.calledOnce).to.be.true; + expect(result).to.deep.equal([]); }); }); }); @@ -1839,13 +1880,12 @@ function mockChannels(): void { channels.getUnreadCountByConversation = () => {}; channels.getMessageCountByType = () => {}; channels.getMessagesByConversation = () => {}; - channels.getLastMessagesByConversation = () => {}; - channels.getOldestMessageInConversation = () => {}; - channels.getMessageCount = () => {}; - channels.getFirstUnreadMessageIdInConversation = () => {}; - channels.getFirstUnreadMessageWithMention = () => {}; - channels.hasConversationOutgoingMessage = () => {}; - channels.getLastHashBySnode = () => {}; - channels.getSeenMessagesByHashList = () => {}; - channels.removeAllMessagesInConversation = () => {}; + channels.findAllMessageFromSendersInConversation = () => {}; + channels.findAllMessageHashesInConversation = () => {}; + channels.findAllMessageHashesInConversationMatchingAuthor = () => {}; + channels.fetchAllGroupUpdateFailedMessage = () => {}; + channels.getMessagesBySentAt = () => {}; + channels.getExpiredMessages = () => {}; + channels.getOutgoingWithoutExpiresAt = () => {}; + channels.getNextExpiringMessage = () => {}; } From 2da9a484955ec12dc90925ecc060e6fc36157dd8 Mon Sep 17 00:00:00 2001 From: andyo Date: Sun, 10 Aug 2025 11:14:20 -0600 Subject: [PATCH 22/25] chore: adding more unit tests --- ts/test/data/data_test.ts | 140 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index 7ee46ba4a..f0db9ee3b 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -22,6 +22,7 @@ import { MsgDuplicateSearchOpenGroup, SaveConversationReturn, SaveSeenMessageHash, + UnprocessedParameter, UpdateLastHashType, } from '../../types/sqlSharedTypes'; import { UserUtils } from '../../session/utils'; @@ -1833,6 +1834,137 @@ describe('data', () => { expect(result).to.deep.equal([]); }); }); + + describe('getUnprocessedCount', () => { + it('returns unprocessed message count', async () => { + const expectedCount = 42; + const stub = Sinon.stub(channels, 'getUnprocessedCount').resolves(expectedCount); + const result = await Data.getUnprocessedCount(); + + expect(stub.calledOnce).to.be.true; + expect(result).to.equal(expectedCount); + }); + }); + + describe('getAllUnprocessed', () => { + it('returns all unprocessed messages', async () => { + const expectedData = [ + { id: 'unprocessed_1', data: 'message_data_1' }, + { id: 'unprocessed_2', data: 'message_data_2' }, + ]; + const stub = Sinon.stub(channels, 'getAllUnprocessed').resolves(expectedData); + const result = await Data.getAllUnprocessed(); + + expect(stub.calledOnce).to.be.true; + expect(result).to.deep.equal(expectedData); + }); + + it('returns empty array when no unprocessed messages', async () => { + const stub = Sinon.stub(channels, 'getAllUnprocessed').resolves([]); + const result = await Data.getAllUnprocessed(); + + expect(stub.calledOnce).to.be.true; + expect(result).to.deep.equal([]); + }); + }); + + describe('getUnprocessedById', () => { + it('returns unprocessed message by id', async () => { + const id = 'unprocessed_123'; + const expectedData = { id, data: 'message_content' }; + const stub = Sinon.stub(channels, 'getUnprocessedById').resolves(expectedData); + const result = await Data.getUnprocessedById(id); + + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(id)).to.be.true; + expect(result).to.deep.equal(expectedData); + }); + + it('returns undefined when id not found', async () => { + const id = 'nonexistent_id'; + const stub = Sinon.stub(channels, 'getUnprocessedById').resolves(undefined); + const result = await Data.getUnprocessedById(id); + + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(id)).to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('saveUnprocessed', () => { + it('saves unprocessed message data', async () => { + const data: UnprocessedParameter = { + id: 'new_unprocessed', + version: 100, + envelope: '1', + timestamp: 123456789, + messageHash: 'foo', + attempts: 1, + }; + const stub = Sinon.stub(channels, 'saveUnprocessed').resolves(); + const result = await Data.saveUnprocessed(data); + + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(data)).to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('updateUnprocessedAttempts', () => { + it('updates attempts count for unprocessed message', async () => { + const id = 'unprocessed_retry'; + const attempts = 3; + const stub = Sinon.stub(channels, 'updateUnprocessedAttempts').resolves(); + const result = await Data.updateUnprocessedAttempts(id, attempts); + + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(id, attempts)).to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('updateUnprocessedWithData', () => { + it('updates unprocessed message with new data', async () => { + const id = 'unprocessed_update'; + const data: UnprocessedParameter = { + id, + version: 100, + envelope: '1', + timestamp: 123456789, + messageHash: 'foo', + attempts: 1, + }; + + const stub = Sinon.stub(channels, 'updateUnprocessedWithData').resolves(); + const result = await Data.updateUnprocessedWithData(id, data); + + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(id, data)).to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('removeUnprocessed', () => { + it('removes unprocessed message by id', async () => { + const id = 'unprocessed_remove'; + const stub = Sinon.stub(channels, 'removeUnprocessed').resolves(); + const result = await Data.removeUnprocessed(id); + + expect(stub.calledOnce).to.be.true; + expect(stub.calledWith(id)).to.be.true; + expect(result).to.be.undefined; + }); + }); + + describe('removeAllUnprocessed', () => { + it('removes all unprocessed messages', async () => { + const stub = Sinon.stub(channels, 'removeAllUnprocessed').resolves(); + const result = await Data.removeAllUnprocessed(); + + expect(stub.calledOnce).to.be.true; + expect(result).to.be.undefined; + }); + }); }); function mockChannels(): void { @@ -1888,4 +2020,12 @@ function mockChannels(): void { channels.getExpiredMessages = () => {}; channels.getOutgoingWithoutExpiresAt = () => {}; channels.getNextExpiringMessage = () => {}; + channels.getUnprocessedCount = () => {}; + channels.getAllUnprocessed = () => {}; + channels.getUnprocessedById = () => {}; + channels.saveUnprocessed = () => {}; + channels.updateUnprocessedAttempts = () => {}; + channels.updateUnprocessedWithData = () => {}; + channels.removeUnprocessed = () => {}; + channels.removeAllUnprocessed = () => {}; } From 3dd4c2a84f0d4276289f11a3946ffc6194db836f Mon Sep 17 00:00:00 2001 From: andyo Date: Sun, 10 Aug 2025 15:48:38 -0600 Subject: [PATCH 23/25] chore: add getAllConversations test for invalid data --- ts/test/data/data_test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index f0db9ee3b..509a4c8f6 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -364,6 +364,19 @@ describe('data', () => { }); describe('getAllConversations', () => { + it('returns empty array when channels.getAllConversations yields invalid conversations', async () => { + const getAllConversationsStub = Sinon.stub(channels, 'getAllConversations').resolves([ + {} as any, + { id: 123 } as any, + ]); + + const conversations = await Data.getAllConversations(); + + expect(conversations).to.be.an('array'); + expect(conversations).to.deep.equal([]); + expect(getAllConversationsStub.calledOnce).to.eq(true); + }); + it('returns array of conversation models', async () => { const conversationsData: Array = [ { From eb3f0fb5fc7df850cd6d6f639fca3e0cd5e8f05f Mon Sep 17 00:00:00 2001 From: andyo Date: Tue, 26 Aug 2025 15:58:19 +0200 Subject: [PATCH 24/25] chore: clean up unit test placement --- package.json | 3 ++- ts/test/data/data_test.ts | 30 ++++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 1fc092de3..63cceb9ad 100644 --- a/package.json +++ b/package.json @@ -341,5 +341,6 @@ "ts/webworker/workers/node/libsession/*.node", "!dev-app-update.yml" ] - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/ts/test/data/data_test.ts b/ts/test/data/data_test.ts index d622dc39b..099a26949 100644 --- a/ts/test/data/data_test.ts +++ b/ts/test/data/data_test.ts @@ -377,6 +377,23 @@ describe('data', () => { expect(getAllConversationsStub.calledOnce).to.eq(true); }); + it('filters out invalid and keeps valid when input mixed', async () => { + const getAllConversationsStub = Sinon.stub(channels, 'getAllConversations').resolves([ + { id: 'ok' } as any, + {} as any, + { id: null } as any, + { id: 'ok2' } as any, + ]); + + const conversations = await Data.getAllConversations(); + + expect(conversations).to.be.an('array'); + expect(conversations).to.have.length(2); + expect(conversations[0]).to.be.instanceOf(ConversationModel); + expect(conversations[1]).to.be.instanceOf(ConversationModel); + expect(getAllConversationsStub.calledOnce).to.eq(true); + }); + it('returns array of conversation models', async () => { const conversationsData: Array = [ { @@ -394,14 +411,15 @@ describe('data', () => { const getAllConversationsStub = Sinon.stub(channels, 'getAllConversations').resolves( conversationsData ); - const result = await Data.getAllConversations(); + const conversations = await Data.getAllConversations(); expect(getAllConversationsStub.calledOnce).to.be.true; - expect(result).to.have.length(2); - expect(result[0]).to.be.instanceOf(ConversationModel); - expect(result[1]).to.be.instanceOf(ConversationModel); - expect(result[0].get('id')).to.equal('convo_1'); - expect(result[1].get('id')).to.equal('convo_2'); + expect(conversations).to.be.an('array'); + expect(conversations).to.have.length(2); + expect(conversations[0]).to.be.instanceOf(ConversationModel); + expect(conversations[1]).to.be.instanceOf(ConversationModel); + expect(conversations[0].get('id')).to.equal('convo_1'); + expect(conversations[1].get('id')).to.equal('convo_2'); }); }); From d867b2b6c93fbee4f73c80622f746653d1aa2d1d Mon Sep 17 00:00:00 2001 From: andyo Date: Tue, 26 Aug 2025 15:59:24 +0200 Subject: [PATCH 25/25] fix: restore changes --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 63cceb9ad..1fc092de3 100644 --- a/package.json +++ b/package.json @@ -341,6 +341,5 @@ "ts/webworker/workers/node/libsession/*.node", "!dev-app-update.yml" ] - }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + } }