const request = require('supertest'); const app = require('../../app'); const { User, InboxItem, sequelize } = require('../../models'); const telegramPoller = require('../../services/telegramPoller'); describe('Telegram Duplicate Prevention Integration Tests', () => { let testUser; let originalConsoleLog; let logMessages; beforeAll(async () => { // Capture console.log for verification originalConsoleLog = console.log; logMessages = []; console.log = (...args) => { logMessages.push(args.join(' ')); originalConsoleLog(...args); }; }); beforeEach(async () => { logMessages = []; // Create test user testUser = await User.create({ email: 'test-telegram@example.com', password_digest: 'hashedpassword', telegram_bot_token: 'test-bot-token-123', telegram_chat_id: '987654321', }); // Clear any existing inbox items await InboxItem.destroy({ where: {} }); // Stop and reset poller telegramPoller.stopPolling(); }); afterEach(async () => { telegramPoller.stopPolling(); await User.destroy({ where: {} }); await InboxItem.destroy({ where: {} }); }); afterAll(async () => { console.log = originalConsoleLog; }); describe('Database-level Duplicate Prevention', () => { test('should prevent duplicate inbox items with same content within 30 seconds', async () => { const messageContent = 'Test duplicate message'; // Create first inbox item const item1 = await InboxItem.create({ content: messageContent, source: 'telegram', user_id: testUser.id, metadata: { telegram_message_id: 123 }, }); // Wait a moment await new Promise((resolve) => setTimeout(resolve, 100)); // Try to create duplicate item (should be prevented) const duplicateCheck = await InboxItem.findOne({ where: { content: messageContent, user_id: testUser.id, source: 'telegram', created_at: { [require('sequelize').Op.gte]: new Date( Date.now() - 30000 ), }, }, }); expect(duplicateCheck).toBeTruthy(); expect(duplicateCheck.id).toBe(item1.id); // Verify only one item exists const allItems = await InboxItem.findAll({ where: { user_id: testUser.id }, }); expect(allItems).toHaveLength(1); }); test('should allow duplicate content after 30 seconds', async () => { const messageContent = 'Test time-based duplicate'; // Create first item with backdated timestamp await InboxItem.create({ content: messageContent, source: 'telegram', user_id: testUser.id, created_at: new Date(Date.now() - 35000), // 35 seconds ago metadata: { telegram_message_id: 124 }, }); // Create second item (should be allowed) const item2 = await InboxItem.create({ content: messageContent, source: 'telegram', user_id: testUser.id, metadata: { telegram_message_id: 125 }, }); // Verify both items exist const allItems = await InboxItem.findAll({ where: { user_id: testUser.id }, }); expect(allItems).toHaveLength(2); }); test('should allow same content for different users', async () => { // Create second user const testUser2 = await User.create({ email: 'test2-telegram@example.com', password_digest: 'hashedpassword', telegram_bot_token: 'test-bot-token-456', telegram_chat_id: '123456789', }); const messageContent = 'Shared message content'; // Create item for first user await InboxItem.create({ content: messageContent, source: 'telegram', user_id: testUser.id, metadata: { telegram_message_id: 126 }, }); // Create item for second user (should be allowed) await InboxItem.create({ content: messageContent, source: 'telegram', user_id: testUser2.id, metadata: { telegram_message_id: 127 }, }); // Verify both items exist const allItems = await InboxItem.findAll(); expect(allItems).toHaveLength(2); const user1Items = allItems.filter( (item) => item.user_id === testUser.id ); const user2Items = allItems.filter( (item) => item.user_id === testUser2.id ); expect(user1Items).toHaveLength(1); expect(user2Items).toHaveLength(1); }); }); describe('Poller State Management', () => { test('should add and remove users correctly', async () => { const initialStatus = telegramPoller.getStatus(); expect(initialStatus.usersCount).toBe(0); expect(initialStatus.running).toBe(false); // Add user const addResult = await telegramPoller.addUser(testUser); expect(addResult).toBe(true); const statusAfterAdd = telegramPoller.getStatus(); expect(statusAfterAdd.usersCount).toBe(1); expect(statusAfterAdd.running).toBe(true); // Remove user const removeResult = telegramPoller.removeUser(testUser.id); expect(removeResult).toBe(true); const statusAfterRemove = telegramPoller.getStatus(); expect(statusAfterRemove.usersCount).toBe(0); expect(statusAfterRemove.running).toBe(false); }); test('should not add user without telegram token', async () => { const userWithoutToken = await User.create({ email: 'no-token@example.com', password_digest: 'hashedpassword', // No telegram_bot_token }); const addResult = await telegramPoller.addUser(userWithoutToken); expect(addResult).toBe(false); const status = telegramPoller.getStatus(); expect(status.usersCount).toBe(0); }); test('should handle adding same user multiple times', async () => { // Add user first time await telegramPoller.addUser(testUser); const status1 = telegramPoller.getStatus(); expect(status1.usersCount).toBe(1); // Add same user again await telegramPoller.addUser(testUser); const status2 = telegramPoller.getStatus(); expect(status2.usersCount).toBe(1); // Should still be 1 }); }); describe('Update Processing Logic', () => { test('should handle updates with proper ID tracking', async () => { await telegramPoller.addUser(testUser); // Simulate updates (this tests the internal logic without actual HTTP calls) const mockUpdates = [ { update_id: 1001, message: { message_id: 501, text: 'First message', chat: { id: 987654321 }, }, }, { update_id: 1002, message: { message_id: 502, text: 'Second message', chat: { id: 987654321 }, }, }, ]; // Test highest update ID calculation const highestId = telegramPoller._getHighestUpdateId(mockUpdates); expect(highestId).toBe(1002); // Test update key generation (simulating internal logic) const updateKeys = mockUpdates.map( (update) => `${testUser.id}-${update.update_id}` ); expect(updateKeys).toEqual([ `${testUser.id}-1001`, `${testUser.id}-1002`, ]); }); test('should properly track processed updates', async () => { // Test the Set-based tracking logic const processedUpdates = new Set(); // Add some processed updates processedUpdates.add('1-1001'); processedUpdates.add('1-1002'); // Test filtering logic const newUpdates = [ { update_id: 1001 }, // Should be filtered out { update_id: 1002 }, // Should be filtered out { update_id: 1003 }, // Should remain ].filter((update) => { const updateKey = `1-${update.update_id}`; return !processedUpdates.has(updateKey); }); expect(newUpdates).toHaveLength(1); expect(newUpdates[0].update_id).toBe(1003); }); test('should handle memory management for processed updates', async () => { // Simulate the cleanup logic const processedUpdates = new Set(); // Add many updates (more than the 1000 limit) for (let i = 1; i <= 1100; i++) { processedUpdates.add(`1-${i}`); } expect(processedUpdates.size).toBe(1100); // Simulate cleanup (remove oldest 100) if (processedUpdates.size > 1000) { const oldestEntries = Array.from(processedUpdates).slice( 0, 100 ); oldestEntries.forEach((entry) => processedUpdates.delete(entry) ); } expect(processedUpdates.size).toBe(1000); expect(processedUpdates.has('1-1')).toBe(false); // Oldest should be removed expect(processedUpdates.has('1-1100')).toBe(true); // Newest should remain }); }); describe('Error Handling', () => { test('should handle database errors gracefully', async () => { // Mock InboxItem.create to throw an error const originalCreate = InboxItem.create; InboxItem.create = jest .fn() .mockRejectedValue(new Error('Database error')); await expect( InboxItem.create({ content: 'Test error handling', source: 'telegram', user_id: testUser.id, }) ).rejects.toThrow('Database error'); // Restore original function InboxItem.create = originalCreate; }); test('should handle invalid user data', async () => { const invalidUser = null; const addResult = await telegramPoller.addUser(invalidUser); expect(addResult).toBe(false); }); test('should handle missing message properties', async () => { // Test the processing logic with incomplete message data const incompleteUpdate = { update_id: 2001, message: { // Missing text and other properties message_id: 601, chat: { id: 987654321 }, }, }; // The actual processing would skip this message due to missing text const hasText = incompleteUpdate.message && incompleteUpdate.message.text; expect(hasText).toBeFalsy(); }); }); });