diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index cb2a470ea1..ba5fbc9ce4 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - `DatasetFSProvider.stat()` will now throw a `FileNotFound` error for extenders trying to fetch an MVS resource that does not exist. [#3252](https://github.com/zowe/zowe-explorer-vscode/issues/3252) - Fixed an issue where renaming or deleting a USS file or data set did not update the opened editor. [#3260](https://github.com/zowe/zowe-explorer-vscode/issues/3260) +- Fixed an issue during initialization where a broken team configuration file caused the "Show Config" action in the error dialog to stop working. [#3273](https://github.com/zowe/zowe-explorer-vscode/issues/3273) - Fixed issue where switching the authentication methods would cause `Cannot read properties of undefined` error. [#3142](https://github.com/zowe/zowe-explorer-vscode/issues/3142) ## `3.0.2` diff --git a/packages/zowe-explorer/__tests__/__integration__/bdd/features/dialogs/ShowConfigErrorDialog.feature b/packages/zowe-explorer/__tests__/__integration__/bdd/features/dialogs/ShowConfigErrorDialog.feature new file mode 100644 index 0000000000..3a68bf267f --- /dev/null +++ b/packages/zowe-explorer/__tests__/__integration__/bdd/features/dialogs/ShowConfigErrorDialog.feature @@ -0,0 +1,7 @@ +Feature: Show Config Error Dialog + +Scenario: Initializing Zowe Explorer with a broken profile + When a user opens Zowe Explorer + Then the Show Config dialog should appear + When the user clicks on the "Show Config" button + Then the config should appear in the editor \ No newline at end of file diff --git a/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/dialogs/ShowConfigErrorDialog.steps.ts b/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/dialogs/ShowConfigErrorDialog.steps.ts new file mode 100644 index 0000000000..6fc99ad513 --- /dev/null +++ b/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/dialogs/ShowConfigErrorDialog.steps.ts @@ -0,0 +1,54 @@ +/** + * 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 { Then, When } from "@cucumber/cucumber"; +import { getZoweExplorerContainer } from "../../../../__common__/shared.wdio"; +import { Notification, Workbench } from "wdio-vscode-service"; + +When("a user opens Zowe Explorer", async function () { + this.zoweExplorerPane = await getZoweExplorerContainer(); + await expect(this.zoweExplorerPane).toBeDefined(); +}); + +Then("the Show Config dialog should appear", async function () { + this.workbench = await browser.getWorkbench(); + let notification: Notification; + const notificationCenter = await (this.workbench as Workbench).openNotificationsCenter(); + await notificationCenter.wait(60000); + await browser.waitUntil(async () => { + const notifications: Notification[] = await notificationCenter.getNotifications("error" as any); + for (const n of notifications) { + if ((await n.getMessage()).startsWith("Error encountered when loading your Zowe config.")) { + notification = n; + return true; + } + } + + return false; + }); + await expect(notification).toBeDefined(); + this.configErrorDialog = notification; + await (this.configErrorDialog as Notification).wait(); +}); + +When('the user clicks on the "Show Config" button', async function () { + const button = await this.configErrorDialog.elem.$("a[role='button']"); + await expect(button).toBeClickable(); + await button.click(); +}); + +Then("the config should appear in the editor", async function () { + const editorView = (this.workbench as Workbench).getEditorView(); + await editorView.wait(); + await browser.waitUntil(async () => (await editorView.getOpenEditorTitles()).length > 0); + const editorTitles = await editorView.getOpenEditorTitles(); + await expect(editorTitles.some((editorTitle) => editorTitle.includes("zowe.config.json"))).toBe(true); +}); diff --git a/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/profiles/UpdateCredentials.steps.ts b/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/profiles/UpdateCredentials.steps.ts index cb0cd76fba..c8b80ee65e 100644 --- a/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/profiles/UpdateCredentials.steps.ts +++ b/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/profiles/UpdateCredentials.steps.ts @@ -13,7 +13,6 @@ import * as fs from "fs"; import * as path from "path"; import { AfterAll, Then, When } from "@cucumber/cucumber"; import { paneDivForTree } from "../../../../__common__/shared.wdio"; -import { TreeItem } from "wdio-vscode-service"; import quickPick from "../../../../__pageobjects__/QuickPick"; import { Key } from "webdriverio"; @@ -54,9 +53,17 @@ When(/a user who has profile with (.*) auth in team config/, function (authType: }); When("the user has a profile in their Data Sets tree", async function () { this.treePane = await paneDivForTree("Data Sets"); - this.profileNode = (await this.treePane.getVisibleItems()).pop() as TreeItem; - await expect(this.profileNode).toBeDefined(); - await expect(await this.profileNode.getLabel()).toContain(this.authType); + await browser.waitUntil(async () => { + const visibleItems = await this.treePane.getVisibleItems(); + for (const item of visibleItems) { + if ((await item.getLabel()) === `zosmf_${this.authType as string}`) { + this.profileNode = item; + return true; + } + } + + return false; + }); }); When("a user clicks search button for the profile", async function () { await this.profileNode.elem.moveTo(); diff --git a/packages/zowe-explorer/__tests__/__integration__/bdd/wdio.conf.ts b/packages/zowe-explorer/__tests__/__integration__/bdd/wdio.conf.ts index f3aa0a4ca4..a0ff1bcaa8 100644 --- a/packages/zowe-explorer/__tests__/__integration__/bdd/wdio.conf.ts +++ b/packages/zowe-explorer/__tests__/__integration__/bdd/wdio.conf.ts @@ -13,6 +13,7 @@ import type { Options } from "@wdio/types"; import { join as joinPath, resolve as resolvePath } from "path"; import { emptyDirSync } from "fs-extra"; import { baseConfig } from "../../__common__/base.wdio.conf"; +import { renameSync, rmSync, writeFileSync } from "fs"; const dataDir = joinPath(__dirname, "..", "..", "__common__", ".wdio-vscode-service", "data"); const screenshotDir = joinPath(__dirname, "results", "screenshots"); @@ -152,6 +153,24 @@ export const config: Options.Testrunner = { emptyDirSync(screenshotDir); }, + beforeFeature: async function (uri, feature) { + if (feature.name === "Show Config Error Dialog") { + const configPath = joinPath(process.env["ZOWE_CLI_HOME"], "zowe.config.json"); + const backupConfigPath = joinPath(process.env["ZOWE_CLI_HOME"], "zowe.config.bkp"); + renameSync(configPath, backupConfigPath); + writeFileSync(configPath, "invalidjson"); + } + }, + + afterFeature: async function (uri, feature) { + if (feature.name === "Show Config Error Dialog") { + const backupConfigPath = joinPath(process.env["ZOWE_CLI_HOME"], "zowe.config.bkp"); + const configPath = joinPath(process.env["ZOWE_CLI_HOME"], "zowe.config.json"); + rmSync(configPath); + renameSync(backupConfigPath, configPath); + } + }, + afterStep: async function (step, scenario, result, context) { if (!result.passed) { await browser.saveScreenshot(joinPath(screenshotDir, `${scenario.name} - ${step.text}.png`)); diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts index 2f20c7d619..31916b427e 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts @@ -1108,6 +1108,20 @@ describe("ProfilesUtils unit tests", () => { }; } + it("should return early if profileInfo is nullish", async () => { + const blockMocks = getBlockMocks(); + blockMocks.getValueMock.mockReturnValueOnce(Definitions.V1MigrationStatus.JustMigrated); + blockMocks.setValueMock.mockImplementation(); + const getProfInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue(undefined as any); + const onlyV1ProfilesExistMock = new MockedProperty(imperative.ProfileInfo, "onlyV1ProfilesExist", { get: () => true }); + await ProfilesUtils.handleV1MigrationStatus(); + expect(getProfInfoMock).toHaveBeenCalled(); + expect(onlyV1ProfilesExistMock.mock).not.toHaveBeenCalled(); + blockMocks.getValueMock.mockRestore(); + blockMocks.setValueMock.mockRestore(); + onlyV1ProfilesExistMock[Symbol.dispose](); + }); + it("should call executeCommand with zowe.ds.addSession if the migration status is CreateConfigSelected", async () => { const blockMocks = getBlockMocks(); const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); @@ -1256,6 +1270,13 @@ describe("ProfilesUtils unit tests", () => { }); describe("promptUserWithNoConfigs", () => { + it("returns early if profileInfo is nullish", async () => { + const profInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue(undefined as any); + const showMessageSpy = jest.spyOn(Gui, "showMessage"); + await ProfilesUtils.promptUserWithNoConfigs(); + expect(showMessageSpy).not.toHaveBeenCalled(); + profInfoMock.mockRestore(); + }); it("prompts the user if they don't have any Zowe client configs", async () => { const profInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue({ getTeamConfig: () => ({ exists: false }), diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index 56c67aedc0..d657f7f55e 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -44,11 +44,11 @@ export class Profiles extends ProfilesCache { Constants.PROFILES_CACHE = Profiles.loader; try { await Profiles.loader.refresh(ZoweExplorerApiRegister.getInstance()); + await Profiles.getInstance().getProfileInfo(); } catch (err) { ZoweLogger.error(err); ZoweExplorerExtender.showZoweConfigError(err.message); } - await Profiles.getInstance().getProfileInfo(); return Profiles.loader; } diff --git a/packages/zowe-explorer/src/utils/ProfilesUtils.ts b/packages/zowe-explorer/src/utils/ProfilesUtils.ts index a7369eaef5..95fbc0c07c 100644 --- a/packages/zowe-explorer/src/utils/ProfilesUtils.ts +++ b/packages/zowe-explorer/src/utils/ProfilesUtils.ts @@ -181,7 +181,8 @@ export class ProfilesUtils { /** * Use the default credential manager in Zowe Explorer and setup before use - * @returns Promise the object of profileInfo using the default credential manager + * @returns {imperative.ProfileInfo} a ProfileInfo instance using the default credential manager, + * or undefined if an error occurred unrelated to credential manager initialization */ public static async setupDefaultCredentialManager(): Promise { try { @@ -204,6 +205,9 @@ export class ProfilesUtils { if (err instanceof imperative.ProfInfoErr && err.errorCode === imperative.ProfInfoErr.LOAD_CRED_MGR_FAILED) { await ProfilesUtils.promptAndDisableCredentialManagement(); } + if (err instanceof Error) { + ZoweLogger.error(err.message); + } // Ignore other types of errors since they will be handled later } } @@ -295,6 +299,10 @@ export class ProfilesUtils { ); } + /** + * Creates an instance of ProfileInfo and calls `readProfilesFromDisk` to load profiles. + * @returns An instance of `ProfileInfo`, or `undefined` if there was an error. + */ public static async getProfileInfo(): Promise { ZoweLogger.trace("ProfilesUtils.getProfileInfo called."); const hasSecureCredentialManagerEnabled: boolean = SettingsConfig.getDirectValue(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED); @@ -367,13 +375,17 @@ export class ProfilesUtils { // VS Code registers our updated TreeView IDs. Otherwise, VS Code's "Refresh Extensions" option will break v3 init. const ussPersistentSettings = vscode.workspace.getConfiguration("Zowe-USS-Persistent"); const upgradingFromV1 = ZoweLocalStorage.getValue(Definitions.LocalStorageKey.V1_MIGRATION_STATUS); - const mProfileInfo = await ProfilesUtils.getProfileInfo(); + const profileInfo = await ProfilesUtils.getProfileInfo(); + if (profileInfo == null) { + return; + } + if (ussPersistentSettings != null && upgradingFromV1 == null && imperative.ProfileInfo.onlyV1ProfilesExist) { await ZoweLocalStorage.setValue(Definitions.LocalStorageKey.V1_MIGRATION_STATUS, Definitions.V1MigrationStatus.JustMigrated); await vscode.commands.executeCommand("workbench.action.reloadWindow"); } - if (upgradingFromV1 == null || mProfileInfo.getTeamConfig().exists || !imperative.ProfileInfo.onlyV1ProfilesExist) { + if (upgradingFromV1 == null || profileInfo.getTeamConfig().exists || !imperative.ProfileInfo.onlyV1ProfilesExist) { return; } const userSelection = await this.v1ProfileOptions(); @@ -391,6 +403,10 @@ export class ProfilesUtils { */ public static async promptUserWithNoConfigs(): Promise { const profInfo = await ProfilesUtils.getProfileInfo(); + if (profInfo == null) { + return; + } + if (!profInfo.getTeamConfig().exists && !imperative.ProfileInfo.onlyV1ProfilesExist) { Gui.showMessage( vscode.l10n.t("No Zowe client configurations were detected. Click 'Create New' to create a new Zowe team configuration."),