diff --git a/lib/countly-storage.js b/lib/countly-storage.js index 01ded84..c2d1e7f 100644 --- a/lib/countly-storage.js +++ b/lib/countly-storage.js @@ -6,23 +6,52 @@ var storagePath; var __data = {}; var defaultPath = "../data/"; // Default path var defaultBulkPath = "../bulk_data/"; // Default path +var asyncWriteLock = false; +var asyncWriteQueue = []; +/** + * Sets storage path + * If user didn't provide a path, it sets to default path "../data/" + * Instead if a path is provided, sets the storage path as the provided path + * @param {String} userPath - user provided storage path + */ var setStoragePath = function(userPath) { - storagePath = userPath || defaultPath; - var dir = path.resolve(__dirname, getStoragePath()); + if (userPath === undefined || userPath === null) { + storagePath = defaultPath; + } + else { + storagePath = userPath; + } + var dir = path.resolve(__dirname, storagePath); createDirectory(dir); }; +/** + * Sets bulk storage path if persistQueue is enabled. + * If user didn't provide a path, it sets to default path "../bulk_data/" + * Instead if a path is provided, sets the storage path as the provided path + * @param {String} userPath - user provided storage path + * @param {Boolean} persistQueue - whether to persistently store queue until processed. false in default + */ var setBulkDataPath = function(userPath, persistQueue) { - storagePath = userPath || defaultBulkPath; + if (userPath === undefined || userPath === null) { + storagePath = defaultBulkPath; + } + else { + storagePath = userPath; + } var dir = path.resolve(__dirname, getStoragePath()); if (persistQueue) { createDirectory(dir); } }; +/** + * Returns the storage path + * @returns {String} storage path + */ var getStoragePath = function() { - return storagePath; + return storagePath || undefined; }; var createDirectory = function(dir) { @@ -94,9 +123,6 @@ var forceStore = function() { } }; -var asyncWriteLock = false; -var asyncWriteQueue = []; - /** * Write to file and process queue while in asyncWriteLock * @param {String} key - key for value to store @@ -117,7 +143,10 @@ var writeFile = function(key, value, callback) { if (asyncWriteQueue.length) { setTimeout(() => { var arr = asyncWriteQueue.shift(); - writeFile(arr[0], arr[1], arr[2]); + cc.log(cc.logLevelEnums.DEBUG, "writeFile, Dequeued array:", arr); + if (arr) { + writeFile(arr[0], arr[1], arr[2]); + } }, 0); } else { diff --git a/lib/countly.js b/lib/countly.js index 908c793..9965868 100644 --- a/lib/countly.js +++ b/lib/countly.js @@ -169,6 +169,8 @@ Countly.Bulk = Bulk; // Common module debug value is set to init time debug value cc.debug = conf.debug; + + // Set the storage path CountlyStorage.setStoragePath(conf.storage_path); // clear stored device ID if flag is set @@ -717,6 +719,7 @@ Countly.Bulk = Bulk; add_cly_events(event); } }; + /** * Add events to event queue * @memberof Countly._internals diff --git a/test/customStorageDirectory/.gitignore b/test/customStorageDirectory/.gitignore new file mode 100644 index 0000000..86d0cb2 --- /dev/null +++ b/test/customStorageDirectory/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/test/tests_storage.js b/test/tests_storage.js new file mode 100644 index 0000000..715a097 --- /dev/null +++ b/test/tests_storage.js @@ -0,0 +1,271 @@ +const assert = require("assert"); +var Countly = require("../lib/countly"); +var storage = require("../lib/countly-storage"); +var cc = require("../lib/countly-common"); +var hp = require("./helpers/helper_functions"); + +// example event object to use +var eventObj = { + key: "storage_check", + count: 5, + sum: 3.14, + dur: 2000, + segmentation: { + app_version: "1.0", + country: "Zambia", + }, +}; + +var userDetailObj = { + name: "Akira Kurosawa", + username: "a_kurosawa", + email: "akira.kurosawa@filmlegacy.com", + organization: "Toho Studios", + phone: "+81312345678", + picture: "https://example.com/profile_images/akira_kurosawa.jpg", + gender: "Male", + byear: 1910, + custom: { + "known for": "Film Director", + "notable works": "Seven Samurai, Rashomon, Ran", + }, +}; + +// init function +function initMain(device_id) { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://test.url.ly", + interval: 10000, + max_events: -1, + device_id: device_id, + }); +} +// TODO: move these to helpers to reduce duplication +function validateSdkGeneratedId(providedDeviceId) { + assert.ok(providedDeviceId); + assert.equal(providedDeviceId.length, 36); + assert.ok(cc.isUUID(providedDeviceId)); +} +function checkRequestsForT(queue, expectedInternalType) { + for (var i = 0; i < queue.length; i++) { + assert.ok(queue[i].t); + assert.equal(queue[i].t, expectedInternalType); + } +} +function validateDeviceId(deviceId, deviceIdType, expectedDeviceId, expectedDeviceIdType) { + var rq = hp.readRequestQueue()[0]; + if (expectedDeviceIdType === cc.deviceIdTypeEnums.SDK_GENERATED) { + validateSdkGeneratedId(deviceId); // for SDK-generated IDs + } + else { + assert.equal(deviceId, expectedDeviceId); // for developer-supplied IDs + } + assert.equal(deviceIdType, expectedDeviceIdType); + checkRequestsForT(rq, expectedDeviceIdType); +} +function recordValuesToStorageAndValidate() { + // Set values + var deviceIdType = cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED; + storage.storeSet("cly_id", "SpecialDeviceId"); + storage.storeSet("cly_id_type", deviceIdType); + + // Set values with different data types + storage.storeSet("cly_count", 42); + storage.storeSet("cly_object", { key: "value" }); + storage.storeSet("cly_null", null); + + // Retrieve and assert values + assert.equal(storage.storeGet("cly_id"), "SpecialDeviceId"); + assert.equal(storage.storeGet("cly_id_type"), deviceIdType); + assert.equal(storage.storeGet("cly_count"), 42); + assert.deepEqual(storage.storeGet("cly_object"), { key: "value" }); + assert.equal(storage.storeGet("cly_null"), null); + + // Remove specific items by overriding with null or empty array + storage.storeSet("cly_id", null); + storage.storeSet("cly_object", []); + assert.equal(storage.storeGet("cly_id"), null); + assert.deepEqual(storage.storeGet("cly_object"), []); + + // Reset storage and check if it's empty again + storage.resetStorage(); + assert.equal(storage.storeGet("cly_id"), undefined); + assert.equal(storage.storeGet("cly_id_type"), undefined); + assert.equal(storage.storeGet("cly_count"), undefined); + assert.equal(storage.storeGet("cly_object"), undefined); + assert.equal(storage.storeGet("cly_null"), undefined); +} + +describe("Storage Tests", () => { + it("1- Store Generated Device ID", (done) => { + // clear previous data + hp.clearStorage(); + // initialize SDK + initMain(); + Countly.begin_session(); + // read request queue + setTimeout(() => { + validateSdkGeneratedId(Countly.get_device_id()); + done(); + }, hp.sWait); + }); + + it("1.1- Validate generated device id after process restart", (done) => { + initMain(); + validateDeviceId(Countly.get_device_id(), Countly.get_device_id_type(), undefined, cc.deviceIdTypeEnums.SDK_GENERATED); + done(); + }); + + it("2.Developer supplied device ID", (done) => { + hp.clearStorage(); + initMain("ID"); + Countly.begin_session(); + setTimeout(() => { + validateDeviceId(Countly.get_device_id(), Countly.get_device_id_type(), "ID", cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED); + done(); + }, hp.sWait); + }); + + it("2.1- Validate generated device id after process restart", (done) => { + validateDeviceId(Countly.get_device_id(), Countly.get_device_id_type(), "ID", cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED); + done(); + }); + + it("3- Record and validate all user details", (done) => { + hp.clearStorage(); + initMain(); + Countly.user_details(userDetailObj); + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + hp.userDetailRequestValidator(userDetailObj, req); + done(); + }, hp.sWait); + }); + + it("3.1- Validate stored user detail", (done) => { + var req = hp.readRequestQueue()[0]; + hp.userDetailRequestValidator(userDetailObj, req); + done(); + }); + + it("4- Record event and validate storage", (done) => { + hp.clearStorage(); + initMain(); + Countly.add_event(eventObj); + setTimeout(() => { + var storedEvents = hp.readEventQueue(); + assert.strictEqual(storedEvents.length, 1, "There should be exactly one event stored"); + + var event = storedEvents[0]; + hp.eventValidator(eventObj, event); + done(); + }, hp.mWait); + }); + + it("4.1- Validate event persistence after process restart", (done) => { + // Initialize SDK + initMain(); + + // Read stored events without clearing storage + var storedEvents = hp.readEventQueue(); + assert.strictEqual(storedEvents.length, 1, "There should be exactly one event stored"); + + var event = storedEvents[0]; + hp.eventValidator(eventObj, event); + done(); + }); + + // if storage path is not provided it will be default "../data/" + it("5- Not provide storage path during init", (done) => { + hp.clearStorage(); + initMain(); + assert.equal(storage.getStoragePath(), "../data/"); + done(); + }); + + // if set to undefined it should be set to default path + it("6- Set storage path to undefined", (done) => { + hp.clearStorage(); + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://test.url.ly", + storage_path: undefined, + }); + assert.equal(storage.getStoragePath(), "../data/"); + done(); + }); + + // if set to null it should be set to default path + it("7- Set storage path to null", (done) => { + hp.clearStorage(); + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://test.url.ly", + storage_path: null, + }); + assert.equal(storage.getStoragePath(), "../data/"); + done(); + }); + + // it should be set to the custom directory if provided + it("8- Set storage path to custom directory", (done) => { + hp.clearStorage(); + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://test.url.ly", + interval: 10000, + max_events: -1, + storage_path: "../test/customStorageDirectory/", + }); + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + done(); + }); + + it("9- Reset Storage While on Default Path /no-init", (done) => { + // will set to default storage path + storage.setStoragePath(); + assert.equal(storage.getStoragePath(), "../data/"); + // will set to undefined + storage.resetStorage(); + assert.equal(storage.getStoragePath(), undefined); + done(); + }); + + it("10- Recording to Storage with Default Storage Path /no-init", (done) => { + storage.resetStorage(); + + // Set to default storage path + storage.setStoragePath(); + assert.equal(storage.getStoragePath(), "../data/"); + recordValuesToStorageAndValidate(); + done(); + }); + + it("11- Recording to Storage with Custom Storage Path /no-init", (done) => { + storage.resetStorage(); + // will set to default storage path + storage.setStoragePath("../test/customStorageDirectory/"); + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + recordValuesToStorageAndValidate(); + done(); + }); + + it("12- Recording to Bulk Storage with Default Bulk Data Path /no-init", (done) => { + storage.resetStorage(); + // will set to default storage path + storage.setBulkDataPath(); + assert.equal(storage.getStoragePath(), "../bulk_data/"); + recordValuesToStorageAndValidate(); + done(); + }); + + it("13- Recording to Bulk Storage with Custom Bulk Storage Path /no-init", (done) => { + storage.resetStorage(); + // will set to default storage path + storage.setBulkDataPath("../test/customStorageDirectory/"); + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + recordValuesToStorageAndValidate(); + done(); + }); +});