diff --git a/.gitignore b/.gitignore index 35b3a40..0215164 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ tasks2.csv tasks2.xlsx tasks2.json +# Directories used during unit testing +qvfs_1 +qvfs_2 +qvfs_3 src/node_modules/* diff --git a/.vscode/launch.json b/.vscode/launch.json index f651f00..7a2aeec 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -100,45 +100,55 @@ // ------------------------------------ // Import tasks from CSV file // ------------------------------------ - // "args": [ - // "task-import", - // "--auth-type", - // "cert", - // "--host", - // // "192.168.100.109", - // "10.211.55.15", - - // "--auth-cert-file", - // "../../code/secret/winsrv.local/client.pem", - // "--auth-cert-key-file", - // "../../code/secret/winsrv.local/client_key.pem", - // // "--auth-cert-file", - // // "./cert/client.pem", - // // "--auth-cert-key-file", - // // "./cert/client_key.pem", - // "--auth-user-dir", - // "winsrv1", - // // "LAB", - // "--auth-user-id", - // "goran", - - // "--file-type", - // "csv", + "args": [ + "task-import", + "--auth-type", + "cert", - // "--file-name", - // // "tasks2source.csv", - // // "task-chain.csv", - // // "testdata/reload-tasks.csv", - // "./tasks_all.csv", + // p2w1 + "--host", + "192.168.100.109", + "--auth-cert-file", + // "./cert/client.pem", + "../../code/secret/pro2win1-nopwd/client.pem", + "--auth-cert-key-file", + "../../code/secret/pro2win1-nopwd/client_key.pem", + // "./cert/client_key.pem", + "--auth-user-dir", + "LAB", + "--auth-user-id", + "goran", - // // "--qvf-overwrite", - // // "no", + // Parallels + // "--host", + // "10.211.55.15", + // "--auth-cert-file", + // "../../code/secret/winsrv.local/client.pem", + // "--auth-cert-key-file", + // "../../code/secret/winsrv.local/client_key.pem", + // "--auth-user-dir", + // "winsrv1", + // "--auth-user-id", + // "goran", + + "--file-type", + "csv", + + "--file-name", + // "tasks2source.csv", + // "task-chain.csv", + // "testdata/reload-tasks.csv", + // "./tasks_all.csv", + "./testdata/tasks-1.csv" + + // "--qvf-overwrite", + // "no", - // "--limit-import-count", - // "2", + // "--limit-import-count", + // "2", - // // "--dry-run" - // ] + // "--dry-run" + ] // ------------------------------------ // Export apps to QVF files @@ -275,56 +285,56 @@ // ------------------------------------ // Get reload tasks as table // ------------------------------------ - "args": [ - "task-get", - "--auth-type", - "cert", - "--host", - "192.168.100.109", - "--auth-cert-file", - "../../code/secret/pro2win1-nopwd/client.pem", - // "./cert/client.pem", - "--auth-cert-key-file", - "../../code/secret/pro2win1-nopwd/client_key.pem", - // "./cert/client_key.pem", - "--auth-user-dir", - "LAB", - "--auth-user-id", - "goran", + // "args": [ + // "task-get", + // "--auth-type", + // "cert", + // "--host", + // "192.168.100.109", + // "--auth-cert-file", + // "../../code/secret/pro2win1-nopwd/client.pem", + // // "./cert/client.pem", + // "--auth-cert-key-file", + // "../../code/secret/pro2win1-nopwd/client_key.pem", + // // "./cert/client_key.pem", + // "--auth-user-dir", + // "LAB", + // "--auth-user-id", + // "goran", - // "--task-type", - // "reload", - "ext-program", - - // "--task-id", - // "afc250bc-28c3-49a2-8d63-80966749abe3", - // "5748afa9-3abe-43ab-bb1f-127c48ced075", - // "5520e710-91ad-41d2-aeb6-434cafbf366b", - "--output-format", - // "table", - "tree", - - "--output-dest", - "screen", - // "--output-file-name", - // "tasks.csv", - // "tasks.json", - // "--output-file-format", - // "csv", - // "json", - // "excel", - - // "--text-color", - // "no", + // // "--task-type", + // // "reload", + // "ext-program", - // "--table-details", - // "common", - // "lastexecution", - // "tag", - // "customproperty", - // "schematrigger", - // "compositetrigger", - ] + // // "--task-id", + // // "afc250bc-28c3-49a2-8d63-80966749abe3", + // // "5748afa9-3abe-43ab-bb1f-127c48ced075", + // // "5520e710-91ad-41d2-aeb6-434cafbf366b", + // "--output-format", + // // "table", + // "tree", + + // "--output-dest", + // "screen", + // // "--output-file-name", + // // "tasks.csv", + // // "tasks.json", + // // "--output-file-format", + // // "csv", + // // "json", + // // "excel", + + // // "--text-color", + // // "no", + + // // "--table-details", + // // "common", + // // "lastexecution", + // // "tag", + // // "customproperty", + // // "schematrigger", + // // "compositetrigger", + // ] // ------------------------------------ // Get reload tasks as CSV/Excel/JSON file @@ -791,7 +801,6 @@ // "--log-level", // "info" // ] - } ] } diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..90d85fe --- /dev/null +++ b/jest.config.js @@ -0,0 +1,199 @@ +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +/** @type {import('jest').Config} */ +const config = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/hm/8s3g4t3d76n0w5ddd6jblxqw0000gn/T/jest_dx", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: true, + collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: undefined, + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state before every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state and implementation before every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + // testEnvironment: "jest-environment-node", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; + +module.exports = config; diff --git a/package-lock.json b/package-lock.json index 10d2b13..d20dae5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "yesno": "^0.4.0" }, "devDependencies": { + "@jest/globals": "^29.7.0", "jest": "^29.7.0", "prettier": "^3.0.3", "snyk": "^1.1237.0" diff --git a/package.json b/package.json index 8ad9f6b..afbac8f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Command line tool for interacting with Qlik Sense Enterprise servers", "main": "ctrl-q.js", "scripts": { - "test": "jest --verbose false --watch", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand", "lint": "npx eslint ./src/**/*.js", "pc": "pre-commit run --all-files" }, @@ -57,6 +57,7 @@ "yesno": "^0.4.0" }, "devDependencies": { + "@jest/globals": "^29.7.0", "jest": "^29.7.0", "prettier": "^3.0.3", "snyk": "^1.1237.0" diff --git a/src/__tests__/app.test.js b/src/__tests__/app.test.js index af0e201..d4389d2 100644 --- a/src/__tests__/app.test.js +++ b/src/__tests__/app.test.js @@ -1,11 +1,8 @@ -const { error } = require('console'); +/* eslint-disable no-console */ +const { test, expect, describe } = require('@jest/globals'); const { getApps, getAppById } = require('../lib/util/app'); -const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 600000; // 5 minute default timeout - -console.log(`Jest timeout: ${defaultTestTimeout}`); - const options = { logLevel: process.env.CTRL_Q_LOG_LEVEL || 'info', authType: process.env.CTRL_Q_AUTH_TYPE || 'cert', @@ -22,6 +19,8 @@ const options = { taskType: process.env.CTRL_Q_TASK_TYPE || 'reload', }; +const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 600000; // 5 minute default timeout +console.log(`Jest timeout: ${defaultTestTimeout}`); jest.setTimeout(defaultTestTimeout); // Mock logger @@ -37,7 +36,7 @@ const existingAppId2 = '3a6c9a53-cb8d-42f3-a8ee-c083c1f8ed8e'; const nonExistingAppId1 = '9f0d0e02-cccc-bbbb-aaaa-3e9a4d0c8a3d'; const tag1 = 'Test data'; -// ************************************************************************************************************ +// Get one app by ID describe('getAppById', () => { test('existing app ID', async () => { const result = await getAppById(existingAppId1, options); @@ -50,7 +49,7 @@ describe('getAppById', () => { }); }); -// ************************************************************************************************************ +// Get one or more apps by ID and/or tag describe('getApps', () => { test('one app ID, no tags', async () => { const result = await getApps(options, [existingAppId1]); diff --git a/src/__tests__/app_export.test.js b/src/__tests__/app_export.test.js index b9d9647..27b8c27 100644 --- a/src/__tests__/app_export.test.js +++ b/src/__tests__/app_export.test.js @@ -1,17 +1,16 @@ +/* eslint-disable no-console */ +const { test, expect, describe } = require('@jest/globals'); + const fs = require('fs'); const path = require('path'); const { exportAppToFile } = require('../lib/cmd/exportapp'); -const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 600000; // 5 minute default timeout - -console.log(`Jest timeout: ${defaultTestTimeout}`); - const options = { logLevel: process.env.CTRL_Q_LOG_LEVEL || 'info', authType: process.env.CTRL_Q_AUTH_TYPE || 'cert', - authCertFile: process.env.CTRL_Q_AUTH_CERT_FILE || '', - authCertKeyFile: process.env.CTRL_Q_AUTH_CERT_KEY_FILE || '', + authCertFile: process.env.CTRL_Q_AUTH_CERT_FILE || './cert/client.pem', + authCertKeyFile: process.env.CTRL_Q_AUTH_CERT_KEY_FILE || './cert/client_key.pem', host: process.env.CTRL_Q_HOST || '', port: process.env.CTRL_Q_PORT || '4242', schemaVersion: process.env.CTRL_Q_SCHEMA_VERSION || '12.612.0', @@ -24,6 +23,8 @@ const options = { limitExportCount: process.env.CTRL_Q_LIMIT_EXPORT_COUNT || '0', }; +const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 600000; // 10 minute default timeout +console.log(`Jest timeout: ${defaultTestTimeout}`); jest.setTimeout(defaultTestTimeout); test('get tasks (verify parameters)', async () => { @@ -44,7 +45,7 @@ test('get tasks (verify parameters)', async () => { * --qvf-name-separator _ * --qvf-overwrite true */ -test('export apps, tag "apiCreated", (should succeed)', async () => { +test('export apps, tag "apiCreated"', async () => { options.outputDir = 'qvfs_1'; options.appTag = ['apiCreated']; options.excludeAppData = 'true'; @@ -54,6 +55,14 @@ test('export apps, tag "apiCreated", (should succeed)', async () => { const result = await exportAppToFile(options); expect(result).toBe(true); + + // Verify that output folder contains at least one file + const exportDir = path.resolve(options.outputDir); + const files = fs.readdirSync(exportDir); + expect(files.length).toBeGreaterThan(0); + + // Delete output dir + fs.rmSync(exportDir, { recursive: true }); }); /** @@ -66,7 +75,7 @@ test('export apps, tag "apiCreated", (should succeed)', async () => { * --qvf-name-separator _ * --qvf-overwrite true */ -test('export apps, tag "apiCreated", (should succeed)', async () => { +test('export apps, tag "apiCreated"', async () => { options.outputDir = 'qvfs_2'; options.appTag = ['apiCreated', 'Ctrl-Q import']; options.excludeAppData = 'true'; @@ -76,6 +85,14 @@ test('export apps, tag "apiCreated", (should succeed)', async () => { const result = await exportAppToFile(options); expect(result).toBe(true); + + // Verify that output folder contains at least one file + const exportDir = path.resolve(options.outputDir); + const files = fs.readdirSync(exportDir); + expect(files.length).toBeGreaterThan(0); + + // Delete output dir + fs.rmSync(exportDir, { recursive: true }); }); /** @@ -89,7 +106,7 @@ test('export apps, tag "apiCreated", (should succeed)', async () => { * --qvf-name-separator _ * --qvf-overwrite true */ -test('export apps, tag "apiCreated", (should succeed)', async () => { +test('export apps, tag "apiCreated"', async () => { options.outputDir = 'qvfs_3'; options.appTag = ['apiCreated', 'Ctrl-Q import']; options.appId = ['eb3ab049-d007-43d3-93da-5962f9208c65']; @@ -100,6 +117,14 @@ test('export apps, tag "apiCreated", (should succeed)', async () => { const result = await exportAppToFile(options); expect(result).toBe(true); + + // Verify that output folder contains at least one file + const exportDir = path.resolve(options.outputDir); + const files = fs.readdirSync(exportDir); + expect(files.length).toBeGreaterThan(0); + + // Delete output dir + fs.rmSync(exportDir, { recursive: true }); }); /** @@ -113,7 +138,7 @@ test('export apps, tag "apiCreated", (should succeed)', async () => { * --qvf-name-separator _ * --qvf-overwrite true */ -test('export apps, tag "apiCreated", (should succeed)', async () => { +test('export apps, tag "apiCreated"', async () => { options.outputDir = 'qvfs_4'; options.appTag = ['apiCreated', 'Ctrl-Q import']; options.appId = ['eb3ab049-d007-43d3-93da-5962f9208c65', '2933711d-6638-41d4-a2d2-6dd2d965208b']; @@ -124,6 +149,14 @@ test('export apps, tag "apiCreated", (should succeed)', async () => { const result = await exportAppToFile(options); expect(result).toBe(true); + + // Verify that output folder contains at least one file + const exportDir = path.resolve(options.outputDir); + const files = fs.readdirSync(exportDir); + expect(files.length).toBeGreaterThan(0); + + // Delete output dir + fs.rmSync(exportDir, { recursive: true }); }); /** @@ -137,7 +170,7 @@ test('export apps, tag "apiCreated", (should succeed)', async () => { * --qvf-name-separator _ * --qvf-overwrite true */ -test('export apps, tag "apiCreated", (should succeed)', async () => { +test('export apps, tag "apiCreated"', async () => { options.outputDir = 'qvfs_5'; options.appTag = ['apiCreated', 'Ctrl-Q import']; options.appId = ['eb3ab049-d007-43d3-93da-5962f9208c65', '2933711d-6638-41d4-a2d2-6dd2d965208b']; @@ -153,25 +186,24 @@ test('export apps, tag "apiCreated", (should succeed)', async () => { const result = await exportAppToFile(options); expect(result).toBe(true); -}); - -// Delete output dirs -// let exportDir = path.resolve('qvfs_1'); -// console.log(exportDir); -// fs.rmdirSync(exportDir, { recursive: true }); -// exportDir = path.resolve('qvfs_2'); -// console.log(exportDir); -// fs.rmdirSync(exportDir, { recursive: true }); - -// exportDir = path.resolve('qvfs_3'); -// console.log(exportDir); -// fs.rmdirSync(exportDir, { recursive: true }); - -// exportDir = path.resolve('qvfs_4'); -// console.log(exportDir); -// fs.rmdirSync(exportDir, { recursive: true }); - -// exportDir = path.resolve('qvfs_5'); -// console.log(exportDir); -// fs.rmdirSync(exportDir, { recursive: true }); + // Verify that output folder contains at least one file + const exportDir = path.resolve(options.outputDir); + const files = fs.readdirSync(exportDir); + expect(files.length).toBeGreaterThan(0); + + // Verify that output Excel file has been created + // Get all files in output folder + const files2 = fs.readdirSync(exportDir); + // Filter out Excel files + const excelFiles = files2.filter((file) => file.endsWith('.xlsx')); + expect(excelFiles.length).toBe(1); + + // Size of Exel file should be > 0 + const excelFile = path.resolve(exportDir, excelFiles[0]); + const stats = fs.statSync(excelFile); + expect(stats.size).toBeGreaterThan(0); + + // Delete output dir + fs.rmSync(exportDir, { recursive: true }); +}); diff --git a/src/__tests__/script_get.test.js b/src/__tests__/script_get.test.js index 1d7aa5e..3d9ad50 100644 --- a/src/__tests__/script_get.test.js +++ b/src/__tests__/script_get.test.js @@ -1,14 +1,13 @@ -const { getScript } = require('../lib/cmd/getscript'); - -const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 120000; // 2 minute default timeout +/* eslint-disable no-console */ +const { test, expect, describe } = require('@jest/globals'); -console.log(`Jest timeout: ${defaultTestTimeout}`); +const { getScript } = require('../lib/cmd/getscript'); const options = { logLevel: process.env.CTRL_Q_LOG_LEVEL || 'info', authType: process.env.CTRL_Q_AUTH_TYPE || 'cert', - authCertFile: process.env.CTRL_Q_AUTH_CERT_FILE || '', - authCertKeyFile: process.env.CTRL_Q_AUTH_CERT_KEY_FILE || '', + authCertFile: process.env.CTRL_Q_AUTH_CERT_FILE || './cert/client.pem', + authCertKeyFile: process.env.CTRL_Q_AUTH_CERT_KEY_FILE || './cert/client_key.pem', host: process.env.CTRL_Q_HOST || '', port: process.env.CTRL_Q_PORT || '4747', virtualProxy: process.env.CTRL_Q_VIRTUAL_PROXY || '', @@ -19,25 +18,30 @@ const options = { authUserId: process.env.CTRL_Q_AUTH_USER_ID || '', }; +const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 120000; // 2 minute default timeout +console.log(`Jest timeout: ${defaultTestTimeout}`); jest.setTimeout(defaultTestTimeout); -test('get app script (verify parameters)', async () => { - expect(options.authCertFile).not.toHaveLength(0); - expect(options.authCertKeyFile).not.toHaveLength(0); - expect(options.host).not.toHaveLength(0); - expect(options.authUserDir).not.toHaveLength(0); - expect(options.authUserId).not.toHaveLength(0); -}); +// Get app script +describe('get app script', () => { + test('Verify parameters (should succeed)', async () => { + expect(options.authCertFile).not.toHaveLength(0); + expect(options.authCertKeyFile).not.toHaveLength(0); + expect(options.host).not.toHaveLength(0); + expect(options.authUserDir).not.toHaveLength(0); + expect(options.authUserId).not.toHaveLength(0); + }); -/** - * Get app script - * Should succeed - */ -test('get app script (should succeed)', async () => { - const result = await getScript(options); + /** + * Get app script + * Should succeed + */ + test('get app script (should succeed)', async () => { + const result = await getScript(options); - expect(result.appId).toBe('a3e0f5d2-000a-464f-998d-33d333b175d7'); - expect(result.appCreatedDate).toBe('2021-06-03T22:04:52.283Z'); - expect(result.appModifiedDate).toBe('2021-06-04T15:42:23.759Z'); - expect(result.appScript.length).toBe(1655); + expect(result.appId).toBe('a3e0f5d2-000a-464f-998d-33d333b175d7'); + expect(result.appCreatedDate).toBe('2021-06-03T22:04:52.283Z'); + expect(result.appModifiedDate).toBe('2023-05-05T06:17:05.456Z'); + expect(result.appScript.length).toBe(1989); + }); }); diff --git a/src/__tests__/task.test.js b/src/__tests__/task.test.js index 9372111..a357072 100644 --- a/src/__tests__/task.test.js +++ b/src/__tests__/task.test.js @@ -1,8 +1,7 @@ -const { taskExistById, getTaskByName, getTaskById } = require('../lib/util/task'); - -const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 600000; // 5 minute default timeout +/* eslint-disable no-console */ +const { test, expect, describe } = require('@jest/globals'); -console.log(`Jest timeout: ${defaultTestTimeout}`); +const { taskExistById, getTaskByName, getTaskById } = require('../lib/util/task'); const options = { logLevel: process.env.CTRL_Q_LOG_LEVEL || 'info', @@ -20,6 +19,8 @@ const options = { taskType: process.env.CTRL_Q_TASK_TYPE || 'reload', }; +const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 600000; // 10 minute default timeout +console.log(`Jest timeout: ${defaultTestTimeout}`); jest.setTimeout(defaultTestTimeout); // Mock logger @@ -30,14 +31,14 @@ global.console = { }; // Define existing and non-existing tasks -const existingTaskId = '58dd8322-e39c-4b71-b74e-13c47a2f6dd4'; -const existingTaskName = 'Reload task of Meetup.com'; -const multipleMatchingTaskNames = 'Manually triggered reload of Butler 7 Slack debug'; +const existingTaskId = 'e9100e69-4e8e-414b-bf88-10a1110c43a9'; +const existingTaskName = '[ctrl-q unit test] app 1, task 1'; +const multipleMatchingTaskNames = '[ctrl-q unit test] app 1, task 2 (duplicates exist)'; const nonExistingTaskId = '9f0d0e02-cccc-bbbb-aaaa-3e9a4d0c8a3d'; const nonExistingTaskName = 'Non-existing task 298374293874298734'; -// ************************************************************************************************************ -describe('taskExistById', () => { +// Check if task exists by ID +describe('taskExistById: Check if task exists by ID', () => { test('existing task', async () => { const result = await taskExistById(existingTaskId, options); expect(result).toBe(true); @@ -49,8 +50,8 @@ describe('taskExistById', () => { }); }); -// ************************************************************************************************************ -describe('getTaskByName', () => { +// Get task by name +describe('getTaskByName: Get task by name', () => { test('no matching task', async () => { const result = await getTaskByName(nonExistingTaskName, options); expect(result).toBe(false); @@ -63,6 +64,8 @@ describe('getTaskByName', () => { test('multiple matching task names', async () => { const result = await getTaskByName(multipleMatchingTaskNames, options); + + // Should return false expect(result).toEqual(false); // Ensure correct substring was written to global console log @@ -77,8 +80,8 @@ describe('getTaskByName', () => { }); }); -// ************************************************************************************************************ -describe('getTaskById', () => { +// Get task by ID +describe('getTaskById: Get task by ID', () => { test('no matching task', async () => { const result = await getTaskById(nonExistingTaskId, options); expect(result).toEqual(false); diff --git a/src/__tests__/task_get.test.js b/src/__tests__/task_get.test.js index 63aa918..fa81748 100644 --- a/src/__tests__/task_get.test.js +++ b/src/__tests__/task_get.test.js @@ -1,14 +1,15 @@ -const { getTask } = require('../lib/cmd/gettask'); - -const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 600000; // 5 minute default timeout +/* eslint-disable no-console */ +const { test, expect, describe } = require('@jest/globals'); +const fs = require('fs'); +const path = require('path'); -console.log(`Jest timeout: ${defaultTestTimeout}`); +const { getTask } = require('../lib/cmd/gettask'); const options = { logLevel: process.env.CTRL_Q_LOG_LEVEL || 'info', authType: process.env.CTRL_Q_AUTH_TYPE || 'cert', - authCertFile: process.env.CTRL_Q_AUTH_CERT_FILE || '', - authCertKeyFile: process.env.CTRL_Q_AUTH_CERT_KEY_FILE || '', + authCertFile: process.env.CTRL_Q_AUTH_CERT_FILE || './cert/client.pem', + authCertKeyFile: process.env.CTRL_Q_AUTH_CERT_KEY_FILE || './cert/client_key.pem', host: process.env.CTRL_Q_HOST || '', port: process.env.CTRL_Q_PORT || '4242', schemaVersion: process.env.CTRL_Q_SCHEMA_VERSION || '12.612.0', @@ -17,9 +18,11 @@ const options = { authUserDir: process.env.CTRL_Q_AUTH_USER_DIR || '', authUserId: process.env.CTRL_Q_AUTH_USER_ID || '', - taskType: process.env.CTRL_Q_TASK_TYPE || 'reload', + taskType: process.env.CTRL_Q_TASK_TYPE || ['reload', 'ext-program'], }; +const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 600000; // 10 minute default timeout +console.log(`Jest timeout: ${defaultTestTimeout}`); jest.setTimeout(defaultTestTimeout); test('get tasks (verify parameters)', async () => { @@ -30,72 +33,372 @@ test('get tasks (verify parameters)', async () => { expect(options.authUserId).not.toHaveLength(0); }); -test('get tasks as table on screen, columns "common" (should succeed)', async () => { - options.outputFormat = 'table'; - options.outputDest = 'screen'; - options.tableDetails = ['common']; +// Test suite for task table +describe('get tasks as table', () => { + test('get reload + ext pgm tasks as table on screen, columns "common"', async () => { + options.outputFormat = 'table'; + options.outputDest = 'screen'; + options.tableDetails = ['common']; - const result = await getTask(options); - expect(result).toBe(true); -}); + const result = await getTask(options); + expect(result).toBe(true); + }); -test('get tasks as table on screen, columns "common", "tag" (should succeed)', async () => { - options.outputFormat = 'table'; - options.outputDest = 'screen'; - options.tableDetails = ['common', 'tag']; + test('get tasks as table on screen, columns "common", "tag"', async () => { + options.outputFormat = 'table'; + options.outputDest = 'screen'; + options.tableDetails = ['common', 'tag']; - const result = await getTask(options); - expect(result).toBe(true); -}); + const result = await getTask(options); + expect(result).toBe(true); + }); -test('get tasks as table on screen, no detail columns (should succeed)', async () => { - options.outputFormat = 'table'; - options.outputDest = 'screen'; + test('get tasks as table on screen, no detail columns', async () => { + options.outputFormat = 'table'; + options.outputDest = 'screen'; - const result = await getTask(options); - expect(result).toBe(true); + const result = await getTask(options); + expect(result).toBe(true); + }); }); -test('get tasks as tree on screen, no detail columns, colored text (should succeed)', async () => { - options.outputFormat = 'tree'; - options.outputDest = 'screen'; - options.treeDetails = ''; - options.treeIcons = true; - options.textColor = 'yes'; +// Test suite for task tree +describe('get tasks as tree', () => { + test('get tasks as tree on screen, no detail columns, colored text', async () => { + options.outputFormat = 'tree'; + options.outputDest = 'screen'; + options.treeDetails = ''; + options.treeIcons = true; + options.textColor = 'yes'; - const result = await getTask(options); - expect(result).toBe(true); -}); + const result = await getTask(options); + expect(result).toBe(true); + }); -test('get tasks as tree on screen, no detail columns, no colored text (should succeed)', async () => { - options.outputFormat = 'tree'; - options.outputDest = 'screen'; - options.treeDetails = ''; - options.treeIcons = true; - options.textColor = 'no'; + test('get tasks as tree on screen, no detail columns, no colored text (should succeed)', async () => { + options.outputFormat = 'tree'; + options.outputDest = 'screen'; + options.treeDetails = ''; + options.treeIcons = true; + options.textColor = 'no'; - const result = await getTask(options); - expect(result).toBe(true); + const result = await getTask(options); + expect(result).toBe(true); + }); + + test('get tasks as tree on screen, full detail columns, colored text (should succeed)', async () => { + options.outputFormat = 'tree'; + options.outputDest = 'screen'; + options.treeDetails = ['taskid', 'laststart', 'laststop', 'nextstart', 'appname', 'appstream']; + options.treeIcons = true; + options.textColor = 'yes'; + + const result = await getTask(options); + expect(result).toBe(true); + }); + + test('get tasks as tree on screen, full detail columns, no colored text (should succeed)', async () => { + options.outputFormat = 'tree'; + options.outputDest = 'screen'; + options.treeDetails = ['taskid', 'laststart', 'laststop', 'nextstart', 'appname', 'appstream']; + options.treeIcons = true; + options.textColor = 'no'; + + const result = await getTask(options); + expect(result).toBe(true); + }); }); -test('get tasks as tree on screen, full detail columns, colored text (should succeed)', async () => { - options.outputFormat = 'tree'; - options.outputDest = 'screen'; - options.treeDetails = ['taskid', 'laststart', 'laststop', 'nextstart', 'appname', 'appstream']; - options.treeIcons = true; - options.textColor = 'yes'; +// Test suite for storing table output to file +describe('get tasks as table, store to file', () => { + test('get tasks as table, store to CSV, columns "common", "tag"', async () => { + const outputDir = 'task_export_1'; + const outputFile = `task_get_table_1.csv`; + + options.outputFormat = 'table'; + options.outputDest = 'file'; + options.outputFileFormat = 'csv'; + options.outputFileName = `./${outputDir}/${outputFile}`; + options.outputFileOverwrite = true; + options.tableDetails = ['common', 'tag']; + + // Create output directory, if it doesn't exist + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + + const result = await getTask(options); + expect(result).toBe(true); + + // Verify that output file exists + const exportFile = path.resolve(options.outputFileName); + const fileExists = fs.existsSync(exportFile); + expect(fileExists).toBe(true); + + // Verify that output file contains at least 5 lines + const fileContents = fs.readFileSync(exportFile, 'utf8'); + const lines = fileContents.split('\n'); + expect(lines.length).toBeGreaterThan(5); + + // Verify that the second column in the CSV file contains only "Reload" or "External program" + // except first line, which contains the column header + // Disregard empty lines + for (let i = 1; i < lines.length; i++) { + if (lines[i].length > 0) { + const columns = lines[i].split(','); + expect(columns[1]).toMatch(/Reload|External program/); + } + } + + // Delete output directory + fs.rmSync(outputDir, { recursive: true }); + }); + + test('get tasks as table, store to CSV, no detail columns option', async () => { + const outputDir = 'task_export_2'; + const outputFile = `task_get_table_2.csv`; + + options.outputFormat = 'table'; + options.outputDest = 'file'; + options.outputFileFormat = 'csv'; + options.outputFileName = `./${outputDir}/${outputFile}`; + options.outputFileOverwrite = true; + + // Create output directory, if it doesn't exist + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + + const result = await getTask(options); + expect(result).toBe(true); + + // Verify that output file exists + const exportFile = path.resolve(options.outputFileName); + const fileExists = fs.existsSync(exportFile); + expect(fileExists).toBe(true); + + // Verify that output file contains at least 5 lines + const fileContents = fs.readFileSync(exportFile, 'utf8'); + const lines = fileContents.split('\n'); + expect(lines.length).toBeGreaterThan(5); + + // Verify that the second column in the CSV file contains only "Reload" or "External program" + // except first line, which contains the column header + // Disregard empty lines + for (let i = 1; i < lines.length; i++) { + if (lines[i].length > 0) { + const columns = lines[i].split(','); + expect(columns[1]).toMatch(/Reload|External program/); + } + } + + // Delete output directory + fs.rmSync(outputDir, { recursive: true }); + }); + + test('get tasks as table, store to CSV, reload tasks only', async () => { + const outputDir = 'task_export_3'; + const outputFile = `task_get_table_3.csv`; + + options.outputFormat = 'table'; + options.outputDest = 'file'; + options.outputFileFormat = 'csv'; + options.outputFileName = `./${outputDir}/${outputFile}`; + options.outputFileOverwrite = true; + options.taskType = ['reload']; + + // Create output directory, if it doesn't exist + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + + const result = await getTask(options); + expect(result).toBe(true); + + // Verify that output file exists + const exportFile = path.resolve(options.outputFileName); + const fileExists = fs.existsSync(exportFile); + expect(fileExists).toBe(true); - const result = await getTask(options); - expect(result).toBe(true); + // Verify that output file contains at least 5 lines + const fileContents = fs.readFileSync(exportFile, 'utf8'); + const lines = fileContents.split('\n'); + expect(lines.length).toBeGreaterThan(5); + + // Verify that the second column in the CSV file contains only "Reload" + // except first line, which contains the column header + // Disregard empty lines + for (let i = 1; i < lines.length; i++) { + if (lines[i].length > 0) { + const columns = lines[i].split(','); + expect(columns[1]).toBe('Reload'); + } + } + + // Delete output directory + fs.rmSync(outputDir, { recursive: true }); + }); + + test('get tasks as table, store to CSV, ext pgm tasks only', async () => { + const outputDir = 'task_export_4'; + const outputFile = `task_get_table_4.csv`; + + options.outputFormat = 'table'; + options.outputDest = 'file'; + options.outputFileFormat = 'csv'; + options.outputFileName = `./${outputDir}/${outputFile}`; + options.outputFileOverwrite = true; + options.taskType = ['ext-program']; + + // Create output directory, if it doesn't exist + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + + const result = await getTask(options); + expect(result).toBe(true); + + // Verify that output file exists + const exportFile = path.resolve(options.outputFileName); + const fileExists = fs.existsSync(exportFile); + expect(fileExists).toBe(true); + + // Verify that output file contains at least 5 lines + const fileContents = fs.readFileSync(exportFile, 'utf8'); + const lines = fileContents.split('\n'); + expect(lines.length).toBeGreaterThan(5); + + // Verify that the second column in the CSV file contains only "External program" + // except first line, which contains the column header + // Disregard empty lines + for (let i = 1; i < lines.length; i++) { + if (lines[i].length > 0) { + const columns = lines[i].split(','); + expect(columns[1]).toBe('External program'); + } + } + + // Delete output directory + fs.rmSync(outputDir, { recursive: true }); + }); }); -test('get tasks as tree on screen, full detail columns, no colored text (should succeed)', async () => { - options.outputFormat = 'tree'; - options.outputDest = 'screen'; - options.treeDetails = ['taskid', 'laststart', 'laststop', 'nextstart', 'appname', 'appstream']; - options.treeIcons = true; - options.textColor = 'no'; +// Test suite for storing tree output to file +describe('get tasks as tree, store to JSON file', () => { + test('no detail columns or colored text, use icons', async () => { + const outputDir = 'task_export'; + const outputFile = `task_get_table.json`; + + options.outputFormat = 'tree'; + options.outputDest = 'file'; + options.outputFileFormat = 'json'; + options.outputFileName = `./${outputDir}/${outputFile}`; + options.outputFileOverwrite = true; + options.treeIcons = true; + options.textColor = 'no'; + options.treeDetails = ''; + + // Create output directory, if it doesn't exist + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + + const result = await getTask(options); + expect(result).toBe(true); + + // Verify that output file exists + const exportFile = path.resolve(options.outputFileName); + const fileExists = fs.existsSync(exportFile); + expect(fileExists).toBe(true); + + // Verify that output file contains at least 5 lines + const fileContents = fs.readFileSync(exportFile, 'utf8'); + const lines = fileContents.split('\n'); + expect(lines.length).toBeGreaterThan(5); + + // Verify that the output file contains a valid JSON object + const json = JSON.parse(fileContents); + expect(json).not.toBeNull(); + + // Delete output directory + fs.rmSync(outputDir, { recursive: true }); + }); + + test('no detail columns or colored text or icons', async () => { + const outputDir = 'task_export'; + const outputFile = `task_get_table.json`; + + options.outputFormat = 'tree'; + options.outputDest = 'file'; + options.outputFileFormat = 'json'; + options.outputFileName = `./${outputDir}/${outputFile}`; + options.outputFileOverwrite = true; + options.treeIcons = false; + options.textColor = 'no'; + options.treeDetails = ''; + + // Create output directory, if it doesn't exist + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + + const result = await getTask(options); + expect(result).toBe(true); + + // Verify that output file exists + const exportFile = path.resolve(options.outputFileName); + const fileExists = fs.existsSync(exportFile); + expect(fileExists).toBe(true); + + // Verify that output file contains at least 5 lines + const fileContents = fs.readFileSync(exportFile, 'utf8'); + const lines = fileContents.split('\n'); + expect(lines.length).toBeGreaterThan(5); + + // Verify that the output file contains a valid JSON object + const json = JSON.parse(fileContents); + expect(json).not.toBeNull(); + + // Delete output directory + fs.rmSync(outputDir, { recursive: true }); + }); + + test('no colored text, use icons and all task details', async () => { + const outputDir = 'task_export'; + const outputFile = `task_get_table.json`; + + options.outputFormat = 'tree'; + options.outputDest = 'file'; + options.outputFileFormat = 'json'; + options.outputFileName = `./${outputDir}/${outputFile}`; + options.outputFileOverwrite = true; + options.treeIcons = true; + options.textColor = 'no'; + options.treeDetails = ['taskid', 'laststart', 'laststop', 'nextstart', 'appname', 'appstream']; + + // Create output directory, if it doesn't exist + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + + const result = await getTask(options); + expect(result).toBe(true); + + // Verify that output file exists + const exportFile = path.resolve(options.outputFileName); + const fileExists = fs.existsSync(exportFile); + expect(fileExists).toBe(true); + + // Verify that output file contains at least 5 lines + const fileContents = fs.readFileSync(exportFile, 'utf8'); + const lines = fileContents.split('\n'); + expect(lines.length).toBeGreaterThan(5); + + // Verify that the output file contains a valid JSON object + const json = JSON.parse(fileContents); + expect(json).not.toBeNull(); - const result = await getTask(options); - expect(result).toBe(true); + // Delete output directory + fs.rmSync(outputDir, { recursive: true }); + }); }); diff --git a/src/__tests__/task_import.test.js b/src/__tests__/task_import.test.js new file mode 100644 index 0000000..1083f0f --- /dev/null +++ b/src/__tests__/task_import.test.js @@ -0,0 +1,200 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-console */ +const { test, expect, describe } = require('@jest/globals'); + +const { importTaskFromFile } = require('../lib/cmd/importtask'); +const { getTaskById, deleteExternalProgramTaskById, deleteReloadTaskById, } = require('../lib/util/task'); +const { mapTaskType, } = require('../lib/util/lookups'); + +const options = { + logLevel: process.env.CTRL_Q_LOG_LEVEL || 'info', + authType: process.env.CTRL_Q_AUTH_TYPE || 'cert', + authCertFile: process.env.CTRL_Q_AUTH_CERT_FILE || './cert/client.pem', + authCertKeyFile: process.env.CTRL_Q_AUTH_CERT_KEY_FILE || './cert/client_key.pem', + host: process.env.CTRL_Q_HOST || '', + port: process.env.CTRL_Q_PORT || '4242', + schemaVersion: process.env.CTRL_Q_SCHEMA_VERSION || '12.612.0', + virtualProxy: process.env.CTRL_Q_VIRTUAL_PROXY || '', + secure: process.env.CTRL_Q_SECURE || true, + authUserDir: process.env.CTRL_Q_AUTH_USER_DIR || '', + authUserId: process.env.CTRL_Q_AUTH_USER_ID || '', + updateMode: process.env.CTRL_Q_UPDATE_MODE || 'create', +}; + +const defaultTestTimeout = process.env.CTRL_Q_TEST_TIMEOUT || 600000; // 10 minute default timeout +console.log(`Jest timeout: ${defaultTestTimeout}`); +jest.setTimeout(defaultTestTimeout); + +test('get tasks (verify parameters)', async () => { + expect(options.authCertFile).not.toHaveLength(0); + expect(options.authCertKeyFile).not.toHaveLength(0); + expect(options.host).not.toHaveLength(0); + expect(options.authUserDir).not.toHaveLength(0); + expect(options.authUserId).not.toHaveLength(0); +}); + +// Test suite for task import +describe('import task', () => { + test('csv 1: reload task, no triggers', async () => { + const inputDir = './testdata'; + const inputFile = `tasks-1.csv`; + + options.fileType = 'csv'; + options.fileName = `${inputDir}/${inputFile}`; + + const result = await importTaskFromFile(options); + + // Result should be array with length 1 + expect(result).not.toBe(false); + expect(result.length).toBe(1); + + // Delete all tasks in array + for (let i = 0; i < result.length; i += 1) { + const task = result[i]; + const { taskId } = task; + + await deleteReloadTaskById(taskId, options); + } + }); + + test('csv 2: reload task, 1 schema trigger', async () => { + const inputDir = './testdata'; + const inputFile = `tasks-2.csv`; + + options.fileType = 'csv'; + options.fileName = `${inputDir}/${inputFile}`; + + const result = await importTaskFromFile(options); + + // Result should be array with length 1 + expect(result).not.toBe(false); + expect(result.length).toBe(1); + + // Delete all tasks in array + for (let i = 0; i < result.length; i += 1) { + const task = result[i]; + const { taskId } = task; + + await deleteReloadTaskById(taskId, options); + } + }); + + test('csv 3: reload task, 1 composite trigger', async () => { + const inputDir = './testdata'; + const inputFile = `tasks-3.csv`; + + options.fileType = 'csv'; + options.fileName = `${inputDir}/${inputFile}`; + + const result = await importTaskFromFile(options); + + // Result should be array with length 1 + expect(result).not.toBe(false); + expect(result.length).toBe(1); + + // Delete all tasks in array + for (let i = 0; i < result.length; i += 1) { + const task = result[i]; + const { taskId } = task; + + await deleteReloadTaskById(taskId, options); + } + }); + + test('csv 4: 2 reload tasks, composite & schema triggers', async () => { + const inputDir = './testdata'; + const inputFile = `tasks-4.csv`; + + options.fileType = 'csv'; + options.fileName = `${inputDir}/${inputFile}`; + + const result = await importTaskFromFile(options); + + // Result should be array with length 2 + expect(result).not.toBe(false); + expect(result.length).toBe(2); + + // Delete all tasks in array + for (let i = 0; i < result.length; i += 1) { + const task = result[i]; + const { taskId } = task; + + await deleteReloadTaskById(taskId, options); + } + }); + + test('csv 5: 1 ext program task, schema trigger', async () => { + const inputDir = './testdata'; + const inputFile = `tasks-5.csv`; + + options.fileType = 'csv'; + options.fileName = `${inputDir}/${inputFile}`; + + const result = await importTaskFromFile(options); + + // Result should be array with length 1 + expect(result).not.toBe(false); + expect(result.length).toBe(1); + + // Delete all tasks in array + for (let i = 0; i < result.length; i += 1) { + const task = result[i]; + const { taskId } = task; + + await deleteExternalProgramTaskById(taskId, options); + } + }); + + test('csv 6: 1 ext program task, composite trigger', async () => { + const inputDir = './testdata'; + const inputFile = `tasks-6.csv`; + + options.fileType = 'csv'; + options.fileName = `${inputDir}/${inputFile}`; + + const result = await importTaskFromFile(options); + + // Result should be array with length 1 + expect(result).not.toBe(false); + expect(result.length).toBe(1); + + // Delete all tasks in array + for (let i = 0; i < result.length; i += 1) { + const task = result[i]; + const { taskId } = task; + + await deleteExternalProgramTaskById(taskId, options); + } + }); + + test('csv 7: complex. many schema and composite triggers', async () => { + const inputDir = './testdata'; + const inputFile = `tasks-7.csv`; + + options.fileType = 'csv'; + options.fileName = `${inputDir}/${inputFile}`; + + const result = await importTaskFromFile(options); + + // Result should be array with length 1 + expect(result).not.toBe(false); + expect(result.length).toBe(18); + + // Delete all tasks in array + for (let i = 0; i < result.length; i += 1) { + const task = result[i]; + const { taskId } = task; + + // Call getTaskById to verify that task exists and what task type it is + const task2 = await getTaskById(taskId, options); + const taskType = mapTaskType.get(task2.taskType); + + // taskType should be 'Reload' or 'External Program' + if (taskType === 'Reload') { + await deleteReloadTaskById(taskId, options); + } else if (taskType === 'ExternalProgram') { + await deleteExternalProgramTaskById(taskId, options); + } + } + }); +}); diff --git a/src/lib/cmd/gettask.js b/src/lib/cmd/gettask.js index d87e274..267028b 100644 --- a/src/lib/cmd/gettask.js +++ b/src/lib/cmd/gettask.js @@ -83,6 +83,8 @@ function compareTable(a, b) { return 0; } +// get-task command +// Options are assumed to be verified before calling this function const getTask = async (options) => { try { // Set log level diff --git a/src/lib/cmd/importtask.js b/src/lib/cmd/importtask.js index 9139707..489189f 100644 --- a/src/lib/cmd/importtask.js +++ b/src/lib/cmd/importtask.js @@ -412,7 +412,12 @@ const importTaskFromFile = async (options) => { // Set up new reload task object const qlikSenseTasks = new QlikSenseTasks(); await qlikSenseTasks.init(options, importedApps); - const taskList = await qlikSenseTasks.getTaskModelFromFile(tasksFromFile, tagsExisting, cpExisting); + const taskList = await qlikSenseTasks.getTaskModelFromFile(tasksFromFile, tagsExisting, cpExisting, options); + + if (taskList) { + return taskList; + } + return false; } catch (err) { logger.error(`IMPORT TASK 2: ${err.stack}`); } diff --git a/src/lib/task/class_alltasks.js b/src/lib/task/class_alltasks.js index 5b09c98..ee83aa4 100644 --- a/src/lib/task/class_alltasks.js +++ b/src/lib/task/class_alltasks.js @@ -83,6 +83,7 @@ class QlikSenseTasks { // - cpExisting: Array of existing custom properties in QSEoW // - fakeTaskId: Fake task ID used to associate task with schema/composite events // - nodesWithEvents: Set of nodes that have associated events + // - options: CLI options // // Returns: // Object with two properties: @@ -148,7 +149,7 @@ class QlikSenseTasks { } // eslint-disable-next-line no-await-in-loop - const app = await getAppById(appId); + const app = await getAppById(appId, param?.options); if (!app) { logger.error( @@ -160,7 +161,7 @@ class QlikSenseTasks { // App ID is a proper UUID. We don't know if the app actually exists though. // eslint-disable-next-line no-await-in-loop - const app = await getAppById(appIdRaw); + const app = await getAppById(appIdRaw, param?.options); if (!app) { logger.error( @@ -279,6 +280,7 @@ class QlikSenseTasks { currentTask, fakeTaskId: param.fakeTaskId, nodesWithEvents: param.nodesWithEvents, + options: param?.options, }); // Get composite events for this task @@ -290,6 +292,7 @@ class QlikSenseTasks { currentTask, fakeTaskId: param.fakeTaskId, nodesWithEvents: param.nodesWithEvents, + options: param?.options, }); return { currentTask, taskCreationOption }; @@ -304,6 +307,7 @@ class QlikSenseTasks { // - cpExisting: Array of existing custom properties in QSEoW // - fakeTaskId: Fake task ID used to associate task with schema/composite events // - nodesWithEvents: Set of nodes that have associated events + // - options: CLI options // // Returns: // Object with two properties: @@ -419,6 +423,7 @@ class QlikSenseTasks { currentTask, fakeTaskId: param.fakeTaskId, nodesWithEvents: param.nodesWithEvents, + options: param?.options, }); // Get composite events for this task @@ -430,6 +435,7 @@ class QlikSenseTasks { currentTask, fakeTaskId: param.fakeTaskId, nodesWithEvents: param.nodesWithEvents, + options: param?.options, }); return { currentTask, taskCreationOption }; @@ -444,6 +450,7 @@ class QlikSenseTasks { // - currentTask: Object containing task data // - fakeTaskId: Fake task ID used to associate task with schema/composite events // - nodesWithEvents: Set of nodes that have associated events + // - options: CLI options parseSchemaEvents(param) { // Get schema events for this task, storing the info using the same structure as returned from QRS API const prelSchemaEvents = []; @@ -555,6 +562,7 @@ class QlikSenseTasks { // - currentTask: Object containing task data // - fakeTaskId: Fake task ID used to associate task with schema/composite events // - nodesWithEvents: Set of nodes that have associated events + // - options: CLI options async parseCompositeEvents(param) { // Get all composite events for this task // @@ -686,7 +694,7 @@ class QlikSenseTasks { upstreamTaskExistence = 'exists-in-source-file'; } else { // eslint-disable-next-line no-await-in-loop - upstreamTask = await getTaskById(rule[param.taskFileColumnHeaders.ruleTaskId.pos]); + upstreamTask = await getTaskById(rule[param.taskFileColumnHeaders.ruleTaskId.pos], param?.options); // Save upstream task in shared task list this.compositeEventUpstreamTask.push(upstreamTask); @@ -822,7 +830,8 @@ class QlikSenseTasks { // - tasksFromFile: Object containing data read from file // - tagsExisting: Array of existing tags in QSEoW // - cpExisting: Array of existing custom properties in QSEoW - async getTaskModelFromFile(tasksFromFile, tagsExisting, cpExisting) { + // - options: Options object passed on the command line + async getTaskModelFromFile(tasksFromFile, tagsExisting, cpExisting, options) { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { try { @@ -929,6 +938,7 @@ class QlikSenseTasks { cpExisting, fakeTaskId, nodesWithEvents, + options, }); // Add reload task as node in task network @@ -1059,6 +1069,7 @@ class QlikSenseTasks { cpExisting, fakeTaskId, nodesWithEvents, + options, }); // Add external program task as node in task network diff --git a/src/lib/util/task.js b/src/lib/util/task.js index 87c8d24..c4602e3 100644 --- a/src/lib/util/task.js +++ b/src/lib/util/task.js @@ -1,4 +1,5 @@ const axios = require('axios'); +const fs = require('fs'); const path = require('path'); const { validate } = require('uuid'); @@ -75,6 +76,10 @@ async function taskExistById(taskId, optionsParam) { } } +// Get task metadata, given a task name +// Returs: +// - false if task does not exist or if multiple tasks with the same name exist +// - task metadata if task exists async function getTaskByName(taskName, optionsParam) { try { logger.debug(`Get task with name ${taskName}`); @@ -200,8 +205,144 @@ async function getTaskById(taskId, optionsParam) { } } +// Delete a reload task given its ID +// If the reload task ID is a valid GUID it is assumed to be a reload task ID that exists in Sense. Report an error if not. +async function deleteReloadTaskById(taskId, optionsParam) { + try { + logger.debug(`Delete reload task with ID ${taskId}`); + + // Did we get any options as parameter? + let options; + if (!optionsParam) { + // Get CLI options + options = getCliOptions(); + } else { + options = optionsParam; + } + + // Is the task ID a valid GUID? + if (!validate(taskId)) { + logger.error(`DELETE RELOAD TASK BY ID: Task ID ${taskId} is not a valid GUID.`); + + return false; + } + + logger.verbose(`DELETE RELOAD TASK BY ID: Task ID ${taskId} is a valid GUID. Delete associated task from QSEoW.`); + + // Expand cert file paths + const fileCert = path.resolve(execPath, options.authCertFile); + const fileCertKey = path.resolve(execPath, options.authCertKeyFile); + + // Make sure certificate files exist on disk + if (!fs.existsSync(fileCert)) { + logger.error(`DELETE RELOAD TASK BY ID: Certificate file ${fileCert} does not exist.`); + return false; + } + + if (!fs.existsSync(fileCertKey)) { + logger.error(`DELETE RELOAD TASK BY ID: Certificate key file ${fileCertKey} does not exist.`); + return false; + } + + const axiosConfig = setupQRSConnection(options, { + method: 'delete', + fileCert, + fileCertKey, + path: `/qrs/reloadtask/${taskId}`, + }); + + const result = await axios.request(axiosConfig); + logger.debug(`DELETE RELOAD TASK BY ID: Result=${result.status}`); + + if (result.status === 204) { + logger.verbose(`Reload task with ID ${taskId} deleted successfully.`); + return true; + } + + return false; + } catch (err) { + logger.error(`DEDELETE RELOAD TASK BY ID: ${err}`); + + // Show stack trace if available + if (err?.stack) { + logger.error(`DELETE RELOAD TASK BY ID:\n ${err.stack}`); + } + + return false; + } +} + +// Delete a external program task given its ID +// If the task ID is a valid GUID it is assumed to be a ext pgm task ID that exists in Sense. Report an error if not. +async function deleteExternalProgramTaskById(taskId, optionsParam) { + try { + logger.debug(`Delete external program task with ID ${taskId}`); + + // Did we get any options as parameter? + let options; + if (!optionsParam) { + // Get CLI options + options = getCliOptions(); + } else { + options = optionsParam; + } + + // Is the task ID a valid GUID? + if (!validate(taskId)) { + logger.error(`DELETE EXT PGM TASK BY ID: Task ID ${taskId} is not a valid GUID.`); + + return false; + } + + logger.verbose(`DELETE EXT PGM TASK BY ID: Task ID ${taskId} is a valid GUID. Delete associated task from QSEoW.`); + + // Expand cert file paths + const fileCert = path.resolve(execPath, options.authCertFile); + const fileCertKey = path.resolve(execPath, options.authCertKeyFile); + + // Make sure certificate files exist on disk + if (!fs.existsSync(fileCert)) { + logger.error(`DELETE EXT PGM TASK BY ID: Certificate file ${fileCert} does not exist.`); + return false; + } + + if (!fs.existsSync(fileCertKey)) { + logger.error(`DELETE EXT PGM TASK BY ID: Certificate key file ${fileCertKey} does not exist.`); + return false; + } + + const axiosConfig = setupQRSConnection(options, { + method: 'delete', + fileCert, + fileCertKey, + path: `/qrs/externalprogramtask/${taskId}`, + }); + + const result = await axios.request(axiosConfig); + logger.debug(`DELETE TASK BY ID: Result=${result.status}`); + + if (result.status === 204) { + logger.verbose(`External program task with ID ${taskId} deleted successfully.`); + return true; + } + + return false; + } catch (err) { + logger.error(`DELETE EXT PGM TASK BY ID: ${err}`); + + // Show stack trace if available + if (err?.stack) { + logger.error(`DELETE EXT PGM TASK BY ID:\n ${err.stack}`); + } + + return false; + } +} + module.exports = { taskExistById, getTaskByName, getTaskById, + deleteReloadTaskById, + deleteExternalProgramTaskById, }; diff --git a/testdata/tasks-6.csv b/testdata/tasks-6.csv index f717095..66f71d5 100644 --- a/testdata/tasks-6.csv +++ b/testdata/tasks-6.csv @@ -1,5 +1,5 @@ Task counter,Task type,Task name,Task id,Task enabled,Task timeout,Task retries,App id,Partial reload,Manually triggered,Ext program path,Ext program parameters,Task status,Task started,Task ended,Task duration,Task executedon node,Tags,Custom properties,Event counter,Event type,Event name,Event enabled,Event created date,Event modified date,Event modified by,Schema increment option,Schema increment description,Daylight savings time,Schema start,Schema expiration,Schema filter description,Schema time zone,Time contstraint seconds,Time contstraint minutes,Time contstraint hours,Time contstraint days,Rule counter,Rule state,Rule task name,Rule task id 1,External program,New external program task 1,task-1,1,1440,3,,,,C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe,-File \\pro2-win1\c$\tools\script\ext_task_1.ps1,❔ ?,,,0:00:00,,api1,Department=Finance / Department=Sales,,,,,,,,,,,,,,,,,,,,,, 1,,,,,,,,,,,,,,,,,,,2,Composite,Trigger when upstream tasks are done,,,,LAB\goran,,,,,,,,0,360,0,0,,,, -1,,,,,,,,,,,,,,,,,,,2,,,,,,,,,,,,,,,,,,1,TaskSuccessful,,9b8bd484-9cd9-4c83-b8e5-019176364b82 -1,,,,,,,,,,,,,,,,,,,2,,,,,,,,,,,,,,,,,,2,TaskFail,,d5f0b635-1feb-465e-a9c8-33f6e74b1248 +1,,,,,,,,,,,,,,,,,,,2,,,,,,,,,,,,,,,,,,1,TaskSuccessful,,e9100e69-4e8e-414b-bf88-10a1110c43a9 +1,,,,,,,,,,,,,,,,,,,2,,,,,,,,,,,,,,,,,,2,TaskFail,,a4b7b603-5d42-4e13-a67b-777d42c0d920