From 3ea00c9331b05c1294e56a2e51dd0d8febd7a7fb Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Tue, 23 Jul 2024 18:16:48 -0400 Subject: [PATCH 01/11] WIP Support nested profiles for APIML login Signed-off-by: Timothy Johnson --- .../src/profiles/ProfilesCache.ts | 6 +++- .../src/vscode/ZoweVsCodeExtension.ts | 28 +++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts index ca2143c354..539169da94 100644 --- a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts +++ b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts @@ -320,8 +320,12 @@ export class ProfilesCache { } // This will retrieve the base profile from imperative - public async fetchBaseProfile(): Promise { + public async fetchBaseProfile(profileName?: string): Promise { const mProfileInfo = await this.getProfileInfo(); + if (profileName?.includes(".")) { + const parentProfile = profileName.slice(0, profileName.lastIndexOf(".")); + return this.getProfileLoaded(parentProfile, "base", mProfileInfo.getTeamConfig().api.profiles.get(parentProfile)); + } const baseProfileAttrs = mProfileInfo.getDefaultProfile("base"); if (baseProfileAttrs == null) { return undefined; diff --git a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts index 5ee33adee3..6807d567c8 100644 --- a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts +++ b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts @@ -120,13 +120,13 @@ export class ZoweVsCodeExtension { zeProfiles?: ProfilesCache // Profiles extends ProfilesCache ): Promise { const cache: ProfilesCache = zeProfiles ?? ZoweVsCodeExtension.profilesCache; - const baseProfile = await cache.fetchBaseProfile(); - if (baseProfile == null) { - return false; - } if (typeof serviceProfile === "string") { serviceProfile = await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, serviceProfile); } + const baseProfile = await cache.fetchBaseProfile(serviceProfile.name); + if (baseProfile == null) { + return false; + } const tokenType = serviceProfile.profile.tokenType ?? baseProfile.profile.tokenType ?? loginTokenType ?? imperative.SessConstants.TOKEN_TYPE_APIML; const updSession = new imperative.Session({ @@ -144,7 +144,10 @@ export class ZoweVsCodeExtension { { label: "$(account) User and Password", description: "Log in with basic authentication" }, { label: "$(note) Certificate", description: "Log in with PEM format certificate file" }, ]; - const response = await Gui.showQuickPick(qpItems, { placeHolder: "Select an authentication method for obtaining token" }); + const response = await Gui.showQuickPick(qpItems, { + placeHolder: "Select an authentication method for obtaining token", + title: `[${baseProfile.name}] Log in to authentication service`, + }); if (response === qpItems[0]) { const creds = await ZoweVsCodeExtension.promptUserPass({ session: updSession.ISession, rePrompt: true }); if (!creds) { @@ -182,7 +185,7 @@ export class ZoweVsCodeExtension { // If the connection details do not match, then we MUST forcefully store the token in the service profile let profileToUpdate: imperative.IProfileLoaded; if (connOk) { - profileToUpdate = baseProfile; + profileToUpdate = serviceProfile.name.startsWith(baseProfile.name + ".") ? { ...baseProfile, type: null } : baseProfile; } else { profileToUpdate = serviceProfile; } @@ -214,11 +217,11 @@ export class ZoweVsCodeExtension { zeProfiles?: ProfilesCache // Profiles extends ProfilesCache ): Promise { const cache: ProfilesCache = zeProfiles ?? ZoweVsCodeExtension.profilesCache; - const baseProfile = await cache.fetchBaseProfile(); + if (typeof serviceProfile === "string") { + serviceProfile = await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, serviceProfile); + } + const baseProfile = await cache.fetchBaseProfile(serviceProfile.name); if (baseProfile) { - if (typeof serviceProfile === "string") { - serviceProfile = await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, serviceProfile); - } const tokenType = serviceProfile.profile.tokenType ?? baseProfile.profile.tokenType ?? @@ -234,7 +237,10 @@ export class ZoweVsCodeExtension { }); await (zeRegister?.getCommonApi(serviceProfile).logout ?? Logout.apimlLogout)(updSession); - const connOk = serviceProfile.profile.host === baseProfile.profile.host && serviceProfile.profile.port === baseProfile.profile.port; + const connOk = + serviceProfile.profile.host === baseProfile.profile.host && + serviceProfile.profile.port === baseProfile.profile.port && + !serviceProfile.name.startsWith(baseProfile.name + "."); if (connOk) { await cache.updateBaseProfileFileLogout(baseProfile); } else { From f4d384d8d8aa6e0cb99b5bb7353edf4c0a45b006 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Tue, 23 Jul 2024 18:25:33 -0400 Subject: [PATCH 02/11] Check for tokenValue on base profile and update tests Signed-off-by: Timothy Johnson --- .../profiles/ProfilesCache.unit.test.ts | 26 ++++++++++++++++++- .../vscode/ZoweVsCodeExtension.unit.test.ts | 4 +-- .../src/profiles/ProfilesCache.ts | 9 ++++--- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts index 3b909a1d09..c0eced94c4 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts @@ -121,7 +121,10 @@ function createProfInfoMock(profiles: Partial[]): imp knownArgs: Object.entries(profile.profile as object).map(([k, v]) => ({ argName: k, argValue: v as unknown })), }; }, - getTeamConfig: () => ({ exists: true }), + getTeamConfig: () => ({ + api: { secure: { securePropsForProfile: jest.fn().mockReturnValue([]) } }, + exists: true, + }), updateProperty: jest.fn(), updateKnownProperty: jest.fn(), isSecured: jest.fn(), @@ -551,6 +554,27 @@ describe("ProfilesCache", () => { expect(profile).toMatchObject(baseProfile); }); + fit("fetchBaseProfile should return typeless profile if base profile not found", async () => { + const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); + jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([baseProfile])); + const profile = await profCache.fetchBaseProfile(); + expect(profile).toMatchObject({ name: "lpar1", type: "base" }); + }); + + // it("fetchBaseProfile should return typeless profile if base profile does not contain token value", async () => { + // const profCache = new ProfilesCache(fakeLogger as unknown as zowe.imperative.Logger); + // jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([lpar1Profile])); + // const profile = await profCache.fetchBaseProfile(); + // expect(profile).toBeUndefined(); + // }); + + // it("fetchBaseProfile should return base profile if it contains token value", async () => { + // const profCache = new ProfilesCache(fakeLogger as unknown as zowe.imperative.Logger); + // jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([lpar1Profile])); + // const profile = await profCache.fetchBaseProfile(); + // expect(profile).toBeUndefined(); + // }); + it("fetchBaseProfile should return undefined if base profile not found", async () => { const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([lpar1Profile])); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts index 43a866500e..121135d4d9 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts @@ -121,7 +121,7 @@ describe("ZoweVsCodeExtension", () => { allProfiles, allExternalTypes: [], fetchBaseProfile: jest.fn(), - loadNamedProfile: jest.fn().mockReturnValue({ profile: testProfile }), + loadNamedProfile: jest.fn().mockReturnValue(serviceProfile), updateBaseProfileFileLogin: jest.fn(), updateBaseProfileFileLogout: jest.fn(), getLoadedProfConfig: jest.fn().mockReturnValue({ profile: {} }), @@ -175,7 +175,7 @@ describe("ZoweVsCodeExtension", () => { it("should logout using the base profile given a simple profile name", async () => { testCache.fetchBaseProfile.mockResolvedValue(baseProfile); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); - testSpy.mockResolvedValue({ profile: { ...testProfile, ...updProfile } }); + testSpy.mockResolvedValue({ ...serviceProfile, profile: { ...testProfile, ...updProfile } }); const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); diff --git a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts index 539169da94..1ed5fdf356 100644 --- a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts +++ b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts @@ -322,12 +322,13 @@ export class ProfilesCache { // This will retrieve the base profile from imperative public async fetchBaseProfile(profileName?: string): Promise { const mProfileInfo = await this.getProfileInfo(); - if (profileName?.includes(".")) { + const baseProfileAttrs = mProfileInfo.getDefaultProfile("base"); + const isUsingTokenAuth = (profName: string): boolean => + mProfileInfo.getTeamConfig().api.secure.securePropsForProfile(profName).includes("tokenValue"); + if ((baseProfileAttrs == null || !isUsingTokenAuth(baseProfileAttrs.profName)) && profileName?.includes(".")) { const parentProfile = profileName.slice(0, profileName.lastIndexOf(".")); return this.getProfileLoaded(parentProfile, "base", mProfileInfo.getTeamConfig().api.profiles.get(parentProfile)); - } - const baseProfileAttrs = mProfileInfo.getDefaultProfile("base"); - if (baseProfileAttrs == null) { + } else if (baseProfileAttrs == null) { return undefined; } const profAttr = this.getMergedAttrs(mProfileInfo, baseProfileAttrs); From e76b10a2b39b6ad787cbd74da0d49192097f0ae2 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Mon, 29 Jul 2024 16:47:04 -0400 Subject: [PATCH 03/11] Add unit tests for fetchBaseProfile method Signed-off-by: Timothy Johnson --- .../profiles/ProfilesCache.unit.test.ts | 65 +++++++++---------- .../__mocks__/mockCreators/shared.ts | 9 ++- .../configuration/Profiles.unit.test.ts | 10 +-- 3 files changed, 45 insertions(+), 39 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts index e6dd041604..d755f7566e 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts @@ -14,17 +14,10 @@ import * as fs from "fs"; import * as imperative from "@zowe/imperative"; import { ProfilesCache } from "../../../src/profiles/ProfilesCache"; import { FileManagement, Types } from "../../../src"; +import { mocked } from "../../../__mocks__/mockUtils"; jest.mock("fs"); -const fakeSchema: { properties: object } = { - properties: { - host: { type: "string" }, - port: { type: "number" }, - user: { type: "string", secure: true }, - password: { type: "string", secure: true }, - }, -}; const lpar1Profile: Required> = { name: "lpar1", type: "zosmf", @@ -78,7 +71,7 @@ const baseProfileWithToken = { tokenValue: "baseToken", }, }; -const profilemetadata: imperative.ICommandProfileTypeConfiguration[] = [ +const profileMetadata: imperative.ICommandProfileTypeConfiguration[] = [ { type: "acme", schema: { @@ -91,6 +84,13 @@ const profilemetadata: imperative.ICommandProfileTypeConfiguration[] = [ ]; function createProfInfoMock(profiles: Partial[]): imperative.ProfileInfo { + const teamConfigApi: Partial = { + api: { + profiles: { get: jest.fn() }, + secure: { securePropsForProfile: jest.fn().mockReturnValue([]) }, + } as any, + exists: true, + }; return { getAllProfiles: (profType?: string) => profiles @@ -121,10 +121,7 @@ function createProfInfoMock(profiles: Partial[]): imp knownArgs: Object.entries(profile.profile as object).map(([k, v]) => ({ argName: k, argValue: v as unknown })), }; }, - getTeamConfig: () => ({ - api: { secure: { securePropsForProfile: jest.fn().mockReturnValue([]) } }, - exists: true, - }), + getTeamConfig: () => teamConfigApi, updateProperty: jest.fn(), updateKnownProperty: jest.fn(), isSecured: jest.fn(), @@ -168,16 +165,16 @@ describe("ProfilesCache", () => { it("addToConfigArray should set the profileTypeConfigurations array", () => { const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); - profilemetadata.push(profilemetadata[0]); - profCache.addToConfigArray(profilemetadata); - expect(profCache.profileTypeConfigurations).toEqual(profilemetadata.filter((a, index) => index == 0)); + profileMetadata.push(profileMetadata[0]); + profCache.addToConfigArray(profileMetadata); + expect(profCache.profileTypeConfigurations).toEqual(profileMetadata.filter((a, index) => index == 0)); }); it("getConfigArray should return the data of profileTypeConfigurations Array", () => { const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); - profCache.profileTypeConfigurations = profilemetadata; + profCache.profileTypeConfigurations = profileMetadata; const res = profCache.getConfigArray(); - expect(res).toEqual(profilemetadata); + expect(res).toEqual(profileMetadata); }); it("loadNamedProfile should find profiles by name and type", () => { @@ -548,26 +545,28 @@ describe("ProfilesCache", () => { expect(profile).toMatchObject(baseProfile); }); - fit("fetchBaseProfile should return typeless profile if base profile not found", async () => { + it("fetchBaseProfile should return typeless profile if base profile not found", async () => { const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); - jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([baseProfile])); - const profile = await profCache.fetchBaseProfile(); + jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([])); + const profile = await profCache.fetchBaseProfile("lpar1.zosmf"); expect(profile).toMatchObject({ name: "lpar1", type: "base" }); }); - // it("fetchBaseProfile should return typeless profile if base profile does not contain token value", async () => { - // const profCache = new ProfilesCache(fakeLogger as unknown as zowe.imperative.Logger); - // jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([lpar1Profile])); - // const profile = await profCache.fetchBaseProfile(); - // expect(profile).toBeUndefined(); - // }); + it("fetchBaseProfile should return typeless profile if base profile does not contain token value", async () => { + const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); + jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([baseProfile])); + const profile = await profCache.fetchBaseProfile("lpar1.zosmf"); + expect(profile).toMatchObject({ name: "lpar1", type: "base" }); + }); - // it("fetchBaseProfile should return base profile if it contains token value", async () => { - // const profCache = new ProfilesCache(fakeLogger as unknown as zowe.imperative.Logger); - // jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([lpar1Profile])); - // const profile = await profCache.fetchBaseProfile(); - // expect(profile).toBeUndefined(); - // }); + it("fetchBaseProfile should return base profile if it contains token value", async () => { + const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); + const profInfoMock = createProfInfoMock([baseProfile]); + jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(profInfoMock); + mocked(profInfoMock.getTeamConfig().api.secure.securePropsForProfile).mockReturnValue(["tokenValue"]); + const profile = await profCache.fetchBaseProfile("lpar1.zosmf"); + expect(profile).toMatchObject(baseProfile); + }); it("fetchBaseProfile should return undefined if base profile not found", async () => { const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); diff --git a/packages/zowe-explorer/__tests__/__mocks__/mockCreators/shared.ts b/packages/zowe-explorer/__tests__/__mocks__/mockCreators/shared.ts index 559b267222..144093b9d1 100644 --- a/packages/zowe-explorer/__tests__/__mocks__/mockCreators/shared.ts +++ b/packages/zowe-explorer/__tests__/__mocks__/mockCreators/shared.ts @@ -409,7 +409,14 @@ export function createInstanceOfProfileInfo() { updateProperty: jest.fn(), updateKnownProperty: jest.fn(), createSession: jest.fn(), - getTeamConfig: () => ({ exists: true }), + getTeamConfig: () => ({ + api: { + secure: { + securePropsForProfile: jest.fn().mockReturnValue([]), + }, + }, + exists: true, + }), mergeArgsForProfile: jest.fn().mockReturnValue({ knownArgs: [ { 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 9641ab02a1..c75e2ac04a 100644 --- a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts @@ -994,8 +994,8 @@ describe("Profiles Unit Tests - function enableValidationContext", () => { describe("Profiles Unit Tests - function ssoLogin", () => { let testNode; let globalMocks; - beforeEach(async () => { - globalMocks = await createGlobalMocks(); + beforeEach(() => { + globalMocks = createGlobalMocks(); testNode = new (ZoweTreeNode as any)( "fake", vscode.TreeItemCollapsibleState.None, @@ -1060,8 +1060,8 @@ describe("Profiles Unit Tests - function ssoLogin", () => { describe("Profiles Unit Tests - function ssoLogout", () => { let testNode; let globalMocks; - beforeEach(async () => { - globalMocks = await createGlobalMocks(); + beforeEach(() => { + globalMocks = createGlobalMocks(); testNode = new (ZoweTreeNode as any)( "fake", vscode.TreeItemCollapsibleState.None, @@ -1069,7 +1069,7 @@ describe("Profiles Unit Tests - function ssoLogout", () => { globalMocks.testSession, globalMocks.testProfile ); - + testNode.profile.profile.password = undefined; testNode.profile.profile.user = "fake"; Object.defineProperty(Profiles.getInstance(), "allProfiles", { value: [ From 71fb285774c7154b096033016881b9caf059ba20 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Tue, 30 Jul 2024 15:52:45 -0400 Subject: [PATCH 04/11] Add login unit test and update changelogs Signed-off-by: Timothy Johnson --- packages/zowe-explorer-api/CHANGELOG.md | 1 + .../vscode/ZoweVsCodeExtension.unit.test.ts | 45 +++++++++++++++++++ .../src/vscode/ZoweVsCodeExtension.ts | 10 +---- packages/zowe-explorer/CHANGELOG.md | 1 + 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 26fe8157c6..68ca8a4929 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -36,6 +36,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Added PEM certificate support as an authentication method for logging into the API ML. [#2621](https://github.com/zowe/zowe-explorer-vscode/issues/2621) - Deprecated the `getUSSDocumentFilePath` function on the `IZoweTreeNode` interface as Zowe Explorer no longer uses the local file system for storing USS files. **No replacement is planned**; please access data from tree nodes using their [resource URIs](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider#operations-for-extenders) instead. [#2968](https://github.com/zowe/zowe-explorer-vscode/pull/2968) - **Next Breaking:** Changed `ProfilesCache.convertV1ProfToConfig` method to be a static method that requires `ProfileInfo` instance as a parameter. +- Added support for logging in to multiple API ML instances per team config file. [#2264](https://github.com/zowe/zowe-explorer-vscode/issues/2264) ### Bug fixes diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts index 07a023e6d7..cfe452b83f 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts @@ -250,6 +250,51 @@ describe("ZoweVsCodeExtension", () => { ); quickPickMock.mockRestore(); }); + it("should login using the parent profile given a nested profile name", async () => { + const tempBaseProfile = JSON.parse(JSON.stringify(baseProfile)); + tempBaseProfile.name = "lpar"; + tempBaseProfile.profile.tokenType = "some-dummy-token-type"; + testCache.fetchBaseProfile.mockResolvedValue(tempBaseProfile); + const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); + const newServiceProfile = { + ...serviceProfile, + name: "lpar.service", + profile: { ...testProfile, tokenValue: "tokenValue", host: "dummy" }, + }; + testSpy.mockResolvedValue(newServiceProfile); + jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["user", "pass"]); + const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); + + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); + await ZoweVsCodeExtension.loginWithBaseProfile("lpar.service"); + + const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + delete testSession.ISession.user; + delete testSession.ISession.password; + testSession.ISession.hostname = "dummy"; + testSession.ISession.base64EncodedAuth = "dXNlcjpwYXNz"; + testSession.ISession.tokenType = tempBaseProfile.profile.tokenType; + testSession.ISession.storeCookie = false; + + expect(loginSpy).toHaveBeenCalledWith(testSession); + expect(testSpy).toHaveBeenCalledWith(testCache, "lpar.service"); + expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith( + { + name: tempBaseProfile.name, + type: null, + profile: { + ...serviceProfile.profile, + tokenType: tempBaseProfile.profile.tokenType, + }, + }, + { + tokenType: tempBaseProfile.profile.tokenType, + tokenValue: "tokenValue", + }, + false + ); + quickPickMock.mockRestore(); + }); it("should logout using the service profile given a simple profile name", async () => { testCache.fetchBaseProfile.mockResolvedValue(baseProfile); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); diff --git a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts index 36c5454497..29a23dd6fb 100644 --- a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts +++ b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts @@ -183,11 +183,9 @@ export class ZoweVsCodeExtension { // If base profile already has a token type stored, then we check whether or not the connection details are the same (serviceProfile.profile.host === baseProfile.profile.host && serviceProfile.profile.port === baseProfile.profile.port); // If the connection details do not match, then we MUST forcefully store the token in the service profile - let profileToUpdate: imperative.IProfileLoaded; + let profileToUpdate = serviceProfile; if (connOk) { profileToUpdate = serviceProfile.name.startsWith(baseProfile.name + ".") ? { ...baseProfile, type: null } : baseProfile; - } else { - profileToUpdate = serviceProfile; } await cache.updateBaseProfileFileLogin(profileToUpdate, updBaseProfile, !connOk); @@ -241,11 +239,7 @@ export class ZoweVsCodeExtension { serviceProfile.profile.host === baseProfile.profile.host && serviceProfile.profile.port === baseProfile.profile.port && !serviceProfile.name.startsWith(baseProfile.name + "."); - if (connOk) { - await cache.updateBaseProfileFileLogout(baseProfile); - } else { - await cache.updateBaseProfileFileLogout(serviceProfile); - } + await cache.updateBaseProfileFileLogout(connOk ? baseProfile : serviceProfile); } } diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 2a106b6cc2..d4ec21aed8 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -41,6 +41,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Added support to view the Encoding history for MVS and Dataset in the History View. [#2776](https://github.com/zowe/vscode-extension-for-zowe/issues/2776) - Added error handling for when the default credential manager is unable to initialize. [#2811](https://github.com/zowe/zowe-explorer-vscode/issues/2811) - **Breaking:** Zowe Explorer no longer uses a temporary directory for storing Data Sets and USS files. All settings related to the temporary downloads folder have been removed. In order to access resources stored by Zowe Explorer v3, refer to the [FileSystemProvider documentation](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider) for information on how to build and access resource URIs. Extenders can detect changes to resources using the `onResourceChanged` function in the `ZoweExplorerApiRegister` class. [#2951](https://github.com/zowe/zowe-explorer-vscode/issues/2951) +- Added support for logging in to multiple API ML instances per team config file. [#2264](https://github.com/zowe/zowe-explorer-vscode/issues/2264) ### Bug fixes From 862e450dfe884baf0b9e19d42fa3b1a0bc9ef27c Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Wed, 31 Jul 2024 07:19:15 -0400 Subject: [PATCH 05/11] Show error message when no base profile found to store SSO token Signed-off-by: Timothy Johnson --- packages/zowe-explorer-api/CHANGELOG.md | 1 + .../vscode/ZoweVsCodeExtension.unit.test.ts | 4 ++++ .../src/vscode/ZoweVsCodeExtension.ts | 7 ++++++- .../src/configuration/Profiles.ts | 21 +++++++++++-------- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 68ca8a4929..c4adaae9a8 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -37,6 +37,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Deprecated the `getUSSDocumentFilePath` function on the `IZoweTreeNode` interface as Zowe Explorer no longer uses the local file system for storing USS files. **No replacement is planned**; please access data from tree nodes using their [resource URIs](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider#operations-for-extenders) instead. [#2968](https://github.com/zowe/zowe-explorer-vscode/pull/2968) - **Next Breaking:** Changed `ProfilesCache.convertV1ProfToConfig` method to be a static method that requires `ProfileInfo` instance as a parameter. - Added support for logging in to multiple API ML instances per team config file. [#2264](https://github.com/zowe/zowe-explorer-vscode/issues/2264) +- **Next Breaking:** Changed return type of `ZoweVsCodeExtension.logoutWithBaseProfile` method from `void` to `boolean` to indicate whether logout was successful. ### Bug fixes diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts index cfe452b83f..e96c41cc91 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts @@ -139,16 +139,20 @@ describe("ZoweVsCodeExtension", () => { }); it("should not login if the base profile cannot be fetched", async () => { + const errorMessageSpy = jest.spyOn(Gui, "errorMessage"); testCache.fetchBaseProfile.mockResolvedValue(null); await ZoweVsCodeExtension.loginWithBaseProfile("service"); expect(testCache.fetchBaseProfile).toHaveBeenCalledTimes(1); expect(testCache.updateBaseProfileFileLogin).not.toHaveBeenCalled(); + expect(errorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("Login failed: No base profile found")); }); it("should not logout if the base profile cannot be fetched", async () => { + const errorMessageSpy = jest.spyOn(Gui, "errorMessage"); testCache.fetchBaseProfile.mockResolvedValue(null); await ZoweVsCodeExtension.logoutWithBaseProfile("service"); expect(testCache.fetchBaseProfile).toHaveBeenCalledTimes(1); expect(testCache.updateBaseProfileFileLogin).not.toHaveBeenCalled(); + expect(errorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("Logout failed: No base profile found")); }); describe("user and password chosen", () => { diff --git a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts index 29a23dd6fb..0402d03c7f 100644 --- a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts +++ b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts @@ -125,6 +125,7 @@ export class ZoweVsCodeExtension { } const baseProfile = await cache.fetchBaseProfile(serviceProfile.name); if (baseProfile == null) { + Gui.errorMessage(`Login failed: No base profile found to store SSO token for profile "${serviceProfile.name}"`); return false; } const tokenType = @@ -213,7 +214,7 @@ export class ZoweVsCodeExtension { serviceProfile: string | imperative.IProfileLoaded, zeRegister?: Types.IApiRegisterClient, // ZoweExplorerApiRegister zeProfiles?: ProfilesCache // Profiles extends ProfilesCache - ): Promise { + ): Promise { const cache: ProfilesCache = zeProfiles ?? ZoweVsCodeExtension.profilesCache; if (typeof serviceProfile === "string") { serviceProfile = await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, serviceProfile); @@ -240,6 +241,10 @@ export class ZoweVsCodeExtension { serviceProfile.profile.port === baseProfile.profile.port && !serviceProfile.name.startsWith(baseProfile.name + "."); await cache.updateBaseProfileFileLogout(connOk ? baseProfile : serviceProfile); + return true; + } else { + Gui.errorMessage(`Logout failed: No base profile found to store SSO token for profile "${serviceProfile.name}"`); + return false; } } diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index 980c594f5a..8d82fba7ef 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -835,6 +835,7 @@ export class Profiles extends ProfilesCache { try { this.clearFilterFromAllTrees(node); + let logoutOk = true; // this will handle extenders if ( @@ -844,16 +845,18 @@ export class Profiles extends ProfilesCache { ) { await ZoweExplorerApiRegister.getInstance().getCommonApi(serviceProfile).logout(node.getSession()); } else { - await ZoweVsCodeExtension.logoutWithBaseProfile(serviceProfile, ZoweExplorerApiRegister.getInstance(), this); + logoutOk = await ZoweVsCodeExtension.logoutWithBaseProfile(serviceProfile, ZoweExplorerApiRegister.getInstance(), this); + } + if (logoutOk) { + Gui.showMessage( + vscode.l10n.t({ + message: "Logout from authentication service was successful for {0}.", + args: [serviceProfile.name], + comment: ["Service profile name"], + }) + ); + await Profiles.getInstance().refresh(ZoweExplorerApiRegister.getInstance()); } - Gui.showMessage( - vscode.l10n.t({ - message: "Logout from authentication service was successful for {0}.", - args: [serviceProfile.name], - comment: ["Service profile name"], - }) - ); - await Profiles.getInstance().refresh(ZoweExplorerApiRegister.getInstance()); } catch (error) { const message = vscode.l10n.t({ message: "Unable to log out with {0}. {1}", From 393169eeafd84442b52f4fcd03ca058b0124b398 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Wed, 7 Aug 2024 11:58:53 -0400 Subject: [PATCH 06/11] Remove arrow function for isUsingTokenAuth Signed-off-by: Timothy Johnson --- .../zowe-explorer-api/src/profiles/ProfilesCache.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts index 862e725abe..214b2aaf76 100644 --- a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts +++ b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts @@ -319,11 +319,13 @@ export class ProfilesCache { public async fetchBaseProfile(profileName?: string): Promise { const mProfileInfo = await this.getProfileInfo(); const baseProfileAttrs = mProfileInfo.getDefaultProfile("base"); - const isUsingTokenAuth = (profName: string): boolean => - mProfileInfo.getTeamConfig().api.secure.securePropsForProfile(profName).includes("tokenValue"); - if ((baseProfileAttrs == null || !isUsingTokenAuth(baseProfileAttrs.profName)) && profileName?.includes(".")) { + const configApi = mProfileInfo.getTeamConfig().api; + if ( + profileName?.includes(".") && + (baseProfileAttrs == null || !configApi.secure.securePropsForProfile(baseProfileAttrs.profName).includes("tokenValue")) + ) { const parentProfile = profileName.slice(0, profileName.lastIndexOf(".")); - return this.getProfileLoaded(parentProfile, "base", mProfileInfo.getTeamConfig().api.profiles.get(parentProfile)); + return this.getProfileLoaded(parentProfile, "base", configApi.profiles.get(parentProfile)); } else if (baseProfileAttrs == null) { return undefined; } From 03cf49998d4064eeba3f2a8acac06847d9697360 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Thu, 8 Aug 2024 07:26:37 -0400 Subject: [PATCH 07/11] Add typedoc and address misc PR feedback Signed-off-by: Timothy Johnson --- packages/zowe-explorer-api/CHANGELOG.md | 2 +- .../zowe-explorer-api/src/profiles/ProfilesCache.ts | 12 +++++++++++- .../src/vscode/ZoweVsCodeExtension.ts | 4 +++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index f439dfa81e..d1ae90f616 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -39,7 +39,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Added the `onVaultUpdate` VSCode event to notify extenders when credentials are updated on the OS vault by other applications. [#2994](https://github.com/zowe/zowe-explorer-vscode/pull/2994) - Added the `onCredMgrsUpdate` VSCode event to notify extenders when the local PC's credential manager has been updated by other applications. [#2994](https://github.com/zowe/zowe-explorer-vscode/pull/2994) - **Breaking:** Updated most function signatures for exported programmatic interfaces. Changes make developing with the Zowe Explorer API more efficient for extenders by showing which properties they can expect when calling our APIs. [#2952](https://github.com/zowe/zowe-explorer-vscode/issues/2952) -- Added support for logging in to multiple API ML instances per team config file. [#2264](https://github.com/zowe/zowe-explorer-vscode/issues/2264) +- Enhanced the `ZoweVsCodeExtension.loginWithBaseProfile` and `ZoweVsCodeExtension.logoutWithBaseProfile` methods to store SSO token in parent profile when nested profiles are in use. [#2264](https://github.com/zowe/zowe-explorer-vscode/issues/2264) - **Next Breaking:** Changed return type of `ZoweVsCodeExtension.logoutWithBaseProfile` method from `void` to `boolean` to indicate whether logout was successful. ### Bug fixes diff --git a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts index 214b2aaf76..dc680e275e 100644 --- a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts +++ b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts @@ -315,7 +315,13 @@ export class ProfilesCache { return baseProfile; } - // This will retrieve the base profile from imperative + /** + * Retrieves the base profile from Imperative to use for log in/out. If a + * nested profile name is specified (e.g. "lpar.zosmf"), then its parent + * profile is returned unless token is already stored in the base profile. + * @param profileName Name of profile that was selected in the tree + * @returns IProfileLoaded object or undefined if no profile was found + */ public async fetchBaseProfile(profileName?: string): Promise { const mProfileInfo = await this.getProfileInfo(); const baseProfileAttrs = mProfileInfo.getDefaultProfile("base"); @@ -324,6 +330,10 @@ export class ProfilesCache { profileName?.includes(".") && (baseProfileAttrs == null || !configApi.secure.securePropsForProfile(baseProfileAttrs.profName).includes("tokenValue")) ) { + // Retrieve parent typeless profile as base profile if: + // (1) The active profile name is nested (contains a period) AND + // (2) No default base profile was found OR + // Default base profile does not have tokenValue in secure array const parentProfile = profileName.slice(0, profileName.lastIndexOf(".")); return this.getProfileLoaded(parentProfile, "base", configApi.profiles.get(parentProfile)); } else if (baseProfileAttrs == null) { diff --git a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts index 17ebdded69..97acc167b4 100644 --- a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts +++ b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts @@ -186,6 +186,7 @@ export class ZoweVsCodeExtension { // If the connection details do not match, then we MUST forcefully store the token in the service profile let profileToUpdate = serviceProfile; if (connOk) { + // If active profile is nested (e.g. lpar.zosmf), then set type to null so token can be stored in parent typeless profile profileToUpdate = serviceProfile.name.startsWith(baseProfile.name + ".") ? { ...baseProfile, type: null } : baseProfile; } @@ -236,6 +237,7 @@ export class ZoweVsCodeExtension { }); await (zeRegister?.getCommonApi(serviceProfile).logout ?? Logout.apimlLogout)(updSession); + // If active profile is nested (e.g. lpar.zosmf), then update service profile since base profile may be typeless const connOk = serviceProfile.profile.host === baseProfile.profile.host && serviceProfile.profile.port === baseProfile.profile.port && @@ -243,7 +245,7 @@ export class ZoweVsCodeExtension { await cache.updateBaseProfileFileLogout(connOk ? baseProfile : serviceProfile); return true; } else { - Gui.errorMessage(`Logout failed: No base profile found to store SSO token for profile "${serviceProfile.name}"`); + Gui.errorMessage(`Logout failed: No base profile found to remove SSO token for profile "${serviceProfile.name}"`); return false; } } From a2f1d12752b1178b75becdac00eda753d5a753d3 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Fri, 9 Aug 2024 08:38:53 -0400 Subject: [PATCH 08/11] Remove duplicate changelog entry Signed-off-by: Timothy Johnson --- packages/zowe-explorer-api/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index c6f0b8c760..35ffca4865 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -38,7 +38,6 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - **Next Breaking:** Changed `ProfilesCache.convertV1ProfToConfig` method to be a static method that requires `ProfileInfo` instance as a parameter. - Added the `onVaultUpdate` VSCode event to notify extenders when credentials are updated on the OS vault by other applications. [#2994](https://github.com/zowe/zowe-explorer-vscode/pull/2994) - Added the `onCredMgrsUpdate` VSCode event to notify extenders when the local PC's credential manager has been updated by other applications. [#2994](https://github.com/zowe/zowe-explorer-vscode/pull/2994) -- **Breaking:** Updated most function signatures for exported programmatic interfaces. Changes make developing with the Zowe Explorer API more efficient for extenders by showing which properties they can expect when calling our APIs. [#2952](https://github.com/zowe/zowe-explorer-vscode/issues/2952) - **LTS Breaking:** Updated most function signatures for exported programmatic interfaces. Changes make developing with the Zowe Explorer API more efficient for extenders by showing which properties they can expect when calling our APIs. [#2952](https://github.com/zowe/zowe-explorer-vscode/issues/2952) - Updated `IApiExplorerExtender.ts`, see changes below: From 4708616435335f5ea11373a4192ccb4d8f4439f5 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Thu, 15 Aug 2024 15:38:23 -0400 Subject: [PATCH 09/11] Fetch correct base profile for multiple levels of nesting Signed-off-by: Timothy Johnson --- .../profiles/ProfilesCache.unit.test.ts | 19 ++++++++++++++-- .../src/profiles/ProfilesCache.ts | 22 +++++++++++++++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts index c14104f3f7..5c31c1c8b3 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts @@ -86,8 +86,14 @@ const profileMetadata: imperative.ICommandProfileTypeConfiguration[] = [ function createProfInfoMock(profiles: Partial[]): imperative.ProfileInfo { const teamConfigApi: Partial = { api: { - profiles: { get: jest.fn() }, - secure: { securePropsForProfile: jest.fn().mockReturnValue([]) }, + profiles: { + get: jest.fn(), + getProfilePathFromName: jest.fn().mockImplementation((x) => x), + }, + secure: { + secureFields: jest.fn().mockReturnValue([]), + securePropsForProfile: jest.fn().mockReturnValue([]), + }, } as any, exists: true, }; @@ -568,6 +574,15 @@ describe("ProfilesCache", () => { expect(profile).toMatchObject(baseProfile); }); + it("fetchBaseProfile should return typeless profile up one level if it contains token value", async () => { + const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); + const profInfoMock = createProfInfoMock([]); + jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(profInfoMock); + mocked(profInfoMock.getTeamConfig().api.secure.secureFields).mockReturnValue(["sysplex1.properties.tokenValue"]); + const profile = await profCache.fetchBaseProfile("sysplex1.lpar1.zosmf"); + expect(profile).toMatchObject({ name: "sysplex1", type: "base" }); + }); + it("fetchBaseProfile should return undefined if base profile not found", async () => { const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([lpar1Profile])); diff --git a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts index dc680e275e..6daefbb4ec 100644 --- a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts +++ b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts @@ -325,17 +325,17 @@ export class ProfilesCache { public async fetchBaseProfile(profileName?: string): Promise { const mProfileInfo = await this.getProfileInfo(); const baseProfileAttrs = mProfileInfo.getDefaultProfile("base"); - const configApi = mProfileInfo.getTeamConfig().api; + const config = mProfileInfo.getTeamConfig(); if ( profileName?.includes(".") && - (baseProfileAttrs == null || !configApi.secure.securePropsForProfile(baseProfileAttrs.profName).includes("tokenValue")) + (baseProfileAttrs == null || !config.api.secure.securePropsForProfile(baseProfileAttrs.profName).includes("tokenValue")) ) { // Retrieve parent typeless profile as base profile if: // (1) The active profile name is nested (contains a period) AND // (2) No default base profile was found OR // Default base profile does not have tokenValue in secure array - const parentProfile = profileName.slice(0, profileName.lastIndexOf(".")); - return this.getProfileLoaded(parentProfile, "base", configApi.profiles.get(parentProfile)); + const parentProfile = this.getParentProfileForToken(profileName, config); + return this.getProfileLoaded(parentProfile, "base", config.api.profiles.get(parentProfile)); } else if (baseProfileAttrs == null) { return undefined; } @@ -416,6 +416,20 @@ export class ProfilesCache { return allTypes; } + private getParentProfileForToken(profileName: string, config: imperative.Config): string { + const secureProps = config.api.secure.secureFields(); + let parentProfile = profileName.slice(0, profileName.lastIndexOf(".")); + let tempProfile = profileName; + while (tempProfile.includes(".")) { + tempProfile = tempProfile.slice(0, tempProfile.lastIndexOf(".")); + if (secureProps.includes(`${config.api.profiles.getProfilePathFromName(tempProfile)}.properties.tokenValue`)) { + parentProfile = tempProfile; + break; + } + } + return parentProfile; + } + private shouldRemoveTokenFromProfile(profile: imperative.IProfileLoaded, baseProfile: imperative.IProfileLoaded): boolean { return ((baseProfile?.profile?.host || baseProfile?.profile?.port) && profile?.profile?.host && From 7898854d73f1e8aa420e2d4a7fd40d711eb7fe28 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Thu, 15 Aug 2024 23:57:16 -0400 Subject: [PATCH 10/11] Fix data set not opening when the token has expired Co-authored-by: Karan Patel <17771013+KaranP25@users.noreply.github.com> Signed-off-by: Timothy Johnson --- packages/zowe-explorer/CHANGELOG.md | 1 + .../src/trees/dataset/ZoweDatasetNode.ts | 28 +++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index bf260a6de7..253c7be22f 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -68,6 +68,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Fixed issue where creating a new team configuration file could cause Zowe Explorer to crash, resulting in all sessions disappearing from trees. [#2906](https://github.com/zowe/zowe-explorer-vscode/issues/2906) - Update Zowe SDKs to `8.0.0-next.202407232256` for technical currency. [#2994](https://github.com/zowe/zowe-explorer-vscode/pull/2994) - Addressed breaking changes from the Zowe Explorer API package.[#2952](https://github.com/zowe/zowe-explorer-vscode/issues/2952) +- Fixed data set not opening when the token has expired. [#3001](https://github.com/zowe/zowe-explorer-vscode/issues/3001) ## `3.0.0-next.202404242037` diff --git a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts index 0395b32a81..f7a0e220bf 100644 --- a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts +++ b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts @@ -228,7 +228,8 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod } // Gets the datasets from the pattern or members of the dataset and displays any thrown errors - const responses = await this.getDatasets(); + const cachedProfile = Profiles.getInstance().loadNamedProfile(this.getProfileName()); + const responses = await this.getDatasets(cachedProfile); if (responses.length === 0) { return; } @@ -256,7 +257,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod label: item.dsname, collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: this, - profile: this.getProfile(), + profile: cachedProfile, }); elementChildren[temp.label.toString()] = temp; // Creates a ZoweDatasetNode for a dataset with imperative errors @@ -266,7 +267,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: this, contextOverride: Constants.DS_FILE_ERROR_CONTEXT, - profile: this.getProfile(), + profile: cachedProfile, }); temp.errorDetails = item.error; // Save imperative error to avoid extra z/OS requests elementChildren[temp.label.toString()] = temp; @@ -277,7 +278,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: this, contextOverride: Constants.DS_MIGRATED_FILE_CONTEXT, - profile: this.getProfile(), + profile: cachedProfile, }); elementChildren[temp.label.toString()] = temp; // Creates a ZoweDatasetNode for a VSAM file @@ -296,7 +297,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: this, contextOverride: Constants.VSAM_CONTEXT, - profile: this.getProfile(), + profile: cachedProfile, }); } } else if (SharedContext.isSession(this)) { @@ -307,7 +308,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: this, encoding: cachedEncoding, - profile: this.getProfile(), + profile: cachedProfile, }); temp.command = { command: "vscode.open", title: "", arguments: [temp.resourceUri] }; elementChildren[temp.label.toString()] = temp; @@ -321,7 +322,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod parentNode: this, contextOverride: memberInvalid ? Constants.DS_FILE_ERROR_CONTEXT : undefined, encoding: cachedEncoding, - profile: this.getProfile(), + profile: cachedProfile, }); if (!memberInvalid) { temp.command = { command: "vscode.open", title: "", arguments: [temp.resourceUri] }; @@ -524,13 +525,12 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod return fileEntry.etag; } - private async getDatasets(): Promise { + private async getDatasets(profile: imperative.IProfileLoaded): Promise { ZoweLogger.trace("ZoweDatasetNode.getDatasets called."); const responses: zosfiles.IZosFilesResponse[] = []; - const cachedProfile = Profiles.getInstance().loadNamedProfile(this.getProfileName()); const options: zosfiles.IListOptions = { attributes: true, - responseTimeout: cachedProfile.profile.responseTimeout, + responseTimeout: profile.profile.responseTimeout, }; if (SharedContext.isSession(this) && this.pattern) { const dsPatterns = [ @@ -541,8 +541,8 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod .map((p) => p.trim()) ), ]; - const mvsApi = ZoweExplorerApiRegister.getMvsApi(cachedProfile); - if (!mvsApi.getSession(cachedProfile)) { + const mvsApi = ZoweExplorerApiRegister.getMvsApi(profile); + if (!mvsApi.getSession(profile)) { throw new imperative.ImperativeError({ msg: vscode.l10n.t("Profile auth error"), additionalDetails: vscode.l10n.t("Profile is not authenticated, please log in to continue"), @@ -560,10 +560,10 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod this.memberPattern = this.memberPattern.toUpperCase(); for (const memPattern of this.memberPattern.split(",")) { options.pattern = memPattern; - responses.push(await ZoweExplorerApiRegister.getMvsApi(cachedProfile).allMembers(this.label as string, options)); + responses.push(await ZoweExplorerApiRegister.getMvsApi(profile).allMembers(this.label as string, options)); } } else { - responses.push(await ZoweExplorerApiRegister.getMvsApi(cachedProfile).allMembers(this.label as string, options)); + responses.push(await ZoweExplorerApiRegister.getMvsApi(profile).allMembers(this.label as string, options)); } return responses; } From abaf0747ce32b5322e58743437ec605a402b4f23 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Thu, 15 Aug 2024 23:58:51 -0400 Subject: [PATCH 11/11] Add unit test for node profiles and migrate misc tests Signed-off-by: Timothy Johnson --- .../commands/ZoweCommandProvider.unit.test.ts | 2 +- .../configuration/Profiles.unit.test.ts | 47 ++- .../__unit__/misc/ProfilesCache.unit.test.ts | 46 --- .../__unit__/misc/ZoweNode.unit.test.ts | 290 ----------------- .../dataset/ZoweDatasetNode.unit.test.ts | 297 +++++++++++++++++- 5 files changed, 341 insertions(+), 341 deletions(-) delete mode 100644 packages/zowe-explorer/__tests__/__unit__/misc/ProfilesCache.unit.test.ts delete mode 100644 packages/zowe-explorer/__tests__/__unit__/misc/ZoweNode.unit.test.ts 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 77952117aa..629f65ef87 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts @@ -39,7 +39,7 @@ describe("ZoweCommandProvider Unit Tests", () => { }); }); -describe("ZoweCommandProvide Unit Tests - function checkCurrentProfile", () => { +describe("ZoweCommandProvider Unit Tests - function checkCurrentProfile", () => { const testNode: any = new ZoweDatasetNode({ label: "test", collapsibleState: vscode.TreeItemCollapsibleState.None, 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 c4b4430295..abbc23b843 100644 --- a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts @@ -28,7 +28,7 @@ import { } from "../../__mocks__/mockCreators/shared"; import { createDatasetSessionNode, createDatasetTree } from "../../__mocks__/mockCreators/datasets"; import { createProfileManager } from "../../__mocks__/mockCreators/profiles"; -import { imperative, Gui, ProfilesCache, ZoweTreeNode, ZoweVsCodeExtension, IZoweTree, IZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { imperative, Gui, ZoweTreeNode, ZoweVsCodeExtension, IZoweTree, IZoweTreeNode } from "@zowe/zowe-explorer-api"; import { Profiles } from "../../../src/configuration/Profiles"; import { ZoweExplorerExtender } from "../../../src/extending/ZoweExplorerExtender"; import { ZoweExplorerApiRegister } from "../../../src/extending/ZoweExplorerApiRegister"; @@ -88,7 +88,6 @@ function createGlobalMocks(): { [key: string]: any } { port: 143, }, mockProfileInstance: null as any as Profiles, - mockProfilesCache: null as any as ProfilesCache, mockConfigInstance: createConfigInstance(), mockConfigLoad: null as any as typeof imperative.Config, FileSystemProvider: { @@ -100,7 +99,6 @@ function createGlobalMocks(): { [key: string]: any } { jest.spyOn(JobFSProvider.instance, "createDirectory").mockImplementation(newMocks.FileSystemProvider.createDirectory); jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(newMocks.FileSystemProvider.createDirectory); - newMocks.mockProfilesCache = new ProfilesCache(imperative.Logger.getAppLogger()); newMocks.withProgress = jest.fn().mockImplementation((_progLocation, _callback) => { return newMocks.mockCallback; }); @@ -206,6 +204,48 @@ afterEach(() => { jest.clearAllMocks(); }); +describe("Profiles Unit Tests - Function getProfileInfo", () => { + const zoweDir = jest.requireActual("@zowe/imperative").ConfigUtils.getZoweDir(); + + beforeAll(() => { + // Disable Imperative mock to use real Config API + jest.dontMock("@zowe/imperative"); + }); + + beforeEach(() => { + // Reset module cache and re-require the Profiles API in each test + // below. This ensures that the tests cover static properties defined + // at import time in the zowe-explorer-api package. + jest.resetModules(); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it("should load profiles from both home directory and current directory", async () => { + const { ProfilesCache, ...zeApi } = await import("@zowe/zowe-explorer-api"); + Object.defineProperty(zeApi.imperative.ProfileCredentials.prototype, "isSecured", { get: () => false }); + const profilesCache = new ProfilesCache(imperative.Logger.getAppLogger(), __dirname); + const config = (await profilesCache.getProfileInfo()).getTeamConfig(); + expect(config.layers[0].path).toContain(__dirname); + expect(config.layers[1].path).toContain(__dirname); + expect(config.layers[2].path).toContain(zoweDir); + expect(config.layers[3].path).toContain(zoweDir); + expect(config.layers.map((layer) => layer.exists)).toEqual([true, true, true, true]); + }); + + it("should not load project profiles from same directory as global profiles", async () => { + const { ProfilesCache, ...zeApi } = await import("@zowe/zowe-explorer-api"); + Object.defineProperty(zeApi.imperative.ProfileCredentials.prototype, "isSecured", { get: () => false }); + const profilesCache = new ProfilesCache(imperative.Logger.getAppLogger(), zoweDir); + const config = (await profilesCache.getProfileInfo()).getTeamConfig(); + expect(config.layers[0].path).not.toContain(zoweDir); + expect(config.layers[1].path).not.toContain(zoweDir); + expect(config.layers.map((layer) => layer.exists)).toEqual([true, true, true, true]); + }); +}); + describe("Profiles Unit Test - Function createInstance", () => { const mockWorkspaceFolders = jest.fn(); @@ -220,6 +260,7 @@ describe("Profiles Unit Test - Function createInstance", () => { }); return originalVscodeMock; }); + jest.doMock("@zowe/imperative"); }); beforeEach(() => { diff --git a/packages/zowe-explorer/__tests__/__unit__/misc/ProfilesCache.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/misc/ProfilesCache.unit.test.ts deleted file mode 100644 index a9d77005ea..0000000000 --- a/packages/zowe-explorer/__tests__/__unit__/misc/ProfilesCache.unit.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * 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 { imperative, ProfilesCache } from "@zowe/zowe-explorer-api"; - -jest.mock("fs"); -jest.unmock("@zowe/imperative"); - -describe("ProfilesCache API", () => { - const zoweDir = imperative.ConfigUtils.getZoweDir(); - - beforeAll(() => { - // Disable loading credential manager in ProfileInfo API - Object.defineProperty(imperative.ProfileCredentials.prototype, "isSecured", { get: () => false }); - }); - - afterAll(() => { - jest.restoreAllMocks(); - }); - - it("should load profiles from both home directory and current directory", async () => { - const profilesCache = new ProfilesCache(imperative.Logger.getAppLogger(), __dirname); - const config = (await profilesCache.getProfileInfo()).getTeamConfig(); - expect(config.layers[0].path).toContain(__dirname); - expect(config.layers[1].path).toContain(__dirname); - expect(config.layers[2].path).toContain(zoweDir); - expect(config.layers[3].path).toContain(zoweDir); - expect(config.layers.map((layer) => layer.exists)).toEqual([true, true, true, true]); - }); - - it("should not load project profiles from same directory as global profiles", async () => { - const profilesCache = new ProfilesCache(imperative.Logger.getAppLogger(), zoweDir); - const config = (await profilesCache.getProfileInfo()).getTeamConfig(); - expect(config.layers[0].path).not.toContain(zoweDir); - expect(config.layers[1].path).not.toContain(zoweDir); - expect(config.layers.map((layer) => layer.exists)).toEqual([true, true, true, true]); - }); -}); diff --git a/packages/zowe-explorer/__tests__/__unit__/misc/ZoweNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/misc/ZoweNode.unit.test.ts deleted file mode 100644 index 8d18a3b13c..0000000000 --- a/packages/zowe-explorer/__tests__/__unit__/misc/ZoweNode.unit.test.ts +++ /dev/null @@ -1,290 +0,0 @@ -/** - * 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 * as vscode from "vscode"; -import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; -import { imperative, Sorting } from "@zowe/zowe-explorer-api"; -import { ZoweDatasetNode } from "../../../src/trees/dataset/ZoweDatasetNode"; -import { Constants } from "../../../src/configuration/Constants"; -import { Profiles } from "../../../src/configuration/Profiles"; -import { DatasetFSProvider } from "../../../src/trees/dataset/DatasetFSProvider"; - -jest.mock("vscode"); -jest.mock("@zowe/zos-files-for-zowe-sdk"); -jest.mock("Session"); - -describe("Unit Tests (Jest)", () => { - // Globals - const session = new imperative.Session({ - user: "fake", - password: "fake", - hostname: "fake", - protocol: "https", - type: "basic", - }); - const profileOne: imperative.IProfileLoaded = { - name: "profile1", - profile: {}, - type: "zosmf", - message: "", - failNotFound: false, - }; - const ProgressLocation = jest.fn().mockImplementation(() => { - return { - Notification: 15, - }; - }); - - const withProgress = jest.fn().mockImplementation((progLocation, callback) => { - return callback(); - }); - - Object.defineProperty(vscode, "ProgressLocation", { value: ProgressLocation }); - Object.defineProperty(vscode.window, "withProgress", { value: withProgress }); - - beforeEach(() => { - withProgress.mockImplementation((progLocation, callback) => { - return callback(); - }); - }); - - const showErrorMessage = jest.fn(); - Object.defineProperty(vscode.window, "showErrorMessage", { value: showErrorMessage }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - /************************************************************************************************************* - * Creates an ZoweDatasetNode and checks that its members are all initialized by the constructor - *************************************************************************************************************/ - it("Testing that the ZoweDatasetNode is defined", () => { - const testNode = new ZoweDatasetNode({ label: "BRTVS99", collapsibleState: vscode.TreeItemCollapsibleState.None, session }); - testNode.contextValue = Constants.DS_SESSION_CONTEXT; - - expect(testNode.label).toBeDefined(); - expect(testNode.collapsibleState).toBeDefined(); - expect(testNode.label).toBeDefined(); - expect(testNode.getParent()).toBeUndefined(); - expect(testNode.getSession()).toBeDefined(); - }); - - /************************************************************************************************************* - * Checks that returning an unsuccessful response results in an error being thrown and caught - *************************************************************************************************************/ - it( - "Checks that when bright.List.dataSet/allMembers() returns an unsuccessful response, " + "it returns a label of 'No data sets found'", - async () => { - Object.defineProperty(Profiles, "getInstance", { - value: jest.fn(() => { - return { - loadNamedProfile: jest.fn().mockReturnValue(profileOne), - }; - }), - }); - // Creating a rootNode - const rootNode = new ZoweDatasetNode({ - label: "root", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session, - profile: profileOne, - }); - rootNode.contextValue = Constants.DS_SESSION_CONTEXT; - rootNode.dirty = true; - const subNode = new ZoweDatasetNode({ - label: "Response Fail", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: rootNode, - profile: profileOne, - }); - jest.spyOn(subNode as any, "getDatasets").mockReturnValueOnce([ - { - success: true, - apiResponse: { - items: [], - }, - }, - ]); - subNode.dirty = true; - const response = await subNode.getChildren(); - expect(response[0].label).toBe("No data sets found"); - } - ); - - /************************************************************************************************************* - * Checks that passing a session node that is not dirty ignores the getChildren() method - *************************************************************************************************************/ - it("Checks that passing a session node that is not dirty the getChildren() method is exited early", async () => { - // Creating a rootNode - const rootNode = new ZoweDatasetNode({ - label: "root", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session, - profile: profileOne, - }); - const infoChild = new ZoweDatasetNode({ - label: "Use the search button to display data sets", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: rootNode, - profile: profileOne, - contextOverride: Constants.INFORMATION_CONTEXT, - }); - infoChild.command = { - command: "zowe.placeholderCommand", - title: "Placeholder", - }; - rootNode.contextValue = Constants.DS_SESSION_CONTEXT; - rootNode.dirty = false; - await expect(await rootNode.getChildren()).toEqual([infoChild]); - }); - - /************************************************************************************************************* - * Checks that passing a session node with no hlq ignores the getChildren() method - *************************************************************************************************************/ - it("Checks that passing a session node with no hlq the getChildren() method is exited early", async () => { - // Creating a rootNode - const rootNode = new ZoweDatasetNode({ - label: "root", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session, - profile: profileOne, - }); - const infoChild = new ZoweDatasetNode({ - label: "Use the search button to display data sets", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: rootNode, - profile: profileOne, - contextOverride: Constants.INFORMATION_CONTEXT, - }); - infoChild.command = { - command: "zowe.placeholderCommand", - title: "Placeholder", - }; - rootNode.contextValue = Constants.DS_SESSION_CONTEXT; - await expect(await rootNode.getChildren()).toEqual([infoChild]); - }); - - /************************************************************************************************************* - * Checks that when getSession() is called on a memeber it returns the proper session - *************************************************************************************************************/ - it("Checks that a member can reach its session properly", async () => { - // Creating a rootNode - const rootNode = new ZoweDatasetNode({ - label: "root", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session, - profile: profileOne, - }); - rootNode.contextValue = Constants.DS_SESSION_CONTEXT; - const subNode = new ZoweDatasetNode({ - label: Constants.DS_PDS_CONTEXT, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: rootNode, - profile: profileOne, - }); - const member = new ZoweDatasetNode({ - label: Constants.DS_MEMBER_CONTEXT, - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: subNode, - profile: profileOne, - }); - await expect(member.getSession()).toBeDefined(); - }); - /************************************************************************************************************* - * Tests that certain types can't have children - *************************************************************************************************************/ - it("Testing that certain types can't have children", async () => { - // Creating a rootNode - const rootNode = new ZoweDatasetNode({ - label: "root", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session, - profile: profileOne, - }); - rootNode.dirty = true; - rootNode.contextValue = Constants.DS_DS_CONTEXT; - expect(await rootNode.getChildren()).toHaveLength(0); - rootNode.contextValue = Constants.DS_MEMBER_CONTEXT; - expect(await rootNode.getChildren()).toHaveLength(0); - rootNode.contextValue = Constants.INFORMATION_CONTEXT; - expect(await rootNode.getChildren()).toHaveLength(0); - }); - /************************************************************************************************************* - * Tests that we shouldn't be updating children - *************************************************************************************************************/ - it("Tests that we shouldn't be updating children", async () => { - // Creating a rootNode - const rootNode = new ZoweDatasetNode({ - label: "root", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session, - profile: profileOne, - }); - rootNode.children = [ - new ZoweDatasetNode({ label: "onestep", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, session, profile: profileOne }), - ]; - rootNode.dirty = false; - rootNode.contextValue = Constants.DS_PDS_CONTEXT; - expect((await rootNode.getChildren())[0].label).toEqual("onestep"); - }); - - /************************************************************************************************************* - * Multiple member names returned - *************************************************************************************************************/ - it("Testing what happens when response has multiple members", async () => { - Object.defineProperty(Profiles, "getInstance", { - value: jest.fn(() => { - return { - loadNamedProfile: jest.fn().mockReturnValue(profileOne), - }; - }), - }); - - const getStatsMock = jest.spyOn(ZoweDatasetNode.prototype, "getStats").mockImplementation(); - - const sessionNode = { - encodingMap: {}, - getSessionNode: jest.fn(), - sort: { method: Sorting.DatasetSortOpts.Name, direction: Sorting.SortDirection.Ascending }, - } as unknown as ZoweDatasetNode; - const getSessionNodeSpy = jest.spyOn(ZoweDatasetNode.prototype, "getSessionNode").mockReturnValue(sessionNode); - // Creating a rootNode - const pds = new ZoweDatasetNode({ - label: "[root]: something", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: sessionNode, - session, - profile: profileOne, - }); - pds.dirty = true; - pds.contextValue = Constants.DS_PDS_CONTEXT; - const allMembers = jest.fn(); - allMembers.mockImplementationOnce(() => { - return { - success: true, - apiResponse: { - items: [{ member: "BADMEM\ufffd" }, { member: "GOODMEM1" }], - }, - }; - }); - jest.spyOn(DatasetFSProvider.instance, "exists").mockReturnValue(false); - jest.spyOn(DatasetFSProvider.instance, "writeFile").mockImplementation(); - jest.spyOn(DatasetFSProvider.instance, "createDirectory").mockImplementation(); - Object.defineProperty(zosfiles.List, "allMembers", { value: allMembers }); - const pdsChildren = await pds.getChildren(); - expect(pdsChildren[0].label).toEqual("BADMEM\ufffd"); - expect(pdsChildren[0].contextValue).toEqual(Constants.DS_FILE_ERROR_CONTEXT); - expect(pdsChildren[1].label).toEqual("GOODMEM1"); - expect(pdsChildren[1].contextValue).toEqual(Constants.DS_MEMBER_CONTEXT); - getSessionNodeSpy.mockRestore(); - getStatsMock.mockRestore(); - }); -}); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts index e6a8c07555..0a069db2c4 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts @@ -10,7 +10,8 @@ */ import * as vscode from "vscode"; -import { DsEntry, Gui, PdsEntry, Validation } from "@zowe/zowe-explorer-api"; +import { DsEntry, Gui, imperative, PdsEntry, Validation } from "@zowe/zowe-explorer-api"; +import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import { createSessCfgFromArgs, createInstanceOfProfile, @@ -65,6 +66,300 @@ function createGlobalMocks() { return newMocks; } +describe("ZoweDatasetNode Unit Tests", () => { + // Globals + const session = new imperative.Session({ + user: "fake", + password: "fake", + hostname: "fake", + protocol: "https", + type: "basic", + }); + const profileOne: imperative.IProfileLoaded = { + name: "profile1", + profile: {}, + type: "zosmf", + message: "", + failNotFound: false, + }; + const ProgressLocation = jest.fn().mockImplementation(() => { + return { + Notification: 15, + }; + }); + + const withProgress = jest.fn().mockImplementation((progLocation, callback) => { + return callback(); + }); + + Object.defineProperty(vscode, "ProgressLocation", { value: ProgressLocation }); + Object.defineProperty(vscode.window, "withProgress", { value: withProgress }); + + beforeEach(() => { + withProgress.mockImplementation((progLocation, callback) => { + return callback(); + }); + }); + + const showErrorMessage = jest.fn(); + Object.defineProperty(vscode.window, "showErrorMessage", { value: showErrorMessage }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + /************************************************************************************************************* + * Creates an ZoweDatasetNode and checks that its members are all initialized by the constructor + *************************************************************************************************************/ + it("Testing that the ZoweDatasetNode is defined", () => { + const testNode = new ZoweDatasetNode({ label: "BRTVS99", collapsibleState: vscode.TreeItemCollapsibleState.None, session }); + testNode.contextValue = Constants.DS_SESSION_CONTEXT; + + expect(testNode.label).toBeDefined(); + expect(testNode.collapsibleState).toBeDefined(); + expect(testNode.label).toBeDefined(); + expect(testNode.getParent()).toBeUndefined(); + expect(testNode.getSession()).toBeDefined(); + }); + + /************************************************************************************************************* + * Checks that returning an unsuccessful response results in an error being thrown and caught + *************************************************************************************************************/ + it("Checks that when List.dataSet/allMembers() returns an unsuccessful response, " + "it returns a label of 'No data sets found'", async () => { + Object.defineProperty(Profiles, "getInstance", { + value: jest.fn(() => { + return { + loadNamedProfile: jest.fn().mockReturnValue(profileOne), + }; + }), + }); + // Creating a rootNode + const rootNode = new ZoweDatasetNode({ + label: "root", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session, + profile: profileOne, + }); + rootNode.contextValue = Constants.DS_SESSION_CONTEXT; + rootNode.dirty = true; + const subNode = new ZoweDatasetNode({ + label: "Response Fail", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + parentNode: rootNode, + profile: profileOne, + }); + jest.spyOn(subNode as any, "getDatasets").mockReturnValueOnce([ + { + success: true, + apiResponse: { + items: [], + }, + }, + ]); + subNode.dirty = true; + const response = await subNode.getChildren(); + expect(response[0].label).toBe("No data sets found"); + }); + + /************************************************************************************************************* + * Checks that passing a session node that is not dirty ignores the getChildren() method + *************************************************************************************************************/ + it("Checks that passing a session node that is not dirty the getChildren() method is exited early", async () => { + // Creating a rootNode + const rootNode = new ZoweDatasetNode({ + label: "root", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session, + profile: profileOne, + }); + const infoChild = new ZoweDatasetNode({ + label: "Use the search button to display data sets", + collapsibleState: vscode.TreeItemCollapsibleState.None, + parentNode: rootNode, + profile: profileOne, + contextOverride: Constants.INFORMATION_CONTEXT, + }); + infoChild.command = { + command: "zowe.placeholderCommand", + title: "Placeholder", + }; + rootNode.contextValue = Constants.DS_SESSION_CONTEXT; + rootNode.dirty = false; + expect(await rootNode.getChildren()).toEqual([infoChild]); + }); + + /************************************************************************************************************* + * Checks that passing a session node with no hlq ignores the getChildren() method + *************************************************************************************************************/ + it("Checks that passing a session node with no hlq the getChildren() method is exited early", async () => { + // Creating a rootNode + const rootNode = new ZoweDatasetNode({ + label: "root", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session, + profile: profileOne, + }); + const infoChild = new ZoweDatasetNode({ + label: "Use the search button to display data sets", + collapsibleState: vscode.TreeItemCollapsibleState.None, + parentNode: rootNode, + profile: profileOne, + contextOverride: Constants.INFORMATION_CONTEXT, + }); + infoChild.command = { + command: "zowe.placeholderCommand", + title: "Placeholder", + }; + rootNode.contextValue = Constants.DS_SESSION_CONTEXT; + expect(await rootNode.getChildren()).toEqual([infoChild]); + }); + + /************************************************************************************************************* + * Checks that when getSession() is called on a memeber it returns the proper session + *************************************************************************************************************/ + it("Checks that a member can reach its session properly", () => { + // Creating a rootNode + const rootNode = new ZoweDatasetNode({ + label: "root", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session, + profile: profileOne, + }); + rootNode.contextValue = Constants.DS_SESSION_CONTEXT; + const subNode = new ZoweDatasetNode({ + label: Constants.DS_PDS_CONTEXT, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + parentNode: rootNode, + profile: profileOne, + }); + const member = new ZoweDatasetNode({ + label: Constants.DS_MEMBER_CONTEXT, + collapsibleState: vscode.TreeItemCollapsibleState.None, + parentNode: subNode, + profile: profileOne, + }); + expect(member.getSession()).toBeDefined(); + }); + /************************************************************************************************************* + * Tests that certain types can't have children + *************************************************************************************************************/ + it("Testing that certain types can't have children", async () => { + // Creating a rootNode + const rootNode = new ZoweDatasetNode({ + label: "root", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session, + profile: profileOne, + }); + rootNode.dirty = true; + rootNode.contextValue = Constants.DS_DS_CONTEXT; + expect(await rootNode.getChildren()).toHaveLength(0); + rootNode.contextValue = Constants.DS_MEMBER_CONTEXT; + expect(await rootNode.getChildren()).toHaveLength(0); + rootNode.contextValue = Constants.INFORMATION_CONTEXT; + expect(await rootNode.getChildren()).toHaveLength(0); + }); + /************************************************************************************************************* + * Tests that we shouldn't be updating children + *************************************************************************************************************/ + it("Tests that we shouldn't be updating children", async () => { + // Creating a rootNode + const rootNode = new ZoweDatasetNode({ + label: "root", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + session, + profile: profileOne, + }); + rootNode.children = [ + new ZoweDatasetNode({ label: "onestep", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, session, profile: profileOne }), + ]; + rootNode.dirty = false; + rootNode.contextValue = Constants.DS_PDS_CONTEXT; + expect((await rootNode.getChildren())[0].label).toEqual("onestep"); + }); + + /************************************************************************************************************* + * Multiple member names returned + *************************************************************************************************************/ + it("Testing what happens when response has multiple members", async () => { + Object.defineProperty(Profiles, "getInstance", { + value: jest.fn(() => { + return { + loadNamedProfile: jest.fn().mockReturnValue(profileOne), + }; + }), + }); + + const getStatsMock = jest.spyOn(ZoweDatasetNode.prototype, "getStats").mockImplementation(); + + const sessionNode = createDatasetSessionNode(session, profileOne); + const getSessionNodeSpy = jest.spyOn(ZoweDatasetNode.prototype, "getSessionNode").mockReturnValue(sessionNode); + // Creating a rootNode + const pds = new ZoweDatasetNode({ + label: "[root]: something", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + parentNode: sessionNode, + session, + profile: profileOne, + }); + pds.dirty = true; + pds.contextValue = Constants.DS_PDS_CONTEXT; + const allMembers = jest.fn().mockReturnValueOnce({ + success: true, + apiResponse: { + items: [{ member: "BADMEM\ufffd" }, { member: "GOODMEM1" }], + }, + }); + jest.spyOn(DatasetFSProvider.instance, "exists").mockReturnValue(false); + jest.spyOn(DatasetFSProvider.instance, "writeFile").mockImplementation(); + jest.spyOn(DatasetFSProvider.instance, "createDirectory").mockImplementation(); + Object.defineProperty(zosfiles.List, "allMembers", { value: allMembers }); + const pdsChildren = await pds.getChildren(); + expect(pdsChildren[0].label).toEqual("BADMEM\ufffd"); + expect(pdsChildren[0].contextValue).toEqual(Constants.DS_FILE_ERROR_CONTEXT); + expect(pdsChildren[1].label).toEqual("GOODMEM1"); + expect(pdsChildren[1].contextValue).toEqual(Constants.DS_MEMBER_CONTEXT); + getSessionNodeSpy.mockRestore(); + getStatsMock.mockRestore(); + }); + + /************************************************************************************************************* + * Profile properties have changed + *************************************************************************************************************/ + it("Testing what happens when profile has been updated", async () => { + Object.defineProperty(Profiles, "getInstance", { + value: jest.fn(() => { + return { + loadNamedProfile: jest.fn().mockReturnValue({ ...profileOne, profile: { encoding: "IBM-939" } }), + }; + }), + }); + + const sessionNode = createDatasetSessionNode(session, profileOne); + const pds = new ZoweDatasetNode({ + label: "TEST.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + parentNode: sessionNode, + session, + profile: profileOne, + }); + pds.dirty = true; + pds.contextValue = Constants.DS_PDS_CONTEXT; + jest.spyOn(pds as any, "getDatasets").mockReturnValueOnce([ + { + success: true, + apiResponse: { + items: [{ member: "IEFBR14" }], + }, + }, + ]); + const pdsChildren = await pds.getChildren(); + expect(pdsChildren[0].label).toEqual("IEFBR14"); + expect(pds.getProfile().profile?.encoding).toBeUndefined(); + expect(pdsChildren[0].getProfile().profile?.encoding).toBe("IBM-939"); + }); +}); + describe("ZoweDatasetNode Unit Tests - Function node.openDs()", () => { function createBlockMocks() { const session = createISession();