diff --git a/demo/search.html b/demo/search.html index d0c34550..dc92aae1 100644 --- a/demo/search.html +++ b/demo/search.html @@ -182,7 +182,7 @@ threadIdObj = JSON.parse(threadIdEl.value); } let _has = has.value || null; - search.searchQuery(queryEl.value, senderIdObj, threadIdObj, _has, startDate, endDate, limitEl.value, offsetEl.value, 0).then(function (result) { + search.searchQuery(queryEl.value, senderIdObj, threadIdObj, _has, startDate, endDate, parseInt(limitEl.value, 10), parseInt(offsetEl.value, 10), 0).then(function (result) { if (result.messages.length < 1) { resultsEl.innerHTML = "No results found" } @@ -220,7 +220,6 @@ }); sendMessage.addEventListener('click', function () { - search.deleteRealTimeFolder(); if (realTimeIndexing.value !== "") { let message = realTimeIndexing.value; search.batchRealTimeIndexing(JSON.parse(message)); diff --git a/js/cryptoLib/crypto.js b/js/cryptoLib/crypto.js index ada68415..031f16b4 100644 --- a/js/cryptoLib/crypto.js +++ b/js/cryptoLib/crypto.js @@ -10,6 +10,9 @@ let Transform = stream.Transform; let util = require('util'); let crypto = require('crypto'); +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); + let KEY_LENGTH = 32; // bytes let GCM_NONCE_LENGTH = 12; //bytes let GCM_MAC_LENGTH = 16; //bytes @@ -151,7 +154,8 @@ DecryptionStream.prototype._transform = function(chunk, enc, cb) { DecryptionStream.prototype._flush = function(cb) { let mac = pullOutMac(this._cipherTextChunks); if (!mac) { - return this.emit('error', new Error('Decryption failed: bad cipher text.')); + log.send(logLevels.ERROR, 'Crypto: Decryption failed: bad cipher text.'); + return cb(); } this._decipher.setAuthTag(mac); let decrypted = this._cipherTextChunks.map(function(item) { @@ -160,6 +164,7 @@ DecryptionStream.prototype._flush = function(cb) { try { this._decipher.final(); } catch (e) { + log.send(logLevels.ERROR, 'Crypto: Decryption failed: ' + e); return cb(); } decrypted.forEach(function(item) { diff --git a/js/cryptoLib/index.js b/js/cryptoLib/index.js index 6a393328..6b14b22a 100644 --- a/js/cryptoLib/index.js +++ b/js/cryptoLib/index.js @@ -18,12 +18,13 @@ class Crypto { * @param key */ constructor(userId, key) { - this.indexDataFolder = `${searchConfig.FOLDERS_CONSTANTS.PREFIX_NAME_PATH}_${userId}_${searchConfig.INDEX_VERSION}`; - this.permanentIndexName = `${searchConfig.FOLDERS_CONSTANTS.PREFIX_NAME}_${userId}_${searchConfig.INDEX_VERSION}`; - this.dump = DUMP_PATH; + this.indexDataFolder = `${searchConfig.FOLDERS_CONSTANTS.PREFIX_NAME_PATH}_${userId}`; + this.permanentIndexName = `${searchConfig.FOLDERS_CONSTANTS.PREFIX_NAME}_${userId}`; this.key = key; this.encryptedIndex = `${DUMP_PATH}/${this.permanentIndexName}.enc`; this.dataFolder = searchConfig.FOLDERS_CONSTANTS.INDEX_PATH; + this.lz4Temp = `${DUMP_PATH}/${this.permanentIndexName}${searchConfig.TAR_LZ4_EXT}`; + this.decryptedTemp = `${DUMP_PATH}/decrypted${searchConfig.TAR_LZ4_EXT}`; } /** @@ -51,22 +52,37 @@ class Crypto { if (response && response.stderr) { log.send(logLevels.WARN, 'Crypto: Child process stderr while compression, ' + response.stderr); } - const input = fs.createReadStream(`${this.dump}/${this.permanentIndexName}${searchConfig.TAR_LZ4_EXT}`); + const input = fs.createReadStream(this.lz4Temp); const outputEncryption = fs.createWriteStream(this.encryptedIndex); let config = { key: key }; - const encrypt = crypto.encrypt(config); + let encrypt; + try { + encrypt = crypto.encrypt(config); + } catch (e) { + log.send(logLevels.ERROR, 'Error encrypting : ' + e); + if (fs.existsSync(this.lz4Temp)) { + fs.unlinkSync(this.lz4Temp); + } + reject(); + return; + } let encryptionProcess = input.pipe(encrypt).pipe(outputEncryption); encryptionProcess.on('finish', (err) => { if (err) { log.send(logLevels.ERROR, 'Crypto: Error while encrypting the compressed file: ' + err); + if (fs.existsSync(this.lz4Temp)) { + fs.unlinkSync(this.lz4Temp); + } reject(new Error(err)); return; } - fs.unlinkSync(`${this.dump}/${this.permanentIndexName}${searchConfig.TAR_LZ4_EXT}`); + if (fs.existsSync(this.lz4Temp)) { + fs.unlinkSync(this.lz4Temp); + } resolve('Success'); }); }); @@ -88,23 +104,33 @@ class Crypto { } const input = fs.createReadStream(this.encryptedIndex); - const output = fs.createWriteStream(`${this.dump}/decrypted${searchConfig.TAR_LZ4_EXT}`); + const output = fs.createWriteStream(this.decryptedTemp); let config = { key: this.key }; - const decrypt = crypto.decrypt(config); + let decrypt; + try { + decrypt = crypto.decrypt(config); + } catch (e) { + log.send(logLevels.ERROR, 'Error decrypting : ' + e); + if (fs.existsSync(this.decryptedTemp)) { + fs.unlinkSync(this.decryptedTemp); + } + reject(); + return; + } let decryptionProcess = input.pipe(decrypt).pipe(output); decryptionProcess.on('finish', () => { - if (!fs.existsSync(`${this.dump}/decrypted${searchConfig.TAR_LZ4_EXT}`)){ + if (!fs.existsSync(this.decryptedTemp)){ log.send(logLevels.ERROR, 'decrypted.tar.lz4 file not found'); reject(); return; } - lz4.deCompression(`${this.dump}/decrypted${searchConfig.TAR_LZ4_EXT}`,(error, response) => { + lz4.deCompression(this.decryptedTemp,(error, response) => { if (error) { log.send(logLevels.ERROR, 'Crypto: Error while deCompression, ' + error); // no return, need to unlink if error @@ -113,7 +139,7 @@ class Crypto { if (response && response.stderr) { log.send(logLevels.WARN, 'Crypto: Child process stderr while deCompression, ' + response.stderr); } - fs.unlink(`${this.dump}/decrypted${searchConfig.TAR_LZ4_EXT}`, () => { + fs.unlink(this.decryptedTemp, () => { resolve('success'); }); }) diff --git a/js/preload/preloadMain.js b/js/preload/preloadMain.js index 1c7f5b0d..6a8d138f 100644 --- a/js/preload/preloadMain.js +++ b/js/preload/preloadMain.js @@ -369,8 +369,17 @@ function createAPI() { }); } + // reset the badge count whenever an user refreshes the electron client + function resetBadgeCount() { + local.ipcRenderer.send(apiName, { + cmd: apiCmds.setBadgeCount, + count: 0 + }); + } + window.addEventListener('offline', updateOnlineStatus, false); window.addEventListener('online', updateOnlineStatus, false); + window.addEventListener('beforeunload', resetBadgeCount, false); updateOnlineStatus(); } \ No newline at end of file diff --git a/js/search/queue.js b/js/search/queue.js index 65452808..ab635167 100644 --- a/js/search/queue.js +++ b/js/search/queue.js @@ -6,7 +6,7 @@ let makeBoundTimedCollector = function(isIndexing, timeout, callback) { return function (...args) { if (!timer){ timer = setTimeout(function(){ - if (!isIndexing) { + if (!isIndexing()) { flush(getQueue()); } }, timeout); @@ -24,7 +24,9 @@ let makeBoundTimedCollector = function(isIndexing, timeout, callback) { clearTimeout(timer); timer = null; resetQueue(); - callback(JSON.stringify(queue)); + if (queue) { + callback(JSON.stringify(queue)); + } } function getQueue(){ diff --git a/js/search/search.js b/js/search/search.js index 239a9eba..01886f04 100644 --- a/js/search/search.js +++ b/js/search/search.js @@ -31,7 +31,7 @@ class Search { this.isInitialized = false; this.userId = userId; this.key = key; - this.indexFolderName = `${searchConfig.FOLDERS_CONSTANTS.PREFIX_NAME_PATH}_${this.userId}_${searchConfig.INDEX_VERSION}`; + this.indexFolderName = `${searchConfig.FOLDERS_CONSTANTS.PREFIX_NAME_PATH}_${this.userId}`; this.dataFolder = searchConfig.FOLDERS_CONSTANTS.INDEX_PATH; this.realTimeIndex = searchConfig.FOLDERS_CONSTANTS.TEMP_REAL_TIME_INDEX; this.batchIndex = searchConfig.FOLDERS_CONSTANTS.TEMP_BATCH_INDEX_FOLDER; @@ -92,7 +92,7 @@ class Search { return new Promise((resolve, reject) => { if (!messages) { log.send(logLevels.ERROR, 'Batch Indexing: Messages not provided'); - reject(new Error('Batch Indexing: Messages is required')); + reject(new Error('Batch Indexing: Messages are required')); return; } @@ -115,6 +115,12 @@ class Search { return; } + if (!fs.existsSync(this.dataFolder)) { + log.send(logLevels.ERROR, 'User index folder not found'); + reject(new Error('User index folder not found')); + return; + } + const indexId = randomString.generate(searchConfig.BATCH_RANDOM_INDEX_PATH_LENGTH); libSymphonySearch.symSECreatePartialIndexAsync(this.batchIndex, indexId, messages, (err, res) => { if (err) { @@ -133,6 +139,13 @@ class Search { */ mergeIndexBatches() { return new Promise((resolve, reject) => { + + if (!fs.existsSync(this.dataFolder)) { + log.send(logLevels.ERROR, 'User index folder not found'); + reject(new Error('User index folder not found')); + return; + } + libSymphonySearch.symSEMergePartialIndexAsync(this.indexFolderName, this.batchIndex, (err, res) => { if (err) { log.send(logLevels.ERROR, 'Error merging the index ->' + err); @@ -169,10 +182,6 @@ class Search { * @param message */ realTimeIndexing(message) { - if (!message) { - log.send(logLevels.ERROR, 'RealTime Indexing: Messages not provided'); - return new Error('RealTime Indexing: Messages is required'); - } try { let msg = JSON.parse(message); @@ -182,12 +191,17 @@ class Search { } } catch(e) { log.send(logLevels.ERROR, 'RealTime Indexing: parse error -> ' + e); - return (new Error(e)); + throw (new Error(e)); } if (!this.isInitialized) { log.send(logLevels.ERROR, 'Library not initialized'); - return new Error('Library not initialized'); + throw new Error('Library not initialized'); + } + + if (!fs.existsSync(this.dataFolder)) { + log.send(logLevels.ERROR, 'User index folder not found'); + throw new Error('User index folder not found'); } this.isRealTimeIndexing = true; @@ -195,7 +209,7 @@ class Search { this.isRealTimeIndexing = false; if (err) { log.send(logLevels.ERROR, 'RealTime Indexing: error -> ' + err); - return new Error(err); + throw new Error(err); } return result; }); @@ -268,7 +282,7 @@ class Search { if (!fs.existsSync(this.indexFolderName) || !fs.existsSync(this.realTimeIndex)) { log.send(logLevels.ERROR, 'Index folder does not exist.'); - reject('Index folder does not exist.'); + reject(new Error('Index folder does not exist.')); return; } @@ -296,15 +310,15 @@ class Search { } } - if (!_limit && _limit === "" && typeof _limit !== 'number' && Math.round(_limit) !== _limit) { + if (!_limit || _limit === "" || typeof _limit !== 'number' || Math.round(_limit) !== _limit) { _limit = 25; } - if (!_offset && _offset === "" && typeof _offset !== 'number' && Math.round(_offset) !== _offset) { + if (!_offset || _offset === "" || typeof _offset !== 'number' || Math.round(_offset) !== _offset) { _offset = 0 } - if (!_sortOrder && _sortOrder === "" && typeof _sortOrder !== 'number' && Math.round(_sortOrder) !== _sortOrder) { + if (!_sortOrder || _sortOrder === "" || typeof _sortOrder !== 'number' || Math.round(_sortOrder) !== _sortOrder) { _sortOrder = searchConfig.SORT_BY_SCORE; } @@ -327,13 +341,13 @@ class Search { return new Promise((resolve, reject) => { if (!this.isInitialized) { log.send(logLevels.ERROR, 'Library not initialized'); - reject('Not initialized'); + reject(new Error('Not initialized')); return; } if (!fs.existsSync(this.indexFolderName)) { log.send(logLevels.ERROR, 'Index folder does not exist.'); - reject('Index folder does not exist.'); + reject(new Error('Index folder does not exist.')); return; } diff --git a/js/search/searchConfig.js b/js/search/searchConfig.js index 9be1bf89..3f38ad64 100644 --- a/js/search/searchConfig.js +++ b/js/search/searchConfig.js @@ -51,7 +51,6 @@ const searchConfig = { REAL_TIME_INDEXING_TIME: 60000, MINIMUM_DATE: '0000000000000', MAXIMUM_DATE: '9999999999999', - INDEX_VERSION: 'v1', SORT_BY_SCORE: 0, BATCH_RANDOM_INDEX_PATH_LENGTH: 20, LIBRARY_CONSTANTS: libraryPaths, diff --git a/js/search/searchUtils.js b/js/search/searchUtils.js index 3ab3985f..4269f0f3 100644 --- a/js/search/searchUtils.js +++ b/js/search/searchUtils.js @@ -23,14 +23,7 @@ class SearchUtils { if (!isMac) { this.path = this.path.substring(0, 2); } - checkDiskSpace(this.path, function (error, res) { - - if (error) { - return reject(new Error(error)); - } - - return resolve(res >= searchConfig.MINIMUM_DISK_SPACE); - }); + checkDiskSpace(this.path, resolve, reject); }); } @@ -120,7 +113,13 @@ function createUser(userId, oldConfig) { function createUserConfigFile(userId, data) { let createStream = fs.createWriteStream(searchConfig.FOLDERS_CONSTANTS.USER_CONFIG_FILE); if (data) { - createStream.write(`{"${userId}": ${JSON.stringify(data)}}`); + let jsonData; + try { + jsonData = JSON.stringify(data); + createStream.write(`{"${userId}": ${jsonData}}`); + } catch (e) { + createStream.write(`{"${userId}": {}}`); + } } else { createStream.write(`{"${userId}": {}}`); } @@ -149,7 +148,7 @@ function updateConfig(userId, data, resolve, reject) { oldConfig = JSON.parse(oldData); } catch (e) { createUserConfigFile(userId, data); - return reject('can not parse user config file data: ' + e); + return reject(new Error('can not parse user config file data: ' + e)); } let newConfig = Object.assign({}, oldConfig); diff --git a/js/search/utils/checkDiskSpace.js b/js/search/utils/checkDiskSpace.js index 08fd297e..f8d1c729 100644 --- a/js/search/utils/checkDiskSpace.js +++ b/js/search/utils/checkDiskSpace.js @@ -1,42 +1,43 @@ const { exec } = require('child_process'); const { isMac } = require('../../utils/misc'); +const searchConfig = require('../searchConfig.js'); -function checkDiskSpace(path, callback) { +function checkDiskSpace(path, resolve, reject) { if (!path) { - return "Please provide path" + reject(new Error("Please provide path")); + return; } if (isMac) { exec("df -k '" + path.replace(/'/g,"'\\''") + "'", (error, stdout, stderr) => { if (error) { if (stderr.indexOf("No such file or directory") !== -1) { - return callback("No such file or directory : " + error) + return reject(new Error("No such file or directory : " + error)) } - return callback("Error : " + error) + return reject(new Error("Error : " + error)); } let data = stdout.trim().split("\n"); let disk_info_str = data[data.length - 1].replace( /[\s\n\r]+/g,' '); let freeSpace = disk_info_str.split(' '); - return callback(null, freeSpace[3] * 1024); + let space = freeSpace[3] * 1024; + return resolve(space >= searchConfig.MINIMUM_DISK_SPACE); }); } else { exec(`fsutil volume diskfree ${path}`, (error, stdout, stderr) => { if (error) { if (stderr.indexOf("No such file or directory") !== -1) { - return callback("No such file or directory : " + error) + return reject(new Error("No such file or directory : " + error)); } - return callback("Error : " + error) + return reject(new Error("Error : " + error)); } let data = stdout.trim().split("\n"); let disk_info_str = data[data.length - 1].split(':'); - return callback(null, disk_info_str[1]); + return resolve(disk_info_str[1] >= searchConfig.MINIMUM_DISK_SPACE); }); } - - return null; } module.exports = { diff --git a/js/windowMgr.js b/js/windowMgr.js index 9c8de54e..3dc0a287 100644 --- a/js/windowMgr.js +++ b/js/windowMgr.js @@ -311,16 +311,18 @@ function doCreateMainWindow(initialUrl, initialBounds) { // open external links in default browser - a tag with href='_blank' or window.open mainWindow.webContents.on('new-window', function (event, newWinUrl, frameName, disposition, newWinOptions) { - + let newWinParsedUrl = getParsedUrl(newWinUrl); let mainWinParsedUrl = getParsedUrl(url); let newWinHost = newWinParsedUrl && newWinParsedUrl.host; let mainWinHost = mainWinParsedUrl && mainWinParsedUrl.host; + let emptyUrlString = 'about:blank'; + // only allow window.open to succeed is if coming from same hsot, // otherwise open in default browser. - if (disposition === 'new-window' && newWinHost === mainWinHost) { + if (disposition === 'new-window' && ((newWinHost === mainWinHost) || newWinUrl === emptyUrlString)) { // handle: window.open if (!frameName) { diff --git a/tests/Search.test.js b/tests/Search.test.js new file mode 100644 index 00000000..3ea02e21 --- /dev/null +++ b/tests/Search.test.js @@ -0,0 +1,527 @@ +const childProcess = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +let executionPath = null; +let userConfigDir = null; + +let searchConfig; +let SearchApi; + +jest.mock('electron', function() { + return { + app: { + getPath: mockedGetPath + } + } +}); + +function mockedGetPath(type) { + if (type === 'exe') { + return executionPath; + } + + if (type === 'userData') { + return userConfigDir + } + return ''; +} + +describe('Tests for Search', function() { + + let userId; + let key; + let dataFolderPath; + let realTimeIndexPath; + let tempBatchPath; + let currentDate = new Date().getTime(); + + jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000; + + beforeAll(function (done) { + childProcess.exec(`npm rebuild --target=${process.version} --build-from-source`, function(err) { + + userId = 12345678910112; + key = 'jjjehdnctsjyieoalskcjdhsnahsadndfnusdfsdfsd='; + + executionPath = path.join(__dirname, 'library'); + userConfigDir = path.join(__dirname, '..'); + + searchConfig = require('../js/search/searchConfig.js'); + const { Search } = require('../js/search/search.js'); + SearchApi = new Search(userId, key); + + realTimeIndexPath = path.join(userConfigDir, 'data', 'temp_realtime_index'); + tempBatchPath = path.join(userConfigDir, 'data', 'temp_batch_indexes'); + dataFolderPath = path.join(searchConfig.FOLDERS_CONSTANTS.EXEC_PATH, '..', 'data'); + if (fs.existsSync(dataFolderPath)) { + fs.unlinkSync(dataFolderPath) + } + done(); + }); + }); + + afterAll(function (done) { + setTimeout(function () { + + deleteIndexFolders(dataFolderPath); + let root = path.join(searchConfig.FOLDERS_CONSTANTS.EXEC_PATH, '..', `${searchConfig.FOLDERS_CONSTANTS.PREFIX_NAME}_${userId}.enc`); + if (fs.existsSync(root)) { + fs.unlinkSync(root); + } + + done(); + }, 3000); + }); + + function deleteIndexFolders(location) { + if (fs.existsSync(location)) { + fs.readdirSync(location).forEach(function(file) { + let curPath = location + "/" + file; + if (fs.lstatSync(curPath).isDirectory()) { + deleteIndexFolders(curPath); + } else { + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(location); + } + } + + describe('Search Initial checks', function() { + + it('should be initialized', function (done) { + setTimeout(function () { + + expect(SearchApi.isInitialized).toBe(true); + expect(SearchApi.indexFolderName).toBe(`${searchConfig.FOLDERS_CONSTANTS.PREFIX_NAME_PATH}_${userId}`); + expect(SearchApi.dataFolder).toBe(searchConfig.FOLDERS_CONSTANTS.INDEX_PATH); + expect(SearchApi.realTimeIndex).toBe(searchConfig.FOLDERS_CONSTANTS.TEMP_REAL_TIME_INDEX); + expect(SearchApi.batchIndex).toBe(searchConfig.FOLDERS_CONSTANTS.TEMP_BATCH_INDEX_FOLDER); + expect(SearchApi.messageData).toEqual([]); + expect(SearchApi.isRealTimeIndexing).toBe(false); + + done(); + }, 3000) + }); + + it('should isLibInit to true', function () { + let init = SearchApi.isLibInit(); + expect(init).toEqual(true); + }); + + it('should isLibInit to false', function () { + SearchApi.isInitialized = false; + let init = SearchApi.isLibInit(); + expect(init).toEqual(false); + SearchApi.isInitialized = true; + }); + + it('should exist index folder', function() { + expect(fs.existsSync(path.join(userConfigDir, 'data', 'search_index_12345678910112'))).toBe(true); + expect(fs.existsSync(realTimeIndexPath)).toBe(true); + }); + + it('should not exist index folder', function() { + expect(fs.existsSync(tempBatchPath)).toBe(false); + }); + }); + + describe('Batch indexing process tests', function () { + + it('should index in a batch', function (done) { + let messages = [{ + messageId: "Jc+4K8RtPxHJfyuDQU9atX///qN3KHYXdA==", + threadId: "Au8O2xKHyX1LtE6zW019GX///rZYegAtdA==", + ingestionDate: currentDate.toString(), + senderId: "71811853189212", + chatType: "CHATROOM", + isPublic: "false", + sendingApp: "lc", + text: "it works" + }, { + messageId: "Jc+4K8RtPxHJfyuDQU9atX///qN3KHYXdA==", + threadId: "Au8O2xKHyX1LtE6zW019GX///rZYegAtdA==", + ingestionDate: currentDate.toString(), + senderId: "71811853189212", + chatType: "CHATROOM", + isPublic: "false", + sendingApp: "lc", + text: "it works" + }, { + messageId: "Jc+4K8RtPxHJfyuDQU9atX///qN3KHYXdA==", + threadId: "Au8O2xKHyX1LtE6zW019GX///rZYegAtdA==", + ingestionDate: currentDate.toString(), + senderId: "71811853189212", + chatType: "CHATROOM", + isPublic: "false", + sendingApp: "lc", + text: "it works" + }]; + const indexBatch = jest.spyOn(SearchApi, 'indexBatch'); + SearchApi.indexBatch(JSON.stringify(messages)).then(function () { + expect(fs.existsSync(tempBatchPath)).toBe(true); + expect(indexBatch).toHaveBeenCalledWith(JSON.stringify(messages)); + done(); + }); + }); + + it('should not batch index', function (done) { + const indexBatch = jest.spyOn(SearchApi, 'indexBatch'); + SearchApi.indexBatch().catch(function (err) { + expect(indexBatch).toHaveBeenCalled(); + expect(err).toBeTruthy(); + done(); + }); + }); + + it('should not batch index invalid object', function (done) { + const indexBatch = jest.spyOn(SearchApi, 'indexBatch'); + SearchApi.indexBatch('message').catch(function (err) { + expect(err).toBeTruthy(); + expect(indexBatch).toHaveBeenCalledWith('message'); + done(); + }); + }); + + it('should not batch index parse error', function (done) { + let message = { + messageId: "Jc+4K8RtPxHJfyuDQU9atX///qN3KHYXdA==", + threadId: "Au8O2xKHyX1LtE6zW019GX///rZYegAtdA==", + ingestionDate: currentDate.toString(), + senderId: "71811853189212", + chatType: "CHATROOM", + isPublic: "false", + sendingApp: "lc", + text: "it works" + }; + const indexBatch = jest.spyOn(SearchApi, 'indexBatch'); + SearchApi.indexBatch(JSON.stringify(message)).catch(function (err) { + expect(err).toBeTruthy(); + expect(indexBatch).toHaveBeenCalled(); + done(); + }); + }); + + it('should not batch index isInitialized is false', function (done) { + SearchApi.isInitialized = false; + let message = [ { + messageId: "Jc+4K8RtPxHJfyuDQU9atX///qN3KHYXdA==", + threadId: "Au8O2xKHyX1LtE6zW019GX///rZYegAtdA==", + ingestionDate: currentDate.toString(), + senderId: "71811853189212", + chatType: "CHATROOM", + isPublic: "false", + sendingApp: "lc", + text: "it fails" + } ]; + const indexBatch = jest.spyOn(SearchApi, 'indexBatch'); + SearchApi.indexBatch(JSON.stringify(message)).catch(function (err) { + expect(err).toBeTruthy(); + expect(indexBatch).toHaveBeenCalledWith(JSON.stringify(message)); + SearchApi.isInitialized = true; + done(); + }); + }); + + it('should match messages length after batch indexing', function (done) { + const searchQuery = jest.spyOn(SearchApi, 'searchQuery'); + SearchApi.searchQuery('it works', [], [], '', undefined, undefined, 25, 0, 0).then(function (res) { + expect(res.messages.length).toEqual(0); + expect(searchQuery).toHaveBeenCalled(); + done() + }); + }); + + it('should merge batch index to user index', function (done) { + const mergeIndexBatches = jest.spyOn(SearchApi, 'mergeIndexBatches'); + SearchApi.mergeIndexBatches().then(function () { + expect(fs.existsSync(tempBatchPath)).toBe(false); + expect(mergeIndexBatches).toHaveBeenCalled(); + done(); + }); + }); + + it('should match messages length after batch indexing', function (done) { + const searchQuery = jest.spyOn(SearchApi, 'searchQuery'); + SearchApi.searchQuery('it works', [], [], '', undefined, undefined, 25, 0, 0).then(function (res) { + expect(res.messages.length).toEqual(3); + expect(searchQuery).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('RealTime indexing process', function () { + + it('should index realTime message', function () { + let message = [{ + messageId: "Jc+4K8RtPxHJfyuDQU9atX///qN3KHYXdA==", + threadId: "Au8O2xKHyX1LtE6zW019GX///rZYegAtdA==", + ingestionDate: currentDate.toString(), + senderId: "71811853189212", + chatType: "CHATROOM", + isPublic: "false", + sendingApp: "lc", + text: "realtime working" + }]; + + const batchRealTimeIndexing = jest.spyOn(SearchApi, 'batchRealTimeIndexing'); + SearchApi.batchRealTimeIndexing(message); + expect(batchRealTimeIndexing).toHaveBeenCalled(); + }); + + it('should match message length', function (done) { + const searchQuery = jest.spyOn(SearchApi, 'searchQuery'); + SearchApi.searchQuery('realtime working', ["71811853189212"], ["Au8O2xKHyX1LtE6zW019GX///rZYegAtdA=="], '', undefined, undefined, 25, 0, 0).then(function (res) { + expect(res.messages.length).toEqual(3); + expect(fs.existsSync(realTimeIndexPath)).toBe(true); + expect(searchQuery).toHaveBeenCalled(); + done(); + }) + }); + + it('should not index realTime message', function (done) { + let message = [{ + messageId: "Jc+4K8RtPxHJfyuDQU9atX///qN3KHYXdA==", + threadId: "Au8O2xKHyX1LtE6zW019GX///rZYegAtdA==", + ingestionDate: currentDate.toString(), + senderId: "71811853189212", + chatType: "CHATROOM", + isPublic: "false", + sendingApp: "lc", + text: "isRealTimeIndexing" + }]; + + const batchRealTimeIndexing = jest.spyOn(SearchApi, 'batchRealTimeIndexing'); + const realTimeIndexing = jest.spyOn(SearchApi, 'realTimeIndexing'); + SearchApi.isRealTimeIndexing = true; + expect(SearchApi.checkIsRealTimeIndexing()).toBe(true); + SearchApi.batchRealTimeIndexing(message); + expect(batchRealTimeIndexing).toHaveBeenCalled(); + expect(realTimeIndexing).not.toBeCalled(); + setTimeout(function () { + + SearchApi.searchQuery('isRealTimeIndexing', [], [], '', undefined, undefined, 25, 0, 0).then(function (res) { + expect(res.messages.length).toEqual(0); + expect(fs.existsSync(realTimeIndexPath)).toBe(true); + + done(); + }); + }, 6000) + }); + + it('should not call the real-time index', function () { + let message = [{ + messageId: "Jc+4K8RtPxHJfyuDQU9atX///qN3KHYXdA==", + threadId: "Au8O2xKHyX1LtE6zW019GX///rZYegAtdA==", + ingestionDate: currentDate.toString(), + senderId: "71811853189212", + chatType: "CHATROOM", + isPublic: "false", + sendingApp: "lc", + text: "isRealTimeIndexing" + }]; + + const batchRealTimeIndexing = jest.spyOn(SearchApi, 'batchRealTimeIndexing'); + const realTimeIndexing = jest.spyOn(SearchApi, 'realTimeIndexing'); + SearchApi.isRealTimeIndexing = true; + SearchApi.batchRealTimeIndexing(message); + expect(batchRealTimeIndexing).toHaveBeenCalled(); + expect(realTimeIndexing).not.toBeCalled(); + }); + + it('should not realTime index invalid object', function () { + let message = [{ + messageId: "Jc+4K8RtPxHJfyuDQU9atX///qN3KHYXdA==", + threadId: "Au8O2xKHyX1LtE6zW019GX///rZYegAtdA==", + ingestionDate: currentDate.toString(), + senderId: "71811853189212", + chatType: "CHATROOM", + isPublic: "false", + sendingApp: "lc", + text: "isRealTimeIndexing" + }]; + const realTimeIndexing = jest.spyOn(SearchApi, 'realTimeIndexing'); + expect(function () { + SearchApi.realTimeIndexing('message') + }).toThrow(); + + expect(function () { + SearchApi.realTimeIndexing() + }).toThrow(); + + SearchApi.isInitialized = false; + expect(function () { + SearchApi.realTimeIndexing(JSON.stringify(message)) + }).toThrow(new Error('Library not initialized')); + SearchApi.isInitialized = true; + expect(realTimeIndexing).toHaveBeenCalled(); + expect(realTimeIndexing).toHaveBeenCalledTimes(3); + }); + + it('should return realTime bool', function () { + const checkIsRealTimeIndexing = jest.spyOn(SearchApi, 'checkIsRealTimeIndexing'); + SearchApi.isRealTimeIndexing = true; + expect(SearchApi.checkIsRealTimeIndexing()).toBe(true); + SearchApi.isRealTimeIndexing = false; + expect(SearchApi.checkIsRealTimeIndexing()).toBe(false); + expect(checkIsRealTimeIndexing).toHaveBeenCalled(); + expect(checkIsRealTimeIndexing).toHaveBeenCalledTimes(2); + }); + + it('should delete realtime index', function () { + const deleteRealTimeFolder = jest.spyOn(SearchApi, 'deleteRealTimeFolder'); + SearchApi.deleteRealTimeFolder(); + expect(fs.existsSync(realTimeIndexPath)).toBe(true); + expect(deleteRealTimeFolder).toHaveBeenCalled(); + }); + }); + + describe('Test for encryption of the index', function () { + + it('should encrypt user index', function (done) { + const encryptIndex = jest.spyOn(SearchApi, 'encryptIndex'); + SearchApi.encryptIndex(key); + expect(encryptIndex).toHaveBeenCalled(); + done(); + }); + + it('should exist encrypted file', function (done) { + setTimeout(function () { + + expect(fs.existsSync(path.join(userConfigDir, 'search_index_12345678910112.enc'))).toBe(true); + expect(fs.existsSync(path.join(userConfigDir, 'search_index_12345678910112.tar.lz4'))).toBe(false); + + done(); + }, 3000); + }); + }); + + describe('Test for latest timestamp', function () { + + it('should get the latest timestamp', function (done) { + const getLatestMessageTimestamp = jest.spyOn(SearchApi, 'getLatestMessageTimestamp'); + SearchApi.getLatestMessageTimestamp().then(function (res) { + expect(res).toEqual(currentDate.toString()); + expect(getLatestMessageTimestamp).toHaveBeenCalled(); + done(); + }); + }); + + it('should not get the latest timestamp', function (done) { + const getLatestMessageTimestamp = jest.spyOn(SearchApi, 'getLatestMessageTimestamp'); + SearchApi.isInitialized = false; + SearchApi.getLatestMessageTimestamp().catch(function (err) { + expect(err).toEqual(new Error('Not initialized')); + expect(getLatestMessageTimestamp).toHaveBeenCalled(); + SearchApi.isInitialized = true; + done(); + }); + }); + + it('should not get the latest timestamp', function (done) { + SearchApi.indexFolderName = ''; + const getLatestMessageTimestamp = jest.spyOn(SearchApi, 'getLatestMessageTimestamp'); + SearchApi.getLatestMessageTimestamp().catch(function (err) { + expect(err).toEqual(new Error('Index folder does not exist.')); + SearchApi.indexFolderName = `${dataFolderPath}/${searchConfig.FOLDERS_CONSTANTS.PREFIX_NAME}_${userId}`; + expect(getLatestMessageTimestamp).toHaveBeenCalled(); + expect(getLatestMessageTimestamp).toHaveBeenCalledTimes(3); + done(); + }); + }); + }); + + describe('Test to decrypt the index', function () { + + it('should decrypt the index', function () { + deleteIndexFolders(dataFolderPath); + const decryptAndInit = jest.spyOn(SearchApi, 'decryptAndInit'); + SearchApi.decryptAndInit(); + expect(decryptAndInit).toHaveBeenCalled(); + }); + + it('should get message from the decrypted index', function (done) { + setTimeout(function () { + const searchQuery = jest.spyOn(SearchApi, 'searchQuery'); + let endTime = new Date().getTime(); + let startTime = new Date().getTime() - (4 * 31 * 24 * 60 * 60 * 1000); + SearchApi.searchQuery('it works', [], [], '', startTime.toString(), endTime.toString(), '0', 0.2, 0.1).then(function (res) { + expect(res.messages.length).toEqual(3); + expect(searchQuery).toHaveBeenCalled(); + done() + }); + }, 3000) + }); + }); + + describe('Test for search functions', function () { + + it('should search fail isInitialized is false', function (done) { + const searchQuery = jest.spyOn(SearchApi, 'searchQuery'); + SearchApi.isInitialized = false; + SearchApi.searchQuery('it works', [], [], '', '', '', 25, 0, 0).catch(function (err) { + expect(err).toEqual(new Error('Library not initialized')); + expect(searchQuery).toHaveBeenCalled(); + SearchApi.isInitialized = true; + done(); + }); + }); + + it('should filter search limit ', function (done) { + const searchQuery = jest.spyOn(SearchApi, 'searchQuery'); + SearchApi.searchQuery('works', [], [], '', '', '', 2, 0, 0).then(function (res) { + expect(res.messages.length).toBe(2); + expect(searchQuery).toHaveBeenCalledTimes(7); + expect(searchQuery).toHaveBeenCalled(); + done(); + }); + }); + + it('should search fails index folder not fund', function (done) { + const searchQuery = jest.spyOn(SearchApi, 'searchQuery'); + deleteIndexFolders(dataFolderPath); + SearchApi.searchQuery('it works', [], [], '', '', '', 25, 0, 0).catch(function (err) { + expect(err).toEqual(new Error('Index folder does not exist.')); + expect(searchQuery).toHaveBeenCalledTimes(8); + expect(searchQuery).toHaveBeenCalled(); + SearchApi = undefined; + const { Search } = require('../js/search/search.js'); + SearchApi = new Search(userId, key); + done(); + }); + }); + + it('should search fails query is undefined', function (done) { + setTimeout(function () { + const searchQuery = jest.spyOn(SearchApi, 'searchQuery'); + expect(SearchApi.isInitialized).toBe(true); + SearchApi.searchQuery(undefined, [], [], '', '', '', 25, 0, 0).catch(function (err) { + expect(err).toEqual(new Error('Search query error')); + expect(searchQuery).toHaveBeenCalled(); + done(); + }); + }, 3000); + }); + + it('should search for hashtag', function (done) { + const searchQuery = jest.spyOn(SearchApi, 'searchQuery'); + SearchApi.searchQuery('#123 "testing"', [], [], 'attachment', '', '', 25, 0, 0).then(function (res) { + expect(res.messages.length).toEqual(0); + expect(searchQuery).toHaveBeenCalled(); + done(); + }); + }); + + it('should search for pdf', function (done) { + const searchQuery = jest.spyOn(SearchApi, 'searchQuery'); + SearchApi.searchQuery('', [], [], 'pdf', '', '', 25, 0, 0).then(function (res) { + expect(res.messages.length).toEqual(0); + expect(searchQuery).toHaveBeenCalled(); + expect(searchQuery).toHaveBeenCalledTimes(3); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/tests/SearchUtils.test.js b/tests/SearchUtils.test.js new file mode 100644 index 00000000..fc3ccb0c --- /dev/null +++ b/tests/SearchUtils.test.js @@ -0,0 +1,152 @@ +const fs = require('fs'); +const path = require('path'); + +let executionPath = null; +let userConfigDir = null; + +let SearchUtilsAPI; +let searchConfig; + +jest.mock('electron', function() { + return { + app: { + getPath: mockedGetPath + } + } +}); + +function mockedGetPath(type) { + switch (type) { + case 'exe': + return executionPath; + case 'userData': + return userConfigDir; + default: + return '' + } +} + +describe('Tests for Search Utils', function() { + + jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000; + + beforeAll(function (done) { + executionPath = path.join(__dirname, 'library'); + userConfigDir = path.join(__dirname, '..'); + searchConfig = require('../js/search/searchConfig.js'); + const { SearchUtils } = require('../js/search/searchUtils.js'); + SearchUtilsAPI = new SearchUtils(); + SearchUtilsAPI.path = userConfigDir; + if (fs.existsSync(searchConfig.FOLDERS_CONSTANTS.USER_CONFIG_FILE)) { + fs.unlinkSync(searchConfig.FOLDERS_CONSTANTS.USER_CONFIG_FILE); + } + done(); + }); + + afterAll(function (done) { + fs.unlinkSync(searchConfig.FOLDERS_CONSTANTS.USER_CONFIG_FILE); + done(); + }); + + describe('Tests for checking disk space', function () { + + it('should return free space', function (done) { + const checkFreeSpace = jest.spyOn(SearchUtilsAPI, 'checkFreeSpace'); + SearchUtilsAPI.checkFreeSpace().then(function () { + expect(checkFreeSpace).toHaveBeenCalled(); + done(); + }); + }); + + it('should return error', function (done) { + const checkFreeSpace = jest.spyOn(SearchUtilsAPI, 'checkFreeSpace'); + SearchUtilsAPI.path = undefined; + SearchUtilsAPI.checkFreeSpace().catch(function (err) { + expect(err).toEqual(new Error("Please provide path")); + expect(checkFreeSpace).toHaveBeenCalled(); + done(); + }); + }); + + it('should return error invalid path', function (done) { + const checkFreeSpace = jest.spyOn(SearchUtilsAPI, 'checkFreeSpace'); + SearchUtilsAPI.path = './tp'; + SearchUtilsAPI.checkFreeSpace().catch(function (err) { + expect(checkFreeSpace).toHaveBeenCalled(); + expect(err).toEqual(err); + done(); + }); + }); + }); + + describe('Test for search users config', function () { + + it('should return null for new user config', function (done) { + SearchUtilsAPI.getSearchUserConfig(1234567891011).then(function (res) { + expect(res).toEqual(null); + done(); + }); + }); + + it('should exist users config file', function (done) { + setTimeout(function () { + expect(fs.existsSync(searchConfig.FOLDERS_CONSTANTS.USER_CONFIG_FILE)).toEqual(true); + done(); + }, 2000) + }); + + it('should exist users config file', function (done) { + setTimeout(function () { + SearchUtilsAPI.getSearchUserConfig(1234567891011).then(function (res) { + expect(res).toEqual({}); + done(); + }); + }, 3000) + }); + + it('should update user config file', function (done) { + let data = { + rotationId: 0, + version: 1, + language: 'en' + }; + SearchUtilsAPI.updateUserConfig(1234567891011, data).then(function (res) { + expect(res).toEqual(data); + done(); + }) + }); + + it('should modify user config file', function (done) { + let data = { + rotationId: 1, + version: 1, + language: 'en' + }; + SearchUtilsAPI.updateUserConfig(1234567891011, data).then(function (res) { + expect(res.rotationId).toEqual(1); + done(); + }) + }); + + it('should create user if not exist', function (done) { + SearchUtilsAPI.getSearchUserConfig(2234567891011).catch(function (err) { + expect(err).toEqual(null); + done(); + }) + }); + + it('should create file on update', function (done) { + fs.unlinkSync(searchConfig.FOLDERS_CONSTANTS.USER_CONFIG_FILE); + let data = { + rotationId: 0, + version: 2, + language: 'en' + }; + SearchUtilsAPI.updateUserConfig(2234567891011, data).catch(function (err) { + expect(err).toEqual(null); + done(); + }) + }); + }); + +}); \ No newline at end of file