Skip to content

Commit

Permalink
Merge branch 'master' into dir-to-uss-encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
jace-roell authored Nov 8, 2024
2 parents 54581d9 + f532f09 commit ddf4203
Show file tree
Hide file tree
Showing 18 changed files with 222 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,9 @@ describe("Upload file-to-data-set handler", () => {
statusMessage: "Uploading to data set"
}
});
expect(jsonObj).toMatchSnapshot();
expect(apiMessage).toMatchSnapshot();

expect(apiMessage).toBe("");

expect(logMessage).toMatch(/success:.*false/);
expect(logMessage).toMatch(/from:.*test-file/);
expect(logMessage).toMatch(/file_to_upload:.*1/);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Upload file-to-data-set handler process method should display error when upload file to data set 1`] = `
Object {
"apiResponse": Array [
Object {
"from": "test-file",
"success": false,
"to": "testing",
},
],
"commandResponse": "upload failed",
"success": false,
}
`;

exports[`Upload file-to-data-set handler process method should display error when upload file to data set 2`] = `""`;

exports[`Upload file-to-data-set handler process method should upload a file to a data set if requested 1`] = `
Object {
"apiResponse": Array [
Expand Down
4 changes: 4 additions & 0 deletions packages/imperative/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to the Imperative package will be documented in this file.

## Recent Changes

- BugFix: Enabled commands with either the `--help` or `--version` flags to still display their information despite any configuration file issues. [#2301](https://github.com/zowe/zowe-cli/pull/2301)

## `8.7.1`

- BugFix: Deprecated `IO` functions `createDirsSync` and `mkdirp` due to code duplication. Please use `createDirSync` instead. [#2352](https://github.com/zowe/zowe-cli/pull/2352)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
*/

import { ITestEnvironment } from "../../../__src__/environment/doc/response/ITestEnvironment";
import { SetupTestEnvironment } from "../../../__src__/environment/SetupTestEnvironment";
import * as TestUtils from "../../../src/TestUtil";
import * as fs from "fs";

// Test Environment populated in the beforeAll();
let TEST_ENVIRONMENT: ITestEnvironment;

describe("Hello World", () => {
// Create the unique test environment
beforeAll(async () => {
TEST_ENVIRONMENT = await SetupTestEnvironment.createTestEnv({
cliHomeEnvVar: "CMD_CLI_CLI_HOME",
testName: "cmd_cli_test_badConfig",
});
fs.copyFileSync(__dirname+"/hello-world-cli.config.json", TEST_ENVIRONMENT.workingDir+"/hello-world-cli.config.json");
});

it ("should print version even if bad config", async () => {
const response = await TestUtils.runCliScript(__dirname + "/scripts/version.sh", TEST_ENVIRONMENT.workingDir);
expect(response.status).toBe(0);
expect(response.stdout.toString()).toContain("Version");
expect(response.stderr.toString()).toContain("Please check this configuration file for errors.");
});

it ("should print help even if bad config", async () => {
const response = await TestUtils.runCliScript(__dirname + "/scripts/help.sh", TEST_ENVIRONMENT.workingDir);
expect(response.status).toBe(0);
expect(response.stdout.toString()).toContain("help");
expect(response.stderr.toString()).toContain("Please check this configuration file for errors.");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"profiles": {
"fruit": {
"properties": {
"origin": "BadConfig"
},
"profiles": {
"apple": {
"type": "fruit",
"properties": {
"color": "green"
}
},
"orange": {
"type": "fruit",
"properties": {
"color": "orange"
}
}
},
"secure": [
"secret"
]
}
},
"defaults": {
"fruit": "fruit.orange"
}
"plugins": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"overrides": {
"CredentialManager": "cmd-sample-cli"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,37 @@ const FAKE_HELP_GENERATOR: IHelpGenerator = {
const ENV_VAR_PREFIX: string = "UNIT_TEST";

describe("Command Processor", () => {
describe("Command Processor with --help and --version flags", () => {
let faultyConfigProcessor: CommandProcessor;

beforeEach(() => {
faultyConfigProcessor = new CommandProcessor({
envVariablePrefix: ENV_VAR_PREFIX,
definition: SAMPLE_COMMAND_DEFINITION,
helpGenerator: FAKE_HELP_GENERATOR,
rootCommandName: SAMPLE_ROOT_COMMAND,
commandLine: "",
promptPhrase: "dummyPrompt",
config: {
validate: () => ({valid: false}), // Simulate faulty config
} as any,
});

jest.spyOn(console, "log").mockImplementation(() => {}); // Prevent console logs in tests
});

afterEach(() => {
jest.restoreAllMocks();
});

it("should fail command execution without --help or --version if config is faulty", async () => {
const parms: any = { arguments: { _: ["some", "command"], $0: "" }, silent: true };
const response: ICommandResponse = await faultyConfigProcessor.invoke(parms);

expect(response).toBeDefined();
expect(response.success).toBe(false);
});
});
beforeEach(() => {
// Mock read stdin
jest.spyOn(SharedOptions, "readStdinIfRequested").mockResolvedValueOnce(false);
Expand All @@ -203,7 +234,6 @@ describe("Command Processor", () => {
process.stderr.write = ORIGINAL_STDERR_WRITE;
jest.restoreAllMocks();
});

it("should allow us to create an instance", () => {
let caughtError;

Expand Down
67 changes: 67 additions & 0 deletions packages/imperative/src/config/__tests__/Config.api.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ConfigConstants } from "../src/ConfigConstants";
import { IConfig } from "../src/doc/IConfig";
import { IConfigLayer } from "../src/doc/IConfigLayer";
import { IConfigProfile } from "../src/doc/IConfigProfile";
import { ImperativeError, Logger } from "../..";

const MY_APP = "my_app";

Expand Down Expand Up @@ -264,6 +265,72 @@ describe("Config API tests", () => {
expect(readFileSpy).toHaveBeenCalledTimes(1);
expect(secureLoadSpy).toHaveBeenCalledTimes(1);
});
it("should error when ignoreErrors is implicit false", async () => {
const config = await Config.load(MY_APP);
const existsSpy = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true);
const readFileSpy = jest.spyOn(fs, "readFileSync");
const secureLoadSpy = jest.spyOn(config.api.secure, "loadCached");
const jsoncParseSpy = jest.spyOn(JSONC, "parse").mockImplementationOnce(() => { throw "failed to parse"; });
let caughtError: ImperativeError = {} as any;
try {
config.api.layers.read();
} catch(err) {
caughtError = err;
}
expect(existsSpy).toHaveBeenCalledTimes(5); // Once for each config layer and one more time for read
expect(readFileSpy).toHaveBeenCalledTimes(1);
expect(secureLoadSpy).toHaveBeenCalledTimes(0);
expect(jsoncParseSpy).toHaveBeenCalledTimes(1);
expect(caughtError.message).toContain("Please check this configuration file for errors");
});

it("should error when ignoreErrors is explicit false", async () => {
const config = await Config.load(MY_APP);
const existsSpy = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true);
const readFileSpy = jest.spyOn(fs, "readFileSync");
const secureLoadSpy = jest.spyOn(config.api.secure, "loadCached");
const jsoncParseSpy = jest.spyOn(JSONC, "parse").mockImplementationOnce(() => { throw "failed to parse"; });
let caughtError: ImperativeError = {} as any;
try {
config.api.layers.read({user: false, global: false, ignoreErrors: false});
} catch(err) {
caughtError = err;
}
expect(existsSpy).toHaveBeenCalledTimes(5); // Once for each config layer and one more time for read
expect(readFileSpy).toHaveBeenCalledTimes(1);
expect(secureLoadSpy).toHaveBeenCalledTimes(0);
expect(jsoncParseSpy).toHaveBeenCalledTimes(1);
expect(caughtError.message).toContain("Please check this configuration file for errors");
});

it("shouldnt error when ignoreErrors is true", async () => {
const config = await Config.load(MY_APP);
const existsSpy = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true);
const readFileSpy = jest.spyOn(fs, "readFileSync");
const secureLoadSpy = jest.spyOn(config.api.secure, "loadCached");
const jsoncParseSpy = jest.spyOn(JSONC, "parse").mockImplementationOnce(() => { throw "failed to parse"; });

let logMsg: string = "Nothing logged";
jest.spyOn(Logger, "getConsoleLogger").mockImplementation(() => {
return {
error: jest.fn((errMsg) => {
logMsg = errMsg;
})
} as any;
});
let caughtError;
try {
config.api.layers.read({user: false, global: false, ignoreErrors: true});
} catch(err) {
caughtError = err;
}
expect(existsSpy).toHaveBeenCalledTimes(5); // Once for each config layer and one more time for read
expect(readFileSpy).toHaveBeenCalledTimes(1);
expect(secureLoadSpy).toHaveBeenCalledTimes(1);
expect(jsoncParseSpy).toHaveBeenCalledTimes(1);
expect(caughtError).toBeUndefined();
expect(logMsg).toContain("Please check this configuration file for errors");
});
});
describe("write", () => {
it("should save the active config layer", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1843,6 +1843,7 @@ describe("TeamConfig ProfileInfo tests", () => {
properties: { profiles: {}, defaults: {} },
global: true,
user: false,
ignoreErrors: false
});
expect(cfgSchemaBuildMock).toHaveBeenCalledWith([{
type: "some-type-with-source",
Expand Down
5 changes: 3 additions & 2 deletions packages/imperative/src/config/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export class Config {
opts = opts || {};

// Create the basic empty configuration
const myNewConfig = new Config(opts);
const myNewConfig = new Config();
myNewConfig.mApp = app;
myNewConfig.mActive = { user: false, global: false };
myNewConfig.mVault = opts.vault;
Expand Down Expand Up @@ -179,7 +179,8 @@ export class Config {
exists: false,
properties: Config.empty(),
global: layer === Layers.GlobalUser || layer === Layers.GlobalConfig,
user: layer === Layers.ProjectUser || layer === Layers.GlobalUser
user: layer === Layers.ProjectUser || layer === Layers.GlobalUser,
ignoreErrors: opts?.ignoreErrors
});
}

Expand Down
3 changes: 2 additions & 1 deletion packages/imperative/src/config/src/__mocks__/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export class Config {
defaults: {}
},
global: true,
user: false
user: false,
ignoreErrors: false
}];

return config;
Expand Down
18 changes: 12 additions & 6 deletions packages/imperative/src/config/src/api/ConfigLayers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ConfigConstants } from "../ConfigConstants";
import { IConfigLayer } from "../doc/IConfigLayer";
import { ConfigApi } from "./ConfigApi";
import { IConfig } from "../doc/IConfig";
import { Logger } from "../../../logger";

/**
* API Class for manipulating config layers.
Expand All @@ -31,7 +32,7 @@ export class ConfigLayers extends ConfigApi {
* @param opts The user and global flags that indicate which of the four
* config files (aka layers) is to be read.
*/
public read(opts?: { user: boolean; global: boolean }) {
public read(opts?: { user: boolean; global: boolean; ignoreErrors?: boolean}) {
// Attempt to populate the layer
const layer = opts ? this.mConfig.findLayer(opts.user, opts.global) : this.mConfig.layerActive();
if (fs.existsSync(layer.path)) {
Expand All @@ -49,11 +50,16 @@ export class ConfigLayers extends ConfigApi {
layer.properties = JSONC.parse(fileContents.toString()) as any;
layer.exists = true;
} catch (e) {
throw new ImperativeError({
msg: `Error parsing JSON in the file '${layer.path}'.\n` +
`Please check this configuration file for errors.\nError details: ${e.message}\nLine ${e.line}, Column ${e.column}`,
suppressDump: true
});
const msg = `Error parsing JSON in the file '${layer.path}'.\n` +
`Please check this configuration file for errors.\nError details: ${e.message}\nLine ${e.line}, Column ${e.column}`;
if (!opts?.ignoreErrors){
throw new ImperativeError({
msg:msg,
suppressDump: true
});
} else {
Logger.getConsoleLogger().error(msg);
}
}
this.mConfig.api.secure.loadCached(opts);
} else if (layer.exists) {
Expand Down
1 change: 1 addition & 0 deletions packages/imperative/src/config/src/doc/IConfigLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export interface IConfigLayer {
properties: IConfig;
global: boolean;
user: boolean;
ignoreErrors?: boolean;
}
5 changes: 5 additions & 0 deletions packages/imperative/src/config/src/doc/IConfigOpts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
import { IConfigVault } from "./IConfigVault";

export interface IConfigOpts {
/**
* Continue command execution despite config errors because version or help flag present
*/
ignoreErrors?: boolean;

/**
* Directory where global config files are located. Defaults to `~/.appName`.
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/imperative/src/constants/src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export class Constants {
public static readonly JSON_OPTION_ALIAS = "rfj";
public static readonly HELP_OPTION = "help";
public static readonly HELP_OPTION_ALIAS = "h";
public static readonly VERSION_OPTION = "version";
public static readonly VERSION_OPTION_ALIAS = "V";
public static readonly HELP_EXAMPLES = "help-examples";
public static readonly HELP_WEB_OPTION = "help-web";
public static readonly HELP_WEB_OPTION_ALIAS = "hw";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,9 @@ describe("Configuration Import command handler", () => {
it("should throw error when schema file is not valid JSON", async () => {
jest.spyOn(RestClient, "getExpectString").mockResolvedValueOnce("invalid JSON");
let error: any;

try {
await downloadSchema(new URL(schemaUrl), schemaDestPath);
await downloadSchema(new URL(schemaUrl), schemaDestPath); // Normal execution
} catch (err) {
error = err;
}
Expand Down
10 changes: 9 additions & 1 deletion packages/imperative/src/imperative/src/Imperative.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ export class Imperative {
ConfigurationValidator.validate(config);
ImperativeConfig.instance.loadedConfig = config;

// Detect CLI arguments to determine if errors should be ignored
const ignoreErrors = process.argv.includes(Constants.OPT_LONG_DASH + Constants.HELP_OPTION) ||
process.argv.includes(Constants.OPT_SHORT_DASH + Constants.HELP_OPTION_ALIAS) ||
process.argv.includes(Constants.OPT_LONG_DASH + Constants.VERSION_OPTION) ||
process.argv.includes(Constants.OPT_SHORT_DASH + Constants.VERSION_OPTION_ALIAS) ||
process.argv[process.argv.length - 1] === require.resolve('@zowe/cli');

/**
* Get the command name from the package bin.
* If no command name exists, we will instead use the file name invoked
Expand Down Expand Up @@ -178,7 +185,7 @@ export class Imperative {

try {
ImperativeConfig.instance.config = await Config.load(configAppName,
{ homeDir: ImperativeConfig.instance.cliHome }
{ homeDir: ImperativeConfig.instance.cliHome, ignoreErrors }
);
} catch (err) {
delayedConfigLoadError = err;
Expand Down Expand Up @@ -226,6 +233,7 @@ export class Imperative {
ImperativeConfig.instance.config = await Config.load(configAppName,
{
homeDir: ImperativeConfig.instance.cliHome,
ignoreErrors,
noLoad: true
}
);
Expand Down
Loading

0 comments on commit ddf4203

Please sign in to comment.