From e6cfbfd6dc109657166406c6c186c71bfd0bb068 Mon Sep 17 00:00:00 2001 From: modos189 Date: Thu, 27 Jul 2023 14:41:38 +0300 Subject: [PATCH 1/5] Reset storage state before each test run The storage state is now reset before each test run to ensure test isolation and avoid interference between tests. This guarantees that each test starts with a clean state and avoids potential issues with shared data. --- test/manager.0.base.spec.js | 1 + test/manager.1.build-in.spec.js | 1 + test/manager.2.external.spec.js | 5 ++++- test/manager.3.repo.spec.js | 1 + test/storage.js | 5 +++++ 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/test/manager.0.base.spec.js b/test/manager.0.base.spec.js index 3a07708..9375063 100644 --- a/test/manager.0.base.spec.js +++ b/test/manager.0.base.spec.js @@ -8,6 +8,7 @@ import { expect } from 'chai'; describe('manage.js base integration tests', function () { let manager = null; before(function () { + storage.resetStorage(); const params = { storage: storage, channel: 'beta', diff --git a/test/manager.1.build-in.spec.js b/test/manager.1.build-in.spec.js index 937e932..8d7f8b3 100644 --- a/test/manager.1.build-in.spec.js +++ b/test/manager.1.build-in.spec.js @@ -8,6 +8,7 @@ import { expect } from 'chai'; describe('manage.js build-in plugins integration tests', function () { let manager = null; before(function () { + storage.resetStorage(); const params = { storage: storage, channel: 'release', diff --git a/test/manager.2.external.spec.js b/test/manager.2.external.spec.js index 2cb0823..5804001 100644 --- a/test/manager.2.external.spec.js +++ b/test/manager.2.external.spec.js @@ -21,6 +21,7 @@ const expectThrowsAsync = async (method, errorMessage) => { describe('manage.js external plugins integration tests', function () { let manager = null; before(function () { + storage.resetStorage(); const params = { storage: storage, channel: 'release', @@ -387,7 +388,9 @@ describe('manage.js external plugins integration tests', function () { expect(db_data['release_plugins_flat'][external_1_uid]['status'], "release_plugins_flat['status']: " + external_1_uid).to.equal('off'); - expect(db_data['release_plugins_flat'][external_1_uid]['code'], "release_plugins_flat['code']: " + external_1_uid).to.have.lengthOf(578); + expect(db_data['release_plugins_flat'][external_1_uid]['code'], "release_plugins_flat['code']: " + external_1_uid).to.have.lengthOf( + external_code.length + ); expect(db_data['release_plugins_flat'][external_1_uid]['override'], "release_plugins_flat['override']: " + external_1_uid).to.not.be.true; }); diff --git a/test/manager.3.repo.spec.js b/test/manager.3.repo.spec.js index dd82b9a..9869491 100644 --- a/test/manager.3.repo.spec.js +++ b/test/manager.3.repo.spec.js @@ -8,6 +8,7 @@ import { expect } from 'chai'; describe('manage.js custom repo integration tests', function () { let manager = null; before(function () { + storage.resetStorage(); const params = { storage: storage, channel: 'release', diff --git a/test/storage.js b/test/storage.js index 79d2699..9d15c7b 100644 --- a/test/storage.js +++ b/test/storage.js @@ -48,4 +48,9 @@ export default { console.error('Unexpected type of key when trying to set storage value: ' + typeof obj); } }, + resetStorage() { + for (const key in store) { + delete store[key]; + } + }, }; From 2a9492375fb9423db12a63af9ea7a3a031516072 Mon Sep 17 00:00:00 2001 From: modos189 Date: Thu, 27 Jul 2023 14:53:51 +0300 Subject: [PATCH 2/5] Allow 'null' argument in storage's 'get' method to retrieve all data and add code comments" The 'get' method in the 'storage.js' mock now accepts 'null' as an argument, allowing the retrieval of all data stored in the object. Additionally, comprehensive code comments have been added to explain each function's purpose and behavior in the 'storage.js' file. This improves code readability and maintainability. --- test/storage.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/storage.js b/test/storage.js index 9d15c7b..1bca258 100644 --- a/test/storage.js +++ b/test/storage.js @@ -1,13 +1,25 @@ // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 const store = {}; + export default { + /** + * Converts the input key to an array if it is a string. + * @param {string|Array} key - The key or keys to be converted to an array. + * @returns {Array} An array containing the keys. + */ _one_to_array: function (key) { if (typeof key === 'string') { key = [key]; } return key; }, + + /** + * Retrieves the value associated with the given key from the store. + * @param {string} key - The key for the value to be retrieved. + * @returns {any} The value associated with the key, or null if the key is not found. + */ _get_one(key) { const value = key in store ? store[key] : null; @@ -19,6 +31,12 @@ export default { return value; } }, + + /** + * Sets the value associated with the given key in the store. + * @param {string} key - The key for the value to be set. + * @param {any} value - The value to be stored. + */ _set_one(key, value) { if (typeof value !== 'string') { value = JSON.stringify(value); @@ -26,7 +44,16 @@ export default { store[key] = value; }, + /** + * Retrieves data from the store for the specified keys. + * If 'keys' is null, returns a copy of all data in the store. + * @param {null|string|Array} keys - The key or keys for the data to be retrieved. + * @returns {Object} An object containing the data associated with the keys. + */ async get(keys) { + if (keys === null) { + return { ...store }; // Return a copy of all data in the store + } keys = this._one_to_array(keys); if (Array.isArray(keys)) { const data = {}; @@ -38,6 +65,11 @@ export default { console.error('Unexpected type of key when trying to get storage value: ' + typeof keys); } }, + + /** + * Sets data in the store for the specified keys and values. + * @param {Object} obj - An object containing the keys and values to be stored. + */ async set(obj) { if (typeof obj === 'object') { Object.entries(obj).forEach((entry) => { @@ -48,6 +80,10 @@ export default { console.error('Unexpected type of key when trying to set storage value: ' + typeof obj); } }, + + /** + * Resets the storage by removing all data from the store. + */ resetStorage() { for (const key in store) { delete store[key]; From 994fd8754a046de1f979099f196e784a6e290e66 Mon Sep 17 00:00:00 2001 From: modos189 Date: Thu, 27 Jul 2023 15:52:00 +0300 Subject: [PATCH 3/5] Fix the `storage.get` method in tests to handle null argument Instead of returning a simple copy of the entire storage, it now correctly iterates over all keys in the store and applies the '_get_one' method to each key, returning an object with all the data. --- test/storage.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/storage.js b/test/storage.js index 1bca258..33d4cbd 100644 --- a/test/storage.js +++ b/test/storage.js @@ -52,7 +52,11 @@ export default { */ async get(keys) { if (keys === null) { - return { ...store }; // Return a copy of all data in the store + const data = {}; + for (const key in store) { + data[key] = this._get_one(key); + } + return data; } keys = this._one_to_array(keys); if (Array.isArray(keys)) { From 0060a0a996438c428346c50bdee4645668fca6c8 Mon Sep 17 00:00:00 2001 From: modos189 Date: Fri, 28 Jul 2023 22:12:46 +0300 Subject: [PATCH 4/5] Add methods for exporting and importing backups In the 'Manager' class, the 'getBackupData' and 'setBackupData' methods have been added to handle the export and import of backups, respectively. These methods allow the user to retrieve backup data based on specified parameters and set backup data using the provided backup object. --- package.json | 7 +- src/backup.js | 212 ++++++++++++++++++++++++++++++++++ src/manager.js | 56 +++++++++ src/worker.js | 9 ++ test/manager.9.backup.spec.js | 166 ++++++++++++++++++++++++++ 5 files changed, 447 insertions(+), 3 deletions(-) create mode 100644 src/backup.js create mode 100644 test/manager.9.backup.spec.js diff --git a/package.json b/package.json index ac5b5ef..db799dc 100644 --- a/package.json +++ b/package.json @@ -21,15 +21,16 @@ "homepage": "https://github.com/IITC-CE/lib-iitc-manager#readme", "devDependencies": { "chai": "^4.3.6", - "http-server": "^14.1.0", - "mocha": "^9.2.2", - "node-fetch": "^3.2.3", "eslint": "^8.20.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", + "http-server": "^14.1.0", + "mocha": "^9.2.2", + "node-fetch": "^3.2.3", "prettier": "^2.7.1" }, "dependencies": { + "@bundled-es-modules/deepmerge": "^4.3.1", "xhr2": "^0.2.1" } } diff --git a/src/backup.js b/src/backup.js new file mode 100644 index 0000000..62a4c58 --- /dev/null +++ b/src/backup.js @@ -0,0 +1,212 @@ +// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 + +import { parseMeta } from './helpers.js'; +import deepmerge from '@bundled-es-modules/deepmerge'; + +/** + * Processes the input parameters for backup data retrieval. + * + * This function takes an input object containing parameters for backup data retrieval + * and returns a new object with processed parameters. If the input parameters are not + * an object, an empty object is used as the default value. The function combines the + * input parameters with default parameters to ensure all required properties are present. + * + * @param {BackupParams} params - The parameters for setting the backup data. + * @returns {Object} The processed parameters object. + */ +export function paramsProcessing(params) { + if (typeof params !== 'object') params = {}; + + // Default parameters + const default_params = { + settings: false, + data: false, + external: false, + }; + + // Combine the default parameters with the input parameters using spread syntax + return { ...default_params, ...params }; +} + +/** + * Exports specific IITC settings from the provided storage object. + * + * This function takes a storage object and extracts specific IITC settings based on + * predefined keys. It creates a new object containing only the specified IITC settings + * and returns it. + * + * @param {Object} all_storage - The storage object containing all data. + * @returns {Object} An object containing specific IITC settings. + */ +export const exportIitcSettings = (all_storage) => { + const iitc_settings = {}; + + // An array of predefined keys for IITC settings + const storage_keys = ['channel', 'network_host', 'release_update_check_interval', 'beta_update_check_interval', 'custom_update_check_interval']; + + // Loop through all_storage and check if the keys are present in storage_keys + // If present, add them to the iitc_settings object + for (const key in all_storage) { + if (storage_keys.includes(key)) { + iitc_settings[key] = all_storage[key]; + } + } + return iitc_settings; +}; + +/** + * Exports specific plugin settings from the provided storage object. + * + * This function takes a storage object and extracts plugin settings that have keys starting + * with the prefix 'VMin'. It creates a new object containing only the plugin settings + * and returns it. + * + * @param {Object} all_storage - The storage object containing all data. + * @returns {Object} An object containing specific plugin settings. + */ +export const exportPluginsSettings = (all_storage) => { + const plugins_storage = {}; + + // Loop through all_storage and check if the keys start with the prefix 'VMin' + // If so, add them to the plugins_storage object + for (const key in all_storage) { + if (key.startsWith('VMin')) { + plugins_storage[key] = all_storage[key]; + } + } + return plugins_storage; +}; + +/** + * Exports external plugins from the provided storage object. + * + * This function takes a storage object and extracts external plugins based on predefined keys. + * It creates a new object containing the external plugins organized by their channels and filenames, + * and returns it. + * + * @param {Object} all_storage - The storage object containing all data. + * @returns {Object} An object containing external plugins organized by channels and filenames. + */ +export const exportExternalPlugins = (all_storage) => { + const external_plugins = {}; + + // An array of predefined keys for external plugins + const storage_keys = ['release_plugins_user', 'beta_plugins_user', 'custom_plugins_user']; + + // Loop through all_storage and check if the keys are present in storage_keys + // If present, process and add the external plugins to the external_plugins object + for (const key in all_storage) { + if (storage_keys.includes(key)) { + // Extract the channel name from the key by splitting at '_' + const channel = key.split('_')[0]; + external_plugins[channel] = {}; + + // Loop through each plugin UID in the current key's storage data + for (const plugin_uid in all_storage[key]) { + // Get the plugin's filename and code from the storage data and add to the external_plugins object + const plugin_filename = all_storage[key][plugin_uid]['filename']; + external_plugins[channel][plugin_filename] = all_storage[key][plugin_uid]['code']; + } + } + } + + return external_plugins; +}; + +/** + * Imports IITC settings from the provided backup object. + * + * @async + * @param {Object} self - IITC manager object. + * @param {Object} backup - The backup object containing IITC settings to import. + * @returns {Promise} A promise that resolves when the import is complete. + */ +export const importIitcSettings = async (self, backup) => { + const backup_obj = Object.assign({}, backup); + const default_channel = self.channel; + + // Set the IITC settings from the backup object into the storage + await self.storage.set(backup_obj); + + // Check if the channel in the backup object is different from the original channel + const set_channel = backup_obj.channel; + if (set_channel !== default_channel) { + await self.setChannel(set_channel); + } +}; + +/** + * Imports plugin settings from the provided backup object. + * + * The function first retrieves all data from the storage object + * using `self.storage.get(null)` and filters out the records with keys starting with 'VMin' + * to create a new object `vMinRecords` containing only plugin-related data. The function then + * merges the `vMinRecords` object with the provided backup object using the `deepmerge` library, + * resulting in a new storage object `new_storage` that contains updated plugin settings. Finally, + * the updated storage object is set into the 'self' object using `self.storage.set()`. + * + * @async + * @param {Object} self - IITC manager object. + * @param {Object} backup - The backup object containing plugin settings to import. + * @returns {Promise} A promise that resolves when the import is complete. + */ +export const importPluginsSettings = async (self, backup) => { + const all_storage = await self.storage.get(null); + + // Create a new object containing only plugin-related data (keys starting with 'VMin') + const vMinRecords = {}; + Object.keys(all_storage).forEach((key) => { + if (key.startsWith('VMin')) { + vMinRecords[key] = all_storage[key]; + } + }); + + // Merge the 'vMinRecords' object with the provided backup object and set into storage + const new_storage = deepmerge(vMinRecords, backup); + await self.storage.set(new_storage); +}; + +/** + * Imports external plugins from the provided backup object. + * + * The function iterates through each channel in the backup object, + * sets the current channel using `self.setChannel()`, and then extracts the plugin information + * (metadata and code) for each plugin in the channel. The plugin information is added to the 'scripts' + * array, which is then passed to `self.addUserScripts()` to add the external plugins. After processing + * all channels, the function sets the default channel using `self.setChannel()` if it was changed during + * the import process. + * + * @async + * @param {Object} self - IITC manager object. + * @param {Object} backup - The backup object containing external plugins to import. + * @returns {Promise} A promise that resolves when the import is complete. + */ +export const importExternalPlugins = async (self, backup) => { + const default_channel = self.channel; + + // Iterate through each channel in the backup object + for (const channel of Object.keys(backup)) { + // Initialize an empty array to store the plugin information (metadata and code) + const scripts = []; + await self.setChannel(channel); + + // Iterate through each plugin in the current channel and extract plugin information + for (const [filename, code] of Object.entries(backup[channel])) { + // Parse the metadata from the plugin code using the 'parseMeta()' function + const meta = parseMeta(code); + meta['filename'] = filename; + + // Push the plugin information (metadata and code) to the 'scripts' array + scripts.push({ meta: meta, code: code }); + } + + // Add the external plugins using the 'self.addUserScripts()' method + await self.addUserScripts(scripts); + } + + // If the current channel is different from the default channel, + // set the default channel using the 'self.setChannel()' method + if (self.channel !== default_channel) { + await self.setChannel(default_channel); + } +}; diff --git a/src/manager.js b/src/manager.js index bc9f44e..85d24bc 100644 --- a/src/manager.js +++ b/src/manager.js @@ -3,6 +3,7 @@ import { Worker } from './worker.js'; import * as migrations from './migrations.js'; import { getUID, isSet } from './helpers.js'; +import * as backup from './backup.js'; /** * @classdesc This class contains methods for managing IITC and plugins. @@ -298,4 +299,59 @@ export class Manager extends Worker { if (all_plugins === undefined) return null; return all_plugins[uid]; } + + /** + * Asynchronously retrieves backup data based on the specified parameters. + * + * @async + * @param {BackupParams} params - The parameters for the backup data retrieval. + * @return {Promise} A promise that resolves to the backup data. + */ + async getBackupData(params) { + // Process the input parameters using the 'paramsProcessing' function from the 'backup' module. + params = backup.paramsProcessing(params); + + // Initialize the backup_data object with its properties. + const backup_data = { + external_plugins: {}, + data: { + iitc_settings: {}, + plugins_data: {}, + app: 'IITC Button', + }, + }; + + // Retrieve all_storage using the 'get' method of 'storage' module. + const all_storage = await this.storage.get(null); + + if (params.settings) backup_data.data.iitc_settings = backup.exportIitcSettings(all_storage); + if (params.data) backup_data.data.plugins_data = backup.exportPluginsSettings(all_storage); + if (params.external) backup_data.external_plugins = backup.exportExternalPlugins(all_storage); + + // Return the backup_data object. + return backup_data; + } + + /** + * Asynchronously sets backup data based on the specified parameters. + * + * This function takes the provided parameters and backup data object and sets the data + * accordingly. The input parameters are processed using the 'paramsProcessing' function + * from the 'backup' module. Depending on the parameters, the function imports IITC settings, + * plugin data, and external plugins into the 'this' object using appropriate functions from + * the 'backup' module. + * + * @async + * @param {BackupParams} params - The parameters for setting the backup data. + * @param {object} backup_data - The backup data object containing the data to be set. + * @return {Promise} A promise that resolves when the backup data is set. + */ + async setBackupData(params, backup_data) { + // Process the input parameters using the 'paramsProcessing' function from the 'backup' module. + params = backup.paramsProcessing(params); + + if (params.settings) await backup.importIitcSettings(this, backup_data.data.iitc_settings); + if (params.data) await backup.importPluginsSettings(this, backup_data.data.plugins_data); + if (params.external) await backup.importExternalPlugins(this, backup_data.external_plugins); + } } diff --git a/src/worker.js b/src/worker.js index f76ab54..e174567 100644 --- a/src/worker.js +++ b/src/worker.js @@ -141,6 +141,15 @@ import { ajaxGet, clearWait, getUID, isSet, parseMeta, wait } from './helpers.js * @property {string[]} grant */ +/** + * Parameters for retrieving backup data. + * + * @typedef {Object} BackupParams + * @property {boolean} settings - Whether to import/export IITC settings. + * @property {boolean} data - Whether to import/export plugins' data. + * @property {boolean} external - Whether to import/export external plugins. + */ + /** * @classdesc This class contains methods for managing IITC and plugins. */ diff --git a/test/manager.9.backup.spec.js b/test/manager.9.backup.spec.js new file mode 100644 index 0000000..d1c85f3 --- /dev/null +++ b/test/manager.9.backup.spec.js @@ -0,0 +1,166 @@ +// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 + +import { describe, it, before } from 'mocha'; +import { Manager } from '../src/manager.js'; +import storage from '../test/storage.js'; +import { expect } from 'chai'; + +describe('getBackupData and setBackupData', function () { + let manager = null; + const first_plugin_uid = 'Available AP statistics+https://github.com/IITC-CE/ingress-intel-total-conversion'; + const external_code = '// ==UserScript==\n// @name IITC plugin\n// ==/UserScript==\nreturn false;'; + const initialBackupData = { + external_plugins: { + beta: {}, + custom: {}, + release: { + 'bookmarks1.user.js': external_code, + }, + }, + data: { + iitc_settings: { + channel: 'release', + network_host: { + release: 'http://127.0.0.1:31606/release', + beta: 'http://127.0.0.1:31606/beta', + custom: 'http://127.0.0.1/', + }, + }, + plugins_data: { + VMin5555: 'test', + }, + app: 'IITC Button', + }, + }; + + const backupData = { + external_plugins: { + release: { + 'bookmarks2.user.js': external_code, + }, + beta: { + 'bookmarks3.user.js': external_code, + }, + }, + data: { + iitc_settings: { + channel: 'beta', + }, + plugins_data: { + VMin5555: 'backup1', + VMin9999: 'backup2', + }, + app: 'IITC Button', + }, + }; + + before(async function () { + storage.resetStorage(); + const params = { + storage: storage, + channel: 'release', + network_host: { + release: 'http://127.0.0.1:31606/release', + beta: 'http://127.0.0.1:31606/beta', + custom: 'http://127.0.0.1/', + }, + inject_user_script: function callBack(data) { + expect(data).to.include('// ==UserScript=='); + }, + inject_plugin: function callBack(data) { + expect(data['code']).to.include('// ==UserScript=='); + }, + progressbar: function callBack(is_show) { + expect(is_show).to.be.oneOf([true, false]); + }, + is_daemon: false, + }; + manager = new Manager(params); + }); + + describe('run', function () { + it('Should not return an error', async function () { + const run = await manager.run(); + expect(run).to.be.undefined; + }); + }); + + describe('Enable plugins and add plugin settings data', function () { + it('Enable first plugin', async function () { + const run = await manager.managePlugin(first_plugin_uid, 'on'); + expect(run).to.be.undefined; + }); + it('Add external plugin', async function () { + const scripts = [ + { + meta: { + id: 'bookmarks1', + namespace: 'https://github.com/IITC-CE/ingress-intel-total-conversion', + name: 'Bookmarks for maps and portals', + category: 'Controls', + filename: 'bookmarks1.user.js', + }, + code: external_code, + }, + ]; + const installed = { + 'Bookmarks for maps and portals+https://github.com/IITC-CE/ingress-intel-total-conversion': { + uid: 'Bookmarks for maps and portals+https://github.com/IITC-CE/ingress-intel-total-conversion', + id: 'bookmarks1', + namespace: 'https://github.com/IITC-CE/ingress-intel-total-conversion', + name: 'Bookmarks for maps and portals', + category: 'Controls', + status: 'on', + user: true, + code: external_code, + filename: 'bookmarks1.user.js', + }, + }; + const run = await manager.addUserScripts(scripts); + expect(run).to.deep.equal(installed); + }); + it('Add plugin settings data', async function () { + await storage.set({ VMin5555: 'test' }); + }); + }); + + describe('getBackupData', function () { + it('Should return the correct backup data', async function () { + const backupDataFromManager = await manager.getBackupData({ + settings: true, + data: true, + external: true, + }); + expect(backupDataFromManager).to.deep.equal(initialBackupData); + }); + }); + + describe('setBackupData', function () { + it('Should set the backup data correctly', async function () { + await manager.setBackupData( + { + settings: true, + data: true, + external: true, + }, + backupData + ); + + // Check if the data was set correctly in storage + expect(manager.channel).to.equal('beta'); + + const pluginsData = await storage.get(['VMin5555', 'VMin9999']); + expect(pluginsData).to.deep.equal({ + VMin5555: 'backup1', + VMin9999: 'backup2', + }); + + const externalPlugins = await storage.get(['release_plugins_user', 'beta_plugins_user']); + expect(externalPlugins['release_plugins_user']).to.have.all.keys( + 'Bookmarks for maps and portals+https://github.com/IITC-CE/ingress-intel-total-conversion', + 'bookmarks2.user.js+IITC plugin' + ); + expect(externalPlugins['beta_plugins_user']).to.have.all.keys('bookmarks3.user.js+IITC plugin'); + }); + }); +}); From d288ca7f4684a0e18abdf631b44635eca2b885be Mon Sep 17 00:00:00 2001 From: modos189 Date: Fri, 28 Jul 2023 23:51:38 +0300 Subject: [PATCH 5/5] 1.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index db799dc..9c09eaf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lib-iitc-manager", - "version": "1.6.1", + "version": "1.7.0", "description": "Library for managing IITC plugins", "main": "src/index.js", "type": "module",