From 5ca75e0b1b22a92a1db188fea96fd0a8a0028822 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Wed, 28 Aug 2024 13:53:04 -0400 Subject: [PATCH 01/10] Fix logout to give base profile precedence over parent profile Signed-off-by: Timothy Johnson --- packages/zowe-explorer-api/CHANGELOG.md | 1 + .../__tests__/__unit__/profiles/ProfilesCache.unit.test.ts | 2 +- packages/zowe-explorer-api/src/profiles/ProfilesCache.ts | 7 ++----- .../zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index efee42ecb2..64d64c4d24 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -103,6 +103,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - **Breaking:** Consolidated WebView API options into a single object (`WebViewOpts` type), both for developer convenience and to support future options. - 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. +- **Breaking:** Changed behavior of the `ProfilesCache.fetchBaseProfile` method so that 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. - Renamed the `_lookup` function to `lookup` in the `BaseProvider` class and updated its access to public, allowing extenders to look up resources outside of the provider implementations. The `_lookup` function is still accessible, but now deprecated in favor of the public `lookup` function. [#3040](https://github.com/zowe/zowe-explorer-vscode/pull/3040) - **Breaking:** Removed the `MemberEntry` filesystem class, in favor of using the `DsEntry` class with `isMember` set to `true`. - Changed `TableViewProvider.setTableView` function to be asynchronous for more optimized data updates. 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 5c31c1c8b3..b2c11d6763 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 @@ -558,7 +558,7 @@ describe("ProfilesCache", () => { expect(profile).toMatchObject({ name: "lpar1", type: "base" }); }); - it("fetchBaseProfile should return typeless profile if base profile does not contain token value", async () => { + it("fetchBaseProfile should return typeless profile if base profile does not contain token type", 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"); diff --git a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts index 6daefbb4ec..2a1b574d40 100644 --- a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts +++ b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts @@ -326,14 +326,11 @@ export class ProfilesCache { const mProfileInfo = await this.getProfileInfo(); const baseProfileAttrs = mProfileInfo.getDefaultProfile("base"); const config = mProfileInfo.getTeamConfig(); - if ( - profileName?.includes(".") && - (baseProfileAttrs == null || !config.api.secure.securePropsForProfile(baseProfileAttrs.profName).includes("tokenValue")) - ) { + if (profileName?.includes(".") && (baseProfileAttrs == null || config.api.profiles.get(baseProfileAttrs.profName).tokenType == null)) { // 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 + // Default base profile does not have tokenType defined const parentProfile = this.getParentProfileForToken(profileName, config); return this.getProfileLoaded(parentProfile, "base", config.api.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 25e889212a..b2e72b89d7 100644 --- a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts +++ b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts @@ -228,8 +228,8 @@ export class ZoweVsCodeExtension { const baseProfile = await cache.fetchBaseProfile(serviceProfile.name); if (baseProfile) { const tokenType = - serviceProfile.profile.tokenType ?? baseProfile.profile.tokenType ?? + serviceProfile.profile.tokenType ?? zeRegister?.getCommonApi(serviceProfile).getTokenTypeName() ?? imperative.SessConstants.TOKEN_TYPE_APIML; const updSession = new imperative.Session({ @@ -237,7 +237,7 @@ export class ZoweVsCodeExtension { port: serviceProfile.profile.port, rejectUnauthorized: serviceProfile.profile.rejectUnauthorized, tokenType: tokenType, - tokenValue: serviceProfile.profile.tokenValue ?? baseProfile.profile.tokenValue, + tokenValue: baseProfile.profile.tokenValue ?? serviceProfile.profile.tokenValue, type: imperative.SessConstants.AUTH_TYPE_TOKEN, }); await (zeRegister?.getCommonApi(serviceProfile).logout ?? Logout.apimlLogout)(updSession); From a9b0ec863541b23cbed906b323d4f555e08989b9 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Wed, 28 Aug 2024 14:08:30 -0400 Subject: [PATCH 02/10] Fix ProfilesCache unit tests Signed-off-by: Timothy Johnson --- .../__tests__/__unit__/profiles/ProfilesCache.unit.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 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 b2c11d6763..a7c7fbfee2 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 @@ -87,12 +87,11 @@ function createProfInfoMock(profiles: Partial[]): imp const teamConfigApi: Partial = { api: { profiles: { - get: jest.fn(), + get: jest.fn().mockReturnValue({}), getProfilePathFromName: jest.fn().mockImplementation((x) => x), }, secure: { secureFields: jest.fn().mockReturnValue([]), - securePropsForProfile: jest.fn().mockReturnValue([]), }, } as any, exists: true, @@ -565,11 +564,11 @@ describe("ProfilesCache", () => { expect(profile).toMatchObject({ name: "lpar1", type: "base" }); }); - it("fetchBaseProfile should return base profile if it contains token value", async () => { + it("fetchBaseProfile should return base profile if it contains token type", 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"]); + mocked(profInfoMock.getTeamConfig().api.profiles.get).mockReturnValueOnce({ tokenType: imperative.SessConstants.TOKEN_TYPE_JWT }); const profile = await profCache.fetchBaseProfile("lpar1.zosmf"); expect(profile).toMatchObject(baseProfile); }); From 60ff77177be57f018f744cb8f24555c5d37c2661 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 30 Aug 2024 12:58:19 -0400 Subject: [PATCH 03/10] fix(fsp): Do not fetch changes during conflict; use custom context val Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/fs/BaseProvider.ts | 13 ++++++++++--- packages/zowe-explorer/package.json | 4 ++-- .../src/trees/dataset/DatasetFSProvider.ts | 4 +++- .../zowe-explorer/src/trees/uss/UssFSProvider.ts | 4 +++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/zowe-explorer-api/src/fs/BaseProvider.ts b/packages/zowe-explorer-api/src/fs/BaseProvider.ts index 4ff2bf7872..504ad3bc4c 100644 --- a/packages/zowe-explorer-api/src/fs/BaseProvider.ts +++ b/packages/zowe-explorer-api/src/fs/BaseProvider.ts @@ -27,6 +27,7 @@ export class BaseProvider { public onDidChangeFile: vscode.Event = this._onDidChangeFileEmitter.event; protected root: DirEntry; public openedUris: vscode.Uri[] = []; + public onDocClosedEventDisposable: vscode.Disposable = null; protected constructor() {} @@ -296,14 +297,19 @@ export class BaseProvider { // This event removes the "diff view" flag from the local file, // so that API calls can continue after the conflict dialog is closed. - private static onCloseEvent(provider: BaseProvider, e: vscode.TextDocument): void { + private static onCloseEvent(this: BaseProvider, e: vscode.TextDocument): void { if (e.uri.query && e.uri.scheme.startsWith("zowe-")) { const queryParams = new URLSearchParams(e.uri.query); if (queryParams.has("conflict")) { - const fsEntry = provider._lookupAsFile(e.uri, { silent: true }); + const fsEntry = this._lookupAsFile(e.uri, { silent: true }); if (fsEntry) { fsEntry.inDiffView = false; } + + if (vscode.window.visibleTextEditors.every((editor) => !editor.document.uri.query.includes("conflict=true"))) { + vscode.commands.executeCommand("setContext", "zowe.vscode-extension-for-zowe.inConflict", false); + this.onDocClosedEventDisposable.dispose(); + } } } } @@ -332,13 +338,14 @@ export class BaseProvider { // User selected "Compare", show diff with local contents and LPAR contents if (userSelection === conflictOptions[0]) { - vscode.workspace.onDidCloseTextDocument(BaseProvider.onCloseEvent.bind(this)); + await vscode.commands.executeCommand("setContext", "zowe.vscode-extension-for-zowe.inConflict", true); await vscode.commands.executeCommand( "vscode.diff", uri.with({ query: "conflict=true" }), uri.with({ query: "inDiff=true" }), `${entry.name} (Remote) ↔ ${entry.name}` ); + this.onDocClosedEventDisposable = vscode.workspace.onDidCloseTextDocument(BaseProvider.onCloseEvent.bind(this)); return ConflictViewSelection.Compare; } diff --git a/packages/zowe-explorer/package.json b/packages/zowe-explorer/package.json index 48a652fb21..071c68f431 100644 --- a/packages/zowe-explorer/package.json +++ b/packages/zowe-explorer/package.json @@ -684,12 +684,12 @@ { "command": "zowe.diff.useLocalContent", "group": "navigation@0", - "when": "resourceScheme =~ /zowe-.*/ && isInDiffEditor" + "when": "resourceScheme =~ /zowe-.*/ && isInDiffEditor && zowe.vscode-extension-for-zowe.inConflict" }, { "command": "zowe.diff.useRemoteContent", "group": "navigation@1", - "when": "resourceScheme =~ /zowe-.*/ && isInDiffEditor" + "when": "resourceScheme =~ /zowe-.*/ && isInDiffEditor && zowe.vscode-extension-for-zowe.inConflict" } ], "view/title": [ diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 13f50186e3..31054f041a 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -95,6 +95,8 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem const queryParams = new URLSearchParams(uri.query); if (queryParams.has("conflict")) { return { ...this.lookup(uri, false), permissions: vscode.FilePermission.Readonly }; + } else if (queryParams.has("inDiff")) { + return this.lookup(uri, false); } isFetching = queryParams.has("fetch") && queryParams.get("fetch") === "true"; } @@ -389,7 +391,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem const isConflict = urlQuery.has("conflict"); // we need to fetch the contents from the mainframe if the file hasn't been accessed yet - if (!file.wasAccessed || isConflict) { + if ((!file.wasAccessed && !urlQuery.has("inDiff")) || isConflict) { await this.fetchDatasetAtUri(uri, { isConflict }); if (!isConflict) { file.wasAccessed = true; diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 001b709c4f..40c48eee2b 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -67,6 +67,8 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const queryParams = new URLSearchParams(uri.query); if (queryParams.has("conflict")) { return { ...this.lookup(uri, false), permissions: vscode.FilePermission.Readonly }; + } else if (queryParams.has("inDiff")) { + return this.lookup(uri, false); } isFetching = queryParams.has("fetch") && queryParams.get("fetch") === "true"; } @@ -315,7 +317,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv // Fetch contents from the mainframe if: // - the file hasn't been accessed yet // - fetching a conflict from the remote FS - if (!file.wasAccessed || isConflict) { + if ((!file.wasAccessed && !urlQuery.has("inDiff")) || isConflict) { await this.fetchFileAtUri(uri, { isConflict }); if (!isConflict) { file.wasAccessed = true; From 4f24050c32fb82e3c3c7fae0d21f98df471252b6 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Fri, 30 Aug 2024 20:17:16 -0400 Subject: [PATCH 04/10] WIP Refactor login/logout methods to use interface Signed-off-by: Timothy Johnson --- .../src/vscode/ZoweVsCodeExtension.ts | 127 ++++++++++-------- .../src/vscode/doc/BaseProfileAuth.ts | 24 ++++ .../zowe-explorer-api/src/vscode/doc/index.ts | 1 + .../src/configuration/Profiles.ts | 30 ++++- 4 files changed, 122 insertions(+), 60 deletions(-) create mode 100644 packages/zowe-explorer-api/src/vscode/doc/BaseProfileAuth.ts diff --git a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts index b2e72b89d7..0bb19476f1 100644 --- a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts +++ b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts @@ -17,6 +17,7 @@ import * as imperative from "@zowe/imperative"; import { Gui } from "../globals/Gui"; import { PromptCredentialsOptions } from "./doc/PromptCredentials"; import { Types } from "../Types"; +import { BaseProfileAuthOptions } from "./doc"; /** * Collection of utility functions for writing Zowe Explorer VS Code extensions. @@ -94,6 +95,7 @@ export class ZoweVsCodeExtension { shouldSave = await ZoweVsCodeExtension.saveCredentials(loadProfile); } + // TODO Should we call updateProfileInCache method here? if (shouldSave) { // write changes to the file, autoStore value determines if written to file const upd = { profileName: loadProfile.name, profileType: loadProfile.type }; @@ -111,30 +113,30 @@ export class ZoweVsCodeExtension { * Trigger a login operation with the merged contents between the service profile and the base profile. * If the connection details (host:port) do not match (service vs base), the token will be stored in the service profile. * If there is no API registered for the profile type, this method defaults the login behavior to that of the APIML. - * @param serviceProfile Profile to be used for login pursposes (either the name of the IProfileLoaded instance) + * @param serviceProfile Profile to be used for login purposes (either the name of the IProfileLoaded instance) * @param loginTokenType The tokenType value for compatibility purposes * @param node The node for compatibility purposes * @param zeRegister The ZoweExplorerApiRegister instance for compatibility purposes * @param zeProfiles The Zowe Explorer "Profiles.ts" instance for compatibility purposes */ - public static async loginWithBaseProfile( - serviceProfile: string | imperative.IProfileLoaded, - loginTokenType?: string, - node?: Types.IZoweNodeType, - zeRegister?: Types.IApiRegisterClient, // ZoweExplorerApiRegister - zeProfiles?: ProfilesCache // Profiles extends ProfilesCache - ): Promise { - const cache: ProfilesCache = zeProfiles ?? ZoweVsCodeExtension.profilesCache; - if (typeof serviceProfile === "string") { - serviceProfile = await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, serviceProfile); - } + public static async loginWithBaseProfile(opts: BaseProfileAuthOptions): Promise { + const cache: ProfilesCache = opts.zeProfiles ?? ZoweVsCodeExtension.profilesCache; + const serviceProfile = + typeof opts.serviceProfile === "string" + ? await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, opts.serviceProfile) + : opts.serviceProfile; 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 primaryProfile = opts.preferBaseToken ? baseProfile : serviceProfile; + const secondaryProfile = opts.preferBaseToken ? serviceProfile : baseProfile; const tokenType = - serviceProfile.profile.tokenType ?? baseProfile.profile.tokenType ?? loginTokenType ?? imperative.SessConstants.TOKEN_TYPE_APIML; + primaryProfile.profile.tokenType ?? + secondaryProfile.profile.tokenType ?? + opts.defaultTokenType ?? + imperative.SessConstants.TOKEN_TYPE_APIML; const updSession = new imperative.Session({ hostname: serviceProfile.profile.host, port: serviceProfile.profile.port, @@ -173,7 +175,7 @@ export class ZoweVsCodeExtension { return false; } - const loginToken = await (zeRegister?.getCommonApi(serviceProfile).login ?? Login.apimlLogin)(updSession); + const loginToken = await (opts.zeRegister?.getCommonApi(serviceProfile).login ?? Login.apimlLogin)(updSession); const updBaseProfile: imperative.IProfile = { tokenType: updSession.ISession.tokenType ?? tokenType, tokenValue: loginToken, @@ -196,15 +198,8 @@ export class ZoweVsCodeExtension { } await cache.updateBaseProfileFileLogin(profileToUpdate, updBaseProfile, !connOk); - const baseIndex = cache.allProfiles.findIndex((profile) => profile.name === profileToUpdate.name); - cache.allProfiles[baseIndex] = { ...profileToUpdate, profile: { ...profileToUpdate.profile, ...updBaseProfile } }; - - if (node) { - node.setProfileToChoice({ - ...node.getProfile(), - profile: { ...node.getProfile().profile, ...updBaseProfile }, - }); - } + serviceProfile.profile = { ...serviceProfile.profile, updBaseProfile }; + await this.updateProfileInCache({ ...opts, serviceProfile }); return true; } @@ -212,47 +207,65 @@ export class ZoweVsCodeExtension { * Trigger a logout operation with the merged contents between the service profile and the base profile. * If the connection details (host:port) do not match (service vs base), the token will be removed from the service profile. * If there is no API registered for the profile type, this method defaults the logout behavior to that of the APIML. - * @param serviceProfile Profile to be used for logout pursposes (either the name of the IProfileLoaded instance) + * @param serviceProfile Profile to be used for logout purposes (either the name of the IProfileLoaded instance) * @param zeRegister The ZoweExplorerApiRegister instance for compatibility purposes * @param zeProfiles The Zowe Explorer "Profiles.ts" instance for compatibility purposes */ - public static async logoutWithBaseProfile( - serviceProfile: string | imperative.IProfileLoaded, - zeRegister?: Types.IApiRegisterClient, // ZoweExplorerApiRegister - zeProfiles?: ProfilesCache // Profiles extends ProfilesCache - ): Promise { - const cache: ProfilesCache = zeProfiles ?? ZoweVsCodeExtension.profilesCache; - if (typeof serviceProfile === "string") { - serviceProfile = await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, serviceProfile); - } - const baseProfile = await cache.fetchBaseProfile(serviceProfile.name); - if (baseProfile) { - const tokenType = - baseProfile.profile.tokenType ?? - serviceProfile.profile.tokenType ?? - zeRegister?.getCommonApi(serviceProfile).getTokenTypeName() ?? - imperative.SessConstants.TOKEN_TYPE_APIML; - const updSession = new imperative.Session({ - hostname: serviceProfile.profile.host, - port: serviceProfile.profile.port, - rejectUnauthorized: serviceProfile.profile.rejectUnauthorized, - tokenType: tokenType, - tokenValue: baseProfile.profile.tokenValue ?? serviceProfile.profile.tokenValue, - type: imperative.SessConstants.AUTH_TYPE_TOKEN, - }); - await (zeRegister?.getCommonApi(serviceProfile).logout ?? Logout.apimlLogout)(updSession); + public static async logoutWithBaseProfile(opts: BaseProfileAuthOptions): Promise { + const cache: ProfilesCache = opts.zeProfiles ?? ZoweVsCodeExtension.profilesCache; + const serviceProfile = + typeof opts.serviceProfile === "string" + ? await ZoweVsCodeExtension.getServiceProfileForAuthPurposes(cache, opts.serviceProfile) + : opts.serviceProfile; - // 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 && - !serviceProfile.name.startsWith(baseProfile.name + "."); - await cache.updateBaseProfileFileLogout(connOk ? baseProfile : serviceProfile); - return true; - } else { + const baseProfile = await cache.fetchBaseProfile(serviceProfile.name); + if (!baseProfile) { Gui.errorMessage(`Logout failed: No base profile found to remove SSO token for profile "${serviceProfile.name}"`); return false; } + const primaryProfile = opts.preferBaseToken ? baseProfile : serviceProfile; + const secondaryProfile = opts.preferBaseToken ? serviceProfile : baseProfile; + const tokenType = + primaryProfile.profile.tokenType ?? + secondaryProfile.profile.tokenType ?? + opts.defaultTokenType ?? + imperative.SessConstants.TOKEN_TYPE_APIML; + const updSession = new imperative.Session({ + hostname: serviceProfile.profile.host, + port: serviceProfile.profile.port, + rejectUnauthorized: serviceProfile.profile.rejectUnauthorized, + tokenType: tokenType, + tokenValue: primaryProfile.profile.tokenValue ?? secondaryProfile.profile.tokenValue, + type: imperative.SessConstants.AUTH_TYPE_TOKEN, + }); + await (opts.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 && + !serviceProfile.name.startsWith(baseProfile.name + "."); + await cache.updateBaseProfileFileLogout(connOk ? baseProfile : serviceProfile); + serviceProfile.profile = { ...serviceProfile.profile, tokenType: undefined, tokenValue: undefined }; + await this.updateProfileInCache({ ...opts, serviceProfile }); + return true; + } + + private static async updateProfileInCache(opts: BaseProfileAuthOptions & { serviceProfile: imperative.IProfileLoaded }): Promise { + const cache: ProfilesCache = opts.zeProfiles ?? ZoweVsCodeExtension.profilesCache; + if ((await cache.getProfileInfo()).getTeamConfig().properties.autoStore) { + await cache.refresh(); + } else { + // TODO For loginWithBaseProfile, when autoStore=false is it correct to update service profile here instead of base? + const profIndex = cache.allProfiles.findIndex((profile) => profile.name === opts.serviceProfile.name); + cache.allProfiles[profIndex] = opts.serviceProfile; + } + if (opts.profileNode) { + opts.profileNode.setProfileToChoice({ + ...opts.profileNode.getProfile(), + profile: { ...opts.profileNode.getProfile().profile, ...opts.serviceProfile.profile }, + }); + } } /** diff --git a/packages/zowe-explorer-api/src/vscode/doc/BaseProfileAuth.ts b/packages/zowe-explorer-api/src/vscode/doc/BaseProfileAuth.ts new file mode 100644 index 0000000000..c91921ac4e --- /dev/null +++ b/packages/zowe-explorer-api/src/vscode/doc/BaseProfileAuth.ts @@ -0,0 +1,24 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import * as imperative from "@zowe/imperative"; +import { Types } from "../../Types"; +import { ProfilesCache } from "../../profiles"; + +// TODO Add jsdoc to interface properties +export interface BaseProfileAuthOptions { + serviceProfile: string | imperative.IProfileLoaded; + defaultTokenType?: string; + preferBaseToken?: boolean; + profileNode?: Types.IZoweNodeType; + zeProfiles?: ProfilesCache; + zeRegister?: Types.IApiRegisterClient; +} diff --git a/packages/zowe-explorer-api/src/vscode/doc/index.ts b/packages/zowe-explorer-api/src/vscode/doc/index.ts index ad1c1f0890..be56790557 100644 --- a/packages/zowe-explorer-api/src/vscode/doc/index.ts +++ b/packages/zowe-explorer-api/src/vscode/doc/index.ts @@ -9,4 +9,5 @@ * */ +export * from "./BaseProfileAuth"; export * from "./PromptCredentials"; diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index 32047cba24..d13bc19000 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -554,6 +554,7 @@ export class Profiles extends ProfilesCache { return; // See https://github.com/zowe/zowe-explorer-vscode/issues/1827 } + // TODO Is this redundant with refresh call in ZoweVsCodeExtension.updateCredentials? const returnValue: string[] = [promptInfo.profile.user, promptInfo.profile.password, promptInfo.profile.base64EncodedAuth]; this.updateProfilesArrays(promptInfo); @@ -737,7 +738,14 @@ export class Profiles extends ProfilesCache { if (loginTokenType && !loginTokenType.startsWith(imperative.SessConstants.TOKEN_TYPE_APIML)) { loginOk = await this.loginWithRegularProfile(serviceProfile, node); } else { - loginOk = await ZoweVsCodeExtension.loginWithBaseProfile(serviceProfile, loginTokenType, node, zeInstance, this); + loginOk = await ZoweVsCodeExtension.loginWithBaseProfile({ + serviceProfile, + defaultTokenType: loginTokenType, + profileNode: node, + zeRegister: zeInstance, + zeProfiles: this, + preferBaseToken: true, + }); } if (loginOk) { Gui.showMessage( @@ -835,7 +843,14 @@ export class Profiles extends ProfilesCache { case AuthUtils.isProfileUsingBasicAuth(serviceProfile): { let loginOk = false; if (loginTokenType && loginTokenType.startsWith("apimlAuthenticationToken")) { - loginOk = await ZoweVsCodeExtension.loginWithBaseProfile(serviceProfile, loginTokenType, node, zeInstance, this); + loginOk = await ZoweVsCodeExtension.loginWithBaseProfile({ + serviceProfile, + defaultTokenType: loginTokenType, + profileNode: node, + zeRegister: zeInstance, + zeProfiles: this, + preferBaseToken: true, + }); } else { loginOk = await this.loginWithRegularProfile(serviceProfile, node); } @@ -966,7 +981,15 @@ export class Profiles extends ProfilesCache { ) { await ZoweExplorerApiRegister.getInstance().getCommonApi(serviceProfile).logout(node.getSession()); } else { - logoutOk = await ZoweVsCodeExtension.logoutWithBaseProfile(serviceProfile, ZoweExplorerApiRegister.getInstance(), this); + const zeRegister = ZoweExplorerApiRegister.getInstance(); + logoutOk = await ZoweVsCodeExtension.logoutWithBaseProfile({ + serviceProfile, + defaultTokenType: zeRegister?.getCommonApi(serviceProfile).getTokenTypeName(), + profileNode: node, + zeRegister, + zeProfiles: this, + preferBaseToken: true, + }); } if (logoutOk) { Gui.showMessage( @@ -1031,6 +1054,7 @@ export class Profiles extends ProfilesCache { session.ISession.user = creds[0]; session.ISession.password = creds[1]; await ZoweExplorerApiRegister.getInstance().getCommonApi(serviceProfile).login(session); + // TODO Should we call ZoweVsCodeExtension.updateProfileInCache method here? const profIndex = this.allProfiles.findIndex((profile) => profile.name === serviceProfile.name); this.allProfiles[profIndex] = { ...serviceProfile, profile: { ...serviceProfile, ...session } }; if (node) { From 1774640fc1e2bc8714bbe9835c0497493cd9bfe7 Mon Sep 17 00:00:00 2001 From: zFernand0 <37381190+zFernand0@users.noreply.github.com> Date: Fri, 6 Sep 2024 11:00:26 -0400 Subject: [PATCH 05/10] test: fix unit tests and start addressing TODO items :yum: Signed-off-by: zFernand0 <37381190+zFernand0@users.noreply.github.com> --- .../vscode/ZoweVsCodeExtension.unit.test.ts | 129 ++++++++++-------- .../__unit__/vscode/ui/TableView.unit.test.ts | 14 +- .../vscode/ui/TableViewProvider.unit.test.ts | 16 +-- .../vscode/ui/utils/TableBuilder.unit.test.ts | 4 +- .../zosconsole/ZosConsolePanel.unit.test.ts | 7 +- packages/zowe-explorer/l10n/bundle.l10n.json | 14 +- 6 files changed, 110 insertions(+), 74 deletions(-) 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 20cdc6bd72..68e9d49e74 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 @@ -90,50 +90,61 @@ describe("ZoweVsCodeExtension", () => { }); describe("login and logout with base profiles", () => { - const testProfile = { - host: "dummy", - port: 1234, - }; - const baseProfile = { name: "base", type: "base", profile: testProfile }; - const serviceProfile: any = { name: "service", type: "service", profile: testProfile }; - const allProfiles = [serviceProfile, baseProfile]; - const testNode: any = { - setProfileToChoice: jest.fn(), - getProfile: jest.fn().mockReturnValue(serviceProfile), - }; - const expectedSession = new Session({ - hostname: "dummy", - password: "Password", - port: 1234, - tokenType: "apimlAuthenticationToken", - type: "token", - user: "Username", - }); - const updProfile = { tokenType: "apimlAuthenticationToken", tokenValue: "tokenValue" }; - const testRegister: any = { - getCommonApi: () => ({ - login: jest.fn().mockReturnValue("tokenValue"), - logout: jest.fn(), - getTokenTypeName: () => "apimlAuthenticationToken", - }), - }; - const testCache: any = { - allProfiles, - allExternalTypes: [], - fetchBaseProfile: jest.fn(), - loadNamedProfile: jest.fn().mockReturnValue(serviceProfile), - updateBaseProfileFileLogin: jest.fn(), - updateBaseProfileFileLogout: jest.fn(), - getLoadedProfConfig: jest.fn().mockReturnValue({ profile: {} }), - getProfileInfo: jest.fn().mockReturnValue({ - isSecured: jest.fn().mockReturnValue(false), - getAllProfiles: jest.fn().mockReturnValue(allProfiles), - mergeArgsForProfile: jest.fn().mockReturnValue({ knownArgs: [] }), - }), - refresh: jest.fn(), - }; + let testProfile; + let baseProfile; + let serviceProfile: any; + let allProfiles; + let testNode; + let expectedSession; + let updProfile; + let testRegister: any; + let testCache: any; beforeEach(() => { + testProfile = { + host: "dummy", + port: 1234, + }; + baseProfile = { name: "base", type: "base", profile: testProfile }; + serviceProfile = { name: "service", type: "service", profile: testProfile }; + allProfiles = [serviceProfile, baseProfile]; + testNode = { + setProfileToChoice: jest.fn(), + getProfile: jest.fn().mockReturnValue(serviceProfile), + }; + expectedSession = new Session({ + hostname: "dummy", + password: "Password", + port: 1234, + tokenType: "apimlAuthenticationToken", + type: "token", + user: "Username", + }); + updProfile = { tokenType: "apimlAuthenticationToken", tokenValue: "tokenValue" }; + testRegister = { + getCommonApi: () => ({ + login: jest.fn().mockReturnValue("tokenValue"), + logout: jest.fn(), + getTokenTypeName: () => "apimlAuthenticationToken", + }), + }; + testCache = { + allProfiles, + allExternalTypes: [], + fetchBaseProfile: jest.fn(), + loadNamedProfile: jest.fn().mockReturnValue(serviceProfile), + updateBaseProfileFileLogin: jest.fn(), + updateBaseProfileFileLogout: jest.fn(), + getLoadedProfConfig: jest.fn().mockReturnValue({ profile: {} }), + getProfileInfo: jest.fn().mockReturnValue({ + isSecured: jest.fn().mockReturnValue(false), + getTeamConfig: jest.fn().mockReturnValue({ properties: { autoStore: true } }), + getAllProfiles: jest.fn().mockReturnValue(allProfiles), + mergeArgsForProfile: jest.fn().mockReturnValue({ knownArgs: [] }), + }), + refresh: jest.fn(), + }; + jest.spyOn(ZoweVsCodeExtension as any, "profilesCache", "get").mockReturnValue(testCache); jest.spyOn(vscode.extensions, "getExtension").mockReturnValueOnce(fakeVsce); }); @@ -141,7 +152,7 @@ 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"); + await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); expect(testCache.fetchBaseProfile).toHaveBeenCalledTimes(1); expect(testCache.updateBaseProfileFileLogin).not.toHaveBeenCalled(); expect(errorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("Login failed: No base profile found")); @@ -149,7 +160,7 @@ describe("ZoweVsCodeExtension", () => { 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"); + await ZoweVsCodeExtension.logoutWithBaseProfile({ serviceProfile: "service" }); expect(testCache.fetchBaseProfile).toHaveBeenCalledTimes(1); expect(testCache.updateBaseProfileFileLogin).not.toHaveBeenCalled(); expect(errorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("Logout failed: No base profile found")); @@ -163,7 +174,7 @@ describe("ZoweVsCodeExtension", () => { const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.loginWithBaseProfile("service"); + await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); delete testSession.ISession.user; @@ -183,7 +194,7 @@ describe("ZoweVsCodeExtension", () => { const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.logoutWithBaseProfile("service"); + await ZoweVsCodeExtension.logoutWithBaseProfile({ serviceProfile: "service" }); const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); testSession.ISession.tokenValue = "tokenValue"; @@ -207,7 +218,7 @@ describe("ZoweVsCodeExtension", () => { const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.loginWithBaseProfile("service"); + await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); delete testSession.ISession.user; @@ -232,7 +243,7 @@ describe("ZoweVsCodeExtension", () => { const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.loginWithBaseProfile("service"); + await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); delete testSession.ISession.user; @@ -270,7 +281,7 @@ describe("ZoweVsCodeExtension", () => { const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.loginWithBaseProfile("lpar.service"); + await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "lpar.service" }); const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); delete testSession.ISession.user; @@ -307,7 +318,7 @@ describe("ZoweVsCodeExtension", () => { const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.logoutWithBaseProfile("service"); + await ZoweVsCodeExtension.logoutWithBaseProfile({ serviceProfile: "service" }); const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); testSession.ISession.hostname = "service"; @@ -328,7 +339,13 @@ describe("ZoweVsCodeExtension", () => { const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.loginWithBaseProfile(serviceProfile, "apimlAuthenticationToken", testNode, testRegister, testCache); + await ZoweVsCodeExtension.loginWithBaseProfile({ + serviceProfile, + defaultTokenType: "apimlAuthenticationToken", + profileNode: testNode, + zeRegister: testRegister, + zeProfiles: testCache, + }); const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); testSession.ISession.base64EncodedAuth = "dXNlcjpwYXNz"; @@ -347,7 +364,11 @@ describe("ZoweVsCodeExtension", () => { const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); const newServiceProfile = { ...serviceProfile, profile: { ...testProfile, ...updProfile } }; - await ZoweVsCodeExtension.logoutWithBaseProfile(newServiceProfile, testRegister, testCache); + await ZoweVsCodeExtension.logoutWithBaseProfile({ + serviceProfile: newServiceProfile, + zeRegister: testRegister, + zeProfiles: testCache, + }); const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); testSession.ISession.tokenValue = "tokenValue"; @@ -373,7 +394,7 @@ describe("ZoweVsCodeExtension", () => { // case 1: User selects "user/password" for login quick pick const promptCertMock = jest.spyOn(ZoweVsCodeExtension as any, "promptCertificate").mockImplementation(); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[1]); - await ZoweVsCodeExtension.loginWithBaseProfile("service"); + await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); delete testSession.ISession.user; @@ -400,7 +421,7 @@ describe("ZoweVsCodeExtension", () => { const promptCertMock = jest .spyOn(ZoweVsCodeExtension as any, "promptCertificate") .mockRejectedValueOnce(new Error("invalid certificate")); - await expect(ZoweVsCodeExtension.loginWithBaseProfile("service")).resolves.toBe(false); + await expect(ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" })).resolves.toBe(false); expect(promptCertMock).toHaveBeenCalled(); expect(quickPickMock).toHaveBeenCalled(); }); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts index ba8722c824..10d136a585 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -201,8 +201,8 @@ describe("Table.View", () => { globalMocks.updateWebviewMock.mockResolvedValueOnce(true); const cols = [ { field: "apple", valueFormatter: (data: { value: Table.ContentTypes }) => `${data.value.toString()} apples` }, - { field: "banana", comparator: (valueA, valueB, nodeA, nodeB, isDescending) => -1, colSpan: (params) => 2 }, - { field: "orange", rowSpan: (params) => 2 }, + { field: "banana", comparator: (_valueA, _valueB, _nodeA, _nodeB, _isDescending) => -1, colSpan: (_params) => 2 }, + { field: "orange", rowSpan: (_params) => 2 }, ]; await expect(view.setColumns(cols)).resolves.toBe(true); expect((view as any).data.columns).toStrictEqual( @@ -352,7 +352,7 @@ describe("Table.View", () => { command: "multi-action", callback: { typ: "multi-row", - fn: (_view: Table.View, row: Record) => { + fn: (_view: Table.View, _row: Record) => { multiCallbackMock(); }, }, @@ -364,7 +364,7 @@ describe("Table.View", () => { command: "zero-action", callback: { typ: "single-row", - fn: (_view: Table.View, row: Table.RowInfo) => { + fn: (_view: Table.View, _row: Table.RowInfo) => { zeroCallbackMock(); }, }, @@ -428,12 +428,12 @@ describe("Table.View", () => { it("sets the columns on the internal data structure and calls updateWebview", async () => { const globalMocks = createGlobalMocks(); const mockCols = [ - { field: "name", sort: "desc", colSpan: (params) => 2, rowSpan: (params) => 2 }, + { field: "name", sort: "desc", colSpan: (_params) => 2, rowSpan: (_params) => 2 }, { field: "address", sort: "asc", - comparator: (valueA, valueB, nodeA, nodeB, isDescending) => 1, - valueFormatter: (data: { value }) => `Located at ${data.value.toString()}`, + comparator: (_valueA, _valueB, _nodeA, _nodeB, _isDescending) => 1, + valueFormatter: (data: { value }) => `Located at ${(data.value as string).toString()}`, }, ] as Table.ColumnOpts[]; const data = { title: "Table w/ cols", columns: [], rows: [] }; diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts index 96ba59fa3d..66561e94c6 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts @@ -27,7 +27,7 @@ describe("TableViewProvider", () => { }); describe("setTableView", () => { - it("sets the table to the given table view", () => { + it("sets the table to the given table view", async () => { // case 1: table did not previously exist const builder = new TableBuilder(fakeExtContext); @@ -45,29 +45,29 @@ describe("TableViewProvider", () => { { apple: 9, banana: 10, orange: 11 }, ]) .build(); - TableViewProvider.getInstance().setTableView(tableOne); + await TableViewProvider.getInstance().setTableView(tableOne); expect((TableViewProvider.getInstance() as any).tableView).toBe(tableOne); const disposeSpy = jest.spyOn(tableOne, "dispose"); // case 2: table previously existed, dispose called on old table const tableTwo = builder.options({ pagination: false }).build(); - TableViewProvider.getInstance().setTableView(tableTwo); + await TableViewProvider.getInstance().setTableView(tableTwo); expect((TableViewProvider.getInstance() as any).tableView).toBe(tableTwo); expect(disposeSpy).toHaveBeenCalled(); }); }); describe("getTableView", () => { - beforeEach(() => { - TableViewProvider.getInstance().setTableView(null); + beforeEach(async () => { + await TableViewProvider.getInstance().setTableView(null); }); it("returns null if no table view has been provided", () => { expect(TableViewProvider.getInstance().getTableView()).toBe(null); }); - it("returns a valid table view if one has been provided", () => { + it("returns a valid table view if one has been provided", async () => { expect(TableViewProvider.getInstance().getTableView()).toBe(null); const table = new TableBuilder(fakeExtContext) .isView() @@ -81,7 +81,7 @@ describe("TableViewProvider", () => { { a: 3, b: 4, c: 5 }, ]) .build(); - TableViewProvider.getInstance().setTableView(table); + await TableViewProvider.getInstance().setTableView(table); expect(TableViewProvider.getInstance().getTableView()).toBe(table); }); }); @@ -89,7 +89,7 @@ describe("TableViewProvider", () => { describe("resolveWebviewView", () => { it("correctly resolves the view and calls resolveForView on the table", async () => { const table = new TableBuilder(fakeExtContext).isView().build(); - TableViewProvider.getInstance().setTableView(table); + await TableViewProvider.getInstance().setTableView(table); const resolveForViewSpy = jest.spyOn(table, "resolveForView"); const fakeView = { onDidDispose: jest.fn(), diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts index 14d8768236..8639148f97 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts @@ -142,8 +142,8 @@ describe("TableBuilder", () => { const builder = new TableBuilder(globalMocks.context as any); const newCols: Table.ColumnOpts[] = [ { field: "cat", valueFormatter: (data: { value: Table.ContentTypes }) => `val: ${data.value.toString()}` }, - { field: "doge", filter: true, comparator: (valueA, valueB, nodeA, nodeB, isDescending) => -1, colSpan: (params) => 2 }, - { field: "parrot", sort: "asc", rowSpan: (params) => 2 }, + { field: "doge", filter: true, comparator: (_valueA, _valueB, _nodeA, _nodeB, _isDescending) => -1, colSpan: (_params) => 2 }, + { field: "parrot", sort: "asc", rowSpan: (_params) => 2 }, ]; expect((builder as any).convertColumnOpts(newCols)).toStrictEqual( newCols.map((col) => ({ diff --git a/packages/zowe-explorer/__tests__/__unit__/zosconsole/ZosConsolePanel.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/zosconsole/ZosConsolePanel.unit.test.ts index a7bba2ca5f..7260408033 100644 --- a/packages/zowe-explorer/__tests__/__unit__/zosconsole/ZosConsolePanel.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/zosconsole/ZosConsolePanel.unit.test.ts @@ -14,6 +14,11 @@ import { ZosConsoleViewProvider } from "../../../src/zosconsole/ZosConsolePanel" import { Profiles } from "../../../src/configuration/Profiles"; import * as vscode from "vscode"; +jest.mock("@zowe/zowe-explorer-api", () => ({ + ...jest.requireActual("@zowe/zowe-explorer-api"), + HTMLTemplate: jest.requireActual("../../../../zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate"), +})); + describe("ZosConsoleViewProvider", () => { function createGlobalMocks(): any { const newMocks = { @@ -42,7 +47,7 @@ describe("ZosConsoleViewProvider", () => { const globalMocks = createGlobalMocks(); const myconsole = new ZosConsoleViewProvider({} as any); myconsole.resolveWebviewView(globalMocks.testWebView, {} as any, { isCancellationRequested: false } as any); - expect(globalMocks.testWebView.webview.onDidReceiveMessage).toBeCalled(); + expect(globalMocks.testWebView.webview.onDidReceiveMessage).toHaveBeenCalled(); }); }); }); diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index b9e86d1727..14f20dab26 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -929,9 +929,19 @@ }, "Profile Type": "Profile Type", "User Name": "User Name", - "Enter the user name for the {0} connection. Leave blank to not store.": "Enter the user name for the {0} connection. Leave blank to not store.", + "Enter the user name for the {0} connection. Leave blank to not store./Profile name": { + "message": "Enter the user name for the {0} connection. Leave blank to not store.", + "comment": [ + "Profile name" + ] + }, "Password": "Password", - "Enter the password for the {0} connection. Leave blank to not store.": "Enter the password for the {0} connection. Leave blank to not store.", + "Enter the password for the {0} connection. Leave blank to not store./Profile name": { + "message": "Enter the password for the {0} connection. Leave blank to not store.", + "comment": [ + "Profile name" + ] + }, "Select the profile you want to delete": "Select the profile you want to delete", "Validating {0} Profile./The profile name": { "message": "Validating {0} Profile.", From 14dd91688cfda636438fe581b5158a275d7f7a35 Mon Sep 17 00:00:00 2001 From: zFernand0 <37381190+zFernand0@users.noreply.github.com> Date: Fri, 6 Sep 2024 11:30:10 -0400 Subject: [PATCH 06/10] chore: update changelog Signed-off-by: zFernand0 <37381190+zFernand0@users.noreply.github.com> --- packages/zowe-explorer-api/CHANGELOG.md | 2 ++ packages/zowe-explorer/CHANGELOG.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 7f4c9427b0..0c9bce72af 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t ### Bug fixes +- Fixed behavior of logout action when token is defined in both base profile and parent profile. [#3076](https://github.com/zowe/zowe-explorer-vscode/issues/3076) + ## `3.0.0-next.202408301858` ### New features and enhancements diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 034421782c..ad0859f85a 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen ### Bug fixes +- Fixed behavior of logout action when token is defined in both base profile and parent profile. [#3076](https://github.com/zowe/zowe-explorer-vscode/issues/3076) + ## `3.0.0-next.202408301858` ### New features and enhancements From bec09330cb2b10fe7689b8a50cc0da956f71fbad Mon Sep 17 00:00:00 2001 From: zFernand0 <37381190+zFernand0@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:35:16 -0400 Subject: [PATCH 07/10] tests: add more unit tests around login Signed-off-by: zFernand0 <37381190+zFernand0@users.noreply.github.com> --- .../vscode/ZoweVsCodeExtension.unit.test.ts | 79 ++++++++++++++++--- .../src/vscode/doc/BaseProfileAuth.ts | 32 +++++++- 2 files changed, 97 insertions(+), 14 deletions(-) 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 68e9d49e74..ffe4959eab 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 @@ -11,9 +11,7 @@ import * as vscode from "vscode"; import { Gui } from "../../../src/globals/Gui"; -import { Session } from "@zowe/imperative"; -import { PromptCredentialsOptions, ZoweVsCodeExtension } from "../../../src/vscode"; -import { ProfilesCache, Types } from "../../../src"; +import { PromptCredentialsOptions, ZoweVsCodeExtension, ProfilesCache, Types } from "../../../src"; import { Login, Logout } from "@zowe/core-for-zowe-sdk"; import * as imperative from "@zowe/imperative"; @@ -112,7 +110,7 @@ describe("ZoweVsCodeExtension", () => { setProfileToChoice: jest.fn(), getProfile: jest.fn().mockReturnValue(serviceProfile), }; - expectedSession = new Session({ + expectedSession = new imperative.Session({ hostname: "dummy", password: "Password", port: 1234, @@ -136,7 +134,11 @@ describe("ZoweVsCodeExtension", () => { updateBaseProfileFileLogin: jest.fn(), updateBaseProfileFileLogout: jest.fn(), getLoadedProfConfig: jest.fn().mockReturnValue({ profile: {} }), + getMergedAttrs: (ProfilesCache.prototype as any).getMergedAttrs, + getProfileLoaded: (ProfilesCache.prototype as any).getProfileLoaded, getProfileInfo: jest.fn().mockReturnValue({ + ...imperative.ProfileInfo.prototype, + getDefaultProfile: jest.fn().mockReturnValue({ ...baseProfile, profName: baseProfile.name, profType: baseProfile.type }), isSecured: jest.fn().mockReturnValue(false), getTeamConfig: jest.fn().mockReturnValue({ properties: { autoStore: true } }), getAllProfiles: jest.fn().mockReturnValue(allProfiles), @@ -176,7 +178,7 @@ describe("ZoweVsCodeExtension", () => { const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; testSession.ISession.base64EncodedAuth = "dXNlcjpwYXNz"; @@ -196,7 +198,7 @@ describe("ZoweVsCodeExtension", () => { const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); await ZoweVsCodeExtension.logoutWithBaseProfile({ serviceProfile: "service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); testSession.ISession.tokenValue = "tokenValue"; delete testSession.ISession.base64EncodedAuth; delete testSession.ISession.user; @@ -220,7 +222,7 @@ describe("ZoweVsCodeExtension", () => { const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; testSession.ISession.hostname = "service"; @@ -245,7 +247,7 @@ describe("ZoweVsCodeExtension", () => { const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; testSession.ISession.hostname = "service"; @@ -283,7 +285,7 @@ describe("ZoweVsCodeExtension", () => { const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "lpar.service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; testSession.ISession.hostname = "dummy"; @@ -320,7 +322,7 @@ describe("ZoweVsCodeExtension", () => { const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); await ZoweVsCodeExtension.logoutWithBaseProfile({ serviceProfile: "service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); testSession.ISession.hostname = "service"; testSession.ISession.tokenValue = "tokenValue"; delete testSession.ISession.base64EncodedAuth; @@ -347,7 +349,7 @@ describe("ZoweVsCodeExtension", () => { zeProfiles: testCache, }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); testSession.ISession.base64EncodedAuth = "dXNlcjpwYXNz"; expect(loginSpy).not.toHaveBeenCalled(); @@ -370,7 +372,7 @@ describe("ZoweVsCodeExtension", () => { zeProfiles: testCache, }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); testSession.ISession.tokenValue = "tokenValue"; delete testSession.ISession.base64EncodedAuth; delete testSession.ISession.user; @@ -396,7 +398,7 @@ describe("ZoweVsCodeExtension", () => { const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[1]); await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); - const testSession = new Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; delete testSession.ISession.base64EncodedAuth; @@ -425,6 +427,57 @@ describe("ZoweVsCodeExtension", () => { expect(promptCertMock).toHaveBeenCalled(); expect(quickPickMock).toHaveBeenCalled(); }); + + it("should not login if the user cancels the operation when selecting the authentication method", async () => { + const baseProfileLoaded = testCache.getProfileLoaded(baseProfile.name, baseProfile.type, baseProfile.profile); + testCache.fetchBaseProfile.mockResolvedValue(baseProfileLoaded); + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation(() => undefined as any); + const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + serviceProfile: serviceProfile, + defaultTokenType: "apimlAuthenticationToken", + profileNode: testNode, + zeRegister: testRegister, + zeProfiles: testCache, + }); + + expect(didLogin).toBeFalsy(); + quickPickMock.mockRestore(); + }); + + it("should prefer base profile token type even if the user does not provide credentials to login with ", async () => { + const serviceProfileLoaded = testCache.getProfileLoaded(serviceProfile.name, serviceProfile.type, serviceProfile.profile); + serviceProfileLoaded.profile.tokenType = "serviceProfileTokenType"; + const baseProfileLoaded = testCache.getProfileLoaded(baseProfile.name, baseProfile.type, baseProfile.profile); + baseProfileLoaded.profile.tokenType = "baseProfileTokenType"; + testCache.fetchBaseProfile.mockResolvedValue(baseProfileLoaded); + + const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(undefined); + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); + const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + serviceProfile: serviceProfileLoaded, + defaultTokenType: "apimlAuthenticationToken", + profileNode: testNode, + zeRegister: testRegister, + zeProfiles: testCache, + preferBaseToken: true, // force to use base profile to login + }); + + const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + testSession.ISession.base64EncodedAuth = "VXNlcm5hbWU6UGFzc3dvcmQ="; + delete testSession.ISession.user; + delete testSession.ISession.password; + expect(testSpy).toHaveBeenCalledWith({ + rePrompt: true, + session: { ...testSession.ISession, tokenType: "baseProfileTokenType" }, + }); + expect(didLogin).toBeFalsy(); + quickPickMock.mockRestore(); + }); + + it("should prefer base profile when it exists and it has tokenValue in secure array", async () => {}); + it("should prefer base profile when it exists, it does not have tokenValue in its secure array, and service profile is flat", async () => {}); + it("should prefer parent profile when base profile does not exist and service profile is nested", () => {}); + it("should cancel the operation if the base profile does not exist and service profile is flat", () => {}); }); describe("updateCredentials", () => { const promptCredsOptions: PromptCredentialsOptions.ComplexOptions = { diff --git a/packages/zowe-explorer-api/src/vscode/doc/BaseProfileAuth.ts b/packages/zowe-explorer-api/src/vscode/doc/BaseProfileAuth.ts index c91921ac4e..72a520b2a7 100644 --- a/packages/zowe-explorer-api/src/vscode/doc/BaseProfileAuth.ts +++ b/packages/zowe-explorer-api/src/vscode/doc/BaseProfileAuth.ts @@ -13,12 +13,42 @@ import * as imperative from "@zowe/imperative"; import { Types } from "../../Types"; import { ProfilesCache } from "../../profiles"; -// TODO Add jsdoc to interface properties +/** + * Interface containing the properties used for centralized auth operations (e.g. login/logout) + */ export interface BaseProfileAuthOptions { + /** + * The service profile used to perform the auth operation + * If the specified value is a string, we use it as the profile name. Note: Nested profiles are allowed here, e.g. lpar.service. + * If the specified value is a loaded profile, it should contain all properties already merged by the ProfileInfo APIs + */ serviceProfile: string | imperative.IProfileLoaded; + + /** + * The type of token to use as a fallback whenever a token type cannot be identified from the service, parent, or base profiles + * For example: "apimlAuthenticationToken" + */ defaultTokenType?: string; + + /** + * Indicates whether the auth operation should give precedence to the base profile in cases where there are conflicts. + * You may use this only if you need to override the default behavior. + * For details on the default behavior, see the flowchart in: https://github.com/zowe/zowe-explorer-vscode/issues/2264#issuecomment-2236914511 + */ preferBaseToken?: boolean; + + /** + * The node used for this auth operation which will need to be explicitly updated/refresh after a successful auth operation. + */ profileNode?: Types.IZoweNodeType; + + /** + * The instance of the ProfilesCache used to perform a cache refresh after a successful auth operation. + */ zeProfiles?: ProfilesCache; + + /** + * The instance of the API Register from which we will get the auth method provided by extenders (if available). + */ zeRegister?: Types.IApiRegisterClient; } From ea82408ab65189de8d3fe39af6993470b176654c Mon Sep 17 00:00:00 2001 From: zFernand0 <37381190+zFernand0@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:57:40 -0400 Subject: [PATCH 08/10] fix: Issue where osLoc is not specified, and the updProfile being defined in the IProfile Signed-off-by: zFernand0 <37381190+zFernand0@users.noreply.github.com> --- .../vscode/ZoweVsCodeExtension.unit.test.ts | 48 +++++++++++++++---- .../src/profiles/ProfilesCache.ts | 2 +- .../src/vscode/ZoweVsCodeExtension.ts | 4 +- 3 files changed, 43 insertions(+), 11 deletions(-) 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 ffe4959eab..c5b292d1a5 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 @@ -97,6 +97,7 @@ describe("ZoweVsCodeExtension", () => { let updProfile; let testRegister: any; let testCache: any; + let getTeamConfig: any; beforeEach(() => { testProfile = { @@ -126,7 +127,9 @@ describe("ZoweVsCodeExtension", () => { getTokenTypeName: () => "apimlAuthenticationToken", }), }; + getTeamConfig = jest.fn().mockReturnValue({ properties: { autoStore: true } }); testCache = { + ...ProfilesCache.prototype, allProfiles, allExternalTypes: [], fetchBaseProfile: jest.fn(), @@ -140,11 +143,12 @@ describe("ZoweVsCodeExtension", () => { ...imperative.ProfileInfo.prototype, getDefaultProfile: jest.fn().mockReturnValue({ ...baseProfile, profName: baseProfile.name, profType: baseProfile.type }), isSecured: jest.fn().mockReturnValue(false), - getTeamConfig: jest.fn().mockReturnValue({ properties: { autoStore: true } }), + getTeamConfig, getAllProfiles: jest.fn().mockReturnValue(allProfiles), mergeArgsForProfile: jest.fn().mockReturnValue({ knownArgs: [] }), }), refresh: jest.fn(), + getAllProfileTypes: (ProfilesCache.prototype as any).getAllProfileTypes, }; jest.spyOn(ZoweVsCodeExtension as any, "profilesCache", "get").mockReturnValue(testCache); @@ -446,9 +450,9 @@ describe("ZoweVsCodeExtension", () => { it("should prefer base profile token type even if the user does not provide credentials to login with ", async () => { const serviceProfileLoaded = testCache.getProfileLoaded(serviceProfile.name, serviceProfile.type, serviceProfile.profile); - serviceProfileLoaded.profile.tokenType = "serviceProfileTokenType"; + serviceProfileLoaded.profile.tokenType = "SERVICE"; const baseProfileLoaded = testCache.getProfileLoaded(baseProfile.name, baseProfile.type, baseProfile.profile); - baseProfileLoaded.profile.tokenType = "baseProfileTokenType"; + baseProfileLoaded.profile.tokenType = "BASE"; testCache.fetchBaseProfile.mockResolvedValue(baseProfileLoaded); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(undefined); @@ -468,16 +472,42 @@ describe("ZoweVsCodeExtension", () => { delete testSession.ISession.password; expect(testSpy).toHaveBeenCalledWith({ rePrompt: true, - session: { ...testSession.ISession, tokenType: "baseProfileTokenType" }, + session: { ...testSession.ISession, tokenType: "BASE" }, }); expect(didLogin).toBeFalsy(); quickPickMock.mockRestore(); }); - it("should prefer base profile when it exists and it has tokenValue in secure array", async () => {}); - it("should prefer base profile when it exists, it does not have tokenValue in its secure array, and service profile is flat", async () => {}); - it("should prefer parent profile when base profile does not exist and service profile is nested", () => {}); - it("should cancel the operation if the base profile does not exist and service profile is flat", () => {}); + it("should update the cache when autoStore is false after a successful login operation", async () => { + const serviceProfileLoaded = testCache.getProfileLoaded(serviceProfile.name, serviceProfile.type, serviceProfile.profile); + serviceProfileLoaded.profile = { ...testProfile, tokenType: "SERVICE" }; + const baseProfileLoaded = testCache.getProfileLoaded(baseProfile.name, baseProfile.type, baseProfile.profile); + baseProfileLoaded.profile = { ...testProfile, tokenType: "BASE" }; + testCache.fetchBaseProfile.mockResolvedValue(baseProfileLoaded); + + getTeamConfig.mockReturnValue({ exists: true, properties: { autoStore: false } }); + + jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["user", "pass"]); + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); + const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + serviceProfile: serviceProfileLoaded, + defaultTokenType: "apimlAuthenticationToken", + profileNode: testNode, + zeRegister: testRegister, + zeProfiles: testCache, + }); + + const expectedProfile = { ...serviceProfileLoaded, profile: { ...testProfile, tokenType: "SERVICE", tokenValue: "tokenValue" } }; + expect(didLogin).toBeTruthy(); + expect(testCache.allProfiles).toEqual([expectedProfile, baseProfile]); + quickPickMock.mockRestore(); + }); + + // TODO: e2e test the following scenarios + // it("should prefer base profile when it exists and it has tokenValue in secure array", async () => {}); + // it("should prefer base profile when it exists, it does not have tokenValue in its secure array, and service profile is flat", async () => {}); + // it("should prefer parent profile when base profile does not exist and service profile is nested", () => {}); + // it("should cancel the operation if the base profile does not exist and service profile is flat", () => {}); }); describe("updateCredentials", () => { const promptCredsOptions: PromptCredentialsOptions.ComplexOptions = { @@ -509,7 +539,7 @@ describe("ZoweVsCodeExtension", () => { expect(mockUpdateProperty).toHaveBeenCalledTimes(2); }); - it("should update user and password as secure fields with reprompt", async () => { + it("should update user and password as secure fields with rePrompt", async () => { const mockUpdateProperty = jest.fn(); jest.spyOn(ZoweVsCodeExtension as any, "profilesCache", "get").mockReturnValue({ getLoadedProfConfig: jest.fn().mockReturnValue({ diff --git a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts index 669d48709e..0799e40096 100644 --- a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts +++ b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts @@ -155,7 +155,7 @@ export class ProfilesCache { for (const type of allTypes) { const tmpAllProfiles: imperative.IProfileLoaded[] = []; // Step 1: Get all profiles for each registered type - const profilesForType = mProfileInfo.getAllProfiles(type).filter((temp) => temp.profLoc.osLoc.length !== 0); + const profilesForType = mProfileInfo.getAllProfiles(type).filter((temp) => temp.profLoc.osLoc?.length > 0); if (profilesForType && profilesForType.length > 0) { for (const prof of profilesForType) { // Step 2: Merge args for each profile diff --git a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts index 0bb19476f1..e4b0ee5ac3 100644 --- a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts +++ b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts @@ -198,7 +198,7 @@ export class ZoweVsCodeExtension { } await cache.updateBaseProfileFileLogin(profileToUpdate, updBaseProfile, !connOk); - serviceProfile.profile = { ...serviceProfile.profile, updBaseProfile }; + serviceProfile.profile = { ...serviceProfile.profile, ...updBaseProfile }; await this.updateProfileInCache({ ...opts, serviceProfile }); return true; } @@ -257,6 +257,8 @@ export class ZoweVsCodeExtension { await cache.refresh(); } else { // TODO For loginWithBaseProfile, when autoStore=false is it correct to update service profile here instead of base? + // zFernand0: Yes, it is fine to update the service profile in the cache to avoid impacting other in memory credentials. + // Note: It should be expected that nested profiles within this service profile will have their credentials updated. const profIndex = cache.allProfiles.findIndex((profile) => profile.name === opts.serviceProfile.name); cache.allProfiles[profIndex] = opts.serviceProfile; } From b82f83ea38af9e679882e2c4dea6f44dd4aaab14 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Mon, 16 Sep 2024 12:07:31 -0400 Subject: [PATCH 09/10] Add e2e unit tests for fetching base profiles Signed-off-by: Timothy Johnson --- .../vscode/ZoweVsCodeExtension.unit.test.ts | 459 ++++++++++++------ .../src/vscode/ZoweVsCodeExtension.ts | 6 +- .../src/configuration/Profiles.ts | 2 - 3 files changed, 312 insertions(+), 155 deletions(-) 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 c5b292d1a5..da210b70ed 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 @@ -16,8 +16,9 @@ import { Login, Logout } from "@zowe/core-for-zowe-sdk"; import * as imperative from "@zowe/imperative"; describe("ZoweVsCodeExtension", () => { + const fakeLogger = { debug: jest.fn() }; const fakeVsce = { - exports: "zowe", + exports: undefined, packageJSON: { version: "1.0.1" }, } as vscode.Extension; @@ -88,93 +89,122 @@ describe("ZoweVsCodeExtension", () => { }); describe("login and logout with base profiles", () => { - let testProfile; - let baseProfile; - let serviceProfile: any; - let allProfiles; - let testNode; - let expectedSession; - let updProfile; - let testRegister: any; - let testCache: any; - let getTeamConfig: any; + let blockMocks: ReturnType; - beforeEach(() => { - testProfile = { + function createBlockMocks() { + const testProfile: imperative.IProfile = { host: "dummy", port: 1234, }; - baseProfile = { name: "base", type: "base", profile: testProfile }; - serviceProfile = { name: "service", type: "service", profile: testProfile }; - allProfiles = [serviceProfile, baseProfile]; - testNode = { - setProfileToChoice: jest.fn(), - getProfile: jest.fn().mockReturnValue(serviceProfile), + const baseProfile: imperative.IProfileLoaded = { + failNotFound: false, + message: "", + name: "base", + type: "base", + profile: { ...testProfile }, }; - expectedSession = new imperative.Session({ - hostname: "dummy", - password: "Password", - port: 1234, - tokenType: "apimlAuthenticationToken", - type: "token", - user: "Username", - }); - updProfile = { tokenType: "apimlAuthenticationToken", tokenValue: "tokenValue" }; - testRegister = { - getCommonApi: () => ({ - login: jest.fn().mockReturnValue("tokenValue"), - logout: jest.fn(), - getTokenTypeName: () => "apimlAuthenticationToken", - }), + const serviceProfile: imperative.IProfileLoaded = { + failNotFound: false, + message: "", + name: "service", + type: "service", + profile: { ...testProfile }, }; - getTeamConfig = jest.fn().mockReturnValue({ properties: { autoStore: true } }); - testCache = { - ...ProfilesCache.prototype, - allProfiles, - allExternalTypes: [], - fetchBaseProfile: jest.fn(), - loadNamedProfile: jest.fn().mockReturnValue(serviceProfile), - updateBaseProfileFileLogin: jest.fn(), - updateBaseProfileFileLogout: jest.fn(), - getLoadedProfConfig: jest.fn().mockReturnValue({ profile: {} }), - getMergedAttrs: (ProfilesCache.prototype as any).getMergedAttrs, - getProfileLoaded: (ProfilesCache.prototype as any).getProfileLoaded, - getProfileInfo: jest.fn().mockReturnValue({ - ...imperative.ProfileInfo.prototype, - getDefaultProfile: jest.fn().mockReturnValue({ ...baseProfile, profName: baseProfile.name, profType: baseProfile.type }), - isSecured: jest.fn().mockReturnValue(false), - getTeamConfig, - getAllProfiles: jest.fn().mockReturnValue(allProfiles), - mergeArgsForProfile: jest.fn().mockReturnValue({ knownArgs: [] }), + const testCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); + const testProfInfo = new imperative.ProfileInfo("zowe"); + const configLayer: imperative.IConfigLayer = { + exists: true, + path: "zowe.config.json", + properties: { + ...imperative.Config.empty(), + profiles: { + service: { + type: "service", + properties: testProfile, + }, + base: { + type: "base", + properties: testProfile, + }, + }, + }, + global: false, + user: true, + }; + const testConfig = new (imperative.Config as any)(); + Object.assign(testConfig, { + ...new (imperative.Config as any)(), + layerActive: jest.fn().mockReturnValue(configLayer), + layerMerge: jest.fn().mockReturnValue(configLayer.properties), + mLayers: [configLayer], + }); + Object.assign(testProfInfo, { + getTeamConfig: jest.fn().mockReturnValue(testConfig), + loadSchema: jest.fn().mockReturnValue({}), + mLoadedConfig: testConfig, + mProfileSchemaCache: new Map(), + readProfilesFromDisk: jest.fn(), + }); + jest.spyOn(testCache, "getProfileInfo").mockResolvedValue(testProfInfo); + + return { + testProfile, + baseProfile, + serviceProfile, + testNode: { + setProfileToChoice: jest.fn(), + getProfile: jest.fn().mockReturnValue(serviceProfile), + }, + expectedSession: new imperative.Session({ + hostname: "dummy", + password: "Password", + port: 1234, + tokenType: "apimlAuthenticationToken", + type: "token", + user: "Username", }), - refresh: jest.fn(), - getAllProfileTypes: (ProfilesCache.prototype as any).getAllProfileTypes, + updProfile: { tokenType: "apimlAuthenticationToken", tokenValue: "tokenValue" }, + testRegister: { + getCommonApi: () => ({ + login: jest.fn().mockReturnValue("tokenValue"), + logout: jest.fn(), + getTokenTypeName: () => "apimlAuthenticationToken", + }), + }, + configLayer, + testCache, }; + } - jest.spyOn(ZoweVsCodeExtension as any, "profilesCache", "get").mockReturnValue(testCache); + beforeEach(() => { + blockMocks = createBlockMocks(); + jest.spyOn(ZoweVsCodeExtension as any, "profilesCache", "get").mockReturnValue(blockMocks.testCache); jest.spyOn(vscode.extensions, "getExtension").mockReturnValueOnce(fakeVsce); }); it("should not login if the base profile cannot be fetched", async () => { const errorMessageSpy = jest.spyOn(Gui, "errorMessage"); - testCache.fetchBaseProfile.mockResolvedValue(null); + const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(undefined); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); - expect(testCache.fetchBaseProfile).toHaveBeenCalledTimes(1); - expect(testCache.updateBaseProfileFileLogin).not.toHaveBeenCalled(); + expect(fetchBaseProfileSpy).toHaveBeenCalledTimes(1); + expect(updateBaseProfileFileLoginSpy).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); + const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(undefined); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); await ZoweVsCodeExtension.logoutWithBaseProfile({ serviceProfile: "service" }); - expect(testCache.fetchBaseProfile).toHaveBeenCalledTimes(1); - expect(testCache.updateBaseProfileFileLogin).not.toHaveBeenCalled(); + expect(fetchBaseProfileSpy).toHaveBeenCalledTimes(1); + expect(updateBaseProfileFileLoginSpy).not.toHaveBeenCalled(); expect(errorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("Logout failed: No base profile found")); }); describe("user and password chosen", () => { it("should login using the base profile given a simple profile name", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["user", "pass"]); const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); @@ -182,43 +212,48 @@ describe("ZoweVsCodeExtension", () => { const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); - const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; testSession.ISession.base64EncodedAuth = "dXNlcjpwYXNz"; testSession.ISession.storeCookie = false; expect(loginSpy).toHaveBeenCalledWith(testSession); - expect(testSpy).toHaveBeenCalledWith(testCache, "service"); + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "service"); expect(quickPickMock).toHaveBeenCalled(); - expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith(baseProfile, updProfile, false); + expect(updateBaseProfileFileLoginSpy).toHaveBeenCalledWith(blockMocks.baseProfile, blockMocks.updProfile, false); }); it("should logout using the base profile given a simple profile name", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const updateBaseProfileFileLogoutSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogout"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); - testSpy.mockResolvedValue({ ...serviceProfile, profile: { ...testProfile, ...updProfile } }); + testSpy.mockResolvedValue({ ...blockMocks.serviceProfile, profile: { ...blockMocks.testProfile, ...blockMocks.updProfile } }); const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); await ZoweVsCodeExtension.logoutWithBaseProfile({ serviceProfile: "service" }); - const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); testSession.ISession.tokenValue = "tokenValue"; delete testSession.ISession.base64EncodedAuth; delete testSession.ISession.user; delete testSession.ISession.password; expect(logoutSpy).toHaveBeenCalledWith(testSession); - expect(testSpy).toHaveBeenCalledWith(testCache, "service"); - expect(testCache.updateBaseProfileFileLogout).toHaveBeenCalledWith(baseProfile); + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "service"); + expect(updateBaseProfileFileLogoutSpy).toHaveBeenCalledWith(blockMocks.baseProfile); quickPickMock.mockRestore(); }); it("should login using the base profile if the base profile does not have a tokenType stored", async () => { - const tempBaseProfile = JSON.parse(JSON.stringify(baseProfile)); + const tempBaseProfile = JSON.parse(JSON.stringify(blockMocks.baseProfile)); tempBaseProfile.profile.tokenType = undefined; - testCache.fetchBaseProfile.mockResolvedValue(tempBaseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(tempBaseProfile); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); - const newServiceProfile = { ...serviceProfile, profile: { ...testProfile, tokenValue: "tokenValue", host: "service" } }; + const newServiceProfile = { + ...blockMocks.serviceProfile, + profile: { ...blockMocks.testProfile, tokenValue: "tokenValue", host: "service" }, + }; testSpy.mockResolvedValue(newServiceProfile); jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["user", "pass"]); const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); @@ -226,7 +261,7 @@ describe("ZoweVsCodeExtension", () => { const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); - const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; testSession.ISession.hostname = "service"; @@ -234,16 +269,20 @@ describe("ZoweVsCodeExtension", () => { testSession.ISession.storeCookie = false; expect(loginSpy).toHaveBeenCalledWith(testSession); - expect(testSpy).toHaveBeenCalledWith(testCache, "service"); - expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith(tempBaseProfile, updProfile, false); + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "service"); + expect(updateBaseProfileFileLoginSpy).toHaveBeenCalledWith(tempBaseProfile, blockMocks.updProfile, false); quickPickMock.mockRestore(); }); it("should login using the service profile given a simple profile name", async () => { - const tempBaseProfile = JSON.parse(JSON.stringify(baseProfile)); + const tempBaseProfile = JSON.parse(JSON.stringify(blockMocks.baseProfile)); tempBaseProfile.profile.tokenType = "some-dummy-token-type"; - testCache.fetchBaseProfile.mockResolvedValue(tempBaseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(tempBaseProfile); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); - const newServiceProfile = { ...serviceProfile, profile: { ...testProfile, tokenValue: "tokenValue", host: "service" } }; + const newServiceProfile = { + ...blockMocks.serviceProfile, + profile: { ...blockMocks.testProfile, tokenValue: "tokenValue", host: "service" }, + }; testSpy.mockResolvedValue(newServiceProfile); jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["user", "pass"]); const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); @@ -251,7 +290,7 @@ describe("ZoweVsCodeExtension", () => { const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); - const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; testSession.ISession.hostname = "service"; @@ -260,8 +299,8 @@ describe("ZoweVsCodeExtension", () => { testSession.ISession.storeCookie = false; expect(loginSpy).toHaveBeenCalledWith(testSession); - expect(testSpy).toHaveBeenCalledWith(testCache, "service"); - expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith( + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "service"); + expect(updateBaseProfileFileLoginSpy).toHaveBeenCalledWith( newServiceProfile, { tokenType: tempBaseProfile.profile.tokenType, @@ -272,24 +311,36 @@ describe("ZoweVsCodeExtension", () => { quickPickMock.mockRestore(); }); it("should login using the parent profile given a nested profile name", async () => { - const tempBaseProfile = JSON.parse(JSON.stringify(baseProfile)); + const tempBaseProfile = JSON.parse(JSON.stringify(blockMocks.baseProfile)); tempBaseProfile.name = "lpar"; tempBaseProfile.profile.tokenType = "some-dummy-token-type"; - testCache.fetchBaseProfile.mockResolvedValue(tempBaseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(tempBaseProfile); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); const newServiceProfile = { - ...serviceProfile, + ...blockMocks.serviceProfile, name: "lpar.service", - profile: { ...testProfile, tokenValue: "tokenValue", host: "dummy" }, + profile: { ...blockMocks.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"); + blockMocks.configLayer.properties.profiles = { + lpar: { + profiles: { + service: { + type: "service", + properties: [], + }, + }, + properties: [], + }, + }; const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "lpar.service" }); - const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; testSession.ISession.hostname = "dummy"; @@ -298,13 +349,13 @@ describe("ZoweVsCodeExtension", () => { testSession.ISession.storeCookie = false; expect(loginSpy).toHaveBeenCalledWith(testSession); - expect(testSpy).toHaveBeenCalledWith(testCache, "lpar.service"); - expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith( + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "lpar.service"); + expect(updateBaseProfileFileLoginSpy).toHaveBeenCalledWith( { - name: tempBaseProfile.name, + ...tempBaseProfile, type: null, profile: { - ...serviceProfile.profile, + ...blockMocks.serviceProfile.profile, tokenType: tempBaseProfile.profile.tokenType, }, }, @@ -317,16 +368,20 @@ describe("ZoweVsCodeExtension", () => { quickPickMock.mockRestore(); }); it("should logout using the service profile given a simple profile name", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const updateBaseProfileFileLogoutSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogout"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); - const newServiceProfile = { ...serviceProfile, profile: { ...testProfile, ...updProfile, host: "service" } }; + const newServiceProfile = { + ...blockMocks.serviceProfile, + profile: { ...blockMocks.testProfile, ...blockMocks.updProfile, host: "service" }, + }; testSpy.mockResolvedValue(newServiceProfile); const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); await ZoweVsCodeExtension.logoutWithBaseProfile({ serviceProfile: "service" }); - const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); testSession.ISession.hostname = "service"; testSession.ISession.tokenValue = "tokenValue"; delete testSession.ISession.base64EncodedAuth; @@ -334,49 +389,51 @@ describe("ZoweVsCodeExtension", () => { delete testSession.ISession.password; expect(logoutSpy).toHaveBeenCalledWith(testSession); - expect(testSpy).toHaveBeenCalledWith(testCache, "service"); - expect(testCache.updateBaseProfileFileLogout).toHaveBeenCalledWith(newServiceProfile); + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "service"); + expect(updateBaseProfileFileLogoutSpy).toHaveBeenCalledWith(newServiceProfile); quickPickMock.mockRestore(); }); it("should login using the base profile when provided with a node, register, and cache instance", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); 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({ - serviceProfile, + serviceProfile: blockMocks.serviceProfile, defaultTokenType: "apimlAuthenticationToken", - profileNode: testNode, - zeRegister: testRegister, - zeProfiles: testCache, + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, }); - const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); testSession.ISession.base64EncodedAuth = "dXNlcjpwYXNz"; expect(loginSpy).not.toHaveBeenCalled(); expect(testSpy).not.toHaveBeenCalled(); - expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith(baseProfile, updProfile, false); - expect(testNode.setProfileToChoice).toHaveBeenCalled(); + expect(updateBaseProfileFileLoginSpy).toHaveBeenCalledWith(blockMocks.baseProfile, blockMocks.updProfile, false); + expect(blockMocks.testNode.setProfileToChoice).toHaveBeenCalled(); quickPickMock.mockRestore(); }); }); it("should logout using the base profile when provided with a node, register, and cache instance", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const updateBaseProfileFileLogoutSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogout"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); - const newServiceProfile = { ...serviceProfile, profile: { ...testProfile, ...updProfile } }; + const newServiceProfile = { ...blockMocks.serviceProfile, profile: { ...blockMocks.testProfile, ...blockMocks.updProfile } }; await ZoweVsCodeExtension.logoutWithBaseProfile({ serviceProfile: newServiceProfile, - zeRegister: testRegister, - zeProfiles: testCache, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, }); - const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); testSession.ISession.tokenValue = "tokenValue"; delete testSession.ISession.base64EncodedAuth; delete testSession.ISession.user; @@ -384,11 +441,12 @@ describe("ZoweVsCodeExtension", () => { expect(logoutSpy).not.toHaveBeenCalled(); expect(testSpy).not.toHaveBeenCalled(); - expect(testCache.updateBaseProfileFileLogout).toHaveBeenCalledWith(baseProfile); + expect(updateBaseProfileFileLogoutSpy).toHaveBeenCalledWith(blockMocks.baseProfile); }); it("calls promptCertificate if 'Certificate' was selected in quick pick", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); + const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "getServiceProfileForAuthPurposes"); jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["user", "pass"]); let sessionCopy; @@ -402,23 +460,23 @@ describe("ZoweVsCodeExtension", () => { const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[1]); await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); - const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; delete testSession.ISession.password; delete testSession.ISession.base64EncodedAuth; testSession.ISession.storeCookie = false; expect(sessionCopy.ISession.type).toBe(imperative.SessConstants.AUTH_TYPE_CERT_PEM); - expect(testSpy).toHaveBeenCalledWith(testCache, "service"); + expect(testSpy).toHaveBeenCalledWith(blockMocks.testCache, "service"); expect(loginSpy).toHaveBeenCalledWith(sessionCopy); expect(promptCertMock).toHaveBeenCalled(); expect(quickPickMock).toHaveBeenCalled(); - expect(testCache.updateBaseProfileFileLogin).toHaveBeenCalledWith(baseProfile, updProfile, false); + expect(updateBaseProfileFileLoginSpy).toHaveBeenCalledWith(blockMocks.baseProfile, blockMocks.updProfile, false); promptCertMock.mockRestore(); }); it("returns false if there's an error from promptCertificate", async () => { - testCache.fetchBaseProfile.mockResolvedValue(baseProfile); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["user", "pass"]); // case 1: User selects "user/password" for login quick pick @@ -433,40 +491,37 @@ describe("ZoweVsCodeExtension", () => { }); it("should not login if the user cancels the operation when selecting the authentication method", async () => { - const baseProfileLoaded = testCache.getProfileLoaded(baseProfile.name, baseProfile.type, baseProfile.profile); - testCache.fetchBaseProfile.mockResolvedValue(baseProfileLoaded); + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation(() => undefined as any); const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ - serviceProfile: serviceProfile, + serviceProfile: blockMocks.serviceProfile, defaultTokenType: "apimlAuthenticationToken", - profileNode: testNode, - zeRegister: testRegister, - zeProfiles: testCache, + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, }); expect(didLogin).toBeFalsy(); quickPickMock.mockRestore(); }); - it("should prefer base profile token type even if the user does not provide credentials to login with ", async () => { - const serviceProfileLoaded = testCache.getProfileLoaded(serviceProfile.name, serviceProfile.type, serviceProfile.profile); - serviceProfileLoaded.profile.tokenType = "SERVICE"; - const baseProfileLoaded = testCache.getProfileLoaded(baseProfile.name, baseProfile.type, baseProfile.profile); - baseProfileLoaded.profile.tokenType = "BASE"; - testCache.fetchBaseProfile.mockResolvedValue(baseProfileLoaded); + it("should prefer base profile token type even if the user does not provide credentials to login with", async () => { + const serviceProfileLoaded = { ...blockMocks.serviceProfile, profile: { ...blockMocks.serviceProfile.profile, tokenType: "SERVICE" } }; + const baseProfileLoaded = { ...blockMocks.baseProfile, profile: { ...blockMocks.baseProfile.profile, tokenType: "BASE" } }; + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(baseProfileLoaded); const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(undefined); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: serviceProfileLoaded, defaultTokenType: "apimlAuthenticationToken", - profileNode: testNode, - zeRegister: testRegister, - zeProfiles: testCache, + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, preferBaseToken: true, // force to use base profile to login }); - const testSession = new imperative.Session(JSON.parse(JSON.stringify(expectedSession.ISession))); + const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); testSession.ISession.base64EncodedAuth = "VXNlcm5hbWU6UGFzc3dvcmQ="; delete testSession.ISession.user; delete testSession.ISession.password; @@ -479,35 +534,141 @@ describe("ZoweVsCodeExtension", () => { }); it("should update the cache when autoStore is false after a successful login operation", async () => { - const serviceProfileLoaded = testCache.getProfileLoaded(serviceProfile.name, serviceProfile.type, serviceProfile.profile); - serviceProfileLoaded.profile = { ...testProfile, tokenType: "SERVICE" }; - const baseProfileLoaded = testCache.getProfileLoaded(baseProfile.name, baseProfile.type, baseProfile.profile); - baseProfileLoaded.profile = { ...testProfile, tokenType: "BASE" }; - testCache.fetchBaseProfile.mockResolvedValue(baseProfileLoaded); - - getTeamConfig.mockReturnValue({ exists: true, properties: { autoStore: false } }); + const serviceProfileLoaded = { ...blockMocks.serviceProfile, profile: { ...blockMocks.serviceProfile.profile, tokenType: "SERVICE" } }; + const baseProfileLoaded = { ...blockMocks.baseProfile, profile: { ...blockMocks.baseProfile.profile, tokenType: "BASE" } }; + jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(baseProfileLoaded); + blockMocks.configLayer.properties.autoStore = false; jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["user", "pass"]); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); + await ZoweVsCodeExtension.profilesCache.refresh({ + registeredApiTypes: jest.fn().mockReturnValue(["service"]), + } as unknown as Types.IApiRegisterClient); const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: serviceProfileLoaded, defaultTokenType: "apimlAuthenticationToken", - profileNode: testNode, - zeRegister: testRegister, - zeProfiles: testCache, + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, }); - const expectedProfile = { ...serviceProfileLoaded, profile: { ...testProfile, tokenType: "SERVICE", tokenValue: "tokenValue" } }; + const expectedProfile = { + ...serviceProfileLoaded, + profile: { ...blockMocks.testProfile, tokenType: "SERVICE", tokenValue: "tokenValue" }, + }; expect(didLogin).toBeTruthy(); - expect(testCache.allProfiles).toEqual([expectedProfile, baseProfile]); + expect(blockMocks.testCache.allProfiles).toEqual([expectedProfile, blockMocks.baseProfile]); + quickPickMock.mockRestore(); + }); + + it("should prefer base profile when it exists and has tokenType defined", async () => { + const serviceProfileLoaded = { ...blockMocks.serviceProfile, profile: { ...blockMocks.serviceProfile.profile, tokenType: "SERVICE" } }; + const baseProfileLoaded = { ...blockMocks.baseProfile, profile: { ...blockMocks.baseProfile.profile, tokenType: "BASE" } }; + const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile"); + blockMocks.configLayer.properties.profiles.service.properties.tokenType = "SERVICE"; + blockMocks.configLayer.properties.profiles.base.properties.tokenType = "BASE"; + blockMocks.configLayer.properties.defaults.base = "base"; + + const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["abc", "def"]); + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); + const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + serviceProfile: serviceProfileLoaded, + defaultTokenType: "apimlAuthenticationToken", + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + preferBaseToken: true, // force to use base profile to login + }); + + expect(testSpy).toHaveBeenCalled(); + expect(didLogin).toBe(true); + await expect(fetchBaseProfileSpy.mock.results[0].value).resolves.toEqual(baseProfileLoaded); + quickPickMock.mockRestore(); + }); + + it("should prefer base profile when it exists, does not have tokenType defined, and service profile is flat", async () => { + const serviceProfileLoaded = { ...blockMocks.serviceProfile, profile: { ...blockMocks.serviceProfile.profile, tokenType: "SERVICE" } }; + const baseProfileLoaded = { ...blockMocks.baseProfile, profile: { ...blockMocks.baseProfile.profile } }; + const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile"); + blockMocks.configLayer.properties.profiles.service.properties.tokenType = "SERVICE"; + blockMocks.configLayer.properties.profiles.base.properties.tokenType = undefined; + blockMocks.configLayer.properties.defaults.base = "base"; + + const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["abc", "def"]); + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); + const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + serviceProfile: serviceProfileLoaded, + defaultTokenType: "apimlAuthenticationToken", + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + preferBaseToken: true, // force to use base profile to login + }); + + expect(testSpy).toHaveBeenCalled(); + expect(didLogin).toBe(true); + await expect(fetchBaseProfileSpy.mock.results[0].value).resolves.toEqual(baseProfileLoaded); + quickPickMock.mockRestore(); + }); + + it("should prefer parent profile when base profile does not exist and service profile is nested", async () => { + const serviceProfileLoaded = { + ...blockMocks.serviceProfile, + name: "lpar.service", + profile: { ...blockMocks.serviceProfile.profile, tokenType: "SERVICE" }, + }; + const baseProfileLoaded = { ...blockMocks.baseProfile, name: "lpar", profile: {} }; + const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile"); + blockMocks.configLayer.properties.profiles = { + lpar: { + profiles: { + service: { + type: "service", + properties: [], + }, + }, + properties: [], + }, + }; + + const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["abc", "def"]); + const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); + const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + serviceProfile: serviceProfileLoaded, + defaultTokenType: "apimlAuthenticationToken", + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + preferBaseToken: true, // force to use base profile to login + }); + + expect(testSpy).toHaveBeenCalled(); + expect(didLogin).toBe(true); + await expect(fetchBaseProfileSpy.mock.results[0].value).resolves.toEqual(baseProfileLoaded); quickPickMock.mockRestore(); }); - // TODO: e2e test the following scenarios - // it("should prefer base profile when it exists and it has tokenValue in secure array", async () => {}); - // it("should prefer base profile when it exists, it does not have tokenValue in its secure array, and service profile is flat", async () => {}); - // it("should prefer parent profile when base profile does not exist and service profile is nested", () => {}); - // it("should cancel the operation if the base profile does not exist and service profile is flat", () => {}); + it("should cancel the operation if the base profile does not exist and service profile is flat", async () => { + const serviceProfileLoaded = { ...blockMocks.serviceProfile, profile: { ...blockMocks.serviceProfile.profile, tokenType: "SERVICE" } }; + const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile"); + delete blockMocks.configLayer.properties.profiles.base; + + const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass"); + const errorMessageSpy = jest.spyOn(Gui, "errorMessage"); + const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + serviceProfile: serviceProfileLoaded, + defaultTokenType: "apimlAuthenticationToken", + profileNode: blockMocks.testNode, + zeRegister: blockMocks.testRegister, + zeProfiles: blockMocks.testCache, + preferBaseToken: true, // force to use base profile to login + }); + + expect(testSpy).not.toHaveBeenCalled(); + expect(errorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("No base profile found")); + expect(didLogin).toBe(false); + await expect(fetchBaseProfileSpy.mock.results[0].value).resolves.toBeUndefined(); + }); }); describe("updateCredentials", () => { const promptCredsOptions: PromptCredentialsOptions.ComplexOptions = { diff --git a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts index e4b0ee5ac3..c8399647cd 100644 --- a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts +++ b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts @@ -253,14 +253,12 @@ export class ZoweVsCodeExtension { private static async updateProfileInCache(opts: BaseProfileAuthOptions & { serviceProfile: imperative.IProfileLoaded }): Promise { const cache: ProfilesCache = opts.zeProfiles ?? ZoweVsCodeExtension.profilesCache; - if ((await cache.getProfileInfo()).getTeamConfig().properties.autoStore) { + if ((await cache.getProfileInfo()).getTeamConfig().properties.autoStore !== false) { await cache.refresh(); } else { - // TODO For loginWithBaseProfile, when autoStore=false is it correct to update service profile here instead of base? - // zFernand0: Yes, it is fine to update the service profile in the cache to avoid impacting other in memory credentials. // Note: It should be expected that nested profiles within this service profile will have their credentials updated. const profIndex = cache.allProfiles.findIndex((profile) => profile.name === opts.serviceProfile.name); - cache.allProfiles[profIndex] = opts.serviceProfile; + cache.allProfiles[profIndex] = { ...cache.allProfiles[profIndex], profile: opts.serviceProfile.profile }; } if (opts.profileNode) { opts.profileNode.setProfileToChoice({ diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index 7729151d60..5bd22baceb 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -563,7 +563,6 @@ export class Profiles extends ProfilesCache { return; // See https://github.com/zowe/zowe-explorer-vscode/issues/1827 } - // TODO Is this redundant with refresh call in ZoweVsCodeExtension.updateCredentials? const returnValue: string[] = [promptInfo.profile.user, promptInfo.profile.password, promptInfo.profile.base64EncodedAuth]; this.updateProfilesArrays(promptInfo); @@ -1063,7 +1062,6 @@ export class Profiles extends ProfilesCache { session.ISession.user = creds[0]; session.ISession.password = creds[1]; await ZoweExplorerApiRegister.getInstance().getCommonApi(serviceProfile).login(session); - // TODO Should we call ZoweVsCodeExtension.updateProfileInCache method here? const profIndex = this.allProfiles.findIndex((profile) => profile.name === serviceProfile.name); this.allProfiles[profIndex] = { ...serviceProfile, profile: { ...serviceProfile, ...session } }; if (node) { From df87926180a501f28dfd3cac901abe4204a8b116 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Mon, 16 Sep 2024 17:03:24 -0400 Subject: [PATCH 10/10] Deprecate methods to avoid breaking change Signed-off-by: Timothy Johnson --- packages/zowe-explorer-api/CHANGELOG.md | 3 ++ .../vscode/ZoweVsCodeExtension.unit.test.ts | 38 +++++++++---------- .../src/vscode/ZoweVsCodeExtension.ts | 37 ++++++++++++++++-- .../configuration/Profiles.unit.test.ts | 6 +-- .../src/configuration/Profiles.ts | 6 +-- 5 files changed, 62 insertions(+), 28 deletions(-) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 5cd1cc21bb..f79d44ca29 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -6,6 +6,9 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t ### New features and enhancements +- Added the `BaseProfileAuthOptions` interface to define base profile authentication options for SSO login and logout. [#3076](https://github.com/zowe/zowe-explorer-vscode/pull/3076) +- Deprecated the methods `ZoweVsCodeExtension.loginWithBaseProfile` and `ZoweVsCodeExtension.logoutWithBaseProfile`. Use `ZoweVsCodeExtension.ssoLogin` and `ZoweVsCodeExtension.ssoLogout` instead, which use the `BaseProfileAuthOptions` interface and allow you to choose whether the token value in the base profile should have precedence in case there are conflicts. [#3076](https://github.com/zowe/zowe-explorer-vscode/pull/3076) + ### Bug fixes - Fixed behavior of logout action when token is defined in both base profile and parent profile. [#3076](https://github.com/zowe/zowe-explorer-vscode/issues/3076) 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 da210b70ed..e3c431880e 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 @@ -186,7 +186,7 @@ describe("ZoweVsCodeExtension", () => { const errorMessageSpy = jest.spyOn(Gui, "errorMessage"); const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(undefined); const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); - await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: "service" }); expect(fetchBaseProfileSpy).toHaveBeenCalledTimes(1); expect(updateBaseProfileFileLoginSpy).not.toHaveBeenCalled(); expect(errorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("Login failed: No base profile found")); @@ -195,7 +195,7 @@ describe("ZoweVsCodeExtension", () => { const errorMessageSpy = jest.spyOn(Gui, "errorMessage"); const fetchBaseProfileSpy = jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(undefined); const updateBaseProfileFileLoginSpy = jest.spyOn(blockMocks.testCache, "updateBaseProfileFileLogin"); - await ZoweVsCodeExtension.logoutWithBaseProfile({ serviceProfile: "service" }); + await ZoweVsCodeExtension.ssoLogout({ serviceProfile: "service" }); expect(fetchBaseProfileSpy).toHaveBeenCalledTimes(1); expect(updateBaseProfileFileLoginSpy).not.toHaveBeenCalled(); expect(errorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("Logout failed: No base profile found")); @@ -210,7 +210,7 @@ describe("ZoweVsCodeExtension", () => { const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: "service" }); const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; @@ -231,7 +231,7 @@ describe("ZoweVsCodeExtension", () => { const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.logoutWithBaseProfile({ serviceProfile: "service" }); + await ZoweVsCodeExtension.ssoLogout({ serviceProfile: "service" }); const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); testSession.ISession.tokenValue = "tokenValue"; @@ -259,7 +259,7 @@ describe("ZoweVsCodeExtension", () => { const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: "service" }); const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; @@ -288,7 +288,7 @@ describe("ZoweVsCodeExtension", () => { const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: "service" }); const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; @@ -338,7 +338,7 @@ describe("ZoweVsCodeExtension", () => { }; const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "lpar.service" }); + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: "lpar.service" }); const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; @@ -379,7 +379,7 @@ describe("ZoweVsCodeExtension", () => { const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.logoutWithBaseProfile({ serviceProfile: "service" }); + await ZoweVsCodeExtension.ssoLogout({ serviceProfile: "service" }); const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); testSession.ISession.hostname = "service"; @@ -401,7 +401,7 @@ describe("ZoweVsCodeExtension", () => { const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue("tokenValue"); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - await ZoweVsCodeExtension.loginWithBaseProfile({ + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: blockMocks.serviceProfile, defaultTokenType: "apimlAuthenticationToken", profileNode: blockMocks.testNode, @@ -427,7 +427,7 @@ describe("ZoweVsCodeExtension", () => { const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockImplementation(jest.fn()); const newServiceProfile = { ...blockMocks.serviceProfile, profile: { ...blockMocks.testProfile, ...blockMocks.updProfile } }; - await ZoweVsCodeExtension.logoutWithBaseProfile({ + await ZoweVsCodeExtension.ssoLogout({ serviceProfile: newServiceProfile, zeRegister: blockMocks.testRegister, zeProfiles: blockMocks.testCache, @@ -458,7 +458,7 @@ describe("ZoweVsCodeExtension", () => { // case 1: User selects "user/password" for login quick pick const promptCertMock = jest.spyOn(ZoweVsCodeExtension as any, "promptCertificate").mockImplementation(); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[1]); - await ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" }); + await ZoweVsCodeExtension.ssoLogin({ serviceProfile: "service" }); const testSession = new imperative.Session(JSON.parse(JSON.stringify(blockMocks.expectedSession.ISession))); delete testSession.ISession.user; @@ -485,7 +485,7 @@ describe("ZoweVsCodeExtension", () => { const promptCertMock = jest .spyOn(ZoweVsCodeExtension as any, "promptCertificate") .mockRejectedValueOnce(new Error("invalid certificate")); - await expect(ZoweVsCodeExtension.loginWithBaseProfile({ serviceProfile: "service" })).resolves.toBe(false); + await expect(ZoweVsCodeExtension.ssoLogin({ serviceProfile: "service" })).resolves.toBe(false); expect(promptCertMock).toHaveBeenCalled(); expect(quickPickMock).toHaveBeenCalled(); }); @@ -493,7 +493,7 @@ describe("ZoweVsCodeExtension", () => { it("should not login if the user cancels the operation when selecting the authentication method", async () => { jest.spyOn(blockMocks.testCache, "fetchBaseProfile").mockResolvedValue(blockMocks.baseProfile); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation(() => undefined as any); - const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + const didLogin = await ZoweVsCodeExtension.ssoLogin({ serviceProfile: blockMocks.serviceProfile, defaultTokenType: "apimlAuthenticationToken", profileNode: blockMocks.testNode, @@ -512,7 +512,7 @@ describe("ZoweVsCodeExtension", () => { const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(undefined); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + const didLogin = await ZoweVsCodeExtension.ssoLogin({ serviceProfile: serviceProfileLoaded, defaultTokenType: "apimlAuthenticationToken", profileNode: blockMocks.testNode, @@ -544,7 +544,7 @@ describe("ZoweVsCodeExtension", () => { await ZoweVsCodeExtension.profilesCache.refresh({ registeredApiTypes: jest.fn().mockReturnValue(["service"]), } as unknown as Types.IApiRegisterClient); - const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + const didLogin = await ZoweVsCodeExtension.ssoLogin({ serviceProfile: serviceProfileLoaded, defaultTokenType: "apimlAuthenticationToken", profileNode: blockMocks.testNode, @@ -571,7 +571,7 @@ describe("ZoweVsCodeExtension", () => { const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["abc", "def"]); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + const didLogin = await ZoweVsCodeExtension.ssoLogin({ serviceProfile: serviceProfileLoaded, defaultTokenType: "apimlAuthenticationToken", profileNode: blockMocks.testNode, @@ -596,7 +596,7 @@ describe("ZoweVsCodeExtension", () => { const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["abc", "def"]); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + const didLogin = await ZoweVsCodeExtension.ssoLogin({ serviceProfile: serviceProfileLoaded, defaultTokenType: "apimlAuthenticationToken", profileNode: blockMocks.testNode, @@ -633,7 +633,7 @@ describe("ZoweVsCodeExtension", () => { const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass").mockResolvedValue(["abc", "def"]); const quickPickMock = jest.spyOn(Gui, "showQuickPick").mockImplementation((items) => items[0]); - const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + const didLogin = await ZoweVsCodeExtension.ssoLogin({ serviceProfile: serviceProfileLoaded, defaultTokenType: "apimlAuthenticationToken", profileNode: blockMocks.testNode, @@ -655,7 +655,7 @@ describe("ZoweVsCodeExtension", () => { const testSpy = jest.spyOn(ZoweVsCodeExtension as any, "promptUserPass"); const errorMessageSpy = jest.spyOn(Gui, "errorMessage"); - const didLogin = await ZoweVsCodeExtension.loginWithBaseProfile({ + const didLogin = await ZoweVsCodeExtension.ssoLogin({ serviceProfile: serviceProfileLoaded, defaultTokenType: "apimlAuthenticationToken", profileNode: blockMocks.testNode, diff --git a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts index c8399647cd..4ea46ba295 100644 --- a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts +++ b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts @@ -95,7 +95,6 @@ export class ZoweVsCodeExtension { shouldSave = await ZoweVsCodeExtension.saveCredentials(loadProfile); } - // TODO Should we call updateProfileInCache method here? if (shouldSave) { // write changes to the file, autoStore value determines if written to file const upd = { profileName: loadProfile.name, profileType: loadProfile.type }; @@ -113,13 +112,30 @@ export class ZoweVsCodeExtension { * Trigger a login operation with the merged contents between the service profile and the base profile. * If the connection details (host:port) do not match (service vs base), the token will be stored in the service profile. * If there is no API registered for the profile type, this method defaults the login behavior to that of the APIML. + * @deprecated Use `ZoweVsCodeExtension.ssoLogin` instead * @param serviceProfile Profile to be used for login purposes (either the name of the IProfileLoaded instance) * @param loginTokenType The tokenType value for compatibility purposes * @param node The node for compatibility purposes * @param zeRegister The ZoweExplorerApiRegister instance for compatibility purposes * @param zeProfiles The Zowe Explorer "Profiles.ts" instance for compatibility purposes */ - public static async loginWithBaseProfile(opts: BaseProfileAuthOptions): Promise { + public static async loginWithBaseProfile( + serviceProfile: string | imperative.IProfileLoaded, + loginTokenType?: string, + node?: Types.IZoweNodeType, + zeRegister?: Types.IApiRegisterClient, // ZoweExplorerApiRegister + zeProfiles?: ProfilesCache // Profiles extends ProfilesCache + ): Promise { + return this.ssoLogin({ serviceProfile, defaultTokenType: loginTokenType, profileNode: node, zeRegister, zeProfiles }); + } + + /** + * Trigger a login operation with the merged contents between the service profile and the base profile. + * If the connection details (host:port) do not match (service vs base), the token will be stored in the service profile. + * If there is no API registered for the profile type, this method defaults the login behavior to that of the APIML. + * @param {BaseProfileAuthOptions} opts Object defining options for base profile authentication + */ + public static async ssoLogin(opts: BaseProfileAuthOptions): Promise { const cache: ProfilesCache = opts.zeProfiles ?? ZoweVsCodeExtension.profilesCache; const serviceProfile = typeof opts.serviceProfile === "string" @@ -207,11 +223,26 @@ export class ZoweVsCodeExtension { * Trigger a logout operation with the merged contents between the service profile and the base profile. * If the connection details (host:port) do not match (service vs base), the token will be removed from the service profile. * If there is no API registered for the profile type, this method defaults the logout behavior to that of the APIML. + * @deprecated Use `ZoweVsCodeExtension.ssoLogout` instead * @param serviceProfile Profile to be used for logout purposes (either the name of the IProfileLoaded instance) * @param zeRegister The ZoweExplorerApiRegister instance for compatibility purposes * @param zeProfiles The Zowe Explorer "Profiles.ts" instance for compatibility purposes */ - public static async logoutWithBaseProfile(opts: BaseProfileAuthOptions): Promise { + public static async logoutWithBaseProfile( + serviceProfile: string | imperative.IProfileLoaded, + zeRegister?: Types.IApiRegisterClient, // ZoweExplorerApiRegister + zeProfiles?: ProfilesCache // Profiles extends ProfilesCache + ): Promise { + return this.ssoLogout({ serviceProfile, zeRegister, zeProfiles }); + } + + /** + * Trigger a logout operation with the merged contents between the service profile and the base profile. + * If the connection details (host:port) do not match (service vs base), the token will be removed from the service profile. + * If there is no API registered for the profile type, this method defaults the logout behavior to that of the APIML. + * @param {BaseProfileAuthOptions} opts Object defining options for base profile authentication + */ + public static async ssoLogout(opts: BaseProfileAuthOptions): Promise { const cache: ProfilesCache = opts.zeProfiles ?? ZoweVsCodeExtension.profilesCache; const serviceProfile = typeof opts.serviceProfile === "string" 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 b9cd7af9d8..7ab2b03e48 100644 --- a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts @@ -1210,7 +1210,7 @@ describe("Profiles Unit Tests - function ssoLogin", () => { getTokenTypeName: () => imperative.SessConstants.TOKEN_TYPE_APIML, login: jest.fn(), } as never); - const loginBaseProfMock = jest.spyOn(ZoweVsCodeExtension, "loginWithBaseProfile").mockRejectedValueOnce(new Error("test error.")); + const loginBaseProfMock = jest.spyOn(ZoweVsCodeExtension, "ssoLogin").mockRejectedValueOnce(new Error("test error.")); jest.spyOn(Profiles.getInstance() as any, "loginCredentialPrompt").mockReturnValue(["fake", "12345"]); await expect(Profiles.getInstance().ssoLogin(testNode, "fake")).resolves.not.toThrow(); expect(ZoweLogger.error).toHaveBeenCalled(); @@ -1309,7 +1309,7 @@ describe("Profiles Unit Tests - function handleSwitchAuthentication", () => { getTokenTypeName: () => "apimlAuthenticationToken", } as never); - jest.spyOn(ZoweVsCodeExtension, "loginWithBaseProfile").mockResolvedValue(true); + jest.spyOn(ZoweVsCodeExtension, "ssoLogin").mockResolvedValue(true); await Profiles.getInstance().handleSwitchAuthentication(testNode); expect(Gui.showMessage).toHaveBeenCalled(); expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType); @@ -1368,7 +1368,7 @@ describe("Profiles Unit Tests - function handleSwitchAuthentication", () => { getTokenTypeName: () => "apimlAuthenticationToken", } as never); - jest.spyOn(ZoweVsCodeExtension, "loginWithBaseProfile").mockResolvedValue(false); + jest.spyOn(ZoweVsCodeExtension, "ssoLogin").mockResolvedValue(false); await Profiles.getInstance().handleSwitchAuthentication(testNode); expect(Gui.errorMessage).toHaveBeenCalled(); expect(testNode.profile.profile.tokenType).toBe(modifiedTestNode.profile.profile.tokenType); diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index 27f608b1ab..d8f3cea1db 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -787,7 +787,7 @@ export class Profiles extends ProfilesCache { if (loginTokenType && !loginTokenType.startsWith(imperative.SessConstants.TOKEN_TYPE_APIML)) { loginOk = await this.loginWithRegularProfile(serviceProfile, node); } else { - loginOk = await ZoweVsCodeExtension.loginWithBaseProfile({ + loginOk = await ZoweVsCodeExtension.ssoLogin({ serviceProfile, defaultTokenType: loginTokenType, profileNode: node, @@ -892,7 +892,7 @@ export class Profiles extends ProfilesCache { case AuthUtils.isProfileUsingBasicAuth(serviceProfile): { let loginOk = false; if (loginTokenType && loginTokenType.startsWith("apimlAuthenticationToken")) { - loginOk = await ZoweVsCodeExtension.loginWithBaseProfile({ + loginOk = await ZoweVsCodeExtension.ssoLogin({ serviceProfile, defaultTokenType: loginTokenType, profileNode: node, @@ -1031,7 +1031,7 @@ export class Profiles extends ProfilesCache { await ZoweExplorerApiRegister.getInstance().getCommonApi(serviceProfile).logout(node.getSession()); } else { const zeRegister = ZoweExplorerApiRegister.getInstance(); - logoutOk = await ZoweVsCodeExtension.logoutWithBaseProfile({ + logoutOk = await ZoweVsCodeExtension.ssoLogout({ serviceProfile, defaultTokenType: zeRegister?.getCommonApi(serviceProfile).getTokenTypeName(), profileNode: node,