diff --git a/.eslintrc.js b/.eslintrc.js index 2e3218d37f..20b722d1da 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -25,6 +25,7 @@ module.exports = { ], "rules": { "@typescript-eslint/no-magic-numbers": "off", + "@typescript-eslint/no-restricted-imports": "off", "jest/expect-expect": ["warn", { "assertFunctionNames": ["expect*", "**.*expect*"] }], @@ -64,6 +65,11 @@ module.exports = { "ignoreEnums": true, "ignoreReadonlyClassProperties": true }], + "@typescript-eslint/no-restricted-imports": ["error", { + "patterns": [{ + "group": ["**/../lib", "**/../src"] + }] + }], "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/semi": "warn", diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 8d08046c1c..8969d73a5f 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -21,7 +21,8 @@ jobs: - name: Configure Sonar Scan uses: zowe-actions/octorelease/script@v1 env: - COVERAGE_ARTIFACT: "ubuntu-latest-18.x-results:__tests__/__results__" + COVERAGE_ARTIFACT: "ubuntu-22.04-20.x-results:__tests__/__results__" + # COVERAGE_ARTIFACT: "ubuntu-latest-20.x-results:__tests__/__results__" with: script: sonarConfig diff --git a/__tests__/__packages__/cli-test-utils/CHANGELOG.md b/__tests__/__packages__/cli-test-utils/CHANGELOG.md index bc707aabee..2218f50dfa 100644 --- a/__tests__/__packages__/cli-test-utils/CHANGELOG.md +++ b/__tests__/__packages__/cli-test-utils/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the Zowe CLI test utils package will be documented in this file. +## Recent Changes + +- BugFix: Improved the error message shown on MacOS when `runCliScript` method fails to run script that is missing shebang line. [#2314](https://github.com/zowe/zowe-cli/pull/2314) + ## `8.1.1` - BugFix: Updated peer dependencies to `^8.0.0`, dropping support for versions tagged `next`. [#2287](https://github.com/zowe/zowe-cli/pull/2287) diff --git a/__tests__/__packages__/cli-test-utils/package.json b/__tests__/__packages__/cli-test-utils/package.json index e812890352..5a40c21885 100644 --- a/__tests__/__packages__/cli-test-utils/package.json +++ b/__tests__/__packages__/cli-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/cli-test-utils", - "version": "8.2.0", + "version": "8.7.0", "description": "Test utilities package for Zowe CLI plug-ins", "author": "Zowe", "license": "EPL-2.0", @@ -43,7 +43,7 @@ "devDependencies": { "@types/js-yaml": "^4.0.9", "@types/uuid": "^10.0.0", - "@zowe/imperative": "8.2.0" + "@zowe/imperative": "8.7.0" }, "peerDependencies": { "@zowe/imperative": "^8.0.0" diff --git a/__tests__/__packages__/cli-test-utils/src/TestUtils.ts b/__tests__/__packages__/cli-test-utils/src/TestUtils.ts index 6952abcc7c..90cb8be95a 100644 --- a/__tests__/__packages__/cli-test-utils/src/TestUtils.ts +++ b/__tests__/__packages__/cli-test-utils/src/TestUtils.ts @@ -10,6 +10,7 @@ */ import * as fs from "fs"; +import * as path from "path"; import { spawnSync, SpawnSyncReturns, ExecFileException } from "child_process"; import { ITestEnvironment } from "./environment/doc/response/ITestEnvironment"; import { ICommandDefinition, IHandlerParameters } from "@zowe/imperative"; @@ -55,12 +56,16 @@ export function runCliScript(scriptPath: string, testEnvironment: ITestEnvironme } catch { fs.chmodSync(scriptPath, "755"); } - return spawnSync(scriptPath, args, { + const response = spawnSync(scriptPath, args, { cwd: testEnvironment.workingDir, env: childEnv, encoding: "buffer" }); + if (process.platform === "darwin" && (response.error as ExecFileException)?.errno === -8) { + throw new Error(`The script file ${path.basename(scriptPath)} failed to execute. Check that it starts with a shebang line.`); + } + return response; } else { throw new Error(`The script file ${scriptPath} doesn't exist`); @@ -121,7 +126,9 @@ export function mockHandlerParameters(params: PartialHandlerParameters): IHandle }, progress: { startBar: jest.fn((parms) => undefined), - endBar: jest.fn(() => undefined) + endBar: jest.fn(() => undefined), + startSpinner: jest.fn(() => undefined), + endSpinner: jest.fn(() => undefined) }, format: { output: jest.fn((parms) => { diff --git a/__tests__/__resources__/properties/default_properties.yaml b/__tests__/__resources__/properties/default_properties.yaml index 9e3c215a5e..21fc02313f 100644 --- a/__tests__/__resources__/properties/default_properties.yaml +++ b/__tests__/__resources__/properties/default_properties.yaml @@ -104,6 +104,8 @@ zosjobs: modifiedJobclass: B # System affinity sysaff: zosjobs-sysaff + # CIM Support + skipCIM: false #-----------------------------------------------------------------------------# # Set of properties for testing provisioning # #-----------------------------------------------------------------------------# diff --git a/__tests__/__src__/properties/ITestZosJobsSchema.ts b/__tests__/__src__/properties/ITestZosJobsSchema.ts index ba6685b85d..cb18e46020 100644 --- a/__tests__/__src__/properties/ITestZosJobsSchema.ts +++ b/__tests__/__src__/properties/ITestZosJobsSchema.ts @@ -16,4 +16,5 @@ export interface ITestZosJobsSchema { jobclass: string; modifiedJobclass: string; sysaff: string; + skipCIM?: boolean; } diff --git a/jest.config.js b/jest.config.js index c3077263a8..0e05bcca5d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -133,14 +133,11 @@ module.exports = { ...projectConfig, "reporters": [ "default", + "jest-stare", ["jest-junit", { "outputDirectory": "__tests__/__results__", "reportTestSuiteErrors": true }], - ["jest-stare", { - "coverageLink": "../unit/coverage/lcov-report/index.html", - "resultDir": "__tests__/__results__/jest-stare" - }], ["github-actions", { "silent": false } ] ], "testResultsProcessor": "jest-sonar-reporter", diff --git a/lerna.json b/lerna.json index 49dd1d0b98..7f74212cf8 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "8.3.0", + "version": "8.7.0", "command": { "publish": { "ignoreChanges": [ diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 7bddbcdbd2..43a462220f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -52,7 +52,7 @@ }, "__tests__/__packages__/cli-test-utils": { "name": "@zowe/cli-test-utils", - "version": "8.2.0", + "version": "8.7.0", "license": "EPL-2.0", "dependencies": { "find-up": "^5.0.0", @@ -63,7 +63,7 @@ "devDependencies": { "@types/js-yaml": "^4.0.9", "@types/uuid": "^10.0.0", - "@zowe/imperative": "8.2.0" + "@zowe/imperative": "8.7.0" }, "peerDependencies": { "@zowe/imperative": "^8.0.0" @@ -16267,21 +16267,21 @@ }, "packages/cli": { "name": "@zowe/cli", - "version": "8.3.0", + "version": "8.7.0", "hasInstallScript": true, "license": "EPL-2.0", "dependencies": { - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0", - "@zowe/provisioning-for-zowe-sdk": "8.2.0", - "@zowe/zos-console-for-zowe-sdk": "8.2.0", - "@zowe/zos-files-for-zowe-sdk": "8.2.0", - "@zowe/zos-jobs-for-zowe-sdk": "8.2.0", - "@zowe/zos-logs-for-zowe-sdk": "8.2.0", - "@zowe/zos-tso-for-zowe-sdk": "8.2.0", - "@zowe/zos-uss-for-zowe-sdk": "8.2.0", - "@zowe/zos-workflows-for-zowe-sdk": "8.2.0", - "@zowe/zosmf-for-zowe-sdk": "8.2.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0", + "@zowe/provisioning-for-zowe-sdk": "8.7.0", + "@zowe/zos-console-for-zowe-sdk": "8.7.0", + "@zowe/zos-files-for-zowe-sdk": "8.7.0", + "@zowe/zos-jobs-for-zowe-sdk": "8.7.0", + "@zowe/zos-logs-for-zowe-sdk": "8.7.0", + "@zowe/zos-tso-for-zowe-sdk": "8.7.0", + "@zowe/zos-uss-for-zowe-sdk": "8.7.0", + "@zowe/zos-workflows-for-zowe-sdk": "8.7.0", + "@zowe/zosmf-for-zowe-sdk": "8.7.0", "find-process": "1.4.7", "lodash": "4.17.21", "minimatch": "9.0.5", @@ -16294,7 +16294,7 @@ "@types/diff": "^5.0.9", "@types/lodash": "^4.17.6", "@types/tar": "^6.1.11", - "@zowe/cli-test-utils": "8.2.0", + "@zowe/cli-test-utils": "8.7.0", "comment-json": "^4.2.3", "strip-ansi": "^6.0.1", "which": "^4.0.0" @@ -16350,15 +16350,15 @@ }, "packages/core": { "name": "@zowe/core-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "license": "EPL-2.0", "dependencies": { "comment-json": "~4.2.3", "string-width": "^4.2.3" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/imperative": "8.7.0" }, "engines": { "node": ">=18.12.0" @@ -16369,7 +16369,7 @@ }, "packages/imperative": { "name": "@zowe/imperative", - "version": "8.2.0", + "version": "8.7.0", "license": "EPL-2.0", "dependencies": { "@types/yargs": "^17.0.32", @@ -16563,16 +16563,16 @@ }, "packages/provisioning": { "name": "@zowe/provisioning-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "license": "EPL-2.0", "dependencies": { "js-yaml": "^4.1.0" }, "devDependencies": { "@types/js-yaml": "^4.0.9", - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "engines": { "node": ">=18.12.0" @@ -16597,15 +16597,15 @@ }, "packages/workflows": { "name": "@zowe/zos-workflows-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "license": "EPL-2.0", "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.2.0" + "@zowe/zos-files-for-zowe-sdk": "8.7.0" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "engines": { "node": ">=18.12.0" @@ -16617,12 +16617,12 @@ }, "packages/zosconsole": { "name": "@zowe/zos-console-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "license": "EPL-2.0", "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "engines": { "node": ">=18.12.0" @@ -16634,17 +16634,17 @@ }, "packages/zosfiles": { "name": "@zowe/zos-files-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "license": "EPL-2.0", "dependencies": { "lodash": "^4.17.21", "minimatch": "^9.0.5" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0", - "@zowe/zos-uss-for-zowe-sdk": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0", + "@zowe/zos-uss-for-zowe-sdk": "8.7.0" }, "engines": { "node": ">=18.12.0" @@ -16676,15 +16676,15 @@ }, "packages/zosjobs": { "name": "@zowe/zos-jobs-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "license": "EPL-2.0", "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.2.0" + "@zowe/zos-files-for-zowe-sdk": "8.7.0" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "engines": { "node": ">=18.12.0" @@ -16696,12 +16696,12 @@ }, "packages/zoslogs": { "name": "@zowe/zos-logs-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "license": "EPL-2.0", "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "engines": { "node": ">=18.12.0" @@ -16713,12 +16713,12 @@ }, "packages/zosmf": { "name": "@zowe/zosmf-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "license": "EPL-2.0", "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "engines": { "node": ">=18.12.0" @@ -16730,15 +16730,15 @@ }, "packages/zostso": { "name": "@zowe/zos-tso-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "license": "EPL-2.0", "dependencies": { - "@zowe/zosmf-for-zowe-sdk": "8.2.0" + "@zowe/zosmf-for-zowe-sdk": "8.7.0" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "engines": { "node": ">=18.12.0" @@ -16750,15 +16750,15 @@ }, "packages/zosuss": { "name": "@zowe/zos-uss-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "license": "EPL-2.0", "dependencies": { "ssh2": "^1.15.0" }, "devDependencies": { "@types/ssh2": "^1.11.19", - "@zowe/cli-test-utils": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/imperative": "8.7.0" }, "engines": { "node": ">=18.12.0" diff --git a/package.json b/package.json index 1339c31a74..dc705bb400 100644 --- a/package.json +++ b/package.json @@ -82,5 +82,13 @@ }, "jestSonar": { "reportPath": "__tests__/__results__/jest-sonar" + }, + "jest-stare": { + "additionalResultsProcessors": [ + "jest-junit", + "jest-sonar-reporter" + ], + "coverageLink": "../unit/coverage/lcov-report/index.html", + "resultDir": "__tests__/__results__/jest-stare" } } diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index b249587912..8bfca62e1c 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to the Zowe CLI package will be documented in this file. +## `8.7.0` + +- Enhancement: Added --wait-for-active and --wait-for-output to download options on zosjobs. [#2328](https://github.com/zowe/zowe-cli/pull/2328) + +## `8.6.2` + +- BugFix: Resolved issue where `zowe zos-files upload file-to-uss` was not properly handling command flags. [#2234](https://github.com/zowe/zowe-cli/pull/2334) + +## `8.6.1` + +- BugFix: Fixed an issue where the `zowe zos-logs list logs` command could fail or not return all logs if a start time was not supplied. [#2336](https://github.com/zowe/zowe-cli/pull/2336) + +## `8.6.0` + +- Enhancement: Added support for running applications on TSO/E address spaces. Start applications and receive/transmit messages using the new `tso start`, `tso receive` and `tso send` commands. [#2280](https://github.com/zowe/zowe-cli/pull/2280) + +## `8.4.0` + +- Enhancement: Added optional `--attributes` flag to `zowe zos-files upload file-to-uss` to allow passing a .zosattributes file path for upload encoding format. [#2319](https://github.com/zowe/zowe-cli/pull/2319) + ## `8.3.0` - Enhancement: Issue the `zowe files search data-sets` command with the new `encoding` option to use a different code page when searching data set contents. [#2161](https://github.com/zowe/zowe-cli/issues/2161) diff --git a/packages/cli/__tests__/config/auto-init/__system__/__scripts__/config_auto_init.sh b/packages/cli/__tests__/config/auto-init/__system__/__scripts__/config_auto_init.sh index 6328d7d363..dd62bf40a8 100755 --- a/packages/cli/__tests__/config/auto-init/__system__/__scripts__/config_auto_init.sh +++ b/packages/cli/__tests__/config/auto-init/__system__/__scripts__/config_auto_init.sh @@ -1,3 +1,4 @@ +#!/bin/bash set -e HOST=$1 diff --git a/packages/cli/__tests__/config/auto-init/__system__/__scripts__/config_auto_init_cert.sh b/packages/cli/__tests__/config/auto-init/__system__/__scripts__/config_auto_init_cert.sh index 88d6e63d2c..d1478052f2 100755 --- a/packages/cli/__tests__/config/auto-init/__system__/__scripts__/config_auto_init_cert.sh +++ b/packages/cli/__tests__/config/auto-init/__system__/__scripts__/config_auto_init_cert.sh @@ -1,3 +1,4 @@ +#!/bin/bash set -e HOST=$1 diff --git a/packages/cli/__tests__/config/auto-init/__system__/__scripts__/config_auto_init_profile.sh b/packages/cli/__tests__/config/auto-init/__system__/__scripts__/config_auto_init_profile.sh index 1b39110909..a0ed357b6a 100755 --- a/packages/cli/__tests__/config/auto-init/__system__/__scripts__/config_auto_init_profile.sh +++ b/packages/cli/__tests__/config/auto-init/__system__/__scripts__/config_auto_init_profile.sh @@ -1,3 +1,4 @@ +#!/bin/bash set -e zowe config auto-init diff --git a/packages/cli/__tests__/config/auto-init/__system__/cli.config.auto-init.system.test.ts b/packages/cli/__tests__/config/auto-init/__system__/cli.config.auto-init.system.test.ts index 863f93d0ef..9a4bd30fe2 100644 --- a/packages/cli/__tests__/config/auto-init/__system__/cli.config.auto-init.system.test.ts +++ b/packages/cli/__tests__/config/auto-init/__system__/cli.config.auto-init.system.test.ts @@ -45,6 +45,7 @@ describe("config auto-init without profile", () => { base.rejectUnauthorized ] ); + expect(response.status).toEqual(0); const config = fs.readFileSync(path.join(TEST_ENVIRONMENT.workingDir, "zowe.config.json")).toString(); // Typecasting because of this issue: https://github.com/kaelzhang/node-comment-json/issues/42 @@ -107,6 +108,7 @@ describe("config auto-init without profile", () => { base.rejectUnauthorized ] ); + expect(response.status).toEqual(0); config = fs.readFileSync(path.join(TEST_ENVIRONMENT.workingDir, "zowe.config.json")).toString(); // Typecasting because of this issue: https://github.com/kaelzhang/node-comment-json/issues/42 @@ -186,6 +188,7 @@ describe("config auto-init without profile and with certificates", () => { base.rejectUnauthorized ] ); + expect(response.status).toEqual(0); const config = fs.readFileSync(path.join(TEST_ENVIRONMENT.workingDir, "zowe.config.json")).toString(); // Typecasting because of this issue: https://github.com/kaelzhang/node-comment-json/issues/42 @@ -254,6 +257,7 @@ describe("config auto-init without profile and with certificates", () => { base.rejectUnauthorized ] ); + expect(response.status).toEqual(0); config = fs.readFileSync(path.join(TEST_ENVIRONMENT.workingDir, "zowe.config.json")).toString(); // Typecasting because of this issue: https://github.com/kaelzhang/node-comment-json/issues/42 @@ -319,6 +323,7 @@ describe("config auto-init with profile", () => { it("should successfully issue the auto-init command", () => { const response = runCliScript(__dirname + "/__scripts__/config_auto_init_profile.sh", TEST_ENVIRONMENT); + expect(response.status).toEqual(0); const config = fs.readFileSync(path.join(TEST_ENVIRONMENT.workingDir, "zowe.config.json")).toString(); // Typecasting because of this issue: https://github.com/kaelzhang/node-comment-json/issues/42 @@ -379,6 +384,7 @@ describe("config auto-init with profile and certificates", () => { it("should successfully issue the auto-init command", () => { const response = runCliScript(__dirname + "/__scripts__/config_auto_init_profile.sh", TEST_ENVIRONMENT); + expect(response.status).toEqual(0); const config = fs.readFileSync(path.join(TEST_ENVIRONMENT.workingDir, "zowe.config.json")).toString(); // Typecasting because of this issue: https://github.com/kaelzhang/node-comment-json/issues/42 diff --git a/packages/cli/__tests__/zosfiles/__integration__/upload/ftu/__snapshots__/cli.files.upload.ftu.integration.test.ts.snap b/packages/cli/__tests__/zosfiles/__integration__/upload/ftu/__snapshots__/cli.files.upload.ftu.integration.test.ts.snap index 8d37ccd150..de432abd19 100644 --- a/packages/cli/__tests__/zosfiles/__integration__/upload/ftu/__snapshots__/cli.files.upload.ftu.integration.test.ts.snap +++ b/packages/cli/__tests__/zosfiles/__integration__/upload/ftu/__snapshots__/cli.files.upload.ftu.integration.test.ts.snap @@ -43,6 +43,10 @@ exports[`Upload uss file should display the help 1`] = ` Data content in encoding mode, which means that data conversion is performed according to the encoding specified. + --attributes | --attrs (string) + + Path of an attributes file to control how files are uploaded. + --response-timeout | --rto (number) The maximum amount of time in seconds the z/OSMF Files TSO servlet should run @@ -152,8 +156,8 @@ exports[`Upload uss file should display the help 1`] = ` \\"success\\": true, \\"exitCode\\": 0, \\"message\\": \\"The help was constructed for command: file-to-uss.\\", - \\"stdout\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n file-to-uss | ftu\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Upload content to a USS file from local file.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-files upload file-to-uss [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n inputfile\\\\t\\\\t (string)\\\\n\\\\n The local file that you want to upload to a USS file\\\\n\\\\n USSFileName\\\\t\\\\t (string)\\\\n\\\\n The name of the USS file to which you want to upload the file\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --binary | -b (boolean)\\\\n\\\\n Data content in binary mode, which means that no data conversion is performed.\\\\n The data transfer process returns each record as-is, without translation. No\\\\n delimiters are added between records.\\\\n\\\\n --encoding | --ec (string)\\\\n\\\\n Data content in encoding mode, which means that data conversion is performed\\\\n according to the encoding specified.\\\\n\\\\n --response-timeout | --rto (number)\\\\n\\\\n The maximum amount of time in seconds the z/OSMF Files TSO servlet should run\\\\n before returning a response. Any request exceeding this amount of time will be\\\\n terminated and return an error. Allowed values: 5 - 600\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Upload to the USS file \\\\\\"/a/ibmuser/my_text.txt\\\\\\" from the\\\\n file \\\\\\"file.txt\\\\\\":\\\\n\\\\n $ zowe zos-files upload file-to-uss \\\\\\"file.txt\\\\\\" \\\\\\"/a/ibmuser/my_text.txt\\\\\\"\\\\n\\\\n\\", + \\"stdout\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n file-to-uss | ftu\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Upload content to a USS file from local file.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-files upload file-to-uss [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n inputfile\\\\t\\\\t (string)\\\\n\\\\n The local file that you want to upload to a USS file\\\\n\\\\n USSFileName\\\\t\\\\t (string)\\\\n\\\\n The name of the USS file to which you want to upload the file\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --binary | -b (boolean)\\\\n\\\\n Data content in binary mode, which means that no data conversion is performed.\\\\n The data transfer process returns each record as-is, without translation. No\\\\n delimiters are added between records.\\\\n\\\\n --encoding | --ec (string)\\\\n\\\\n Data content in encoding mode, which means that data conversion is performed\\\\n according to the encoding specified.\\\\n\\\\n --attributes | --attrs (string)\\\\n\\\\n Path of an attributes file to control how files are uploaded.\\\\n\\\\n --response-timeout | --rto (number)\\\\n\\\\n The maximum amount of time in seconds the z/OSMF Files TSO servlet should run\\\\n before returning a response. Any request exceeding this amount of time will be\\\\n terminated and return an error. Allowed values: 5 - 600\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Upload to the USS file \\\\\\"/a/ibmuser/my_text.txt\\\\\\" from the\\\\n file \\\\\\"file.txt\\\\\\":\\\\n\\\\n $ zowe zos-files upload file-to-uss \\\\\\"file.txt\\\\\\" \\\\\\"/a/ibmuser/my_text.txt\\\\\\"\\\\n\\\\n\\", \\"stderr\\": \\"\\", - \\"data\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n file-to-uss | ftu\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Upload content to a USS file from local file.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-files upload file-to-uss [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n inputfile\\\\t\\\\t (string)\\\\n\\\\n The local file that you want to upload to a USS file\\\\n\\\\n USSFileName\\\\t\\\\t (string)\\\\n\\\\n The name of the USS file to which you want to upload the file\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --binary | -b (boolean)\\\\n\\\\n Data content in binary mode, which means that no data conversion is performed.\\\\n The data transfer process returns each record as-is, without translation. No\\\\n delimiters are added between records.\\\\n\\\\n --encoding | --ec (string)\\\\n\\\\n Data content in encoding mode, which means that data conversion is performed\\\\n according to the encoding specified.\\\\n\\\\n --response-timeout | --rto (number)\\\\n\\\\n The maximum amount of time in seconds the z/OSMF Files TSO servlet should run\\\\n before returning a response. Any request exceeding this amount of time will be\\\\n terminated and return an error. Allowed values: 5 - 600\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Upload to the USS file \\\\\\"/a/ibmuser/my_text.txt\\\\\\" from the\\\\n file \\\\\\"file.txt\\\\\\":\\\\n\\\\n $ zowe zos-files upload file-to-uss \\\\\\"file.txt\\\\\\" \\\\\\"/a/ibmuser/my_text.txt\\\\\\"\\\\n\\\\n\\" + \\"data\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n file-to-uss | ftu\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Upload content to a USS file from local file.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-files upload file-to-uss [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n inputfile\\\\t\\\\t (string)\\\\n\\\\n The local file that you want to upload to a USS file\\\\n\\\\n USSFileName\\\\t\\\\t (string)\\\\n\\\\n The name of the USS file to which you want to upload the file\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --binary | -b (boolean)\\\\n\\\\n Data content in binary mode, which means that no data conversion is performed.\\\\n The data transfer process returns each record as-is, without translation. No\\\\n delimiters are added between records.\\\\n\\\\n --encoding | --ec (string)\\\\n\\\\n Data content in encoding mode, which means that data conversion is performed\\\\n according to the encoding specified.\\\\n\\\\n --attributes | --attrs (string)\\\\n\\\\n Path of an attributes file to control how files are uploaded.\\\\n\\\\n --response-timeout | --rto (number)\\\\n\\\\n The maximum amount of time in seconds the z/OSMF Files TSO servlet should run\\\\n before returning a response. Any request exceeding this amount of time will be\\\\n terminated and return an error. Allowed values: 5 - 600\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Upload to the USS file \\\\\\"/a/ibmuser/my_text.txt\\\\\\" from the\\\\n file \\\\\\"file.txt\\\\\\":\\\\n\\\\n $ zowe zos-files upload file-to-uss \\\\\\"file.txt\\\\\\" \\\\\\"/a/ibmuser/my_text.txt\\\\\\"\\\\n\\\\n\\" }" `; diff --git a/packages/cli/__tests__/zosfiles/__unit__/upload/ftu/FileToUSS.handler.unit.test.ts b/packages/cli/__tests__/zosfiles/__unit__/upload/ftu/FileToUSS.handler.unit.test.ts index c4eedd2bd4..5956179949 100644 --- a/packages/cli/__tests__/zosfiles/__unit__/upload/ftu/FileToUSS.handler.unit.test.ts +++ b/packages/cli/__tests__/zosfiles/__unit__/upload/ftu/FileToUSS.handler.unit.test.ts @@ -9,7 +9,7 @@ * */ -import { Upload } from "@zowe/zos-files-for-zowe-sdk"; +import { Upload, ZosFilesAttributes } from "@zowe/zos-files-for-zowe-sdk"; import { UNIT_TEST_ZOSMF_PROF_OPTS } from "../../../../../../../__tests__/__src__/TestConstants"; describe("Upload file-to-uss handler", () => { @@ -29,7 +29,7 @@ describe("Upload file-to-uss handler", () => { let fakeSession = null; // Mock the submit JCL function - Upload.fileToUssFile = jest.fn(async (session, file, name, options = {}) => { + Upload.uploadFile = jest.fn(async (session, file, name, options = {}) => { fakeSession = session; return { success: true, @@ -79,10 +79,102 @@ describe("Upload file-to-uss handler", () => { } expect(error).toBeUndefined(); - expect(Upload.fileToUssFile).toHaveBeenCalledTimes(1); - expect(Upload.fileToUssFile).toHaveBeenCalledWith(fakeSession, inputfile, USSFileName, { + expect(Upload.uploadFile).toHaveBeenCalledTimes(1); + expect(Upload.uploadFile).toHaveBeenCalledWith(fakeSession, inputfile, USSFileName, { binary: undefined, - encoding: undefined, + task: { + percentComplete: 0, + stageName: 0, + statusMessage: "Uploading USS file" + }, + includeHidden: undefined, + maxConcurrentRequests: undefined, + responseTimeout: undefined + }); + expect(jsonObj).toMatchSnapshot(); + expect(apiMessage).toMatchSnapshot(); + expect(logMessage).toMatchSnapshot(); + }); + it("should upload a file to a USS if requested - zosattributes file", async () => { + // Require the handler and create a new instance + const handlerReq = require("../../../../../src/zosfiles/upload/ftu/FileToUSS.handler"); + const handler = new handlerReq.default(); + const inputfile = "test-file"; + const USSFileName = "testing"; + let zosAttributes: any; + + let error; + let apiMessage = ""; + let jsonObj; + let logMessage = ""; + let fakeSession = null; + + jest.spyOn(ZosFilesAttributes, "loadFromFile").mockImplementation(() => { + zosAttributes = Object.create(ZosFilesAttributes.prototype); + zosAttributes.attributes = new Map([ + ['*.json', { ignore: true }], + ['*.bin', { ignore: false, localEncoding: 'binary', remoteEncoding: 'binary' }], + ['*.jcl', { ignore: false, localEncoding: 'IBM-1047', remoteEncoding: 'IBM-1047' }], + ['*.md', { ignore: false, localEncoding: 'UTF-8', remoteEncoding: 'UTF-8' }], + ['*.txt', { ignore: false, localEncoding: 'UTF-8', remoteEncoding: 'IBM-1047' }] + ]); + zosAttributes.basePath = undefined; + return zosAttributes; + }); + Upload.uploadFile = jest.fn(async (session, file, name, options = {}) => { + fakeSession = session; + return { + success: true, + commandResponse: "uploaded", + apiResponse: [ + { success: true, from: inputfile, to: USSFileName } + ] + }; + }); + try { + await handler.process({ + arguments: { + $0: "fake", + _: ["fake"], + inputfile, + USSFileName, + ...UNIT_TEST_ZOSMF_PROF_OPTS + }, + response: { + data: { + setMessage: jest.fn((setMsgArgs) => { + apiMessage = setMsgArgs; + }), + setObj: jest.fn((setObjArgs) => { + jsonObj = setObjArgs; + }) + }, + console: { + log: jest.fn((logArgs) => { + logMessage += "\n" + logArgs; + }) + }, + progress: { + startBar: jest.fn(() => { + // do nothing + }), + endBar: jest.fn(() => { + // do nothing + }) + } + } + } as any); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + expect(Upload.uploadFile).toHaveBeenCalledTimes(1); + expect(Upload.uploadFile).toHaveBeenCalledWith(fakeSession, inputfile, USSFileName, { + attributes: zosAttributes, + binary: undefined, + includeHidden: undefined, + maxConcurrentRequests: undefined, + responseTimeout: undefined, task: { percentComplete: 0, stageName: 0, @@ -93,6 +185,95 @@ describe("Upload file-to-uss handler", () => { expect(apiMessage).toMatchSnapshot(); expect(logMessage).toMatchSnapshot(); }); + it("should upload a file to a USS if requested - zosattributes file - binary", async () => { + // Require the handler and create a new instance + const handlerReq = require("../../../../../src/zosfiles/upload/ftu/FileToUSS.handler"); + const handler = new handlerReq.default(); + const inputfile = "test-file"; + const USSFileName = "testing"; + let zosAttributes: any; + let error; + let apiMessage = ""; + let jsonObj; + let logMessage = ""; + let fakeSession = null; + + jest.spyOn(ZosFilesAttributes, "loadFromFile").mockImplementation(() => { + zosAttributes = Object.create(ZosFilesAttributes.prototype); + zosAttributes.attributes = new Map([ + ['*.json', { ignore: true }], + ['*.bin', { ignore: false, localEncoding: 'binary', remoteEncoding: 'binary' }], + ['*.jcl', { ignore: false, localEncoding: 'IBM-1047', remoteEncoding: 'IBM-1047' }], + ['*.md', { ignore: false, localEncoding: 'UTF-8', remoteEncoding: 'UTF-8' }], + ['*.txt', { ignore: false, localEncoding: 'binary', remoteEncoding: 'binary' }] + ]); + zosAttributes.basePath = undefined; + return zosAttributes; + }); + Upload.uploadFile = jest.fn(async (session, file, name, options = {}) => { + fakeSession = session; + return { + success: true, + commandResponse: "uploaded", + apiResponse: [ + { success: true, from: inputfile, to: USSFileName } + ] + }; + }); + try { + await handler.process({ + arguments: { + $0: "fake", + _: ["fake"], + inputfile, + USSFileName, + ...UNIT_TEST_ZOSMF_PROF_OPTS + }, + response: { + data: { + setMessage: jest.fn((setMsgArgs) => { + apiMessage = setMsgArgs; + }), + setObj: jest.fn((setObjArgs) => { + jsonObj = setObjArgs; + }) + }, + console: { + log: jest.fn((logArgs) => { + logMessage += "\n" + logArgs; + }) + }, + progress: { + startBar: jest.fn(() => { + // do nothing + }), + endBar: jest.fn(() => { + // do nothing + }) + } + } + } as any); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + expect(Upload.uploadFile).toHaveBeenCalledTimes(1); + expect(Upload.uploadFile).toHaveBeenCalledWith(fakeSession, inputfile, USSFileName, { + attributes: zosAttributes, + binary: undefined, + includeHidden: undefined, + maxConcurrentRequests: undefined, + responseTimeout: undefined, + task: { + percentComplete: 0, + stageName: 0, + statusMessage: "Uploading USS file" + } + }); + expect(jsonObj).toMatchSnapshot(); + expect(apiMessage).toMatchSnapshot(); + expect(logMessage).toMatchSnapshot(); + }); }); }); diff --git a/packages/cli/__tests__/zosfiles/__unit__/upload/ftu/__snapshots__/FileToUSS.definition.unit.test.ts.snap b/packages/cli/__tests__/zosfiles/__unit__/upload/ftu/__snapshots__/FileToUSS.definition.unit.test.ts.snap index c701260de3..985f225221 100644 --- a/packages/cli/__tests__/zosfiles/__unit__/upload/ftu/__snapshots__/FileToUSS.definition.unit.test.ts.snap +++ b/packages/cli/__tests__/zosfiles/__unit__/upload/ftu/__snapshots__/FileToUSS.definition.unit.test.ts.snap @@ -18,6 +18,17 @@ Array [ "name": "encoding", "type": "string", }, + Object { + "aliases": Array [ + "attrs", + ], + "conflictsWith": Array [ + "ascii-files, binary-files", + ], + "description": "Path of an attributes file to control how files are uploaded.", + "name": "attributes", + "type": "string", + }, ] `; diff --git a/packages/cli/__tests__/zosfiles/__unit__/upload/ftu/__snapshots__/FileToUSS.handler.unit.test.ts.snap b/packages/cli/__tests__/zosfiles/__unit__/upload/ftu/__snapshots__/FileToUSS.handler.unit.test.ts.snap index 463337181f..f6a4e0d87a 100644 --- a/packages/cli/__tests__/zosfiles/__unit__/upload/ftu/__snapshots__/FileToUSS.handler.unit.test.ts.snap +++ b/packages/cli/__tests__/zosfiles/__unit__/upload/ftu/__snapshots__/FileToUSS.handler.unit.test.ts.snap @@ -1,5 +1,59 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Upload file-to-uss handler process method should upload a file to a USS if requested - zosattributes file - binary 1`] = ` +Object { + "apiResponse": Array [ + Object { + "from": "test-file", + "success": true, + "to": "testing", + }, + ], + "commandResponse": "uploaded", + "success": true, +} +`; + +exports[`Upload file-to-uss handler process method should upload a file to a USS if requested - zosattributes file - binary 2`] = `""`; + +exports[`Upload file-to-uss handler process method should upload a file to a USS if requested - zosattributes file - binary 3`] = ` +" +-  + success: true + from:  test-file + to:  testing + + +uploaded" +`; + +exports[`Upload file-to-uss handler process method should upload a file to a USS if requested - zosattributes file 1`] = ` +Object { + "apiResponse": Array [ + Object { + "from": "test-file", + "success": true, + "to": "testing", + }, + ], + "commandResponse": "uploaded", + "success": true, +} +`; + +exports[`Upload file-to-uss handler process method should upload a file to a USS if requested - zosattributes file 2`] = `""`; + +exports[`Upload file-to-uss handler process method should upload a file to a USS if requested - zosattributes file 3`] = ` +" +-  + success: true + from:  test-file + to:  testing + + +uploaded" +`; + exports[`Upload file-to-uss handler process method should upload a file to a uss if requested 1`] = ` Object { "apiResponse": Array [ diff --git a/packages/cli/__tests__/zosjobs/__integration__/download/__snapshots__/cli.zos-jobs.download.output.integration.test.ts.snap b/packages/cli/__tests__/zosjobs/__integration__/download/__snapshots__/cli.zos-jobs.download.output.integration.test.ts.snap index 5be065e078..6dd3b29ae4 100644 --- a/packages/cli/__tests__/zosjobs/__integration__/download/__snapshots__/cli.zos-jobs.download.output.integration.test.ts.snap +++ b/packages/cli/__tests__/zosjobs/__integration__/download/__snapshots__/cli.zos-jobs.download.output.integration.test.ts.snap @@ -93,6 +93,14 @@ exports[`zos-jobs download output command should display the help 1`] = ` Download the spool file content with encoding mode, which means that data conversion is performed using the file encoding specified. + --wait-for-active | --wfa (boolean) + + Wait for the job to enter ACTIVE status before completing the command. + + --wait-for-output | --wfo (boolean) + + Wait for the job to enter OUTPUT status before completing the command. + ZOSMF CONNECTION OPTIONS ------------------------ @@ -195,8 +203,8 @@ exports[`zos-jobs download output command should display the help 1`] = ` \\"success\\": true, \\"exitCode\\": 0, \\"message\\": \\"The help was constructed for command: output.\\", - \\"stdout\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n output | o\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Download all job output to a local directory. Each spool DD will be downloaded\\\\n to its own file in the directory.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-jobs download output [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n jobid\\\\t\\\\t (string)\\\\n\\\\n The z/OS JOBID of the job containing the spool files you want to view. No\\\\n pre-validation of the JOBID is performed.\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --directory | -d | --dir (string)\\\\n\\\\n The local directory you would like to download the output for the job to.\\\\n\\\\n --extension | -e (string)\\\\n\\\\n A file extension to save the job output with. Defaults to '.txt'.\\\\n\\\\n --omit-jobid-directory | --ojd (boolean)\\\\n\\\\n If specified, job output will be saved directly to the specified directory\\\\n rather than creating a subdirectory named after the ID of the job.\\\\n\\\\n --binary | -b (boolean)\\\\n\\\\n If specified, job output will be downloaded in binary format instead of\\\\n performing text conversion. Conflicts with record.\\\\n\\\\n --record | -r (boolean)\\\\n\\\\n If specified, job output will be downloaded in record format instead of\\\\n performing text conversion. Conflicts with binary.\\\\n\\\\n --encoding | --ec (string)\\\\n\\\\n Download the spool file content with encoding mode, which means that data\\\\n conversion is performed using the file encoding specified.\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Download all the output of the job with job ID JOB00234 to\\\\n an automatically generated directory.:\\\\n\\\\n $ zowe zos-jobs download output JOB00234\\\\n\\\\n\\", + \\"stdout\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n output | o\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Download all job output to a local directory. Each spool DD will be downloaded\\\\n to its own file in the directory.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-jobs download output [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n jobid\\\\t\\\\t (string)\\\\n\\\\n The z/OS JOBID of the job containing the spool files you want to view. No\\\\n pre-validation of the JOBID is performed.\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --directory | -d | --dir (string)\\\\n\\\\n The local directory you would like to download the output for the job to.\\\\n\\\\n --extension | -e (string)\\\\n\\\\n A file extension to save the job output with. Defaults to '.txt'.\\\\n\\\\n --omit-jobid-directory | --ojd (boolean)\\\\n\\\\n If specified, job output will be saved directly to the specified directory\\\\n rather than creating a subdirectory named after the ID of the job.\\\\n\\\\n --binary | -b (boolean)\\\\n\\\\n If specified, job output will be downloaded in binary format instead of\\\\n performing text conversion. Conflicts with record.\\\\n\\\\n --record | -r (boolean)\\\\n\\\\n If specified, job output will be downloaded in record format instead of\\\\n performing text conversion. Conflicts with binary.\\\\n\\\\n --encoding | --ec (string)\\\\n\\\\n Download the spool file content with encoding mode, which means that data\\\\n conversion is performed using the file encoding specified.\\\\n\\\\n --wait-for-active | --wfa (boolean)\\\\n\\\\n Wait for the job to enter ACTIVE status before completing the command.\\\\n\\\\n --wait-for-output | --wfo (boolean)\\\\n\\\\n Wait for the job to enter OUTPUT status before completing the command.\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Download all the output of the job with job ID JOB00234 to\\\\n an automatically generated directory.:\\\\n\\\\n $ zowe zos-jobs download output JOB00234\\\\n\\\\n\\", \\"stderr\\": \\"\\", - \\"data\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n output | o\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Download all job output to a local directory. Each spool DD will be downloaded\\\\n to its own file in the directory.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-jobs download output [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n jobid\\\\t\\\\t (string)\\\\n\\\\n The z/OS JOBID of the job containing the spool files you want to view. No\\\\n pre-validation of the JOBID is performed.\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --directory | -d | --dir (string)\\\\n\\\\n The local directory you would like to download the output for the job to.\\\\n\\\\n --extension | -e (string)\\\\n\\\\n A file extension to save the job output with. Defaults to '.txt'.\\\\n\\\\n --omit-jobid-directory | --ojd (boolean)\\\\n\\\\n If specified, job output will be saved directly to the specified directory\\\\n rather than creating a subdirectory named after the ID of the job.\\\\n\\\\n --binary | -b (boolean)\\\\n\\\\n If specified, job output will be downloaded in binary format instead of\\\\n performing text conversion. Conflicts with record.\\\\n\\\\n --record | -r (boolean)\\\\n\\\\n If specified, job output will be downloaded in record format instead of\\\\n performing text conversion. Conflicts with binary.\\\\n\\\\n --encoding | --ec (string)\\\\n\\\\n Download the spool file content with encoding mode, which means that data\\\\n conversion is performed using the file encoding specified.\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Download all the output of the job with job ID JOB00234 to\\\\n an automatically generated directory.:\\\\n\\\\n $ zowe zos-jobs download output JOB00234\\\\n\\\\n\\" + \\"data\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n output | o\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Download all job output to a local directory. Each spool DD will be downloaded\\\\n to its own file in the directory.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-jobs download output [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n jobid\\\\t\\\\t (string)\\\\n\\\\n The z/OS JOBID of the job containing the spool files you want to view. No\\\\n pre-validation of the JOBID is performed.\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --directory | -d | --dir (string)\\\\n\\\\n The local directory you would like to download the output for the job to.\\\\n\\\\n --extension | -e (string)\\\\n\\\\n A file extension to save the job output with. Defaults to '.txt'.\\\\n\\\\n --omit-jobid-directory | --ojd (boolean)\\\\n\\\\n If specified, job output will be saved directly to the specified directory\\\\n rather than creating a subdirectory named after the ID of the job.\\\\n\\\\n --binary | -b (boolean)\\\\n\\\\n If specified, job output will be downloaded in binary format instead of\\\\n performing text conversion. Conflicts with record.\\\\n\\\\n --record | -r (boolean)\\\\n\\\\n If specified, job output will be downloaded in record format instead of\\\\n performing text conversion. Conflicts with binary.\\\\n\\\\n --encoding | --ec (string)\\\\n\\\\n Download the spool file content with encoding mode, which means that data\\\\n conversion is performed using the file encoding specified.\\\\n\\\\n --wait-for-active | --wfa (boolean)\\\\n\\\\n Wait for the job to enter ACTIVE status before completing the command.\\\\n\\\\n --wait-for-output | --wfo (boolean)\\\\n\\\\n Wait for the job to enter OUTPUT status before completing the command.\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Download all the output of the job with job ID JOB00234 to\\\\n an automatically generated directory.:\\\\n\\\\n $ zowe zos-jobs download output JOB00234\\\\n\\\\n\\" }" `; diff --git a/packages/cli/__tests__/zosjobs/__resources__/GetJobsData.ts b/packages/cli/__tests__/zosjobs/__resources__/GetJobsData.ts index 56a1fe3f59..1bb395ed13 100644 --- a/packages/cli/__tests__/zosjobs/__resources__/GetJobsData.ts +++ b/packages/cli/__tests__/zosjobs/__resources__/GetJobsData.ts @@ -27,6 +27,11 @@ export class GetJobsData { public static readonly SAMPLE_COMPLETE_JOB: IJob = { "jobid": "TSUxxx", "jobname": "IBMUSER$", + "exec-started": '2024-01-02T15:57:58.350Z', + "exec-ended": '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', + "exec-submitted": '2024-01-02T15:58:00.600Z', "subsystem": "JES2", "owner": "IBMUSER", "status": "OUTPUT", @@ -49,6 +54,11 @@ export class GetJobsData { public static readonly SAMPLE_COMPLETE_JOB_AGAIN: IJob = { "jobid": "JOBxxx", "jobname": "CAUSER$", + "exec-started": '2024-01-02T15:57:58.350Z', + "exec-ended": '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', + "exec-submitted": '2024-01-02T15:58:00.600Z', "subsystem": "JES2", "owner": "CAUSER", "status": "OUTPUT", @@ -79,6 +89,11 @@ export class GetJobsData { public static readonly SAMPLE_ACTIVE_JOB: IJob = { "retcode": null, "jobname": "KELDA16$", + "exec-started": '2024-01-02T15:57:58.350Z', + "exec-ended": '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', + "exec-submitted": '2024-01-02T15:58:00.600Z', "status": "INPUT", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "class": "A", diff --git a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job.sh b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job.sh index d3632092d7..9e11ffd46b 100755 --- a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job.sh +++ b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job.sh @@ -16,6 +16,8 @@ then exit $RC fi +sleep 1 + # Cancel the job zowe jobs cancel job $JOBID RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_fully_qualified.sh b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_fully_qualified.sh index a192432415..66e33b592f 100755 --- a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_fully_qualified.sh +++ b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_fully_qualified.sh @@ -22,6 +22,8 @@ then exit $RC fi +sleep 1 + # Cancel the job zowe jobs cancel job $JOBID --host $HOST --port $PORT --user $USER --password $PASS --ru=false RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v1.sh b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v1.sh index 3c21f25e03..1f08c8455b 100755 --- a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v1.sh +++ b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v1.sh @@ -16,6 +16,8 @@ then exit $RC fi +sleep 1 + # Cancel the job 1 zowe jobs cancel job $JOBID --modify-version 1.0 RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v1_fully_qualified.sh b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v1_fully_qualified.sh index a9d11afbe5..0a062d6d82 100755 --- a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v1_fully_qualified.sh +++ b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v1_fully_qualified.sh @@ -22,6 +22,8 @@ then exit $RC fi +sleep 1 + # Cancel the job zowe jobs cancel job $JOBID --host $HOST --port $PORT --user $USER --password $PASS --ru=false --modify-version 1.0 RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v2.sh b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v2.sh index d5abaaaee2..0c1170f4fe 100755 --- a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v2.sh +++ b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v2.sh @@ -16,6 +16,8 @@ then exit $RC fi +sleep 1 + # Cancel the job 1 zowe jobs cancel job $JOBID --modify-version 2.0 RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v2_bad.sh b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v2_bad.sh index c3165a9047..c87f49179f 100755 --- a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v2_bad.sh +++ b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v2_bad.sh @@ -16,6 +16,8 @@ then exit $RC fi +sleep 1 + # Cancel the job 1 zowe jobs cancel job $JOBID --modify-version 2.0 RC=$? @@ -26,6 +28,8 @@ then exit $RC fi +sleep 1 + # Cancel the job 2 zowe jobs cancel job $JOBID --modify-version 2.0 RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v2_fully_qualified.sh b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v2_fully_qualified.sh index 3a0d1b7177..5f1f40917d 100755 --- a/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v2_fully_qualified.sh +++ b/packages/cli/__tests__/zosjobs/__system__/cancel/__scripts__/job/cancel_job_v2_fully_qualified.sh @@ -22,6 +22,8 @@ then exit $RC fi +sleep 1 + # Cancel the job zowe jobs cancel job $JOBID --host $HOST --port $PORT --user $USER --password $PASS --ru=false --modify-version 2.0 RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/cancel/cli.zos-jobs.cancel.job.system.test.ts b/packages/cli/__tests__/zosjobs/__system__/cancel/cli.zos-jobs.cancel.job.system.test.ts index 06bc908393..9d9f82b0ab 100644 --- a/packages/cli/__tests__/zosjobs/__system__/cancel/cli.zos-jobs.cancel.job.system.test.ts +++ b/packages/cli/__tests__/zosjobs/__system__/cancel/cli.zos-jobs.cancel.job.system.test.ts @@ -21,6 +21,7 @@ let TEST_ENVIRONMENT: ITestEnvironment; const LOCAL_JCL_FILE: string = __dirname + "/" + "testFileOfLocalJCL.txt"; const jobDataRegexV1 = /Successfully submitted request to cancel job (\w+) \((JOB\d+)\)/; const jobDataRegex = /Successfully canceled job (\w+) \((JOB\d+)\)/; +const modifyVersionDefaultUsesCIM = false; describe("zos-jobs cancel job command", () => { // Create the unique test environment @@ -73,13 +74,17 @@ describe("zos-jobs cancel job command", () => { describe("successful scenario", () => { it("should cancel a job v1", () => { - const response = runCliScript(__dirname + "/__scripts__/job/cancel_job_v1.sh", TEST_ENVIRONMENT, [LOCAL_JCL_FILE]); - expect(response.stderr.toString()).toBe(""); - expect(response.status).toBe(0); - expect(response.stdout.toString()).toContain("Successfully submitted request to cancel job"); + if (TEST_ENVIRONMENT.systemTestProperties.zosjobs.skipCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const response = runCliScript(__dirname + "/__scripts__/job/cancel_job_v1.sh", TEST_ENVIRONMENT, [LOCAL_JCL_FILE]); + expect(response.stderr.toString()).toBe(""); + expect(response.status).toBe(0); + expect(response.stdout.toString()).toContain("Successfully submitted request to cancel job"); - const jobid = response.stdout.toString().match(jobDataRegexV1).pop(); - TEST_ENVIRONMENT.resources.jobs.push(jobid); + const jobid = response.stdout.toString().match(jobDataRegexV1).pop(); + TEST_ENVIRONMENT.resources.jobs.push(jobid); + } }); it("should cancel a job v2", () => { @@ -94,15 +99,19 @@ describe("zos-jobs cancel job command", () => { }); it("should cancel a job default", () => { - const response = runCliScript(__dirname + "/__scripts__/job/cancel_job.sh", TEST_ENVIRONMENT, [LOCAL_JCL_FILE]); - expect(response.stderr.toString()).toBe(""); - expect(response.status).toBe(0); - expect(response.stdout.toString()).toContain("Successfully canceled job"); - expect(response.stdout.toString()).not.toContain("Failed to cancel job"); - expect(response.stdout.toString()).not.toContain("Failed to cancel job"); + if (TEST_ENVIRONMENT.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const response = runCliScript(__dirname + "/__scripts__/job/cancel_job.sh", TEST_ENVIRONMENT, [LOCAL_JCL_FILE]); + expect(response.stderr.toString()).toBe(""); + expect(response.status).toBe(0); + expect(response.stdout.toString()).toContain("Successfully canceled job"); + expect(response.stdout.toString()).not.toContain("Failed to cancel job"); + expect(response.stdout.toString()).not.toContain("Failed to cancel job"); - const jobid = response.stdout.toString().match(jobDataRegex).pop(); - TEST_ENVIRONMENT.resources.jobs.push(jobid); + const jobid = response.stdout.toString().match(jobDataRegex).pop(); + TEST_ENVIRONMENT.resources.jobs.push(jobid); + } }); describe("without profiles", () => { @@ -124,21 +133,25 @@ describe("zos-jobs cancel job command", () => { }); it("cancel a job without a profile 1.0", async () => { - const response = runCliScript(__dirname + "/__scripts__/job/cancel_job_v1_fully_qualified.sh", - TEST_ENVIRONMENT_NO_PROF, - [ - LOCAL_JCL_FILE, - DEFAULT_SYSTEM_PROPS.zosmf.host, - DEFAULT_SYSTEM_PROPS.zosmf.port, - DEFAULT_SYSTEM_PROPS.zosmf.user, - DEFAULT_SYSTEM_PROPS.zosmf.password, - ]); - expect(response.stderr.toString()).toBe(""); - expect(response.status).toBe(0); - expect(response.stdout.toString()).toContain("Successfully submitted request to cancel job"); - - const jobid = response.stdout.toString().match(jobDataRegexV1).pop(); - TEST_ENVIRONMENT_NO_PROF.resources.jobs.push(jobid); + if (TEST_ENVIRONMENT.systemTestProperties.zosjobs.skipCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const response = runCliScript(__dirname + "/__scripts__/job/cancel_job_v1_fully_qualified.sh", + TEST_ENVIRONMENT_NO_PROF, + [ + LOCAL_JCL_FILE, + DEFAULT_SYSTEM_PROPS.zosmf.host, + DEFAULT_SYSTEM_PROPS.zosmf.port, + DEFAULT_SYSTEM_PROPS.zosmf.user, + DEFAULT_SYSTEM_PROPS.zosmf.password, + ]); + expect(response.stderr.toString()).toBe(""); + expect(response.status).toBe(0); + expect(response.stdout.toString()).toContain("Successfully submitted request to cancel job"); + + const jobid = response.stdout.toString().match(jobDataRegexV1).pop(); + TEST_ENVIRONMENT_NO_PROF.resources.jobs.push(jobid); + } }); it("cancel a job without a profile 2.0", async () => { @@ -160,21 +173,25 @@ describe("zos-jobs cancel job command", () => { }); it("cancel a job without a profile default", async () => { - const response = runCliScript(__dirname + "/__scripts__/job/cancel_job_fully_qualified.sh", - TEST_ENVIRONMENT_NO_PROF, - [ - LOCAL_JCL_FILE, - DEFAULT_SYSTEM_PROPS.zosmf.host, - DEFAULT_SYSTEM_PROPS.zosmf.port, - DEFAULT_SYSTEM_PROPS.zosmf.user, - DEFAULT_SYSTEM_PROPS.zosmf.password, - ]); - expect(response.stderr.toString()).toBe(""); - expect(response.status).toBe(0); - expect(response.stdout.toString()).toContain("Successfully canceled job"); - - const jobid = response.stdout.toString().match(jobDataRegex).pop(); - TEST_ENVIRONMENT_NO_PROF.resources.jobs.push(jobid); + if (TEST_ENVIRONMENT.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const response = runCliScript(__dirname + "/__scripts__/job/cancel_job_fully_qualified.sh", + TEST_ENVIRONMENT_NO_PROF, + [ + LOCAL_JCL_FILE, + DEFAULT_SYSTEM_PROPS.zosmf.host, + DEFAULT_SYSTEM_PROPS.zosmf.port, + DEFAULT_SYSTEM_PROPS.zosmf.user, + DEFAULT_SYSTEM_PROPS.zosmf.password, + ]); + expect(response.stderr.toString()).toBe(""); + expect(response.status).toBe(0); + expect(response.stdout.toString()).toContain("Successfully canceled job"); + + const jobid = response.stdout.toString().match(jobDataRegex).pop(); + TEST_ENVIRONMENT_NO_PROF.resources.jobs.push(jobid); + } }); }); }); diff --git a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job.sh b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job.sh index e560bfdadf..a8d234d19d 100755 --- a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job.sh +++ b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job.sh @@ -17,7 +17,7 @@ then fi # Loop until the job goes to the output queue -until [ $ATTEMPTS -gt 0 ] +until [ $ATTEMPTS -lt 1 ] do STATUS=`zowe jobs view job-status-by-jobid $JOBID --rff status --rft string` RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_fully_qualified.sh b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_fully_qualified.sh index 2642614827..40477cafd7 100755 --- a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_fully_qualified.sh +++ b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_fully_qualified.sh @@ -23,7 +23,7 @@ then fi # Loop until the job goes to the output queue -until [ $ATTEMPTS -gt 0 ] +until [ $ATTEMPTS -lt 1 ] do STATUS=`zowe jobs view job-status-by-jobid $JOBID --host $HOST --port $PORT --user $USER --password $PASS --ru=false --rff status --rft string` RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v1.sh b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v1.sh index 5142e33080..9fad992df1 100755 --- a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v1.sh +++ b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v1.sh @@ -17,7 +17,7 @@ then fi # Loop until the job goes to the output queue -until [ $ATTEMPTS -gt 0 ] +until [ $ATTEMPTS -lt 1 ] do STATUS=`zowe jobs view job-status-by-jobid $JOBID --rff status --rft string` RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v1_fully_qualified.sh b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v1_fully_qualified.sh index ddb46643a3..5041a60c53 100755 --- a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v1_fully_qualified.sh +++ b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v1_fully_qualified.sh @@ -23,7 +23,7 @@ then fi # Loop until the job goes to the output queue -until [ $ATTEMPTS -gt 0 ] +until [ $ATTEMPTS -lt 1 ] do STATUS=`zowe jobs view job-status-by-jobid $JOBID --host $HOST --port $PORT --user $USER --password $PASS --ru=false --rff status --rft string` RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v2.sh b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v2.sh index b8a80a8af4..0887eab49a 100755 --- a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v2.sh +++ b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v2.sh @@ -17,7 +17,7 @@ then fi # Loop until the job goes to the output queue -until [ $ATTEMPTS -gt 0 ] +until [ $ATTEMPTS -lt 1 ] do STATUS=`zowe jobs view job-status-by-jobid $JOBID --rff status --rft string` RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v2_fully_qualified.sh b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v2_fully_qualified.sh index e8f114b68c..f10be13a6f 100755 --- a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v2_fully_qualified.sh +++ b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/job/delete_job_v2_fully_qualified.sh @@ -23,7 +23,7 @@ then fi # Loop until the job goes to the output queue -until [ $ATTEMPTS -gt 0 ] +until [ $ATTEMPTS -lt 1 ] do STATUS=`zowe jobs view job-status-by-jobid $JOBID --host $HOST --port $PORT --user $USER --password $PASS --ru=false --rff status --rft string` RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/old-jobs/delete_old_jobs.sh b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/old-jobs/delete_old_jobs.sh index 6946cc549d..e165a2fb02 100755 --- a/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/old-jobs/delete_old_jobs.sh +++ b/packages/cli/__tests__/zosjobs/__system__/delete/__scripts__/old-jobs/delete_old_jobs.sh @@ -18,7 +18,7 @@ then fi # Loop until the job goes to the output queue -until [ $ATTEMPTS -gt 0 ] +until [ $ATTEMPTS -lt 1 ] do STATUS=`zowe jobs view job-status-by-jobid $JOBID --rff status --rft string` RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/delete/cli.zos-jobs.delete.job.system.test.ts b/packages/cli/__tests__/zosjobs/__system__/delete/cli.zos-jobs.delete.job.system.test.ts index e34559b5fe..12d55be2bc 100644 --- a/packages/cli/__tests__/zosjobs/__system__/delete/cli.zos-jobs.delete.job.system.test.ts +++ b/packages/cli/__tests__/zosjobs/__system__/delete/cli.zos-jobs.delete.job.system.test.ts @@ -17,6 +17,7 @@ import { ITestPropertiesSchema } from "../../../../../../__tests__/__src__/prope // Test Environment populated in the beforeAll(); let TEST_ENVIRONMENT: ITestEnvironment; let IEFBR14_JCL: string; +const modifyVersionDefaultUsesCIM = false; describe("zos-jobs delete job command", () => { // Create the unique test environment @@ -41,11 +42,15 @@ describe("zos-jobs delete job command", () => { describe("successful scenario", () => { it("should delete a job 1.0", () => { - const response = runCliScript(__dirname + "/__scripts__/job/delete_job_v1.sh", - TEST_ENVIRONMENT, [IEFBR14_JCL]); - expect(response.stderr.toString()).toBe(""); - expect(response.status).toBe(0); - expect(response.stdout.toString()).toContain("Successfully submitted request to delete job"); + if (TEST_ENVIRONMENT.systemTestProperties.zosjobs.skipCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const response = runCliScript(__dirname + "/__scripts__/job/delete_job_v1.sh", + TEST_ENVIRONMENT, [IEFBR14_JCL]); + expect(response.stderr.toString()).toBe(""); + expect(response.status).toBe(0); + expect(response.stdout.toString()).toContain("Successfully submitted request to delete job"); + } }); it("should delete a job 2.0", () => { @@ -57,11 +62,15 @@ describe("zos-jobs delete job command", () => { }); it("should delete a job default", () => { - const response = runCliScript(__dirname + "/__scripts__/job/delete_job.sh", - TEST_ENVIRONMENT, [IEFBR14_JCL]); - expect(response.stderr.toString()).toBe(""); - expect(response.status).toBe(0); - expect(response.stdout.toString()).toContain("Successfully deleted job"); + if (TEST_ENVIRONMENT.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const response = runCliScript(__dirname + "/__scripts__/job/delete_job.sh", + TEST_ENVIRONMENT, [IEFBR14_JCL]); + expect(response.stderr.toString()).toBe(""); + expect(response.status).toBe(0); + expect(response.stdout.toString()).toContain("Successfully deleted job"); + } }); describe("without profiles", () => { @@ -83,26 +92,30 @@ describe("zos-jobs delete job command", () => { }); it("delete a job without a profile 1.0", async () => { - const ZOWE_OPT_BASE_PATH = "ZOWE_OPT_BASE_PATH"; - - // if API Mediation layer is being used (basePath has a value) then - // set an ENVIRONMENT variable to be used by zowe. - if (DEFAULT_SYSTEM_PROPS.zosmf.basePath != null) { - TEST_ENVIRONMENT_NO_PROF.env[ZOWE_OPT_BASE_PATH] = DEFAULT_SYSTEM_PROPS.zosmf.basePath; + if (TEST_ENVIRONMENT.systemTestProperties.zosjobs.skipCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const ZOWE_OPT_BASE_PATH = "ZOWE_OPT_BASE_PATH"; + + // if API Mediation layer is being used (basePath has a value) then + // set an ENVIRONMENT variable to be used by zowe. + if (DEFAULT_SYSTEM_PROPS.zosmf.basePath != null) { + TEST_ENVIRONMENT_NO_PROF.env[ZOWE_OPT_BASE_PATH] = DEFAULT_SYSTEM_PROPS.zosmf.basePath; + } + + const response = runCliScript(__dirname + "/__scripts__/job/delete_job_v1_fully_qualified.sh", + TEST_ENVIRONMENT_NO_PROF, + [ + IEFBR14_JCL, + DEFAULT_SYSTEM_PROPS.zosmf.host, + DEFAULT_SYSTEM_PROPS.zosmf.port, + DEFAULT_SYSTEM_PROPS.zosmf.user, + DEFAULT_SYSTEM_PROPS.zosmf.password, + ]); + expect(response.stderr.toString()).toBe(""); + expect(response.status).toBe(0); + expect(response.stdout.toString()).toContain("Successfully submitted request to delete job"); } - - const response = runCliScript(__dirname + "/__scripts__/job/delete_job_v1_fully_qualified.sh", - TEST_ENVIRONMENT_NO_PROF, - [ - IEFBR14_JCL, - DEFAULT_SYSTEM_PROPS.zosmf.host, - DEFAULT_SYSTEM_PROPS.zosmf.port, - DEFAULT_SYSTEM_PROPS.zosmf.user, - DEFAULT_SYSTEM_PROPS.zosmf.password, - ]); - expect(response.stderr.toString()).toBe(""); - expect(response.status).toBe(0); - expect(response.stdout.toString()).toContain("Successfully submitted request to delete job"); }); it("delete a job without a profile 2.0", async () => { @@ -129,26 +142,30 @@ describe("zos-jobs delete job command", () => { }); it("delete a job without a profile default", async () => { - const ZOWE_OPT_BASE_PATH = "ZOWE_OPT_BASE_PATH"; - - // if API Mediation layer is being used (basePath has a value) then - // set an ENVIRONMENT variable to be used by zowe. - if (DEFAULT_SYSTEM_PROPS.zosmf.basePath != null) { - TEST_ENVIRONMENT_NO_PROF.env[ZOWE_OPT_BASE_PATH] = DEFAULT_SYSTEM_PROPS.zosmf.basePath; + if (TEST_ENVIRONMENT.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const ZOWE_OPT_BASE_PATH = "ZOWE_OPT_BASE_PATH"; + + // if API Mediation layer is being used (basePath has a value) then + // set an ENVIRONMENT variable to be used by zowe. + if (DEFAULT_SYSTEM_PROPS.zosmf.basePath != null) { + TEST_ENVIRONMENT_NO_PROF.env[ZOWE_OPT_BASE_PATH] = DEFAULT_SYSTEM_PROPS.zosmf.basePath; + } + + const response = runCliScript(__dirname + "/__scripts__/job/delete_job_fully_qualified.sh", + TEST_ENVIRONMENT_NO_PROF, + [ + IEFBR14_JCL, + DEFAULT_SYSTEM_PROPS.zosmf.host, + DEFAULT_SYSTEM_PROPS.zosmf.port, + DEFAULT_SYSTEM_PROPS.zosmf.user, + DEFAULT_SYSTEM_PROPS.zosmf.password, + ]); + expect(response.stderr.toString()).toBe(""); + expect(response.status).toBe(0); + expect(response.stdout.toString()).toContain("Successfully deleted job"); } - - const response = runCliScript(__dirname + "/__scripts__/job/delete_job_fully_qualified.sh", - TEST_ENVIRONMENT_NO_PROF, - [ - IEFBR14_JCL, - DEFAULT_SYSTEM_PROPS.zosmf.host, - DEFAULT_SYSTEM_PROPS.zosmf.port, - DEFAULT_SYSTEM_PROPS.zosmf.user, - DEFAULT_SYSTEM_PROPS.zosmf.password, - ]); - expect(response.stderr.toString()).toBe(""); - expect(response.status).toBe(0); - expect(response.stdout.toString()).toContain("Successfully deleted job"); }); }); }); diff --git a/packages/cli/__tests__/zosjobs/__system__/delete/cli.zos-jobs.delete.old-jobs.system.test.ts b/packages/cli/__tests__/zosjobs/__system__/delete/cli.zos-jobs.delete.old-jobs.system.test.ts index be09c0dfaf..743e3a41a9 100644 --- a/packages/cli/__tests__/zosjobs/__system__/delete/cli.zos-jobs.delete.old-jobs.system.test.ts +++ b/packages/cli/__tests__/zosjobs/__system__/delete/cli.zos-jobs.delete.old-jobs.system.test.ts @@ -17,6 +17,7 @@ import { runCliScript } from "@zowe/cli-test-utils"; // Test Environment populated in the beforeAll(); let TEST_ENVIRONMENT: ITestEnvironment; let IEFBR14_JCL: string; +const modifyVersionDefaultUsesCIM = false; describe("zos-jobs delete old-jobs command", () => { // Create the unique test environment @@ -39,35 +40,51 @@ describe("zos-jobs delete old-jobs command", () => { describe("successful scenario", () => { it("should delete all old jobs sequentially default", () => { - const response = runCliScript(__dirname + "/__scripts__/old-jobs/delete_old_jobs.sh", - TEST_ENVIRONMENT, [IEFBR14_JCL]); - expect(response.status).toBe(0); - expect(response.stderr.toString()).toBe(""); - expect(response.stdout.toString()).toContain("Successfully deleted"); + if (TEST_ENVIRONMENT.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const response = runCliScript(__dirname + "/__scripts__/old-jobs/delete_old_jobs.sh", + TEST_ENVIRONMENT, [IEFBR14_JCL]); + expect(response.status).toBe(0); + expect(response.stderr.toString()).toBe(""); + expect(response.stdout.toString()).toContain("Successfully deleted"); + } }); it("should delete all old jobs in parallel default", () => { - const response = runCliScript(__dirname + "/__scripts__/old-jobs/delete_old_jobs.sh", - TEST_ENVIRONMENT, [IEFBR14_JCL, "--mcr", "0"]); - expect(response.status).toBe(0); - expect(response.stderr.toString()).toBe(""); - expect(response.stdout.toString()).toContain("Successfully deleted"); + if (TEST_ENVIRONMENT.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const response = runCliScript(__dirname + "/__scripts__/old-jobs/delete_old_jobs.sh", + TEST_ENVIRONMENT, [IEFBR14_JCL, "--mcr", "0"]); + expect(response.status).toBe(0); + expect(response.stderr.toString()).toBe(""); + expect(response.stdout.toString()).toContain("Successfully deleted"); + } }); it("should delete all old jobs with modifyVersion 1.0 sequentially", () => { - const response = runCliScript(__dirname + "/__scripts__/old-jobs/delete_old_jobs.sh", - TEST_ENVIRONMENT, [IEFBR14_JCL, "--modify-version", "1.0"]); - expect(response.status).toBe(0); - expect(response.stderr.toString()).toBe(""); - expect(response.stdout.toString()).toContain("Successfully deleted"); + if (TEST_ENVIRONMENT.systemTestProperties.zosjobs.skipCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const response = runCliScript(__dirname + "/__scripts__/old-jobs/delete_old_jobs.sh", + TEST_ENVIRONMENT, [IEFBR14_JCL, "--modify-version", "1.0"]); + expect(response.status).toBe(0); + expect(response.stderr.toString()).toBe(""); + expect(response.stdout.toString()).toContain("Successfully deleted"); + } }); it("should delete all old jobs with modifyVersion 1.0 parallel", () => { - const response = runCliScript(__dirname + "/__scripts__/old-jobs/delete_old_jobs.sh", - TEST_ENVIRONMENT, [IEFBR14_JCL, "--mcr", "0", "--modify-version", "1.0"]); - expect(response.status).toBe(0); - expect(response.stderr.toString()).toBe(""); - expect(response.stdout.toString()).toContain("Successfully deleted"); + if (TEST_ENVIRONMENT.systemTestProperties.zosjobs.skipCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const response = runCliScript(__dirname + "/__scripts__/old-jobs/delete_old_jobs.sh", + TEST_ENVIRONMENT, [IEFBR14_JCL, "--mcr", "0", "--modify-version", "1.0"]); + expect(response.status).toBe(0); + expect(response.stderr.toString()).toBe(""); + expect(response.stdout.toString()).toContain("Successfully deleted"); + } }); it("should delete all old jobs with modifyVersion 2.0 sequentially", () => { diff --git a/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download.sh b/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download.sh index 8563a01a94..0ccea48361 100755 --- a/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download.sh +++ b/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download.sh @@ -20,7 +20,7 @@ fi echo "Submitted job ID: $JOBID" # Loop until the job goes to the output queue -until [ $ATTEMPTS -gt 0 ] +until [ $ATTEMPTS -lt 1 ] do STATUS=`zowe jobs view job-status-by-jobid $JOBID --rff status --rft string` RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download_fully_qualified.sh b/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download_fully_qualified.sh index cb7def1726..534394bf09 100755 --- a/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download_fully_qualified.sh +++ b/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download_fully_qualified.sh @@ -26,7 +26,7 @@ fi echo "Submitted job ID: $JOBID" # Loop until the job goes to the output queue -until [ $ATTEMPTS -gt 0 ] +until [ $ATTEMPTS -lt 1 ] do STATUS=`zowe jobs view job-status-by-jobid $JOBID --host $HOST --port $PORT --user $USER --password $PASS --ru=false --rff status --rft string` RC=$? diff --git a/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download_job_wait_for_active.sh b/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download_job_wait_for_active.sh new file mode 100755 index 0000000000..c8893a5766 --- /dev/null +++ b/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download_job_wait_for_active.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +JOBID=$(zowe jobs submit ds "$1" --rff jobid --rft string) + +zowe zos-jobs download output $JOBID --wfa +exit $? \ No newline at end of file diff --git a/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download_job_wait_for_output.sh b/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download_job_wait_for_output.sh new file mode 100755 index 0000000000..fa86b13891 --- /dev/null +++ b/packages/cli/__tests__/zosjobs/__system__/download/__scripts__/download-output/download_job_wait_for_output.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +JOBID=$(zowe jobs submit ds "$1" --rff jobid --rft string) + +zowe zos-jobs download output $JOBID --wfo +exit $? \ No newline at end of file diff --git a/packages/cli/__tests__/zosjobs/__system__/download/cli.zos-jobs.download.output.system.test.ts b/packages/cli/__tests__/zosjobs/__system__/download/cli.zos-jobs.download.output.system.test.ts index 32ad3be62d..c646971f07 100644 --- a/packages/cli/__tests__/zosjobs/__system__/download/cli.zos-jobs.download.output.system.test.ts +++ b/packages/cli/__tests__/zosjobs/__system__/download/cli.zos-jobs.download.output.system.test.ts @@ -14,6 +14,8 @@ import { ITestEnvironment } from "../../../../../../__tests__/__src__/environmen import { runCliScript } from "@zowe/cli-test-utils"; import { ITestPropertiesSchema } from "../../../../../../__tests__/__src__/properties/ITestPropertiesSchema"; import * as fs from "fs"; +import { Session } from "@zowe/imperative"; +import { GetJobs } from "@zowe/zos-jobs-for-zowe-sdk"; // Test Environment populated in the beforeAll(); let TEST_ENVIRONMENT: ITestEnvironment; @@ -43,12 +45,25 @@ describe("zos-jobs download output command", () => { }); }); + describe("Live system tests", () => { + it("should download a job and wait for it to reach output status", async () => { + const response = runCliScript(__dirname + "/__scripts__/download-output/download_job_wait_for_output.sh", + TEST_ENVIRONMENT, [IEFBR14_JCL]); + expect(response.stderr.toString()).toBe(""); + expect(response.status).toBe(0); + }); + it("should download a job and wait for it to reach active status", async () => { + const response = runCliScript(__dirname + "/__scripts__/download-output/download_job_wait_for_active.sh", + TEST_ENVIRONMENT, [IEFBR14_JCL]); + expect(response.stderr.toString()).toBe(""); + expect(response.status).toBe(0); + }); + }); describe("output", () => { it("should download all spool files of a job", () => { const outdir: string = TEST_ENVIRONMENT.workingDir + "/output/JES2"; const response = runCliScript(__dirname + "/__scripts__/download-output/download.sh", TEST_ENVIRONMENT, [IEFBR14_JCL]); - // Extract the JOBID from the response output const jobidRegex = /Submitted job ID: (JOB\d+)/; const match = response.stdout.toString().match(jobidRegex); diff --git a/packages/cli/__tests__/zosjobs/__system__/list/__scripts__/list-jobs/submit_and_list_jobs.sh b/packages/cli/__tests__/zosjobs/__system__/list/__scripts__/list-jobs/submit_and_list_jobs.sh index fc6f6e9979..bff24c1186 100755 --- a/packages/cli/__tests__/zosjobs/__system__/list/__scripts__/list-jobs/submit_and_list_jobs.sh +++ b/packages/cli/__tests__/zosjobs/__system__/list/__scripts__/list-jobs/submit_and_list_jobs.sh @@ -3,10 +3,13 @@ set -e # Submit two jobs and capture their IDs JOBID1=`zowe zos-jobs submit data-set $1 --rff jobid --rft string` +sleep 1 JOBID2=`zowe zos-jobs submit data-set $1 --rff jobid --rft string` echo "Listing jobs to find job IDs $JOBID1 and $JOBID2" +sleep 1 + LIST_JOB_OUTPUT=`zowe zos-jobs list jobs` if echo $LIST_JOB_OUTPUT | grep -q $JOBID1 then diff --git a/packages/cli/__tests__/zosjobs/__system__/list/__scripts__/list-jobs/submit_and_list_jobs_fully_qualified.sh b/packages/cli/__tests__/zosjobs/__system__/list/__scripts__/list-jobs/submit_and_list_jobs_fully_qualified.sh index 922016bd87..1c05c40b9c 100755 --- a/packages/cli/__tests__/zosjobs/__system__/list/__scripts__/list-jobs/submit_and_list_jobs_fully_qualified.sh +++ b/packages/cli/__tests__/zosjobs/__system__/list/__scripts__/list-jobs/submit_and_list_jobs_fully_qualified.sh @@ -9,8 +9,11 @@ PASS=$5 # Submit two jobs and capture their IDs JOBID1=`zowe zos-jobs submit data-set $JCL --host $HOST --port $PORT --user $USER --password $PASS --ru=false --rff jobid --rft string` +sleep 1 JOBID2=`zowe zos-jobs submit data-set $JCL --host $HOST --port $PORT --user $USER --password $PASS --ru=false --rff jobid --rft string` +sleep 1 + echo "Listing jobs to find job IDs $JOBID1 and $JOBID2" LIST_JOB_OUTPUT=`zowe zos-jobs list jobs --host $HOST --port $PORT --user $USER --password $PASS --ru=false` diff --git a/packages/cli/__tests__/zosjobs/__system__/view/__scripts__/all-spool-content/get_all_spool_content.sh b/packages/cli/__tests__/zosjobs/__system__/view/__scripts__/all-spool-content/get_all_spool_content.sh index f60e02909b..0cfed87ae2 100755 --- a/packages/cli/__tests__/zosjobs/__system__/view/__scripts__/all-spool-content/get_all_spool_content.sh +++ b/packages/cli/__tests__/zosjobs/__system__/view/__scripts__/all-spool-content/get_all_spool_content.sh @@ -17,7 +17,7 @@ then fi # Loop until the job goes to the output queue -until [ $ATTEMPTS -gt 0 ] +until [ $ATTEMPTS -lt 1 ] do STATUS=`zowe zos-jobs view job-status-by-jobid $JOBID --rff status --rft string` RC=$? diff --git a/packages/cli/__tests__/zosjobs/__unit__/cancel/job/__snapshots__/Job.handler.unit.test.ts.snap b/packages/cli/__tests__/zosjobs/__unit__/cancel/job/__snapshots__/Job.handler.unit.test.ts.snap index a2ca50e52b..14675179cb 100644 --- a/packages/cli/__tests__/zosjobs/__unit__/cancel/job/__snapshots__/Job.handler.unit.test.ts.snap +++ b/packages/cli/__tests__/zosjobs/__unit__/cancel/job/__snapshots__/Job.handler.unit.test.ts.snap @@ -7,6 +7,11 @@ exports[`cancel job handler tests should be able to cancel a job by job id 2`] = exports[`cancel job handler tests should be able to cancel a job by job id 3`] = ` Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -29,6 +34,11 @@ exports[`cancel job handler tests should be able to cancel a job by job id versi exports[`cancel job handler tests should be able to cancel a job by job id version 2.0 3`] = ` Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", diff --git a/packages/cli/__tests__/zosjobs/__unit__/delete/job/__snapshots__/Job.handler.unit.test.ts.snap b/packages/cli/__tests__/zosjobs/__unit__/delete/job/__snapshots__/Job.handler.unit.test.ts.snap index 520ed858b3..240d19e4d6 100644 --- a/packages/cli/__tests__/zosjobs/__unit__/delete/job/__snapshots__/Job.handler.unit.test.ts.snap +++ b/packages/cli/__tests__/zosjobs/__unit__/delete/job/__snapshots__/Job.handler.unit.test.ts.snap @@ -7,6 +7,11 @@ exports[`delete job handler tests should be able to delete a job by job id 2`] = exports[`delete job handler tests should be able to delete a job by job id 3`] = ` Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -29,6 +34,11 @@ exports[`delete job handler tests should be able to delete a job by job id versi exports[`delete job handler tests should be able to delete a job by job id version 2.0 3`] = ` Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", diff --git a/packages/cli/__tests__/zosjobs/__unit__/delete/old-jobs/__snapshots__/OldJobs.handler.unit.test.ts.snap b/packages/cli/__tests__/zosjobs/__unit__/delete/old-jobs/__snapshots__/OldJobs.handler.unit.test.ts.snap index 5a8b586a63..3a0dd3c904 100644 --- a/packages/cli/__tests__/zosjobs/__unit__/delete/old-jobs/__snapshots__/OldJobs.handler.unit.test.ts.snap +++ b/packages/cli/__tests__/zosjobs/__unit__/delete/old-jobs/__snapshots__/OldJobs.handler.unit.test.ts.snap @@ -13,6 +13,11 @@ Object { "output": Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -28,6 +33,11 @@ Object { }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", @@ -51,6 +61,11 @@ exports[`delete old-jobs handler tests should delete all jobs using defaults in Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -66,6 +81,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", @@ -95,6 +115,11 @@ Object { "output": Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -110,6 +135,11 @@ Object { }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", @@ -133,6 +163,11 @@ exports[`delete old-jobs handler tests should delete all jobs using defaults seq Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -148,6 +183,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", @@ -177,6 +217,11 @@ Object { "output": Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -192,6 +237,11 @@ Object { }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", @@ -215,6 +265,11 @@ exports[`delete old-jobs handler tests should delete jobs with a specific prefix Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -230,6 +285,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", @@ -259,6 +319,11 @@ Object { "output": Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -274,6 +339,11 @@ Object { }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", @@ -297,6 +367,11 @@ exports[`delete old-jobs handler tests should delete jobs with modifyVersion 2.0 Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -312,6 +387,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", diff --git a/packages/cli/__tests__/zosjobs/__unit__/download/download-output/Output.handler.unit.test.ts b/packages/cli/__tests__/zosjobs/__unit__/download/download-output/Output.handler.unit.test.ts index b2bc1e02a9..104187d1a6 100644 --- a/packages/cli/__tests__/zosjobs/__unit__/download/download-output/Output.handler.unit.test.ts +++ b/packages/cli/__tests__/zosjobs/__unit__/download/download-output/Output.handler.unit.test.ts @@ -288,4 +288,66 @@ describe("download output handler tests", () => { expect(error instanceof ImperativeError).toBe(true); expect(error.message).toMatchSnapshot(); }); + + it("should download a job output using wfa", async () => { + let passedSession: Session = null; + GetJobs.getJob = jest.fn(async (session, jobid) => { + passedSession = session; + return GetJobsData.SAMPLE_COMPLETE_JOB; + }); + DownloadJobs.downloadAllSpoolContentCommon = jest.fn( + async (session, options) => { + return; + } + ); + const handler = new OutputHandler.default(); + const params = Object.assign({}, ...[DEFAULT_PARAMETERS]); + params.arguments = Object.assign({}, ...[DEFAULT_PARAMETERS.arguments]); + params.arguments.jobid = GetJobsData.SAMPLE_COMPLETE_JOB.jobid; + params.arguments.wfa = true; + const opts: IDownloadAllSpoolContentParms = { + jobname: GetJobsData.SAMPLE_COMPLETE_JOB.jobname, + jobid: GetJobsData.SAMPLE_COMPLETE_JOB.jobid, + outDir: DownloadJobs.DEFAULT_JOBS_OUTPUT_DIR, + omitJobidDirectory: false, + waitForActive: params.arguments.waitForActive + }; + await handler.process(params); + expect(GetJobs.getJob).toHaveBeenCalledTimes(1); + expect(DownloadJobs.downloadAllSpoolContentCommon).toHaveBeenCalledWith( + passedSession, + opts + ); + }); + + it("should download a job output using wfo", async () => { + let passedSession: Session = null; + GetJobs.getJob = jest.fn(async (session, jobid) => { + passedSession = session; + return GetJobsData.SAMPLE_COMPLETE_JOB; + }); + DownloadJobs.downloadAllSpoolContentCommon = jest.fn( + async (session, options) => { + return; + } + ); + const handler = new OutputHandler.default(); + const params = Object.assign({}, ...[DEFAULT_PARAMETERS]); + params.arguments = Object.assign({}, ...[DEFAULT_PARAMETERS.arguments]); + params.arguments.jobid = GetJobsData.SAMPLE_COMPLETE_JOB.jobid; + params.arguments.wfo = true; + const opts: IDownloadAllSpoolContentParms = { + jobname: GetJobsData.SAMPLE_COMPLETE_JOB.jobname, + jobid: GetJobsData.SAMPLE_COMPLETE_JOB.jobid, + outDir: DownloadJobs.DEFAULT_JOBS_OUTPUT_DIR, + omitJobidDirectory: false, + waitForOutput: params.arguments.waitForOutput + }; + await handler.process(params); + expect(GetJobs.getJob).toHaveBeenCalledTimes(1); + expect(DownloadJobs.downloadAllSpoolContentCommon).toHaveBeenCalledWith( + passedSession, + opts + ); + }); }); diff --git a/packages/cli/__tests__/zosjobs/__unit__/download/download-output/__snapshots__/Output.definition.unit.test.ts.snap b/packages/cli/__tests__/zosjobs/__unit__/download/download-output/__snapshots__/Output.definition.unit.test.ts.snap index 16ec55cdd7..f73cf31e05 100644 --- a/packages/cli/__tests__/zosjobs/__unit__/download/download-output/__snapshots__/Output.definition.unit.test.ts.snap +++ b/packages/cli/__tests__/zosjobs/__unit__/download/download-output/__snapshots__/Output.definition.unit.test.ts.snap @@ -70,6 +70,28 @@ Object { "name": "encoding", "type": "string", }, + Object { + "aliases": Array [ + "wfa", + ], + "conflictsWith": Array [ + "wait-for-output", + ], + "description": "Wait for the job to enter ACTIVE status before completing the command.", + "name": "wait-for-active", + "type": "boolean", + }, + Object { + "aliases": Array [ + "wfo", + ], + "conflictsWith": Array [ + "wait-for-active", + ], + "description": "Wait for the job to enter OUTPUT status before completing the command.", + "name": "wait-for-output", + "type": "boolean", + }, ], "positionals": Array [ Object { diff --git a/packages/cli/__tests__/zosjobs/__unit__/download/download-output/__snapshots__/Output.handler.unit.test.ts.snap b/packages/cli/__tests__/zosjobs/__unit__/download/download-output/__snapshots__/Output.handler.unit.test.ts.snap index ebc2d62e34..1faf288cb0 100644 --- a/packages/cli/__tests__/zosjobs/__unit__/download/download-output/__snapshots__/Output.handler.unit.test.ts.snap +++ b/packages/cli/__tests__/zosjobs/__unit__/download/download-output/__snapshots__/Output.handler.unit.test.ts.snap @@ -35,6 +35,20 @@ Destination: ./output" exports[`download output handler tests should download a job output using defaults 2`] = `Object {}`; +exports[`download output handler tests should download a job output using wfa 1`] = ` +"Successfully downloaded the job output +Destination: ./output" +`; + +exports[`download output handler tests should download a job output using wfa 2`] = `Object {}`; + +exports[`download output handler tests should download a job output using wfo 1`] = ` +"Successfully downloaded the job output +Destination: ./output" +`; + +exports[`download output handler tests should download a job output using wfo 2`] = `Object {}`; + exports[`download output handler tests should download a job output with a specific extension 1`] = ` "Successfully downloaded the job output Destination: ./output" diff --git a/packages/cli/__tests__/zosjobs/__unit__/list/jobs/__snapshots__/Jobs.handler.unit.test.ts.snap b/packages/cli/__tests__/zosjobs/__unit__/list/jobs/__snapshots__/Jobs.handler.unit.test.ts.snap index a1b05c64c4..21f22a85a1 100644 --- a/packages/cli/__tests__/zosjobs/__unit__/list/jobs/__snapshots__/Jobs.handler.unit.test.ts.snap +++ b/packages/cli/__tests__/zosjobs/__unit__/list/jobs/__snapshots__/Jobs.handler.unit.test.ts.snap @@ -4,6 +4,11 @@ exports[`list jobs handler tests should be able to get a list of jobs for a spec Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -19,6 +24,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", @@ -49,6 +59,11 @@ Object { "output": Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -64,6 +79,11 @@ Object { }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", @@ -85,6 +105,11 @@ exports[`list jobs handler tests should be able to get a list of jobs for a spec Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -100,6 +125,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", @@ -130,6 +160,11 @@ Object { "output": Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -145,6 +180,11 @@ Object { }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", @@ -166,6 +206,11 @@ exports[`list jobs handler tests should be able to get a list of jobs using defa Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -181,6 +226,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", @@ -211,6 +261,11 @@ Object { "output": Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -226,6 +281,11 @@ Object { }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "JOBxxx", diff --git a/packages/cli/__tests__/zosjobs/__unit__/modify/job/Job.handler.unit.test.ts b/packages/cli/__tests__/zosjobs/__unit__/modify/job/Job.handler.unit.test.ts index b703740e85..afe417b4ee 100644 --- a/packages/cli/__tests__/zosjobs/__unit__/modify/job/Job.handler.unit.test.ts +++ b/packages/cli/__tests__/zosjobs/__unit__/modify/job/Job.handler.unit.test.ts @@ -39,6 +39,11 @@ const SAMPLE_COMPLETE_JOB: IJob= { "subsystem": "JES2", "type": "JOB", "url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A", + "exec-started": '2024-01-02T15:57:58.350Z', + "exec-ended": '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', + "exec-submitted": '2024-01-02T15:58:00.600Z' }; const SUCCESS_FEEDBACK: IJobFeedback = { diff --git a/packages/cli/__tests__/zosjobs/__unit__/view/job/__snapshots__/JobStatusByJobid.handler.unit.test.ts.snap b/packages/cli/__tests__/zosjobs/__unit__/view/job/__snapshots__/JobStatusByJobid.handler.unit.test.ts.snap index c41d29f992..a9e6fb30eb 100644 --- a/packages/cli/__tests__/zosjobs/__unit__/view/job/__snapshots__/JobStatusByJobid.handler.unit.test.ts.snap +++ b/packages/cli/__tests__/zosjobs/__unit__/view/job/__snapshots__/JobStatusByJobid.handler.unit.test.ts.snap @@ -5,6 +5,11 @@ exports[`view job-status-by-jobid handler tests should be able respond with erro exports[`view job-status-by-jobid handler tests should be able to get a job 1`] = ` Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -33,6 +38,11 @@ Object { "format": "object", "output": Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", diff --git a/packages/cli/__tests__/zosmf/__system__/check/status/zosmf.check.status.system.test.ts b/packages/cli/__tests__/zosmf/__system__/check/status/zosmf.check.status.system.test.ts index a2cb9867c9..70dbf02be3 100644 --- a/packages/cli/__tests__/zosmf/__system__/check/status/zosmf.check.status.system.test.ts +++ b/packages/cli/__tests__/zosmf/__system__/check/status/zosmf.check.status.system.test.ts @@ -14,6 +14,8 @@ import { TestEnvironment } from "../../../../../../../__tests__/__src__/environm import { ITestPropertiesSchema } from "../../../../../../../__tests__/__src__/properties/ITestPropertiesSchema"; import { stripNewLines } from "../../../../../../../__tests__/__src__/TestUtils"; import { IO } from "@zowe/imperative"; +import { join } from "path"; +import { chmodSync } from "fs"; let testEnvironment: ITestEnvironment; let host: string; @@ -105,15 +107,16 @@ describe("zosmf check status", () => { (!process.env.HTTP_PROXY && !process.env.HTTPS_PROXY ? it : it.skip)("should fail due to invalid port", async () => { // update temporary zowe profile with an invalid port - const scriptPath = testEnvironment.workingDir + "_create_profile_invalid_port"; + const scriptPath = join(testEnvironment.workingDir, "create_profile_invalid_port.sh"); const bogusPort = 12345; - const command = `zowe config set profiles.${testEnvironment.tempProfiles?.zosmf[0]}.properties.port ${bogusPort} --gc`; + const command = `#!/bin/bash\nzowe config set profiles.${testEnvironment.tempProfiles?.zosmf[0]}.properties.port ${bogusPort} --gc`; await IO.writeFileAsync(scriptPath, command); + if (process.platform !== "win32") { chmodSync(scriptPath, 0o755); } let response = runCliScript(scriptPath, testEnvironment); expect(response.status).toBe(0); // now check the status response = runCliScript(__dirname + "/__scripts__/command/zosmf_check_status.sh", testEnvironment); - expect(stripNewLines(response.stderr.toString())).toContain("connect ECONNREFUSED"); + expect(stripNewLines(response.stderr.toString())).toMatch(/.*(ECONNREFUSED|ECONNRESET|ETIMEDOUT).*/); }); }); }); diff --git a/packages/cli/__tests__/zosmf/__system__/list/systems/zosmf.list.systems.system.test.ts b/packages/cli/__tests__/zosmf/__system__/list/systems/zosmf.list.systems.system.test.ts index 790f7849b3..1654798fd9 100644 --- a/packages/cli/__tests__/zosmf/__system__/list/systems/zosmf.list.systems.system.test.ts +++ b/packages/cli/__tests__/zosmf/__system__/list/systems/zosmf.list.systems.system.test.ts @@ -14,6 +14,8 @@ import { TestEnvironment } from "../../../../../../../__tests__/__src__/environm import { ITestPropertiesSchema } from "../../../../../../../__tests__/__src__/properties/ITestPropertiesSchema"; import { stripNewLines } from "../../../../../../../__tests__/__src__/TestUtils"; import { IO } from "@zowe/imperative"; +import { join } from "path"; +import { chmodSync } from "fs"; let testEnvironment: ITestEnvironment; let host: string; @@ -123,15 +125,16 @@ describe("zosmf list systems", () => { (!process.env.HTTP_PROXY && !process.env.HTTPS_PROXY ? it : it.skip)("should fail due to invalid port", async () => { // update temporary zowe profile with an invalid port - const scriptPath = testEnvironment.workingDir + "_create_profile_invalid_port"; + const scriptPath = join(testEnvironment.workingDir, "create_profile_invalid_port.sh"); const bogusPort = 12345; - const command = `zowe config set profiles.${testEnvironment.tempProfiles?.zosmf[0]}.properties.port ${bogusPort} --gc`; + const command = `#!/bin/bash\nzowe config set profiles.${testEnvironment.tempProfiles?.zosmf[0]}.properties.port ${bogusPort} --gc`; await IO.writeFileAsync(scriptPath, command); + if (process.platform !== "win32") { chmodSync(scriptPath, 0o755); } let response = runCliScript(scriptPath, testEnvironment); expect(response.status).toBe(0); // now check the status response = runCliScript(__dirname + "/__scripts__/command/zosmf_list_systems.sh", testEnvironment); - expect(stripNewLines(response.stderr.toString())).toContain("connect ECONNREFUSED"); + expect(stripNewLines(response.stderr.toString())).toMatch(/.*(ECONNREFUSED|ECONNRESET|ETIMEDOUT).*/); }); }); }); diff --git a/packages/cli/__tests__/zostso/__integration__/root/__snapshots__/cli.zos-tso.integration.test.ts.snap b/packages/cli/__tests__/zostso/__integration__/root/__snapshots__/cli.zos-tso.integration.test.ts.snap index be8a0738b8..6878be1a6d 100644 --- a/packages/cli/__tests__/zostso/__integration__/root/__snapshots__/cli.zos-tso.integration.test.ts.snap +++ b/packages/cli/__tests__/zostso/__integration__/root/__snapshots__/cli.zos-tso.integration.test.ts.snap @@ -17,11 +17,12 @@ exports[`zos-tso should display the help 1`] = ` GROUPS ------ - issue Issue TSO commands - ping Ping a TSO address space - send Send data to TSO - start | st Start TSO/E address space - stop | sp Stop TSO/E address space + issue Issue TSO commands + ping Ping a TSO address space + receive | r Receive message from TSO address space app + send Send data to TSO + start | st Start TSO/E address space + stop | sp Stop TSO/E address space GLOBAL OPTIONS -------------- @@ -46,8 +47,8 @@ exports[`zos-tso should display the help 1`] = ` \\"success\\": true, \\"exitCode\\": 0, \\"message\\": \\"The help was constructed for command: zos-tso.\\", - \\"stdout\\": \\"\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Issue TSO commands and interact with TSO address spaces.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-tso \\\\n\\\\n Where is one of the following:\\\\n\\\\n GROUPS\\\\n ------\\\\n\\\\n issue Issue TSO commands \\\\n ping Ping a TSO address space \\\\n send Send data to TSO \\\\n start | st Start TSO/E address space\\\\n stop | sp Stop TSO/E address space \\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --help-examples (boolean)\\\\n\\\\n Display examples for all the commands in a group\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n\\", + \\"stdout\\": \\"\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Issue TSO commands and interact with TSO address spaces.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-tso \\\\n\\\\n Where is one of the following:\\\\n\\\\n GROUPS\\\\n ------\\\\n\\\\n issue Issue TSO commands \\\\n ping Ping a TSO address space \\\\n receive | r Receive message from TSO address space app\\\\n send Send data to TSO \\\\n start | st Start TSO/E address space \\\\n stop | sp Stop TSO/E address space \\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --help-examples (boolean)\\\\n\\\\n Display examples for all the commands in a group\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n\\", \\"stderr\\": \\"\\", - \\"data\\": \\"\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Issue TSO commands and interact with TSO address spaces.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-tso \\\\n\\\\n Where is one of the following:\\\\n\\\\n GROUPS\\\\n ------\\\\n\\\\n issue Issue TSO commands \\\\n ping Ping a TSO address space \\\\n send Send data to TSO \\\\n start | st Start TSO/E address space\\\\n stop | sp Stop TSO/E address space \\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --help-examples (boolean)\\\\n\\\\n Display examples for all the commands in a group\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n\\" + \\"data\\": \\"\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Issue TSO commands and interact with TSO address spaces.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-tso \\\\n\\\\n Where is one of the following:\\\\n\\\\n GROUPS\\\\n ------\\\\n\\\\n issue Issue TSO commands \\\\n ping Ping a TSO address space \\\\n receive | r Receive message from TSO address space app\\\\n send Send data to TSO \\\\n start | st Start TSO/E address space \\\\n stop | sp Stop TSO/E address space \\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --help-examples (boolean)\\\\n\\\\n Display examples for all the commands in a group\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n\\" }" `; diff --git a/packages/cli/__tests__/zostso/__integration__/root/cli.zos-tso.integration.test.ts b/packages/cli/__tests__/zostso/__integration__/root/cli.zos-tso.integration.test.ts index 75a1fb62dc..9bb895c171 100644 --- a/packages/cli/__tests__/zostso/__integration__/root/cli.zos-tso.integration.test.ts +++ b/packages/cli/__tests__/zostso/__integration__/root/cli.zos-tso.integration.test.ts @@ -50,7 +50,7 @@ describe("zos-tso", () => { expect(response.stdout.toString()).toBe(""); expect(response.stderr.toString()).toContain('Unknown arguments: not-valid-option, notValidOption'); expect(response.stderr.toString()).toContain('Command failed due to improper syntax'); - expect(response.stderr.toString()).toContain('Did you mean: zos-tso start'); + expect(response.stderr.toString()).toContain('Did you mean: zos-tso send app'); expect(response.stderr.toString()).toContain('Command entered: "zos-tso --not-valid-option"'); expect(response.stderr.toString()).toContain('Use "zowe zos-tso --help" to view groups, commands, and options.'); }); diff --git a/packages/cli/__tests__/zostso/__integration__/send/__snapshots__/cli.zos-tso.send.integration.test.ts.snap b/packages/cli/__tests__/zostso/__integration__/send/__snapshots__/cli.zos-tso.send.integration.test.ts.snap index 5ec94b307f..583114ba18 100644 --- a/packages/cli/__tests__/zostso/__integration__/send/__snapshots__/cli.zos-tso.send.integration.test.ts.snap +++ b/packages/cli/__tests__/zostso/__integration__/send/__snapshots__/cli.zos-tso.send.integration.test.ts.snap @@ -17,7 +17,8 @@ exports[`zos-tso send should display the help 1`] = ` COMMANDS -------- - address-space | as Send data to a TSO address space + address-space | as Send data to a TSO address space + app | a Send message to an application at a TSO address space GLOBAL OPTIONS -------------- @@ -42,8 +43,8 @@ exports[`zos-tso send should display the help 1`] = ` \\"success\\": true, \\"exitCode\\": 0, \\"message\\": \\"The help was constructed for command: send.\\", - \\"stdout\\": \\"\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Send data to TSO and collect responses until the prompt is reached.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-tso send \\\\n\\\\n Where is one of the following:\\\\n\\\\n COMMANDS\\\\n --------\\\\n\\\\n address-space | as Send data to a TSO address space\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --help-examples (boolean)\\\\n\\\\n Display examples for all the commands in a group\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n\\", + \\"stdout\\": \\"\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Send data to TSO and collect responses until the prompt is reached.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-tso send \\\\n\\\\n Where is one of the following:\\\\n\\\\n COMMANDS\\\\n --------\\\\n\\\\n address-space | as Send data to a TSO address space \\\\n app | a Send message to an application at a TSO address space\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --help-examples (boolean)\\\\n\\\\n Display examples for all the commands in a group\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n\\", \\"stderr\\": \\"\\", - \\"data\\": \\"\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Send data to TSO and collect responses until the prompt is reached.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-tso send \\\\n\\\\n Where is one of the following:\\\\n\\\\n COMMANDS\\\\n --------\\\\n\\\\n address-space | as Send data to a TSO address space\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --help-examples (boolean)\\\\n\\\\n Display examples for all the commands in a group\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n\\" + \\"data\\": \\"\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Send data to TSO and collect responses until the prompt is reached.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-tso send \\\\n\\\\n Where is one of the following:\\\\n\\\\n COMMANDS\\\\n --------\\\\n\\\\n address-space | as Send data to a TSO address space \\\\n app | a Send message to an application at a TSO address space\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --help-examples (boolean)\\\\n\\\\n Display examples for all the commands in a group\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n\\" }" `; diff --git a/packages/cli/__tests__/zostso/__integration__/start/__snapshots__/cli.zos-tso.start.integration.test.ts.snap b/packages/cli/__tests__/zostso/__integration__/start/__snapshots__/cli.zos-tso.start.integration.test.ts.snap index 1f08304a7a..ed2a55c2d9 100644 --- a/packages/cli/__tests__/zostso/__integration__/start/__snapshots__/cli.zos-tso.start.integration.test.ts.snap +++ b/packages/cli/__tests__/zostso/__integration__/start/__snapshots__/cli.zos-tso.start.integration.test.ts.snap @@ -17,7 +17,8 @@ exports[`zos-tso start should display the help 1`] = ` COMMANDS -------- - address-space | as Start a TSO address space + address-space | as Start a TSO address space + app | a Start application at TSO address space GLOBAL OPTIONS -------------- @@ -42,8 +43,8 @@ exports[`zos-tso start should display the help 1`] = ` \\"success\\": true, \\"exitCode\\": 0, \\"message\\": \\"The help was constructed for command: start.\\", - \\"stdout\\": \\"\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Start TSO/E address space.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-tso start \\\\n\\\\n Where is one of the following:\\\\n\\\\n COMMANDS\\\\n --------\\\\n\\\\n address-space | as Start a TSO address space\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --help-examples (boolean)\\\\n\\\\n Display examples for all the commands in a group\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n\\", + \\"stdout\\": \\"\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Start TSO/E address space.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-tso start \\\\n\\\\n Where is one of the following:\\\\n\\\\n COMMANDS\\\\n --------\\\\n\\\\n address-space | as Start a TSO address space \\\\n app | a Start application at TSO address space\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --help-examples (boolean)\\\\n\\\\n Display examples for all the commands in a group\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n\\", \\"stderr\\": \\"\\", - \\"data\\": \\"\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Start TSO/E address space.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-tso start \\\\n\\\\n Where is one of the following:\\\\n\\\\n COMMANDS\\\\n --------\\\\n\\\\n address-space | as Start a TSO address space\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --help-examples (boolean)\\\\n\\\\n Display examples for all the commands in a group\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n\\" + \\"data\\": \\"\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Start TSO/E address space.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-tso start \\\\n\\\\n Where is one of the following:\\\\n\\\\n COMMANDS\\\\n --------\\\\n\\\\n address-space | as Start a TSO address space \\\\n app | a Start application at TSO address space\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --help-examples (boolean)\\\\n\\\\n Display examples for all the commands in a group\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n\\" }" `; diff --git a/packages/cli/__tests__/zostso/__integration__/start/cli.zos-tso.start.integration.test.ts b/packages/cli/__tests__/zostso/__integration__/start/cli.zos-tso.start.integration.test.ts index c0a244761a..93b34e7e15 100644 --- a/packages/cli/__tests__/zostso/__integration__/start/cli.zos-tso.start.integration.test.ts +++ b/packages/cli/__tests__/zostso/__integration__/start/cli.zos-tso.start.integration.test.ts @@ -41,7 +41,7 @@ describe("zos-tso start", () => { expect(response.stderr.toString()).toContain('Command failed due to improper syntax'); expect(response.stderr.toString()).toContain('Did you mean: zos-tso start'); expect(response.stderr.toString()).toContain('Command entered: "zos-tso start foobar"'); - expect(response.stderr.toString()).toContain('Available commands are "address-space".'); + expect(response.stderr.toString()).toContain('Available commands are "address-space, app".'); expect(response.stderr.toString()).toContain('Use "zowe zos-tso start --help" to view groups, commands, and options.'); expect(response.stderr.toString()).toContain('Error: Unknown argument: foobar'); }); @@ -53,7 +53,7 @@ describe("zos-tso start", () => { expect(response.stderr.toString()).toContain('Unknown arguments: foo-bar, fooBar'); expect(response.stderr.toString()).toContain('Command failed due to improper syntax'); expect(response.stderr.toString()).toContain('Command entered: "zos-tso start --foo-bar"'); - expect(response.stderr.toString()).toContain('Available commands are "address-space".'); + expect(response.stderr.toString()).toContain('Available commands are "address-space, app".'); expect(response.stderr.toString()).toContain('Use "zowe zos-tso start --help" to view groups, commands, and options.'); expect(response.stderr.toString()).toContain('Error: Unknown arguments: foo-bar, fooBar'); }); diff --git a/packages/cli/__tests__/zostso/__system__/stop/__scripts__/address-space/as_fully_qualified.sh b/packages/cli/__tests__/zostso/__system__/stop/__scripts__/address-space/as_fully_qualified.sh index 97bf3bf6e4..a4966fd2d0 100755 --- a/packages/cli/__tests__/zostso/__system__/stop/__scripts__/address-space/as_fully_qualified.sh +++ b/packages/cli/__tests__/zostso/__system__/stop/__scripts__/address-space/as_fully_qualified.sh @@ -7,7 +7,8 @@ PORT=$3 USER=$4 PASSWORD=$5 -SERVLET_KEY=`zowe tso start as --host $HOST --account $ACCOUNT --port $PORT --user $USER --password $PASSWORD --ru=false | grep -oP "(?<=: ).*"` +# SERVLET_KEY=`zowe tso start as --host $HOST --account $ACCOUNT --port $PORT --user $USER --password $PASSWORD --ru=false | grep -oP "(?<=: ).*"` +SERVLET_KEY=`zowe tso start as --host $HOST --account $ACCOUNT --port $PORT --user $USER --password $PASSWORD --ru=false | awk -F': ' '{print $2}' | sed '1p;d'` zowe zos-tso stop address-space ${SERVLET_KEY} --host $HOST --port $PORT --user $USER --password $PASSWORD --ru=false exit $? \ No newline at end of file diff --git a/packages/cli/__tests__/zostso/__system__/stop/__scripts__/address-space/as_success_stop.sh b/packages/cli/__tests__/zostso/__system__/stop/__scripts__/address-space/as_success_stop.sh index 88d2188082..a32a4d780a 100755 --- a/packages/cli/__tests__/zostso/__system__/stop/__scripts__/address-space/as_success_stop.sh +++ b/packages/cli/__tests__/zostso/__system__/stop/__scripts__/address-space/as_success_stop.sh @@ -1,7 +1,8 @@ #!/bin/bash set -e -SERVLET_KEY=`zowe tso start as | grep -oP "(?<=: ).*"` +# SERVLET_KEY=`zowe tso start as | grep -oP "(?<=: ).*"` +SERVLET_KEY=`zowe tso start as | awk -F': ' '{print $2}' | sed '1p;d'` zowe zos-tso stop address-space ${SERVLET_KEY} exit $? \ No newline at end of file diff --git a/packages/cli/__tests__/zostso/__system__/stop/__scripts__/address-space/as_success_stop_rfj.sh b/packages/cli/__tests__/zostso/__system__/stop/__scripts__/address-space/as_success_stop_rfj.sh index f587af271f..bcccfa0f26 100755 --- a/packages/cli/__tests__/zostso/__system__/stop/__scripts__/address-space/as_success_stop_rfj.sh +++ b/packages/cli/__tests__/zostso/__system__/stop/__scripts__/address-space/as_success_stop_rfj.sh @@ -1,7 +1,8 @@ #!/bin/bash set -e -SERVLET_KEY=`zowe tso start as | grep -oP "(?<=: ).*"` +# SERVLET_KEY=`zowe tso start as | grep -oP "(?<=: ).*"` +SERVLET_KEY=`zowe tso start as | awk -F': ' '{print $2}' | sed '1p;d'` zowe zos-tso stop address-space ${SERVLET_KEY} --response-format-json exit $? \ No newline at end of file diff --git a/packages/cli/__tests__/zostso/__system__/stop/__snapshots__/cli.zos-tso.stop.system.test.ts.snap b/packages/cli/__tests__/zostso/__system__/stop/__snapshots__/cli.zos-tso.stop.system.test.ts.snap index aab1477817..fc29854daa 100644 --- a/packages/cli/__tests__/zostso/__system__/stop/__snapshots__/cli.zos-tso.stop.system.test.ts.snap +++ b/packages/cli/__tests__/zostso/__system__/stop/__snapshots__/cli.zos-tso.stop.system.test.ts.snap @@ -5,7 +5,7 @@ exports[`zos-tso stop should fail with invalid option 1`] = ` Unknown arguments: foo-bar, fooBar Command failed due to improper syntax Command entered: \\"zos-tso start as --foo-bar\\" -Available commands are \\"address-space\\". +Available commands are \\"address-space, app\\". Use \\"zowe zos-tso start --help\\" to view groups, commands, and options. Response From Service diff --git a/packages/cli/__tests__/zostso/__unit__/ZosTso.definition.unit.test.ts b/packages/cli/__tests__/zostso/__unit__/ZosTso.definition.unit.test.ts index a6f099ca60..80dcbc0034 100644 --- a/packages/cli/__tests__/zostso/__unit__/ZosTso.definition.unit.test.ts +++ b/packages/cli/__tests__/zostso/__unit__/ZosTso.definition.unit.test.ts @@ -13,7 +13,7 @@ import { ICommandDefinition } from "@zowe/imperative"; describe("zos-tso group definition", () => { it("should not have changed", () => { - const CHILDREN = 5; + const CHILDREN = 6; const definition: ICommandDefinition = require("../../../src/zostso/ZosTso.definition"); expect(definition).toBeDefined(); expect(definition.children.length).toBe(CHILDREN); diff --git a/packages/cli/__tests__/zostso/__unit__/receive/Receive.definition.unit.test.ts b/packages/cli/__tests__/zostso/__unit__/receive/Receive.definition.unit.test.ts new file mode 100644 index 0000000000..7b950ce791 --- /dev/null +++ b/packages/cli/__tests__/zostso/__unit__/receive/Receive.definition.unit.test.ts @@ -0,0 +1,22 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { ICommandDefinition } from "@zowe/imperative"; + +describe("zos-tso receive group definition", () => { + it("should not have changed", () => { + const definition: ICommandDefinition = require("../../../../src/zostso/receive/Receive.definition").ReceiveCommand; + expect(definition).toBeDefined(); + expect(definition.children.length).toBe(1); + delete definition.children; + expect(definition).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/__tests__/zostso/__unit__/receive/__snapshots__/Receive.definition.unit.test.ts.snap b/packages/cli/__tests__/zostso/__unit__/receive/__snapshots__/Receive.definition.unit.test.ts.snap new file mode 100644 index 0000000000..7be0203f57 --- /dev/null +++ b/packages/cli/__tests__/zostso/__unit__/receive/__snapshots__/Receive.definition.unit.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`zos-tso receive group definition should not have changed 1`] = ` +Object { + "aliases": Array [ + "r", + ], + "description": "Receive message from TSO address space app", + "name": "receive", + "summary": "Receive message from TSO address space app", + "type": "group", +} +`; diff --git a/packages/cli/__tests__/zostso/__unit__/receive/app/ReceiveASApp.handler.unit.test.ts b/packages/cli/__tests__/zostso/__unit__/receive/app/ReceiveASApp.handler.unit.test.ts new file mode 100644 index 0000000000..133bc072f1 --- /dev/null +++ b/packages/cli/__tests__/zostso/__unit__/receive/app/ReceiveASApp.handler.unit.test.ts @@ -0,0 +1,65 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { ZosmfRestClient } from "@zowe/core-for-zowe-sdk"; +import { AddressSpaceApps } from "@zowe/zos-tso-for-zowe-sdk"; +import * as ReceiveASAppHandler from "../../../../../src/zostso/receive/app/ReceiveASApp.handler"; +import * as ReceiveASAppDefinition from "../../../../../src/zostso/receive/app/ReceiveASApp.definition"; +import { IHandlerParameters } from "@zowe/imperative"; +import { mockHandlerParameters } from "@zowe/cli-test-utils"; +import { UNIT_TEST_ZOSMF_PROF_OPTS } from "../../../../../../../__tests__/__src__/TestConstants"; + +const DEFAULT_PARAMETERS: IHandlerParameters = mockHandlerParameters({ + arguments: UNIT_TEST_ZOSMF_PROF_OPTS, + positionals: ["zos-tso","r","app"], + definition: ReceiveASAppDefinition.ReceiveASApp +}); +const MOCK_RECEIVE_RESPONSE: any = { + version: undefined, + reused: false, + timeout: false, + servletKey: "ZOWEUSER-122-aabyaaaj", + queueID: null, + tsoData: [ + { + "TSO MESSAGE": { VERSION: "0100", DATA: "Processing started." }, + }, + { + "TSO MESSAGE": { VERSION: "0100", DATA: "READY" }, + }, + ], +}; + +describe("receive TSO app handler behavior", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should properly receive and parse data from receive TSO response", async () => { + jest.spyOn(ZosmfRestClient, "getExpectJSON").mockResolvedValueOnce( + MOCK_RECEIVE_RESPONSE + ); + const receiveSpy = jest.spyOn(AddressSpaceApps,"receive"); + const handler = new ReceiveASAppHandler.default(); + const params = Object.assign({}, ...[DEFAULT_PARAMETERS]); + let error = undefined; + params.arguments = {...params.arguments,account: "izuacct", appKey: "test2", servletKey: "ZOWEUSER-122-aabyaaaj", runUntilReady: true}; + try{ + await handler.process(params); + } + catch(e) + { + error = e; + } + expect(error).toBeUndefined(); + expect(receiveSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/cli/__tests__/zostso/__unit__/receive/app/__snapshots__/ReceiveASApp.handler.unit.test.ts.snap b/packages/cli/__tests__/zostso/__unit__/receive/app/__snapshots__/ReceiveASApp.handler.unit.test.ts.snap new file mode 100644 index 0000000000..804ca2c953 --- /dev/null +++ b/packages/cli/__tests__/zostso/__unit__/receive/app/__snapshots__/ReceiveASApp.handler.unit.test.ts.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`receive TSO app handler behavior should properly receive and parse data from receive TSO response 1`] = ` +" +" +`; + +exports[`receive TSO app handler behavior should properly receive and parse data from receive TSO response 2`] = `"Processing started."`; + +exports[`receive TSO app handler behavior should properly receive and parse data from receive TSO response 3`] = `"READY"`; + +exports[`receive TSO app handler behavior should properly receive and parse data from receive TSO response 4`] = ` +Object { + "queueID": null, + "reused": false, + "servletKey": "ZOWEUSER-122-aabyaaaj", + "timeout": false, + "tsoData": Array [ + Object { + "DATA": "Processing started.", + "VERSION": "0100", + }, + Object { + "DATA": "READY", + "VERSION": "0100", + }, + ], + "version": undefined, +} +`; diff --git a/packages/cli/__tests__/zostso/__unit__/send/Send.definition.unit.test.ts b/packages/cli/__tests__/zostso/__unit__/send/Send.definition.unit.test.ts index 9272a3ec33..119a5ff9b8 100644 --- a/packages/cli/__tests__/zostso/__unit__/send/Send.definition.unit.test.ts +++ b/packages/cli/__tests__/zostso/__unit__/send/Send.definition.unit.test.ts @@ -15,7 +15,7 @@ describe("zos-tso send group definition", () => { it("should not have changed", () => { const definition: ICommandDefinition = require("../../../../src/zostso/send/Send.definition").SendCommand; expect(definition).toBeDefined(); - expect(definition.children.length).toBe(1); + expect(definition.children.length).toBe(2); delete definition.children; expect(definition).toMatchSnapshot(); }); diff --git a/packages/cli/__tests__/zostso/__unit__/send/app/SendASApp.handler.unit.test.ts b/packages/cli/__tests__/zostso/__unit__/send/app/SendASApp.handler.unit.test.ts new file mode 100644 index 0000000000..6b7aa1b720 --- /dev/null +++ b/packages/cli/__tests__/zostso/__unit__/send/app/SendASApp.handler.unit.test.ts @@ -0,0 +1,70 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { ZosmfRestClient } from "@zowe/core-for-zowe-sdk"; +import { AddressSpaceApps } from "@zowe/zos-tso-for-zowe-sdk"; +import * as SendASAppHandler from "../../../../../src/zostso/send/as-app/SendASApp.handler"; +import * as SendASAppDefinition from "../../../../../src/zostso/send/as-app/SendASApp.definition"; +import { IHandlerParameters } from "@zowe/imperative"; +import { mockHandlerParameters } from "@zowe/cli-test-utils"; +import { UNIT_TEST_ZOSMF_PROF_OPTS } from "../../../../../../../__tests__/__src__/TestConstants"; + +const DEFAULT_PARAMETERS: IHandlerParameters = mockHandlerParameters({ + arguments: UNIT_TEST_ZOSMF_PROF_OPTS, + positionals: ["zos-tso","send","app","--app-key","test2"], + definition: SendASAppDefinition.SendASApp +}); +const MOCK_SEND_RESPONSE = Promise.resolve({ + servletKey: "ZOWEUSER-127-aabeaaag", + ver: "0100", + tsoData: [ + { + "TSO MESSAGE": { + VERSION: "0100", + DATA: "HELLOW exec processing has started.", + }, + }, + { + "TSO MESSAGE": { + VERSION: "0100", + DATA: "UNIX message queue id = 1048608", + }, + }, + ], + reused: false, + timeout: false, +}); + +describe("receive TSO app handler behavior", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should properly receive and parse data from receive TSO response", async () => { + jest.spyOn(ZosmfRestClient, "putExpectJSON").mockResolvedValueOnce( + MOCK_SEND_RESPONSE + ); + const sendSpy = jest.spyOn(AddressSpaceApps,"send"); + const handler = new SendASAppHandler.default(); + const params = Object.assign({}, ...[DEFAULT_PARAMETERS]); + let error = undefined; + params.arguments = {...params.arguments,account: "izuacct", appKey: "test2", servletKey: "ZOWEUSER-129-aaceaaap", message: "LONG 100"}; + try{ + await handler.process(params); + } + catch(e) + { + error = e; + } + expect(error).toBeUndefined(); + expect(sendSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/cli/__tests__/zostso/__unit__/send/app/__snapshots__/SendASApp.handler.unit.test.ts.snap b/packages/cli/__tests__/zostso/__unit__/send/app/__snapshots__/SendASApp.handler.unit.test.ts.snap new file mode 100644 index 0000000000..7f6ec7a519 --- /dev/null +++ b/packages/cli/__tests__/zostso/__unit__/send/app/__snapshots__/SendASApp.handler.unit.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`receive TSO app handler behavior should properly receive and parse data from receive TSO response 1`] = ` +"{ + \\"version\\": \\"0100\\", + \\"reused\\": false, + \\"timeout\\": false, + \\"servletKey\\": \\"ZOWEUSER-127-aabeaaag\\", + \\"queueID\\": null, + \\"tsoData\\": [ + { + \\"VERSION\\": \\"0100\\", + \\"DATA\\": \\"HELLOW exec processing has started.\\" + }, + { + \\"VERSION\\": \\"0100\\", + \\"DATA\\": \\"UNIX message queue id = 1048608\\" + } + ] +}" +`; diff --git a/packages/cli/__tests__/zostso/__unit__/start/Start.definition.unit.test.ts b/packages/cli/__tests__/zostso/__unit__/start/Start.definition.unit.test.ts index 8e62ed0856..e0ce1f037e 100644 --- a/packages/cli/__tests__/zostso/__unit__/start/Start.definition.unit.test.ts +++ b/packages/cli/__tests__/zostso/__unit__/start/Start.definition.unit.test.ts @@ -15,7 +15,7 @@ describe("zos-tso start group definition", () => { it("should not have changed", () => { const definition: ICommandDefinition = require("../../../../src/zostso/start/Start.definition").StartCommand; expect(definition).toBeDefined(); - expect(definition.children.length).toBe(1); + expect(definition.children.length).toBe(2); delete definition.children; expect(definition).toMatchSnapshot(); }); diff --git a/packages/cli/__tests__/zostso/__unit__/start/app/StartASApp.handler.unit.test.ts b/packages/cli/__tests__/zostso/__unit__/start/app/StartASApp.handler.unit.test.ts new file mode 100644 index 0000000000..ddfc9cdcd3 --- /dev/null +++ b/packages/cli/__tests__/zostso/__unit__/start/app/StartASApp.handler.unit.test.ts @@ -0,0 +1,155 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { ZosmfRestClient } from "@zowe/core-for-zowe-sdk"; +import { StartTso, AddressSpaceApps, IStartStopResponses } from "@zowe/zos-tso-for-zowe-sdk"; +import * as StartASAppHandler from "../../../../../src/zostso/start/as-app/StartASApp.handler"; +import * as StartASAppDefinition from "../../../../../src/zostso/start/as-app/StartASApp.definition"; +import { IHandlerParameters } from "@zowe/imperative"; +import { mockHandlerParameters } from "@zowe/cli-test-utils"; +import { UNIT_TEST_ZOSMF_PROF_OPTS } from "../../../../../../../__tests__/__src__/TestConstants"; + +const NEW_AS_SPACE_PARAMS: IHandlerParameters = mockHandlerParameters({ + arguments: UNIT_TEST_ZOSMF_PROF_OPTS, + positionals: ["zos-tso", "start", "app"], + definition: StartASAppDefinition.StartASApp, +}); +const EXISTING_AS_SPACE_PARAMS: IHandlerParameters = mockHandlerParameters({ + arguments: UNIT_TEST_ZOSMF_PROF_OPTS, + positionals: ["zos-tso", "start", "app"], + definition: StartASAppDefinition.StartASApp, +}); +const MOCK_RESPONSE = Promise.resolve({ + version: "0100", + reused: false, + timeout: false, + servletKey: "ZOWEUSER-123-aaaaaa", + queueID: "983068", + tsoData: [ + { + "TSO MESSAGE": { + VERSION: "0100", + DATA: "HELLOW exec processing has started.", + }, + }, + { + "TSO MESSAGE": { + VERSION: "0100", + DATA: "UNIX message queue id = 983068", + }, + }, + { + "TSO MESSAGE": { + VERSION: "0100", + DATA: "Input message type = 32772", + }, + }, + { "TSO MESSAGE": { VERSION: "0100", DATA: "Output message type = 4" } }, + { + "TSO MESSAGE": { + VERSION: "0100", + DATA: "Reading application input from the UNIX message queue.", + }, + }, + ], +}); + +const MOCK_START_RESPONSE: Promise = Promise.resolve({ + collectedResponses: [], + messages: + "IKJ56455I ZOWEUSER LOGON IN PROGRESS AT 11:18:56 ON OCTOBER 14, 2024\nIKJ56951I NO BROADCAST MESSAGES\nREADY \n", + servletKey: "ZOWEUSER-123-aaaaaa", + success: true, + zosmfTsoResponse: { + ver: "0100", + queueID: "983068", + reused: false, + servletKey: "ZOWEUSER-123-aaaaaa", + sessionID: "0x00", + timeout: false, + tsoData: [{}], + }, +}); +describe("receive TSO app handler behavior", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should properly start TSO address space and run an application at the created address space", async () => { + jest.spyOn(StartTso, "start").mockReturnValue(MOCK_START_RESPONSE); + jest.spyOn(ZosmfRestClient, "postExpectJSON").mockReturnValue( + MOCK_RESPONSE + ); + + const startSpy = jest.spyOn(AddressSpaceApps, "start"); + const startASSpaceSpy = jest.spyOn(StartTso, "start"); + const zosmfSpy = jest.spyOn(ZosmfRestClient, "postExpectJSON"); + const handler = new StartASAppHandler.default(); + const params = Object.assign({}, ...[NEW_AS_SPACE_PARAMS]); + let error = undefined; + params.arguments = { + ...params.arguments, + account: "izuacct", + startup: "EXEC 'ZOWEUSER.PUBLIC.REXX(VAREXX)'", + appKey: "test2", + }; + try { + await handler.process(params); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + expect(startSpy).toHaveBeenCalledTimes(1); + expect(startASSpaceSpy).toHaveBeenCalledTimes(1); + expect(zosmfSpy).toHaveBeenCalledTimes(1); + }); + it("should properly start TSO address space at an existing TSO address space", async () => { + jest.spyOn(StartTso, "start").mockReturnValue(MOCK_START_RESPONSE); + jest.spyOn(ZosmfRestClient, "postExpectJSON").mockReturnValue( + MOCK_RESPONSE + ); + + const startSpy = jest.spyOn(AddressSpaceApps, "start"); + const startASSpaceSpy = jest.spyOn(StartTso, "start"); + const zosmfSpy = jest.spyOn(ZosmfRestClient, "postExpectJSON"); + const handler = new StartASAppHandler.default(); + const params = Object.assign({}, ...[EXISTING_AS_SPACE_PARAMS]); + let error = undefined; + params.arguments = { + ...params.arguments, + account: "izuacct", + startup: "EXEC 'ZOWEUSER.PUBLIC.REXX(VAREXX)'", + queueId: "983068", + servletKey: "ZOWEUSER-123-aaaaaa", + appKey: "test2", + }; + try { + await handler.process(params); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + expect(startSpy).toHaveBeenCalledTimes(1); + expect(startASSpaceSpy).toHaveBeenCalledTimes(0); + expect(zosmfSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/cli/__tests__/zostso/__unit__/start/app/__snapshots__/StartASApp.handler.unit.test.ts.snap b/packages/cli/__tests__/zostso/__unit__/start/app/__snapshots__/StartASApp.handler.unit.test.ts.snap new file mode 100644 index 0000000000..ca5889138b --- /dev/null +++ b/packages/cli/__tests__/zostso/__unit__/start/app/__snapshots__/StartASApp.handler.unit.test.ts.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`receive TSO app handler behavior should properly start TSO address space and run an application at the created address space 1`] = ` +"{ + \\"reused\\": false, + \\"timeout\\": false, + \\"servletKey\\": \\"ZOWEUSER-123-aaaaaa\\", + \\"queueID\\": \\"983068\\", + \\"tsoData\\": [ + { + \\"VERSION\\": \\"0100\\", + \\"DATA\\": \\"HELLOW exec processing has started.\\" + }, + { + \\"VERSION\\": \\"0100\\", + \\"DATA\\": \\"UNIX message queue id = 983068\\" + }, + { + \\"VERSION\\": \\"0100\\", + \\"DATA\\": \\"Input message type = 32772\\" + }, + { + \\"VERSION\\": \\"0100\\", + \\"DATA\\": \\"Output message type = 4\\" + }, + { + \\"VERSION\\": \\"0100\\", + \\"DATA\\": \\"Reading application input from the UNIX message queue.\\" + } + ] +}" +`; + +exports[`receive TSO app handler behavior should properly start TSO address space at an existing TSO address space 1`] = ` +"{ + \\"reused\\": false, + \\"timeout\\": false, + \\"servletKey\\": \\"ZOWEUSER-123-aaaaaa\\", + \\"queueID\\": \\"983068\\", + \\"tsoData\\": [ + { + \\"VERSION\\": \\"0100\\", + \\"DATA\\": \\"HELLOW exec processing has started.\\" + }, + { + \\"VERSION\\": \\"0100\\", + \\"DATA\\": \\"UNIX message queue id = 983068\\" + }, + { + \\"VERSION\\": \\"0100\\", + \\"DATA\\": \\"Input message type = 32772\\" + }, + { + \\"VERSION\\": \\"0100\\", + \\"DATA\\": \\"Output message type = 4\\" + }, + { + \\"VERSION\\": \\"0100\\", + \\"DATA\\": \\"Reading application input from the UNIX message queue.\\" + } + ] +}" +`; diff --git a/packages/cli/package.json b/packages/cli/package.json index 851aa412a1..02199be485 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/cli", - "version": "8.3.0", + "version": "8.7.0", "zoweVersion": "v3.0.0", "description": "Zowe CLI is a command line interface (CLI) that provides a simple and streamlined way to interact with IBM z/OS.", "author": "Zowe", @@ -58,17 +58,17 @@ "preshrinkwrap": "node ../../scripts/rewriteShrinkwrap.js" }, "dependencies": { - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0", - "@zowe/provisioning-for-zowe-sdk": "8.2.0", - "@zowe/zos-console-for-zowe-sdk": "8.2.0", - "@zowe/zos-files-for-zowe-sdk": "8.2.0", - "@zowe/zos-jobs-for-zowe-sdk": "8.2.0", - "@zowe/zos-logs-for-zowe-sdk": "8.2.0", - "@zowe/zos-tso-for-zowe-sdk": "8.2.0", - "@zowe/zos-uss-for-zowe-sdk": "8.2.0", - "@zowe/zos-workflows-for-zowe-sdk": "8.2.0", - "@zowe/zosmf-for-zowe-sdk": "8.2.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0", + "@zowe/provisioning-for-zowe-sdk": "8.7.0", + "@zowe/zos-console-for-zowe-sdk": "8.7.0", + "@zowe/zos-files-for-zowe-sdk": "8.7.0", + "@zowe/zos-jobs-for-zowe-sdk": "8.7.0", + "@zowe/zos-logs-for-zowe-sdk": "8.7.0", + "@zowe/zos-tso-for-zowe-sdk": "8.7.0", + "@zowe/zos-uss-for-zowe-sdk": "8.7.0", + "@zowe/zos-workflows-for-zowe-sdk": "8.7.0", + "@zowe/zosmf-for-zowe-sdk": "8.7.0", "find-process": "1.4.7", "lodash": "4.17.21", "minimatch": "9.0.5", @@ -78,7 +78,7 @@ "@types/diff": "^5.0.9", "@types/lodash": "^4.17.6", "@types/tar": "^6.1.11", - "@zowe/cli-test-utils": "8.2.0", + "@zowe/cli-test-utils": "8.7.0", "comment-json": "^4.2.3", "strip-ansi": "^6.0.1", "which": "^4.0.0" diff --git a/packages/cli/src/zosfiles/upload/ftu/FileToUSS.definition.ts b/packages/cli/src/zosfiles/upload/ftu/FileToUSS.definition.ts index 1cd07d4b82..ea80d10433 100644 --- a/packages/cli/src/zosfiles/upload/ftu/FileToUSS.definition.ts +++ b/packages/cli/src/zosfiles/upload/ftu/FileToUSS.definition.ts @@ -48,7 +48,8 @@ export const FileToUSSDefinition: ICommandDefinition = { ], options: [ UploadOptions.binary, - UploadOptions.encoding + UploadOptions.encoding, + UploadOptions.attributes ], examples: [ { diff --git a/packages/cli/src/zosfiles/upload/ftu/FileToUSS.handler.ts b/packages/cli/src/zosfiles/upload/ftu/FileToUSS.handler.ts index 2efe480004..57047f97c4 100644 --- a/packages/cli/src/zosfiles/upload/ftu/FileToUSS.handler.ts +++ b/packages/cli/src/zosfiles/upload/ftu/FileToUSS.handler.ts @@ -9,8 +9,14 @@ * */ -import { AbstractSession, IHandlerParameters, ITaskWithStatus, TaskStage, TextUtils } from "@zowe/imperative"; -import { Upload, IZosFilesResponse } from "@zowe/zos-files-for-zowe-sdk"; +import { + AbstractSession, + IHandlerParameters, + ITaskWithStatus, + TaskStage, + TextUtils, +} from "@zowe/imperative"; +import { Upload, IZosFilesResponse, IUploadOptions, ZosFilesAttributes } from "@zowe/zos-files-for-zowe-sdk"; import { ZosFilesBaseHandler } from "../../ZosFilesBase.handler"; /** @@ -18,22 +24,41 @@ import { ZosFilesBaseHandler } from "../../ZosFilesBase.handler"; * @export */ export default class FileToUSSHandler extends ZosFilesBaseHandler { - public async processWithSession(commandParameters: IHandlerParameters, - session: AbstractSession): Promise { + public async processWithSession( + commandParameters: IHandlerParameters, + session: AbstractSession + ): Promise { const task: ITaskWithStatus = { percentComplete: 0, statusMessage: "Uploading USS file", - stageName: TaskStage.IN_PROGRESS + stageName: TaskStage.IN_PROGRESS, }; commandParameters.response.progress.startBar({ task }); - const response = await Upload.fileToUssFile(session, commandParameters.arguments.inputfile, - commandParameters.arguments.USSFileName, { - binary: commandParameters.arguments.binary, - encoding: commandParameters.arguments.encoding, - task, - responseTimeout: commandParameters.arguments.responseTimeout - }); + const uploadOptions: IUploadOptions = { + binary: commandParameters.arguments.binary, + encoding: commandParameters.arguments.encoding, + maxConcurrentRequests: + commandParameters.arguments.maxConcurrentRequests, + task: task, + responseTimeout: commandParameters.arguments.responseTimeout, + includeHidden: commandParameters.arguments.includeHidden, + }; + + const attributes = ZosFilesAttributes.loadFromFile( + commandParameters.arguments.attributes, + commandParameters.arguments.inputDir + ); + if (attributes != null) { + uploadOptions.attributes = attributes; + } + + const response = await Upload.uploadFile( + session, + commandParameters.arguments.inputfile, + commandParameters.arguments.USSFileName, + uploadOptions + ); const formatMessage = TextUtils.prettyJson(response.apiResponse); commandParameters.response.console.log(formatMessage); diff --git a/packages/cli/src/zosjobs/download/download-output/Output.definition.ts b/packages/cli/src/zosjobs/download/download-output/Output.definition.ts index f8b1b773a7..75e5c89d42 100644 --- a/packages/cli/src/zosjobs/download/download-output/Output.definition.ts +++ b/packages/cli/src/zosjobs/download/download-output/Output.definition.ts @@ -73,7 +73,19 @@ export const OutputDefinition: ICommandDefinition = { "data conversion is performed using the file encoding specified.", type: "string", conflictsWith: ["binary", "record"] - } + }, + { + name: "wait-for-active", aliases: ["wfa"], + description: "Wait for the job to enter ACTIVE status before completing the command.", + type: "boolean", + conflictsWith: ["wait-for-output"] + }, + { + name: "wait-for-output", aliases: ["wfo"], + description: "Wait for the job to enter OUTPUT status before completing the command.", + type: "boolean", + conflictsWith: ["wait-for-active"] + }, ] as ICommandOptionDefinition[]), examples: [ { diff --git a/packages/cli/src/zosjobs/download/download-output/Output.handler.ts b/packages/cli/src/zosjobs/download/download-output/Output.handler.ts index dfca03141e..76725fbba0 100644 --- a/packages/cli/src/zosjobs/download/download-output/Output.handler.ts +++ b/packages/cli/src/zosjobs/download/download-output/Output.handler.ts @@ -35,6 +35,8 @@ export default class OutputHandler extends ZosmfBaseHandler { const binary: boolean = this.mArguments.binary; const record: boolean = this.mArguments.record; const encoding: string = this.mArguments.encoding; + const waitForActive: boolean = this.mArguments.waitForActive; + const waitForOutput: boolean = this.mArguments.waitForOutput; // Get the job details const job: IJob = await GetJobs.getJob(this.mSession, jobid); const options: IDownloadAllSpoolContentParms = { @@ -45,7 +47,9 @@ export default class OutputHandler extends ZosmfBaseHandler { extension, binary, record, - encoding + encoding, + waitForActive, + waitForOutput }; // Download 'em all await DownloadJobs.downloadAllSpoolContentCommon(this.mSession, options); diff --git a/packages/cli/src/zoslogs/list/logs/Logs.handler.ts b/packages/cli/src/zoslogs/list/logs/Logs.handler.ts index 4ff94a9b3c..66c4e504e3 100644 --- a/packages/cli/src/zoslogs/list/logs/Logs.handler.ts +++ b/packages/cli/src/zoslogs/list/logs/Logs.handler.ts @@ -22,7 +22,8 @@ export default class LogsHandler extends ZosmfBaseHandler { public async processCmd(commandParameters: IHandlerParameters) { const outputHeader = "Retrieved %s messages from %s, starts from %s, ends at %s. "; - let startTime = new Date().toISOString(); + let startTime: any; + const startDate = new Date().toISOString(); if (commandParameters.arguments.startTime !== undefined) { startTime = commandParameters.arguments.startTime; // in case the input is milliseconds format, which is a long number @@ -39,6 +40,7 @@ export default class LogsHandler extends ZosmfBaseHandler { try{ const resp: IZosLogType = await GetZosLog.getZosLog(this.mSession, zosLogParms); + if (startTime == null) { startTime = startDate; } const logItems: Array = resp.items; if (logItems === undefined || logItems.length === 0) { diff --git a/packages/cli/src/zostso/ZosTso.definition.ts b/packages/cli/src/zostso/ZosTso.definition.ts index 04e4ded6de..7700b4f1a7 100644 --- a/packages/cli/src/zostso/ZosTso.definition.ts +++ b/packages/cli/src/zostso/ZosTso.definition.ts @@ -16,6 +16,7 @@ import { StopCommand } from "./stop/Stop.definition"; import { PingCommand } from "./ping/Ping.definition"; import { IssueCommand } from "./issue/Issue.definition"; import { ZosmfSession } from "@zowe/zosmf-for-zowe-sdk"; +import { ReceiveCommand } from "./receive/Receive.definition"; export const definition: ICommandDefinition = { name: "zos-tso", @@ -24,7 +25,7 @@ export const definition: ICommandDefinition = { summary: "Interact with TSO", description: "Issue TSO commands and interact with TSO address spaces.", children: [ - SendCommand, StartCommand, PingCommand, StopCommand, IssueCommand + SendCommand, StartCommand, PingCommand, StopCommand, IssueCommand, ReceiveCommand ], passOn: [{ property: "options", diff --git a/packages/cli/src/zostso/receive/Receive.definition.ts b/packages/cli/src/zostso/receive/Receive.definition.ts new file mode 100644 index 0000000000..30c8e7e2fb --- /dev/null +++ b/packages/cli/src/zostso/receive/Receive.definition.ts @@ -0,0 +1,24 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { ICommandDefinition } from "@zowe/imperative"; +import { ReceiveASApp } from "./app/ReceiveASApp.definition"; + +export const ReceiveCommand: ICommandDefinition = { + name: "receive", + aliases: ["r"], + type: "group", + summary: "Receive message from TSO address space app", + description: "Receive message from TSO address space app", + children: [ + ReceiveASApp + ], +}; diff --git a/packages/cli/src/zostso/receive/app/ReceiveASApp.definition.ts b/packages/cli/src/zostso/receive/app/ReceiveASApp.definition.ts new file mode 100644 index 0000000000..8dafae9a58 --- /dev/null +++ b/packages/cli/src/zostso/receive/app/ReceiveASApp.definition.ts @@ -0,0 +1,58 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { ICommandDefinition, ICommandOptionDefinition } from "@zowe/imperative"; +import { TsoProfileConstants } from "@zowe/zos-tso-for-zowe-sdk"; + +export const ReceiveASApp: ICommandDefinition = { + name: "app", + aliases: ["a"], + summary: "Receive message from TSO address space app", + description: "Receive message from TSO address space app,", + type: "command", + handler: __dirname + "/ReceiveASApp.handler", + profile: { + optional: ["zosmf", "tso"] + }, + options: TsoProfileConstants.TSO_PROFILE_OPTIONS.concat([ + { + name: "app-key", + aliases: ["ak"], + description: "App key of application running at TSO address space, app key should be the value established when app instance was started", + type: "string", + required: true + }, + { + name: "servlet-key", + aliases: ["sk"], + description: "Servlet Key of TSO address space", + type: "string", + required: true + }, + { + name: "receive-until-ready", + aliases: ["rur"], + description: "Receives data until keyword is received or timeout", + type: "boolean", + required: false, + defaultValue: false + }, + { + name: "timeout", + aliases: ["t"], + description: "Timeout length in seconds, all data at the time of timeout will be returned to user", + type: "number", + required: false, + defaultValue: 600 + } + ] as ICommandOptionDefinition[]), + examples: [], +}; diff --git a/packages/cli/src/zostso/receive/app/ReceiveASApp.handler.ts b/packages/cli/src/zostso/receive/app/ReceiveASApp.handler.ts new file mode 100644 index 0000000000..03cd8e27fc --- /dev/null +++ b/packages/cli/src/zostso/receive/app/ReceiveASApp.handler.ts @@ -0,0 +1,50 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { IHandlerParameters } from "@zowe/imperative"; +import { ZosTsoBaseHandler, AddressSpaceApps } from "@zowe/zos-tso-for-zowe-sdk"; + +/** + * Handler to receive message from an app at an address space + * @export + * @class Handler + * @implements {ICommandHandler} + */ +export default class Handler extends ZosTsoBaseHandler { + // Process the command and produce the start response (returns servlet) + public async processCmd(commandParameters: IHandlerParameters) { + + commandParameters.response.progress.startSpinner("Receiving response..."); + + const response = await AddressSpaceApps.receive( + this.mSession, + this.mArguments.account, + { + appKey: commandParameters.arguments.appKey, + servletKey: commandParameters.arguments.servletKey, + receiveUntilReady: commandParameters.arguments.receiveUntilReady, + timeout: commandParameters.arguments.timeout + }, + ); + + commandParameters.response.progress.endSpinner(); + + commandParameters.response.console.log("\n"); + response.tsoData.forEach((data) => { + if(typeof data === 'string') { + commandParameters.response.console.log(data); + } else if (data && data.DATA) { + commandParameters.response.console.log(data.DATA); + } + }); + commandParameters.response.data.setObj(response); + } +} diff --git a/packages/cli/src/zostso/send/Send.definition.ts b/packages/cli/src/zostso/send/Send.definition.ts index c5997f6b9d..f521ab605c 100644 --- a/packages/cli/src/zostso/send/Send.definition.ts +++ b/packages/cli/src/zostso/send/Send.definition.ts @@ -13,6 +13,7 @@ import { ICommandDefinition } from "@zowe/imperative"; import { SendToAddressSpaceCommandDefinition } from "./address_space/SendToAddressSpace.definition"; +import { SendASApp } from "./as-app/SendASApp.definition"; export const SendCommand: ICommandDefinition = { name: "send", @@ -21,6 +22,6 @@ export const SendCommand: ICommandDefinition = { summary: "Send data to TSO", description: "Send data to TSO and collect responses until the prompt is reached.", children: [ - SendToAddressSpaceCommandDefinition + SendToAddressSpaceCommandDefinition, SendASApp ] }; diff --git a/packages/cli/src/zostso/send/as-app/SendASApp.definition.ts b/packages/cli/src/zostso/send/as-app/SendASApp.definition.ts new file mode 100644 index 0000000000..ef2d06c501 --- /dev/null +++ b/packages/cli/src/zostso/send/as-app/SendASApp.definition.ts @@ -0,0 +1,49 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { ICommandDefinition, ICommandOptionDefinition } from "@zowe/imperative"; +import { TsoProfileConstants } from "@zowe/zos-tso-for-zowe-sdk"; + +export const SendASApp: ICommandDefinition = { + name: "app", + aliases: ["a"], + summary: "Send message to an application at a TSO address space", + description: "Send message to an application at a TSO address space,", + type: "command", + handler: __dirname + "/SendASApp.handler", + profile: { + optional: ["zosmf", "tso"] + }, + options: TsoProfileConstants.TSO_PROFILE_OPTIONS.concat([ + { + name: "app-key", + aliases: ["ak"], + description: "App key of application running at TSO address space, app key should be the value established when app instance was started", + type: "string", + required: true + }, + { + name: "servlet-key", + aliases: ["sk"], + description: "Servlet Key of TSO address space", + type: "string", + required: true + }, + { + name: "message", + aliases: ["m"], + description: "Message payload to be sent to the TSO address space application", + type: "string", + required: true + } + ] as ICommandOptionDefinition[]), + examples: [], +}; diff --git a/packages/cli/src/zostso/send/as-app/SendASApp.handler.ts b/packages/cli/src/zostso/send/as-app/SendASApp.handler.ts new file mode 100644 index 0000000000..cb14bf80a9 --- /dev/null +++ b/packages/cli/src/zostso/send/as-app/SendASApp.handler.ts @@ -0,0 +1,44 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { IHandlerParameters } from "@zowe/imperative"; +import { ZosTsoBaseHandler, AddressSpaceApps } from "@zowe/zos-tso-for-zowe-sdk"; + +/** + * Handler to send a message to address space + * @export + * @class Handler + * @implements {ICommandHandler} + */ +export default class Handler extends ZosTsoBaseHandler { + // Process the command and transmit a message to an app running at a TSO address space + public async processCmd(commandParameters: IHandlerParameters) { + + commandParameters.response.progress.startSpinner("Sending request..."); + + const response = await AddressSpaceApps.send( + this.mSession, + this.mArguments.account, + { + appKey: commandParameters.arguments.appKey, + servletKey: commandParameters.arguments.servletKey, + message: commandParameters.arguments.message, + }, + this.mTsoStart + ); + + commandParameters.response.progress.endSpinner(); + + commandParameters.response.console.log( + JSON.stringify(response, null, 2) + ); + } +} diff --git a/packages/cli/src/zostso/start/Start.definition.ts b/packages/cli/src/zostso/start/Start.definition.ts index e8fac1f28f..2b8afbf44c 100644 --- a/packages/cli/src/zostso/start/Start.definition.ts +++ b/packages/cli/src/zostso/start/Start.definition.ts @@ -11,6 +11,7 @@ import { ICommandDefinition } from "@zowe/imperative"; import { AddressSpaceDefinition } from "./address-space/AddressSpace.definition"; +import { StartASApp } from "./as-app/StartASApp.definition"; export const StartCommand: ICommandDefinition = { name: "start", @@ -19,6 +20,6 @@ export const StartCommand: ICommandDefinition = { summary: "Start TSO/E address space", description: "Start TSO/E address space.", children: [ - AddressSpaceDefinition - ] + AddressSpaceDefinition, StartASApp + ], }; diff --git a/packages/cli/src/zostso/start/as-app/StartASApp.definition.ts b/packages/cli/src/zostso/start/as-app/StartASApp.definition.ts new file mode 100644 index 0000000000..efdf4946ac --- /dev/null +++ b/packages/cli/src/zostso/start/as-app/StartASApp.definition.ts @@ -0,0 +1,57 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { ICommandDefinition, ICommandOptionDefinition } from "@zowe/imperative"; +import { TsoProfileConstants } from "@zowe/zos-tso-for-zowe-sdk"; + +export const StartASApp: ICommandDefinition = { + name: "app", + aliases: ["a"], + summary: "Start application at TSO address space", + description: "Start application at TSO address space,", + type: "command", + handler: __dirname + "/StartASApp.handler", + profile: { + optional: ["zosmf", "tso"] + }, + options: TsoProfileConstants.TSO_PROFILE_OPTIONS.concat([ + { + name: "app-key", + aliases: ["ak"], + description: "App key of application running at TSO address space, " + + "this app key value should be referenced when sending or receiving messages from the application instance", + type: "string", + required: true + }, + { + name: "startup", + aliases: ["sc"], + description: "Command to be ran to start application at the TSO address space", + type: "string", + required: true + }, + { + name: "queue-id", + aliases: ["qi"], + description: "Queue ID of TSO address space", + type: "string", + implies: ["servlet-key"] + }, + { + name: "servlet-key", + aliases: ["sk"], + description: "Servlet Key of TSO address space", + type: "string", + implies: ["queue-id"] + } + ] as ICommandOptionDefinition[]), + examples: [], +}; diff --git a/packages/cli/src/zostso/start/as-app/StartASApp.handler.ts b/packages/cli/src/zostso/start/as-app/StartASApp.handler.ts new file mode 100644 index 0000000000..a5d4f4e541 --- /dev/null +++ b/packages/cli/src/zostso/start/as-app/StartASApp.handler.ts @@ -0,0 +1,37 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { IHandlerParameters } from "@zowe/imperative"; +import { ZosTsoBaseHandler, AddressSpaceApps } from "@zowe/zos-tso-for-zowe-sdk"; + +/** + * Handler to start app at an address space + * @export + * @class Handler + * @implements {ICommandHandler} + */ +export default class Handler extends ZosTsoBaseHandler { + // Process the command and produce the start response (returns servlet) + public async processCmd(commandParameters: IHandlerParameters) { + const response = await AddressSpaceApps.start( + this.mSession, + this.mArguments.account, + { + startupCommand: commandParameters.arguments.startup, + appKey: commandParameters.arguments.appKey, + servletKey: commandParameters.arguments.servletKey, + queueID: commandParameters.arguments.queueId, + }, + this.mTsoStart + ); + commandParameters.response.console.log(JSON.stringify(response,null,2)); + } +} diff --git a/packages/core/package.json b/packages/core/package.json index 271f62d453..ff642b9554 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/core-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "description": "Core libraries shared by Zowe SDK packages", "author": "Zowe", "license": "EPL-2.0", @@ -49,8 +49,8 @@ "string-width": "^4.2.3" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/imperative": "8.7.0" }, "peerDependencies": { "@zowe/imperative": "^8.0.0" diff --git a/packages/imperative/CHANGELOG.md b/packages/imperative/CHANGELOG.md index 635a745450..8c8b741884 100644 --- a/packages/imperative/CHANGELOG.md +++ b/packages/imperative/CHANGELOG.md @@ -6,6 +6,18 @@ All notable changes to the Imperative package will be documented in this file. - BugFix: Enabled commands with either the `--help` or `--version` flags to still display their information despite any config file issues [#2301](https://github.com/zowe/zowe-cli/pull/2301) +## `8.7.0` + +- Enhancement: Added optional `proxy` object to ISession interface for extenders to pass a ProxyVariables object that would override the environment variables if in place. [#2330](https://github.com/zowe/zowe-cli/pull/2330) + +## `8.6.1` + +- BugFix: Handled an HTTP 1.1 race condition where an SDK user may experience an ECONNRESET error if a session was reused on Node 20 and above due to HTTP Keep-Alive. [#2339](https://github.com/zowe/zowe-cli/pull/2339) + +## `8.3.1` + +- BugFix: Fixed an issue where the `plugins install` command could fail when installing a scoped package because scoped registry was used to fetch all dependencies. [#2317](https://github.com/zowe/zowe-cli/issues/2317) + ## `8.2.0` - Enhancement: Use the new SDK method `ConfigUtils.hasTokenExpired` to check whether a given JSON web token has expired. [#2298](https://github.com/zowe/zowe-cli/pull/2298) diff --git a/packages/imperative/__tests__/src/packages/imperative/plugins/suites/InstallingPlugins.ts b/packages/imperative/__tests__/src/packages/imperative/plugins/suites/InstallingPlugins.ts index c1e6c22702..4bc6abe53b 100644 --- a/packages/imperative/__tests__/src/packages/imperative/plugins/suites/InstallingPlugins.ts +++ b/packages/imperative/__tests__/src/packages/imperative/plugins/suites/InstallingPlugins.ts @@ -83,7 +83,7 @@ describe("Installing Plugins", () => { name: "override-plugin", usage: "override-plugin" }, - location: { + sample_registry: { location: "imperative-sample-plugin", name: "imperative-sample-plugin", usage: "sample-plugin" @@ -667,7 +667,7 @@ describe("Installing Plugins", () => { const savedPluginJson = readFileSync(pluginJsonLocation); const expectedContent: IPluginJson = fileContent as IPluginJson; - expectedContent[plugins.normal.name].location = envNpmRegistry; + expectedContent[plugins.normal.name].location = plugins.normal.location; expect(savedPluginJson).toEqual(expectedContent); }); @@ -735,7 +735,7 @@ describe("Installing Plugins", () => { const actualJson = readFileSync(pluginJsonLocation); // Add missing registry to expected - expectedJson[plugins.normal.name].location = envNpmRegistry; + expectedJson[plugins.normal.name].location = plugins.normal.location; // Add missing normal2 plugin not present in before each expectedJson[plugins.normal3.name] = { @@ -751,7 +751,7 @@ describe("Installing Plugins", () => { it("should error when a package and --file is specified", function () { expect( T.stripNewLines( - executeCommandString(this, `${pluginGroup} install ${plugins.location.location} --file ${testFile}`).stderr + executeCommandString(this, `${pluginGroup} install ${plugins.sample_registry.location} --file ${testFile}`).stderr ) ).toContain("Option --file can not be specified if positional package... is as well. They are mutually exclusive."); }); @@ -760,7 +760,7 @@ describe("Installing Plugins", () => { expect( T.stripNewLines( executeCommandString(this, - `${pluginGroup} install ${plugins.location.location} --file ${testFile} --registry ${TEST_REGISTRY}`).stderr + `${pluginGroup} install ${plugins.sample_registry.location} --file ${testFile} --registry ${TEST_REGISTRY}`).stderr ) ).toContain("The following options conflict (mutually exclusive)"); }); diff --git a/packages/imperative/package.json b/packages/imperative/package.json index ed2ed8fb69..be8a6d11cc 100644 --- a/packages/imperative/package.json +++ b/packages/imperative/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/imperative", - "version": "8.2.0", + "version": "8.7.0", "description": "framework for building configurable CLIs", "author": "Zowe", "license": "EPL-2.0", diff --git a/packages/imperative/src/cmd/__tests__/response/CommandResponse.unit.test.ts b/packages/imperative/src/cmd/__tests__/response/CommandResponse.unit.test.ts index 001d0861ed..894e26bd60 100644 --- a/packages/imperative/src/cmd/__tests__/response/CommandResponse.unit.test.ts +++ b/packages/imperative/src/cmd/__tests__/response/CommandResponse.unit.test.ts @@ -327,6 +327,17 @@ describe("Command Response", () => { expect((response.progress as any).mProgressBarInterval).not.toBeDefined(); }); + it("should start and stop a spinner", () => { + const response: CommandResponse = new CommandResponse({ + silent: false, + responseFormat: "default", + stream, + }); + (response.progress as any).startSpinner("Test Spinner...."); + expect((response as any).progress.spinnerInterval).toBeDefined(); + (response.progress as any).endSpinner("Done"); + expect((response as any).progress.spinnerInterval).toBe(null); + }); it("should allow us to create an instance", () => { let caughtError; try { @@ -414,7 +425,6 @@ describe("Command Response", () => { response.failed(); expect(response.buildJsonResponse()).toMatchSnapshot(); }); - it("should allow us to indicate that the command succeeded", () => { const response = new CommandResponse(); response.failed(); diff --git a/packages/imperative/src/cmd/src/doc/ICommandDefinition.ts b/packages/imperative/src/cmd/src/doc/ICommandDefinition.ts index 3d8294934d..8a750b165d 100644 --- a/packages/imperative/src/cmd/src/doc/ICommandDefinition.ts +++ b/packages/imperative/src/cmd/src/doc/ICommandDefinition.ts @@ -14,7 +14,7 @@ import { IChainedHandlerEntry } from "./handler/IChainedHandlerEntry"; import { ICommandOptionDefinition } from "./option/ICommandOptionDefinition"; import { ICommandPositionalDefinition } from "./option/ICommandPositionalDefinition"; import { ICommandDefinitionPassOn } from "./ICommandDefinitionPassOn"; -import { ICommandProfile } from "../../src/doc/profiles/definition/ICommandProfile"; +import { ICommandProfile } from "../doc/profiles/definition/ICommandProfile"; /** * Command Segment type - either "group" or "command". * diff --git a/packages/imperative/src/cmd/src/doc/handler/ICommandHandlerResponseValidator.ts b/packages/imperative/src/cmd/src/doc/handler/ICommandHandlerResponseValidator.ts index 6ec47d051c..581bef3595 100644 --- a/packages/imperative/src/cmd/src/doc/handler/ICommandHandlerResponseValidator.ts +++ b/packages/imperative/src/cmd/src/doc/handler/ICommandHandlerResponseValidator.ts @@ -9,7 +9,7 @@ * */ -import { ICommandResponse } from "../../../src/doc/response/response/ICommandResponse"; +import { ICommandResponse } from "../../doc/response/response/ICommandResponse"; import { CommandResponse } from "../../response/CommandResponse"; export interface ICommandHandlerResponseValidator { diff --git a/packages/imperative/src/cmd/src/doc/response/api/handler/IHandlerFormatOutputApi.ts b/packages/imperative/src/cmd/src/doc/response/api/handler/IHandlerFormatOutputApi.ts index f52555ee6d..4ab25a36e9 100644 --- a/packages/imperative/src/cmd/src/doc/response/api/handler/IHandlerFormatOutputApi.ts +++ b/packages/imperative/src/cmd/src/doc/response/api/handler/IHandlerFormatOutputApi.ts @@ -9,7 +9,7 @@ * */ -import { ICommandOutputFormat } from "../../../../../src/doc/response/response/ICommandOutputFormat"; +import { ICommandOutputFormat } from "../../../../doc/response/response/ICommandOutputFormat"; export interface IHandlerFormatOutputApi { output: (format: ICommandOutputFormat) => void; diff --git a/packages/imperative/src/cmd/src/doc/response/api/handler/IHandlerProgressApi.ts b/packages/imperative/src/cmd/src/doc/response/api/handler/IHandlerProgressApi.ts index 3aeab96d61..9b728b0c53 100644 --- a/packages/imperative/src/cmd/src/doc/response/api/handler/IHandlerProgressApi.ts +++ b/packages/imperative/src/cmd/src/doc/response/api/handler/IHandlerProgressApi.ts @@ -29,4 +29,16 @@ export interface IHandlerProgressApi { * @memberof IHandlerProgressApi */ endBar(): void; + /** + * Start a spinner - displays on the users terminal + * @param {IProgressBarParms} params + * @memberof IHandlerProgressApi + */ + startSpinner?(pendingText: string): void; + /** + * Stop a spinner - displays on the users terminal + * @param {IProgressBarParms} params + * @memberof IHandlerProgressApi + */ + endSpinner?(endText?: string): void; } diff --git a/packages/imperative/src/cmd/src/help/DefaultHelpGenerator.ts b/packages/imperative/src/cmd/src/help/DefaultHelpGenerator.ts index 59387adb23..19d1dd2e1a 100644 --- a/packages/imperative/src/cmd/src/help/DefaultHelpGenerator.ts +++ b/packages/imperative/src/cmd/src/help/DefaultHelpGenerator.ts @@ -17,7 +17,7 @@ import { CommandUtils } from "../utils/CommandUtils"; import { ImperativeError } from "../../../error"; import { IHelpGeneratorParms } from "./doc/IHelpGeneratorParms"; import { IHelpGeneratorFactoryParms } from "./doc/IHelpGeneratorFactoryParms"; -import { compareCommands, ICommandDefinition } from "../../src/doc/ICommandDefinition"; +import { compareCommands, ICommandDefinition } from "../doc/ICommandDefinition"; import stripAnsi = require("strip-ansi"); import { CliUtils } from "../../../utilities/src/CliUtils"; diff --git a/packages/imperative/src/cmd/src/response/CommandResponse.ts b/packages/imperative/src/cmd/src/response/CommandResponse.ts index d35af49898..62c44037ef 100644 --- a/packages/imperative/src/cmd/src/response/CommandResponse.ts +++ b/packages/imperative/src/cmd/src/response/CommandResponse.ts @@ -24,7 +24,7 @@ import { ImperativeExpect } from "../../../expect"; import { IHandlerFormatOutputApi } from "../doc/response/api/handler/IHandlerFormatOutputApi"; import { ICommandOutputFormat, OUTPUT_FORMAT } from "../doc/response/response/ICommandOutputFormat"; import { Arguments } from "yargs"; -import { ICommandDefinition } from "../../src/doc/ICommandDefinition"; +import { ICommandDefinition } from "../doc/ICommandDefinition"; import { OptionConstants } from "../constants/OptionConstants"; import { inspect } from "util"; import * as DeepMerge from "deepmerge"; @@ -698,6 +698,32 @@ export class CommandResponse implements ICommandResponseApi { // Create an instance of the class this.mProgressApi = new class { + private spinnerIndex = 0; + private spinnerInterval: any; + + /** + * Start a spinner + */ + public startSpinner(pendingText: string): void { + if (this.spinnerInterval == null) { + this.spinnerInterval = setInterval(() => { + outer.writeStdout(`\r${pendingText} ${this.mProgressBarSpinnerChars[this.spinnerIndex]}`); + this.spinnerIndex = (this.spinnerIndex + 1) % this.mProgressBarSpinnerChars.length; + }, 100); // eslint-disable-line + } + } + /** + * Stop a spinner + */ + public endSpinner(stopText?: string): void { + if (this.spinnerInterval != null) { + clearInterval(this.spinnerInterval); + this.spinnerInterval = null; + if(stopText) outer.writeStdout(`\r${stopText}\n`); + outer.writeStdout("\r\x1b[K"); + } + } + private mProgressBarSpinnerIndex = 0; private mProgressTask: ITaskWithStatus; private mProgressBarPollFrequency = 65; // eslint-disable-line @typescript-eslint/no-magic-numbers diff --git a/packages/imperative/src/cmd/src/yargs/AbstractCommandYargs.ts b/packages/imperative/src/cmd/src/yargs/AbstractCommandYargs.ts index a179f2786b..d0a5381066 100644 --- a/packages/imperative/src/cmd/src/yargs/AbstractCommandYargs.ts +++ b/packages/imperative/src/cmd/src/yargs/AbstractCommandYargs.ts @@ -21,7 +21,7 @@ import { ImperativeYargsCommandAction, IYargsResponse } from "./doc/IYargsRespon import { GroupCommandYargs } from "./GroupCommandYargs"; import { IHelpGeneratorFactory } from "../help/doc/IHelpGeneratorFactory"; import { CommandResponse } from "../response/CommandResponse"; -import { ICommandResponse } from "../../src/doc/response/response/ICommandResponse"; +import { ICommandResponse } from "../doc/response/response/ICommandResponse"; import { ICommandExampleDefinition } from "../.."; import { ImperativeConfig } from "../../../utilities/src/ImperativeConfig"; diff --git a/packages/imperative/src/cmd/src/yargs/CommandYargs.ts b/packages/imperative/src/cmd/src/yargs/CommandYargs.ts index 637e8389d3..3b22542ec0 100644 --- a/packages/imperative/src/cmd/src/yargs/CommandYargs.ts +++ b/packages/imperative/src/cmd/src/yargs/CommandYargs.ts @@ -14,11 +14,11 @@ import { inspect } from "util"; import { Constants } from "../../../constants"; import { IYargsResponse } from "./doc/IYargsResponse"; import { AbstractCommandYargs, YargsCommandCompleted } from "./AbstractCommandYargs"; -import { ICommandOptionDefinition } from "../../src/doc/option/ICommandOptionDefinition"; +import { ICommandOptionDefinition } from "../doc/option/ICommandOptionDefinition"; import { ICommandDefinition } from "../doc/ICommandDefinition"; import { CommandProcessor } from "../CommandProcessor"; -import { ICommandResponse } from "../../src/doc/response/response/ICommandResponse"; -import { CommandResponse } from "../../src/response/CommandResponse"; +import { ICommandResponse } from "../doc/response/response/ICommandResponse"; +import { CommandResponse } from "../response/CommandResponse"; import { ImperativeConfig } from "../../../utilities"; /** diff --git a/packages/imperative/src/cmd/src/yargs/doc/IYargsResponse.ts b/packages/imperative/src/cmd/src/yargs/doc/IYargsResponse.ts index 6d3337007f..97c5b922b5 100644 --- a/packages/imperative/src/cmd/src/yargs/doc/IYargsResponse.ts +++ b/packages/imperative/src/cmd/src/yargs/doc/IYargsResponse.ts @@ -9,7 +9,7 @@ * */ -import { ICommandResponse } from "../../../src/doc/response/response/ICommandResponse"; +import { ICommandResponse } from "../../doc/response/response/ICommandResponse"; /** * Indicates the action performed. */ diff --git a/packages/imperative/src/config/src/ConfigUtils.ts b/packages/imperative/src/config/src/ConfigUtils.ts index 87705b1bb5..8128b99032 100644 --- a/packages/imperative/src/config/src/ConfigUtils.ts +++ b/packages/imperative/src/config/src/ConfigUtils.ts @@ -300,8 +300,8 @@ export class ConfigUtils { * Checks if the given token has expired. Supports JSON web tokens only. * * @param {string} token - The JSON web token to check - * @returns {boolean} Whether the token has expired. Returns `false` if the token cannot be decoded or an expire time is - * not specified in the payload. + * @returns {boolean} Whether the token has expired. + * Returns `false` if the token cannot be decoded or an expire time is not specified in the payload. */ public static hasTokenExpired(token: string): boolean { // JWT format: [header].[payload].[signature] diff --git a/packages/imperative/src/imperative/__tests__/config/cmd/secure/secure.handler.unit.test.ts b/packages/imperative/src/imperative/__tests__/config/cmd/secure/secure.handler.unit.test.ts index 1a3b368fb7..3228c6089e 100644 --- a/packages/imperative/src/imperative/__tests__/config/cmd/secure/secure.handler.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/config/cmd/secure/secure.handler.unit.test.ts @@ -16,8 +16,12 @@ import { ImperativeConfig } from "../../../../../utilities"; import { IImperativeConfig } from "../../../../src/doc/IImperativeConfig"; import { ICredentialManagerInit } from "../../../../../security/src/doc/ICredentialManagerInit"; import { CredentialManagerFactory } from "../../../../../security"; -import { expectedGlobalConfigObject, expectedGlobalUserConfigObject, expectedProjectConfigObject, expectedProjectUserConfigObject } from - "../../../../../../__tests__/__integration__/imperative/__tests__/__integration__/cli/config/__resources__/expectedObjects"; +import { + expectedGlobalConfigObject, + expectedGlobalUserConfigObject, + expectedProjectConfigObject, + expectedProjectUserConfigObject, +} from "../../../../../../__tests__/__integration__/imperative/__tests__/__integration__/cli/config/__resources__/expectedObjects"; import SecureHandler from "../../../../src/config/cmd/secure/secure.handler"; import * as config from "../../../../../../__tests__/__integration__/imperative/src/imperative"; import { keyring as keytar } from "@zowe/secrets-for-zowe-sdk"; @@ -39,7 +43,7 @@ const getIHandlerParametersObject = (): IHandlerParameters => { }), setObj: jest.fn((setObjArgs) => { // Nothing - }) + }), }, console: { log: jest.fn((logs) => { @@ -49,11 +53,11 @@ const getIHandlerParametersObject = (): IHandlerParameters => { // Nothing }), errorHeader: jest.fn(() => undefined), - prompt: readPromptSpy - } + prompt: readPromptSpy, + }, }, arguments: {}, - definition: {} + definition: {}, }; return x as IHandlerParameters; }; @@ -61,7 +65,7 @@ const getIHandlerParametersObject = (): IHandlerParameters => { const credentialManager: ICredentialManagerInit = { service: "Zowe", displayName: "imperativeTestCredentialManager", - invalidOnFailure: false + invalidOnFailure: false, }; const fakeConfig = config as IImperativeConfig; @@ -69,16 +73,32 @@ const fakeProjPath = path.join(__dirname, "fakeapp.config.json"); const fakeSchemaPath = path.join(__dirname, "fakeapp.schema.json"); const fakeProjUserPath = path.join(__dirname, "fakeapp.config.user.json"); const fakeGblProjPath = path.join(__dirname, ".fakeapp", "fakeapp.config.json"); -const fakeGblSchemaPath = path.join(__dirname, ".fakeapp", "fakeapp.schema.json"); -const fakeGblProjUserPath = path.join(__dirname, ".fakeapp", "fakeapp.config.user.json"); +const fakeGblSchemaPath = path.join( + __dirname, + ".fakeapp", + "fakeapp.schema.json" +); +const fakeGblProjUserPath = path.join( + __dirname, + ".fakeapp", + "fakeapp.config.user.json" +); const fakeUnrelatedPath = path.join(__dirname, "fakeapp.unrelated.config.json"); const fakeSecureDataJson: any = {}; -fakeSecureDataJson[fakeProjPath] = {"profiles.project_base.properties.secure": "fakeSecureValue"}; -fakeSecureDataJson[fakeGblProjPath] = {"profiles.global_base.properties.secure": "fakeSecureValue"}; -fakeSecureDataJson[fakeUnrelatedPath] = {"profiles.project_base.properties.secure": "anotherFakeSecureValue"}; +fakeSecureDataJson[fakeProjPath] = { + "profiles.project_base.properties.secure": "fakeSecureValue", +}; +fakeSecureDataJson[fakeGblProjPath] = { + "profiles.global_base.properties.secure": "fakeSecureValue", +}; +fakeSecureDataJson[fakeUnrelatedPath] = { + "profiles.project_base.properties.secure": "anotherFakeSecureValue", +}; -const fakeSecureData = Buffer.from(JSON.stringify(fakeSecureDataJson)).toString("base64"); +const fakeSecureData = Buffer.from(JSON.stringify(fakeSecureDataJson)).toString( + "base64" +); describe("Configuration Secure command handler", () => { let readFileSyncSpy: any; @@ -98,11 +118,11 @@ describe("Configuration Secure command handler", () => { }, save: (k: string, v: any): Promise => { return CredentialManagerFactory.manager.save(k, v); - } - } + }, + }, }; - beforeAll( async() => { + beforeAll(async () => { keytarGetPasswordSpy = jest.spyOn(keytar, "getPassword"); keytarSetPasswordSpy = jest.spyOn(keytar, "setPassword"); keytarDeletePasswordSpy = jest.spyOn(keytar, "deletePassword"); @@ -116,7 +136,7 @@ describe("Configuration Secure command handler", () => { await CredentialManagerFactory.initialize(credentialManager); // Prepare config setup }); - beforeEach( async () => { + beforeEach(async () => { ImperativeConfig.instance.loadedConfig = lodash.cloneDeep(fakeConfig); searchSpy = jest.spyOn(Config, "search"); @@ -126,10 +146,12 @@ describe("Configuration Secure command handler", () => { readPromptSpy.mockClear(); jest.spyOn(EventUtils, "validateAppName").mockImplementation(jest.fn()); - jest.spyOn(EventOperator, "getZoweProcessor").mockReturnValue({emitZoweEvent: jest.fn()} as any); + jest.spyOn(EventOperator, "getZoweProcessor").mockReturnValue({ + emitZoweEvent: jest.fn(), + } as any); }); - afterEach( () => { + afterEach(() => { jest.restoreAllMocks(); }); @@ -153,9 +175,14 @@ describe("Configuration Secure command handler", () => { eco.$schema = "./fakeapp.schema.json"; readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); - existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(true).mockReturnValue(false); // Only the project config exists + existsSyncSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + .mockReturnValue(false); // Only the project config exists writeFileSyncSpy.mockImplementation(); - searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath); // Give search something to return + searchSpy + .mockReturnValueOnce(fakeProjUserPath) + .mockReturnValueOnce(fakeProjPath); // Give search something to return await setupConfigToLoad(undefined, configOpts); // Setup the config @@ -165,17 +192,25 @@ describe("Configuration Secure command handler", () => { existsSyncSpy.mockClear(); readFileSyncSpy.mockClear(); - setSchemaSpy = jest.spyOn(ImperativeConfig.instance.config, "setSchema"); - (params.response.console as any).prompt = jest.fn(() => "fakePromptingData"); + setSchemaSpy = jest.spyOn( + ImperativeConfig.instance.config, + "setSchema" + ); + (params.response.console as any).prompt = jest.fn( + () => "fakePromptingData" + ); await handler.process(params); - const fakeSecureDataExpectedJson: { [key: string]: any} = lodash.cloneDeep(fakeSecureDataJson); + const fakeSecureDataExpectedJson: { [key: string]: any } = + lodash.cloneDeep(fakeSecureDataJson); delete fakeSecureDataExpectedJson[fakeProjPath]; fakeSecureDataExpectedJson[fakeProjPath] = { - "profiles.project_base.properties.secret": "fakePromptingData" + "profiles.project_base.properties.secret": "fakePromptingData", }; - const fakeSecureDataExpected = Buffer.from(JSON.stringify(fakeSecureDataExpectedJson)).toString("base64"); + const fakeSecureDataExpected = Buffer.from( + JSON.stringify(fakeSecureDataExpectedJson) + ).toString("base64"); const compObj: any = {}; // Make changes to satisfy what would be stored on the JSON @@ -190,9 +225,17 @@ describe("Configuration Secure command handler", () => { } expect(keytarGetPasswordSpy).toHaveBeenCalledTimes(1); expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(1); - expect(keytarSetPasswordSpy).toHaveBeenCalledWith("Zowe", "secure_config_props", fakeSecureDataExpected); + expect(keytarSetPasswordSpy).toHaveBeenCalledWith( + "Zowe", + "secure_config_props", + fakeSecureDataExpected + ); expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); - expect(writeFileSyncSpy).toHaveBeenNthCalledWith(1, fakeProjPath, JSON.stringify(compObj, null, 4)); // Config + expect(writeFileSyncSpy).toHaveBeenNthCalledWith( + 1, + fakeProjPath, + JSON.stringify(compObj, null, 4) + ); // Config }); it("should attempt to secure the project user configuration", async () => { @@ -217,7 +260,9 @@ describe("Configuration Secure command handler", () => { readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); existsSyncSpy.mockReturnValueOnce(true).mockReturnValue(false); // Only the user config exists writeFileSyncSpy.mockImplementation(); - searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath); // Give search something to return + searchSpy + .mockReturnValueOnce(fakeProjUserPath) + .mockReturnValueOnce(fakeProjPath); // Give search something to return await setupConfigToLoad(undefined, configOpts); // Setup the config @@ -227,15 +272,21 @@ describe("Configuration Secure command handler", () => { existsSyncSpy.mockClear(); readFileSyncSpy.mockClear(); - setSchemaSpy = jest.spyOn(ImperativeConfig.instance.config, "setSchema"); + setSchemaSpy = jest.spyOn( + ImperativeConfig.instance.config, + "setSchema" + ); await handler.process(params); - const fakeSecureDataExpectedJson: { [key: string]: any} = lodash.cloneDeep(fakeSecureDataJson); + const fakeSecureDataExpectedJson: { [key: string]: any } = + lodash.cloneDeep(fakeSecureDataJson); fakeSecureDataExpectedJson[fakeProjUserPath] = { - "profiles.project_base.properties.secret": "fakePromptingData" + "profiles.project_base.properties.secret": "fakePromptingData", }; - const fakeSecureDataExpected = Buffer.from(JSON.stringify(fakeSecureDataExpectedJson)).toString("base64"); + const fakeSecureDataExpected = Buffer.from( + JSON.stringify(fakeSecureDataExpectedJson) + ).toString("base64"); const compObj: any = {}; // Make changes to satisfy what would be stored on the JSON @@ -250,9 +301,17 @@ describe("Configuration Secure command handler", () => { } expect(keytarGetPasswordSpy).toHaveBeenCalledTimes(1); expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(1); - expect(keytarSetPasswordSpy).toHaveBeenCalledWith("Zowe", "secure_config_props", fakeSecureDataExpected); + expect(keytarSetPasswordSpy).toHaveBeenCalledWith( + "Zowe", + "secure_config_props", + fakeSecureDataExpected + ); expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); - expect(writeFileSyncSpy).toHaveBeenNthCalledWith(1, fakeProjUserPath, JSON.stringify(compObj, null, 4)); // Config + expect(writeFileSyncSpy).toHaveBeenNthCalledWith( + 1, + fakeProjUserPath, + JSON.stringify(compObj, null, 4) + ); // Config }); it("should attempt to secure the global configuration", async () => { @@ -275,10 +334,16 @@ describe("Configuration Secure command handler", () => { eco.$schema = "./fakeapp.schema.json"; readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); - existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(false).mockReturnValueOnce(false) - .mockReturnValueOnce(true).mockReturnValue(false); // Only the global config exists + existsSyncSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(false) + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + .mockReturnValue(false); // Only the global config exists writeFileSyncSpy.mockImplementation(); - searchSpy.mockReturnValueOnce(fakeGblProjUserPath).mockReturnValueOnce(fakeGblProjPath); // Give search something to return + searchSpy + .mockReturnValueOnce(fakeGblProjUserPath) + .mockReturnValueOnce(fakeGblProjPath); // Give search something to return await setupConfigToLoad(undefined, configOpts); // Setup the config @@ -288,16 +353,22 @@ describe("Configuration Secure command handler", () => { existsSyncSpy.mockClear(); readFileSyncSpy.mockClear(); - setSchemaSpy = jest.spyOn(ImperativeConfig.instance.config, "setSchema"); + setSchemaSpy = jest.spyOn( + ImperativeConfig.instance.config, + "setSchema" + ); await handler.process(params); - const fakeSecureDataExpectedJson: { [key: string]: any} = lodash.cloneDeep(fakeSecureDataJson); + const fakeSecureDataExpectedJson: { [key: string]: any } = + lodash.cloneDeep(fakeSecureDataJson); delete fakeSecureDataExpectedJson[fakeGblProjPath]; fakeSecureDataExpectedJson[fakeGblProjPath] = { - "profiles.global_base.properties.secret": "fakePromptingData" + "profiles.global_base.properties.secret": "fakePromptingData", }; - const fakeSecureDataExpected = Buffer.from(JSON.stringify(fakeSecureDataExpectedJson)).toString("base64"); + const fakeSecureDataExpected = Buffer.from( + JSON.stringify(fakeSecureDataExpectedJson) + ).toString("base64"); const compObj: any = {}; // Make changes to satisfy what would be stored on the JSON @@ -312,9 +383,17 @@ describe("Configuration Secure command handler", () => { } expect(keytarGetPasswordSpy).toHaveBeenCalledTimes(1); expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(1); - expect(keytarSetPasswordSpy).toHaveBeenCalledWith("Zowe", "secure_config_props", fakeSecureDataExpected); + expect(keytarSetPasswordSpy).toHaveBeenCalledWith( + "Zowe", + "secure_config_props", + fakeSecureDataExpected + ); expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); - expect(writeFileSyncSpy).toHaveBeenNthCalledWith(1, fakeGblProjPath, JSON.stringify(compObj, null, 4)); // Config + expect(writeFileSyncSpy).toHaveBeenNthCalledWith( + 1, + fakeGblProjPath, + JSON.stringify(compObj, null, 4) + ); // Config }); it("should attempt to secure the global user configuration", async () => { @@ -337,10 +416,15 @@ describe("Configuration Secure command handler", () => { eco.$schema = "./fakeapp.schema.json"; readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); - existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(false).mockReturnValueOnce(true) + existsSyncSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) .mockReturnValue(false); // Only the global user config exists writeFileSyncSpy.mockImplementation(); - searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath); // Give search something to return + searchSpy + .mockReturnValueOnce(fakeProjUserPath) + .mockReturnValueOnce(fakeProjPath); // Give search something to return await setupConfigToLoad(undefined, configOpts); // Setup the config @@ -350,16 +434,22 @@ describe("Configuration Secure command handler", () => { existsSyncSpy.mockClear(); readFileSyncSpy.mockClear(); - setSchemaSpy = jest.spyOn(ImperativeConfig.instance.config, "setSchema"); + setSchemaSpy = jest.spyOn( + ImperativeConfig.instance.config, + "setSchema" + ); await handler.process(params); - const fakeSecureDataExpectedJson: { [key: string]: any} = lodash.cloneDeep(fakeSecureDataJson); + const fakeSecureDataExpectedJson: { [key: string]: any } = + lodash.cloneDeep(fakeSecureDataJson); delete fakeSecureDataExpectedJson[fakeGblProjUserPath]; fakeSecureDataExpectedJson[fakeGblProjUserPath] = { - "profiles.global_base.properties.secret": "fakePromptingData" + "profiles.global_base.properties.secret": "fakePromptingData", }; - const fakeSecureDataExpected = Buffer.from(JSON.stringify(fakeSecureDataExpectedJson)).toString("base64"); + const fakeSecureDataExpected = Buffer.from( + JSON.stringify(fakeSecureDataExpectedJson) + ).toString("base64"); const compObj: any = {}; // Make changes to satisfy what would be stored on the JSON @@ -374,9 +464,17 @@ describe("Configuration Secure command handler", () => { } expect(keytarGetPasswordSpy).toHaveBeenCalledTimes(1); expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(1); - expect(keytarSetPasswordSpy).toHaveBeenCalledWith("Zowe", "secure_config_props", fakeSecureDataExpected); + expect(keytarSetPasswordSpy).toHaveBeenCalledWith( + "Zowe", + "secure_config_props", + fakeSecureDataExpected + ); expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); - expect(writeFileSyncSpy).toHaveBeenNthCalledWith(1, fakeGblProjUserPath, JSON.stringify(compObj, null, 4)); // Config + expect(writeFileSyncSpy).toHaveBeenNthCalledWith( + 1, + fakeGblProjUserPath, + JSON.stringify(compObj, null, 4) + ); // Config }); it("should fail to secure the project configuration if there is no project configuration", async () => { @@ -399,10 +497,15 @@ describe("Configuration Secure command handler", () => { eco.$schema = "./fakeapp.schema.json"; readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); - existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(false).mockReturnValueOnce(true) + existsSyncSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) .mockReturnValue(false); // Only the global user config exists writeFileSyncSpy.mockImplementation(); - searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath); // Give search something to return + searchSpy + .mockReturnValueOnce(fakeProjUserPath) + .mockReturnValueOnce(fakeProjPath); // Give search something to return await setupConfigToLoad(undefined, configOpts); // Setup the config @@ -412,7 +515,10 @@ describe("Configuration Secure command handler", () => { existsSyncSpy.mockClear(); readFileSyncSpy.mockClear(); - setSchemaSpy = jest.spyOn(ImperativeConfig.instance.config, "setSchema"); + setSchemaSpy = jest.spyOn( + ImperativeConfig.instance.config, + "setSchema" + ); await handler.process(params); @@ -420,7 +526,9 @@ describe("Configuration Secure command handler", () => { expect(keytarGetPasswordSpy).toHaveBeenCalledTimes(1); expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(0); expect(writeFileSyncSpy).toHaveBeenCalledTimes(0); - expect(ImperativeConfig.instance.config.api.secure.secureFields().length).toEqual(0); + expect( + ImperativeConfig.instance.config.api.secure.secureFields().length + ).toEqual(0); }); it("should secure the project configuration and prune unused properties", async () => { @@ -444,9 +552,14 @@ describe("Configuration Secure command handler", () => { eco.$schema = "./fakeapp.schema.json"; readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); - existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(true).mockReturnValue(false); // Only the project config exists + existsSyncSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + .mockReturnValue(false); // Only the project config exists writeFileSyncSpy.mockImplementation(); - searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath); // Give search something to return + searchSpy + .mockReturnValueOnce(fakeProjUserPath) + .mockReturnValueOnce(fakeProjPath); // Give search something to return await setupConfigToLoad(undefined, configOpts); // Setup the config @@ -456,16 +569,21 @@ describe("Configuration Secure command handler", () => { existsSyncSpy.mockClear(); readFileSyncSpy.mockClear(); - setSchemaSpy = jest.spyOn(ImperativeConfig.instance.config, "setSchema"); + setSchemaSpy = jest.spyOn( + ImperativeConfig.instance.config, + "setSchema" + ); await handler.process(params); - const fakeSecureDataExpectedJson: { [key: string]: any} = { + const fakeSecureDataExpectedJson: { [key: string]: any } = { [fakeProjPath]: { - "profiles.project_base.properties.secret": "fakePromptingData" - } + "profiles.project_base.properties.secret": "fakePromptingData", + }, }; - const fakeSecureDataExpected = Buffer.from(JSON.stringify(fakeSecureDataExpectedJson)).toString("base64"); + const fakeSecureDataExpected = Buffer.from( + JSON.stringify(fakeSecureDataExpectedJson) + ).toString("base64"); const compObj: any = {}; // Make changes to satisfy what would be stored on the JSON @@ -480,9 +598,17 @@ describe("Configuration Secure command handler", () => { } expect(keytarGetPasswordSpy).toHaveBeenCalledTimes(1); expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(2); - expect(keytarSetPasswordSpy).toHaveBeenCalledWith("Zowe", "secure_config_props", fakeSecureDataExpected); + expect(keytarSetPasswordSpy).toHaveBeenCalledWith( + "Zowe", + "secure_config_props", + fakeSecureDataExpected + ); expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); - expect(writeFileSyncSpy).toHaveBeenNthCalledWith(1, fakeProjPath, JSON.stringify(compObj, null, 4)); // Config + expect(writeFileSyncSpy).toHaveBeenNthCalledWith( + 1, + fakeProjPath, + JSON.stringify(compObj, null, 4) + ); // Config }); describe("special prompting for auth token", () => { @@ -491,11 +617,9 @@ describe("Configuration Secure command handler", () => { properties: { host: "example.com", port: 443, - tokenType: SessConstants.TOKEN_TYPE_JWT + tokenType: SessConstants.TOKEN_TYPE_JWT, }, - secure: [ - "tokenValue" - ] + secure: ["tokenValue"], }; const expectedProjConfigObjectWithToken: IConfig = { @@ -504,31 +628,39 @@ describe("Configuration Secure command handler", () => { project_base: baseProfile, }, defaults: { - base: "project_base" - } + base: "project_base", + }, }; - const authHandlerPath = __dirname + "/../../../../src/auth/handlers/AbstractAuthHandler"; + const authHandlerPath = + __dirname + "/../../../../src/auth/handlers/AbstractAuthHandler"; const handler = new SecureHandler(); const params = getIHandlerParametersObject(); let mockAuthHandlerApi: any; beforeAll(() => { mockAuthHandlerApi = { - promptParams: { defaultTokenType: SessConstants.TOKEN_TYPE_JWT }, - createSessCfg: jest.fn(x => x), - sessionLogin: jest.fn().mockResolvedValue("fakeLoginData") + promptParams: { + defaultTokenType: SessConstants.TOKEN_TYPE_JWT, + }, + createSessCfg: jest.fn((x) => x), + sessionLogin: jest.fn().mockResolvedValue("fakeLoginData"), }; jest.doMock(authHandlerPath, () => { - const { AbstractAuthHandler } = jest.requireActual(authHandlerPath); + const { AbstractAuthHandler } = + jest.requireActual(authHandlerPath); return { default: jest.fn(() => { - const handler = Object.create(AbstractAuthHandler.prototype); + const handler = Object.create( + AbstractAuthHandler.prototype + ); return Object.assign(handler, { - getAuthHandlerApi: jest.fn().mockReturnValue(mockAuthHandlerApi) + getAuthHandlerApi: jest + .fn() + .mockReturnValue(mockAuthHandlerApi), }); - }) + }), }; }); }); @@ -559,50 +691,76 @@ describe("Configuration Secure command handler", () => { // Create another base profile and mock the loggers to test multiple login operations in a single config-secure eco.profiles["base2"] = baseProfile; - const dummyLogger: any = {debug: jest.fn(), info: jest.fn()}; - jest.spyOn(Logger, "getImperativeLogger").mockReturnValue(dummyLogger); + const dummyLogger: any = { debug: jest.fn(), info: jest.fn() }; + jest.spyOn(Logger, "getImperativeLogger").mockReturnValue( + dummyLogger + ); jest.spyOn(Logger, "getAppLogger").mockReturnValue(dummyLogger); readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); - existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(true).mockReturnValue(false); // Only the project config exists + existsSyncSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + .mockReturnValue(false); // Only the project config exists writeFileSyncSpy.mockImplementation(); - searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath); // Give search something to return + searchSpy + .mockReturnValueOnce(fakeProjUserPath) + .mockReturnValueOnce(fakeProjPath); // Give search something to return await setupConfigToLoad(undefined, configOpts); // Setup the config - jest.spyOn(ImperativeConfig.instance, "loadedConfig", "get").mockReturnValue({ + jest.spyOn( + ImperativeConfig.instance, + "loadedConfig", + "get" + ).mockReturnValue({ ...fakeConfig, - profiles: [{ - type: "base", - authConfig: [{ handler: authHandlerPath } as any] - } as any] + profiles: [ + { + type: "base", + authConfig: [{ handler: authHandlerPath } as any], + } as any, + ], }); await handler.process(params); - const fakeSecureDataExpectedJson: { [key: string]: any} = lodash.cloneDeep(fakeSecureDataJson); + const fakeSecureDataExpectedJson: { [key: string]: any } = + lodash.cloneDeep(fakeSecureDataJson); delete fakeSecureDataExpectedJson[fakeProjPath]; fakeSecureDataExpectedJson[fakeProjPath] = { "profiles.project_base.properties.tokenValue": "fakeLoginData", - "profiles.base2.properties.tokenValue": "fakeLoginData" + "profiles.base2.properties.tokenValue": "fakeLoginData", }; - const fakeSecureDataExpected = Buffer.from(JSON.stringify(fakeSecureDataExpectedJson)).toString("base64"); + const fakeSecureDataExpected = Buffer.from( + JSON.stringify(fakeSecureDataExpectedJson) + ).toString("base64"); const compObj: any = {}; // Make changes to satisfy what would be stored on the JSON compObj.$schema = "./fakeapp.schema.json"; // Fill in the name of the schema file, and make it first lodash.merge(compObj, ImperativeConfig.instance.config.properties); // Add the properties from the config - delete compObj.profiles.project_base.properties.tokenValue; // Delete the secret - delete compObj.profiles.base2.properties.tokenValue; // Delete the secret + delete compObj.profiles.project_base.properties.tokenValue; // Delete the secret + delete compObj.profiles.base2.properties.tokenValue; // Delete the secret - expect(keytarDeletePasswordSpy).toHaveBeenCalledTimes(process.platform === "win32" ? 4 : 3); + expect(keytarDeletePasswordSpy).toHaveBeenCalledTimes( + process.platform === "win32" ? 4 : 3 + ); expect(keytarGetPasswordSpy).toHaveBeenCalledTimes(1); expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(1); expect(mockAuthHandlerApi.createSessCfg).toHaveBeenCalledTimes(2); expect(mockAuthHandlerApi.sessionLogin).toHaveBeenCalledTimes(2); - expect(keytarSetPasswordSpy).toHaveBeenCalledWith("Zowe", "secure_config_props", fakeSecureDataExpected); + expect(keytarSetPasswordSpy).toHaveBeenCalledWith( + "Zowe", + "secure_config_props", + fakeSecureDataExpected + ); expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); - expect(writeFileSyncSpy).toHaveBeenNthCalledWith(1, fakeProjPath, JSON.stringify(compObj, null, 4)); // Config + expect(writeFileSyncSpy).toHaveBeenNthCalledWith( + 1, + fakeProjPath, + JSON.stringify(compObj, null, 4) + ); // Config }); it("should not invoke auth handler if profile type is undefined", async () => { @@ -610,33 +768,52 @@ describe("Configuration Secure command handler", () => { delete eco.profiles.project_base.type; readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); - existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(true).mockReturnValue(false); // Only the project config exists + existsSyncSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + .mockReturnValue(false); // Only the project config exists writeFileSyncSpy.mockImplementation(); - searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath); // Give search something to return + searchSpy + .mockReturnValueOnce(fakeProjUserPath) + .mockReturnValueOnce(fakeProjPath); // Give search something to return await setupConfigToLoad(undefined, configOpts); // Setup the config await handler.process(params); - const fakeSecureDataExpectedJson: { [key: string]: any} = lodash.cloneDeep(fakeSecureDataJson); + const fakeSecureDataExpectedJson: { [key: string]: any } = + lodash.cloneDeep(fakeSecureDataJson); delete fakeSecureDataExpectedJson[fakeProjPath]; fakeSecureDataExpectedJson[fakeProjPath] = { - "profiles.project_base.properties.tokenValue": "fakePromptingData" + "profiles.project_base.properties.tokenValue": + "fakePromptingData", }; - const fakeSecureDataExpected = Buffer.from(JSON.stringify(fakeSecureDataExpectedJson)).toString("base64"); + const fakeSecureDataExpected = Buffer.from( + JSON.stringify(fakeSecureDataExpectedJson) + ).toString("base64"); const compObj: any = {}; // Make changes to satisfy what would be stored on the JSON compObj.$schema = "./fakeapp.schema.json"; // Fill in the name of the schema file, and make it first lodash.merge(compObj, ImperativeConfig.instance.config.properties); // Add the properties from the config - delete compObj.profiles.project_base.properties.tokenValue; // Delete the secret + delete compObj.profiles.project_base.properties.tokenValue; // Delete the secret - expect(keytarDeletePasswordSpy).toHaveBeenCalledTimes(process.platform === "win32" ? 4 : 3); + expect(keytarDeletePasswordSpy).toHaveBeenCalledTimes( + process.platform === "win32" ? 4 : 3 + ); expect(keytarGetPasswordSpy).toHaveBeenCalledTimes(1); expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(1); expect(mockAuthHandlerApi.sessionLogin).toHaveBeenCalledTimes(0); - expect(keytarSetPasswordSpy).toHaveBeenCalledWith("Zowe", "secure_config_props", fakeSecureDataExpected); + expect(keytarSetPasswordSpy).toHaveBeenCalledWith( + "Zowe", + "secure_config_props", + fakeSecureDataExpected + ); expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); - expect(writeFileSyncSpy).toHaveBeenNthCalledWith(1, fakeProjPath, JSON.stringify(compObj, null, 4)); // Config + expect(writeFileSyncSpy).toHaveBeenNthCalledWith( + 1, + fakeProjPath, + JSON.stringify(compObj, null, 4) + ); // Config }); it("should not invoke auth handler if profile token type is undefined", async () => { @@ -644,139 +821,223 @@ describe("Configuration Secure command handler", () => { delete eco.profiles.project_base.properties.tokenType; readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); - existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(true).mockReturnValue(false); // Only the project config exists + existsSyncSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + .mockReturnValue(false); // Only the project config exists writeFileSyncSpy.mockImplementation(); - searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath); // Give search something to return + searchSpy + .mockReturnValueOnce(fakeProjUserPath) + .mockReturnValueOnce(fakeProjPath); // Give search something to return await setupConfigToLoad(undefined, configOpts); // Setup the config await handler.process(params); - const fakeSecureDataExpectedJson: { [key: string]: any} = lodash.cloneDeep(fakeSecureDataJson); + const fakeSecureDataExpectedJson: { [key: string]: any } = + lodash.cloneDeep(fakeSecureDataJson); delete fakeSecureDataExpectedJson[fakeProjPath]; fakeSecureDataExpectedJson[fakeProjPath] = { - "profiles.project_base.properties.tokenValue": "fakePromptingData" + "profiles.project_base.properties.tokenValue": + "fakePromptingData", }; - const fakeSecureDataExpected = Buffer.from(JSON.stringify(fakeSecureDataExpectedJson)).toString("base64"); + const fakeSecureDataExpected = Buffer.from( + JSON.stringify(fakeSecureDataExpectedJson) + ).toString("base64"); const compObj: any = {}; // Make changes to satisfy what would be stored on the JSON compObj.$schema = "./fakeapp.schema.json"; // Fill in the name of the schema file, and make it first lodash.merge(compObj, ImperativeConfig.instance.config.properties); // Add the properties from the config - delete compObj.profiles.project_base.properties.tokenValue; // Delete the secret + delete compObj.profiles.project_base.properties.tokenValue; // Delete the secret - expect(keytarDeletePasswordSpy).toHaveBeenCalledTimes(process.platform === "win32" ? 4 : 3); + expect(keytarDeletePasswordSpy).toHaveBeenCalledTimes( + process.platform === "win32" ? 4 : 3 + ); expect(keytarGetPasswordSpy).toHaveBeenCalledTimes(1); expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(1); expect(readPromptSpy).toHaveBeenCalledTimes(1); expect(mockAuthHandlerApi.sessionLogin).toHaveBeenCalledTimes(0); - expect(keytarSetPasswordSpy).toHaveBeenCalledWith("Zowe", "secure_config_props", fakeSecureDataExpected); + expect(keytarSetPasswordSpy).toHaveBeenCalledWith( + "Zowe", + "secure_config_props", + fakeSecureDataExpected + ); expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); - expect(writeFileSyncSpy).toHaveBeenNthCalledWith(1, fakeProjPath, JSON.stringify(compObj, null, 4)); // Config + expect(writeFileSyncSpy).toHaveBeenNthCalledWith( + 1, + fakeProjPath, + JSON.stringify(compObj, null, 4) + ); // Config }); it("should not invoke auth handler if no matching auth config is found", async () => { const eco = lodash.cloneDeep(expectedProjConfigObjectWithToken); readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); - existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(true).mockReturnValue(false); // Only the project config exists + existsSyncSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + .mockReturnValue(false); // Only the project config exists writeFileSyncSpy.mockImplementation(); - searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath); // Give search something to return + searchSpy + .mockReturnValueOnce(fakeProjUserPath) + .mockReturnValueOnce(fakeProjPath); // Give search something to return await setupConfigToLoad(undefined, configOpts); // Setup the config - jest.spyOn(ImperativeConfig.instance, "loadedConfig", "get").mockReturnValueOnce({ - profiles: [{ - type: "not-base", - authConfig: [{ handler: authHandlerPath } as any] - } as any] + jest.spyOn( + ImperativeConfig.instance, + "loadedConfig", + "get" + ).mockReturnValueOnce({ + profiles: [ + { + type: "not-base", + authConfig: [{ handler: authHandlerPath } as any], + } as any, + ], }); await handler.process(params); - const fakeSecureDataExpectedJson: { [key: string]: any} = lodash.cloneDeep(fakeSecureDataJson); + const fakeSecureDataExpectedJson: { [key: string]: any } = + lodash.cloneDeep(fakeSecureDataJson); delete fakeSecureDataExpectedJson[fakeProjPath]; fakeSecureDataExpectedJson[fakeProjPath] = { - "profiles.project_base.properties.tokenValue": "fakePromptingData" + "profiles.project_base.properties.tokenValue": + "fakePromptingData", }; - const fakeSecureDataExpected = Buffer.from(JSON.stringify(fakeSecureDataExpectedJson)).toString("base64"); + const fakeSecureDataExpected = Buffer.from( + JSON.stringify(fakeSecureDataExpectedJson) + ).toString("base64"); const compObj: any = {}; // Make changes to satisfy what would be stored on the JSON compObj.$schema = "./fakeapp.schema.json"; // Fill in the name of the schema file, and make it first lodash.merge(compObj, ImperativeConfig.instance.config.properties); // Add the properties from the config - delete compObj.profiles.project_base.properties.tokenValue; // Delete the secret + delete compObj.profiles.project_base.properties.tokenValue; // Delete the secret - expect(keytarDeletePasswordSpy).toHaveBeenCalledTimes(process.platform === "win32" ? 4 : 3); + expect(keytarDeletePasswordSpy).toHaveBeenCalledTimes( + process.platform === "win32" ? 4 : 3 + ); expect(keytarGetPasswordSpy).toHaveBeenCalledTimes(1); expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(1); expect(readPromptSpy).toHaveBeenCalledTimes(1); expect(mockAuthHandlerApi.sessionLogin).toHaveBeenCalledTimes(0); - expect(keytarSetPasswordSpy).toHaveBeenCalledWith("Zowe", "secure_config_props", fakeSecureDataExpected); + expect(keytarSetPasswordSpy).toHaveBeenCalledWith( + "Zowe", + "secure_config_props", + fakeSecureDataExpected + ); expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); - expect(writeFileSyncSpy).toHaveBeenNthCalledWith(1, fakeProjPath, JSON.stringify(compObj, null, 4)); // Config + expect(writeFileSyncSpy).toHaveBeenNthCalledWith( + 1, + fakeProjPath, + JSON.stringify(compObj, null, 4) + ); // Config }); it("should not invoke auth handler if auth handler is for different token type", async () => { const eco = lodash.cloneDeep(expectedProjConfigObjectWithToken); readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); - existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(true).mockReturnValue(false); // Only the project config exists + existsSyncSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + .mockReturnValue(false); // Only the project config exists writeFileSyncSpy.mockImplementation(); - searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath); // Give search something to return + searchSpy + .mockReturnValueOnce(fakeProjUserPath) + .mockReturnValueOnce(fakeProjPath); // Give search something to return await setupConfigToLoad(undefined, configOpts); // Setup the config - jest.spyOn(ImperativeConfig.instance, "loadedConfig", "get").mockReturnValueOnce({ - profiles: [{ - type: "base", - authConfig: [{ handler: authHandlerPath } as any] - } as any] + jest.spyOn( + ImperativeConfig.instance, + "loadedConfig", + "get" + ).mockReturnValueOnce({ + profiles: [ + { + type: "base", + authConfig: [{ handler: authHandlerPath } as any], + } as any, + ], }); - mockAuthHandlerApi.promptParams.defaultTokenType = SessConstants.TOKEN_TYPE_LTPA; + mockAuthHandlerApi.promptParams.defaultTokenType = + SessConstants.TOKEN_TYPE_LTPA; await handler.process(params); - mockAuthHandlerApi.promptParams.defaultTokenType = SessConstants.TOKEN_TYPE_JWT; + mockAuthHandlerApi.promptParams.defaultTokenType = + SessConstants.TOKEN_TYPE_JWT; - const fakeSecureDataExpectedJson: { [key: string]: any} = lodash.cloneDeep(fakeSecureDataJson); + const fakeSecureDataExpectedJson: { [key: string]: any } = + lodash.cloneDeep(fakeSecureDataJson); delete fakeSecureDataExpectedJson[fakeProjPath]; fakeSecureDataExpectedJson[fakeProjPath] = { - "profiles.project_base.properties.tokenValue": "fakePromptingData" + "profiles.project_base.properties.tokenValue": + "fakePromptingData", }; - const fakeSecureDataExpected = Buffer.from(JSON.stringify(fakeSecureDataExpectedJson)).toString("base64"); + const fakeSecureDataExpected = Buffer.from( + JSON.stringify(fakeSecureDataExpectedJson) + ).toString("base64"); const compObj: any = {}; // Make changes to satisfy what would be stored on the JSON compObj.$schema = "./fakeapp.schema.json"; // Fill in the name of the schema file, and make it first lodash.merge(compObj, ImperativeConfig.instance.config.properties); // Add the properties from the config - delete compObj.profiles.project_base.properties.tokenValue; // Delete the secret + delete compObj.profiles.project_base.properties.tokenValue; // Delete the secret - expect(keytarDeletePasswordSpy).toHaveBeenCalledTimes(process.platform === "win32" ? 4 : 3); + expect(keytarDeletePasswordSpy).toHaveBeenCalledTimes( + process.platform === "win32" ? 4 : 3 + ); expect(keytarGetPasswordSpy).toHaveBeenCalledTimes(1); expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(1); expect(readPromptSpy).toHaveBeenCalledTimes(1); expect(mockAuthHandlerApi.sessionLogin).toHaveBeenCalledTimes(0); - expect(keytarSetPasswordSpy).toHaveBeenCalledWith("Zowe", "secure_config_props", fakeSecureDataExpected); + expect(keytarSetPasswordSpy).toHaveBeenCalledWith( + "Zowe", + "secure_config_props", + fakeSecureDataExpected + ); expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); - expect(writeFileSyncSpy).toHaveBeenNthCalledWith(1, fakeProjPath, JSON.stringify(compObj, null, 4)); // Config + expect(writeFileSyncSpy).toHaveBeenNthCalledWith( + 1, + fakeProjPath, + JSON.stringify(compObj, null, 4) + ); // Config }); it("should only prompt for profiles that matched profile param", async () => { const eco = lodash.cloneDeep(expectedProjConfigObjectWithToken); readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); - existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(true).mockReturnValue(false); // Only the project config exists + existsSyncSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + .mockReturnValue(false); // Only the project config exists writeFileSyncSpy.mockImplementation(); - searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath); // Give search something to return + searchSpy + .mockReturnValueOnce(fakeProjUserPath) + .mockReturnValueOnce(fakeProjPath); // Give search something to return await setupConfigToLoad(undefined, configOpts); // Setup the config - jest.spyOn(ImperativeConfig.instance, "loadedConfig", "get").mockReturnValueOnce({ - profiles: [{ - type: "base", - authConfig: [{ handler: authHandlerPath } as any] - } as any] + jest.spyOn( + ImperativeConfig.instance, + "loadedConfig", + "get" + ).mockReturnValueOnce({ + profiles: [ + { + type: "base", + authConfig: [{ handler: authHandlerPath } as any], + } as any, + ], }); - mockAuthHandlerApi.sessionLogin.mockRejectedValueOnce(new Error("bad handler")); + mockAuthHandlerApi.sessionLogin.mockRejectedValueOnce( + new Error("bad handler") + ); let caughtError; try { @@ -787,7 +1048,7 @@ describe("Configuration Secure command handler", () => { expect(caughtError).toBeDefined(); expect(caughtError.message).toContain("Failed to fetch jwtToken"); - expect(readPromptSpy).toHaveBeenCalledTimes(2); // User and password + expect(readPromptSpy).toHaveBeenCalledTimes(2); // User and password expect(mockAuthHandlerApi.sessionLogin).toHaveBeenCalledTimes(1); expect(keytarSetPasswordSpy).toHaveBeenCalledTimes(0); expect(writeFileSyncSpy).toHaveBeenCalledTimes(0); @@ -805,6 +1066,9 @@ describe("Configuration Secure command handler", () => { params.arguments.globalConfig = true; // Mock the console prompt to return an empty string + myPromptSpy = jest + .spyOn(params.response.console, "prompt") + .mockResolvedValue(""); myPromptSpy = jest.spyOn(params.response.console, "prompt").mockResolvedValue(""); // Reset spies @@ -824,11 +1088,22 @@ describe("Configuration Secure command handler", () => { const eco = lodash.cloneDeep(expectedGlobalUserConfigObject); eco.$schema = "./fakeapp.schema.json"; readFileSyncSpy.mockReturnValueOnce(JSON.stringify(eco)); - existsSyncSpy.mockReturnValueOnce(false).mockReturnValueOnce(false).mockReturnValueOnce(true).mockReturnValue(false); - searchSpy.mockReturnValueOnce(fakeProjUserPath).mockReturnValueOnce(fakeProjPath); + existsSyncSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + .mockReturnValue(false); + searchSpy + .mockReturnValueOnce(fakeProjUserPath) + .mockReturnValueOnce(fakeProjPath); await setupConfigToLoad(undefined, configOpts); // Setup mock secure fields + + jest.spyOn( + ImperativeConfig.instance.config.api.secure, + "secureFields" + ).mockReturnValue(secureFields); jest.spyOn(ImperativeConfig.instance.config.api.secure, "secureFields").mockReturnValue(secureFields); let caughtError; @@ -841,7 +1116,10 @@ describe("Configuration Secure command handler", () => { // Verify prompt count and inclusion of expected secure fields expect(myPromptSpy).toHaveBeenCalledTimes(expectedPromptTimes); if (expectedPromptTimes > 0) { - expect(myPromptSpy).toHaveBeenCalledWith(expect.stringContaining(expectedSecureField), { "hideText": true }); + expect(myPromptSpy).toHaveBeenCalledWith( + expect.stringContaining(expectedSecureField), + { hideText: true } + ); } expect(caughtError).toBeUndefined(); }; @@ -852,7 +1130,7 @@ describe("Configuration Secure command handler", () => { [ "profiles.noMatchProfile.properties.tokenValue", "profiles.GoodProfile.properties.tokenValue", - "profiles.abcdefg.properties.tokenValue" + "profiles.abcdefg.properties.tokenValue", ], 1, "profiles.GoodProfile.properties.tokenValue" @@ -865,7 +1143,7 @@ describe("Configuration Secure command handler", () => { [ "profiles.noMatchProfile.properties.tokenValue", "profiles.lpar1.profiles.GoodProfile.properties.tokenValue", - "profiles.abcdefg.properties.tokenValue" + "profiles.abcdefg.properties.tokenValue", ], 1, "profiles.lpar1.profiles.GoodProfile.properties.tokenValue" @@ -878,7 +1156,7 @@ describe("Configuration Secure command handler", () => { [ "profiles.noMatchProfile.properties.tokenValue", "profiles.GoodProfile.properties.tokenValue", - "profiles.abcdefg.properties.tokenValue" + "profiles.abcdefg.properties.tokenValue", ], 1, "profiles.GoodProfile.properties.tokenValue" @@ -891,13 +1169,12 @@ describe("Configuration Secure command handler", () => { [ "profiles.lpar1.profiles.test.properties.tokenValue", "profiles.GoodProfile.properties.tokenValue", - "profiles.abcdefg.properties.tokenValue" + "profiles.abcdefg.properties.tokenValue", ], 3, "profiles.lpar1.profiles.test.properties.tokenValue" ); }); }); - }); }); diff --git a/packages/imperative/src/imperative/__tests__/plugins/cmd/install/install.handler.unit.test.ts b/packages/imperative/src/imperative/__tests__/plugins/cmd/install/install.handler.unit.test.ts index 1cbba51d8a..dbd84c1ba0 100644 --- a/packages/imperative/src/imperative/__tests__/plugins/cmd/install/install.handler.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/plugins/cmd/install/install.handler.unit.test.ts @@ -10,31 +10,22 @@ */ /* eslint-disable jest/expect-expect */ -import Mock = jest.Mock; - let expectedVal: unknown; let returnedVal: unknown; jest.mock("cross-spawn"); jest.mock("jsonfile"); -jest.mock("../../../../src/plugins/utilities/npm-interface/install"); -jest.mock("../../../../src/plugins/utilities/runValidatePlugin"); jest.mock("../../../../src/plugins/utilities/PMFConstants"); -jest.mock("../../../../../cmd/src/response/CommandResponse"); jest.mock("../../../../../cmd/src/response/HandlerResponse"); -jest.mock("../../../../../cmd/src/doc/handler/IHandlerParameters"); -jest.mock("../../../../../logger"); -jest.mock("../../../../src/Imperative"); -jest.mock("../../../../src/plugins/utilities/NpmFunctions"); jest.doMock("path", () => { const originalPath = jest.requireActual("path"); return { ...originalPath, - resolve: (...path: string[]) => { - if (path[0] == expectedVal) { + resolve: (...paths: string[]) => { + if (paths[0] === expectedVal) { return returnedVal ? returnedVal : expectedVal; } else { - return originalPath.resolve(...path); + return originalPath.resolve(...paths); } } }; @@ -43,29 +34,29 @@ jest.doMock("path", () => { import { HandlerResponse, IHandlerParameters } from "../../../../../cmd"; import { Console } from "../../../../../console"; import { ImperativeError } from "../../../../../error"; -import { install } from "../../../../src/plugins/utilities/npm-interface"; -import { runValidatePlugin } from "../../../../src/plugins/utilities/runValidatePlugin"; +import * as npmInterface from "../../../../src/plugins/utilities/npm-interface"; +import * as validatePlugin from "../../../../src/plugins/utilities/runValidatePlugin"; import InstallHandler from "../../../../src/plugins/cmd/install/install.handler"; import { IPluginJson } from "../../../../src/plugins/doc/IPluginJson"; import { Logger } from "../../../../../logger"; -import { readFileSync, writeFileSync } from "jsonfile"; +import * as jsonfile from "jsonfile"; import { PMFConstants } from "../../../../src/plugins/utilities/PMFConstants"; import { TextUtils } from "../../../../../utilities"; -import { getRegistry, getScopeRegistry, npmLogin } from "../../../../src/plugins/utilities/NpmFunctions"; +import { NpmRegistryUtils } from "../../../../src/plugins/utilities/NpmFunctions"; import * as spawn from "cross-spawn"; -import { IO } from "../../../../../io"; describe("Plugin Management Facility install handler", () => { // Objects created so types are correct. + const origGetRegistry = jest.requireActual("../../../../src/plugins/utilities/NpmFunctions").NpmRegistryUtils.getRegistry; const mocks = { - npmLogin: npmLogin as Mock, - getRegistry: getRegistry as unknown as Mock, - getScopeRegistry: getScopeRegistry as unknown as Mock, - readFileSync: readFileSync as Mock, - writeFileSync: writeFileSync as Mock, - install: install as unknown as Mock, - runValidatePlugin: runValidatePlugin as unknown as Mock, + npmLogin: jest.spyOn(NpmRegistryUtils, "npmLogin"), + getRegistry: jest.spyOn(NpmRegistryUtils, "getRegistry"), + getScopeRegistry: jest.spyOn(NpmRegistryUtils as any, "getScopeRegistry"), + readFileSync: jest.spyOn(jsonfile, "readFileSync"), + writeFileSync: jest.spyOn(jsonfile, "writeFileSync"), + install: jest.spyOn(npmInterface, "install"), + runValidatePlugin: jest.spyOn(validatePlugin, "runValidatePlugin"), }; // two plugin set of values @@ -84,11 +75,11 @@ describe("Plugin Management Facility install handler", () => { jest.clearAllMocks(); // This needs to be mocked before running process function of uninstall handler - (Logger.getImperativeLogger as unknown as Mock).mockReturnValue(new Logger(new Console()) as any); - mocks.getRegistry.mockReturnValue(packageRegistry as any); - mocks.readFileSync.mockReturnValue({} as any); - npmLogin(packageRegistry); - mocks.runValidatePlugin.mockReturnValue(finalValidationMsg as any); + jest.spyOn(Logger, "getImperativeLogger").mockReturnValue(new Logger(new Console())); + mocks.getRegistry.mockReturnValue(packageRegistry); + mocks.readFileSync.mockReturnValue({}); + mocks.runValidatePlugin.mockReturnValue(finalValidationMsg); + mocks.install.mockImplementation(); expectedVal = undefined; returnedVal = undefined; }); @@ -141,15 +132,16 @@ describe("Plugin Management Facility install handler", () => { const wasInstallCallValid = ( packageLocation: string, registry: string, - installFromFile = false + installFromFile = false, + extraNpmArgs = {} ) => { if (installFromFile) { expect(mocks.install).toHaveBeenCalledWith( - packageLocation, registry, true + packageLocation, { location: registry, npmArgs: { registry, ...extraNpmArgs } }, true ); } else { expect(mocks.install).toHaveBeenCalledWith( - packageLocation, registry + packageLocation, { location: registry, npmArgs: { registry, ...extraNpmArgs } } ); } }; @@ -162,8 +154,9 @@ describe("Plugin Management Facility install handler", () => { */ const wasInstallSuccessful = (params: IHandlerParameters) => { // get the text of the last message that was displayed - const outputMsg = (params.response.console.log as Mock).mock.calls[(params.response.console.log as Mock).mock.calls.length - 1][0]; - expect(outputMsg).toContain(finalValidationMsg); + expect(params.response.console.log).toHaveBeenLastCalledWith( + expect.stringContaining(finalValidationMsg) + ); }; /** @@ -193,10 +186,10 @@ describe("Plugin Management Facility install handler", () => { }; // Override the return value for this test only - mocks.readFileSync.mockReturnValueOnce(fileJson as any); + mocks.readFileSync.mockReturnValueOnce(fileJson); mocks.install - .mockReturnValueOnce("a" as any) - .mockReturnValueOnce("plugin2" as any); + .mockResolvedValueOnce("a") + .mockResolvedValueOnce("plugin2"); const handler = new InstallHandler(); @@ -213,9 +206,6 @@ describe("Plugin Management Facility install handler", () => { // Validate the call to get the registry value wasGetRegistryCalled(); - // Validate the call to login - wasNpmLoginCallValid(packageRegistry); - expect(mocks.install).toHaveBeenCalledTimes(2); wasInstallCallValid(`${fileJson.a.package}@${fileJson.a.version}`, packageRegistry, true); wasInstallCallValid(fileJson.plugin2.package, packageRegistry2, true); @@ -224,7 +214,6 @@ describe("Plugin Management Facility install handler", () => { expect(mocks.runValidatePlugin).toHaveBeenCalledWith("a"); expect(mocks.runValidatePlugin).toHaveBeenCalledWith("plugin2"); - // Validate that the read was correct wasReadFileSyncCallValid(resolveVal); @@ -265,9 +254,6 @@ describe("Plugin Management Facility install handler", () => { // Validate the call to get the registry value wasGetRegistryCalled(); - // Validate the call to login - wasNpmLoginCallValid(packageRegistry); - // Check that install worked as expected wasInstallCallValid(params.arguments.plugin[0], packageRegistry); @@ -290,6 +276,27 @@ describe("Plugin Management Facility install handler", () => { wasInstallSuccessful(params); }); + it("should install single package with registry and login specified", async () => { + mocks.npmLogin.mockReturnValueOnce(); + const handler = new InstallHandler(); + + const params = getIHandlerParametersObject(); + params.arguments.plugin = ["sample1"]; + params.arguments.registry = "http://localhost:4873/"; + params.arguments.login = true; + + await handler.process(params as IHandlerParameters); + + // Validate the call to login + wasNpmLoginCallValid(packageRegistry); + + // Check that install worked as expected + wasInstallCallValid(params.arguments.plugin[0], params.arguments.registry); + + // Check that success is output + wasInstallSuccessful(params); + }); + it("should install multiple packages", async () => { const handler = new InstallHandler(); @@ -301,9 +308,6 @@ describe("Plugin Management Facility install handler", () => { // Validate the install wasGetRegistryCalled(); - // Validate the call to login - wasNpmLoginCallValid(packageRegistry); - // Validate that install was called with each of these values expect(mocks.install).toHaveBeenCalledTimes(params.arguments.plugin.length); wasInstallCallValid(params.arguments.plugin[0], packageRegistry); @@ -324,8 +328,7 @@ describe("Plugin Management Facility install handler", () => { // Validate the call to get the registry value wasGetRegistryCalled(); - // Validate the call to login - wasNpmLoginCallValid(packageRegistry); + // Validate that the read was correct wasReadFileSyncCallValid(PMFConstants.instance.PLUGIN_JSON); expect(params.response.console.log).toHaveBeenCalledWith("No packages were found in " + @@ -369,7 +372,7 @@ describe("Plugin Management Facility install handler", () => { }); jest.spyOn(spawn, "sync").mockReturnValueOnce({ status: 1 } as any); - mocks.getRegistry.mockImplementationOnce(jest.requireActual("../../../../src/plugins/utilities/NpmFunctions").getRegistry); + mocks.getRegistry.mockImplementationOnce(origGetRegistry); try { await handler.process(params); @@ -381,13 +384,13 @@ describe("Plugin Management Facility install handler", () => { expect(expectedError.additionalDetails).toContain("Command failed"); expect(expectedError.additionalDetails).toContain("npm"); }); - it("should handle installed plugins via public scope", async () => { + it("should handle installed plugins via package name", async () => { const handler = new InstallHandler(); const params = getIHandlerParametersObject(); params.arguments.plugin = ["@public/sample1"]; - mocks.getScopeRegistry.mockReturnValueOnce("publicRegistryUrl" as any); + mocks.getScopeRegistry.mockReturnValueOnce("publicRegistryUrl"); try { await handler.process(params); @@ -395,14 +398,13 @@ describe("Plugin Management Facility install handler", () => { expect(e).toBeUndefined(); } - expect(mocks.install).toHaveBeenCalledWith("@public/sample1","publicRegistryUrl"); + wasInstallCallValid("@public/sample1", packageRegistry, false, { "@public:registry": "publicRegistryUrl" }); }); it("should handle installed plugins via project/directory", async () => { const handler = new InstallHandler(); const params = getIHandlerParametersObject(); params.arguments.plugin = ["path/to/dir"]; - jest.spyOn(IO, 'isDir').mockReturnValue(true); try{ await handler.process(params); @@ -411,7 +413,7 @@ describe("Plugin Management Facility install handler", () => { expect(e).toBeUndefined(); } - expect(mocks.install).toHaveBeenCalledWith("path/to/dir","path/to/dir"); + wasInstallCallValid("path/to/dir", "path/to/dir", false, { registry: packageRegistry }); }); it("should handle installed plugins via tarball file", async () => { const handler = new InstallHandler(); @@ -426,16 +428,15 @@ describe("Plugin Management Facility install handler", () => { expect(e).toBeUndefined(); } - expect(mocks.install).toHaveBeenCalledWith("path/to/dir/file.tgz","path/to/dir/file.tgz"); + wasInstallCallValid("path/to/dir/file.tgz", "path/to/dir/file.tgz", false, { registry: packageRegistry }); }); - it("should handle multiple installed plugins via tarball, director, public registry, and private registry", async () => { + it("should handle multiple installed plugins via tarball, directory, and registry", async () => { const handler = new InstallHandler(); const params = getIHandlerParametersObject(); - params.arguments.plugin = ["@public/sample1","@private/sample1","path/to/dir","path/to/dir/file.tgz"]; - mocks.getScopeRegistry.mockReturnValueOnce("publicRegistryUrl" as any); - mocks.getScopeRegistry.mockReturnValueOnce("privateRegistryUrl" as any); - jest.spyOn(IO, 'isDir').mockReturnValue(true); + params.arguments.plugin = ["@public/sample1", "@private/sample1", "path/to/dir", "path/to/dir/file.tgz"]; + mocks.getScopeRegistry.mockReturnValueOnce("publicRegistryUrl"); + mocks.getScopeRegistry.mockReturnValueOnce("privateRegistryUrl"); try{ await handler.process(params); @@ -444,10 +445,10 @@ describe("Plugin Management Facility install handler", () => { expect(e).toBeUndefined(); } - expect(mocks.install).toHaveBeenCalledWith("@public/sample1","publicRegistryUrl"); - expect(mocks.install).toHaveBeenCalledWith("@private/sample1","privateRegistryUrl"); - expect(mocks.install).toHaveBeenCalledWith("path/to/dir","path/to/dir"); - expect(mocks.install).toHaveBeenCalledWith("path/to/dir/file.tgz","path/to/dir/file.tgz"); + expect(mocks.install).toHaveBeenCalledTimes(params.arguments.plugin.length); + wasInstallCallValid("@public/sample1", packageRegistry, false, { "@public:registry": "publicRegistryUrl" }); + wasInstallCallValid("@private/sample1", packageRegistry, false, { "@private:registry": "privateRegistryUrl" }); + wasInstallCallValid("path/to/dir", "path/to/dir", false, { registry: packageRegistry }); + wasInstallCallValid("path/to/dir/file.tgz", "path/to/dir/file.tgz", false, { registry: packageRegistry }); }); }); - diff --git a/packages/imperative/src/imperative/__tests__/plugins/cmd/list/list.handler.unit.test.ts b/packages/imperative/src/imperative/__tests__/plugins/cmd/list/list.handler.unit.test.ts index 8db628f52b..ecf7670ab0 100644 --- a/packages/imperative/src/imperative/__tests__/plugins/cmd/list/list.handler.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/plugins/cmd/list/list.handler.unit.test.ts @@ -84,7 +84,7 @@ describe("Plugin Management Facility list handler", () => { mocks.readFileSync.mockReturnValue({} as any); }); - test("list packages", async () => { + it("list packages", async () => { // plugin definitions mocking unsorted file contents const fileJson: IPluginJson = { @@ -113,7 +113,7 @@ describe("Plugin Management Facility list handler", () => { }); - test("list packages short", async () => { + it("list packages short", async () => { // plugin definitions mocking unsorted file contents const fileJson: IPluginJson = { @@ -143,4 +143,3 @@ describe("Plugin Management Facility list handler", () => { }); }); - diff --git a/packages/imperative/src/imperative/__tests__/plugins/cmd/uninstall/uninstall.handler.unit.test.ts b/packages/imperative/src/imperative/__tests__/plugins/cmd/uninstall/uninstall.handler.unit.test.ts index 84a3592d04..6d1e5042e0 100644 --- a/packages/imperative/src/imperative/__tests__/plugins/cmd/uninstall/uninstall.handler.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/plugins/cmd/uninstall/uninstall.handler.unit.test.ts @@ -109,7 +109,7 @@ describe("Plugin Management Facility uninstall handler", () => { expect(params.response.console.log).toHaveBeenCalledWith("Removal of the npm package(s) was successful.\n"); }; - test("uninstall specified package", async () => { + it("uninstall specified package", async () => { // plugin definitions mocking file contents const fileJson: IPluginJson = { a: { diff --git a/packages/imperative/src/imperative/__tests__/plugins/cmd/update/update.handler.unit.test.ts b/packages/imperative/src/imperative/__tests__/plugins/cmd/update/update.handler.unit.test.ts index d4c29e9c2c..cd13230b3c 100644 --- a/packages/imperative/src/imperative/__tests__/plugins/cmd/update/update.handler.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/plugins/cmd/update/update.handler.unit.test.ts @@ -9,17 +9,11 @@ * */ -import Mock = jest.Mock; - +/* eslint-disable jest/expect-expect */ jest.mock("child_process"); jest.mock("jsonfile"); -jest.mock("../../../../src/plugins/utilities/npm-interface/update"); jest.mock("../../../../src/plugins/utilities/PMFConstants"); -jest.mock("../../../../../cmd/src/doc/handler/IHandlerParameters"); -jest.mock("../../../../../cmd/src/response/CommandResponse"); jest.mock("../../../../../cmd/src/response/HandlerResponse"); -jest.mock("../../../../../logger"); -jest.mock("../../../../src/plugins/utilities/NpmFunctions"); import { HandlerResponse, IHandlerParameters } from "../../../../../cmd"; import { Console } from "../../../../../console"; @@ -27,21 +21,21 @@ import { IPluginJson } from "../../../../src/plugins/doc/IPluginJson"; import { Logger } from "../../../../../logger"; import { PMFConstants } from "../../../../src/plugins/utilities/PMFConstants"; import UpdateHandler from "../../../../src/plugins/cmd/update/update.handler"; -import * as NpmFunctions from "../../../../src/plugins/utilities/NpmFunctions"; -import * as ChildProcesses from "child_process"; -import * as JsonFile from "jsonfile"; -import * as NpmInterface from "../../../../src/plugins/utilities/npm-interface"; +import { NpmRegistryUtils } from "../../../../src/plugins/utilities/NpmFunctions"; +import * as childProcess from "child_process"; +import * as jsonfile from "jsonfile"; +import * as npmInterface from "../../../../src/plugins/utilities/npm-interface"; describe("Plugin Management Facility update handler", () => { const resolveVal = "test/imperative-plugins"; const mocks = { - npmLoginSpy: jest.spyOn(NpmFunctions, 'npmLogin') as jest.SpyInstance, - execSyncSpy: jest.spyOn(ChildProcesses, 'execSync') as jest.SpyInstance, - readFileSyncSpy: jest.spyOn(JsonFile, 'readFileSync') as jest.SpyInstance, - writeFileSyncSpy: jest.spyOn(JsonFile, 'writeFileSync') as jest.SpyInstance, - updateSpy: jest.spyOn(NpmInterface, 'update') as jest.SpyInstance, + npmLoginSpy: jest.spyOn(NpmRegistryUtils, "npmLogin"), + execSyncSpy: jest.spyOn(childProcess, "execSync"), + readFileSyncSpy: jest.spyOn(jsonfile, "readFileSync"), + writeFileSyncSpy: jest.spyOn(jsonfile, "writeFileSync"), + updateSpy: jest.spyOn(npmInterface, "update"), }; // two plugin set of values @@ -56,14 +50,13 @@ describe("Plugin Management Facility update handler", () => { const pluginName = "imperative-sample-plugin"; beforeEach(() => { - // Mocks need cleared after every test for clean test runs + // Mocks need cleared after every test for clean test runs jest.resetAllMocks(); // This needs to be mocked before running process function of update handler - (Logger.getImperativeLogger as unknown as Mock).mockReturnValue(new Logger(new Console()) as any); - mocks.execSyncSpy.mockReturnValue(packageRegistry as any); - mocks.readFileSyncSpy.mockReturnValue({} as any); - NpmFunctions.npmLogin(packageRegistry); + jest.spyOn(Logger, "getImperativeLogger").mockReturnValue(new Logger(new Console())); + mocks.execSyncSpy.mockReturnValue(packageRegistry); + mocks.readFileSyncSpy.mockReturnValue({}); }); /** @@ -96,7 +89,7 @@ describe("Plugin Management Facility update handler", () => { registry: string ) => { expect(mocks.updateSpy).toHaveBeenCalledWith( - packageNameParm, registry + packageNameParm, { location: registry, npmArgs: { registry } } ); }; @@ -107,7 +100,8 @@ describe("Plugin Management Facility update handler", () => { * process function. */ const wasUpdateSuccessful = (params: IHandlerParameters) => { - expect(params.response.console.log).toHaveBeenCalledWith(`Update of the npm package(${params.arguments.plugin}) was successful.\n`); + expect(params.response.console.log).toHaveBeenCalledWith( + `Update of the npm package(${resolveVal}) was successful.\n`); }; /** @@ -131,7 +125,7 @@ describe("Plugin Management Facility update handler", () => { ); }; - test("update specified plugin", async () => { + it("update specified plugin", async () => { // plugin definitions mocking file contents const fileJson: IPluginJson = { @@ -148,13 +142,14 @@ describe("Plugin Management Facility update handler", () => { }; // Override the return value for this test only - mocks.readFileSyncSpy.mockReturnValueOnce(fileJson as any); + mocks.readFileSyncSpy.mockReturnValueOnce(fileJson); const handler = new UpdateHandler(); const params = getIHandlerParametersObject(); params.arguments.plugin = pluginName; params.arguments.registry = packageRegistry; + params.arguments.login = true; await handler.process(params as IHandlerParameters); @@ -162,12 +157,10 @@ describe("Plugin Management Facility update handler", () => { wasNpmLoginCallValid(packageRegistry); wasWriteFileSyncValid(PMFConstants.instance.PLUGIN_JSON, fileJson); wasUpdateCallValid(packageName, packageRegistry); - - expect(params.response.console.log).toHaveBeenCalledWith( - `Update of the npm package(${resolveVal}) was successful.\n`); + wasUpdateSuccessful(params); }); - test("update imperative-sample-plugin", async () => { + it("update imperative-sample-plugin", async () => { // plugin definitions mocking file contents const fileJson: IPluginJson = { @@ -179,7 +172,7 @@ describe("Plugin Management Facility update handler", () => { }; // Override the return value for this test only - mocks.readFileSyncSpy.mockReturnValueOnce(fileJson as any); + mocks.readFileSyncSpy.mockReturnValueOnce(fileJson); const handler = new UpdateHandler(); @@ -189,11 +182,8 @@ describe("Plugin Management Facility update handler", () => { await handler.process(params as IHandlerParameters); // Validate the call to login - wasNpmLoginCallValid(packageRegistry); wasWriteFileSyncValid(PMFConstants.instance.PLUGIN_JSON, fileJson); wasUpdateCallValid(resolveVal, packageRegistry); - expect(params.response.console.log).toHaveBeenCalledWith( - `Update of the npm package(${resolveVal}) was successful.\n`); + wasUpdateSuccessful(params); }); }); - diff --git a/packages/imperative/src/imperative/__tests__/plugins/utilities/NpmFunctions.unit.test.ts b/packages/imperative/src/imperative/__tests__/plugins/utilities/NpmFunctions.unit.test.ts index a710f1df2c..b0e39b5137 100644 --- a/packages/imperative/src/imperative/__tests__/plugins/utilities/NpmFunctions.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/plugins/utilities/NpmFunctions.unit.test.ts @@ -40,7 +40,7 @@ describe("NpmFunctions", () => { status: 0, stdout: stdoutBuffer } as any); - const result = npmFunctions.installPackages("fakePrefix", fakeRegistry, "samplePlugin"); + const result = npmFunctions.installPackages("samplePlugin", { prefix: "fakePrefix", registry: fakeRegistry }); expect(spawnSyncSpy.mock.calls[0][0]).toBe(npmCmd); expect(spawnSyncSpy.mock.calls[0][1]).toEqual(expect.arrayContaining(["install", "samplePlugin"])); expect(spawnSyncSpy.mock.calls[0][1]).toEqual(expect.arrayContaining(["--prefix", "fakePrefix"])); @@ -54,7 +54,7 @@ describe("NpmFunctions", () => { status: 0, stdout: stdoutBuffer } as any); - const result = npmFunctions.getRegistry(); + const result = npmFunctions.NpmRegistryUtils.getRegistry(); expect(spawnSyncSpy.mock.calls[0][0]).toBe(npmCmd); expect(spawnSyncSpy.mock.calls[0][1]).toEqual(["config", "get", "registry"]); expect(result).toBe(stdoutBuffer.toString()); @@ -62,7 +62,7 @@ describe("NpmFunctions", () => { it("npmLogin should run npm login command", () => { const spawnSyncSpy = jest.spyOn(spawn, "sync").mockReturnValueOnce({ status: 0 } as any); - npmFunctions.npmLogin(fakeRegistry); + npmFunctions.NpmRegistryUtils.npmLogin(fakeRegistry); expect(spawnSyncSpy.mock.calls[0][0]).toBe(npmCmd); expect(spawnSyncSpy.mock.calls[0][1]).toContain("login"); expect(spawnSyncSpy.mock.calls[0][1]).toEqual(expect.arrayContaining(["--registry", fakeRegistry])); @@ -136,10 +136,10 @@ describe("NpmFunctions", () => { expect(pacote.manifest).toHaveBeenCalledTimes(1); }); - it("getScopeRegistry() should return registry for 'test' scope", async () => { + it("getScopeRegistry() should return registry for 'test' scope", () => { const spawnSpy = jest.spyOn(ExecUtils, "spawnAndGetOutput"); spawnSpy.mockReturnValueOnce("https://test123.com"); - const result = await npmFunctions.getScopeRegistry("test"); + const result = (npmFunctions.NpmRegistryUtils as any).getScopeRegistry("test"); expect(result).toBe("https://test123.com"); expect(spawnSpy).toHaveBeenCalledTimes(1); }); diff --git a/packages/imperative/src/imperative/__tests__/plugins/utilities/npm-interface/install.unit.test.ts b/packages/imperative/src/imperative/__tests__/plugins/utilities/npm-interface/install.unit.test.ts index 9fc5aace6a..2e15b6d88b 100644 --- a/packages/imperative/src/imperative/__tests__/plugins/utilities/npm-interface/install.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/plugins/utilities/npm-interface/install.unit.test.ts @@ -10,31 +10,21 @@ */ /* eslint-disable jest/expect-expect */ -import Mock = jest.Mock; let expectedVal: any; let returnedVal: any; jest.mock("cross-spawn"); jest.mock("jsonfile"); -jest.mock("find-up"); jest.mock("../../../../src/plugins/utilities/PMFConstants"); -jest.mock("../../../../src/plugins/PluginManagementFacility"); -jest.mock("../../../../src/ConfigurationLoader"); -jest.mock("../../../../src/UpdateImpConfig"); -jest.mock("../../../../../config/src/ConfigSchema"); -jest.mock("../../../../../logger"); -jest.mock("../../../../../cmd/src/response/CommandResponse"); -jest.mock("../../../../../cmd/src/response/HandlerResponse"); -jest.mock("../../../../src/plugins/utilities/NpmFunctions"); jest.doMock("path", () => { const originalPath = jest.requireActual("path"); return { ...originalPath, - resolve: (...path: string[]) => { - if (path[0] == expectedVal) { + resolve: (...paths: string[]) => { + if (paths[0] === expectedVal) { return returnedVal ? returnedVal : expectedVal; } else { - return originalPath.resolve(...path); + return originalPath.resolve(...paths); } } }; @@ -48,9 +38,10 @@ import { IPluginJson } from "../../../../src/plugins/doc/IPluginJson"; import { IPluginJsonObject } from "../../../../src/plugins/doc/IPluginJsonObject"; import { Logger } from "../../../../../logger"; import { PMFConstants } from "../../../../src/plugins/utilities/PMFConstants"; -import { readFileSync, writeFileSync } from "jsonfile"; -import { sync } from "find-up"; -import { getPackageInfo, installPackages } from "../../../../src/plugins/utilities/NpmFunctions"; +import * as spawn from "cross-spawn"; +import * as jsonfile from "jsonfile"; +import * as findUp from "find-up"; +import * as npmFns from "../../../../src/plugins/utilities/NpmFunctions"; import { ConfigSchema } from "../../../../../config/src/ConfigSchema"; import { PluginManagementFacility } from "../../../../src/plugins/PluginManagementFacility"; import { AbstractPluginLifeCycle } from "../../../../src/plugins/AbstractPluginLifeCycle"; @@ -73,17 +64,19 @@ describe("PMF: Install Interface", () => { // Objects created so types are correct. const pmfI = PluginManagementFacility.instance; const mocks = { - installPackages: installPackages as unknown as Mock, - readFileSync: readFileSync as Mock, - writeFileSync: writeFileSync as Mock, - sync: sync as unknown as Mock, - getPackageInfo: getPackageInfo as unknown as Mock, - ConfigSchema_updateSchema: ConfigSchema.updateSchema as unknown as Mock, - PMF_requirePluginModuleCallback: pmfI.requirePluginModuleCallback as Mock, - ConfigurationLoader_load: ConfigurationLoader.load as Mock, - UpdateImpConfig_addProfiles: UpdateImpConfig.addProfiles as Mock, - path: path as unknown as Mock, - ConfigSchema_loadSchema: jest.spyOn(ConfigSchema, "loadSchema"), + installPackages: jest.spyOn(npmFns, "installPackages"), + readFileSync: jest.spyOn(jsonfile, "readFileSync"), + writeFileSync: jest.spyOn(jsonfile, "writeFileSync"), + findUpSync: jest.spyOn(findUp, "sync"), + getPackageInfo: jest.spyOn(npmFns, "getPackageInfo"), + requirePluginModuleCallback: jest.spyOn(pmfI, "requirePluginModuleCallback"), + loadConfiguration: jest.spyOn(ConfigurationLoader, "load"), + addProfiles: jest.spyOn(UpdateImpConfig, "addProfiles"), + spawnSync: jest.spyOn(spawn, "sync"), + ConfigSchema: { + loadSchema: jest.spyOn(ConfigSchema, "loadSchema"), + updateSchema: jest.spyOn(ConfigSchema, "updateSchema") + }, ConfigUtils: { readExtendersJson: jest.spyOn(ConfigUtils, "readExtendersJson"), writeExtendersJson: jest.spyOn(ConfigUtils, "writeExtendersJson") @@ -93,6 +86,7 @@ describe("PMF: Install Interface", () => { const packageName = "a"; const packageVersion = "1.2.3"; const packageRegistry = "https://registry.npmjs.org/"; + const registryInfo = npmFns.NpmRegistryUtils.buildRegistryInfo(packageName, packageRegistry); beforeEach(() => { // Mocks need cleared after every test for clean test runs @@ -101,15 +95,13 @@ describe("PMF: Install Interface", () => { returnedVal = undefined; // This needs to be mocked before running install - (Logger.getImperativeLogger as unknown as Mock).mockReturnValue(new Logger(new Console()) as any); + jest.spyOn(Logger, "getImperativeLogger").mockReturnValue(new Logger(new Console())); /* Since install() adds new plugins into the value returned from - * readFileSyc(plugins.json), we must reset readFileSync to return an empty set before each test. + * readFileSync(plugins.json), we must reset readFileSync to return an empty set before each test. */ - mocks.readFileSync.mockReturnValue({} as any); - mocks.sync.mockReturnValue("fake_find-up_sync_result" as any); - jest.spyOn(path, "dirname").mockReturnValue("fake-dirname"); - jest.spyOn(path, "join").mockReturnValue("/fake/join/path"); + mocks.readFileSync.mockReturnValue({}); + mocks.findUpSync.mockReturnValue("fake_find-up_sync_result"); mocks.ConfigUtils.readExtendersJson.mockReturnValue({ profileTypes: { "zosmf": { @@ -118,8 +110,13 @@ describe("PMF: Install Interface", () => { } }); mocks.ConfigUtils.writeExtendersJson.mockImplementation(); - mocks.ConfigSchema_loadSchema.mockReturnValue([mockTypeConfig]); - mocks.ConfigurationLoader_load.mockReturnValue({ profiles: [mockTypeConfig] } as any); + mocks.ConfigSchema.loadSchema.mockReturnValue([mockTypeConfig]); + mocks.ConfigSchema.updateSchema.mockImplementation(); + mocks.loadConfiguration.mockReturnValue({ profiles: [mockTypeConfig] }); + mocks.spawnSync.mockReturnValue({ + status: 0, + stdout: Buffer.from(`+ ${packageName}`) + } as any); }); afterAll(() => { @@ -133,8 +130,8 @@ describe("PMF: Install Interface", () => { * @param {string} expectedRegistry The registry that should be sent to npm install */ const wasNpmInstallCallValid = (expectedPackage: string, expectedRegistry: string, updateSchema?: boolean) => { - expect(mocks.installPackages).toHaveBeenCalledWith(PMFConstants.instance.PLUGIN_INSTALL_LOCATION, - expectedRegistry, expectedPackage); + expect(mocks.installPackages).toHaveBeenCalledWith(expectedPackage, + { prefix: PMFConstants.instance.PLUGIN_INSTALL_LOCATION, registry: expectedRegistry }); shouldUpdateSchema(updateSchema ?? true); }; @@ -142,16 +139,16 @@ describe("PMF: Install Interface", () => { * Validates that plugins install call updates the global schema. */ const shouldUpdateSchema = (shouldUpdate: boolean) => { - expect(mocks.PMF_requirePluginModuleCallback).toHaveBeenCalledTimes(1); - expect(mocks.ConfigurationLoader_load).toHaveBeenCalledTimes(1); + expect(mocks.requirePluginModuleCallback).toHaveBeenCalledTimes(1); + expect(mocks.loadConfiguration).toHaveBeenCalledTimes(1); if (shouldUpdate) { - expect(mocks.UpdateImpConfig_addProfiles).toHaveBeenCalledTimes(1); - expect(mocks.ConfigSchema_updateSchema).toHaveBeenCalledTimes(1); - expect(mocks.ConfigSchema_updateSchema).toHaveBeenCalledWith(expect.objectContaining({ layer: "global" })); + expect(mocks.addProfiles).toHaveBeenCalledTimes(1); + expect(mocks.ConfigSchema.updateSchema).toHaveBeenCalledTimes(1); + expect(mocks.ConfigSchema.updateSchema).toHaveBeenCalledWith(expect.objectContaining({ layer: "global" })); } else { - expect(mocks.UpdateImpConfig_addProfiles).not.toHaveBeenCalled(); - expect(mocks.ConfigSchema_updateSchema).not.toHaveBeenCalled(); + expect(mocks.addProfiles).not.toHaveBeenCalled(); + expect(mocks.ConfigSchema.updateSchema).not.toHaveBeenCalled(); } }; @@ -182,9 +179,8 @@ describe("PMF: Install Interface", () => { describe("Basic install", () => { beforeEach(() => { - mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: packageVersion } as never); + mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: packageVersion }); jest.spyOn(fs, "existsSync").mockReturnValue(true); - jest.spyOn(path, "normalize").mockReturnValue("testing"); jest.spyOn(fs, "lstatSync").mockReturnValue({ isSymbolicLink: jest.fn().mockReturnValue(true) } as any); @@ -192,7 +188,7 @@ describe("PMF: Install Interface", () => { it("should install from the npm registry", async () => { setResolve(packageName); - await install(packageName, packageRegistry); + await install(packageName, registryInfo); // Validate the install wasNpmInstallCallValid(packageName, packageRegistry); @@ -208,7 +204,7 @@ describe("PMF: Install Interface", () => { jest.spyOn(path, "isAbsolute").mockReturnValueOnce(true); setResolve(rootFile); - await install(rootFile, packageRegistry); + await install(rootFile, registryInfo); // Validate the install wasNpmInstallCallValid(rootFile, packageRegistry); @@ -224,7 +220,7 @@ describe("PMF: Install Interface", () => { jest.spyOn(path, "isAbsolute").mockReturnValueOnce(true); setResolve(rootFile); - await install(rootFile, packageRegistry); + await install(rootFile, registryInfo); // Validate the install wasNpmInstallCallValid(rootFile, packageRegistry); @@ -251,7 +247,7 @@ describe("PMF: Install Interface", () => { // Call the install function setResolve(relativePath, absolutePath); - await install(relativePath, packageRegistry); + await install(relativePath, registryInfo); // Validate results wasNpmInstallCallValid(absolutePath, packageRegistry); @@ -269,7 +265,7 @@ describe("PMF: Install Interface", () => { // mocks.isUrl.mockReturnValue(true); - await install(installUrl, packageRegistry); + await install(installUrl, registryInfo); wasNpmInstallCallValid(installUrl, packageRegistry); wasWriteFileSyncCallValid({}, packageName, { @@ -280,9 +276,9 @@ describe("PMF: Install Interface", () => { }); it("should install plugin that does not define profiles", async () => { - mocks.ConfigurationLoader_load.mockReturnValueOnce({} as any); + mocks.loadConfiguration.mockReturnValueOnce({}); setResolve(packageName); - await install(packageName, packageRegistry); + await install(packageName, registryInfo); // Validate the install wasNpmInstallCallValid(packageName, packageRegistry, false); @@ -301,14 +297,13 @@ describe("PMF: Install Interface", () => { const location = "/this/should/not/change"; jest.spyOn(path, "isAbsolute").mockReturnValueOnce(false); - jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); - mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: packageVersion } as never); - jest.spyOn(path, "normalize").mockReturnValue("testing"); + jest.spyOn(fs, "existsSync").mockReturnValue(true); + mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: packageVersion }); jest.spyOn(fs, "lstatSync").mockReturnValue({ isSymbolicLink: jest.fn().mockReturnValue(true) } as any); - await install(location, packageRegistry, true); + await install(location, registryInfo, true); wasNpmInstallCallValid(location, packageRegistry); expect(mocks.writeFileSync).toHaveBeenCalled(); @@ -319,7 +314,6 @@ describe("PMF: Install Interface", () => { const semverPackage = `${packageName}@${semverVersion}`; jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); - jest.spyOn(path, "normalize").mockReturnValue("testing"); jest.spyOn(fs, "lstatSync").mockReturnValue({ isSymbolicLink: jest.fn().mockReturnValue(true) } as any); @@ -329,11 +323,11 @@ describe("PMF: Install Interface", () => { jest.spyOn(path, "isAbsolute").mockReturnValueOnce(true); // This is valid under semver ^1.5.2 - mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: "1.5.16" } as never); + mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: "1.5.16" }); // Call the install setResolve(semverPackage); - await install(semverPackage, packageRegistry); + await install(semverPackage, registryInfo); // Test that shit happened wasNpmInstallCallValid(semverPackage, packageRegistry); @@ -354,16 +348,15 @@ describe("PMF: Install Interface", () => { } }; - mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: packageVersion } as never); + mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: packageVersion }); jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); - jest.spyOn(path, "normalize").mockReturnValue("testing"); jest.spyOn(fs, "lstatSync").mockReturnValue({ isSymbolicLink: jest.fn().mockReturnValue(true) } as any); - mocks.readFileSync.mockReturnValue(oneOldPlugin as any); + mocks.readFileSync.mockReturnValue(oneOldPlugin); setResolve(packageName); - await install(packageName, packageRegistry); + await install(packageName, registryInfo); wasNpmInstallCallValid(packageName, packageRegistry); wasWriteFileSyncCallValid(oneOldPlugin, packageName, { @@ -389,20 +382,19 @@ describe("PMF: Install Interface", () => { }; if (opts.newProfileType) { const schema = { ...mockTypeConfig, schema: { ...mockTypeConfig.schema, version: opts.version } }; - mocks.ConfigurationLoader_load.mockReturnValue({ + mocks.loadConfiguration.mockReturnValue({ profiles: [ schema ] - } as any); + }); } - mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: packageVersion } as never); + mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: packageVersion }); jest.spyOn(fs, "existsSync").mockReturnValueOnce(true).mockReturnValueOnce(opts.schemaExists); - jest.spyOn(path, "normalize").mockReturnValue("testing"); jest.spyOn(fs, "lstatSync").mockReturnValue({ isSymbolicLink: jest.fn().mockReturnValue(true) } as any); - mocks.readFileSync.mockReturnValue(oneOldPlugin as any); + mocks.readFileSync.mockReturnValue(oneOldPlugin); if (opts.lastVersion) { mocks.ConfigUtils.readExtendersJson.mockReturnValueOnce({ @@ -417,11 +409,11 @@ describe("PMF: Install Interface", () => { } setResolve(packageName); - await install(packageName, packageRegistry); + await install(packageName, registryInfo); if (opts.schemaExists) { - expect(mocks.ConfigSchema_updateSchema).toHaveBeenCalled(); + expect(mocks.ConfigSchema.updateSchema).toHaveBeenCalled(); } else { - expect(mocks.ConfigSchema_updateSchema).not.toHaveBeenCalled(); + expect(mocks.ConfigSchema.updateSchema).not.toHaveBeenCalled(); } if (opts.version && opts.lastVersion) { @@ -501,7 +493,7 @@ describe("PMF: Install Interface", () => { }); try { - await install("test", "http://www.example.com"); + await install("test", registryInfo); } catch (e) { expectedError = e; } @@ -629,7 +621,7 @@ describe("PMF: Install Interface", () => { } catch (err) { thrownErr = err; } - expect(requirePluginModuleCallbackSpy as any).toHaveBeenCalledTimes(1); + expect(requirePluginModuleCallbackSpy).toHaveBeenCalledTimes(1); expect(thrownErr).not.toBeDefined(); expect(postInstallWorked).toBe(true); }); diff --git a/packages/imperative/src/imperative/__tests__/plugins/utilities/npm-interface/uninstall.unit.test.ts b/packages/imperative/src/imperative/__tests__/plugins/utilities/npm-interface/uninstall.unit.test.ts index 49714cfa17..5c396ccdbb 100644 --- a/packages/imperative/src/imperative/__tests__/plugins/utilities/npm-interface/uninstall.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/plugins/utilities/npm-interface/uninstall.unit.test.ts @@ -10,24 +10,18 @@ */ /* eslint-disable jest/expect-expect */ -import Mock = jest.Mock; - jest.mock("cross-spawn"); jest.mock("jsonfile"); jest.mock("../../../../src/plugins/utilities/PMFConstants"); -jest.mock("../../../../../logger"); -jest.mock("../../../../../cmd/src/response/CommandResponse"); -jest.mock("../../../../../cmd/src/response/HandlerResponse"); import * as fs from "fs"; import * as jsonfile from "jsonfile"; import { Console } from "../../../../../console"; -import { sync } from "cross-spawn"; +import * as spawn from "cross-spawn"; import { ImperativeError } from "../../../../../error"; import { IPluginJson } from "../../../../src/plugins/doc/IPluginJson"; import { Logger } from "../../../../../logger"; import { PMFConstants } from "../../../../src/plugins/utilities/PMFConstants"; -import { readFileSync, writeFileSync } from "jsonfile"; import { findNpmOnPath } from "../../../../src/plugins/utilities/NpmFunctions"; import { uninstall } from "../../../../src/plugins/utilities/npm-interface"; import { ConfigSchema, ConfigUtils } from "../../../../../config"; @@ -39,9 +33,9 @@ import { updateAndGetRemovedTypes } from "../../../../src/plugins/utilities/npm- describe("PMF: Uninstall Interface", () => { // Objects created so types are correct. const mocks = { - spawnSync: sync as unknown as Mock, - readFileSync: readFileSync as Mock, - writeFileSync: writeFileSync as Mock + spawnSync: jest.spyOn(spawn, "sync"), + readFileSync: jest.spyOn(jsonfile, "readFileSync"), + writeFileSync: jest.spyOn(jsonfile, "writeFileSync") }; const samplePackageName = "imperative-sample-plugin"; @@ -54,7 +48,7 @@ describe("PMF: Uninstall Interface", () => { jest.resetAllMocks(); // This needs to be mocked before running uninstall - (Logger.getImperativeLogger as unknown as Mock).mockReturnValue(new Logger(new Console()) as any); + jest.spyOn(Logger, "getImperativeLogger").mockReturnValue(new Logger(new Console())); }); afterAll(() => { @@ -127,7 +121,7 @@ describe("PMF: Uninstall Interface", () => { } }; - mocks.readFileSync.mockReturnValue(pluginJsonFile as any); + mocks.readFileSync.mockReturnValue(pluginJsonFile); uninstall(packageName); @@ -151,7 +145,7 @@ describe("PMF: Uninstall Interface", () => { } }; - mocks.readFileSync.mockReturnValue(pluginJsonFile as any); + mocks.readFileSync.mockReturnValue(pluginJsonFile); uninstall(samplePackageName); @@ -192,7 +186,7 @@ describe("PMF: Uninstall Interface", () => { } }; - mocks.readFileSync.mockReturnValue(pluginJsonFile as any); + mocks.readFileSync.mockReturnValue(pluginJsonFile); jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); let caughtError; @@ -244,7 +238,7 @@ describe("PMF: Uninstall Interface", () => { } }; - mocks.readFileSync.mockReturnValue(pluginJsonFile as any); + mocks.readFileSync.mockReturnValue(pluginJsonFile); const blockMocks = getBlockMocks(); if (opts.schemaUpdated) { blockMocks.fs.existsSync.mockReturnValueOnce(true); diff --git a/packages/imperative/src/imperative/__tests__/plugins/utilities/npm-interface/update.unit.test.ts b/packages/imperative/src/imperative/__tests__/plugins/utilities/npm-interface/update.unit.test.ts index f873f360e7..240fc1eacf 100644 --- a/packages/imperative/src/imperative/__tests__/plugins/utilities/npm-interface/update.unit.test.ts +++ b/packages/imperative/src/imperative/__tests__/plugins/utilities/npm-interface/update.unit.test.ts @@ -9,30 +9,25 @@ * */ -import Mock = jest.Mock; - jest.mock("cross-spawn"); jest.mock("jsonfile"); jest.mock("../../../../src/plugins/utilities/PMFConstants"); -jest.mock("../../../../../logger"); -jest.mock("../../../../../cmd/src/response/CommandResponse"); -jest.mock("../../../../../cmd/src/response/HandlerResponse"); -jest.mock("../../../../src/plugins/utilities/NpmFunctions"); import { Console } from "../../../../../console"; import { IPluginJson } from "../../../../src/plugins/doc/IPluginJson"; import { Logger } from "../../../../../logger"; import { PMFConstants } from "../../../../src/plugins/utilities/PMFConstants"; -import { readFileSync } from "jsonfile"; +import * as jsonfile from "jsonfile"; import { update } from "../../../../src/plugins/utilities/npm-interface"; -import { getPackageInfo, installPackages } from "../../../../src/plugins/utilities/NpmFunctions"; +import * as npmFns from "../../../../src/plugins/utilities/NpmFunctions"; describe("PMF: update Interface", () => { // Objects created so types are correct. const mocks = { - installPackages: installPackages as unknown as Mock, - readFileSync: readFileSync as Mock, - getPackageInfo: getPackageInfo as unknown as Mock + installPackages: jest.spyOn(npmFns, "installPackages"), + readFileSync: jest.spyOn(jsonfile, "readFileSync"), + getPackageInfo: jest.spyOn(npmFns, "getPackageInfo"), + getScopeRegistry: jest.spyOn(npmFns.NpmRegistryUtils as any, "getScopeRegistry") }; const packageName = "pretty-format"; @@ -44,12 +39,13 @@ describe("PMF: update Interface", () => { jest.resetAllMocks(); // This needs to be mocked before running update - (Logger.getImperativeLogger as unknown as Mock).mockReturnValue(new Logger(new Console()) as any); + jest.spyOn(Logger, "getImperativeLogger").mockReturnValue(new Logger(new Console())); /* Since update() adds new plugins into the value returned from - * readFileSyc(plugins.json), we must reset readFileSync to return an empty set before each test. + * readFileSync(plugins.json), we must reset readFileSync to return an empty set before each test. */ - mocks.readFileSync.mockReturnValue({} as any); + mocks.readFileSync.mockReturnValue({}); + mocks.getScopeRegistry.mockReturnValue(packageRegistry); }); afterAll(() => { @@ -64,51 +60,53 @@ describe("PMF: update Interface", () => { * @param {boolean} [updateFromFile=false] was the update from a file. This affects * the pipe sent to spawnSync stdio option. */ - const wasNpmInstallCallValid = (expectedPackage: string, expectedRegistry: string) => { - expect(mocks.installPackages).toHaveBeenCalledWith(PMFConstants.instance.PLUGIN_INSTALL_LOCATION, - expectedRegistry, expectedPackage); + const wasNpmInstallCallValid = (expectedPackage: string, expectedRegistry: Record) => { + expect(mocks.installPackages).toHaveBeenCalledWith(expectedPackage, + { prefix: PMFConstants.instance.PLUGIN_INSTALL_LOCATION, ...expectedRegistry }); }; - describe("Basic update", () => { - it("should update from the npm registry", async () => { + it("should update from the npm registry", async () => { - // value for our plugins.json - const oneOldPlugin: IPluginJson = { - plugin1: { - package: packageName, - location: packageRegistry, - version: packageVersion - } - }; + // value for our plugins.json + const oneOldPlugin: IPluginJson = { + plugin1: { + package: packageName, + location: packageRegistry, + version: packageVersion + } + }; - mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: packageVersion } as never); - mocks.readFileSync.mockReturnValue(oneOldPlugin as any); + mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: packageVersion }); + mocks.readFileSync.mockReturnValue(oneOldPlugin); - const data = await update(packageName, packageRegistry); - expect(data).toEqual(packageVersion); + const registryInfo = npmFns.NpmRegistryUtils.buildRegistryInfo(oneOldPlugin.plugin1); + const data = await update(packageName, registryInfo); + expect(data).toEqual(packageVersion); - // Validate the update - wasNpmInstallCallValid(packageName, packageRegistry); - }); + // Validate the update + wasNpmInstallCallValid(packageName, { registry: packageRegistry }); }); - it("should update from the npm registry", async () => { + + it("should update from scoped npm registry", async () => { // value for our plugins.json + const scopedPackageName = `@org/${packageName}`; const oneOldPlugin: IPluginJson = { plugin1: { - package: packageName, + package: scopedPackageName, location: packageRegistry, version: packageVersion } }; - mocks.getPackageInfo.mockResolvedValue({ name: packageName, version: packageVersion } as never); - mocks.readFileSync.mockReturnValue(oneOldPlugin as any); + mocks.getPackageInfo.mockResolvedValue({ name: scopedPackageName, version: packageVersion }); + mocks.readFileSync.mockReturnValue(oneOldPlugin); - const data = await update(packageName, packageRegistry); + const registryInfo = npmFns.NpmRegistryUtils.buildRegistryInfo(oneOldPlugin.plugin1); + const data = await update(scopedPackageName, registryInfo); expect(data).toEqual(packageVersion); // Validate the update - wasNpmInstallCallValid(packageName, packageRegistry); + wasNpmInstallCallValid(scopedPackageName, { registry: packageRegistry, "@org:registry": packageRegistry }); }); }); diff --git a/packages/imperative/src/imperative/src/plugins/PluginManagementFacility.ts b/packages/imperative/src/imperative/src/plugins/PluginManagementFacility.ts index 58094e46a5..bb152f156b 100644 --- a/packages/imperative/src/imperative/src/plugins/PluginManagementFacility.ts +++ b/packages/imperative/src/imperative/src/plugins/PluginManagementFacility.ts @@ -9,8 +9,8 @@ * */ -import { IImperativeConfig } from "../../src/doc/IImperativeConfig"; -import { UpdateImpConfig } from "../../src/UpdateImpConfig"; +import { IImperativeConfig } from "../doc/IImperativeConfig"; +import { UpdateImpConfig } from "../UpdateImpConfig"; import { isAbsolute, join } from "path"; import { ImperativeConfig, JsUtils } from "../../../utilities"; import { Logger } from "../../../logger"; diff --git a/packages/imperative/src/imperative/src/plugins/cmd/install/install.handler.ts b/packages/imperative/src/imperative/src/plugins/cmd/install/install.handler.ts index 9f36fa32c7..e49163d0d4 100644 --- a/packages/imperative/src/imperative/src/plugins/cmd/install/install.handler.ts +++ b/packages/imperative/src/imperative/src/plugins/cmd/install/install.handler.ts @@ -20,17 +20,14 @@ import { readFileSync } from "jsonfile"; import { ImperativeConfig, TextUtils } from "../../../../../utilities"; import { ImperativeError } from "../../../../../error"; import { runValidatePlugin } from "../../utilities/runValidatePlugin"; -import { - getRegistry, - getScopeRegistry, - npmLogin, -} from "../../utilities/NpmFunctions"; -import { IO } from "../../../../../io"; +import { NpmRegistryUtils } from "../../utilities/NpmFunctions"; + /** * The install command handler for cli plugin install. * * @see {installDefinition} - */ export default class InstallHandler implements ICommandHandler { + */ +export default class InstallHandler implements ICommandHandler { /** * A logger for this class * @@ -72,180 +69,89 @@ import { IO } from "../../../../../io"; * * @throws {ImperativeError} */ - - private locationTypeTest(plugin: string){ - let isDirTest: boolean; - let installRegistry = getRegistry(); - try { - isDirTest = IO.isDir(plugin); - } catch (e) { - isDirTest = false; - } - - if (plugin.startsWith("@")) { - installRegistry = getScopeRegistry( - plugin.split("/")[0].substring(1) - ).replace("\n", ""); - } else if ( - plugin.substring(plugin.lastIndexOf(".") + 1) === "tgz" || - isDirTest - ) { - installRegistry = plugin; - } - return installRegistry; - } - public async process(params: IHandlerParameters): Promise { const chalk = TextUtils.chalk; - this.console.debug( - `Root Directory: ${PMFConstants.instance.PLUGIN_INSTALL_LOCATION}` - ); + this.console.debug(`Root Directory: ${PMFConstants.instance.PLUGIN_INSTALL_LOCATION}`); - if ( - params.arguments.plugin != null && - params.arguments.plugin.length > 0 && - typeof params.arguments.file !== "undefined" - ) { + if (params.arguments.plugin != null && params.arguments.plugin.length > 0 && typeof params.arguments.file !== "undefined") { throw new ImperativeError({ - msg: - `Option ${chalk.yellow.bold( - "--file" - )} can not be specified if positional ${chalk.yellow.bold( - "package..." - )} is as well. ` + `They are mutually exclusive.`, + msg: `Option ${chalk.yellow.bold("--file")} can not be specified if positional ${chalk.yellow.bold("package...")} is as well. ` + + `They are mutually exclusive.` }); } else { try { - let installRegistry = - params.arguments.registry ?? - getRegistry().replace("\n", ""); + const installRegistry = NpmRegistryUtils.getRegistry(params.arguments.registry); + + // Get the registry to install to + if (params.arguments.registry != null && params.arguments.login) { + NpmRegistryUtils.npmLogin(installRegistry); + } + + params.response.console.log( + "Plug-ins within the Imperative CLI Framework can legitimately gain\n" + + `control of the ${ImperativeConfig.instance.rootCommandName} CLI application ` + + "during the execution of every command.\n" + + "Install 3rd party plug-ins at your own risk.\n" + ); // This section determines which npm logic needs to take place - if ( - params.arguments.plugin == null || - params.arguments.plugin.length === 0 - ) { - const configFile = - typeof params.arguments.file === "undefined" - ? PMFConstants.instance.PLUGIN_JSON - : resolve(params.arguments.file); + if (params.arguments.plugin == null || params.arguments.plugin.length === 0) { + const configFile = typeof params.arguments.file === "undefined" ? + PMFConstants.instance.PLUGIN_JSON : resolve(params.arguments.file); - this.console.debug( - "Need to install using plugins.json file" - ); + this.console.debug("Need to install using plugins.json file"); this.console.debug(`Using config file: ${configFile}`); // Attempt to load that file and formulate the corresponding package const packageJson: IPluginJson = readFileSync(configFile); if (Object.keys(packageJson).length === 0) { - params.response.console.log( - "No packages were found in " + - configFile + - ", so no plugins were installed." - ); + params.response.console.log("No packages were found in " + + configFile + ", so no plugins were installed."); return; } for (const packageName in packageJson) { - if ( - Object.prototype.hasOwnProperty.call( - packageJson, - packageName - ) - ) { - const packageInfo: IPluginJsonObject = - packageJson[packageName]; + if (Object.prototype.hasOwnProperty.call(packageJson, packageName)) { + const packageInfo: IPluginJsonObject = packageJson[packageName]; // Registry is typed as optional in the doc but the function expects it // to be passed. So we'll always set it if it hasn't been done yet. + const registryInfo = NpmRegistryUtils.buildRegistryInfo(packageInfo, params.arguments.registry); if (!packageInfo.location) { - packageInfo.location = installRegistry; + packageInfo.location = registryInfo.location; } - installRegistry = this.locationTypeTest(packageInfo.location); - - this.console.debug( - `Installing plugin: ${packageName}` - ); - this.console.debug( - `Package: ${packageInfo.package}` - ); - this.console.debug( - `Location: ${packageInfo.location}` - ); - this.console.debug( - `Version : ${packageInfo.version}` - ); + this.console.debug(`Installing plugin: ${packageName}`); + this.console.debug(`Package: ${packageInfo.package}`); + this.console.debug(`Location: ${packageInfo.location}`); + this.console.debug(`Version : ${packageInfo.version}`); // Get the argument to the install command // For simplicity a / or \ indicates that we are not dealing with an npm package - const packageArgument = - packageInfo.package === packageName - ? `${packageInfo.package}@${packageInfo.version}` - : packageInfo.package; + const packageArgument = packageInfo.package === packageName ? + `${packageInfo.package}@${packageInfo.version}` : packageInfo.package; this.console.debug(`Package: ${packageArgument}`); - params.response.console.log( - "Plug-ins within the Imperative CLI Framework can legitimately gain\n" + - `control of the ${ImperativeConfig.instance.rootCommandName} CLI application ` + - "during the execution of every command.\n" + - "Install 3rd party plug-ins at your own risk.\n" - ); - params.response.console.log( - "Location = " + installRegistry - ); - - params.response.console.log( - "\n_______________________________________________________________" - ); - const pluginName = await install( - packageArgument, - packageInfo.location, - true - ); - params.response.console.log( - "Installed plugin name = '" + pluginName + "'" - ); - params.response.console.log( - runValidatePlugin(pluginName) - ); + params.response.console.log("\n_______________________________________________________________"); + params.response.console.log("Location = " + packageInfo.location + "\n"); + const pluginName = await install(packageArgument, registryInfo, true); + params.response.console.log("Installed plugin name = '" + pluginName + "'"); + params.response.console.log(runValidatePlugin(pluginName)); } } - } - for (const plugin of params.arguments.plugin ?? []) { - // Get the registry to install to - if (typeof params.arguments.registry === "undefined") { - installRegistry = this.locationTypeTest(plugin); - } else { - installRegistry = params.arguments.registry; - if (params.arguments.login) { - npmLogin(installRegistry); - } + // write the json file when done if not the plugin json file + } else { + for (const packageString of params.arguments.plugin) { + params.response.console.log("\n_______________________________________________________________"); + const registryInfo = NpmRegistryUtils.buildRegistryInfo(packageString, params.arguments.registry); + params.response.console.log("Location = " + registryInfo.location + "\n"); + const pluginName = await install(packageString, registryInfo); + params.response.console.log("Installed plugin name = '" + pluginName + "'"); + params.response.console.log(runValidatePlugin(pluginName)); } - params.response.console.log( - "Plug-ins within the Imperative CLI Framework can legitimately gain\n" + - `control of the ${ImperativeConfig.instance.rootCommandName} CLI application ` + - "during the execution of every command.\n" + - "Install 3rd party plug-ins at your own risk.\n" - ); - params.response.console.log( - "Location = " + installRegistry - ); - - params.response.console.log( - "\n_______________________________________________________________" - ); - const pluginName = await install( - `${plugin}`, - installRegistry - ); - params.response.console.log( - "Installed plugin name = '" + pluginName + "'" - ); - params.response.console.log(runValidatePlugin(pluginName)); } } catch (e) { let installResultMsg = "Install Failed"; @@ -253,25 +159,19 @@ import { IO } from "../../../../../io"; * give a special message, as per UX request. */ if (e.mMessage) { - const matchArray = e.mMessage.match( - /The intended symlink.*already exists and is not a symbolic link/ - ); + const matchArray = e.mMessage.match(/The intended symlink.*already exists and is not a symbolic link/); if (matchArray !== null) { - installResultMsg = - "Installation completed. However, the plugin incorrectly contains\nits own copy of " + + installResultMsg = "Installation completed. However, the plugin incorrectly contains\nits own copy of " + `${PMFConstants.instance.CLI_CORE_PKG_NAME} or ${PMFConstants.instance.IMPERATIVE_PKG_NAME}.\n` + "Some plugin operations may not work correctly."; - } else if ( - e.mMessage.includes("Failed to create symbolic link") - ) { - installResultMsg = - "Installation completed. However, due to the following error, the plugin will not operate correctly."; + } else if (e.mMessage.includes("Failed to create symbolic link")) { + installResultMsg = "Installation completed. However, due to the following error, the plugin will not operate correctly."; } } throw new ImperativeError({ msg: installResultMsg, causeErrors: e, - additionalDetails: e.message, + additionalDetails: e.message }); } } diff --git a/packages/imperative/src/imperative/src/plugins/cmd/update/update.handler.ts b/packages/imperative/src/imperative/src/plugins/cmd/update/update.handler.ts index 551af090b4..93b2a2b8f7 100644 --- a/packages/imperative/src/imperative/src/plugins/cmd/update/update.handler.ts +++ b/packages/imperative/src/imperative/src/plugins/cmd/update/update.handler.ts @@ -17,7 +17,7 @@ import { ImperativeError } from "../../../../../error"; import { TextUtils } from "../../../../../utilities"; import { IPluginJson } from "../../doc/IPluginJson"; import { readFileSync, writeFileSync } from "jsonfile"; -import { npmLogin } from "../../utilities/NpmFunctions"; +import { NpmRegistryUtils } from "../../utilities/NpmFunctions"; /** * The update command handler for cli plugin install. @@ -52,7 +52,7 @@ export default class UpdateHandler implements ICommandHandler { this.console.debug(`Root Directory: ${PMFConstants.instance.PLUGIN_INSTALL_LOCATION}`); const plugin: string = params.arguments.plugin; - let registry = params.arguments.registry; + const registry = NpmRegistryUtils.getRegistry(params.arguments.registry); if (params.arguments.plugin == null || params.arguments.plugin.length === 0) { throw new ImperativeError({ @@ -63,8 +63,8 @@ export default class UpdateHandler implements ICommandHandler { iConsole.debug("Reading in the current configuration."); const installedPlugins: IPluginJson = readFileSync(PMFConstants.instance.PLUGIN_JSON); - if (params.arguments.login) { - npmLogin(registry); + if (params.arguments.registry != null && params.arguments.login) { + NpmRegistryUtils.npmLogin(registry); } if (Object.prototype.hasOwnProperty.call(installedPlugins, plugin)) { @@ -76,12 +76,10 @@ export default class UpdateHandler implements ICommandHandler { // as package may not match the plugin value. This is true for plugins installed by // folder location. Example: plugin 'imperative-sample-plugin' installed from ../imperative-plugins packageName = installedPlugins[pluginName].package; - if (registry === undefined) { - registry = installedPlugins[pluginName].location; - } + const registryInfo = NpmRegistryUtils.buildRegistryInfo(installedPlugins[pluginName], params.arguments.registry); // Call update which returns the plugin's version so plugins.json can be updated - installedPlugins[pluginName].version = await update(packageName, registry); - installedPlugins[pluginName].location = registry; // update in case it changed + installedPlugins[pluginName].version = await update(packageName, registryInfo); + installedPlugins[pluginName].location = registryInfo.location; // update in case it changed writeFileSync(PMFConstants.instance.PLUGIN_JSON, installedPlugins, { spaces: 2 diff --git a/packages/imperative/src/imperative/src/plugins/doc/INpmInstallArgs.ts b/packages/imperative/src/imperative/src/plugins/doc/INpmInstallArgs.ts new file mode 100644 index 0000000000..873740303e --- /dev/null +++ b/packages/imperative/src/imperative/src/plugins/doc/INpmInstallArgs.ts @@ -0,0 +1,30 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +/** + * Npm config options passed to the install command. + */ +export interface INpmInstallArgs { + /** + * The location to install global packages + */ + prefix: string; + + /** + * The base URL of the npm package registry + */ + registry?: string; + + /** + * Allows us to handle scoped registries in the future + */ + [key: string]: string; +} diff --git a/packages/imperative/src/imperative/src/plugins/doc/INpmRegistryInfo.ts b/packages/imperative/src/imperative/src/plugins/doc/INpmRegistryInfo.ts new file mode 100644 index 0000000000..220f675a5d --- /dev/null +++ b/packages/imperative/src/imperative/src/plugins/doc/INpmRegistryInfo.ts @@ -0,0 +1,27 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { INpmInstallArgs } from "./INpmInstallArgs"; + +/** + * Location info for an npm package. + */ +export interface INpmRegistryInfo { + /** + * The origin of npm package (registry URL or absolute path) + */ + location: string; + + /** + * Defines npm config values to pass to `npm install` command + */ + npmArgs: Partial; +} diff --git a/packages/imperative/src/imperative/src/plugins/utilities/NpmFunctions.ts b/packages/imperative/src/imperative/src/plugins/utilities/NpmFunctions.ts index 0acb75ac1a..b400464b07 100644 --- a/packages/imperative/src/imperative/src/plugins/utilities/NpmFunctions.ts +++ b/packages/imperative/src/imperative/src/plugins/utilities/NpmFunctions.ts @@ -17,7 +17,9 @@ import { readFileSync } from "jsonfile"; import * as npmPackageArg from "npm-package-arg"; import * as pacote from "pacote"; import { ExecUtils } from "../../../../utilities"; -import { IO } from "../../../../io"; +import { INpmInstallArgs } from "../doc/INpmInstallArgs"; +import { IPluginJsonObject } from "../doc/IPluginJsonObject"; +import { INpmRegistryInfo } from "../doc/INpmRegistryInfo"; const npmCmd = findNpmOnPath(); /** @@ -40,29 +42,15 @@ export function findNpmOnPath(): string { * @return {string} command response * */ -export function installPackages(prefix: string, registry: string, npmPackage: string): string { +export function installPackages(npmPackage: string, npmArgs: INpmInstallArgs): string { const pipe: StdioOptions = ["pipe", "pipe", process.stderr]; - - const args = [ - "install", npmPackage, - "--prefix", prefix, - "-g" - ]; - let isDirTest: boolean; - - try{ - isDirTest = IO.isDir(registry); - } - catch(e){ - isDirTest = false; - } - - if (!(registry.substring(registry.lastIndexOf(".") + 1) === "tgz") && !isDirTest) { - args.push("--registry",registry); + const args = ["install", npmPackage, "-g", "--legacy-peer-deps"]; + for (const [k, v] of Object.entries(npmArgs)) { + if (v != null) { + // If npm arg starts with @ like @zowe:registry, must use = as separator + args.push(...k.startsWith("@") ? [`--${k}=${v}`] : [`--${k}`, v]); + } } - - args.push("--legacy-peer-deps"); - const execOutput = ExecUtils.spawnAndGetOutput(npmCmd, args, { cwd: PMFConstants.instance.PMF_ROOT, stdio: pipe @@ -71,40 +59,6 @@ export function installPackages(prefix: string, registry: string, npmPackage: st return execOutput.toString(); } - -/** - * Get the registry to install to. - * - * @return {string} - */ -export function getRegistry(): string { - const execOutput = ExecUtils.spawnAndGetOutput(npmCmd, [ "config", "get", "registry" ]); - return execOutput.toString(); -} - -export function getScopeRegistry(scope: string): string { - const execOutput = ExecUtils.spawnAndGetOutput(npmCmd, [ "config", "get", `@${scope}:registry` ]); - if(execOutput.toString().trim() === 'undefined') return getRegistry(); - return execOutput.toString(); -} - -/** - * NPM login to be able to install from secure registry - * @param {string} registry The npm registry to install from. - */ -export function npmLogin(registry: string) { - ExecUtils.spawnAndGetOutput(npmCmd, - [ - "login", - "--registry", registry, - "--always-auth", - "--auth-type=legacy" - ], { - stdio: [0,1,2] - } - ); -} - /** * Fetch name and version of NPM package that was installed * @param pkgSpec The package name as specified on NPM install @@ -119,3 +73,78 @@ export async function getPackageInfo(pkgSpec: string): Promise<{ name: string, v return pacote.manifest(pkgSpec); } } + +export class NpmRegistryUtils { + /** + * Get the registry to install to. + * @param userRegistry Registry override specified on the command line + * @return {string} + */ + public static getRegistry(userRegistry?: string): string { + if (userRegistry != null) return userRegistry; + const execOutput = ExecUtils.spawnAndGetOutput(npmCmd, ["config", "get", "registry"]); + return execOutput.toString().replace("\n", ""); + } + + /** + * NPM login to be able to install from secure registry + * @param {string} registry The npm registry to install from. + */ + public static npmLogin(registry: string) { + ExecUtils.spawnAndGetOutput(npmCmd, + [ + "login", + "--registry", registry, + "--always-auth", + "--auth-type=legacy" + ], + { stdio: "inherit" } + ); + } + + /** + * Get package location and npm registry args for installing it. + * @param packageInfo Plugin name or object from plugins.json + * @param userRegistry Registry override specified on the command line + * @returns Location info for npm package to be installed + */ + public static buildRegistryInfo(packageInfo: string | IPluginJsonObject, userRegistry?: string): INpmRegistryInfo { + const packageName = typeof packageInfo === "string" ? packageInfo : packageInfo.package; + const packageScope = packageName.startsWith("@") ? packageName.split("/")[0] : undefined; + if (userRegistry != null) { + // If --registry was passed on the command line, it takes precedence + return { + location: userRegistry, + npmArgs: this.buildRegistryNpmArgs(userRegistry, packageScope) + }; + } else if (typeof packageInfo === "string" || !packageInfo.location) { + // If installing a plug-in for the first time, get default registry + const defaultRegistry = this.getRegistry(); + return { + location: npmPackageArg(packageName).registry ? defaultRegistry : packageName, + npmArgs: this.buildRegistryNpmArgs(defaultRegistry, packageScope) + }; + } else { + // If updating a plug-in, fetch registry info from plugins.json + const cachedRegistry = npmPackageArg(packageInfo.package).registry ? packageInfo.location : undefined; + return { + location: packageInfo.location, + npmArgs: this.buildRegistryNpmArgs(cachedRegistry ?? this.getRegistry(), packageScope) + }; + } + } + + private static buildRegistryNpmArgs(registryUrl: string, scope?: string): Partial { + const npmArgs: INpmRegistryInfo["npmArgs"] = { registry: registryUrl }; + if (scope != null) { + npmArgs[`${scope}:registry`] = this.getScopeRegistry(scope); + } + return npmArgs; + } + + private static getScopeRegistry(scope: string): string | undefined { + const execOutput = ExecUtils.spawnAndGetOutput(npmCmd, ["config", "get", `${scope}:registry`]); + if (execOutput.toString().trim() === "undefined") return; + return execOutput.toString().replace("\n", ""); + } +} diff --git a/packages/imperative/src/imperative/src/plugins/utilities/npm-interface/install.ts b/packages/imperative/src/imperative/src/plugins/utilities/npm-interface/install.ts index ea6d4030fd..70ef2e0f39 100644 --- a/packages/imperative/src/imperative/src/plugins/utilities/npm-interface/install.ts +++ b/packages/imperative/src/imperative/src/plugins/utilities/npm-interface/install.ts @@ -28,6 +28,7 @@ import { IProfileTypeConfiguration } from "../../../../../profiles"; import * as semver from "semver"; import { ConfigUtils } from "../../../../../config"; import { IExtendersJsonOpts } from "../../../../../config/src/doc/IExtenderOpts"; +import { INpmRegistryInfo } from "../../doc/INpmRegistryInfo"; // Helper function to update extenders.json object during plugin install. // Returns true if the object was updated, and false otherwise @@ -74,9 +75,7 @@ export const updateExtendersJson = ( * be converted to an absolute path prior to being passed to the * `npm install` command. * - * @param {string} registry The npm registry to use, this is expected to be passed by every caller - * so if calling functions don't have a registry available, they need - * to get it from npm. + * @param {INpmRegistryInfo} registryInfo The npm registry to use. * * @param {boolean} [installFromFile=false] If installing from a file, the package location is * automatically interpreted as an absolute location. @@ -87,7 +86,7 @@ export const updateExtendersJson = ( * it. * @returns {string} The name of the plugin. */ -export async function install(packageLocation: string, registry: string, installFromFile = false) { +export async function install(packageLocation: string, registryInfo: INpmRegistryInfo, installFromFile = false) { const iConsole = Logger.getImperativeLogger(); let npmPackage = packageLocation; @@ -117,12 +116,15 @@ export async function install(packageLocation: string, registry: string, install } try { - iConsole.debug(`Installing from registry ${registry}`); + iConsole.debug(`Installing from registry ${registryInfo.location}`); // Perform the npm install. iConsole.info("Installing packages...this may take some time."); - installPackages(PMFConstants.instance.PLUGIN_INSTALL_LOCATION, registry, npmPackage); + installPackages(npmPackage, { + prefix: PMFConstants.instance.PLUGIN_INSTALL_LOCATION, + ...registryInfo.npmArgs, + }); // We fetch the package name and version of newly installed plugin const packageInfo = await getPackageInfo(npmPackage); @@ -150,7 +152,7 @@ export async function install(packageLocation: string, registry: string, install const newPlugin: IPluginJsonObject = { package: npmPackage, - location: registry, + location: registryInfo.location, version: packageVersion }; iConsole.debug("Updating the current configuration with new plugin:\n" + diff --git a/packages/imperative/src/imperative/src/plugins/utilities/npm-interface/update.ts b/packages/imperative/src/imperative/src/plugins/utilities/npm-interface/update.ts index 837f92bf92..44df21b6ad 100644 --- a/packages/imperative/src/imperative/src/plugins/utilities/npm-interface/update.ts +++ b/packages/imperative/src/imperative/src/plugins/utilities/npm-interface/update.ts @@ -12,6 +12,7 @@ import { PMFConstants } from "../PMFConstants"; import { Logger } from "../../../../../logger"; import { getPackageInfo, installPackages } from "../NpmFunctions"; +import { INpmRegistryInfo } from "../../doc/INpmRegistryInfo"; /** * @TODO - allow multiple packages to be updated? @@ -19,10 +20,10 @@ import { getPackageInfo, installPackages } from "../NpmFunctions"; * * @param {string} packageName A package name. This value is a valid npm package name. * - * @param {string} registry The npm registry. + * @param {INpmRegistryInfo} registryInfo The npm registry to use. * */ -export async function update(packageName: string, registry: string) { +export async function update(packageName: string, registryInfo: INpmRegistryInfo) { const iConsole = Logger.getImperativeLogger(); const npmPackage = packageName; @@ -31,7 +32,10 @@ export async function update(packageName: string, registry: string) { // NOTE: Using npm install in order to retrieve the version which may be updated iConsole.info("updating package...this may take some time."); - installPackages(PMFConstants.instance.PLUGIN_INSTALL_LOCATION, registry, npmPackage); + installPackages(npmPackage, { + prefix: PMFConstants.instance.PLUGIN_INSTALL_LOCATION, + ...registryInfo.npmArgs, + }); // We fetch the package version of newly installed plugin const packageInfo = await getPackageInfo(npmPackage); @@ -42,4 +46,3 @@ export async function update(packageName: string, registry: string) { // return the package version so the plugins.json file can be updated return packageVersion; } - diff --git a/packages/imperative/src/rest/__tests__/client/AbstractRestClient.unit.test.ts b/packages/imperative/src/rest/__tests__/client/AbstractRestClient.unit.test.ts index d77a854961..e5d7a047ea 100644 --- a/packages/imperative/src/rest/__tests__/client/AbstractRestClient.unit.test.ts +++ b/packages/imperative/src/rest/__tests__/client/AbstractRestClient.unit.test.ts @@ -426,6 +426,56 @@ describe("AbstractRestClient tests", () => { expect(error.message).toMatchSnapshot(); }); + // called IRL when socket is reused and HTTP 1.1 race condition is hit + it("should handle a socket reuse error", async () => { + const errorObject: any = { + name: "socket hang up", + message: "socket hang up", + code: "ECONNRESET" + }; + + let reusedSocket = true; + const requestFnc = jest.fn((options, callback) => { + const emitter = new MockHttpRequestResponse(); + + ProcessUtils.nextTick(() => { + callback(emitter); + + if (reusedSocket) { + emitter.reusedSocket = true; + ProcessUtils.nextTick(() => { + emitter.emit("error", errorObject); + reusedSocket = false; + }); + } else { + ProcessUtils.nextTick(() => { + emitter.emit("data", Buffer.from("\"response data\"", "utf8")); + }); + + ProcessUtils.nextTick(() => { + emitter.emit("end"); + }); + } + }); + return emitter; + }); + + (https.request as any) = requestFnc; + + let error; + let response: string = ""; + + try { + response = await RestClient.getExpectString(new Session({hostname: "test"}), "/resource"); + } catch (thrownError) { + error = thrownError; + } + + expect(error).not.toBeDefined(); + expect(response).toEqual("\"response data\""); + expect(requestFnc).toHaveBeenCalledTimes(2); + }); + it("should call http request for http requests", async () => { const requestEmitter = new MockHttpRequestResponse(); const httpRequestFnc = jest.fn((options, callback) => { @@ -1422,14 +1472,13 @@ describe("AbstractRestClient tests", () => { }); describe('buildOptions', () => { - const privateRestClient = new RestClient( - new Session({ - hostname: "FakeHostName", - type: AUTH_TYPE_CERT_PEM, - cert: "FakePemCert", - certKey: "FakePemCertKey" - }) - ) as any; + const restSession = new Session({ + hostname: "FakeHostName", + type: AUTH_TYPE_CERT_PEM, + cert: "FakePemCert", + certKey: "FakePemCertKey" + }); + const privateRestClient = new RestClient(restSession) as any; let getSystemProxyUrlSpy: jest.SpyInstance; let getProxyAgentSpy: jest.SpyInstance; let setCertPemAuthSpy: jest.SpyInstance; @@ -1453,6 +1502,22 @@ describe("AbstractRestClient tests", () => { const result = privateRestClient.buildOptions(resource, request, reqHeaders); expect(Object.keys(result)).toContain('agent'); }); + + it('Should use session proxy options over env vars for proxy agent', () => { + restSession.ISession.proxy = { proxy_authorization: 'proxy_auth_string'}; + const resource = '/resource'; + const request = ''; + const reqHeaders: any[] = []; + const url = new URL('https://www.zowe.com'); + const proxyAgent = new HttpsProxyAgent(url, { rejectUnauthorized: true }); + getSystemProxyUrlSpy.mockReturnValue(url); + getProxyAgentSpy.mockReturnValue(proxyAgent); + setCertPemAuthSpy.mockReturnValue(true); + const headerSpy = jest.spyOn(privateRestClient, "appendHeaders"); + const result = privateRestClient.buildOptions(resource, request, reqHeaders); + expect(Object.keys(result)).toContain('agent'); + expect(headerSpy).toHaveBeenCalledWith([{'Proxy-Authorization': restSession.ISession.proxy.proxy_authorization}]); + }) }); }); }); diff --git a/packages/imperative/src/rest/__tests__/client/ProxySettings.unit.test.ts b/packages/imperative/src/rest/__tests__/client/ProxySettings.unit.test.ts index 68d5291c4b..e6b47f4d86 100644 --- a/packages/imperative/src/rest/__tests__/client/ProxySettings.unit.test.ts +++ b/packages/imperative/src/rest/__tests__/client/ProxySettings.unit.test.ts @@ -29,9 +29,50 @@ describe("Proxy tests", () => { const httpUrl = "http://www.zowe.com"; const httpsUrl = "https://www.zowe.com"; const noProxyList = "www.zowe.com, fake.com,ibm.com,broadcom.com "; + const passedUrl = "passedurl.com"; let getProxySettingsSpy: jest.SpyInstance; let checkUrlSpy: jest.SpyInstance; + describe("recognise passed proxy values in session", () => { + const noProxySpy = jest.spyOn(privateProxy, "matchesNoProxySettings"); + const httpEnvVarSpy = jest.spyOn(privateProxy, "getHttpEnvVariables"); + const httpsEnvVarSpy = jest.spyOn(privateProxy, "getHttpsEnvVariables"); + checkUrlSpy = jest.spyOn(privateProxy, "checkUrl"); + const expected = { + proxyUrl: passedUrl, + protocol: HTTPS_PROTOCOL + } + + beforeEach(() => { + jest.clearAllMocks(); + checkUrlSpy.mockClear(); + }); + it("Should use the HTTP proxy agent passed with session", () => { + expected.protocol = HTTP_PROTOCOL; + session.proxy = { http_proxy: passedUrl }; + session.protocol = HTTP_PROTOCOL; + noProxySpy.mockReturnValueOnce(false); + expect(httpEnvVarSpy).not.toHaveBeenCalled(); + expect(httpsEnvVarSpy).not.toHaveBeenCalled(); + checkUrlSpy.mockReturnValueOnce(passedUrl); + expect(JSON.stringify(ProxySettings["getProxySettings"](session))).toEqual(JSON.stringify(expected)); + noProxySpy.mockClear(); + checkUrlSpy.mockClear(); + }); + it("Should use the HTTPS proxy agent passed with session", () => { + expected.protocol = HTTPS_PROTOCOL; + session.proxy = { https_proxy: passedUrl }; + session.protocol = HTTPS_PROTOCOL; + noProxySpy.mockReturnValueOnce(false); + expect(httpEnvVarSpy).not.toHaveBeenCalled(); + expect(httpsEnvVarSpy).not.toHaveBeenCalled(); + checkUrlSpy.mockReturnValueOnce(passedUrl); + expect(JSON.stringify(ProxySettings["getProxySettings"](session))).toEqual(JSON.stringify(expected)); + noProxySpy.mockClear(); + checkUrlSpy.mockClear(); + }); + }); + describe("getProxyAgent", () => { beforeEach(() => { jest.clearAllMocks(); @@ -108,7 +149,11 @@ describe("Proxy tests", () => { expect(ProxySettings["matchesNoProxySettings"](session)).toEqual(expected); process.env["NO_PROXY"] = undefined; }); - + it("Should return true for match with no_proxy passed with session proxy", () => { + session.proxy = { http_proxy: passedUrl, no_proxy: ["fake.com"] }; + session.protocol = HTTP_PROTOCOL; + expect(ProxySettings["matchesNoProxySettings"](session)).toEqual(true); + }); it("Should not match session hostname with no_proxy", () => { const expected = false; process.env["NO_PROXY"] = noProxyList; @@ -116,5 +161,10 @@ describe("Proxy tests", () => { expect(ProxySettings["matchesNoProxySettings"](session)).toEqual(expected); process.env["NO_PROXY"] = undefined; }); + it("Should return false for match with no_proxy passed with session proxy", () => { + session.proxy = { http_proxy: passedUrl, no_proxy: ["false.com", "blah.com"] }; + session.protocol = HTTP_PROTOCOL; + expect(ProxySettings["matchesNoProxySettings"](session)).toEqual(false); + }); }); }); diff --git a/packages/imperative/src/rest/__tests__/client/__model__/MockHttpRequestResponse.ts b/packages/imperative/src/rest/__tests__/client/__model__/MockHttpRequestResponse.ts index 5306899a13..59a92ab446 100644 --- a/packages/imperative/src/rest/__tests__/client/__model__/MockHttpRequestResponse.ts +++ b/packages/imperative/src/rest/__tests__/client/__model__/MockHttpRequestResponse.ts @@ -65,4 +65,10 @@ export class MockHttpRequestResponse extends EventEmitter { * @memberof RequestOrResponse */ public headers: { [key: string]: any }; + + /** + * Simulate a reused socket + * @memberof MockHttpRequestResponse + */ + public reusedSocket: boolean = false; } diff --git a/packages/imperative/src/rest/__tests__/session/Session.unit.test.ts b/packages/imperative/src/rest/__tests__/session/Session.unit.test.ts index 4cec4483a0..cb7d3459de 100644 --- a/packages/imperative/src/rest/__tests__/session/Session.unit.test.ts +++ b/packages/imperative/src/rest/__tests__/session/Session.unit.test.ts @@ -273,6 +273,27 @@ describe("Session tests", () => { expect(session.ISession).toMatchSnapshot(); }); + it("should build a session with passed proxy settings updating http_proxy and session's strictSSL", () => { + let error; + let session; + let mySettings; + try { + mySettings = { + http_proxy: "example.com", + https_proxy: undefined, + no_proxy: undefined, + proxy_authorization: undefined, + proxy_strict_ssl: false + }; + session = new Session({hostname: "localhost", type: "bearer", tokenValue: "blahblahblah", proxy: mySettings}); + } catch (thrownError) { + error = thrownError; + } + expect(error).toBeUndefined(); + expect(session.ISession.proxy).toEqual(mySettings); + expect(session.ISession.strictSSL).toEqual(false); + }); + it("should require proper type", () => { let error; try { diff --git a/packages/imperative/src/rest/index.ts b/packages/imperative/src/rest/index.ts index 72ab85c224..f6eeb8cdcc 100644 --- a/packages/imperative/src/rest/index.ts +++ b/packages/imperative/src/rest/index.ts @@ -30,6 +30,7 @@ export * as SessConstants from "./src/session/SessConstants"; export * from "./src/session/doc/ISession"; export * from "./src/session/doc/IOptionsForAddConnProps"; export * from "./src/session/doc/IOverridePromptConnProps"; +export * from "./src/session/doc/ProxyVariables"; export * from "./src/session/AbstractSession"; export * from "./src/session/ConnectionPropsForSessCfg"; export * from "./src/session/Session"; diff --git a/packages/imperative/src/rest/src/client/AbstractRestClient.ts b/packages/imperative/src/rest/src/client/AbstractRestClient.ts index 46b6eec4b3..3d337bf125 100644 --- a/packages/imperative/src/rest/src/client/AbstractRestClient.ts +++ b/packages/imperative/src/rest/src/client/AbstractRestClient.ts @@ -330,11 +330,20 @@ export abstract class AbstractRestClient { * Invoke any onError method whenever an error occurs on writing */ clientRequest.on("error", (errorResponse: any) => { - reject(this.populateError({ - msg: "Failed to send an HTTP request.", - causeErrors: errorResponse, - source: "client" - })); + // Handle the HTTP 1.1 Keep-Alive race condition + if (errorResponse.code === "ECONNRESET" && clientRequest.reusedSocket) { + this.request(options).then((response: string) => { + resolve(response); + }).catch((err) => { + reject(err); + }); + } else { + reject(this.populateError({ + msg: "Failed to send an HTTP request.", + causeErrors: errorResponse, + source: "client" + })); + } }); if (options.requestStream != null) { @@ -467,6 +476,9 @@ export abstract class AbstractRestClient { this.mLogger.info(`Proxy setting "${proxyUrl.href}" will not be used as hostname was found listed under "no_proxy" setting.`); } else { this.mLogger.info(`Using the following proxy setting for the request: ${proxyUrl.href}`); + if (this.session.ISession?.proxy?.proxy_authorization) { + reqHeaders.push({ 'Proxy-Authorization': this.session.ISession.proxy.proxy_authorization}); + } options.agent = ProxySettings.getProxyAgent(this.session.ISession); } } diff --git a/packages/imperative/src/rest/src/client/ProxySettings.ts b/packages/imperative/src/rest/src/client/ProxySettings.ts index f535bc00d4..c414003a64 100644 --- a/packages/imperative/src/rest/src/client/ProxySettings.ts +++ b/packages/imperative/src/rest/src/client/ProxySettings.ts @@ -78,7 +78,7 @@ export class ProxySettings { * @memberof ProxySettings */ public static matchesNoProxySettings(session: ISession): boolean { - const noProxyValues = this.getNoProxyEnvVariables(); + const noProxyValues = session.proxy?.no_proxy ?? this.getNoProxyEnvVariables(); if (!noProxyValues) { return false; } @@ -103,10 +103,10 @@ export class ProxySettings { const protocol = session.protocol ?? HTTPS_PROTOCOL; let envVariable: string | undefined; if (protocol === HTTP_PROTOCOL) { - envVariable = this.getHttpEnvVariables(); + envVariable = session.proxy?.http_proxy ?? this.getHttpEnvVariables(); } else if (protocol === HTTPS_PROTOCOL) { - envVariable = this.getHttpsEnvVariables(); + envVariable = session.proxy?.https_proxy ?? this.getHttpsEnvVariables(); } const proxyUrl = this.checkUrl(envVariable); if (proxyUrl) { diff --git a/packages/imperative/src/rest/src/session/AbstractSession.ts b/packages/imperative/src/rest/src/session/AbstractSession.ts index eff14b1312..dd3d75a79c 100644 --- a/packages/imperative/src/rest/src/session/AbstractSession.ts +++ b/packages/imperative/src/rest/src/session/AbstractSession.ts @@ -213,8 +213,12 @@ export abstract class AbstractSession { } // set strictSSL - if (populatedSession.strictSSL === undefined || populatedSession.strictSSL === null) { - populatedSession.strictSSL = AbstractSession.DEFAULT_STRICT_SSL; + if (populatedSession.proxy?.proxy_strict_ssl === false) { + populatedSession.strictSSL = false; + } else { + if (populatedSession.proxy?.proxy_strict_ssl || populatedSession.strictSSL === undefined || populatedSession.strictSSL === null) { + populatedSession.strictSSL = AbstractSession.DEFAULT_STRICT_SSL; + } } // set port if not set diff --git a/packages/imperative/src/rest/src/session/doc/ISession.ts b/packages/imperative/src/rest/src/session/doc/ISession.ts index 0204088328..23f33ec14e 100644 --- a/packages/imperative/src/rest/src/session/doc/ISession.ts +++ b/packages/imperative/src/rest/src/session/doc/ISession.ts @@ -10,6 +10,7 @@ */ import * as SessConstants from "../SessConstants"; +import { ProxyVariables } from "./ProxyVariables"; /** * Session interface for maintaining cookie and protocol information @@ -185,4 +186,13 @@ export interface ISession { * @memberof ISession */ authTypeOrder?: string[]; + + /** + * Specifies external proxy settings + * values will override environment variable proxy settings + * + * @type {ProxyVariables[]} + * @memberof ISession + */ + proxy?: ProxyVariables; } diff --git a/packages/imperative/src/rest/src/session/doc/ProxyVariables.ts b/packages/imperative/src/rest/src/session/doc/ProxyVariables.ts new file mode 100644 index 0000000000..9c657e24c6 --- /dev/null +++ b/packages/imperative/src/rest/src/session/doc/ProxyVariables.ts @@ -0,0 +1,35 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +export interface ProxyVariables { + /** + * HTTP_PROXY/http_proxy value + */ + http_proxy?: string, + /** + * HTTPS_PROXY/https_proxy value + */ + https_proxy?: string, + /** + * string[] of NO_PROXY/no_proxy values + */ + no_proxy?: string[], + /** + * The value to send as the Proxy-Authorization + * header for every network request + */ + proxy_authorization?: string, + /** + * boolean value for whether the proxy server certificate + * should be verified against the list of supplied CAs + */ + proxy_strict_ssl?: boolean +} diff --git a/packages/provisioning/package.json b/packages/provisioning/package.json index 472b8771cc..e2e2125a0a 100644 --- a/packages/provisioning/package.json +++ b/packages/provisioning/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/provisioning-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "description": "Zowe SDK to interact with the z/OS provisioning APIs", "author": "Zowe", "license": "EPL-2.0", @@ -49,9 +49,9 @@ }, "devDependencies": { "@types/js-yaml": "^4.0.9", - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "peerDependencies": { "@zowe/core-for-zowe-sdk": "^8.0.0", diff --git a/packages/workflows/package.json b/packages/workflows/package.json index 1860828fee..55acc8c231 100644 --- a/packages/workflows/package.json +++ b/packages/workflows/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/zos-workflows-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "description": "Zowe SDK to interact with the z/OS workflows APIs", "author": "Zowe", "license": "EPL-2.0", @@ -45,12 +45,12 @@ "prepack": "node ../../scripts/prepareLicenses.js" }, "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.2.0" + "@zowe/zos-files-for-zowe-sdk": "8.7.0" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "peerDependencies": { "@zowe/core-for-zowe-sdk": "^8.0.0", diff --git a/packages/zosconsole/package.json b/packages/zosconsole/package.json index 6a0225904b..eed0982d91 100644 --- a/packages/zosconsole/package.json +++ b/packages/zosconsole/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/zos-console-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "description": "Zowe SDK to interact with the z/OS console", "author": "Zowe", "license": "EPL-2.0", @@ -45,9 +45,9 @@ "prepack": "node ../../scripts/prepareLicenses.js" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "peerDependencies": { "@zowe/core-for-zowe-sdk": "^8.0.0", diff --git a/packages/zosfiles/CHANGELOG.md b/packages/zosfiles/CHANGELOG.md index c34ceeb4fe..a7d879d22f 100644 --- a/packages/zosfiles/CHANGELOG.md +++ b/packages/zosfiles/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to the Zowe z/OS files SDK package will be documented in this file. +## `8.6.2` + +- BugFix: Resolved issue where encoding argument was missing from `FileToUss.handler.ts` options object. [#2234](https://github.com/zowe/zowe-cli/pull/2334) +- BugFix: Resolved issue where `FileToUss.handler.ts` options object was not properly passed through subsequent command calls. [#2234](https://github.com/zowe/zowe-cli/pull/2334) + +## `8.4.0` + +- Enhancement: Added optional `--attributes` flag to `zowe zos-files upload file-to-uss` to allow passing a .zosattributes file path for upload encoding format. [#2319] (https://github.com/zowe/zowe-cli/pull/2319) + ## `8.2.0` - Enhancement: Added an optional `continueSearch` function to the `ISearchOptions` interface. After a data set listing is completed, the new function is called with the list of data sets about to be searched. This allows the extender or end users to continue with the search or cancel it. [#2300](https://github.com/zowe/zowe-cli/pull/2300) diff --git a/packages/zosfiles/__tests__/__system__/methods/invoke/Invoke.system.test.ts b/packages/zosfiles/__tests__/__system__/methods/invoke/Invoke.system.test.ts index a1daccc33d..ffe59aa738 100644 --- a/packages/zosfiles/__tests__/__system__/methods/invoke/Invoke.system.test.ts +++ b/packages/zosfiles/__tests__/__system__/methods/invoke/Invoke.system.test.ts @@ -205,7 +205,7 @@ describe("Invoke AMS - encoded", () => { systemProps = testEnvironment.systemTestProperties; REAL_SESSION = TestEnvironment.createZosmfSession(testEnvironment); - dsname = getUniqueDatasetName(`${systemProps.zosmf.user}.ZOSFILE.ENCO#ED.VSAM`); + dsname = getUniqueDatasetName(`${systemProps.zosmf.user}.ZOSFILE.ENCO#ED.VSAM`, false, 1); volume = systemProps.datasets.vol.toUpperCase(); }); diff --git a/packages/zosfiles/__tests__/__system__/methods/list/List.system.test.ts b/packages/zosfiles/__tests__/__system__/methods/list/List.system.test.ts index 4e3981e4ae..3cba83972c 100644 --- a/packages/zosfiles/__tests__/__system__/methods/list/List.system.test.ts +++ b/packages/zosfiles/__tests__/__system__/methods/list/List.system.test.ts @@ -648,7 +648,7 @@ describe("List command group", () => { let caughtError; try { - response = await List.dataSetsMatchingPattern(REAL_SESSION, [dsname + ".INVALID"]); + response = await List.dataSetsMatchingPattern(REAL_SESSION, [dsname + ".INVAL"]); } catch (error) { caughtError = error; } @@ -985,7 +985,7 @@ describe("List command group - encoded", () => { let caughtError; try { - response = await List.dataSetsMatchingPattern(REAL_SESSION, [dsname + ".INVALID"]); + response = await List.dataSetsMatchingPattern(REAL_SESSION, [dsname + ".INVAL"]); } catch (error) { caughtError = error; } diff --git a/packages/zosfiles/__tests__/__system__/methods/search/Search.system.test.ts b/packages/zosfiles/__tests__/__system__/methods/search/Search.system.test.ts index db839abc14..f982c81787 100644 --- a/packages/zosfiles/__tests__/__system__/methods/search/Search.system.test.ts +++ b/packages/zosfiles/__tests__/__system__/methods/search/Search.system.test.ts @@ -59,7 +59,7 @@ describe("Search", () => { let expectedApiResponse: any; beforeAll(async () => { - dsnPrefix = getUniqueDatasetName(`${defaultSystem.zosmf.user}.ZOSFILES.SEARCH`); + dsnPrefix = getUniqueDatasetName(`${defaultSystem.zosmf.user}.ZOSFILES.SEARCH`, false, 1); pattern = dsnPrefix + ".*"; goodDsNames = [`${dsnPrefix}.SEQ1`, `${dsnPrefix}.SEQ4`, `${dsnPrefix}.SEQ5`]; diff --git a/packages/zosfiles/__tests__/__system__/methods/upload/Upload.system.test.ts b/packages/zosfiles/__tests__/__system__/methods/upload/Upload.system.test.ts index e0797df365..d9a1e0597e 100644 --- a/packages/zosfiles/__tests__/__system__/methods/upload/Upload.system.test.ts +++ b/packages/zosfiles/__tests__/__system__/methods/upload/Upload.system.test.ts @@ -18,6 +18,7 @@ import { ITestPropertiesSchema } from "../../../../../../__tests__/__src__/prope import { deleteFiles, getUniqueDatasetName, stripNewLines, wait, waitTime } from "../../../../../../__tests__/__src__/TestUtils"; import * as fs from "fs"; import { ITestEnvironment } from "../../../../../../__tests__/__src__/environment/ITestEnvironment"; +import { runCliScript } from "../../../../../../__tests__/__packages__/cli-test-utils/src"; let REAL_SESSION: Session; let testEnvironment: ITestEnvironment; @@ -769,12 +770,11 @@ describe("Upload USS file", () => { }); it("should upload a USS file from local file", async () => { let error; - let uploadResponse; let getResponse; let tagResponse; try { - uploadResponse = await Upload.fileToUssFile(REAL_SESSION, inputfile, ussname); + await Upload.fileToUssFile(REAL_SESSION, inputfile, ussname); getResponse = await Get.USSFile(REAL_SESSION, ussname); tagResponse = await Utilities.isFileTagBinOrAscii(REAL_SESSION, ussname); } catch (err) { @@ -785,16 +785,14 @@ describe("Upload USS file", () => { expect(error).toBeFalsy(); expect(getResponse).toEqual(Buffer.from(testdata)); expect(tagResponse).toBe(false); - }); it("should upload a USS file from local file in binary mode", async () => { let error; - let uploadResponse; let getResponse; let tagResponse; try { - uploadResponse = await Upload.fileToUssFile(REAL_SESSION, inputfile, ussname, { binary: true }); + await Upload.fileToUssFile(REAL_SESSION, inputfile, ussname, { binary: true }); getResponse = await Get.USSFile(REAL_SESSION, ussname, {binary: true}); tagResponse = await Utilities.isFileTagBinOrAscii(REAL_SESSION, ussname); } catch (err) { @@ -833,11 +831,10 @@ describe("Upload USS file", () => { it("should upload a USS file and return Etag", async () => { let error; let uploadResponse; - let getResponse; try { uploadResponse = await Upload.fileToUssFile(REAL_SESSION, inputfile, ussname, {returnEtag: true}); - getResponse = await Get.USSFile(REAL_SESSION, ussname); + await Get.USSFile(REAL_SESSION, ussname); } catch (err) { error = err; Imperative.console.info("Error: " + inspect(error)); @@ -848,6 +845,121 @@ describe("Upload USS file", () => { expect(uploadResponse.success).toBeTruthy(); expect(uploadResponse.apiResponse.etag).toBeDefined(); }); + it("should upload local file to USS using .zosattributes file", async () => { + let response: any; + let error; + let readResponseGood: any; + let readResponseBad: any; + try { + // Upload file + response = runCliScript(__dirname + "/__resources__/upload_file_to_uss.sh", testEnvironment,[ + defaultSystem.tso.account, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + __dirname + "/testfiles/encodingCheck.txt", + ussname, + __dirname + "/__resources__/.zosattributes", + ]); + // View file with matching encoding + readResponseGood = runCliScript(__dirname + "/__resources__/view_file_uss.sh", testEnvironment,[ + defaultSystem.tso.account, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + ussname, + 1047 + ]); + // View file with not matching encoding + readResponseBad = runCliScript(__dirname + "/__resources__/view_file_uss.sh", testEnvironment,[ + defaultSystem.tso.account, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + ussname, + 1147 + ]); + } + catch (err) { + error = err; + Imperative.console.info("Error: " + inspect(error)); + } + // Get contents of file that was uploaded + const fileContents = fs.readFileSync(__dirname + "/testfiles/encodingCheck.txt").toString(); + + // Ensure upload was successful + expect(response.stderr.toString()).toBe(""); + expect(response.stdout.toString()).toBeDefined(); + expect(response.stdout.toString()).toContain("USS file uploaded successfully."); + + // Compare file view with not matching upload and view encoding (1047 vs 1147). + expect(readResponseBad.stdout.toString()).not.toContain(fileContents); + + // Compare file view with matching upload and view encoding (1047). + expect(readResponseGood.stdout.toString()).toContain(fileContents); + }); + it("should upload local file to USS using .zosattributes file - binary", async () => { + let response: any; + let error; + let readResponseGood: any; + let readResponseBad: any; + try { + // Upload file + response = runCliScript(__dirname + "/__resources__/upload_file_to_uss.sh", testEnvironment,[ + defaultSystem.tso.account, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + __dirname + "/testfiles/encodingCheckBinary.txt", + ussname, + __dirname + "/__resources__/.zosattributes-binary", + ]); + // View file with .txt binary encoding in .zosattributes + readResponseGood = runCliScript(__dirname + "/__resources__/view_file_uss_binary.sh", testEnvironment,[ + defaultSystem.tso.account, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + ussname, + true + ]); + readResponseBad = runCliScript(__dirname + "/__resources__/view_file_uss.sh", testEnvironment,[ + defaultSystem.tso.account, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + ussname, + "1047" + ]); + } + catch (err) { + error = err; + Imperative.console.info("Error: " + inspect(error)); + } + const fileContents = fs.readFileSync(__dirname + "/testfiles/encodingCheckBinary.txt").toString(); + + expect(response.stderr.toString()).toBe(""); + expect(response.stdout.toString()).toBeDefined(); + expect(response.stdout.toString()).toContain("USS file uploaded successfully."); + + // Compare file view with not matching upload and view encoding (1047 vs 1147). + expect(readResponseBad.stdout.toString()).not.toContain(fileContents); + + // Compare file view with matching upload and view encoding (1047). + expect(readResponseGood.stdout.toString()).toContain(fileContents); + }); }); }); diff --git a/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/.zosattributes b/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/.zosattributes new file mode 100644 index 0000000000..88a1b0f51f --- /dev/null +++ b/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/.zosattributes @@ -0,0 +1,5 @@ +*.json - +*.bin binary binary +*.jcl IBM-1047 IBM-1047 +*.md UTF-8 UTF-8 +*.txt UTF-8 IBM-1047 \ No newline at end of file diff --git a/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/.zosattributes-binary b/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/.zosattributes-binary new file mode 100644 index 0000000000..0f79a06bc2 --- /dev/null +++ b/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/.zosattributes-binary @@ -0,0 +1,5 @@ +*.json - +*.bin binary binary +*.jcl IBM-1047 IBM-1047 +*.md UTF-8 UTF-8 +*.txt binary binary \ No newline at end of file diff --git a/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/upload_file_to_uss.sh b/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/upload_file_to_uss.sh new file mode 100755 index 0000000000..c09803cce6 --- /dev/null +++ b/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/upload_file_to_uss.sh @@ -0,0 +1,13 @@ +#!/bin/bash +account=$1 +host=$2 +port=$3 +user=$4 +password=$5 +ru=$6 +inputFile=$7 +ussName=$8 +attributes=$9 + +zowe zos-files upload file-to-uss "$inputFile" "$ussName" --attributes "$attributes" --host $host --port $port --user $user --password $password --ru $ru +exit $? \ No newline at end of file diff --git a/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/view_file_uss.sh b/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/view_file_uss.sh new file mode 100755 index 0000000000..bfa85f180b --- /dev/null +++ b/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/view_file_uss.sh @@ -0,0 +1,12 @@ +#!/bin/bash +account=$1 +host=$2 +port=$3 +user=$4 +password=$5 +ru=$6 +ussName=$7 +encoding=$8 + +zowe zos-files view uf "$ussName" --encoding $encoding --host $host --port $port --user $user --password $password --ru $ru +exit $? \ No newline at end of file diff --git a/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/view_file_uss_binary.sh b/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/view_file_uss_binary.sh new file mode 100755 index 0000000000..3eabe90a6f --- /dev/null +++ b/packages/zosfiles/__tests__/__system__/methods/upload/__resources__/view_file_uss_binary.sh @@ -0,0 +1,12 @@ +#!/bin/bash +account=$1 +host=$2 +port=$3 +user=$4 +password=$5 +ru=$6 +ussName=$7 +encoding=$8 + +zowe zos-files view uf $ussName --binary $binary --host $host --port $port --user $user --password $password --ru $ru +exit $? \ No newline at end of file diff --git a/packages/zosfiles/__tests__/__system__/methods/upload/testfiles/encodingCheck.txt b/packages/zosfiles/__tests__/__system__/methods/upload/testfiles/encodingCheck.txt new file mode 100644 index 0000000000..7038873a47 --- /dev/null +++ b/packages/zosfiles/__tests__/__system__/methods/upload/testfiles/encodingCheck.txt @@ -0,0 +1 @@ +á é í ó ú ñ Ç ß 12345 !@#$% ^ [ ] $ £ \ No newline at end of file diff --git a/packages/zosfiles/__tests__/__system__/methods/upload/testfiles/encodingCheckBinary.txt b/packages/zosfiles/__tests__/__system__/methods/upload/testfiles/encodingCheckBinary.txt new file mode 100644 index 0000000000..385781092d --- /dev/null +++ b/packages/zosfiles/__tests__/__system__/methods/upload/testfiles/encodingCheckBinary.txt @@ -0,0 +1 @@ +[][][] \ No newline at end of file diff --git a/packages/zosfiles/__tests__/__unit__/methods/upload/Upload.unit.test.ts b/packages/zosfiles/__tests__/__unit__/methods/upload/Upload.unit.test.ts index 9f232a790e..7e7e8bf363 100644 --- a/packages/zosfiles/__tests__/__unit__/methods/upload/Upload.unit.test.ts +++ b/packages/zosfiles/__tests__/__unit__/methods/upload/Upload.unit.test.ts @@ -199,7 +199,6 @@ describe("z/OS Files - Upload", () => { expect(error).toBeDefined(); expect(error.message).toContain(ZosFilesMessages.missingDatasetName.message); }); - it("should throw underlying fs error", async () => { const rootError = { code: "test", @@ -226,6 +225,22 @@ describe("z/OS Files - Upload", () => { expect(error.additionalDetails).toEqual(rootError.toString()); expect(error.causeErrors).toBe(rootError); }); + it("should throw error if error is null and stats.isFile() is true", async () => { + const testPath = "test/path"; + lstatSpy.mockImplementationOnce((somePath, callback: any) => { + callback(null, {isFile: () => true}); + }); + + try { + response = await Upload.dirToPds(dummySession, testPath, dsName); + } catch (err) { + error = err; + } + + expect(response).toBeUndefined(); + expect(error).toBeDefined(); + expect(error.message).toContain(ZosFilesMessages.pathIsNotDirectory.message); + }); it("should return with proper message when path is pointing to a file", async () => { lstatSpy.mockImplementationOnce((somePath, callback: any) => { @@ -2475,7 +2490,7 @@ describe("z/OS Files - Upload", () => { expect(fileToUssFileSpy).toHaveBeenCalledTimes(1); expect(fileToUssFileSpy).toHaveBeenCalledWith(dummySession, path.normalize(path.join(testPath, "uploadme")), - `${dsName}/uploadme`, { binary: true }); + `${dsName}/uploadme`, { binary: true, attributes: attributesMock, recursive: false }); }); it("should not upload ignored directories", async () => { @@ -2518,7 +2533,7 @@ describe("z/OS Files - Upload", () => { expect(fileToUssFileSpy).toHaveBeenCalledTimes(1); expect(fileToUssFileSpy).toHaveBeenCalledWith(dummySession, path.normalize(path.join(testPath, "uploaddir", "uploadedfile")), - `${dsName}/uploaddir/uploadedfile`, { binary: true }); + `${dsName}/uploaddir/uploadedfile`, { binary: true, attributes: attributesMock }); }); it("should upload files in text or binary according to attributes", async () => { getFileListFromPathSpy.mockReturnValue(["textfile", "binaryfile"]); @@ -2532,10 +2547,10 @@ describe("z/OS Files - Upload", () => { expect(fileToUssFileSpy).toHaveBeenCalledWith(dummySession, path.normalize(path.join(testPath, "textfile")), `${dsName}/textfile`, - { binary: false, encoding: "ISO8859-1", localEncoding: "ISO8859-1" }); + { binary: false, encoding: "ISO8859-1", localEncoding: "ISO8859-1", attributes: attributesMock, recursive: false }); expect(fileToUssFileSpy).toHaveBeenCalledWith(dummySession, path.normalize(path.join(testPath, "binaryfile")), - `${dsName}/binaryfile`, { binary: true }); + `${dsName}/binaryfile`, { binary: true, recursive: false, attributes: attributesMock }); }); it("should call API to tag files according to remote encoding", async () => { diff --git a/packages/zosfiles/package.json b/packages/zosfiles/package.json index 41459f5995..c11755fa64 100644 --- a/packages/zosfiles/package.json +++ b/packages/zosfiles/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/zos-files-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "description": "Zowe SDK to interact with files and data sets on z/OS", "author": "Zowe", "license": "EPL-2.0", @@ -50,10 +50,10 @@ "minimatch": "^9.0.5" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0", - "@zowe/zos-uss-for-zowe-sdk": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0", + "@zowe/zos-uss-for-zowe-sdk": "8.7.0" }, "peerDependencies": { "@zowe/core-for-zowe-sdk": "^8.0.0", diff --git a/packages/zosfiles/src/methods/upload/Upload.ts b/packages/zosfiles/src/methods/upload/Upload.ts index 985cf0455c..13a53abef4 100644 --- a/packages/zosfiles/src/methods/upload/Upload.ts +++ b/packages/zosfiles/src/methods/upload/Upload.ts @@ -678,7 +678,7 @@ export class Upload { } const fileName = path.normalize(path.join(inputDirectory, file.fileName)); const ussFilePath = path.posix.join(ussname, file.fileName); - return this.uploadFile(fileName, ussFilePath, session, + return this.uploadFile(session,fileName, ussFilePath, { ...options, binary: file.binary }); }; @@ -831,7 +831,7 @@ export class Upload { } const filePath = path.normalize(path.join(inputDirectory, file.fileName)); const ussFilePath = path.posix.join(ussname, file.fileName); - return this.uploadFile(filePath, ussFilePath, session, + return this.uploadFile(session, filePath, ussFilePath, { ...options, binary: file.binary }); }; if (maxConcurrentRequests === 0) { @@ -854,35 +854,34 @@ export class Upload { }; } - private static async uploadFile(localPath: string, ussPath: string, - session: AbstractSession, options: IUploadOptions) { - const tempOptions: Partial = {}; - + public static async uploadFile( + session: AbstractSession, + localPath: string, + ussPath: string, + options: IUploadOptions + ): Promise { + const tempOptions: IUploadOptions = { ...options }; if (options.attributes) { if (!options.attributes.fileShouldBeUploaded(localPath)) { return; } tempOptions.binary = options.attributes.getFileTransferMode(localPath, options.binary) === TransferMode.BINARY; const remoteEncoding = options.attributes.getRemoteEncoding(localPath); - if (remoteEncoding != null && remoteEncoding !== Tag.BINARY) { - tempOptions.encoding = remoteEncoding; - } + + if(remoteEncoding === Tag.BINARY) tempOptions.encoding = undefined; + else if(remoteEncoding !== null) tempOptions.encoding = remoteEncoding; + if (!tempOptions.binary) { tempOptions.localEncoding = options.attributes.getLocalEncoding(localPath); } - } else { - if (options.filesMap) { - if (options.filesMap.fileNames.indexOf(path.basename(localPath)) > -1) { - tempOptions.binary = options.filesMap.binary; - } else { - tempOptions.binary = options.binary; - } - } else { - tempOptions.binary = options.binary; - } + } else if(options.filesMap?.fileNames.indexOf(path.basename(localPath)) > -1) { + tempOptions.binary = options.filesMap.binary; + + // Reset encoding to undefined if binary is true to avoid file tagging issues + if(tempOptions.binary) tempOptions.encoding = undefined; } - await this.fileToUssFile(session, localPath, ussPath, tempOptions); + return await this.fileToUssFile(session, localPath, ussPath, tempOptions); } /** diff --git a/packages/zosjobs/CHANGELOG.md b/packages/zosjobs/CHANGELOG.md index 899dbad608..fc986d2800 100644 --- a/packages/zosjobs/CHANGELOG.md +++ b/packages/zosjobs/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the Zowe z/OS jobs SDK package will be documented in this file. +## `8.7.0` +- Enhancement: Added waitForOutput & waitForActive as optional parameters to download on zosjobs. [#2326] (https://github.com/zowe/zowe-cli/pull/2326). + +## `8.5.0` +- Enhancement: Added execData to IJob return data from GetJobs.getJob [#2320](https://github.com/zowe/zowe-cli/pull/2320) + ## `8.1.1` - BugFix: Updated peer dependencies to `^8.0.0`, dropping support for versions tagged `next`. [#2287](https://github.com/zowe/zowe-cli/pull/2287) diff --git a/packages/zosjobs/__tests__/__resources__/api/GetJobsData.ts b/packages/zosjobs/__tests__/__resources__/api/GetJobsData.ts index 645c347331..144f9b8d85 100644 --- a/packages/zosjobs/__tests__/__resources__/api/GetJobsData.ts +++ b/packages/zosjobs/__tests__/__resources__/api/GetJobsData.ts @@ -29,6 +29,11 @@ export class GetJobsData { public static readonly SAMPLE_COMPLETE_JOB: IJob = { "jobid": "TSUxxx", "jobname": "IBMUSER$", + "exec-started": '2024-01-02T15:57:58.350Z', + "exec-ended": '2024-01-02T15:58:00.600Z', + "exec-submitted": '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', "subsystem": "JES2", "owner": "IBMUSER", "status": "OUTPUT", @@ -51,6 +56,11 @@ export class GetJobsData { public static readonly SAMPLE_COMPLETE_JOB_AGAIN: IJob = { "jobid": "JOBxxx", "jobname": "CAUSER$", + "exec-started": '2024-01-02T15:57:58.350Z', + "exec-ended": '2024-01-02T15:58:00.600Z', + "exec-submitted": '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', "subsystem": "JES2", "owner": "CAUSER", "status": "OUTPUT", @@ -81,6 +91,11 @@ export class GetJobsData { public static readonly SAMPLE_ACTIVE_JOB: IJob = { "retcode": null, "jobname": "IBMUSER$", + "exec-started": '2024-01-02T15:57:58.350Z', + "exec-ended": '2024-01-02T15:58:00.600Z', + "exec-submitted": '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', "status": "INPUT", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "class": "A", diff --git a/packages/zosjobs/__tests__/__system__/CancelJobs.system.test.ts b/packages/zosjobs/__tests__/__system__/CancelJobs.system.test.ts index 5ce5b20883..ecd4bb3a20 100644 --- a/packages/zosjobs/__tests__/__system__/CancelJobs.system.test.ts +++ b/packages/zosjobs/__tests__/__system__/CancelJobs.system.test.ts @@ -22,6 +22,7 @@ let sleepJCL: string; let systemProps: ITestPropertiesSchema; let testEnvironment: ITestEnvironment; const LONG_TIMEOUT = 100000; // 100 second timeout - jobs could take a while to complete due to system load +const modifyVersionDefaultUsesCIM = false; describe("CancelJobs System tests", () => { @@ -44,11 +45,15 @@ describe("CancelJobs System tests", () => { describe("Positive tests", () => { it("should be able to cancel a job using cancelJob (modify version 1)", async () => { - const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); - expect(job.retcode).toBeNull(); // job is not complete, no CC - const response = await CancelJobs.cancelJob(REAL_SESSION, job.jobname, job.jobid, "1.0"); - expect(response).toBeUndefined(); - testEnvironment.resources.jobs.push(job); + if (testEnvironment.systemTestProperties.zosjobs.skipCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); + expect(job.retcode).toBeNull(); // job is not complete, no CC + const response = await CancelJobs.cancelJob(REAL_SESSION, job.jobname, job.jobid, "1.0"); + expect(response).toBeUndefined(); + testEnvironment.resources.jobs.push(job); + } }, LONG_TIMEOUT); it("should be able to cancel a job using cancelJob (modify version 2)", async () => { @@ -61,20 +66,28 @@ describe("CancelJobs System tests", () => { }, LONG_TIMEOUT); it("should be able to cancel a job using cancelJob (modify version default)", async () => { - const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); - expect(job.retcode).toBeNull(); // job is not complete, no CC - const response = await CancelJobs.cancelJob(REAL_SESSION, job.jobname, job.jobid); - expect(response).not.toBeUndefined(); - expect(response?.status).toEqual("0"); // intermittent failure - testEnvironment.resources.jobs.push(job); + if (testEnvironment.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); + expect(job.retcode).toBeNull(); // job is not complete, no CC + const response = await CancelJobs.cancelJob(REAL_SESSION, job.jobname, job.jobid); + expect(response).not.toBeUndefined(); + expect(response?.status).toEqual("0"); // intermittent failure + testEnvironment.resources.jobs.push(job); + } }, LONG_TIMEOUT); it("should be able to cancel a job using cancelJobForJob (modify version 1)", async () => { - const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); - expect(job.retcode).toBeNull(); // job is not complete, no CC - const response = await CancelJobs.cancelJobForJob(REAL_SESSION, job, "1.0"); - expect(response).toBeUndefined(); - testEnvironment.resources.jobs.push(job); + if (testEnvironment.systemTestProperties.zosjobs.skipCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); + expect(job.retcode).toBeNull(); // job is not complete, no CC + const response = await CancelJobs.cancelJobForJob(REAL_SESSION, job, "1.0"); + expect(response).toBeUndefined(); + testEnvironment.resources.jobs.push(job); + } }, LONG_TIMEOUT); it("should be able to cancel a job using cancelJobForJob (modify version 2)", async () => { @@ -87,20 +100,28 @@ describe("CancelJobs System tests", () => { }, LONG_TIMEOUT); it("should be able to cancel a job using cancelJobForJob (modify version default)", async () => { - const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); - expect(job.retcode).toBeNull(); // job is not complete, no CC - const response = await CancelJobs.cancelJobForJob(REAL_SESSION, job); - expect(response).not.toBeUndefined(); - expect(response?.status).toEqual("0"); // intermittent failure - testEnvironment.resources.jobs.push(job); + if (testEnvironment.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); + expect(job.retcode).toBeNull(); // job is not complete, no CC + const response = await CancelJobs.cancelJobForJob(REAL_SESSION, job); + expect(response).not.toBeUndefined(); + expect(response?.status).toEqual("0"); // intermittent failure + testEnvironment.resources.jobs.push(job); + } }, LONG_TIMEOUT); it("should be able to cancel a job using cancelJobCommon (job version 1)", async () => { - const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); - expect(job.retcode).toBeNull(); // job is not complete, no CC - const response = await CancelJobs.cancelJobCommon(REAL_SESSION, {jobname: job.jobname, jobid: job.jobid, version: "1.0"}); - expect(response).toBeUndefined(); - testEnvironment.resources.jobs.push(job); + if (testEnvironment.systemTestProperties.zosjobs.skipCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); + expect(job.retcode).toBeNull(); // job is not complete, no CC + const response = await CancelJobs.cancelJobCommon(REAL_SESSION, {jobname: job.jobname, jobid: job.jobid, version: "1.0"}); + expect(response).toBeUndefined(); + testEnvironment.resources.jobs.push(job); + } }, LONG_TIMEOUT); it("should be able to cancel a job using cancelJobCommon (job version 2.0 - synchronous)", async () => { @@ -113,11 +134,15 @@ describe("CancelJobs System tests", () => { }, LONG_TIMEOUT); it("should be able to cancel a job using cancelJobCommon (job version default)", async () => { - const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); - expect(job.retcode).toBeNull(); // job is not complete, no CC - const response = await CancelJobs.cancelJobCommon(REAL_SESSION, {jobname: job.jobname, jobid: job.jobid}); - expect(response?.status).toEqual("0"); // intermittent failure - testEnvironment.resources.jobs.push(job); + if (testEnvironment.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); + expect(job.retcode).toBeNull(); // job is not complete, no CC + const response = await CancelJobs.cancelJobCommon(REAL_SESSION, {jobname: job.jobname, jobid: job.jobid}); + expect(response?.status).toEqual("0"); // intermittent failure + testEnvironment.resources.jobs.push(job); + } }, LONG_TIMEOUT); it("should be able to cancel a job using cancelJobCommon (job version 2.0 - synchronous) and return an error feedback object", async () => { @@ -206,29 +231,41 @@ describe("CancelJobs System tests - encoded", () => { describe("Positive tests", () => { it("should be able to cancel a job using cancelJob (modify version default)", async () => { - const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); - expect(job.retcode).toBeNull(); // job is not complete, no CC - const response = await CancelJobs.cancelJob(REAL_SESSION, job.jobname, job.jobid); - expect(response).not.toBeUndefined(); - expect(response?.status).toEqual("0"); // intermittent failure - testEnvironment.resources.jobs.push(job); + if (testEnvironment.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); + expect(job.retcode).toBeNull(); // job is not complete, no CC + const response = await CancelJobs.cancelJob(REAL_SESSION, job.jobname, job.jobid); + expect(response).not.toBeUndefined(); + expect(response?.status).toEqual("0"); // intermittent failure + testEnvironment.resources.jobs.push(job); + } }, LONG_TIMEOUT); it("should be able to cancel a job using cancelJobForJob (modify version default)", async () => { - const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); - expect(job.retcode).toBeNull(); // job is not complete, no CC - const response = await CancelJobs.cancelJobForJob(REAL_SESSION, job); - expect(response).not.toBeUndefined(); - expect(response?.status).toEqual("0"); // intermittent failure - testEnvironment.resources.jobs.push(job); + if (testEnvironment.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); + expect(job.retcode).toBeNull(); // job is not complete, no CC + const response = await CancelJobs.cancelJobForJob(REAL_SESSION, job); + expect(response).not.toBeUndefined(); + expect(response?.status).toEqual("0"); // intermittent failure + testEnvironment.resources.jobs.push(job); + } }, LONG_TIMEOUT); it("should be able to cancel a job using cancelJobCommon (job version default)", async () => { - const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); - expect(job.retcode).toBeNull(); // job is not complete, no CC - const response = await CancelJobs.cancelJobCommon(REAL_SESSION, {jobname: job.jobname, jobid: job.jobid}); - expect(response?.status).toEqual("0"); // intermittent failure - testEnvironment.resources.jobs.push(job); + if (testEnvironment.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: sleepJCL, status: "INPUT"}); + expect(job.retcode).toBeNull(); // job is not complete, no CC + const response = await CancelJobs.cancelJobCommon(REAL_SESSION, {jobname: job.jobname, jobid: job.jobid}); + expect(response?.status).toEqual("0"); // intermittent failure + testEnvironment.resources.jobs.push(job); + } }, LONG_TIMEOUT); it("should be able to cancel a job using cancelJobCommon (job version 2.0 - synchronous) and return an error feedback object", async () => { diff --git a/packages/zosjobs/__tests__/__system__/DeleteJobs.system.test.ts b/packages/zosjobs/__tests__/__system__/DeleteJobs.system.test.ts index d0f96bf6bf..8dcb1c8b58 100644 --- a/packages/zosjobs/__tests__/__system__/DeleteJobs.system.test.ts +++ b/packages/zosjobs/__tests__/__system__/DeleteJobs.system.test.ts @@ -24,6 +24,7 @@ let iefbr14JCL: string; let defaultSystem: ITestPropertiesSchema; let testEnvironment: ITestEnvironment; const LONG_TIMEOUT = 100000; // 100 second timeout - jobs could take a while to complete due to system load +const modifyVersionDefaultUsesCIM = false; describe("DeleteJobs System tests", () => { @@ -44,24 +45,36 @@ describe("DeleteJobs System tests", () => { describe("Positive tests", () => { it("should be able to delete a job using deleteJob", async () => { - const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: iefbr14JCL}); - expect(job.retcode).toEqual("CC 0000"); - const response = await DeleteJobs.deleteJob(REAL_SESSION, job.jobname, job.jobid); - expect(response).toBeUndefined(); + if (testEnvironment.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: iefbr14JCL}); + expect(job.retcode).toEqual("CC 0000"); + const response = await DeleteJobs.deleteJob(REAL_SESSION, job.jobname, job.jobid); + expect(response).toBeUndefined(); + } }, LONG_TIMEOUT); it("should be able to delete a job using deleteJobForJob", async () => { - const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: iefbr14JCL}); - expect(job.retcode).toEqual("CC 0000"); - const response = await DeleteJobs.deleteJobForJob(REAL_SESSION, job); - expect(response).toBeUndefined(); + if (testEnvironment.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: iefbr14JCL}); + expect(job.retcode).toEqual("CC 0000"); + const response = await DeleteJobs.deleteJobForJob(REAL_SESSION, job); + expect(response).toBeUndefined(); + } }, LONG_TIMEOUT); it("should be able to delete a job using deleteJobCommon", async () => { - const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: iefbr14JCL}); - expect(job.retcode).toEqual("CC 0000"); - const response = await DeleteJobs.deleteJobCommon(REAL_SESSION, {jobname: job.jobname, jobid: job.jobid}); - expect(response).toBeUndefined(); + if (testEnvironment.systemTestProperties.zosjobs.skipCIM && modifyVersionDefaultUsesCIM) { + process.stdout.write("Skipping test because skipCIM is set."); + } else { + const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, {jcl: iefbr14JCL}); + expect(job.retcode).toEqual("CC 0000"); + const response = await DeleteJobs.deleteJobCommon(REAL_SESSION, {jobname: job.jobname, jobid: job.jobid}); + expect(response).toBeUndefined(); + } }, LONG_TIMEOUT); it("should be able to delete a job using deleteJobCommon (job modify version 2.0 - synchronous)", async () => { diff --git a/packages/zosjobs/__tests__/__system__/GetJobs.system.test.ts b/packages/zosjobs/__tests__/__system__/GetJobs.system.test.ts index 5ca70448c9..3e966c7f3e 100644 --- a/packages/zosjobs/__tests__/__system__/GetJobs.system.test.ts +++ b/packages/zosjobs/__tests__/__system__/GetJobs.system.test.ts @@ -719,8 +719,7 @@ describe("Get Jobs - System Tests", () => { it("should be able to get a job that was submitted and get proper error when the job is deleted", async () => { const job = await SubmitJobs.submitJcl(REAL_SESSION, JCL); - const jobStatus = await GetJobs.getStatusForJob(REAL_SESSION, job); - + await wait(3000); await DeleteJobs.deleteJobForJob(REAL_SESSION, job); await wait(3000); // make sure jobs is deleted let error; @@ -978,6 +977,7 @@ describe("Get Jobs - System Tests", () => { describe("invalid request error handling", () => { it("should detect and surface an error for getting JCL that doesnt exist", async () => { const job = await SubmitJobs.submitJcl(REAL_SESSION, JCL); + await wait(3000); await DeleteJobs.deleteJobForJob(REAL_SESSION, job); await wait(3000); let error; diff --git a/packages/zosjobs/__tests__/__system__/ModifyJobs.system.test.ts b/packages/zosjobs/__tests__/__system__/ModifyJobs.system.test.ts index 2ccc7fff18..407b9aefc4 100644 --- a/packages/zosjobs/__tests__/__system__/ModifyJobs.system.test.ts +++ b/packages/zosjobs/__tests__/__system__/ModifyJobs.system.test.ts @@ -15,6 +15,7 @@ import { ITestEnvironment } from "../../../../__tests__/__src__/environment/ITes import { TestEnvironment } from "../../../../__tests__/__src__/environment/TestEnvironment"; import { ITestPropertiesSchema } from "../../../../__tests__/__src__/properties/ITestPropertiesSchema"; import { JobTestsUtils } from "./JobTestsUtils"; +import { wait } from "../../../../__tests__/__src__/TestUtils"; let testEnvironment: ITestEnvironment; let systemProps: ITestPropertiesSchema; @@ -47,6 +48,10 @@ describe("Modify Jobs - System Tests", () => { await TestEnvironment.cleanUp(testEnvironment); }); + beforeEach(async () => { + await wait(1000); + }); + describe("Positive tests", () => { it("should return a success message once jobclass has been modified", async () => { const job: any = await ModifyJobs.modifyJobCommon( @@ -120,6 +125,10 @@ describe("Modify Jobs - System Tests - Encoded", () => { await TestEnvironment.cleanUp(testEnvironment); }); + beforeEach(async () => { + await wait(1000); + }); + describe("Positive tests", () => { it("should return a success message once jobclass has been modified", async () => { const job: any = await ModifyJobs.modifyJobCommon( diff --git a/packages/zosjobs/__tests__/__system__/MonitorJobs.system.test.ts b/packages/zosjobs/__tests__/__system__/MonitorJobs.system.test.ts index 5ba150128f..ad44714e69 100644 --- a/packages/zosjobs/__tests__/__system__/MonitorJobs.system.test.ts +++ b/packages/zosjobs/__tests__/__system__/MonitorJobs.system.test.ts @@ -18,6 +18,7 @@ import { ZosmfRestClient } from "@zowe/core-for-zowe-sdk"; import { TestEnvironment } from "../../../../__tests__/__src__/environment/TestEnvironment"; import { ITestPropertiesSchema } from "../../../../__tests__/__src__/properties/ITestPropertiesSchema"; import { ITestEnvironment } from "../../../../__tests__/__src__/environment/ITestEnvironment"; +import { wait } from "../../../../__tests__/__src__/TestUtils"; // long running test timeout const LONG_TIMEOUT = 100000; @@ -45,6 +46,7 @@ let MONITOR_JOB_NAME: string; // Utility function to cleanup async function cleanTestJobs() { // The tests may submit jobs - we will clean every job that may have been left by failures, etc. + await wait(1000); // Wait for jobs to register in z/OSMF const jobs: IJob[] = await GetJobs.getJobsCommon(REAL_SESSION, {owner: REAL_SESSION.ISession.user, prefix: MONITOR_JOB_NAME}); if (jobs.length > 0) { for (const job of jobs) { @@ -89,12 +91,8 @@ describe.each([false, true])("System Tests - Monitor Jobs - Encoded: %s", (encod SYSAFF = testEnvironment.systemTestProperties.zosjobs.sysaff; }); - // Cleanup before & after each test - this will ensure that hopefully no jobs are left outstanding (or are currently + // Cleanup after each test - this will ensure that hopefully no jobs are left outstanding (or are currently // outstanding) when the tests run - beforeEach(async () => { - GetJobs.getStatusCommon = ORIG_JOBS_STATUS; - await cleanTestJobs(); - }); afterEach(async () => { GetJobs.getStatusCommon = ORIG_JOBS_STATUS; await cleanTestJobs(); diff --git a/packages/zosjobs/__tests__/__unit__/CancelJobs.unit.test.ts b/packages/zosjobs/__tests__/__unit__/CancelJobs.unit.test.ts index 8d19923dc9..655555e852 100644 --- a/packages/zosjobs/__tests__/__unit__/CancelJobs.unit.test.ts +++ b/packages/zosjobs/__tests__/__unit__/CancelJobs.unit.test.ts @@ -40,7 +40,12 @@ describe("Cancel Jobs unit tests", () => { "files-url": "myfakeurl.com/files/records", "phase": 2, "phase-name": "OUTPUT", - "job-correlator": "mycorrelator" + "job-correlator": "mycorrelator", + "exec-started": '2024-01-02T15:57:58.350Z', + "exec-ended": '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', + "exec-submitted": '2024-01-02T15:58:00.600Z' }; describe("Positive tests", () => { it("should allow users to call cancelJob with correct parameters", async () => { diff --git a/packages/zosjobs/__tests__/__unit__/DeleteJobs.unit.test.ts b/packages/zosjobs/__tests__/__unit__/DeleteJobs.unit.test.ts index c28a5c0015..d6bf063145 100644 --- a/packages/zosjobs/__tests__/__unit__/DeleteJobs.unit.test.ts +++ b/packages/zosjobs/__tests__/__unit__/DeleteJobs.unit.test.ts @@ -31,6 +31,8 @@ describe("Delete Jobs unit tests", () => { const fakeJob: IJob = { "jobid": "JOB00001", "jobname": "MYJOB1", + "exec-started": '2024-01-02T15:57:58.350Z', + "exec-ended": '2024-01-02T15:58:00.600Z', "retcode": "CC 0000", "owner": "dummy", "subsystem": "JES2", @@ -41,7 +43,10 @@ describe("Delete Jobs unit tests", () => { "files-url": "myfakeurl.com/files/records", "phase": 2, "phase-name": "OUTPUT", - "job-correlator": "mycorrelator" + "job-correlator": "mycorrelator", + "exec-member": 'SYS1', + "exec-system": 'SYS1', + "exec-submitted": '2024-01-02T15:58:00.600Z' }; describe("Positive tests", () => { diff --git a/packages/zosjobs/__tests__/__unit__/DownloadJobs.unit.test.ts b/packages/zosjobs/__tests__/__unit__/DownloadJobs.unit.test.ts index b38364adec..2005f52df9 100644 --- a/packages/zosjobs/__tests__/__unit__/DownloadJobs.unit.test.ts +++ b/packages/zosjobs/__tests__/__unit__/DownloadJobs.unit.test.ts @@ -10,7 +10,7 @@ */ import { AbstractSession, Headers, ImperativeError, IO } from "@zowe/imperative"; -import { DownloadJobs, GetJobs, IDownloadAllSpoolContentParms, IDownloadSpoolContentParms, IJobFile } from "../../src"; +import { DownloadJobs, GetJobs, IDownloadAllSpoolContentParms, IDownloadSpoolContentParms, IJobFile, MonitorJobs } from "../../src"; import { ZosmfRestClient } from "@zowe/core-for-zowe-sdk"; import { Writable } from "stream"; @@ -246,6 +246,66 @@ describe("DownloadJobs", () => { expect(uri).toContain("?mode=binary"); }); + it("should allow users to call downloadSpoolContentCommon with correct params (default outDir and waitForJobOutputStatus)", async () => { + let uri: string = ""; + ZosmfRestClient.getStreamed = jest.fn(async (session: AbstractSession, resource: string, reqHeaders?: any[]): Promise => { + uri = resource; + }); + const jobFile: IJobFile = JSON.parse(JSON.stringify(jobFiles[0])); + const spoolParms: IDownloadSpoolContentParms = { + jobFile: jobFile, + jobid: fakeJobID, + jobname: fakeJobName, + waitForOutput: true + }; + + const completedJob: any = { + jobname: fakeJobName, + jobid: fakeJobID, + status: "OUTPUT", + retcode: "CC 0000" + }; + + const waitForJobOutputStatusSpy = jest.spyOn(MonitorJobs, "waitForJobOutputStatus").mockImplementation(() => {return completedJob;}); + + const downloadFilePath = DownloadJobs.getSpoolDownloadFilePath(spoolParms); + + await DownloadJobs.downloadSpoolContentCommon(fakeSession, spoolParms); + + expect(IO.createDirsSyncFromFilePath).toHaveBeenCalledWith(downloadFilePath); + expect(waitForJobOutputStatusSpy).toHaveBeenCalledTimes(1); + }); + + it("should allow users to call downloadSpoolContentCommon with correct params (default outDir and waitForActiveStatus)", async () => { + let uri: string = ""; + ZosmfRestClient.getStreamed = jest.fn(async (session: AbstractSession, resource: string, reqHeaders?: any[]): Promise => { + uri = resource; + }); + const jobFile: IJobFile = JSON.parse(JSON.stringify(jobFiles[0])); + const spoolParms: IDownloadSpoolContentParms = { + jobFile: jobFile, + jobid: fakeJobID, + jobname: fakeJobName, + waitForActive: true + }; + + const completedJob: any = { + jobname: fakeJobName, + jobid: fakeJobID, + status: "OUTPUT", + retcode: "CC 0000" + }; + + const waitForActiveStatusSpy = jest.spyOn(MonitorJobs, "waitForActiveStatus").mockImplementation(() => {return completedJob;}); + + const downloadFilePath = DownloadJobs.getSpoolDownloadFilePath(spoolParms); + + await DownloadJobs.downloadSpoolContentCommon(fakeSession, spoolParms); + + expect(IO.createDirsSyncFromFilePath).toHaveBeenCalledWith(downloadFilePath); + expect(waitForActiveStatusSpy).toHaveBeenCalledTimes(1); + }); + it("should allow users to call downloadSpoolContentCommon with correct parameters (streamed in binary mode)", async () => { let uri: string = ""; ZosmfRestClient.getStreamed = jest.fn(async (s: AbstractSession, resource: string, r?: any[], stream?: Writable): Promise => { diff --git a/packages/zosjobs/__tests__/__unit__/GetJobs.unit.test.ts b/packages/zosjobs/__tests__/__unit__/GetJobs.unit.test.ts index f8a0e3138d..3f66dc74d7 100644 --- a/packages/zosjobs/__tests__/__unit__/GetJobs.unit.test.ts +++ b/packages/zosjobs/__tests__/__unit__/GetJobs.unit.test.ts @@ -596,21 +596,37 @@ describe("GetJobs tests", () => { owner: 'zowe', status: 'active', type: 't', class: 'c', retcode: 'r', url: '', 'files-url': '', 'job-correlator': '', - phase: 1, 'phase-name': 'name', 'reason-not-running': 'no' + phase: 1, 'phase-name': 'name', 'reason-not-running': 'no', + 'exec-started': '2024-01-02T15:57:58.350Z', + 'exec-ended': '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', + "exec-submitted": '2024-01-02T15:58:00.600Z' }, { jobid: '2', jobname: 'b', subsystem: 'sub', owner: 'zowe', status: 'Output', type: 't', class: 'c', retcode: 'r', url: '', 'files-url': '', 'job-correlator': '', - phase: 1, 'phase-name': 'name', 'reason-not-running': 'no' + phase: 1, 'phase-name': 'name', 'reason-not-running': 'no', + 'exec-started': '2024-01-02T15:57:58.350Z', + 'exec-ended': '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', + "exec-submitted": '2024-01-02T15:58:00.600Z' }, { jobid: '3', jobname: 'c', subsystem: 'sub', owner: 'kri', status: 'Output', type: 't', class: 'c', retcode: 'r', url: '', 'files-url': '', 'job-correlator': '', - phase: 1, 'phase-name': 'name', 'reason-not-running': 'no' + phase: 1, 'phase-name': 'name', 'reason-not-running': 'no', + 'exec-started': '2024-01-02T15:57:58.350Z', + 'exec-ended': '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', + "exec-submitted": '2024-01-02T15:58:00.600Z' + } ]; const expectedJobs = [ @@ -619,14 +635,24 @@ describe("GetJobs tests", () => { owner: 'zowe', status: 'Output', type: 't', class: 'c', retcode: 'r', url: '', 'files-url': '', 'job-correlator': '', - phase: 1, 'phase-name': 'name', 'reason-not-running': 'no' + phase: 1, 'phase-name': 'name', 'reason-not-running': 'no', + 'exec-started': '2024-01-02T15:57:58.350Z', + 'exec-ended': '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', + "exec-submitted": '2024-01-02T15:58:00.600Z' }, { jobid: '3', jobname: 'c', subsystem: 'sub', owner: 'kri', status: 'Output', type: 't', class: 'c', retcode: 'r', url: '', 'files-url': '', 'job-correlator': '', - phase: 1, 'phase-name': 'name', 'reason-not-running': 'no' + phase: 1, 'phase-name': 'name', 'reason-not-running': 'no', + 'exec-started': '2024-01-02T15:57:58.350Z', + 'exec-ended': '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', + "exec-submitted": '2024-01-02T15:58:00.600Z' } ]; const filteredResults = GetJobs['filterResultsByStatuses'](jobs, { status: 'OUTPUT', owner: 'zowe' }); diff --git a/packages/zosjobs/__tests__/__unit__/MonitorJobs.unit.test.ts b/packages/zosjobs/__tests__/__unit__/MonitorJobs.unit.test.ts index 98a867120e..4a4e9a8694 100644 --- a/packages/zosjobs/__tests__/__unit__/MonitorJobs.unit.test.ts +++ b/packages/zosjobs/__tests__/__unit__/MonitorJobs.unit.test.ts @@ -430,7 +430,124 @@ describe("MonitorJobs", () => { }); }); }); + describe("waitForActiveStatus", () => { + let waitForStatusCommonSpy = jest.spyOn(privateMonitorJobs, "waitForStatusCommon"); + + beforeEach(() => { + waitForStatusCommonSpy.mockReset(); + waitForStatusCommonSpy = jest.spyOn(privateMonitorJobs, "waitForStatusCommon"); + }); + + afterAll(() => { + waitForStatusCommonSpy.mockRestore(); + }); + + describe("expects", () => { + it("should call waitForStatusCommon", async () => { + const returnValue = "RETURNED FROM COMMON METHOD"; + const jobname = "FAKE"; + const jobid = "FAKE"; + waitForStatusCommonSpy.mockReturnValue(returnValue); + + expect(await MonitorJobs.waitForActiveStatus({test: "1234"} as any, jobname, jobid)).toBe(returnValue); + expect(MonitorJobs.waitForStatusCommon).toHaveBeenCalledTimes(1); + expect(MonitorJobs.waitForStatusCommon).toHaveBeenCalledWith({test: "1234"}, { + jobname, + jobid, + status: JOB_STATUS.ACTIVE + }); + }); + + it("should error if missing session", async () => { + let error; + const jobname = "FAKE"; + const jobid = "FAKE"; + try { + await MonitorJobs.waitForActiveStatus(undefined as any, jobname, jobid); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error instanceof ImperativeError).toBe(true); + expect(error.message).toMatchSnapshot(); + }); + + it("should error if missing jobname", async () => { + let error; + const jobname = undefined as any; + const jobid = "FAKE"; + try { + await MonitorJobs.waitForActiveStatus(session, jobname, jobid); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error instanceof ImperativeError).toBe(true); + expect(error.message).toMatchSnapshot(); + }); + it("should error if missing jobid", async () => { + let error; + const jobname = "FAKE"; + const jobid = undefined as any; + try { + await MonitorJobs.waitForActiveStatus(session, jobname, jobid); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error instanceof ImperativeError).toBe(true); + expect(error.message).toMatchSnapshot(); + }); + }); + describe("error handling", () => { + const parms: IMonitorJobWaitForParms = { + jobid : "FAKE", + jobname: "FAKE" + }; + + it("should throw the proper error if catching an ImperativeError", async () => { + const error = new ImperativeError({ + msg: "EXPECT THIS ERROR TO BE THROWN" + }); + + let expectError: any; + + waitForStatusCommonSpy.mockImplementationOnce(async () => { + throw error; + }); + + try { + await MonitorJobs.waitForStatusCommon(session, parms); + } catch (e) { + expectError = e; + } + + expect(expectError).toBeInstanceOf(ImperativeError); + expect(expectError.message).toMatchSnapshot(); + }); + + it("should throw the proper error if catching a normal error", async () => { + const error = new Error("THIS IS A NORMAL ERROR"); + + let expectError: Error; + + waitForStatusCommonSpy.mockImplementationOnce(async () => { + throw error; + }); + + try { + await MonitorJobs.waitForStatusCommon(session, parms); + } catch (e) { + expectError = e; + } + + expect(expectError).toBeInstanceOf(Error); + expect(expectError.message).toMatchSnapshot(); + }); + }); + + }); describe("waitForOutputStatus", () => { let waitForStatusCommonSpy = jest.spyOn(privateMonitorJobs, "waitForStatusCommon"); diff --git a/packages/zosjobs/__tests__/__unit__/SubmitJobs.unit.test.ts b/packages/zosjobs/__tests__/__unit__/SubmitJobs.unit.test.ts index 55d6257d1c..da4305381a 100644 --- a/packages/zosjobs/__tests__/__unit__/SubmitJobs.unit.test.ts +++ b/packages/zosjobs/__tests__/__unit__/SubmitJobs.unit.test.ts @@ -27,6 +27,7 @@ const expectedMockSpoolContent = "Hello! This is my spool content."; const jobFiles: IJobFile[] = [{ "jobid": fakeJobID, "jobname": fakeJobName, + "id": 0, "recfm": "FB", "lrecl": 80, @@ -44,6 +45,11 @@ const sampleJob: IJob = { "jobid": fakeJobID, "jobname": fakeJobName, "subsystem": "JES2", + "exec-started": '2024-01-02T15:57:58.350Z', + "exec-ended": '2024-01-02T15:58:00.600Z', + "exec-member": 'SYS1', + "exec-system": 'SYS1', + "exec-submitted": '2024-01-02T15:58:00.600Z', "owner": "IBMUSER", "status": "OUTPUT", "type": "job", diff --git a/packages/zosjobs/__tests__/__unit__/__snapshots__/GetJobs.unit.test.ts.snap b/packages/zosjobs/__tests__/__unit__/__snapshots__/GetJobs.unit.test.ts.snap index a8c7fff252..f9d948a744 100644 --- a/packages/zosjobs/__tests__/__unit__/__snapshots__/GetJobs.unit.test.ts.snap +++ b/packages/zosjobs/__tests__/__unit__/__snapshots__/GetJobs.unit.test.ts.snap @@ -14,6 +14,11 @@ exports[`GetJobs tests getJobs APIs should allow getting jobs by common method w Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -29,6 +34,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -49,6 +59,11 @@ exports[`GetJobs tests getJobs APIs should allow getting jobs by common method w Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -64,6 +79,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -84,6 +104,11 @@ exports[`GetJobs tests getJobs APIs should get a list of jobs from getJobs and g Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -99,6 +124,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -119,6 +149,11 @@ exports[`GetJobs tests getJobs APIs should get a list of jobs from getJobs and g Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -134,6 +169,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -154,6 +194,11 @@ exports[`GetJobs tests getJobs APIs should get a list of jobs from getJobsCommon Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -169,6 +214,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -189,6 +239,11 @@ exports[`GetJobs tests getJobs APIs should get a list of jobs from getJobsCommon Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -204,6 +259,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -224,6 +284,11 @@ exports[`GetJobs tests getJobs APIs should get a list of jobs from getJobsCommon Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -239,6 +304,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -259,6 +329,11 @@ exports[`GetJobs tests getJobs APIs should get a list of jobs from getJobsCommon Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -274,6 +349,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -294,6 +374,11 @@ exports[`GetJobs tests getJobs APIs should get jobs by owner 1`] = ` Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -309,6 +394,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -329,6 +419,11 @@ exports[`GetJobs tests getJobs APIs should get jobs by owner and prefix 1`] = ` Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -344,6 +439,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -364,6 +464,11 @@ exports[`GetJobs tests getJobs APIs should get jobs by prefix 1`] = ` Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -379,6 +484,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -416,6 +526,11 @@ exports[`GetJobs tests getJobs APIs should have proper URI when using status 1`] exports[`GetJobs tests getJobs APIs should locate a job by jobid 1`] = ` Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -445,6 +560,11 @@ exports[`GetJobs tests getJobsByParameters should get jobs even when no params a Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -460,6 +580,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -480,6 +605,11 @@ exports[`GetJobs tests getJobsByParameters should get jobs when any of the valid Array [ Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -495,6 +625,11 @@ Array [ }, Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "https://tso1:443/zosmf/restjobs/jobs/J0003781USILDAMDD3CE8146.......%3A/files", "job-correlator": "J0003781USILDAMDD3CE8146.......:", "jobid": "JOB03781", @@ -693,6 +828,11 @@ exports[`GetJobs tests getSpoolFiles APIs should have proper URI when getting sp exports[`GetJobs tests getStatus APIs should get a job via getStatus and getStatusCommon 1`] = ` Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", @@ -711,6 +851,11 @@ Object { exports[`GetJobs tests getStatus APIs should get a job via getStatus and getStatusCommon 2`] = ` Object { "class": "A", + "exec-ended": "2024-01-02T15:58:00.600Z", + "exec-member": "SYS1", + "exec-started": "2024-01-02T15:57:58.350Z", + "exec-submitted": "2024-01-02T15:58:00.600Z", + "exec-system": "SYS1", "files-url": "www.nowhere.com/restjobs/jobs/files", "job-correlator": "123545asdfadf", "jobid": "TSUxxx", diff --git a/packages/zosjobs/__tests__/__unit__/__snapshots__/MonitorJobs.unit.test.ts.snap b/packages/zosjobs/__tests__/__unit__/__snapshots__/MonitorJobs.unit.test.ts.snap index 6419eba869..a738550cd6 100644 --- a/packages/zosjobs/__tests__/__unit__/__snapshots__/MonitorJobs.unit.test.ts.snap +++ b/packages/zosjobs/__tests__/__unit__/__snapshots__/MonitorJobs.unit.test.ts.snap @@ -53,6 +53,16 @@ Object { } `; +exports[`MonitorJobs Public Methods waitForActiveStatus error handling should throw the proper error if catching a normal error 1`] = `"THIS IS A NORMAL ERROR"`; + +exports[`MonitorJobs Public Methods waitForActiveStatus error handling should throw the proper error if catching an ImperativeError 1`] = `"EXPECT THIS ERROR TO BE THROWN"`; + +exports[`MonitorJobs Public Methods waitForActiveStatus expects should error if missing jobid 1`] = `"Expect Error: Jobid required"`; + +exports[`MonitorJobs Public Methods waitForActiveStatus expects should error if missing jobname 1`] = `"Expect Error: Jobname required"`; + +exports[`MonitorJobs Public Methods waitForActiveStatus expects should error if missing session 1`] = `"Expect Error: Session must be defined"`; + exports[`MonitorJobs Public Methods waitForJobOutputStatus error handling should throw the proper error if catching a normal error 1`] = `"THIS IS A NORMAL ERROR"`; exports[`MonitorJobs Public Methods waitForJobOutputStatus error handling should throw the proper error if catching an ImperativeError 1`] = `"EXPECT THIS ERROR TO BE THROWN"`; diff --git a/packages/zosjobs/package.json b/packages/zosjobs/package.json index 713c4a5a47..16dd093f24 100644 --- a/packages/zosjobs/package.json +++ b/packages/zosjobs/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/zos-jobs-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "description": "Zowe SDK to interact with jobs on z/OS", "author": "Zowe", "license": "EPL-2.0", @@ -46,12 +46,12 @@ "prepack": "node ../../scripts/prepareLicenses.js" }, "dependencies": { - "@zowe/zos-files-for-zowe-sdk": "8.2.0" + "@zowe/zos-files-for-zowe-sdk": "8.7.0" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "peerDependencies": { "@zowe/core-for-zowe-sdk": "^8.0.0", diff --git a/packages/zosjobs/src/DownloadJobs.ts b/packages/zosjobs/src/DownloadJobs.ts index a506956862..14728422eb 100644 --- a/packages/zosjobs/src/DownloadJobs.ts +++ b/packages/zosjobs/src/DownloadJobs.ts @@ -10,13 +10,15 @@ */ import * as path from "path"; -import { AbstractSession, ImperativeExpect, IO, Logger, Headers } from "@zowe/imperative"; +import { AbstractSession, ImperativeExpect, IO, Logger, Headers} from "@zowe/imperative"; import { JobsConstants } from "./JobsConstants"; import { IDownloadAllSpoolContentParms } from "./doc/input/IDownloadAllSpoolContentParms"; import { IJobFile } from "./doc/response/IJobFile"; import { ZosmfRestClient } from "@zowe/core-for-zowe-sdk"; import { IDownloadSpoolContentParms } from "./doc/input/IDownloadSpoolContentParms"; import { GetJobs } from "./GetJobs"; +import { MonitorJobs } from "./MonitorJobs"; +import { IJob } from "./doc/response/IJob"; /** @@ -89,6 +91,7 @@ export class DownloadJobs { } } + /** * Download spool content to specified directory * @static @@ -101,8 +104,21 @@ export class DownloadJobs { this.log.trace("Entering downloadSpoolContentCommon with parms %s", JSON.stringify(parms)); ImperativeExpect.keysToBeDefined(parms, ["jobFile"], "You must specify a job file on your 'parms' parameter" + " object to the downloadSpoolContentCommon API."); - const job = parms.jobFile; + //waiting for job to be active before continuing with job download + if (parms.waitForActive) { + await MonitorJobs.waitForActiveStatus(session, parms.jobname, parms.jobid); + } + + //waiting for job status to be output before continuing on with job download + if (parms.waitForOutput) { + await MonitorJobs.waitForJobOutputStatus(session, { + jobname: parms.jobname, + jobid: parms.jobid + } as IJob); + } + + const job = parms.jobFile; let debugMessage = `Downloading spool file ${job.ddname} for job ${job.jobname}(${job.jobid})`; let file: string; if (parms.stream == null) { diff --git a/packages/zosjobs/src/GetJobs.ts b/packages/zosjobs/src/GetJobs.ts index 5cefdfc953..4e7c510f12 100644 --- a/packages/zosjobs/src/GetJobs.ts +++ b/packages/zosjobs/src/GetJobs.ts @@ -194,6 +194,7 @@ export class GetJobs { } } + let resource = JobsConstants.RESOURCE; resource += query === JobsConstants.QUERY_ID ? "" : query; Logger.getAppLogger().info("GetJobs.getJobsCommon() resource: " + resource); diff --git a/packages/zosjobs/src/MonitorJobs.ts b/packages/zosjobs/src/MonitorJobs.ts index 5595dd0ba9..ea720198f6 100644 --- a/packages/zosjobs/src/MonitorJobs.ts +++ b/packages/zosjobs/src/MonitorJobs.ts @@ -46,6 +46,25 @@ export class MonitorJobs { */ public static readonly DEFAULT_ATTEMPTS = Infinity; + /** + * Given the jobname/jobid, waits for the status of the job to be "ACTIVE". + * + * See JSDoc for "waitForStatusCommon" for full details on polling and other logic. + * + * @static + * @param {AbstractSession} session - a Rest client session for z/OSMF + * @param {string} jobname - the z/OS jobname of the job to wait for output status (see z/OSMF Jobs APIs for details) + * @param {string} jobid - the z/OS jobid of the job to wait for output status (see z/OSMF Jobs APIS for details) + * @returns {Promise} - the promise to be fulfilled with IJob object (or rejected with an ImperativeError) + * @memberof MonitorJobs + */ + public static waitForActiveStatus(session: AbstractSession, jobname: string, jobid: string): Promise { + ImperativeExpect.toNotBeNullOrUndefined(jobname, "Jobname required"); + ImperativeExpect.toNotBeNullOrUndefined(jobid, "Jobid required"); + ImperativeExpect.toNotBeNullOrUndefined(session, "Session must be defined"); + return MonitorJobs.waitForStatusCommon(session, {jobname, jobid, status: JOB_STATUS.ACTIVE}); + } + /** * Given an IJob (has jobname/jobid), waits for the status of the job to be "OUTPUT". This API will poll for * the OUTPUT status once every 3 seconds indefinitely. If the polling interval/duration is NOT sufficient, use diff --git a/packages/zosjobs/src/doc/input/IDownloadAllSpoolContentParms.ts b/packages/zosjobs/src/doc/input/IDownloadAllSpoolContentParms.ts index 6a9a8cab19..13ce91987c 100644 --- a/packages/zosjobs/src/doc/input/IDownloadAllSpoolContentParms.ts +++ b/packages/zosjobs/src/doc/input/IDownloadAllSpoolContentParms.ts @@ -77,4 +77,15 @@ export interface IDownloadAllSpoolContentParms { * @memberof IDownloadSpoolContentParms */ encoding?: string; + + /** + * Wait for the job to reach output status + */ + waitForActive?: boolean; + + + /** + * Wait for the job to reach output status + */ + waitForOutput?: boolean; } diff --git a/packages/zosjobs/src/doc/response/IJob.ts b/packages/zosjobs/src/doc/response/IJob.ts index 46e00be949..d181827b77 100644 --- a/packages/zosjobs/src/doc/response/IJob.ts +++ b/packages/zosjobs/src/doc/response/IJob.ts @@ -34,6 +34,41 @@ export interface IJob { */ jobname: string; + /** + * start date of the job + * @type {Date} + * @memberof IJob + */ + "exec-started"?: string; + + /** + * end date of the job + * @type {Date} + * @memberof IJob + */ + "exec-ended"?: string; + + /** + * exec-member + * @type {string} + * @memberof IJob + */ + "exec-member"?: string; + + /** + * exec-submitted + * @type {Date} + * @memberof IJob + */ + "exec-submitted"?: string; + + /** + * exec-system + * @type {String} + * @memberof IJob + */ + "exec-system"?: string; + /** * The primary or secondary JES subsystem. * If this value is null, the job was processed by the primary subsystem. diff --git a/packages/zoslogs/package.json b/packages/zoslogs/package.json index 29ebb100e4..af415af4c1 100644 --- a/packages/zoslogs/package.json +++ b/packages/zoslogs/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/zos-logs-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "description": "Zowe SDK to interact with the z/OS logs", "author": "Zowe", "license": "EPL-2.0", @@ -45,9 +45,9 @@ "prepack": "node ../../scripts/prepareLicenses.js" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "peerDependencies": { "@zowe/core-for-zowe-sdk": "^8.0.0", diff --git a/packages/zosmf/__tests__/__system__/methods/CheckStatus.system.test.ts b/packages/zosmf/__tests__/__system__/methods/CheckStatus.system.test.ts index d08670a8d1..2054077af1 100644 --- a/packages/zosmf/__tests__/__system__/methods/CheckStatus.system.test.ts +++ b/packages/zosmf/__tests__/__system__/methods/CheckStatus.system.test.ts @@ -122,9 +122,9 @@ describe("Check Status Api", () => { expect(error).toBeTruthy(); expect(response).toBeFalsy(); const jsonCauseErrors = error.causeErrors; - expect(jsonCauseErrors.code).toMatch(/(ECONNREFUSED|ECONNRESET)/); + expect(jsonCauseErrors.code).toMatch(/(ECONNREFUSED|ECONNRESET|ETIMEDOUT)/); expect(jsonCauseErrors.syscall).toEqual("connect"); expect(jsonCauseErrors.port).toEqual(badPort); - }); + }, 300000); }); }); diff --git a/packages/zosmf/__tests__/__system__/methods/ListDefinedSystems.system.test.ts b/packages/zosmf/__tests__/__system__/methods/ListDefinedSystems.system.test.ts index faeae9c6c2..a855091dae 100644 --- a/packages/zosmf/__tests__/__system__/methods/ListDefinedSystems.system.test.ts +++ b/packages/zosmf/__tests__/__system__/methods/ListDefinedSystems.system.test.ts @@ -122,8 +122,8 @@ describe("List Defined Systems Api", () => { expect(error).toBeTruthy(); expect(response).toBeFalsy(); const jsonCauseErrors = error.causeErrors; - expect(jsonCauseErrors.code).toMatch(/(ECONNREFUSED|ECONNRESET)/); + expect(jsonCauseErrors.code).toMatch(/(ECONNREFUSED|ECONNRESET|ETIMEDOUT)/); expect(jsonCauseErrors.syscall).toMatch(/(connect|read)/); - }); + }, 300000); }); }); diff --git a/packages/zosmf/package.json b/packages/zosmf/package.json index 70450fa279..b90cae3280 100644 --- a/packages/zosmf/package.json +++ b/packages/zosmf/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/zosmf-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "description": "Zowe SDK to interact with the z/OS Management Facility", "author": "Zowe", "license": "EPL-2.0", @@ -44,9 +44,9 @@ "prepack": "node ../../scripts/prepareLicenses.js" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "peerDependencies": { "@zowe/core-for-zowe-sdk": "^8.0.0", diff --git a/packages/zostso/CHANGELOG.md b/packages/zostso/CHANGELOG.md index f5a53aa697..25f79e7a01 100644 --- a/packages/zostso/CHANGELOG.md +++ b/packages/zostso/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to the Zowe z/OS TSO SDK package will be documented in this file. +## `8.6.2` + +- BugFix: Fixed imports that failed to resolve. [#2343](https://github.com/zowe/zowe-cli/pull/2343) + +## `8.6.0` + +- Enhancement: Issue `app` commands to better target communication with a TSO/E application. The `app` command is now included in the `start`/`send` command group and the new `receive` command group, +allowing direct interaction with an application through a z/OS message queue. [#2280] (https://github.com/zowe/zowe-cli/pull/2280) + ## `8.1.1` - BugFix: Updated peer dependencies to `^8.0.0`, dropping support for versions tagged `next`. [#2287](https://github.com/zowe/zowe-cli/pull/2287) diff --git a/packages/zostso/__tests__/__system__/__scripts__/receive/receive_tso_app.sh b/packages/zostso/__tests__/__system__/__scripts__/receive/receive_tso_app.sh new file mode 100755 index 0000000000..7066cf6185 --- /dev/null +++ b/packages/zostso/__tests__/__system__/__scripts__/receive/receive_tso_app.sh @@ -0,0 +1,13 @@ +#!/bin/bash +account=$1 +host=$2 +port=$3 +user=$4 +password=$5 +ru=$6 +servletKey=$7 +appKey=$8 +rur=$9 + +zowe zos-tso r app --app-key $appKey --servlet-key "$servletKey" --rur $rur --account $account --host $host --port $port --user $user --password $password --ru $ru +exit $? diff --git a/packages/zostso/__tests__/__system__/__scripts__/send/send_tso_app.sh b/packages/zostso/__tests__/__system__/__scripts__/send/send_tso_app.sh new file mode 100755 index 0000000000..633d9e3353 --- /dev/null +++ b/packages/zostso/__tests__/__system__/__scripts__/send/send_tso_app.sh @@ -0,0 +1,13 @@ +#!/bin/bash +account=$1 +host=$2 +port=$3 +user=$4 +password=$5 +ru=$6 +servletKey=$7 +message=$8 +appKey=$9 + +zowe zos-tso send app --ak "$appKey" --sk "$servletKey" --message "$message" --account $account --host $host --port $port --user $user --password $password --ru $ru +exit $? diff --git a/packages/zostso/__tests__/__system__/__scripts__/start/start_app_existing_as.sh b/packages/zostso/__tests__/__system__/__scripts__/start/start_app_existing_as.sh new file mode 100755 index 0000000000..1203459b1a --- /dev/null +++ b/packages/zostso/__tests__/__system__/__scripts__/start/start_app_existing_as.sh @@ -0,0 +1,12 @@ +#!/bin/bash +account=$1 +host=$2 +port=$3 +user=$4 +password=$5 +ru=$6 +servletKey=$7 +queueID=$8 +file=$9 +zowe zos-tso start app --app-key "test2" --startup "EXEC '$file'" --servlet-key $servletKey --queue-id $queueID --account $account --host $host --port $port --user $user --password $password --ru $ru +exit $? diff --git a/packages/zostso/__tests__/__system__/__scripts__/start/start_app_new_as.sh b/packages/zostso/__tests__/__system__/__scripts__/start/start_app_new_as.sh new file mode 100755 index 0000000000..d250cc7d95 --- /dev/null +++ b/packages/zostso/__tests__/__system__/__scripts__/start/start_app_new_as.sh @@ -0,0 +1,10 @@ +#!/bin/bash +account=$1 +host=$2 +port=$3 +user=$4 +password=$5 +ru=$6 +file=$7 +zowe zos-tso start app --app-key "test2" --startup "EXEC '$file'" --account $account --host $host --port $port --user $user --password $password --ru $ru +exit $? diff --git a/packages/zostso/__tests__/__system__/__scripts__/start/test_app.rexx b/packages/zostso/__tests__/__system__/__scripts__/start/test_app.rexx new file mode 100644 index 0000000000..46e41bdb61 --- /dev/null +++ b/packages/zostso/__tests__/__system__/__scripts__/start/test_app.rexx @@ -0,0 +1,206 @@ +/* REXX hello world application */ + +mainRc=0 + +PARSE ARG oMsgType iMsgType msgQueueId . /* Pick up 3 input parms. */ + +say 'HELLOW exec processing has started.' + +say 'UNIX message queue id = 'msgQueueId +say 'Input message type = 'iMsgType +say 'Output message type = 'oMsgType +iMsgType = D2C(iMsgType, 4) +oMsgType = D2C(oMsgType, 4) + +if syscalls('ON')>3 then + do + say 'Unable to establish the UNIX SYSCALL environment.' + mainRc=-1 + end + +/* Perform a blocking read on the message queue to get the application + input. +*/ +if mainRc=0 then + do + say 'Reading application input from the UNIX message queue.' + ADDRESS SYSCALL 'msgrcv (msgQueueId) iMsg 999 0 iMsgType' + if rc<>0 then + do + say 'Error reading input. ''msgrcv'' rc = 'mainrc + mainRc=-1 + end + else + do + iMsg = atoe(iMsg) /* Convert input from ASCII to EBCDIC. */ + say 'Application input = 'iMsg + end + end + + +do while iMsg \== 'EXIT' + /* Generate the application response. */ + if mainRc=0 then + do + select + when iMsg = 'RANDOM' then + do + oMsg = randomString() + oMsg = etoa(oMsg) + ADDRESS SYSCALL 'msgsnd (msgQueueId) oMsg' length(oMsg) '0 oMsgType' + oMsg = '"READY "' + oMsg = etoa(oMsg) + ADDRESS SYSCALL 'msgsnd (msgQueueId) oMsg' length(oMsg) '0 oMsgType' + mainRc = 0 + leave + end + when SUBSTR(iMsg,1,4) = 'LONG' then + do + PARSE VAR iMsg "LONG" N + N = N+ 0 + do i = 0 to N + say i + end + + say "READY " + oMsg = '"READY "' + oMsg = etoa(oMsg) + ADDRESS SYSCALL 'msgsnd (msgQueueId) oMsg' length(oMsg) '0 oMsgType' + mainRc = 0 + leave + end + otherwise + oMsg = '"' || MVSVAR(iMsg) || '"' + end + if oMsg = '' then + oMsg = 'No information returned' + /*oMsg = '"Hello 'iMsg'!"' */ + say 'Response = 'oMsg + + /* Write the response to the UNIX message queue. */ + oMsg = etoa(oMsg) /* Convert EBCDIC to ASCII. */ + say 'Writing response to the UNIX message queue.' + ADDRESS SYSCALL 'msgsnd (msgQueueId) oMsg' length(oMsg) '0 oMsgType' + if rc<>0 then + do + say 'Error writing response to the UNIX message queue. ', + '''msgsnd'' rc = 'rc'.' + mainRc=-1 + leave + end + end + say 'Reading application input from the UNIX message queue2.' + ADDRESS SYSCALL 'msgrcv (msgQueueId) iMsg 999 0 iMsgType' + if rc<>0 then + do + say 'Error reading input. ''msgrcv'' rc = 'mainrc + mainRc=-1 + end + else + do + iMsg = atoe(iMsg) /* Convert input from ASCII to EBCDIC. */ + say 'Application input2 = 'iMsg + end +end + +say 'HELLOW exec processing is complete with rc = 'mainRc'.' + +return mainRc + + +/* Convert an ASCII string to EBCDIC. */ +atoe: + parse arg msg /* */ + msg = convertstring('ATOE' msg) /* */ + return msg /* */ +/* Convert an EBCDIC string to ASCII. */ +etoa: + parse arg msg /* */ + msg = convertstring('ETOA' msg) /* */ + return msg /* */ + +/* + Convert ASCII to EBCDIC and EBCDIC to ASCII. Use the UNIX iconv + command. Positional input parameters: + + 1 -- conversion type, either "ATOE" or "ETOA" + 2 -- string to be converted. +*/ +convertstring: + parse arg conv msg + /* Create temporary file names. */ + fn = '/tmp/' || USERID() || '.' || TIME('L') + ifn = fn || '.i' /* Name of file to contain input text. */ + ofn = fn || '.o' /* Name of file to contain output text. */ + address syscall 'creat (ifn) 700' + fd=retval + if retval=-1 then + do + say 'Error creating temporary file 'ifn'. ', + 'errno: 'errno' errnojr: 'errnojr + return -1 + end + /* Write the input text to the temporary file. */ + address syscall 'write (fd) msg' length(msg) + address syscall 'close (fd)' + /* + Call iconv to read the temp file containg intput text, and write + the converted text to the output file. + */ + select /* Which conversion was requested?*/ + when conv = 'ATOE' then /* ASCII to EBCDIC? */ + retcode=bpxwunix('iconv -f ISO8859-1 -t IBM-1047 'ifn' > 'ofn) + when conv = 'ETOA' then /* EBCDIC to ASCII? */ + retcode=bpxwunix('iconv -f IBM-1047 -t ISO8859-1 'ifn' > 'ofn) + otherwise + do + say 'Unknown conversion type: "'conv'". ', + 'Acceptable values are "ATOE" or "ETOA".' + return -1 + end + end + if retcode<>0 then + do + say conv' iconv failed with rc = 'retcode'.' + return -1 + end + /* Read the converted text from the output file. */ + address syscall 'open (ofn)' o_rdonly + fd=retval + if retval=-1 then + do + say 'Open failed for file 'ofn'. ', + 'errno: 'errno' errnojr: 'errnojr + return -1 + end + address syscall 'fstat (fd) st.' /* Get file status, to get size. */ + address syscall 'read (fd) msg' st.st_size /* Read entire file. */ + address syscall 'close (fd)' + /* Delete the temporary files. */ + address syscall 'unlink (ofn)' + address syscall 'unlink (ifn)' + return msg /* Return the converted string. */ + +longOutput: + do i = 0 to 4000 + say i + end + +randomString: + randomChars = '' + numChars = 10 + + /* Loop to generate random characters */ + DO i = 1 TO numChars + /* Generate a random number between 65 and 90 for (A-Z) */ + randomNum = RANDOM(65, 90) + + /* Convert the random character using the C2D function */ + randomChar = C2D(randomNum) + + /* Concatenate the random character to the string */ + randomChars = randomChars || randomChar + END + + /* Display the result */ + return 'Random Characters: ' randomChars diff --git a/packages/zostso/__tests__/__system__/api.TsoASApp.system.test.ts b/packages/zostso/__tests__/__system__/api.TsoASApp.system.test.ts new file mode 100644 index 0000000000..199725d1a8 --- /dev/null +++ b/packages/zostso/__tests__/__system__/api.TsoASApp.system.test.ts @@ -0,0 +1,298 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { ImperativeError, Session } from "@zowe/imperative"; +import { IIssueResponse, StartTso, StopTso } from "../../src"; +import { ITestEnvironment } from "@zowe/cli-test-utils"; +import { TestEnvironment } from "../../../../__tests__/__src__/environment/TestEnvironment"; +import { ITestPropertiesSchema } from "../../../../__tests__/__src__/properties/ITestPropertiesSchema"; +import { runCliScript } from "@zowe/cli-test-utils"; +import { Upload, Create, CreateDataSetTypeEnum, Delete } from "../../../zosfiles/src"; +import { getUniqueDatasetName } from "../../../../__tests__/__src__/TestUtils"; + +let testEnvironment: ITestEnvironment; +let systemProperties: ITestPropertiesSchema; +let REAL_SESSION: Session; +let ACCOUNT_NUMBER: string; +let defaultSystem: ITestPropertiesSchema; +let dsname: string; + +describe("All test", () => { + beforeAll(async () => { + testEnvironment = await TestEnvironment.setUp({ + testName: "zos_tso_as_app", + }); + systemProperties = testEnvironment.systemTestProperties; + REAL_SESSION = TestEnvironment.createZosmfSession(testEnvironment); + ACCOUNT_NUMBER = systemProperties.tso.account; + defaultSystem = testEnvironment.systemTestProperties; + dsname = getUniqueDatasetName(`${defaultSystem.zosmf.user}.ZOSTEST`); + await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_PARTITIONED, dsname); + await Upload.fileToDataset(REAL_SESSION, __dirname + "/__scripts__/start/test_app.rexx", dsname, {}); + }); + afterAll(async () => { + await Delete.dataSet(REAL_SESSION, dsname); + await TestEnvironment.cleanUp(testEnvironment); + }); + + describe("Start TSO app tests", () => { + it("should create TSO address space and run an application instance at the created AS", async () => { + let error: ImperativeError; + + const response = runCliScript( + __dirname + "/__scripts__/start/start_app_new_as.sh", + testEnvironment, + [ + ACCOUNT_NUMBER, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + dsname+"(TESTAPP)" + ] + ); + + expect(response.stdout.toString()).toBeDefined(); + expect(response.stdout.toString()).toContain( + "HELLOW exec processing has started" + ); + expect(error).toBeUndefined(); + await StopTso.stop( + REAL_SESSION, + JSON.parse(response.stdout.toString()).servletKey + ); + }); + + it("should create TSO application instance on existing address space", async () => { + dsname = getUniqueDatasetName(`${defaultSystem.zosmf.user}.ZOSTEST`); + await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_PARTITIONED, dsname); + await Upload.fileToDataset(REAL_SESSION, __dirname + "/__scripts__/start/test_app.rexx", dsname, {}); + + const startResponse: IIssueResponse = { + success: false, + startResponse: await StartTso.start( + REAL_SESSION, + ACCOUNT_NUMBER + ), + startReady: false, + zosmfResponse: null, + commandResponse: null, + stopResponse: null, + }; + + const response = runCliScript( + __dirname + "/__scripts__/start/start_app_existing_as.sh", + testEnvironment, + [ + ACCOUNT_NUMBER, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + startResponse.startResponse.zosmfTsoResponse.servletKey, + startResponse.startResponse.zosmfTsoResponse.queueID, + dsname+"(TESTAPP)" + ] + ); + expect(response.stdout.toString()).toBeDefined(); + expect(response.stdout.toString()).toContain( + "HELLOW exec processing has started" + ); + + //Clean up test + await StopTso.stop( + REAL_SESSION, + startResponse.startResponse.zosmfTsoResponse.servletKey + ); + }); + }); + describe("Send TSO app tests", () => { + it("Should send message to TSO address space app", async () => { + const startResponse = runCliScript( + __dirname + "/__scripts__/start/start_app_new_as.sh", + testEnvironment, + [ + ACCOUNT_NUMBER, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + dsname+"(TESTAPP)" + ] + ); + + const startServletkey = JSON.parse( + startResponse.stdout.toString() + ).servletKey; + + const response = runCliScript( + __dirname + "/__scripts__/send/send_tso_app.sh", + testEnvironment, + [ + ACCOUNT_NUMBER, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + startServletkey, + "LONG 100", + "test2", + ] + ); + const response2 = runCliScript( + __dirname + "/__scripts__/receive/receive_tso_app.sh", + testEnvironment, + [ + ACCOUNT_NUMBER, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + startServletkey, + "test2", + "true", + ] + ); + const responses = response.stdout.toString() + response2.stdout.toString(); + + expect(response.stdout.toString()).toBeDefined(); + expect(response2.stdout.toString()).toBeDefined(); + expect(responses).toContain( + "Application input = LONG 100" + ); + expect(responses).toContain("servletKey"); + expect(responses).toContain("READY "); + + //Clean up test + await StopTso.stop(REAL_SESSION, startServletkey); + }); + }); + describe("Receive TSO app tests", () => { + it("should pull from message queue but not reach the end (--no-rur)", async () => { + const startResponse = runCliScript( + __dirname + "/__scripts__/start/start_app_new_as.sh", + testEnvironment, + [ + ACCOUNT_NUMBER, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + dsname+"(TESTAPP)" + ] + ); + const startServletkey = JSON.parse( + startResponse.stdout.toString() + ).servletKey; + runCliScript( + __dirname + "/__scripts__/send/send_tso_app.sh", + testEnvironment, + [ + ACCOUNT_NUMBER, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + startServletkey, + "LONG 4000", // Greater than 1000 such that receive will contain data. + "test2", + ] + ); + const response = runCliScript( + __dirname + "/__scripts__/receive/receive_tso_app.sh", + testEnvironment, + [ + ACCOUNT_NUMBER, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + startServletkey, + "test2", + "false", + ] + ); + expect(response.stdout.toString()).toContain("1"); + expect(response.stdout.toString()).not.toContain("3999"); + + //Clean up test + await StopTso.stop(REAL_SESSION, startServletkey); + }); + it("should empty message queue using --rur flag", async () => { + const startResponse = runCliScript( + __dirname + "/__scripts__/start/start_app_new_as.sh", + testEnvironment, + [ + ACCOUNT_NUMBER, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + dsname+"(TESTAPP)" + ] + ); + const startServletkey = JSON.parse( + startResponse.stdout.toString() + ).servletKey; + runCliScript( + __dirname + "/__scripts__/send/send_tso_app.sh", + testEnvironment, + [ + ACCOUNT_NUMBER, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + startServletkey, + "LONG 4000", // Greater than 1000 such that receive will contain data. + "test2", + ] + ); + const response = runCliScript( + __dirname + "/__scripts__/receive/receive_tso_app.sh", + testEnvironment, + [ + ACCOUNT_NUMBER, + defaultSystem.zosmf.host, + defaultSystem.zosmf.port, + defaultSystem.zosmf.user, + defaultSystem.zosmf.password, + defaultSystem.zosmf.rejectUnauthorized, + startServletkey, + "test2", + "true", + ] + ); + + expect(response.stdout.toString()).toContain("1000"); + expect(response.stdout.toString()).toContain("2000"); + expect(response.stdout.toString()).toContain("3000"); + expect(response.stdout.toString()).toContain("4000"); + expect(response.stdout.toString()).toContain( + "HELLOW exec processing is complete with rc = 0." + ); + expect(response.stdout.toString()).toContain("READY"); + + //Clean up test + await StopTso.stop(REAL_SESSION, startServletkey); + }); + }); +}); diff --git a/packages/zostso/__tests__/__unit__/ReceiveTsoApp.unit.test.ts b/packages/zostso/__tests__/__unit__/ReceiveTsoApp.unit.test.ts new file mode 100644 index 0000000000..f13ca5a131 --- /dev/null +++ b/packages/zostso/__tests__/__unit__/ReceiveTsoApp.unit.test.ts @@ -0,0 +1,204 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { Session } from "@zowe/imperative"; +import { ZosmfRestClient } from "@zowe/core-for-zowe-sdk"; +import { AddressSpaceApps } from "../../src"; +import { ITsoAppCommunicationParms } from "../../src/doc/input/ITsoAppCommunicationParms"; + +const PRETEND_SESSION = new Session({ + user: "usr", + password: "pasword", + hostname: "host.com", + port: 443, + type: "basic", + rejectUnauthorized: false, +}); + +const MOCK_RECEIVE_RESPONSE: any = { + version: undefined, + reused: false, + timeout: false, + servletKey: "ZOWEUSER-127-aabeaaag", + queueID: null, + tsoData: [ + { + "TSO MESSAGE": { VERSION: "0100", DATA: "Processing started." }, + }, + { + "TSO MESSAGE": { VERSION: "0100", DATA: "READY" }, + }, + ], +}; + +const MOCK_TIMEOUT_RESPONSE: any = { + version: undefined, + reused: false, + timeout: true, + servletKey: "ZOWEUSER-127-aabeaaag", + queueID: null, + tsoData: [], +}; + +describe("ReceiveTsoApp behavior", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should format a successful response to match expected syntax", async () => { + jest.spyOn(ZosmfRestClient, "getExpectJSON").mockResolvedValueOnce( + MOCK_RECEIVE_RESPONSE + ); + + const params: ITsoAppCommunicationParms = { + servletKey: "ZOWEUSER-127-aabeaaag", + appKey: "someAppKey", + timeout: 10, + receiveUntilReady: true, + }; + + const response = await AddressSpaceApps.receive( + PRETEND_SESSION, + "123456", + params + ); + + expect(ZosmfRestClient.getExpectJSON).toHaveBeenCalledWith( + PRETEND_SESSION, + `/zosmf/tsoApp/app/${params.servletKey}/${params.appKey}` + ); + + expect(response).toEqual({ + version: undefined, + reused: false, + timeout: false, + servletKey: "ZOWEUSER-127-aabeaaag", + queueID: null, + tsoData: [ + { VERSION: "0100", DATA: "Processing started." }, + { VERSION: "0100", DATA: "READY" }, + ], + }); + }); + + it("should stop receiving when 'READY' keyword is detected", async () => { + jest.spyOn(ZosmfRestClient, "getExpectJSON").mockResolvedValueOnce( + MOCK_RECEIVE_RESPONSE + ); + + const params: ITsoAppCommunicationParms = { + servletKey: "ZOWEUSER-127-aabeaaag", + appKey: "someAppKey", + timeout: 10, + receiveUntilReady: true, + }; + + const response = await AddressSpaceApps.receive( + PRETEND_SESSION, + "123456", + params + ); + + expect(ZosmfRestClient.getExpectJSON).toHaveBeenCalledTimes(1); // Should only call once + expect( + response.tsoData.some((data) => data.DATA === "READY") + ).toBeTruthy(); + }); + + it("should handle timeout by returning a partial response if timeout occurs", async () => { + jest.spyOn(ZosmfRestClient, "getExpectJSON").mockResolvedValueOnce( + MOCK_TIMEOUT_RESPONSE + ); + + const params: ITsoAppCommunicationParms = { + servletKey: "ZOWEUSER-127-aabeaaag", + appKey: "someAppKey", + timeout: 1, + receiveUntilReady: true, + }; + + const response = await AddressSpaceApps.receive( + PRETEND_SESSION, + "123456", + params + ); + + expect(response).toEqual(MOCK_TIMEOUT_RESPONSE); + expect(response.timeout).toBe(true); + }); + + it("should throw an error if an error occurs and no partial response is available", async () => { + jest.spyOn(ZosmfRestClient, "getExpectJSON").mockRejectedValueOnce( + new Error("Network error") + ); + + const params: ITsoAppCommunicationParms = { + servletKey: "ZOWEUSER-127-aabeaaag", + appKey: "someAppKey", + timeout: 10, + receiveUntilReady: true, + }; + + await expect( + AddressSpaceApps.receive(PRETEND_SESSION, "123456", params) + ).rejects.toThrow("Network error"); + }); + + it("should append multiple responses to combinedResponse.tsoData", async () => { + // Mock the first response without "READY" to simulate multiple API calls + const mockResponse1: any = { + version: "0100", + reused: false, + timeout: false, + servletKey: "ZOWEUSER-127-aabeaaag", + queueID: null, + tsoData: [{"TSO MESSAGE":{ VERSION: "0100", DATA: "First response data." }}], + }; + + // Mock the second response containing "READY" to trigger stopping condition + const mockResponse2: any = { + version: "0100", + reused: false, + timeout: false, + servletKey: "ZOWEUSER-127-aabeaaag", + queueID: null, + tsoData: [ + {"TSO MESSAGE":{ VERSION: "0100", DATA: "Second response data." }}, + {"TSO MESSAGE":{ VERSION: "0100", DATA: "READY" }} + ], + }; + + // Set up mock to return these responses in sequence + jest.spyOn(ZosmfRestClient, "getExpectJSON") + .mockResolvedValueOnce(mockResponse1) // First call + .mockResolvedValueOnce(mockResponse2); // Second call + + const params: ITsoAppCommunicationParms = { + servletKey: "ZOWEUSER-127-aabeaaag", + appKey: "someAppKey", + timeout: 10, + receiveUntilReady: true, + }; + + const response = await AddressSpaceApps.receive( + PRETEND_SESSION, + "123456", + params + ); + + expect(response.tsoData).toEqual([ + { VERSION: "0100", DATA: "First response data." }, + { VERSION: "0100", DATA: "Second response data." }, + { VERSION: "0100", DATA: "READY" }, + ]); + expect(ZosmfRestClient.getExpectJSON).toHaveBeenCalledTimes(2); // Two calls due to no "READY" in the first response + }); +}); diff --git a/packages/zostso/__tests__/__unit__/SendTsoApp.unit.test.ts b/packages/zostso/__tests__/__unit__/SendTsoApp.unit.test.ts new file mode 100644 index 0000000000..3aadc54f2a --- /dev/null +++ b/packages/zostso/__tests__/__unit__/SendTsoApp.unit.test.ts @@ -0,0 +1,95 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { Session } from "@zowe/imperative"; +import { ZosmfRestClient } from "@zowe/core-for-zowe-sdk"; +import { AddressSpaceApps } from "../../src"; +import { ITsoAppCommunicationParms } from "../../src/doc/input/ITsoAppCommunicationParms"; + +const PRETEND_SESSION = new Session({ + user: "usr", + password: "pasword", + hostname: "host.com", + port: 443, + type: "basic", + rejectUnauthorized: false, +}); + +const MOCK_SEND_RESPONSE = Promise.resolve({ + servletKey: "ZOWEUSER-127-aabeaaag", + ver: "0100", + tsoData: [ + { + "TSO MESSAGE": { + VERSION: "0100", + DATA: "HELLOW exec processing has started.", + }, + }, + { + "TSO MESSAGE": { + VERSION: "0100", + DATA: "UNIX message queue id = 1048608", + }, + }, + ], + reused: false, + timeout: false, +}); + +describe("SendTsoApp behavior", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should send a TSO application message and receive a response", async () => { + jest.spyOn(ZosmfRestClient, "putExpectJSON").mockReturnValue( + MOCK_SEND_RESPONSE + ); + + const params: ITsoAppCommunicationParms = { + servletKey: "ZOWEUSER-127-aabeaaag", + appKey: "someAppKey", + message: "Test message", + }; + + const response = await AddressSpaceApps.send( + PRETEND_SESSION, + "123456", + params, + null + ); + + expect(ZosmfRestClient.putExpectJSON).toHaveBeenCalledWith( + PRETEND_SESSION, + `/zosmf/tsoApp/app/${params.servletKey}/${params.appKey}`, + [expect.any(String), "text/plain"], + params.message + ); + + expect(response).toEqual({ + version: "0100", + reused: false, + timeout: false, + servletKey: "ZOWEUSER-127-aabeaaag", + queueID: null, + tsoData: [ + { + VERSION: "0100", + DATA: "HELLOW exec processing has started.", + }, + { + VERSION: "0100", + DATA: "UNIX message queue id = 1048608", + }, + ], + }); + }); +}); diff --git a/packages/zostso/__tests__/__unit__/StartTsoApp.unit.test.ts b/packages/zostso/__tests__/__unit__/StartTsoApp.unit.test.ts new file mode 100644 index 0000000000..688347aeaa --- /dev/null +++ b/packages/zostso/__tests__/__unit__/StartTsoApp.unit.test.ts @@ -0,0 +1,114 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { Session } from "@zowe/imperative"; +import { ZosmfRestClient } from "@zowe/core-for-zowe-sdk"; +import { AddressSpaceApps, StartTso, IStartStopResponses } from "../../src"; + +const PRETEND_SESSION = new Session({ + user: "usr", + password: "pasword", + hostname: "host.com", + port: 443, + type: "basic", + rejectUnauthorized: false +}); +const MOCK_RESPONSE = Promise.resolve({ + version: "0100", + reused: false, + timeout: false, + servletKey: "ZOWEUSER-123-aaaaaa", + queueID: "983068", + tsoData: [ + { "TSO MESSAGE": { VERSION: "0100", DATA: "HELLOW exec processing has started." } }, + { "TSO MESSAGE": { VERSION: "0100", DATA: "UNIX message queue id = 983068" } }, + { "TSO MESSAGE": { VERSION: "0100", DATA: "Input message type = 32772" } }, + { "TSO MESSAGE": { VERSION: "0100", DATA: "Output message type = 4" } }, + { "TSO MESSAGE": { VERSION: "0100", DATA: "Reading application input from the UNIX message queue." } } + ] +}); + +const MOCK_START_RESPONSE: Promise = Promise.resolve({ + collectedResponses: [], + messages: "IKJ56455I ZOWEUSER LOGON IN PROGRESS AT 11:18:56 ON OCTOBER 14, 2024\nIKJ56951I NO BROADCAST MESSAGES\nREADY \n", + servletKey: "ZOWEUSER-123-aaaaaa", + success: true, + zosmfTsoResponse: { + ver: "0100", + queueID: "983068", + reused: false, + servletKey: "ZOWEUSER-123-aaaaaa", + sessionID: "0x00", + timeout: false, + tsoData: [{}] + } +}); + +describe("StartTsoApp behavior", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should issue a TSO address space and start an application at that address space", async () => { + jest.spyOn(StartTso, "start").mockReturnValue(MOCK_START_RESPONSE); + jest.spyOn(ZosmfRestClient, "postExpectJSON").mockReturnValue(MOCK_RESPONSE); + + const response = await AddressSpaceApps.start(PRETEND_SESSION, "izuacct", { + startupCommand: "EXEC 'TEST.EXEC(THISSCRIPTDOESNOTEXIST)'", + appKey: "testappkey", + }, null); + + expect(StartTso.start).toHaveBeenCalled(); + expect(ZosmfRestClient.postExpectJSON).toHaveBeenCalledWith( + PRETEND_SESSION, + expect.stringContaining("/app/"), + expect.any(Array), + expect.objectContaining({ + startcmd: expect.stringContaining("THISSCRIPTDOESNOTEXIST") + }) + ); + expect(response).toMatchObject({ + servletKey: "ZOWEUSER-123-aaaaaa", + queueID: "983068", + tsoData: expect.arrayContaining([ + expect.objectContaining({ DATA: "HELLOW exec processing has started." }) + ]) + }); + }); + it("should start an application at a specified existing TSO address space", async () => { + jest.spyOn(StartTso, "start").mockReturnValue(MOCK_START_RESPONSE); + jest.spyOn(ZosmfRestClient, "postExpectJSON").mockReturnValue(MOCK_RESPONSE); + + const response = await AddressSpaceApps.start(PRETEND_SESSION, "izuacct", { + startupCommand: "EXEC 'TEST.EXEC(THISSCRIPTDOESNOTEXIST)'", + appKey: "testappkey", + queueID: "12345", + servletKey: "ZOWEUSER-123-aaaaaaaa" + }, null); + + expect(StartTso.start).not.toHaveBeenCalled(); + expect(ZosmfRestClient.postExpectJSON).toHaveBeenCalledWith( + PRETEND_SESSION, + expect.stringContaining("/app/"), + expect.any(Array), + expect.objectContaining({ + startcmd: expect.stringContaining("THISSCRIPTDOESNOTEXIST") + }) + ); + expect(response).toMatchObject({ + servletKey: "ZOWEUSER-123-aaaaaaaa", + queueID: "12345", + tsoData: expect.arrayContaining([ + expect.objectContaining({ DATA: "HELLOW exec processing has started." }) + ]) + }); + }); +}); diff --git a/packages/zostso/package.json b/packages/zostso/package.json index a6b5c113ef..6d7d9bd28a 100644 --- a/packages/zostso/package.json +++ b/packages/zostso/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/zos-tso-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "description": "Zowe SDK to interact with TSO on z/OS", "author": "Zowe", "license": "EPL-2.0", @@ -45,12 +45,12 @@ "prepack": "node ../../scripts/prepareLicenses.js" }, "dependencies": { - "@zowe/zosmf-for-zowe-sdk": "8.2.0" + "@zowe/zosmf-for-zowe-sdk": "8.7.0" }, "devDependencies": { - "@zowe/cli-test-utils": "8.2.0", - "@zowe/core-for-zowe-sdk": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/core-for-zowe-sdk": "8.7.0", + "@zowe/imperative": "8.7.0" }, "peerDependencies": { "@zowe/core-for-zowe-sdk": "^8.0.0", diff --git a/packages/zostso/src/AddressSpaceApps.ts b/packages/zostso/src/AddressSpaceApps.ts new file mode 100644 index 0000000000..b79a93d8b9 --- /dev/null +++ b/packages/zostso/src/AddressSpaceApps.ts @@ -0,0 +1,150 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { noAccountNumber, TsoConstants } from "./TsoConstants"; +import { ITsoAppCommunicationParms } from "./doc/input/ITsoAppCommunicationParms"; +import { IASAppResponse } from "./doc/IASAppResponse"; +import { AbstractSession, Headers } from "@zowe/imperative"; +import { ZosmfRestClient } from "@zowe/core-for-zowe-sdk"; +import { IStartTsoParms } from "./doc/input/IStartTsoParms"; +import { TsoValidator } from "./TsoValidator"; +import { IStartTsoAppParms } from "./doc/input/IStartTsoAppParms"; +import { StartTso } from "./StartTso"; +import { IIssueResponse } from "./doc/IIssueResponse"; + +export class AddressSpaceApps { + /** + * Format API response to IASAppResponse structure. + * @static + * @param {any} response - Raw API response + * @param {string | null} servletKey - Servlet key if present + * @param {string | null} queueID - Queue ID if present + * @returns {IASAppResponse} Formatted API response + */ + private static formatResponse(response: any, servletKey: string | null, queueID: string | null): IASAppResponse { + return { + version: response.ver, + reused: response.reused, + timeout: response.timeout, + servletKey: servletKey ?? null, + queueID: queueID ?? null, + tsoData: response.tsoData?.map((message: any) => { + const messageKey = message["TSO MESSAGE"] ? "TSO MESSAGE" : "TSO PROMPT"; + return { + VERSION: message[messageKey].VERSION, + DATA: message[messageKey].DATA, + }; + }) || [response.appData], + }; + } + + public static async start( + session: AbstractSession, + accountNumber: string, + params: IStartTsoAppParms, + startParms: IStartTsoParms + ): Promise { + TsoValidator.validateSession(session); + TsoValidator.validateNotEmptyString(accountNumber, noAccountNumber.message); + + if (!params.queueID || !params.servletKey) { + const response: IIssueResponse = { + success: false, + startResponse: await StartTso.start(session, accountNumber, startParms), + startReady: false, + zosmfResponse: null, + commandResponse: null, + stopResponse: null, + }; + params.servletKey = response.startResponse.zosmfTsoResponse.servletKey; + params.queueID = response.startResponse.zosmfTsoResponse.queueID; + } + + const endpoint = `${TsoConstants.RESOURCE}/app/${params.servletKey}/${params.appKey}`; + const response = await ZosmfRestClient.postExpectJSON( + session, + endpoint, + [Headers.APPLICATION_JSON], + { startcmd: `${params.startupCommand} '&1 &2 ${params.queueID}'` } + ); + + return AddressSpaceApps.formatResponse(response, params.servletKey, params.queueID); + } + + public static async send( + session: AbstractSession, + accountNumber: string, + params: ITsoAppCommunicationParms, + startParms: IStartTsoParms + ): Promise { + TsoValidator.validateSession(session); + TsoValidator.validateNotEmptyString(accountNumber, noAccountNumber.message); + + const endpoint = `${TsoConstants.RESOURCE}/app/${params.servletKey}/${params.appKey}`; + const apiResponse = await ZosmfRestClient.putExpectJSON( + session, + endpoint, + [Headers.CONTENT_TYPE, "text/plain"], + params.message + ); + + return AddressSpaceApps.formatResponse(apiResponse, apiResponse.servletKey ?? null, apiResponse.queueID ?? null); + } + + public static async receive( + session: AbstractSession, + accountNumber: string, + params: ITsoAppCommunicationParms + ): Promise { + TsoValidator.validateSession(session); + TsoValidator.validateNotEmptyString(accountNumber, noAccountNumber.message); + const TIMEOUT_SECONDS: number = params.timeout; + + const endpoint = `${TsoConstants.RESOURCE}/app/${params.servletKey}/${params.appKey}`; + let combinedResponse: IASAppResponse | null = null; + let endKeyword = false; + const startTime = Date.now(); + let timeoutReached = false; + + do { + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + if (Date.now() - startTime > TIMEOUT_SECONDS * 1000) { + timeoutReached = true; + break; + } + + try { + const apiResponse = await ZosmfRestClient.getExpectJSON( + session, + endpoint + ); + const formattedResponse = AddressSpaceApps.formatResponse(apiResponse, apiResponse.servletKey ?? null, apiResponse.queueID ?? null); + + if (combinedResponse === null) { + combinedResponse = formattedResponse; + } else { + combinedResponse.tsoData.push(...formattedResponse.tsoData); + } + + endKeyword = formattedResponse.tsoData.some((data: any) => + typeof data === "string" ? data.trim() === "READY" : data.DATA.trim() === "READY" + ); + } catch (error) { + if (combinedResponse) { + return combinedResponse; + } + throw error; + } + } while (!endKeyword && params.receiveUntilReady && !timeoutReached); + + return combinedResponse!; + } +} diff --git a/packages/zostso/src/IssueTso.ts b/packages/zostso/src/IssueTso.ts index 48f3d3ce6a..2fd0d5e26d 100644 --- a/packages/zostso/src/IssueTso.ts +++ b/packages/zostso/src/IssueTso.ts @@ -72,7 +72,7 @@ export class IssueTso { }; return response; } catch (e) { - if (e.mMessage.includes("status 404")) { + if (e.message?.includes("status 404")) { // Set useNewApi to false to handle fallback logic useNewApi = false; } else { diff --git a/packages/zostso/src/doc/IASAppResponse.ts b/packages/zostso/src/doc/IASAppResponse.ts new file mode 100644 index 0000000000..d9afd33f2f --- /dev/null +++ b/packages/zostso/src/doc/IASAppResponse.ts @@ -0,0 +1,51 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +import { ITsoMessage } from "./zosmf/ITsoMessage"; + +export interface IASAppResponse { + /** + * Version in response + * @type {boolean} + * @memberof IASAppResponse + */ + version: string + /** + * Data from response + * @type {ITsoMessage[]} + * @memberof IASAppResponse + */ + tsoData: ITsoMessage[] + /** + * Reused boolean + * @type {boolean} + * @memberof IASAppResponse + */ + reused: boolean + /** + * Timeout boolean response + * @type {boolean} + * @memberof IASAppResponse + */ + timeout: boolean + /** + * Servlet key from IZosmfTsoResponse + * @type {string} + * @memberof IASAppResponse + */ + servletKey?: string; + /** + * QueueID from created address space + * @type {string} + * @memberof IASAppResponse + */ + queueID?: string; +} diff --git a/packages/zostso/src/doc/IIssueResponse.ts b/packages/zostso/src/doc/IIssueResponse.ts index b54ab48062..6d40d95287 100644 --- a/packages/zostso/src/doc/IIssueResponse.ts +++ b/packages/zostso/src/doc/IIssueResponse.ts @@ -33,7 +33,7 @@ export interface IIssueResponse { startResponse?: IStartStopResponses; /** - * Indicates if started TSO containes "READY " message + * Indicates if started TSO contains "READY" message * @type {boolean} * @memberof IIssueResponse */ diff --git a/packages/zostso/src/doc/input/IStartTsoAppParms.ts b/packages/zostso/src/doc/input/IStartTsoAppParms.ts new file mode 100644 index 0000000000..6dc643306e --- /dev/null +++ b/packages/zostso/src/doc/input/IStartTsoAppParms.ts @@ -0,0 +1,43 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +/** + * Interface for starting an app on a TSO Address Space + * @export + * @interface IStartTsoAppParms + */ +export interface IStartTsoAppParms { + + /** + * Startup command to run at TSO address space + * @type {string} + * @memberof IStartTsoAppParms + */ + startupCommand: string; + /** + * App Key of application to be started at a TSO address space + * @type {string} + * @memberof IStartTsoAppParms + */ + appKey: string; + /** + * Queue ID of an active address space + * @type {string} + * @memberof IStartTsoAppParms + */ + queueID?: string; + /** + * Servlet key of an active address space + * @type {string} + * @memberof IStartTsoAppParms + */ + servletKey?: string; +} diff --git a/packages/zostso/src/doc/input/ITsoAppCommunicationParms.ts b/packages/zostso/src/doc/input/ITsoAppCommunicationParms.ts new file mode 100644 index 0000000000..bf187b724c --- /dev/null +++ b/packages/zostso/src/doc/input/ITsoAppCommunicationParms.ts @@ -0,0 +1,48 @@ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +* +*/ + +/** + * Interface for starting an app on a TSO Address Space + * @export + * @interface ITsoAppCommunicationParms + */ +export interface ITsoAppCommunicationParms { + /** + * Body contents being sent to TSO address space app + * @type {string} + * @memberof ITsoAppCommunicationParms + */ + message?: string; + /** + * App Key of application to be started at a TSO address space + * @type {string} + * @memberof ITsoAppCommunicationParms + */ + appKey: string; + /** + * Servlet key of an active address space + * @type {string} + * @memberof ITsoAppCommunicationParms + */ + servletKey: string; + /** + * Keep receiving until end keyword is found. + * @type {boolean} + * @memberof ITsoAppCommunicationParms + */ + receiveUntilReady?: boolean; + /** + * Timeout duration in seconds + * @type {boolean} + * @memberof ITsoAppCommunicationParms + */ + timeout?: number; +} diff --git a/packages/zostso/src/index.ts b/packages/zostso/src/index.ts index 576da8ddd6..743200c6e7 100644 --- a/packages/zostso/src/index.ts +++ b/packages/zostso/src/index.ts @@ -39,6 +39,7 @@ export * from "./IssueTso"; export * from "./PingTso"; export * from "./SendTso"; export * from "./StartTso"; +export * from "./AddressSpaceApps"; export * from "./StopTso"; export * from "./TsoConstants"; export * from "./TsoResponseService"; diff --git a/packages/zosuss/CHANGELOG.md b/packages/zosuss/CHANGELOG.md index 5ebdb64df4..8c79f414e6 100644 --- a/packages/zosuss/CHANGELOG.md +++ b/packages/zosuss/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to the Zowe z/OS USS SDK package will be documented in this file. +## `8.7.0` + +- BugFix: Removed unnecessary `$ ` characters in front of most output. [zowe-explorer#3079(comment)](https://github.com/zowe/zowe-explorer-vscode/pull/3079#pullrequestreview-2408842655) +- Enhancement: Added the ability to validate if an SSH profile can successfully establish a connection, ensuring quicker troubleshooting of connection issues. [zowe-explorer#3079(comment)](https://github.com/zowe/zowe-explorer-vscode/pull/3079#discussion_r1825783867) + ## `8.1.1` - BugFix: Updated peer dependencies to `^8.0.0`, dropping support for versions tagged `next`. [#2287](https://github.com/zowe/zowe-cli/pull/2287) diff --git a/packages/zosuss/__tests__/__system__/Shell.system.test.ts b/packages/zosuss/__tests__/__system__/Shell.system.test.ts index 1f8c07360d..936531fe0f 100644 --- a/packages/zosuss/__tests__/__system__/Shell.system.test.ts +++ b/packages/zosuss/__tests__/__system__/Shell.system.test.ts @@ -35,6 +35,19 @@ describe("zowe uss issue ssh api call test", () => { await TestEnvironment.cleanUp(TEST_ENVIRONMENT); }); + describe("Function isConnectionValid", () => { + it("should verify that the connection is valid", async () => { + const response = await Shell.isConnectionValid(SSH_SESSION); + expect(response).toBe(true); + }); + it("should verify that the connection is invalid", async () => { + const fakeSession: SshSession = TestEnvironment.createSshSession(TEST_ENVIRONMENT); + fakeSession.ISshSession.hostname = "fake-host"; + const response = await Shell.isConnectionValid(fakeSession); + expect(response).toBe(false); + }); + }); + it ("should execute uname command on the remote system by ssh and return operating system name", async () => { const command = "uname"; let stdoutData = ""; @@ -126,7 +139,7 @@ describe("zowe uss issue ssh api call test", () => { it("should handle errors correctly when connection is refused", async () => { const invalidSshSession = new SshSession({ - hostname: "localhost", + hostname: "127.0.0.1", port: 22, user: "root", password: "**ThisPasswordIsExpectedNotToBeTheRealPasswordForRoot**" @@ -149,7 +162,8 @@ describe("zowe uss issue ssh api call test", () => { expect(error.toString()).toContain(ZosUssMessages.connectionRefused.message); } else { expect(error.toString().includes(ZosUssMessages.allAuthMethodsFailed.message) || - error.toString().includes(ZosUssMessages.connectionRefused.message)).toBe(true); + error.toString().includes(ZosUssMessages.connectionRefused.message) || + error.toString().includes(ZosUssMessages.unexpected.message)).toBe(true); } }, TIME_OUT); diff --git a/packages/zosuss/__tests__/__unit__/Shell.unit.test.ts b/packages/zosuss/__tests__/__unit__/Shell.unit.test.ts index f8c3b397a7..7ddb5058aa 100644 --- a/packages/zosuss/__tests__/__unit__/Shell.unit.test.ts +++ b/packages/zosuss/__tests__/__unit__/Shell.unit.test.ts @@ -54,7 +54,7 @@ const mockShell = jest.fn().mockImplementation((callback) => { (Client as any).mockImplementation(() => { mockClient.connect = mockConnect; mockClient.shell = mockShell; - mockClient.end = jest.fn(); + mockClient.end = jest.fn().mockReturnValue(mockClient); return mockClient; }); @@ -103,6 +103,29 @@ describe("Shell", () => { checkMockFunctionsWithCommand(command); }); + it("Should execute ssh command with cwd option and no extra characters in the output", async () => { + const cwd = "/"; + const command = "commandtest"; + await Shell.executeSshCwd(fakeSshSession, command, cwd, stdoutHandler, true); + + checkMockFunctionsWithCommand(command); + }); + + describe("Connection validation", () => { + it("should determine that the connection is valid", async () => { + const response = await Shell.isConnectionValid(fakeSshSession); + expect(response).toBe(true); + }); + it("should determine that the connection is invalid", async () => { + mockConnect.mockImplementationOnce(() => { + mockClient.emit("error", new Error(Shell.connRefusedFlag)); + mockStream.emit("exit", 0); + }); + const response = await Shell.isConnectionValid(fakeSshSession); + expect(response).toBe(false); + }); + }); + describe("Error handling", () => { it("should fail when password is expired", async () => { mockShell.mockImplementationOnce((callback) => { diff --git a/packages/zosuss/package.json b/packages/zosuss/package.json index ec1dc441bd..138b419c9b 100644 --- a/packages/zosuss/package.json +++ b/packages/zosuss/package.json @@ -1,6 +1,6 @@ { "name": "@zowe/zos-uss-for-zowe-sdk", - "version": "8.2.0", + "version": "8.7.0", "description": "Zowe SDK to interact with USS on z/OS", "author": "Zowe", "license": "EPL-2.0", @@ -49,8 +49,8 @@ }, "devDependencies": { "@types/ssh2": "^1.11.19", - "@zowe/cli-test-utils": "8.2.0", - "@zowe/imperative": "8.2.0" + "@zowe/cli-test-utils": "8.7.0", + "@zowe/imperative": "8.7.0" }, "peerDependencies": { "@zowe/imperative": "^8.0.0" diff --git a/packages/zosuss/src/Shell.ts b/packages/zosuss/src/Shell.ts index dc7ff271f5..ed319fbbd0 100644 --- a/packages/zosuss/src/Shell.ts +++ b/packages/zosuss/src/Shell.ts @@ -23,20 +23,11 @@ export class Shell { public static executeSsh(session: SshSession, command: string, - stdoutHandler: (data: string) => void): Promise { - const authsAllowed = ["none"]; + stdoutHandler: (data: string) => void, + removeExtraCharactersFromOutput = false): Promise { let hasAuthFailed = false; const promise = new Promise((resolve, reject) => { const conn = new Client(); - - // These are needed for authenticationHandler - // The order is critical as this is the order of authentication that will be used. - if (session.ISshSession.privateKey != null && session.ISshSession.privateKey !== "undefined") { - authsAllowed.push("publickey"); - } - if (session.ISshSession.password != null && session.ISshSession.password !== "undefined") { - authsAllowed.push("password"); - } conn.on("ready", () => { conn.shell((err: any, stream: ClientChannel) => { if (err) { throw err; } @@ -77,6 +68,7 @@ export class Shell { return; } dataBuffer += data; + if (dataBuffer.includes("\r")) { // when data is not received with complete lines, // slice the last incomplete line and put it back to dataBuffer until it gets the complete line, @@ -101,6 +93,8 @@ export class Shell { else if (isUserCommand && dataToPrint.length != 0) { if (!dataToPrint.startsWith('\r\n$ '+cmd) && !dataToPrint.startsWith('\r<')){ //only prints command output + if (removeExtraCharactersFromOutput && dataToPrint.startsWith("\r\n$ ")) + dataToPrint = dataToPrint.replace(/\r\n\$\s/, "\r\n"); stdoutHandler(dataToPrint); dataToPrint = ""; } @@ -140,28 +134,53 @@ export class Shell { })); } }); - conn.connect({ - host: session.ISshSession.hostname, - port: session.ISshSession.port, - username: session.ISshSession.user, - password: session.ISshSession.password, - privateKey: session.ISshSession.privateKey != null && session.ISshSession.privateKey !== "undefined" ? - require("fs").readFileSync(session.ISshSession.privateKey) : "", - passphrase: session.ISshSession.keyPassphrase, - authHandler: this.authenticationHandler(authsAllowed), - readyTimeout: session.ISshSession.handshakeTimeout != null && session.ISshSession.handshakeTimeout !== undefined ? - session.ISshSession.handshakeTimeout : 0 - } as any); + Shell.connect(conn, session); }); return promise; } + private static connect(connection: Client, session: SshSession) { + const authsAllowed = ["none"]; + + // These are needed for authenticationHandler + // The order is critical as this is the order of authentication that will be used. + if (session.ISshSession.privateKey != null && session.ISshSession.privateKey !== "undefined") { + authsAllowed.push("publickey"); + } + if (session.ISshSession.password != null && session.ISshSession.password !== "undefined") { + authsAllowed.push("password"); + } + + connection.connect({ + host: session.ISshSession.hostname, + port: session.ISshSession.port, + username: session.ISshSession.user, + password: session.ISshSession.password, + privateKey: session.ISshSession.privateKey != null && session.ISshSession.privateKey !== "undefined" ? + require("fs").readFileSync(session.ISshSession.privateKey) : "", + passphrase: session.ISshSession.keyPassphrase, + authHandler: this.authenticationHandler(authsAllowed), + readyTimeout: session.ISshSession.handshakeTimeout != null && session.ISshSession.handshakeTimeout !== undefined ? + session.ISshSession.handshakeTimeout : 0 + } as any); + } + public static async executeSshCwd(session: SshSession, command: string, cwd: string, - stdoutHandler: (data: string) => void): Promise { + stdoutHandler: (data: string) => void, + removeExtraCharactersFromOutput = false + ): Promise { const cwdCommand = `cd ${cwd} && ${command}`; - return this.executeSsh(session, cwdCommand, stdoutHandler); + return this.executeSsh(session, cwdCommand, stdoutHandler, removeExtraCharactersFromOutput); + } + + public static async isConnectionValid(session: SshSession): Promise{ + return new Promise((resolve, _) => { + const conn = new Client(); + conn.on("ready", () => conn.end() && resolve(true)).on("error", () => resolve(false)); + Shell.connect(conn, session); + }); } private static authenticationHandler(authsAllowed: string[]) {