diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 93ff4635c..938948c93 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Fixed an issue to review inconsistent capitalization across translation strings. [#2935](https://github.com/zowe/zowe-explorer-vscode/issues/2935) - Fixed an issue where the `responseTimeout` profile property was ignored for z/OSMF MVS and USS API calls. [#3225](https://github.com/zowe/zowe-explorer-vscode/issues/3225) +- Fixed an issue where the assignment of the `profile` property in `ZoweTreeNode.setProfileToChoice` caused references to that object to break elsewhere. [#3289](https://github.com/zowe/zowe-explorer-vscode/issues/3289) ## `3.0.2` diff --git a/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts index c8a0ae561..fe723d190 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts @@ -13,9 +13,17 @@ import * as vscode from "vscode"; import { ZoweTreeNode } from "../../../src/tree/ZoweTreeNode"; import { IZoweTreeNode } from "../../../src/tree/IZoweTreeNode"; import * as imperative from "@zowe/imperative"; -import { BaseProvider } from "../../../src"; describe("ZoweTreeNode", () => { + const innerProfile = { user: "apple", password: "banana" }; + const fakeProfile: imperative.IProfileLoaded = { + name: "amazingProfile", + profile: innerProfile, + message: "", + type: "zosmf", + failNotFound: true, + }; + const makeNode = ( name: string, collapseState: vscode.TreeItemCollapsibleState, @@ -48,8 +56,8 @@ describe("ZoweTreeNode", () => { it("getProfile should return profile of current node", () => { const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined); - node.setProfileToChoice("myProfile" as unknown as imperative.IProfileLoaded); - expect(node.getProfile()).toBe("myProfile"); + node.setProfileToChoice(fakeProfile); + expect(node.getProfile().name).toBe("amazingProfile"); }); it("getProfile should return profile of parent node", () => { @@ -83,49 +91,43 @@ describe("ZoweTreeNode", () => { it("setProfileToChoice should update properties on existing profile object", () => { const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined, undefined, { - name: "oldProfile", - profile: { host: "example.com" }, + ...fakeProfile, }); - node.setProfileToChoice({ name: "newProfile", profile: { host: "example.com", port: 443 } } as unknown as imperative.IProfileLoaded); - // Profile name should not change but properties should - expect(node.getProfileName()).toBe("oldProfile"); + node.setProfileToChoice({ ...fakeProfile, profile: { host: "example.com", port: 443 } }); expect(node.getProfile().profile?.port).toBeDefined(); }); it("setProfileToChoice should update profile for associated FSProvider entry", () => { const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined); node.resourceUri = vscode.Uri.file(__dirname); + const prof = { ...fakeProfile, profile: { ...innerProfile } }; const fsEntry = { metadata: { - profile: { name: "oldProfile" }, + profile: prof, }, }; - node.setProfileToChoice( - { name: "newProfile" } as unknown as imperative.IProfileLoaded, - { - lookup: jest.fn().mockReturnValue(fsEntry), - } as unknown as BaseProvider - ); - expect(node.getProfileName()).toBe("newProfile"); - expect(fsEntry.metadata.profile.name).toBe("newProfile"); + prof.profile.user = "banana"; + prof.profile.password = "apple"; + node.setProfileToChoice(prof); + expect(node.getProfile().profile?.user).toBe("banana"); + expect(node.getProfile().profile?.password).toBe("apple"); + expect(fsEntry.metadata.profile.profile?.user).toBe("banana"); + expect(fsEntry.metadata.profile.profile?.password).toBe("apple"); }); it("setProfileToChoice should update child nodes with the new profile", () => { const node = makeNode("test", vscode.TreeItemCollapsibleState.Expanded, undefined); + node.setProfileToChoice({ ...fakeProfile, profile: { ...fakeProfile.profile, user: "banana" } }); const nodeChild = makeNode("child", vscode.TreeItemCollapsibleState.None, undefined); + nodeChild.setProfileToChoice(node.getProfile()); node.children = [nodeChild as any]; - const setProfileToChoiceChildMock = jest.spyOn(nodeChild, "setProfileToChoice").mockImplementation(); const fsEntry = { metadata: { - profile: { name: "oldProfile" }, + profile: node.getProfile(), }, }; - const mockNewProfile = { name: "newProfile" } as unknown as imperative.IProfileLoaded; - const mockProvider = { - lookup: jest.fn().mockReturnValue(fsEntry), - } as unknown as BaseProvider; - node.setProfileToChoice(mockNewProfile, mockProvider); - expect(node.getProfileName()).toBe("newProfile"); - expect(setProfileToChoiceChildMock).toHaveBeenCalledWith(mockNewProfile, mockProvider); + expect(node.getProfile().profile?.user).toBe("banana"); + expect(nodeChild.getProfile().profile?.user).toBe("banana"); + expect(fsEntry.metadata.profile.profile?.user).toBe("banana"); }); }); diff --git a/packages/zowe-explorer-api/src/fs/BaseProvider.ts b/packages/zowe-explorer-api/src/fs/BaseProvider.ts index f68682f2d..63d534fc3 100644 --- a/packages/zowe-explorer-api/src/fs/BaseProvider.ts +++ b/packages/zowe-explorer-api/src/fs/BaseProvider.ts @@ -428,7 +428,7 @@ export class BaseProvider { }) .then(async ({ userResponse }) => { if (userResponse === "Retry" && opts?.retry?.fn != null) { - await opts.retry.fn(...(opts?.retry.args ?? [])); + await opts.retry.fn(...(opts.retry.args ?? [])); } }) .catch(() => { diff --git a/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts b/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts index bdcc061f9..65e4e2bb1 100644 --- a/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts +++ b/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts @@ -101,21 +101,8 @@ export class ZoweTreeNode extends vscode.TreeItem { * @param {imperative.IProfileLoaded} The profile you will set the node to use */ public setProfileToChoice(aProfile: imperative.IProfileLoaded, fsProvider?: BaseProvider): void { - if (this.profile == null) { - this.profile = aProfile; - } else { - // Don't reassign profile, we want to keep object reference shared across nodes - this.profile.profile = aProfile.profile; - } - if (this.resourceUri != null) { - const fsEntry = fsProvider?.lookup(this.resourceUri, true); - if (fsEntry != null) { - fsEntry.metadata.profile = aProfile; - } - } - for (const child of this.children) { - (child as unknown as ZoweTreeNode).setProfileToChoice(aProfile, fsProvider); - } + // Don't reassign profile if its already defined, as we want to keep the reference valid for other nodes and filesystems + this.profile = Object.assign(this.profile ?? {}, aProfile); } /** * Sets the session for this node to the one chosen in parameters. diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 69bbefbb5..8b20ded20 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -24,6 +24,9 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Reduced the number of MVS API calls performed by `vscode.workspace.fs.readFile` when fetching the contents of a data set entry. [#3278](https://github.com/zowe/zowe-explorer-vscode/issues/3278) - Fixed an issue to review inconsistent capitalization across translation strings. [#2935](https://github.com/zowe/zowe-explorer-vscode/issues/2935) - Updated the test for the default credential manager for better compatibility with Cloud-based platforms such as Eclipse Che and Red Hat OpenShift Dev Spaces. [#3297](https://github.com/zowe/zowe-explorer-vscode/pull/3297) +- Fixed an issue where opening a PDS member after renaming an expanded PDS resulted in an error. [#3314](https://github.com/zowe/zowe-explorer-vscode/issues/3314) +- Fixed issue where users were not prompted to enter credentials if a 401 error was encountered when opening files, data sets or spools in the editor. [#3197](https://github.com/zowe/zowe-explorer-vscode/issues/3197) +- Fixed issue where profile credential updates or token changes were not reflected within the filesystem. [#3289](https://github.com/zowe/zowe-explorer-vscode/issues/3289) - Fixed an issue where editing a team config file or updating credentials in OS vault could trigger multiple events for a single action. [#3296](https://github.com/zowe/zowe-explorer-vscode/pull/3296) - Updated Zowe SDKs to `8.2.2` for technical currency. [#3296](https://github.com/zowe/zowe-explorer-vscode/pull/3296) 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 index 6fc99ad51..074421bfa 100644 --- 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 @@ -20,22 +20,22 @@ When("a user opens Zowe Explorer", async function () { 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); + + let configNotification: Notification; await browser.waitUntil(async () => { - const notifications: Notification[] = await notificationCenter.getNotifications("error" as any); + const notifications = await (this.workbench as Workbench).getNotifications(); for (const n of notifications) { - if ((await n.getMessage()).startsWith("Error encountered when loading your Zowe config.")) { - notification = n; + const msg = await n.getMessage(); + if (msg.startsWith("Error encountered when loading your Zowe config.")) { + configNotification = n; return true; } } - return false; }); - await expect(notification).toBeDefined(); - this.configErrorDialog = notification; + + await expect(configNotification).toBeDefined(); + this.configErrorDialog = configNotification; await (this.configErrorDialog as Notification).wait(); }); 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 c8b80ee65..3b7778489 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 @@ -71,6 +71,7 @@ When("a user clicks search button for the profile", async function () { // Locate and select the search button on the profile node const searchButton = actionButtons[actionButtons.length - 1]; + await searchButton.wait(); await expect(searchButton.elem).toBeDefined(); await searchButton.elem.click(); }); @@ -86,7 +87,6 @@ Then(/the user will be prompted for (.*) credentials/, async function (authType: await browser.keys(Key.Escape); }); Then("the profile node icon will be marked as inactive", async function () { - await browser.waitUntil((): Promise => this.profileNode.isExpanded()); const iconElement = await this.profileNode.elem.$(".custom-view-tree-node-item-icon"); const iconPath = (await iconElement.getCSSProperty("background-image")).value; await expect(iconPath).toContain("folder-root-disconnected-closed.svg"); diff --git a/packages/zowe-explorer/__tests__/__mocks__/mockCreators/shared.ts b/packages/zowe-explorer/__tests__/__mocks__/mockCreators/shared.ts index ea5e6e1ff..fe1a97a71 100644 --- a/packages/zowe-explorer/__tests__/__mocks__/mockCreators/shared.ts +++ b/packages/zowe-explorer/__tests__/__mocks__/mockCreators/shared.ts @@ -286,6 +286,7 @@ export function createTreeView(selection?): vscode.TreeView>; } @@ -367,6 +368,7 @@ export function createInstanceOfProfile(profile: imperative.IProfileLoaded) { convertV1ProfToConfig: jest.fn(), getLoadedProfConfig: jest.fn(), getSecurePropsForProfile: jest.fn(), + showProfileInactiveMsg: jest.fn(), } as any; } @@ -616,6 +618,7 @@ export function createTreeProviders() { removeSession: jest.fn(), refresh: jest.fn(), addSingleSession: jest.fn(), + setStatusForSession: jest.fn(), refreshElement: jest.fn(), } as any, uss: { @@ -627,6 +630,7 @@ export function createTreeProviders() { removeSession: jest.fn(), refresh: jest.fn(), addSingleSession: jest.fn(), + setStatusForSession: jest.fn(), refreshElement: jest.fn(), addEncodingHistory: jest.fn(), getEncodingHistory: jest.fn(), @@ -640,6 +644,7 @@ export function createTreeProviders() { deleteSession: jest.fn(), refresh: jest.fn(), addSingleSession: jest.fn(), + setStatusForSession: jest.fn(), } as any, }; } diff --git a/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts index 733d2a601..54916f2a3 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts @@ -80,13 +80,8 @@ describe("ZoweCommandProvider Unit Tests - function checkCurrentProfile", () => }); const profileStatus = { name: "test", status: "inactive" }; jest.spyOn(Profiles.getInstance(), "checkCurrentProfile").mockResolvedValue(profileStatus); - const errorHandlingSpy = jest.spyOn(AuthUtils, "errorHandling").mockImplementation(); + const showProfileInactiveMsg = jest.spyOn(Profiles.getInstance(), "showProfileInactiveMsg").mockImplementation(); await expect(ZoweCommandProvider.prototype.checkCurrentProfile(testNode)).resolves.toEqual(profileStatus); - expect(errorHandlingSpy).toHaveBeenCalledWith( - "Profile Name " + - globalMocks.testProfile.name + - " is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", - { apiType: ZoweExplorerApiType.Command, profile: globalMocks.testProfile } - ); + expect(showProfileInactiveMsg).toHaveBeenCalled(); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts index bd7184478..47879ffa1 100644 --- a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts @@ -1049,7 +1049,7 @@ describe("Profiles Unit Tests - function checkCurrentProfile", () => { jest.spyOn(AuthUtils, "isUsingTokenAuth").mockResolvedValueOnce(true); environmentSetup(globalMocks); setupProfilesCheck(globalMocks); - const ssoLoginSpy = jest.spyOn(Profiles.getInstance(), "ssoLogin").mockResolvedValueOnce(); + const ssoLoginSpy = jest.spyOn(Profiles.getInstance(), "ssoLogin").mockResolvedValueOnce(true); jest.spyOn(Profiles.getInstance(), "loadNamedProfile").mockReturnValueOnce(globalMocks.testProfile); await expect(Profiles.getInstance().checkCurrentProfile(globalMocks.testProfile)).resolves.toEqual({ name: "sestest", status: "active" }); expect(ssoLoginSpy).toHaveBeenCalledTimes(1); @@ -2448,3 +2448,20 @@ describe("Profiles Unit Tests - function tokenAuthClearSecureArray", () => { getProfileFromConfigMock.mockRestore(); }); }); + +describe("Profiles unit tests - function showProfilesInactiveMsg", () => { + it("should call ZoweLogger.error to log the error", () => { + const errorSpy = jest.spyOn(ZoweLogger, "error"); + Profiles.getInstance().showProfileInactiveMsg("profName"); + expect(errorSpy).toHaveBeenCalledWith( + "Profile profName is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct." + ); + }); + it("should call Gui.errorMessage to display the message", () => { + const errorMsgSpy = jest.spyOn(Gui, "errorMessage"); + Profiles.getInstance().showProfileInactiveMsg("profName"); + expect(errorMsgSpy).toHaveBeenCalledWith( + "Profile profName is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct." + ); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/ZoweTreeProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/ZoweTreeProvider.unit.test.ts index 472ed8987..49972a9e6 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/ZoweTreeProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/ZoweTreeProvider.unit.test.ts @@ -33,9 +33,10 @@ import { createUSSSessionNode } from "../../__mocks__/mockCreators/uss"; import { USSInit } from "../../../src/trees/uss/USSInit"; import { JobInit } from "../../../src/trees/job/JobInit"; import { createIJobObject, createJobSessionNode } from "../../__mocks__/mockCreators/jobs"; -import { createDatasetSessionNode } from "../../__mocks__/mockCreators/datasets"; +import { createDatasetFavoritesNode, createDatasetSessionNode } from "../../__mocks__/mockCreators/datasets"; import { DatasetInit } from "../../../src/trees/dataset/DatasetInit"; import { AuthUtils } from "../../../src/utils/AuthUtils"; +import { IconGenerator } from "../../../src/icons/IconGenerator"; async function createGlobalMocks() { Object.defineProperty(ZoweLocalStorage, "storage", { @@ -119,6 +120,7 @@ async function createGlobalMocks() { name: globalMocks.testProfile.name, status: "active", }), + showProfileInactiveMsg: jest.fn(), getProfileSetting: globalMocks.mockGetProfileSetting.mockReturnValue({ name: globalMocks.testProfile.name, status: "active", @@ -310,7 +312,7 @@ describe("ZoweJobNode unit tests - Function checkCurrentProfile", () => { testIJob: createIJobObject(), testJobsProvider: await JobInit.createJobsTree(imperative.Logger.getAppLogger()), jobNode: null, - checkJwtTokenForProfile: jest.spyOn(ZoweTreeProvider as any, "checkJwtTokenForProfile").mockImplementationOnce(() => {}), + checkJwtTokenForProfile: jest.spyOn(ZoweTreeProvider as any, "checkJwtTokenForProfile").mockResolvedValueOnce(true), }; newMocks.jobNode = new ZoweJobNode({ @@ -357,8 +359,13 @@ describe("ZoweJobNode unit tests - Function checkCurrentProfile", () => { it("Tests that checkCurrentProfile is executed successfully with inactive status", async () => { const globalMocks = await createGlobalMocks(); const blockMocks = await createBlockMocks(globalMocks); + jest.spyOn(SharedTreeProviders, "providers", "get").mockReturnValueOnce({ + ds: { setStatusForSession: jest.fn(), mSessionNodes: [createDatasetSessionNode(createISession(), createIProfile())] } as any, + uss: { setStatusForSession: jest.fn(), mSessionNodes: [createUSSSessionNode(createISession(), createIProfile())] } as any, + job: { setStatusForSession: jest.fn(), mSessionNodes: [createJobSessionNode(createISession(), createIProfile())] } as any, + }); blockMocks.jobNode.contextValue = "session"; - globalMocks.mockCheckCurrentProfile.mockReturnValueOnce({ + globalMocks.mockCheckCurrentProfile.mockResolvedValueOnce({ name: globalMocks.testProfile.name, status: "inactive", }); @@ -646,12 +653,14 @@ describe("Tree Provider Unit Tests - function checkJwtTokenForProfile", () => { ]); const hasTokenExpiredForProfile = jest.fn(); const mergeArgsForProfile = jest.fn(); + const showProfileInactiveMsg = jest.fn(); const profilesGetInstance = jest.spyOn(Profiles, "getInstance").mockReturnValue({ getProfileInfo: jest.fn().mockResolvedValue({ hasTokenExpiredForProfile, getAllProfiles, mergeArgsForProfile, } as any), + showProfileInactiveMsg, } as any); return { @@ -673,9 +682,82 @@ describe("Tree Provider Unit Tests - function checkJwtTokenForProfile", () => { it("prompts the user to log in if a JWT token is present and has expired", async () => { const blockMocks = getBlockMocks(); blockMocks.hasTokenExpiredForProfile.mockReturnValueOnce(true); - const promptUserForSsoLogin = jest.spyOn(AuthUtils, "promptUserForSsoLogin").mockImplementation(); + const promptForSsoLogin = jest.spyOn(AuthUtils, "promptForSsoLogin").mockImplementation(); await (ZoweTreeProvider as any).checkJwtTokenForProfile("zosmf"); expect(blockMocks.hasTokenExpiredForProfile).toHaveBeenCalledWith("zosmf"); - expect(promptUserForSsoLogin).toHaveBeenCalled(); + expect(promptForSsoLogin).toHaveBeenCalled(); + }); +}); + +describe("Tree Provider Unit Tests - function updateSessionContext", () => { + function getBlockMocks() { + const profile = createIProfile(); + const session = createISession(); + const sessionNodes = { + ds: createDatasetSessionNode(session, profile), + uss: createUSSSessionNode(session, profile), + job: createJobSessionNode(session, profile), + }; + const sharedProviders = { + ds: { setStatusForSession: jest.fn(), mSessionNodes: [sessionNodes.ds] } as any, + uss: { setStatusForSession: jest.fn(), mSessionNodes: [sessionNodes.uss] } as any, + job: { setStatusForSession: jest.fn(), mSessionNodes: [sessionNodes.job] } as any, + }; + return { + profile, + session, + sessionNodes, + sharedProviders, + sharedProviderMock: jest.spyOn(SharedTreeProviders, "providers", "get").mockReturnValueOnce(sharedProviders), + }; + } + + it("updates the session context across all shared tree providers", () => { + const blockMocks = getBlockMocks(); + (ZoweTreeProvider as any).updateSessionContext(blockMocks.profile.name, Validation.ValidationType.VALID); + expect(blockMocks.sharedProviders.ds.setStatusForSession).toHaveBeenCalledWith(blockMocks.sessionNodes.ds, Validation.ValidationType.VALID); + expect(blockMocks.sharedProviders.uss.setStatusForSession).toHaveBeenCalledWith(blockMocks.sessionNodes.uss, Validation.ValidationType.VALID); + expect(blockMocks.sharedProviders.job.setStatusForSession).toHaveBeenCalledWith(blockMocks.sessionNodes.job, Validation.ValidationType.VALID); + }); +}); + +describe("Tree Provider Unit Tests - function setStatusInSession", () => { + function getBlockMocks() { + const profile = createIProfile(); + const session = createISession(); + const nodeDataChanged = jest.spyOn(ZoweTreeProvider.prototype, "nodeDataChanged").mockImplementation(); + const treeProvider = new ZoweTreeProvider(PersistenceSchemaEnum.Dataset, createDatasetFavoritesNode()); + return { + nodeDataChanged, + profile, + session, + sessionNode: createDatasetSessionNode(session, profile), + treeProvider, + }; + } + + it("updates the session context - VALID", () => { + const { treeProvider, ...blockMocks } = getBlockMocks(); + (treeProvider as any).setStatusForSession(blockMocks.sessionNode, Validation.ValidationType.VALID); + expect(blockMocks.sessionNode.contextValue).toContain(Constants.ACTIVE_CONTEXT); + expect(blockMocks.nodeDataChanged).toHaveBeenCalled(); + }); + it("updates the session context - INVALID", () => { + const { treeProvider, ...blockMocks } = getBlockMocks(); + (treeProvider as any).setStatusForSession(blockMocks.sessionNode, Validation.ValidationType.INVALID); + expect(blockMocks.sessionNode.contextValue).toContain(Constants.INACTIVE_CONTEXT); + expect(blockMocks.nodeDataChanged).toHaveBeenCalled(); + }); + it("updates the session context - UNVERIFIED", () => { + const { treeProvider, ...blockMocks } = getBlockMocks(); + (treeProvider as any).setStatusForSession(blockMocks.sessionNode, Validation.ValidationType.UNVERIFIED); + expect(blockMocks.sessionNode.contextValue).toContain(Constants.UNVERIFIED_CONTEXT); + expect(blockMocks.nodeDataChanged).toHaveBeenCalled(); + }); + it("returns early when a falsy node is provided", () => { + const { treeProvider } = getBlockMocks(); + const getIconByIdMock = jest.spyOn(IconGenerator, "getIconById").mockClear().mockImplementation(); + (treeProvider as any).setStatusForSession(null, Validation.ValidationType.VALID); + expect(getIconByIdMock).not.toHaveBeenCalled(); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index 285a7ee47..ff18d56f1 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -1136,6 +1136,10 @@ describe("rename", () => { it("renames a PDS", async () => { const oldPds = new PdsEntry("USER.DATA.PDS"); + oldPds.metadata = new DsEntryMetadata({ profile: testProfile, path: "/USER.DATA.PDS" }); + const exampleMember = new DsEntry("TESTMEM", true); + exampleMember.metadata = new DsEntryMetadata({ profile: testProfile, path: "/USER.DATA.PDS/TESTMEM" }); + oldPds.entries.set("TESTMEM", exampleMember); oldPds.metadata = testEntries.pds.metadata; const mockMvsApi = { renameDataSet: jest.fn(), @@ -1148,6 +1152,7 @@ describe("rename", () => { .spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory") .mockReturnValueOnce({ ...testEntries.session }); await DatasetFSProvider.instance.rename(testUris.pds, testUris.pds.with({ path: "/USER.DATA.PDS2" }), { overwrite: true }); + expect(exampleMember.metadata.path).toBe("/USER.DATA.PDS2/TESTMEM"); expect(mockMvsApi.renameDataSet).toHaveBeenCalledWith("USER.DATA.PDS", "USER.DATA.PDS2"); _lookupMock.mockRestore(); mvsApiMock.mockRestore(); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts index a9610200e..adf82a102 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts @@ -894,9 +894,24 @@ describe("Dataset Tree Unit Tests - Function addSession", () => { testTree.mSessionNodes.push(blockMocks.datasetSessionNode); jest.spyOn(testTree, "addSingleSession").mockImplementation(); jest.spyOn(SharedTreeProviders, "providers", "get").mockReturnValue({ - ds: { addSingleSession: jest.fn(), mSessionNodes: [blockMocks.datasetSessionNode], refresh: jest.fn() } as any, - uss: { addSingleSession: jest.fn(), mSessionNodes: [blockMocks.datasetSessionNode], refresh: jest.fn() } as any, - jobs: { addSingleSession: jest.fn(), mSessionNodes: [blockMocks.datasetSessionNode], refresh: jest.fn() } as any, + ds: { + addSingleSession: jest.fn(), + mSessionNodes: [blockMocks.datasetSessionNode], + setStatusForSession: jest.fn(), + refresh: jest.fn(), + } as any, + uss: { + addSingleSession: jest.fn(), + mSessionNodes: [blockMocks.datasetSessionNode], + setStatusForSession: jest.fn(), + refresh: jest.fn(), + } as any, + jobs: { + addSingleSession: jest.fn(), + mSessionNodes: [blockMocks.datasetSessionNode], + setStatusForSession: jest.fn(), + refresh: jest.fn(), + } as any, } as any); await testTree.addSession(blockMocks.imperativeProfile.name); @@ -1496,7 +1511,7 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { qpPlaceholder: 'Choose "Create new..." to define a new profile or select an existing profile to add to the Data Set Explorer', mockEnableValidationContext: jest.fn(), testTree: new DatasetTree(), - checkJwtTokenForProfile: jest.spyOn(ZoweTreeProvider as any, "checkJwtTokenForProfile").mockImplementationOnce(() => {}), + checkJwtTokenForProfile: jest.spyOn(ZoweTreeProvider as any, "checkJwtTokenForProfile").mockResolvedValueOnce(true), }; newMocks.datasetSessionNode = createDatasetSessionNode(newMocks.session, newMocks.imperativeProfile); @@ -1535,9 +1550,24 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { jest.spyOn(testTree, "addSingleSession").mockImplementation(); jest.spyOn(SharedTreeProviders, "providers", "get").mockReturnValue({ - ds: { addSingleSession: jest.fn(), mSessionNodes: [blockMocks.datasetSessionNode], refresh: jest.fn() } as any, - uss: { addSingleSession: jest.fn(), mSessionNodes: [blockMocks.datasetSessionNode], refresh: jest.fn() } as any, - jobs: { addSingleSession: jest.fn(), mSessionNodes: [blockMocks.datasetSessionNode], refresh: jest.fn() } as any, + ds: { + addSingleSession: jest.fn(), + setStatusForSession: jest.fn(), + mSessionNodes: [blockMocks.datasetSessionNode], + refresh: jest.fn(), + } as any, + uss: { + addSingleSession: jest.fn(), + setStatusForSession: jest.fn(), + mSessionNodes: [blockMocks.datasetSessionNode], + refresh: jest.fn(), + } as any, + jobs: { + addSingleSession: jest.fn(), + setStatusForSession: jest.fn(), + mSessionNodes: [blockMocks.datasetSessionNode], + refresh: jest.fn(), + } as any, } as any); await testTree.datasetFilterPrompt(favoriteSearch); @@ -1565,7 +1595,6 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { await testTree.datasetFilterPrompt(testTree.mSessionNodes[1]); - expect(testTree.mSessionNodes[1].contextValue).toEqual(Constants.DS_SESSION_CONTEXT + Constants.ACTIVE_CONTEXT); expect(testTree.mSessionNodes[1].pattern).toEqual("HLQ.PROD1.STUFF"); }); it("Checking adding of new filter of multiple ds search", async () => { @@ -1593,7 +1622,6 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { await testTree.datasetFilterPrompt(testTree.mSessionNodes[1]); - expect(testTree.mSessionNodes[1].contextValue).toEqual(Constants.DS_SESSION_CONTEXT + Constants.ACTIVE_CONTEXT); expect(testTree.mSessionNodes[1].pattern).toEqual("HLQ.PROD, HLQ.PROD1*"); }); it("Checking adding of new filter with data set member", async () => { @@ -1606,7 +1634,6 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { const testTree = new DatasetTree(); testTree.mSessionNodes.push(blockMocks.datasetSessionNode); await testTree.datasetFilterPrompt(testTree.mSessionNodes[1]); - expect(testTree.mSessionNodes[1].contextValue).toEqual(Constants.DS_SESSION_CONTEXT + Constants.ACTIVE_CONTEXT); expect(testTree.mSessionNodes[1].pattern).toEqual("HLQ.PROD1"); }); it("Checking adding of new filter with Unverified profile", async () => { @@ -1634,7 +1661,6 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { await testTree.datasetFilterPrompt(testTree.mSessionNodes[1]); - expect(testTree.mSessionNodes[1].contextValue).toEqual(Constants.DS_SESSION_CONTEXT + Constants.UNVERIFIED_CONTEXT); expect(testTree.mSessionNodes[1].pattern).toEqual("HLQ.PROD1.STUFF"); }); it("Checking cancelled attempt to add a filter", async () => { @@ -2430,6 +2456,47 @@ describe("Dataset Tree Unit Tests - Function rename", () => { expect(refreshElementSpy).toHaveBeenCalledWith(child.getParent()); }); + it("Checking function with PDS", async () => { + createGlobalMocks(); + const blockMocks = createBlockMocks(); + mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); + mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); + const testTree = new DatasetTree(); + testTree.mSessionNodes.push(blockMocks.datasetSessionNode); + // Create nodes in Session section + const parent = new ZoweDatasetNode({ + label: "HLQ.TEST.OLDNAME.NODE", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_PDS_CONTEXT, + parentNode: testTree.mSessionNodes[1], + profile: blockMocks.imperativeProfile, + session: blockMocks.session, + }); + const child = new ZoweDatasetNode({ + label: "mem1", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MEMBER_CONTEXT, + parentNode: parent, + }); + // Simulate corresponding nodes in favorites + // Push test nodes to respective arrays + parent.children.push(child); + testTree.mSessionNodes[1].children.push(parent); + + const refreshElementSpy = jest.spyOn(testTree, "refreshElement"); + + const renameDataSetSpy = jest.spyOn((DatasetTree as any).prototype, "renameDataSet"); + + mocked(Gui.showInputBox).mockImplementation((options) => { + return Promise.resolve("HLQ.TEST.NEWNAME.NODE"); + }); + await testTree.rename(parent); + expect(renameDataSetSpy).toHaveBeenLastCalledWith(parent); + expect(parent.resourceUri?.path).toBe("/sestest/HLQ.TEST.NEWNAME.NODE"); + expect(child.resourceUri?.path).toBe("/sestest/HLQ.TEST.NEWNAME.NODE/mem1"); + expect(refreshElementSpy).toHaveBeenCalled(); + }); + it("Checking function with PDS Member given in lowercase", async () => { createGlobalMocks(); const blockMocks = createBlockMocks(); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts index 6e49d17d9..e04d77e58 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts @@ -16,6 +16,7 @@ import { createIJobFile, createIJobObject } from "../../../__mocks__/mockCreator import { ZoweExplorerApiRegister } from "../../../../src/extending/ZoweExplorerApiRegister"; import { JobFSProvider } from "../../../../src/trees/job/JobFSProvider"; import { MockedProperty } from "../../../__mocks__/mockUtils"; +import { AuthUtils } from "../../../../src/utils/AuthUtils"; const testProfile = createIProfile(); @@ -207,7 +208,7 @@ describe("createDirectory", () => { }); describe("fetchSpoolAtUri", () => { - it("fetches the spool contents for a given URI", async () => { + it("fetches the spool contents for a given URI - downloadSingleSpool", async () => { const lookupAsFileMock = jest .spyOn(JobFSProvider.instance as any, "_lookupAsFile") .mockReturnValueOnce({ ...testEntries.spool, data: new Uint8Array() }); @@ -224,6 +225,45 @@ describe("fetchSpoolAtUri", () => { jesApiMock.mockRestore(); lookupAsFileMock.mockRestore(); }); + + it("fetches the spool contents for a given URI - getSpoolContentById", async () => { + const lookupAsFileMock = jest + .spyOn(JobFSProvider.instance as any, "_lookupAsFile") + .mockReturnValueOnce({ ...testEntries.spool, data: new Uint8Array() }); + const lookupParentDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce({ ...testEntries.job }); + const mockJesApi = { + getSpoolContentById: jest.fn((opts) => { + return "spool contents"; + }), + }; + const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); + const entry = await JobFSProvider.instance.fetchSpoolAtUri(testUris.spool); + expect(lookupAsFileMock).toHaveBeenCalledWith(testUris.spool); + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.spool); + expect(mockJesApi.getSpoolContentById).toHaveBeenCalled(); + expect(entry.data.toString()).toStrictEqual("spool contents"); + jesApiMock.mockRestore(); + lookupAsFileMock.mockRestore(); + }); + + it("calls AuthUtils.promptForAuthError when an error occurs", async () => { + const lookupAsFileMock = jest + .spyOn(JobFSProvider.instance as any, "_lookupAsFile") + .mockReturnValueOnce({ ...testEntries.spool, data: new Uint8Array() }); + const mockJesApi = { + downloadSingleSpool: jest.fn((opts) => { + throw new Error("Failed to download spool"); + }), + }; + const promptForAuthErrorMock = jest.spyOn(AuthUtils, "promptForAuthError").mockImplementation(); + const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); + await expect(JobFSProvider.instance.fetchSpoolAtUri(testUris.spool)).rejects.toThrow(); + expect(promptForAuthErrorMock).toHaveBeenCalled(); + expect(lookupAsFileMock).toHaveBeenCalledWith(testUris.spool); + expect(mockJesApi.downloadSingleSpool).toHaveBeenCalled(); + jesApiMock.mockRestore(); + lookupAsFileMock.mockRestore(); + }); }); describe("readFile", () => { @@ -239,13 +279,10 @@ describe("readFile", () => { it("throws error if an error occurred while fetching spool", async () => { const spoolEntry = { ...testEntries.spool }; const lookupAsFileMock = jest.spyOn(JobFSProvider.instance as any, "_lookupAsFile").mockReturnValueOnce(spoolEntry); - const _handleErrorMock = jest.spyOn(JobFSProvider.instance as any, "_handleError").mockImplementation(); const fetchSpoolAtUriMock = jest .spyOn(JobFSProvider.instance, "fetchSpoolAtUri") .mockRejectedValueOnce(new Error("Failed to fetch contents for spool")); await expect(JobFSProvider.instance.readFile(testUris.spool)).rejects.toThrow(); - expect(_handleErrorMock).toHaveBeenCalled(); - _handleErrorMock.mockRestore(); lookupAsFileMock.mockRestore(); fetchSpoolAtUriMock.mockRestore(); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts index 54f98b38d..71b039b6f 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts @@ -226,9 +226,24 @@ describe("ZoweJobNode unit tests - Function addSession", () => { it("Tests that addSession adds the session to the tree", async () => { const globalMocks = await createGlobalMocks(); jest.spyOn(SharedTreeProviders, "providers", "get").mockReturnValue({ - ds: { addSingleSession: jest.fn(), mSessionNodes: [...globalMocks.testJobsProvider.mSessionNodes], refresh: jest.fn() } as any, - uss: { addSingleSession: jest.fn(), mSessionNodes: [...globalMocks.testJobsProvider.mSessionNodes], refresh: jest.fn() } as any, - jobs: { addSingleSession: jest.fn(), mSessionNodes: [...globalMocks.testJobsProvider.mSessionNodes], refresh: jest.fn() } as any, + ds: { + addSingleSession: jest.fn(), + mSessionNodes: [...globalMocks.testJobsProvider.mSessionNodes], + setStatusForSession: jest.fn(), + refresh: jest.fn(), + } as any, + uss: { + addSingleSession: jest.fn(), + mSessionNodes: [...globalMocks.testJobsProvider.mSessionNodes], + setStatusForSession: jest.fn(), + refresh: jest.fn(), + } as any, + jobs: { + addSingleSession: jest.fn(), + mSessionNodes: [...globalMocks.testJobsProvider.mSessionNodes], + setStatusForSession: jest.fn(), + refresh: jest.fn(), + } as any, } as any); await globalMocks.testJobsProvider.addSession("sestest"); expect(globalMocks.testJobsProvider.mSessionNodes[1]).toBeDefined(); @@ -821,7 +836,11 @@ describe("ZosJobsProvider - Function searchPrompt", () => { const globalMocks = await createGlobalMocks(); jest.spyOn(globalMocks.testJobsProvider, "applySavedFavoritesSearchLabel").mockReturnValue(undefined); const applySearchLabelToNode = jest.spyOn(globalMocks.testJobsProvider, "applySearchLabelToNode"); - const jobSessionNode = new ZoweJobNode({ label: "sestest", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed }); + const jobSessionNode = new ZoweJobNode({ + label: "sestest", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + profile: createIProfile(), + }); jobSessionNode.contextValue = Constants.JOBS_SESSION_CONTEXT + Constants.FAV_SUFFIX; await globalMocks.testJobsProvider.searchPrompt(jobSessionNode); expect(applySearchLabelToNode).toHaveBeenCalled(); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts index 12a9831ee..313907320 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts @@ -218,9 +218,24 @@ function createGlobalMocks() { }); jest.spyOn(SharedTreeProviders, "providers", "get").mockReturnValue({ - ds: { addSingleSession: jest.fn(), mSessionNodes: [...globalMocks.testTree.mSessionNodes], refresh: jest.fn() } as any, - uss: { addSingleSession: jest.fn(), mSessionNodes: [...globalMocks.testTree.mSessionNodes], refresh: jest.fn() } as any, - jobs: { addSingleSession: jest.fn(), mSessionNodes: [...globalMocks.testTree.mSessionNodes], refresh: jest.fn() } as any, + ds: { + addSingleSession: jest.fn(), + mSessionNodes: [...globalMocks.testTree.mSessionNodes], + setStatusForSession: jest.fn(), + refresh: jest.fn(), + } as any, + uss: { + addSingleSession: jest.fn(), + mSessionNodes: [...globalMocks.testTree.mSessionNodes], + setStatusForSession: jest.fn(), + refresh: jest.fn(), + } as any, + jobs: { + addSingleSession: jest.fn(), + mSessionNodes: [...globalMocks.testTree.mSessionNodes], + setStatusForSession: jest.fn(), + refresh: jest.fn(), + } as any, } as any); return globalMocks; @@ -551,7 +566,7 @@ describe("USSTree Unit Tests - Function filterPrompt", () => { qpValue: "", qpItem: new FilterDescriptor("\uFF0B " + "Create a new filter"), resolveQuickPickHelper: jest.spyOn(Gui, "resolveQuickPick"), - checkJwtTokenForProfile: jest.spyOn(ZoweTreeProvider as any, "checkJwtTokenForProfile").mockImplementationOnce(() => {}), + checkJwtTokenForProfile: jest.spyOn(ZoweTreeProvider as any, "checkJwtTokenForProfile").mockResolvedValueOnce(true), }; newMocks.resolveQuickPickHelper.mockImplementation(() => Promise.resolve(newMocks.qpItem)); globalMocks.createQuickPick.mockReturnValue({ @@ -608,6 +623,7 @@ describe("USSTree Unit Tests - Function filterPrompt", () => { name: globalMocks.testProfile.name, status: "unverified", }), + showProfileInactiveMsg: jest.fn(), validProfile: Validation.ValidationType.UNVERIFIED, }; }), @@ -649,8 +665,6 @@ describe("USSTree Unit Tests - Function filterPrompt", () => { blockMocks.qpItem = undefined; await globalMocks.testTree.filterPrompt(globalMocks.testTree.mSessionNodes[1]); - expect(globalMocks.showInformationMessage.mock.calls.length).toBe(1); - expect(globalMocks.showInformationMessage.mock.calls[0][0]).toBe("No selection made. Operation cancelled."); }); it("Tests that filter() works correctly for favorited search nodes with credentials", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index 7d99bf286..5cd2f0eaf 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -330,22 +330,20 @@ describe("fetchFileAtUri", () => { expect(fileEntry.data?.byteLength).toBe(exampleData.length); autoDetectEncodingMock.mockRestore(); }); - it("throws an error if it failed to fetch contents", async () => { + it("returns early if it failed to fetch contents", async () => { const fileEntry = { ...testEntries.file }; + const _fireSoonSpy = jest.spyOn((UssFSProvider as any).prototype, "_fireSoon"); const lookupAsFileMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsFile").mockReturnValueOnce(fileEntry); const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockImplementation(); jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ getContents: jest.fn().mockRejectedValue(new Error("error retrieving contents")), } as any); - const _handleErrorMock = jest.spyOn(UssFSProvider.instance as any, "_handleError").mockImplementation(); - await expect(UssFSProvider.instance.fetchFileAtUri(testUris.file)).rejects.toThrow(); - + await UssFSProvider.instance.fetchFileAtUri(testUris.file); expect(lookupAsFileMock).toHaveBeenCalledWith(testUris.file); expect(autoDetectEncodingMock).toHaveBeenCalledWith(fileEntry); - expect(_handleErrorMock).toHaveBeenCalled(); + expect(_fireSoonSpy).not.toHaveBeenCalled(); autoDetectEncodingMock.mockRestore(); - _handleErrorMock.mockRestore(); }); it("calls getContents to get the data for a file entry with encoding", async () => { const fileEntry = { ...testEntries.file }; diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts index 1fdd7d247..d5fc66cf7 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts @@ -426,9 +426,9 @@ describe("ZoweUSSNode Unit Tests - Function node.rename()", () => { parentPath: "/u/user", }), providerSpy: jest.spyOn(SharedTreeProviders, "providers", "get").mockReturnValue({ - ds: { addSingleSession: jest.fn(), mSessionNodes: [], refresh: jest.fn() } as any, - uss: { addSingleSession: jest.fn(), mSessionNodes: [], refresh: jest.fn() } as any, - job: { addSingleSession: jest.fn(), mSessionNodes: [], refresh: jest.fn() } as any, + ds: { addSingleSession: jest.fn(), mSessionNodes: [], setStatusForSession: jest.fn(), refresh: jest.fn() } as any, + uss: { addSingleSession: jest.fn(), mSessionNodes: [], setStatusForSession: jest.fn(), refresh: jest.fn() } as any, + job: { addSingleSession: jest.fn(), mSessionNodes: [], setStatusForSession: jest.fn(), refresh: jest.fn() } as any, }), renameSpy: jest.spyOn(vscode.workspace.fs, "rename").mockImplementation(), getEncodingForFile: jest.spyOn(UssFSProvider.instance as any, "getEncodingForFile").mockReturnValue(undefined), diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/AuthUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/AuthUtils.unit.test.ts new file mode 100644 index 000000000..d285bc03d --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/utils/AuthUtils.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 { Gui, imperative } from "@zowe/zowe-explorer-api"; +import { AuthUtils } from "../../../src/utils/AuthUtils"; +import { Constants } from "../../../src/configuration/Constants"; +import { MockedProperty } from "../../__mocks__/mockUtils"; + +describe("AuthUtils", () => { + describe("promptForAuthError", () => { + it("should prompt for authentication", async () => { + const errorDetails = new imperative.ImperativeError({ + errorCode: 401 as unknown as string, + msg: "All configured authentication methods failed", + }); + const profile = { type: "zosmf" } as any; + const promptForAuthenticationMock = jest + .spyOn(AuthUtils, "promptForAuthentication") + .mockImplementation(async () => Promise.resolve(true)); + AuthUtils.promptForAuthError(errorDetails, profile); + expect(promptForAuthenticationMock).toHaveBeenCalledWith(errorDetails, profile); + }); + }); + describe("promptForSsoLogin", () => { + it("should call ProfilesCache.ssoLogin when 'Log In' option is selected", async () => { + const ssoLogin = jest.fn(); + const profilesCacheMockedProp = new MockedProperty(Constants, "PROFILES_CACHE", { + value: { + ssoLogin, + }, + configurable: true, + }); + const showMessageMock = jest.spyOn(Gui, "showMessage").mockResolvedValueOnce("Log in to Authentication Service"); + await AuthUtils.promptForSsoLogin("aProfileName"); + expect(showMessageMock).toHaveBeenCalledWith( + "Your connection is no longer active for profile 'aProfileName'. " + + "Please log in to an authentication service to restore the connection.", + { items: ["Log in to Authentication Service"], vsCodeOpts: { modal: true } } + ); + expect(ssoLogin).toHaveBeenCalledWith(null, "aProfileName"); + profilesCacheMockedProp[Symbol.dispose](); + }); + it("should not call SSO login if prompt dismissed", async () => { + const ssoLogin = jest.fn(); + const profilesCacheMockedProp = new MockedProperty(Constants, "PROFILES_CACHE", { + value: { + ssoLogin, + }, + configurable: true, + }); + const showMessageMock = jest.spyOn(Gui, "showMessage").mockResolvedValueOnce(undefined); + await AuthUtils.promptForSsoLogin("aProfileName"); + expect(showMessageMock).toHaveBeenCalledWith( + "Your connection is no longer active for profile 'aProfileName'. " + + "Please log in to an authentication service to restore the connection.", + { items: ["Log in to Authentication Service"], vsCodeOpts: { modal: true } } + ); + expect(ssoLogin).not.toHaveBeenCalledWith(null, "aProfileName"); + profilesCacheMockedProp[Symbol.dispose](); + }); + }); +}); 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 9c30ee73a..c22891e44 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts @@ -179,7 +179,7 @@ describe("ProfilesUtils unit tests", () => { }); const scenario = "Task failed successfully"; const showErrorSpy = jest.spyOn(Gui, "errorMessage"); - const showMessageSpy = jest.spyOn(Gui, "showMessage").mockImplementation(() => Promise.resolve("selection")); + const showMessageSpy = jest.spyOn(Gui, "showMessage").mockImplementation(() => Promise.resolve("Log in to Authentication Service")); const ssoLoginSpy = jest.fn(); const profile = { type: "zosmf" } as any; Object.defineProperty(Constants, "PROFILES_CACHE", { diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index b904ce425..25a40d0ec 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -166,12 +166,6 @@ "Profile name" ] }, - "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct./Profile name": { - "message": "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", - "comment": [ - "Profile name" - ] - }, "Use the search button to list USS files": "Use the search button to list USS files", "Invalid node": "Invalid node", "Delete action was cancelled.": "Delete action was cancelled.", @@ -202,48 +196,6 @@ "Profile auth error": "Profile auth error", "Profile is not authenticated, please log in to continue": "Profile is not authenticated, please log in to continue", "Retrieving response from USS list API": "Retrieving response from USS list API", - "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", - "Failed to move {0}/File path": { - "message": "Failed to move {0}", - "comment": [ - "File path" - ] - }, - "Failed to get contents for {0}/File path": { - "message": "Failed to get contents for {0}", - "comment": [ - "File path" - ] - }, - "Profile does not exist for this file.": "Profile does not exist for this file.", - "Saving USS file...": "Saving USS file...", - "Failed to rename {0}/File path": { - "message": "Failed to rename {0}", - "comment": [ - "File path" - ] - }, - "Failed to delete {0}/File name": { - "message": "Failed to delete {0}", - "comment": [ - "File name" - ] - }, - "No error details given": "No error details given", - "Error fetching destination {0} for paste action: {1}/USS pathError message": { - "message": "Error fetching destination {0} for paste action: {1}", - "comment": [ - "USS path", - "Error message" - ] - }, - "Failed to copy {0} to {1}/Source pathDestination path": { - "message": "Failed to copy {0} to {1}", - "comment": [ - "Source path", - "Destination path" - ] - }, "Downloaded: {0}/Download time": { "message": "Downloaded: {0}", "comment": [ @@ -314,6 +266,42 @@ "initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove", "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", "Pulling from Mainframe...": "Pulling from Mainframe...", + "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", + "Failed to move {0}/File path": { + "message": "Failed to move {0}", + "comment": [ + "File path" + ] + }, + "Profile does not exist for this file.": "Profile does not exist for this file.", + "Saving USS file...": "Saving USS file...", + "Failed to rename {0}/File path": { + "message": "Failed to rename {0}", + "comment": [ + "File path" + ] + }, + "Failed to delete {0}/File name": { + "message": "Failed to delete {0}", + "comment": [ + "File name" + ] + }, + "No error details given": "No error details given", + "Error fetching destination {0} for paste action: {1}/USS pathError message": { + "message": "Error fetching destination {0} for paste action: {1}", + "comment": [ + "USS path", + "Error message" + ] + }, + "Failed to copy {0} to {1}/Source pathDestination path": { + "message": "Failed to copy {0} to {1}", + "comment": [ + "Source path", + "Destination path" + ] + }, "{0} location/Node type": { "message": "{0} location", "comment": [ @@ -505,12 +493,6 @@ "Phase Name": "Phase Name", "Error Details": "Error Details", "Fetching spool file...": "Fetching spool file...", - "Failed to get contents for {0}/Spool name": { - "message": "Failed to get contents for {0}", - "comment": [ - "Spool name" - ] - }, "Failed to delete job {0}/Job name": { "message": "Failed to delete job {0}", "comment": [ @@ -980,6 +962,12 @@ }, "Reload Window": "Reload Window", "The Team configuration file has been opened in the editor. Editing or removal of profiles will need to be done manually.": "The Team configuration file has been opened in the editor. Editing or removal of profiles will need to be done manually.", + "Profile {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct./Profile name": { + "message": "Profile {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", + "comment": [ + "Profile name" + ] + }, "Choose \"Create new...\" to define or select a profile to add to the DATA SETS tree": "Choose \"Create new...\" to define or select a profile to add to the DATA SETS tree", "Choose \"Create new...\" to define or select a profile to add to the JOBS tree": "Choose \"Create new...\" to define or select a profile to add to the JOBS tree", "Choose \"Create new...\" to define or select a profile to add to the USS tree": "Choose \"Create new...\" to define or select a profile to add to the USS tree", diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index b61ce66cb..a2b82cdf2 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -517,7 +517,6 @@ "Update Credentials": "", "Required parameter 'host' must not be blank.": "", "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.": "", - "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.": "", "Use the search button to list USS files": "", "Invalid node": "", "Delete action was cancelled.": "", @@ -533,16 +532,6 @@ "Profile auth error": "", "Profile is not authenticated, please log in to continue": "", "Retrieving response from USS list API": "", - "The 'move' function is not implemented for this USS API.": "", - "Failed to move {0}": "", - "Failed to get contents for {0}": "", - "Profile does not exist for this file.": "", - "Saving USS file...": "", - "Failed to rename {0}": "", - "Failed to delete {0}": "", - "No error details given": "", - "Error fetching destination {0} for paste action: {1}": "", - "Failed to copy {0} to {1}": "", "Downloaded: {0}": "", "Encoding: {0}": "", "Binary": "", @@ -571,6 +560,15 @@ "initializeUSSFavorites.error.buttonRemove": "", "File does not exist. It may have been deleted.": "", "Pulling from Mainframe...": "", + "The 'move' function is not implemented for this USS API.": "", + "Failed to move {0}": "", + "Profile does not exist for this file.": "", + "Saving USS file...": "", + "Failed to rename {0}": "", + "Failed to delete {0}": "", + "No error details given": "", + "Error fetching destination {0} for paste action: {1}": "", + "Failed to copy {0} to {1}": "", "{0} location": "", "Choose a location to create the {0}": "", "Name of file or directory": "", @@ -831,6 +829,7 @@ "Internal error: Tried to call a non-existing Common API in API register: {0}": "", "Reload Window": "", "The Team configuration file has been opened in the editor. Editing or removal of profiles will need to be done manually.": "", + "Profile {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.": "", "Choose \"Create new...\" to define or select a profile to add to the DATA SETS tree": "", "Choose \"Create new...\" to define or select a profile to add to the JOBS tree": "", "Choose \"Create new...\" to define or select a profile to add to the USS tree": "", diff --git a/packages/zowe-explorer/src/commands/ZoweCommandProvider.ts b/packages/zowe-explorer/src/commands/ZoweCommandProvider.ts index 7db19f3cf..1ad5f86d9 100644 --- a/packages/zowe-explorer/src/commands/ZoweCommandProvider.ts +++ b/packages/zowe-explorer/src/commands/ZoweCommandProvider.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { IZoweTreeNode, PersistenceSchemaEnum, Validation, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; +import { IZoweTreeNode, PersistenceSchemaEnum, Validation } from "@zowe/zowe-explorer-api"; import { ZowePersistentFilters } from "../tools/ZowePersistentFilters"; import { ZoweLogger } from "../tools/ZoweLogger"; import { SharedContext } from "../trees/shared/SharedContext"; @@ -18,7 +18,6 @@ import { Profiles } from "../configuration/Profiles"; import { Constants } from "../configuration/Constants"; import { IconGenerator } from "../icons/IconGenerator"; import { IconUtils } from "../icons/IconUtils"; -import { AuthUtils } from "../utils/AuthUtils"; export class ZoweCommandProvider { // eslint-disable-next-line no-magic-numbers @@ -69,15 +68,7 @@ export class ZoweCommandProvider { } } - await AuthUtils.errorHandling( - vscode.l10n.t({ - message: - "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", - args: [profile.name], - comment: ["Profile name"], - }), - { apiType: ZoweExplorerApiType.Command, profile } - ); + Profiles.getInstance().showProfileInactiveMsg(profile.name); } else if (profileStatus.status === "active") { if ( SharedContext.isSessionNotFav(node) && diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index aaec1446c..f0ce3cad8 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -88,6 +88,16 @@ export class Profiles extends ProfilesCache { return this.mProfileInfo; } + public showProfileInactiveMsg(profileName: string): void { + const inactiveMsg = vscode.l10n.t({ + message: "Profile {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", + args: [profileName], + comment: ["Profile name"], + }); + ZoweLogger.error(inactiveMsg); + void Gui.errorMessage(inactiveMsg); + } + public async checkCurrentProfile(theProfile: imperative.IProfileLoaded): Promise { ZoweLogger.trace("Profiles.checkCurrentProfile called."); let profileStatus: Validation.IValidationProfile = { name: theProfile.name, status: "unverified" }; @@ -107,8 +117,12 @@ export class Profiles extends ProfilesCache { (profile) => !(profile.name === theProfile.name && profile.status !== "unverified") ); try { - await Profiles.getInstance().ssoLogin(null, theProfile.name); + const loggedIn = await Profiles.getInstance().ssoLogin(null, theProfile.name); theProfile = Profiles.getInstance().loadNamedProfile(theProfile.name); + + if (!loggedIn) { + return { ...profileStatus, status: "inactive" }; + } } catch (error) { await AuthUtils.errorHandling(error, { profile: theProfile }); return profileStatus; @@ -129,6 +143,9 @@ export class Profiles extends ProfilesCache { if (values) { theProfile.profile.user = values[0]; theProfile.profile.password = values[1]; + } else { + this.validProfile = Validation.ValidationType.INVALID; + return { ...profileStatus, status: "inactive" }; } } @@ -601,7 +618,6 @@ export class Profiles extends ProfilesCache { ZoweExplorerApiRegister.getInstance() ); if (!promptInfo) { - Gui.showMessage(this.profilesOpCancelled); return; // See https://github.com/zowe/zowe-explorer-vscode/issues/1827 } @@ -811,8 +827,6 @@ export class Profiles extends ProfilesCache { comment: ["Service profile name"], }) ); - } else { - Gui.showMessage(this.profilesOpCancelled); } return loginOk; } catch (err) { diff --git a/packages/zowe-explorer/src/trees/ZoweTreeProvider.ts b/packages/zowe-explorer/src/trees/ZoweTreeProvider.ts index 9c6ee197e..c1616f38b 100644 --- a/packages/zowe-explorer/src/trees/ZoweTreeProvider.ts +++ b/packages/zowe-explorer/src/trees/ZoweTreeProvider.ts @@ -217,44 +217,72 @@ export class ZoweTreeProvider { await Profiles.getInstance().editSession(profile); } + private setStatusForSession(node: IZoweTreeNode, status: Validation.ValidationType): void { + if (node == null) { + // If no session node was found for this provider, don't try to update it + return; + } + let statusContext: string; + let iconId: IconUtils.IconId; + switch (status) { + default: + case Validation.ValidationType.UNVERIFIED: + statusContext = Constants.UNVERIFIED_CONTEXT; + iconId = IconUtils.IconId.session; + break; + case Validation.ValidationType.VALID: + statusContext = Constants.ACTIVE_CONTEXT; + iconId = IconUtils.IconId.sessionActive; + break; + case Validation.ValidationType.INVALID: + statusContext = Constants.INACTIVE_CONTEXT; + iconId = IconUtils.IconId.sessionInactive; + break; + } + + node.contextValue = node.contextValue.replace(/(?<=.*)(_Active|_Inactive|_Unverified)$/, ""); + node.contextValue = node.contextValue + statusContext; + const inactiveIcon = IconGenerator.getIconById(iconId); + if (inactiveIcon) { + node.iconPath = inactiveIcon.path; + } + this.nodeDataChanged(node as T); + } + + private static updateSessionContext(profileName: string, status: Validation.ValidationType): void { + for (const provider of Object.values(SharedTreeProviders.providers)) { + const session = (provider as IZoweTree).mSessionNodes.find((n) => n.getProfileName() === profileName); + (provider as ZoweTreeProvider)?.setStatusForSession(session, status); + } + } + public async checkCurrentProfile(node: IZoweTreeNode): Promise { ZoweLogger.trace("ZoweTreeProvider.checkCurrentProfile called."); const profile = node.getProfile(); + const profileName = profile.name ?? node.getProfileName(); const profileStatus = await Profiles.getInstance().checkCurrentProfile(profile); + const tokenUnusedOrValid = await ZoweTreeProvider.checkJwtTokenForProfile(profileName); + if (!tokenUnusedOrValid) { + // Mark profile as inactive if user dismissed "token expired/login" prompt + profileStatus.status = "inactive"; + Profiles.getInstance().validProfile = Validation.ValidationType.INVALID; + } if (profileStatus.status === "inactive") { if ( SharedContext.isSessionNotFav(node) && (node.contextValue.toLowerCase().includes("session") || node.contextValue.toLowerCase().includes("server")) ) { - node.contextValue = node.contextValue.replace(/(?<=.*)(_Active|_Inactive|_Unverified)$/, ""); - node.contextValue = node.contextValue + Constants.INACTIVE_CONTEXT; - const inactiveIcon = IconGenerator.getIconById(IconUtils.IconId.sessionInactive); - if (inactiveIcon) { - node.iconPath = inactiveIcon.path; - } + ZoweTreeProvider.updateSessionContext(profileName, Validation.ValidationType.INVALID); Profiles.getInstance().validProfile = Validation.ValidationType.INVALID; } - await AuthUtils.errorHandling( - vscode.l10n.t({ - message: - "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", - args: [profile.name], - comment: ["Profile name"], - }), - { profile } - ); + Profiles.getInstance().showProfileInactiveMsg(profile.name); } else if (profileStatus.status === "active") { if ( SharedContext.isSessionNotFav(node) && (node.contextValue.toLowerCase().includes("session") || node.contextValue.toLowerCase().includes("server")) ) { - node.contextValue = node.contextValue.replace(/(?<=.*)(_Active|_Inactive|_Unverified)$/, ""); - node.contextValue = node.contextValue + Constants.ACTIVE_CONTEXT; - const activeIcon = IconGenerator.getIconById(IconUtils.IconId.sessionActive); - if (activeIcon) { - node.iconPath = activeIcon.path; - } + ZoweTreeProvider.updateSessionContext(profileName, Validation.ValidationType.VALID); Profiles.getInstance().validProfile = Validation.ValidationType.VALID; } } else if (profileStatus.status === "unverified") { @@ -262,12 +290,10 @@ export class ZoweTreeProvider { SharedContext.isSessionNotFav(node) && (node.contextValue.toLowerCase().includes("session") || node.contextValue.toLowerCase().includes("server")) ) { - node.contextValue = node.contextValue.replace(/(?<=.*)(_Active|_Inactive|_Unverified)$/, ""); - node.contextValue = node.contextValue + Constants.UNVERIFIED_CONTEXT; + ZoweTreeProvider.updateSessionContext(profileName, Validation.ValidationType.UNVERIFIED); Profiles.getInstance().validProfile = Validation.ValidationType.UNVERIFIED; } } - await ZoweTreeProvider.checkJwtTokenForProfile(node.getProfileName()); this.refresh(); return profileStatus; } @@ -312,13 +338,28 @@ export class ZoweTreeProvider { * If the token has expired, it will prompt the user to log in again. * * @param profileName The name of the profile to check the JWT token for + * @returns + * `true` if: + * - the user attempted to log in + * - the profile does not have a token + * - the token has not expired on the profile + * + * `false` if: + * - they selected "Cancel" / closed the login prompt */ - protected static async checkJwtTokenForProfile(profileName: string): Promise { + protected static async checkJwtTokenForProfile(profileName: string): Promise { const profInfo = await Profiles.getInstance().getProfileInfo(); if (profInfo.hasTokenExpiredForProfile(profileName)) { - await AuthUtils.promptUserForSsoLogin(profileName); + const userResponse = await AuthUtils.promptForSsoLogin(profileName); + if (userResponse === vscode.l10n.t("Log in to Authentication Service")) { + return true; + } + + return false; } + + return true; } private async loadProfileBySessionName( diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 3c8746629..c15105bb3 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -34,6 +34,7 @@ import { ZoweExplorerApiRegister } from "../../extending/ZoweExplorerApiRegister import { ZoweLogger } from "../../tools/ZoweLogger"; import * as dayjs from "dayjs"; import { DatasetUtils } from "./DatasetUtils"; +import { AuthUtils } from "../../utils/AuthUtils"; export class DatasetFSProvider extends BaseProvider implements vscode.FileSystemProvider { private static _instance: DatasetFSProvider; @@ -432,9 +433,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } catch (error) { //Response will error if the file is not found //Callers of fetchDatasetAtUri() do not expect it to throw an error - if (error instanceof Error) { - ZoweLogger.error(error.message); - } + AuthUtils.promptForAuthError(error, metadata.profile); return null; } } @@ -606,12 +605,12 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem comment: ["Data set name"], }), apiType: ZoweExplorerApiType.Mvs, - profileType: entry.metadata.profile.type, + profileType: entry.metadata.profile?.type, retry: { fn: this.writeFile.bind(this), args: [uri, content, options], }, - templateArgs: { profileName: entry.metadata.profile.name ?? "" }, + templateArgs: { profileName: entry.metadata.profile?.name ?? "" }, }); throw err; } @@ -665,7 +664,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem comment: ["File path"], }), apiType: ZoweExplorerApiType.Mvs, - profileType: entry.metadata.profile.type, + profileType: entry.metadata.profile?.type, retry: { fn: this.delete.bind(this), args: [uri, _options], @@ -712,7 +711,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem comment: ["Data set name"], }), apiType: ZoweExplorerApiType.Mvs, - profileType: entry.metadata.profile.type, + profileType: entry.metadata.profile?.type, retry: { fn: this.rename.bind(this), args: [oldUri, newUri, options], @@ -731,6 +730,15 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem entry.metadata.path = newPath; parentDir.entries.set(newName, entry); + if (FsDatasetsUtils.isPdsEntry(entry)) { + for (const [_, member] of entry.entries) { + member.metadata.path = path.posix.join( + entry.metadata.path, + member.metadata.path.substring(member.metadata.path.lastIndexOf("/") + 1) + ); + } + } + this._fireSoon({ type: vscode.FileChangeType.Deleted, uri: oldUri }, { type: vscode.FileChangeType.Created, uri: newUri }); } } diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts index 9be3c67fb..31e6057cd 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts @@ -1236,6 +1236,20 @@ export class DatasetTree extends ZoweTreeProvider implemen node.resourceUri = newUri; node.label = afterDataSetName; node.tooltip = afterDataSetName; + + if (SharedContext.isPds(node)) { + for (const child of node.children) { + child.resourceUri = child.resourceUri.with({ + path: path.posix.join(newUri.path, child.resourceUri.path.substring(child.resourceUri.path.lastIndexOf("/") + 1)), + }); + child.command = { + title: "", + command: "vscode.open", + arguments: [child.resourceUri], + }; + } + } + this.refreshElement(node.getParent() as IZoweDatasetTreeNode); this.updateFavorites(); } diff --git a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts index 441cf1b10..ec428e9c4 100644 --- a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts +++ b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts @@ -31,6 +31,7 @@ import { IJob, IJobFile } from "@zowe/zos-jobs-for-zowe-sdk"; import { Profiles } from "../../configuration/Profiles"; import { ZoweExplorerApiRegister } from "../../extending/ZoweExplorerApiRegister"; import { SharedContext } from "../shared/SharedContext"; +import { AuthUtils } from "../../utils/AuthUtils"; export class JobFSProvider extends BaseProvider implements vscode.FileSystemProvider { private static _instance: JobFSProvider; @@ -202,14 +203,19 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv const bufBuilder = new BufferBuilder(); const jesApi = ZoweExplorerApiRegister.getJesApi(spoolEntry.metadata.profile); - if (jesApi.downloadSingleSpool) { - await jesApi.downloadSingleSpool({ - jobFile: spoolEntry.spool, - stream: bufBuilder, - }); - } else { - const jobEntry = this._lookupParentDirectory(uri) as JobEntry; - bufBuilder.write(await jesApi.getSpoolContentById(jobEntry.job.jobname, jobEntry.job.jobid, spoolEntry.spool.id)); + try { + if (jesApi.downloadSingleSpool) { + await jesApi.downloadSingleSpool({ + jobFile: spoolEntry.spool, + stream: bufBuilder, + }); + } else { + const jobEntry = this._lookupParentDirectory(uri) as JobEntry; + bufBuilder.write(await jesApi.getSpoolContentById(jobEntry.job.jobname, jobEntry.job.jobid, spoolEntry.spool.id)); + } + } catch (err) { + AuthUtils.promptForAuthError(err, spoolEntry.metadata.profile); + throw err; } this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); @@ -231,25 +237,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv public async readFile(uri: vscode.Uri): Promise { const spoolEntry = this._lookupAsFile(uri) as SpoolEntry; if (!spoolEntry.wasAccessed) { - try { - await this.fetchSpoolAtUri(uri); - } catch (err) { - this._handleError(err, { - additionalContext: vscode.l10n.t({ - message: "Failed to get contents for {0}", - args: [spoolEntry.name], - comment: "Spool name", - }), - apiType: ZoweExplorerApiType.Jes, - profileType: spoolEntry.metadata.profile.type, - retry: { - fn: this.readFile.bind(this), - args: [uri], - }, - templateArgs: { profileName: spoolEntry.metadata.profile?.name ?? "" }, - }); - throw err; - } + await this.fetchSpoolAtUri(uri); spoolEntry.wasAccessed = true; } diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 69e6daaa7..fdcf27024 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -30,6 +30,7 @@ import { USSFileStructure } from "./USSFileStructure"; import { Profiles } from "../../configuration/Profiles"; import { ZoweExplorerApiRegister } from "../../extending/ZoweExplorerApiRegister"; import { ZoweLogger } from "../../tools/ZoweLogger"; +import { AuthUtils } from "../../utils/AuthUtils"; export class UssFSProvider extends BaseProvider implements vscode.FileSystemProvider { // Event objects for provider @@ -278,11 +279,11 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const bufBuilder = new BufferBuilder(); const filePath = uri.path.substring(uriInfo.slashAfterProfilePos); const metadata = file.metadata; - await this.autoDetectEncoding(file as UssFile); - const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; let resp: IZosFilesResponse; try { + await this.autoDetectEncoding(file as UssFile); + const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; resp = await ZoweExplorerApiRegister.getUssApi(metadata.profile).getContents(filePath, { binary: file.encoding?.kind === "binary", encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, @@ -291,21 +292,15 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv stream: bufBuilder, }); } catch (err) { - this._handleError(err, { - additionalContext: vscode.l10n.t({ - message: "Failed to get contents for {0}", - args: [filePath], - comment: ["File path"], - }), - retry: { - fn: this.fetchFileAtUri.bind(this), - args: [uri, options], - }, - apiType: ZoweExplorerApiType.Uss, - profileType: metadata.profile.type, - templateArgs: { profileName: metadata.profile.name }, - }); - throw err; + if (err instanceof Error) { + ZoweLogger.error(err.message); + } + AuthUtils.promptForAuthError(err, metadata.profile); + return; + } + + if (!options?.isConflict) { + file.wasAccessed = true; } const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); @@ -395,9 +390,6 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv // - fetching a conflict from the remote FS if ((!file.wasAccessed && !urlQuery.has("inDiff")) || isConflict) { await this.fetchFileAtUri(uri, { isConflict }); - if (!isConflict) { - file.wasAccessed = true; - } } return isConflict ? file.conflictData.contents : file.data; @@ -509,8 +501,8 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv fn: this.writeFile.bind(this), args: [uri, content, options], }, - profileType: parentDir.metadata.profile.type, - templateArgs: { profileName: parentDir.metadata.profile.name ?? "" }, + profileType: parentDir.metadata.profile?.type, + templateArgs: { profileName: parentDir.metadata.profile?.name ?? "" }, }); throw err; } @@ -619,8 +611,8 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv args: [uri, _options], }, apiType: ZoweExplorerApiType.Uss, - profileType: parent.metadata.profile.type, - templateArgs: { profileName: parent.metadata.profile.name ?? "" }, + profileType: parent.metadata.profile?.type, + templateArgs: { profileName: parent.metadata.profile?.name ?? "" }, }); throw err; } diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index d648b1a13..23689ebd6 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -26,8 +26,8 @@ interface ErrorContext { export class AuthUtils { public static async promptForAuthentication( imperativeError: imperative.ImperativeError, - correlation: CorrelatedError, - profile: imperative.IProfileLoaded + profile: imperative.IProfileLoaded, + correlation?: CorrelatedError ): Promise { if (imperativeError.mDetails.additionalDetails) { const tokenError: string = imperativeError.mDetails.additionalDetails; @@ -35,16 +35,15 @@ export class AuthUtils { if (tokenError.includes("Token is not valid or expired.") || isTokenAuth) { const message = vscode.l10n.t("Log in to Authentication Service"); - const success = Gui.showMessage(correlation.message, { items: [message] }).then(async (selection) => { - if (selection) { - return Constants.PROFILES_CACHE.ssoLogin(null, profile.name); - } + const userResp = await Gui.showMessage(correlation?.message ?? imperativeError.message, { + items: [message], + vsCodeOpts: { modal: true }, }); - return success; + return userResp === message ? Constants.PROFILES_CACHE.ssoLogin(null, profile.name) : false; } } const checkCredsButton = vscode.l10n.t("Update Credentials"); - const creds = await Gui.errorMessage(correlation.message, { + const creds = await Gui.errorMessage(correlation?.message ?? imperativeError.message, { items: [checkCredsButton], vsCodeOpts: { modal: true }, }).then(async (selection) => { @@ -56,6 +55,17 @@ export class AuthUtils { return creds != null ? true : false; } + public static promptForAuthError(err: Error, profile: imperative.IProfileLoaded): void { + if ( + err instanceof imperative.ImperativeError && + profile != null && + (Number(err.errorCode) === imperative.RestConstants.HTTP_STATUS_401 || + err.message.includes("All configured authentication methods failed")) + ) { + void AuthUtils.promptForAuthentication(err, profile).catch((error) => error instanceof Error && ZoweLogger.error(error.message)); + } + } + public static async openConfigForMissingHostname(profile: imperative.IProfileLoaded): Promise { const mProfileInfo = await Constants.PROFILES_CACHE.getProfileInfo(); Gui.errorMessage(vscode.l10n.t("Required parameter 'host' must not be blank.")); @@ -97,14 +107,14 @@ export class AuthUtils { (httpErrorCode === imperative.RestConstants.HTTP_STATUS_401 || imperativeError.message.includes("All configured authentication methods failed")) ) { - return AuthUtils.promptForAuthentication(imperativeError, correlation, profile); + return AuthUtils.promptForAuthentication(imperativeError, profile, correlation); } } if (errorDetails.toString().includes("Could not find profile")) { return false; } - await ErrorCorrelator.getInstance().displayCorrelatedError(correlation, { templateArgs: { profileName: profile?.name ?? "" } }); + void ErrorCorrelator.getInstance().displayCorrelatedError(correlation, { templateArgs: { profileName: profile?.name ?? "" } }); return false; } @@ -112,7 +122,7 @@ export class AuthUtils { * Prompts user to log in to authentication service. * @param profileName The name of the profile used to log in */ - public static promptUserForSsoLogin(profileName: string): Thenable { + public static promptForSsoLogin(profileName: string): Thenable { return Gui.showMessage( vscode.l10n.t({ message: @@ -125,6 +135,7 @@ export class AuthUtils { if (selection) { await Constants.PROFILES_CACHE.ssoLogin(null, profileName); } + return selection; }); }