From e1c39adcf079c37048ccf8e953b900eb7c85accb Mon Sep 17 00:00:00 2001 From: Billie Simmons Date: Sat, 16 Nov 2024 12:13:09 -0500 Subject: [PATCH] Port fixes from main for 3.0.3 (#3301) * DatasetFSprovider fetchDataset() - fix behavior with non-existent datasets and members (#3255) * Fix PDS members in fetchDataset Signed-off-by: Benjamin Santos * Update DatasetFSProvider.unit.test.ts Signed-off-by: Benjamin Santos * Update CHANGELOG.md Signed-off-by: Benjamin Santos * Merge branch 'main' into datasetfsprovider-remote-lookup-fix Signed-off-by: Benjamin Santos * move changelog update Signed-off-by: Benjamin Santos * improve logic Signed-off-by: Benjamin Santos * add ? after checking dsorg Signed-off-by: Benjamin Santos * do not fetch attributes for members Signed-off-by: Benjamin Santos --------- Signed-off-by: Benjamin Santos * fix: use `vscode.workspace.fs` for `delete` and `rename` (#3261) * fix: use vscode.workspace.fs to rename URIs Signed-off-by: Trae Yelovich * chore: update ZE changelog Signed-off-by: Trae Yelovich * update mocks, remaining use of delete/rename Signed-off-by: Trae Yelovich --------- Signed-off-by: Trae Yelovich * Fix: To resolve error message upon switching the authentication methods (#3275) * To handle missing args in basicAuthClearSecureArray Signed-off-by: Santhoshi Boyina * To handle missing args in tokenAuthClearSecureArray Signed-off-by: Santhoshi Boyina * To add unit test case for missing arg's in basicAuthClearSecureArray Signed-off-by: Santhoshi Boyina * To add unit test case for missing arg's in tokenAuthClearSecureArray Signed-off-by: Santhoshi Boyina * To run pre-publish command Signed-off-by: Santhoshi Boyina * To update changelog Signed-off-by: Santhoshi Boyina * To modify tokenAuthClearSecureArray() function Signed-off-by: Santhoshi Boyina * To add additional test cases for tokenAuthClearSecureArray() Signed-off-by: Santhoshi Boyina --------- Signed-off-by: Santhoshi Boyina * fix: "Show Config" button in error dialog does not work during initialization (#3274) * fix: check if profileInfo is nullish during v1 migration Signed-off-by: Trae Yelovich * chore: update ZE changelog Signed-off-by: Trae Yelovich * tests: integration test for broken config Signed-off-by: Trae Yelovich * add unit test for nullish profileinfo Signed-off-by: Trae Yelovich * fix transient failures in UpdateCredentials scenario Signed-off-by: Trae Yelovich * remove extra join import in wdio conf Signed-off-by: Trae Yelovich * make integration test more reliable Signed-off-by: Trae Yelovich * move getprofileinfo call into try/catch during profiles init Signed-off-by: Trae Yelovich * test: open notification center to check for dialog Signed-off-by: Trae Yelovich * add license header to test; add another null check Signed-off-by: Trae Yelovich * add typedoc to ProfilesUtils.getProfileInfo Signed-off-by: Trae Yelovich * setupDefaultCredentialManager: log err msgs, update typedoc Signed-off-by: Trae Yelovich * test: promptUserWithNoConfigs, nullish profileInfo case Signed-off-by: Trae Yelovich * refactor typedoc for setupDefaultCredentialManager Signed-off-by: Trae Yelovich --------- Signed-off-by: Trae Yelovich * fix(ds): Call remoteLookupForResource when entry doesn't exist locally (#3268) Signed-off-by: Trae Yelovich Signed-off-by: Billie Simmons Co-authored-by: Billie Simmons * fix: Only show "No configs detected" prompt if ZE opened (#3281) * fix: show 'No config detected' prompt once when ZE opened Signed-off-by: Trae Yelovich * chore: update ZE changelog Signed-off-by: Trae Yelovich * resolve failing tests Signed-off-by: Trae Yelovich * refactor: move event into static fn, add coverage Signed-off-by: Trae Yelovich * update changelog Signed-off-by: Trae Yelovich * patch coverage for ProfilesUtils.promptUserWithNoConfigs Signed-off-by: Trae Yelovich * move variable definition Signed-off-by: Trae Yelovich --------- Signed-off-by: Trae Yelovich Signed-off-by: Billie Simmons Co-authored-by: Billie Simmons Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> * fix(v3): Pass `responseTimeout` in z/OSMF MVS and USS API calls (#3292) * fix: pass responseTimeout to API functions Signed-off-by: Trae Yelovich * refactor: remove fallback for spreading newOptions Signed-off-by: Trae Yelovich * refactor: use optional chaining; work on resolving tests Signed-off-by: Trae Yelovich * refactor: pass profile props to tests, fix types Signed-off-by: Trae Yelovich * tests: resolve failing cases in ZE Signed-off-by: Trae Yelovich * chore: update changelog entry for ZE API Signed-off-by: Trae Yelovich * refactor: remove fallback for spreading undefined options Signed-off-by: Trae Yelovich * fix: add missing functions to MvsApi test list Signed-off-by: Trae Yelovich --------- Signed-off-by: Trae Yelovich * run package Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> * chore: remediation commit Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Third-Party DCO Remediation Commit for benjamin-t-santos <115251181+benjamin-t-santos@users.noreply.github.com> On behalf of benjamin-t-santos <115251181+benjamin-t-santos@users.noreply.github.com>, I, Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com>, hereby add my Signed-off-by to this commit: 8957a6429c4de5487a2d800616c92228b67a7493 Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Third-Party DCO Remediation Commit for SanthoshiBoyina1 <142206957+SanthoshiBoyina1@users.noreply.github.com> On behalf of SanthoshiBoyina1 <142206957+SanthoshiBoyina1@users.noreply.github.com>, I, Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com>, hereby add my Signed-off-by to this commit: 86a82f62d296a3b16270bb63885c8c76ffb70c41 Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> * DatasetFSProvider.readFile() makes at most one API call (#3279) * fix(ds): Call remoteLookupForResource when entry doesn't exist locally Signed-off-by: Trae Yelovich * readFile() uses one MVS API call Signed-off-by: Benjamin Santos * update unit tests, fix logic of calls Signed-off-by: Benjamin Santos * fix lint error Signed-off-by: Benjamin Santos * Update CHANGELOG.md Signed-off-by: Benjamin Santos --------- Signed-off-by: Trae Yelovich Signed-off-by: Benjamin Santos Signed-off-by: benjamin-t-santos <115251181+benjamin-t-santos@users.noreply.github.com> Co-authored-by: Trae Yelovich Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> * Change default credentials manager check and not-found dialog (#3297) * updates to webpack and package.json for dev mode Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> * Remove endless startup loop when default CM cannot be loaded Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Initial set of test updates Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Merge branch 'main' into change-credentials-manager-check Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Convenience launch to only run currently open test Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Fixed ProfileUtils tests Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Clean up package files Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Updated changelog Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Reworded info message Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Update CHANGELOG.md Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> * Added detail to info message Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> * Updated resource files Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> --------- Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Co-authored-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> Co-authored-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> * fix(3.0.3): Prompt in editor for 401 error, fix profile propagation (#3318) * fix: profile change propagation, prompt on auth error Signed-off-by: Trae Yelovich * tests: profile propagation, tree node cases Signed-off-by: Trae Yelovich * chore: changelogs Signed-off-by: Trae Yelovich * test(jobs): add patch coverage Signed-off-by: Trae Yelovich * tests: AuthUtils.promptForAuthError Signed-off-by: Trae Yelovich * tests: UssFSProvider.fetchFileAtUri Signed-off-by: Trae Yelovich * fix: reset wasAccessed flag if ImperativeError caught Signed-off-by: Trae Yelovich * tests: resolve failing USS tests Signed-off-by: Trae Yelovich * fix failing jobs test Signed-off-by: Trae Yelovich --------- Signed-off-by: Trae Yelovich * fix(ftp): Generate member name if missing in `putContents` (#3313) * fix(ftp): Generate member name if missing in putContents Signed-off-by: Trae Yelovich * chore: add entry to FTP changelog Signed-off-by: Trae Yelovich * refactor: remove unused isAbsolutePath import Signed-off-by: Trae Yelovich * chore: undo updates to l10n since no strings were changed Signed-off-by: Trae Yelovich * tests(ftp): PDS upload case, clean up PS upload test Signed-off-by: Trae Yelovich --------- Signed-off-by: Trae Yelovich Co-authored-by: Billie Simmons Co-authored-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> * run prepublish Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> * Fix infinite loop when fetching USS resources with stat() (#3321) * remove query when looking up parent Signed-off-by: Benjamin Santos * remove leftover comment Signed-off-by: Benjamin Santos * Update CHANGELOG.md Signed-off-by: Benjamin Santos --------- Signed-off-by: Benjamin Santos Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> * run prepublish Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> * Fix port of #3321 for 3.0.3 release Signed-off-by: Timothy Johnson --------- Signed-off-by: Benjamin Santos Signed-off-by: Trae Yelovich Signed-off-by: Santhoshi Boyina Signed-off-by: Billie Simmons Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Signed-off-by: benjamin-t-santos <115251181+benjamin-t-santos@users.noreply.github.com> Signed-off-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> Signed-off-by: Timothy Johnson Co-authored-by: benjamin-t-santos <115251181+benjamin-t-santos@users.noreply.github.com> Co-authored-by: Trae Yelovich Co-authored-by: SanthoshiBoyina1 <142206957+SanthoshiBoyina1@users.noreply.github.com> Co-authored-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Co-authored-by: Peter Haumer <4391934+phaumer@users.noreply.github.com> Co-authored-by: Timothy Johnson --- .vscode/launch.json | 9 + packages/zowe-explorer-api/CHANGELOG.md | 3 + .../ZoweExplorerZosmfApi.unit.test.ts | 138 ++++++++------ .../__unit__/tree/ZoweTreeNode.unit.test.ts | 54 +++--- .../src/profiles/ZoweExplorerZosmfApi.ts | 107 ++++++++--- .../src/tree/ZoweTreeNode.ts | 17 +- .../zowe-explorer-ftp-extension/CHANGELOG.md | 2 + .../Mvs/ZoweExplorerFtpMvsApi.unit.test.ts | 37 +++- .../src/ZoweExplorerFtpMvsApi.ts | 20 ++- packages/zowe-explorer/CHANGELOG.md | 14 ++ .../dialogs/ShowConfigErrorDialog.feature | 7 + .../dialogs/ShowConfigErrorDialog.steps.ts | 54 ++++++ .../profiles/UpdateCredentials.steps.ts | 15 +- .../__integration__/bdd/wdio.conf.ts | 19 ++ .../__tests__/__mocks__/vscode.ts | 5 + .../configuration/Profiles.unit.test.ts | 169 +++++++++++++++++- .../ZoweExplorerZosmfApi.unit.test.ts | 8 +- .../ZoweExplorerZosmfApi.unit.test.ts.snap | 4 + .../trees/dataset/DatasetActions.unit.test.ts | 21 ++- .../dataset/DatasetFSProvider.unit.test.ts | 151 +++++++++++++--- .../trees/dataset/DatasetInit.unit.test.ts | 28 ++- .../trees/dataset/DatasetTree.unit.test.ts | 4 +- .../trees/job/JobFSProvider.unit.test.ts | 48 ++++- .../__unit__/trees/job/JobTree.unit.test.ts | 2 +- .../trees/job/ZoweJobNode.unit.test.ts | 6 +- .../__unit__/trees/uss/USSTree.unit.test.ts | 4 +- .../trees/uss/UssFSProvider.unit.test.ts | 25 ++- .../trees/uss/ZoweUSSNode.unit.test.ts | 8 +- .../__unit__/utils/AuthUtils.unit.test.ts | 30 ++++ .../__unit__/utils/ProfilesUtils.unit.test.ts | 95 ++++++---- packages/zowe-explorer/l10n/bundle.l10n.json | 9 +- packages/zowe-explorer/l10n/poeditor.json | 9 +- .../src/configuration/Profiles.ts | 31 +++- packages/zowe-explorer/src/extension.ts | 1 - .../src/trees/dataset/DatasetActions.ts | 2 +- .../src/trees/dataset/DatasetFSProvider.ts | 147 +++++++++------ .../src/trees/dataset/DatasetInit.ts | 9 + .../src/trees/dataset/DatasetTree.ts | 4 +- .../src/trees/job/JobFSProvider.ts | 33 ++-- .../zowe-explorer/src/trees/job/JobTree.ts | 2 +- .../zowe-explorer/src/trees/uss/USSTree.ts | 4 +- .../src/trees/uss/UssFSProvider.ts | 43 +++-- .../src/trees/uss/ZoweUSSNode.ts | 4 +- packages/zowe-explorer/src/utils/AuthUtils.ts | 106 ++++++----- .../zowe-explorer/src/utils/ProfilesUtils.ts | 114 ++++++++---- 45 files changed, 1216 insertions(+), 406 deletions(-) create mode 100644 packages/zowe-explorer/__tests__/__integration__/bdd/features/dialogs/ShowConfigErrorDialog.feature create mode 100644 packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/dialogs/ShowConfigErrorDialog.steps.ts create mode 100644 packages/zowe-explorer/__tests__/__unit__/utils/AuthUtils.unit.test.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 098a5ca92..772a2e558 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,6 +37,15 @@ "console": "integratedTerminal", "sourceMaps": true }, + { + "type": "node", + "name": "Current Unit Tests (Jest)", + "request": "launch", + "runtimeArgs": ["--inspect-brk", "${workspaceFolder}/node_modules/jest/bin/jest", "-i", "${fileBasenameNoExtension}"], + "cwd": "${workspaceFolder}/packages/zowe-explorer", + "console": "integratedTerminal", + "sourceMaps": true + }, { "type": "node", "name": "API Unit Tests (Jest)", diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index f6452a0dd..9d75f7879 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t ### Bug fixes +- Fixed an issue where the `responseTimeout` profile property was ignored for z/OSMF MVS and USS API calls. [#3225](https://github.com/zowe/zowe-explorer-vscode/issues/3225) +- Fixed an issue where the assignment of the `profile` property in `ZoweTreeNode.setProfileToChoice` caused references to that object to break elsewhere. [#3289](https://github.com/zowe/zowe-explorer-vscode/issues/3289) + ## `3.0.2` ### New features and enhancements diff --git a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts index ca00c2980..6882b7554 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts @@ -21,32 +21,37 @@ import { ZoweExplorerZosmf } from "../../../src/profiles/ZoweExplorerZosmfApi"; import { FileManagement } from "../../../src/utils/FileManagement"; import { MainframeInteraction } from "../../../src/extend"; +type ParametersWithProfileArgs = F extends (...args: infer P) => any ? [...Parameters, profileProperties?: object] : never; + type ITestApi = { [K in keyof T]: { name: K; spy: jest.SpyInstance; - args: jest.ArgsType; - transform?: (args: jest.ArgsType) => any[]; + args: ParametersWithProfileArgs; + transform?: (args: ParametersWithProfileArgs) => any[]; }; }[keyof T]; -type ITestProfile = { - host: string; - port: number; - basePath: string; - rejectUnauthorized: boolean; - user?: string; - password?: string; +const fakeProperties = { + responseTimeout: 60, }; -const fakeProfile: ITestProfile = { +const fakeProfile: imperative.IProfile = { host: "example.com", port: 443, basePath: "/api/v1", rejectUnauthorized: true, user: "admin", password: "123456", + ...fakeProperties, +}; +const loadedProfile: imperative.IProfileLoaded = { + profile: fakeProfile, + message: "", + type: "zosmf", + failNotFound: false, }; + const fakeSession = imperative.Session.createFromUrl(new URL("https://example.com")); const mISshSession: zosuss.ISshSession = { @@ -74,7 +79,7 @@ async function expectUnixCommandApiWithSshSession( async function expectApiWithSession({ name, spy, args, transform }: ITestApi, apiInstance: MainframeInteraction.ICommon): Promise { spy.mockClear().mockResolvedValue(undefined); const getSessionSpy = jest.spyOn(apiInstance, "getSession").mockReturnValue(fakeSession); - await apiInstance[name as string](...args); + await apiInstance[name as string](...Object.values(args)); expect(getSessionSpy).toHaveBeenCalledTimes(1); const params: unknown[] = transform ? transform(args) : args; expect(spy).toHaveBeenCalledWith(fakeSession, ...params); @@ -96,20 +101,22 @@ describe("ZosmfUssApi", () => { password: "password", protocol: "http", user: "aZosmfUser", + ...fakeProperties, }, } as imperative.IProfileLoaded; it("should include profile properties in the built session object", () => { - const api = new ZoweExplorerZosmf.UssApi(); + const api = new ZoweExplorerZosmf.UssApi(loadedProfile); - const transformedProps = { ...exampleProfile.profile, hostname: exampleProfile.profile?.host }; + const transformedProps: Record = { ...exampleProfile.profile, hostname: exampleProfile.profile?.host, ...fakeProperties }; delete transformedProps["host"]; + delete transformedProps["responseTimeout"]; expect((api as any)._getSession(exampleProfile).mISession).toMatchObject(transformedProps); }); }); describe("updateAttributes", () => { - const ussApi = new ZoweExplorerZosmf.UssApi(); + const ussApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const getSessionMock = jest.spyOn(ussApi, "getSession").mockReturnValue(fakeSession); const putUSSPayload = jest.spyOn(zosfiles.Utilities, "putUSSPayload").mockResolvedValue(Buffer.from("test")); @@ -203,20 +210,20 @@ describe("ZosmfUssApi", () => { it("uploads a file from buffer", async () => { const uploadFileSpy = jest.spyOn(zosfiles.Upload, "bufferToUssFile").mockImplementation(); - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const buf = Buffer.from("123abc"); await zosmfApi.uploadFromBuffer(buf, "/some/uss/path"); - expect(uploadFileSpy).toHaveBeenCalledWith(zosmfApi.getSession(), "/some/uss/path", buf, undefined); + expect(uploadFileSpy).toHaveBeenCalledWith(zosmfApi.getSession(), "/some/uss/path", buf, fakeProperties); }); it("constants should be unchanged", () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); expect(zosmfApi.getProfileTypeName()).toMatchSnapshot(); expect(zosmfApi.getTokenTypeName()).toMatchSnapshot(); }); it("getSessionFromCommandArgument should build session from arguments", () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const session = zosmfApi.getSessionFromCommandArgument(fakeProfile as unknown as imperative.ICommandArguments); expect(session).toBeDefined(); const sessCfg: imperative.ISession = { @@ -225,6 +232,7 @@ describe("ZosmfUssApi", () => { type: imperative.SessConstants.AUTH_TYPE_BASIC, }; delete sessCfg["host"]; + delete sessCfg["responseTimeout"]; expect(session.ISession).toMatchObject(sessCfg); }); @@ -234,17 +242,18 @@ describe("ZosmfUssApi", () => { } as unknown as imperative.IProfileLoaded); const session = zosmfApi.getSession(); expect(session).toBeDefined(); - const sessCfg: Partial & { hostname: string; type: string } = { + const sessCfg: Partial & { hostname: string; type: string } = { ...fakeProfile, hostname: fakeProfile.host, type: imperative.SessConstants.AUTH_TYPE_BASIC, }; delete sessCfg.host; + delete sessCfg["responseTimeout"]; expect(session.ISession).toMatchObject(sessCfg); }); it("getSession should build session from profile with token", () => { - const fakeProfileWithToken = { + const fakeProfileWithToken: imperative.IProfile = { ...fakeProfile, tokenType: imperative.SessConstants.TOKEN_TYPE_JWT, tokenValue: "fakeToken", @@ -256,12 +265,13 @@ describe("ZosmfUssApi", () => { } as unknown as imperative.IProfileLoaded); const session = zosmfApi.getSession(); expect(session).toBeDefined(); - const sessCfg: Partial & { hostname: string; type: string } = { + const sessCfg: Partial & { hostname: string; type: string } = { ...fakeProfileWithToken, hostname: fakeProfileWithToken.host, type: imperative.SessConstants.AUTH_TYPE_TOKEN, }; delete sessCfg.host; + delete sessCfg["responseTimeout"]; expect(session.ISession).toMatchObject(sessCfg); }); @@ -274,7 +284,7 @@ describe("ZosmfUssApi", () => { }); it("getStatus should validate active profile", async () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const checkStatusSpy = jest.spyOn(zosmf.CheckStatus, "getZosmfInfo").mockResolvedValue({}); const status = await zosmfApi.getStatus({ profile: fakeProfile } as unknown as imperative.IProfileLoaded, "zosmf"); expect(status).toBe("active"); @@ -282,7 +292,7 @@ describe("ZosmfUssApi", () => { }); it("getStatus should validate inactive profile", async () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const checkStatusSpy = jest.spyOn(zosmf.CheckStatus, "getZosmfInfo").mockResolvedValue(undefined as unknown as zosmf.IZosmfInfoResponse); const status = await zosmfApi.getStatus({ profile: fakeProfile } as unknown as imperative.IProfileLoaded, "zosmf"); expect(status).toBe("inactive"); @@ -290,7 +300,7 @@ describe("ZosmfUssApi", () => { }); it("should test that copy calls zowe.Utilities.putUSSPayload", async () => { - const api = new ZoweExplorerZosmf.UssApi(); + const api = new ZoweExplorerZosmf.UssApi(loadedProfile); api.getSession = jest.fn(); const response = Buffer.from("hello world!"); @@ -303,13 +313,13 @@ describe("ZosmfUssApi", () => { }); it("getStatus should validate unverified profile", async () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const status = await zosmfApi.getStatus({ profile: fakeProfile } as unknown as imperative.IProfileLoaded, "sample"); expect(status).toBe("unverified"); }); it("login and logout should call APIML endpoints", async () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const loginSpy = jest.spyOn(Login, "apimlLogin").mockResolvedValue(""); const logoutSpy = jest.spyOn(Logout, "apimlLogout").mockResolvedValue(); @@ -321,7 +331,7 @@ describe("ZosmfUssApi", () => { }); it("should retrieve the tag of a file", async () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); jest.spyOn(JSON, "parse").mockReturnValue({ stdout: ["-t UTF-8 tesfile.txt"], }); @@ -334,7 +344,7 @@ describe("ZosmfUssApi", () => { }); it("should update the tag attribute when passed in", async () => { - const zosmfApi = new ZoweExplorerZosmf.UssApi(); + const zosmfApi = new ZoweExplorerZosmf.UssApi(loadedProfile); const changeTagSpy = jest.fn(); Object.defineProperty(zosfiles.Utilities, "putUSSPayload", { value: changeTagSpy, @@ -345,7 +355,7 @@ describe("ZosmfUssApi", () => { }); it("calls putUSSPayload to move a directory from old path to new path", async () => { - const api = new ZoweExplorerZosmf.UssApi(); + const api = new ZoweExplorerZosmf.UssApi(loadedProfile); const putUssPayloadSpy = jest.fn(); Object.defineProperty(zosfiles.Utilities, "putUSSPayload", { value: putUssPayloadSpy, @@ -364,7 +374,7 @@ describe("ZosmfUssApi", () => { { name: "fileList", spy: jest.spyOn(zosfiles.List, "fileList"), - args: ["ussPath"], + args: ["ussPath", fakeProperties], }, { name: "isFileTagBinOrAscii", @@ -374,33 +384,33 @@ describe("ZosmfUssApi", () => { { name: "getContents", spy: jest.spyOn(zosfiles.Download, "ussFile"), - args: ["ussPath", {}], + args: ["ussPath", fakeProperties], }, { name: "putContent", spy: jest.spyOn(zosfiles.Upload, "fileToUssFile"), - args: ["localPath", "ussPath", {}], + args: ["localPath", "ussPath", fakeProperties], }, { name: "uploadDirectory", spy: jest.spyOn(zosfiles.Upload, "dirToUSSDirRecursive"), - args: ["localPath", "ussPath", {}], + args: ["localPath", "ussPath", fakeProperties], }, { name: "create", spy: jest.spyOn(zosfiles.Create, "uss"), - args: ["ussPath", "file", "777"], + args: ["ussPath", "file", "777", fakeProperties], }, { name: "delete", spy: jest.spyOn(zosfiles.Delete, "ussFile"), - args: ["/ussPath", false], - transform: (args) => [args[0].slice(1), args[1]], + args: ["/ussPath", false, fakeProperties], + transform: (args) => [args[0].slice(1), args[1], fakeProperties], }, { name: "delete", spy: jest.spyOn(zosfiles.Delete, "ussFile"), - args: ["ussPath", false], + args: ["ussPath", false, fakeProperties], }, { name: "rename", @@ -410,7 +420,7 @@ describe("ZosmfUssApi", () => { ]; ussApis.forEach((ussApi) => { it(`${ussApi?.name} should inject session into Zowe API`, async () => { - await expectApiWithSession(ussApi, new ZoweExplorerZosmf.UssApi()); + await expectApiWithSession(ussApi, new ZoweExplorerZosmf.UssApi(loadedProfile)); }); }); }); @@ -420,38 +430,38 @@ describe("ZosmfMvsApi", () => { { name: "dataSet", spy: jest.spyOn(zosfiles.List, "dataSet"), - args: ["dsname", {}], + args: ["dsname", fakeProperties], }, { name: "allMembers", spy: jest.spyOn(zosfiles.List, "allMembers"), - args: ["dsname", {}], + args: ["dsname", fakeProperties], }, { name: "getContents", spy: jest.spyOn(zosfiles.Download, "dataSet"), - args: ["dsname", {}], + args: ["dsname", fakeProperties], }, { name: "putContents", spy: jest.spyOn(zosfiles.Upload, "pathToDataSet"), - args: ["localPath", "dsname", {}], + args: ["localPath", "dsname", fakeProperties], }, { name: "createDataSet", spy: jest.spyOn(zosfiles.Create, "dataSet"), - args: [0, "dsname", {}], + args: [0, "dsname", fakeProperties], }, { name: "createDataSetMember", spy: jest.spyOn(zosfiles.Upload, "bufferToDataSet"), - args: ["dsname", {}], + args: ["dsname", fakeProperties], transform: (args) => [Buffer.from(""), ...args], }, { name: "allocateLikeDataSet", spy: jest.spyOn(zosfiles.Create, "dataSetLike"), - args: ["dsname1", "dsname2"], + args: ["dsname1", "dsname2", fakeProperties], }, { name: "copyDataSetMember", @@ -459,15 +469,18 @@ describe("ZosmfMvsApi", () => { args: [ { dsn: "dsname1", member: "member1" }, { dsn: "dsname2", member: "member2" }, - { "from-dataset": { dsn: "dsname1", member: "member1" } }, + { "from-dataset": { dsn: "dsname1", member: "member1" }, ...fakeProperties }, ], transform: (args) => [args[1], args[2]], }, { name: "copyDataSetMember", spy: jest.spyOn(zosfiles.Copy, "dataSet"), - args: [{ dsn: "dsname1", member: "member1" }, { dsn: "dsname2", member: "member2" }, {} as any], - transform: (args) => [args[1], { "from-dataset": args[0] }], + args: [ + { dsn: "dsname1", member: "member1" }, + { dsn: "dsname2", member: "member2" }, + ], + transform: (args) => [args[1], { "from-dataset": args[0], ...fakeProperties }], }, { name: "copyDataSetMember", @@ -476,46 +489,57 @@ describe("ZosmfMvsApi", () => { { dsn: "dsname1", member: "member1" }, { dsn: "dsname2", member: "member2" }, ], - transform: (args) => [args[1], { "from-dataset": args[0] }], + transform: (args) => [args[1], { "from-dataset": args[0], ...fakeProperties }], }, { name: "renameDataSet", spy: jest.spyOn(zosfiles.Rename, "dataSet"), - args: ["dsname1", "dsname2"], + args: ["dsname1", "dsname2", fakeProperties], }, { name: "renameDataSetMember", spy: jest.spyOn(zosfiles.Rename, "dataSetMember"), - args: ["dsname", "member1", "member2"], + args: ["dsname", "member1", "member2", fakeProperties], }, { name: "hMigrateDataSet", spy: jest.spyOn(zosfiles.HMigrate, "dataSet"), - args: ["dsname"], + args: ["dsname", fakeProperties], }, { name: "hRecallDataSet", spy: jest.spyOn(zosfiles.HRecall, "dataSet"), - args: ["dsname"], + args: ["dsname", fakeProperties], }, { name: "deleteDataSet", spy: jest.spyOn(zosfiles.Delete, "dataSet"), - args: ["dsname", {}], + args: ["dsname", fakeProperties], + }, + { + name: "dataSetsMatchingPattern", + spy: jest.spyOn(zosfiles.List, "dataSetsMatchingPattern"), + args: [["SAMPLE.A*", "SAMPLE.B*"], fakeProperties], + }, + { + name: "copyDataSet", + spy: jest.spyOn(zosfiles.Copy, "dataSet"), + args: ["FROM.NAME", "TO.NAME", undefined, undefined, fakeProperties], + transform: (args) => [{ dsn: args[1] }, { enq: undefined, "from-dataset": { dsn: args[0] }, replace: undefined, ...fakeProperties }], }, ]; mvsApis.forEach((mvsApi) => { it(`${mvsApi?.name} should inject session into Zowe API`, async () => { - await expectApiWithSession(mvsApi, new ZoweExplorerZosmf.MvsApi()); + await expectApiWithSession(mvsApi, new ZoweExplorerZosmf.MvsApi(loadedProfile)); }); }); it("uploads a data set from buffer", async () => { const uploadFileSpy = jest.spyOn(zosfiles.Upload, "bufferToDataSet").mockImplementation(); - const zosmfApi = new ZoweExplorerZosmf.MvsApi(); + const zosmfApi = new ZoweExplorerZosmf.MvsApi(loadedProfile); const buf = Buffer.from("123abc"); await zosmfApi.uploadFromBuffer(buf, "SOME.DS(MEMB)"); - expect(uploadFileSpy).toHaveBeenCalledWith(zosmfApi.getSession(), buf, "SOME.DS(MEMB)", undefined); + expect(uploadFileSpy).toHaveBeenCalledWith(zosmfApi.getSession(), buf, "SOME.DS(MEMB)", fakeProperties); }); }); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts index c8a0ae561..fe723d190 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts @@ -13,9 +13,17 @@ import * as vscode from "vscode"; import { ZoweTreeNode } from "../../../src/tree/ZoweTreeNode"; import { IZoweTreeNode } from "../../../src/tree/IZoweTreeNode"; import * as imperative from "@zowe/imperative"; -import { BaseProvider } from "../../../src"; describe("ZoweTreeNode", () => { + const innerProfile = { user: "apple", password: "banana" }; + const fakeProfile: imperative.IProfileLoaded = { + name: "amazingProfile", + profile: innerProfile, + message: "", + type: "zosmf", + failNotFound: true, + }; + const makeNode = ( name: string, collapseState: vscode.TreeItemCollapsibleState, @@ -48,8 +56,8 @@ describe("ZoweTreeNode", () => { it("getProfile should return profile of current node", () => { const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined); - node.setProfileToChoice("myProfile" as unknown as imperative.IProfileLoaded); - expect(node.getProfile()).toBe("myProfile"); + node.setProfileToChoice(fakeProfile); + expect(node.getProfile().name).toBe("amazingProfile"); }); it("getProfile should return profile of parent node", () => { @@ -83,49 +91,43 @@ describe("ZoweTreeNode", () => { it("setProfileToChoice should update properties on existing profile object", () => { const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined, undefined, { - name: "oldProfile", - profile: { host: "example.com" }, + ...fakeProfile, }); - node.setProfileToChoice({ name: "newProfile", profile: { host: "example.com", port: 443 } } as unknown as imperative.IProfileLoaded); - // Profile name should not change but properties should - expect(node.getProfileName()).toBe("oldProfile"); + node.setProfileToChoice({ ...fakeProfile, profile: { host: "example.com", port: 443 } }); expect(node.getProfile().profile?.port).toBeDefined(); }); it("setProfileToChoice should update profile for associated FSProvider entry", () => { const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined); node.resourceUri = vscode.Uri.file(__dirname); + const prof = { ...fakeProfile, profile: { ...innerProfile } }; const fsEntry = { metadata: { - profile: { name: "oldProfile" }, + profile: prof, }, }; - node.setProfileToChoice( - { name: "newProfile" } as unknown as imperative.IProfileLoaded, - { - lookup: jest.fn().mockReturnValue(fsEntry), - } as unknown as BaseProvider - ); - expect(node.getProfileName()).toBe("newProfile"); - expect(fsEntry.metadata.profile.name).toBe("newProfile"); + prof.profile.user = "banana"; + prof.profile.password = "apple"; + node.setProfileToChoice(prof); + expect(node.getProfile().profile?.user).toBe("banana"); + expect(node.getProfile().profile?.password).toBe("apple"); + expect(fsEntry.metadata.profile.profile?.user).toBe("banana"); + expect(fsEntry.metadata.profile.profile?.password).toBe("apple"); }); it("setProfileToChoice should update child nodes with the new profile", () => { const node = makeNode("test", vscode.TreeItemCollapsibleState.Expanded, undefined); + node.setProfileToChoice({ ...fakeProfile, profile: { ...fakeProfile.profile, user: "banana" } }); const nodeChild = makeNode("child", vscode.TreeItemCollapsibleState.None, undefined); + nodeChild.setProfileToChoice(node.getProfile()); node.children = [nodeChild as any]; - const setProfileToChoiceChildMock = jest.spyOn(nodeChild, "setProfileToChoice").mockImplementation(); const fsEntry = { metadata: { - profile: { name: "oldProfile" }, + profile: node.getProfile(), }, }; - const mockNewProfile = { name: "newProfile" } as unknown as imperative.IProfileLoaded; - const mockProvider = { - lookup: jest.fn().mockReturnValue(fsEntry), - } as unknown as BaseProvider; - node.setProfileToChoice(mockNewProfile, mockProvider); - expect(node.getProfileName()).toBe("newProfile"); - expect(setProfileToChoiceChildMock).toHaveBeenCalledWith(mockNewProfile, mockProvider); + expect(node.getProfile().profile?.user).toBe("banana"); + expect(nodeChild.getProfile().profile?.user).toBe("banana"); + expect(fsEntry.metadata.profile.profile?.user).toBe("banana"); }); }); diff --git a/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts b/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts index c2fad2367..340da8f7b 100644 --- a/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts +++ b/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts @@ -119,7 +119,7 @@ export namespace ZoweExplorerZosmf { */ export class UssApi extends CommonApi implements MainframeInteraction.IUss { public fileList(ussFilePath: string): Promise { - return zosfiles.List.fileList(this.getSession(), ussFilePath); + return zosfiles.List.fileList(this.getSession(), ussFilePath, { responseTimeout: this.profile?.profile?.responseTimeout }); } public isFileTagBinOrAscii(ussFilePath: string): Promise { @@ -127,11 +127,14 @@ export namespace ZoweExplorerZosmf { } public getContents(inputFilePath: string, options: zosfiles.IDownloadSingleOptions): Promise { - return zosfiles.Download.ussFile(this.getSession(), inputFilePath, options); + return zosfiles.Download.ussFile(this.getSession(), inputFilePath, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public copy(outputPath: string, options?: Omit): Promise { - return zosfiles.Utilities.putUSSPayload(this.getSession(), outputPath, { ...(options ?? {}), request: "copy" }); + return zosfiles.Utilities.putUSSPayload(this.getSession(), outputPath, { ...options, request: "copy" }); } public async move(oldPath: string, newPath: string): Promise { @@ -142,11 +145,17 @@ export namespace ZoweExplorerZosmf { } public uploadFromBuffer(buffer: Buffer, filePath: string, options?: zosfiles.IUploadOptions): Promise { - return zosfiles.Upload.bufferToUssFile(this.getSession(), filePath, buffer, options); + return zosfiles.Upload.bufferToUssFile(this.getSession(), filePath, buffer, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public putContent(inputFilePath: string, ussFilePath: string, options: zosfiles.IUploadOptions): Promise { - return zosfiles.Upload.fileToUssFile(this.getSession(), inputFilePath, ussFilePath, options); + return zosfiles.Upload.fileToUssFile(this.getSession(), inputFilePath, ussFilePath, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public async updateAttributes(ussPath: string, attributes: Partial): Promise { @@ -199,17 +208,20 @@ export namespace ZoweExplorerZosmf { ussDirectoryPath: string, options?: zosfiles.IUploadOptions ): Promise { - return zosfiles.Upload.dirToUSSDirRecursive(this.getSession(), inputDirectoryPath, ussDirectoryPath, options); + return zosfiles.Upload.dirToUSSDirRecursive(this.getSession(), inputDirectoryPath, ussDirectoryPath, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public create(ussPath: string, type: string, mode?: string): Promise { - return zosfiles.Create.uss(this.getSession(), ussPath, type, mode); + return zosfiles.Create.uss(this.getSession(), ussPath, type, mode, { responseTimeout: this.profile?.profile?.responseTimeout }); } public delete(ussPath: string, recursive?: boolean): Promise { // handle zosmf api issue with file paths const fixedName = ussPath.startsWith("/") ? ussPath.substring(1) : ussPath; - return zosfiles.Delete.ussFile(this.getSession(), fixedName, recursive); + return zosfiles.Delete.ussFile(this.getSession(), fixedName, recursive, { responseTimeout: this.profile?.profile?.responseTimeout }); } public async rename(currentUssPath: string, newUssPath: string): Promise { @@ -235,23 +247,35 @@ export namespace ZoweExplorerZosmf { */ export class MvsApi extends CommonApi implements MainframeInteraction.IMvs { public dataSet(filter: string, options?: zosfiles.IListOptions): Promise { - return zosfiles.List.dataSet(this.getSession(), filter, options); + return zosfiles.List.dataSet(this.getSession(), filter, { responseTimeout: this.profile?.profile?.responseTimeout, ...options }); } public allMembers(dataSetName: string, options?: zosfiles.IListOptions): Promise { - return zosfiles.List.allMembers(this.getSession(), dataSetName, options); + return zosfiles.List.allMembers(this.getSession(), dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public getContents(dataSetName: string, options?: zosfiles.IDownloadSingleOptions): Promise { - return zosfiles.Download.dataSet(this.getSession(), dataSetName, options); + return zosfiles.Download.dataSet(this.getSession(), dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public uploadFromBuffer(buffer: Buffer, dataSetName: string, options?: zosfiles.IUploadOptions): Promise { - return zosfiles.Upload.bufferToDataSet(this.getSession(), buffer, dataSetName, options); + return zosfiles.Upload.bufferToDataSet(this.getSession(), buffer, dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public putContents(inputFilePath: string, dataSetName: string, options?: zosfiles.IUploadOptions): Promise { - return zosfiles.Upload.pathToDataSet(this.getSession(), inputFilePath, dataSetName, options); + return zosfiles.Upload.pathToDataSet(this.getSession(), inputFilePath, dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public createDataSet( @@ -259,15 +283,23 @@ export namespace ZoweExplorerZosmf { dataSetName: string, options?: Partial ): Promise { - return zosfiles.Create.dataSet(this.getSession(), dataSetType, dataSetName, options); + return zosfiles.Create.dataSet(this.getSession(), dataSetType, dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public createDataSetMember(dataSetName: string, options?: zosfiles.IUploadOptions): Promise { - return zosfiles.Upload.bufferToDataSet(this.getSession(), Buffer.from(""), dataSetName, options); + return zosfiles.Upload.bufferToDataSet(this.getSession(), Buffer.from(""), dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public allocateLikeDataSet(dataSetName: string, likeDataSetName: string): Promise { - return zosfiles.Create.dataSetLike(this.getSession(), dataSetName, likeDataSetName); + return zosfiles.Create.dataSetLike(this.getSession(), dataSetName, likeDataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + }); } public copyDataSetMember( @@ -282,7 +314,7 @@ export namespace ZoweExplorerZosmf { } else { newOptions = { ...options, - ...{ "from-dataset": { dsn: fromDataSetName, member: fromMemberName } }, + "from-dataset": { dsn: fromDataSetName, member: fromMemberName }, }; } } else { @@ -290,34 +322,59 @@ export namespace ZoweExplorerZosmf { // we will need to break the interface definition in the ZoweExplorerApi newOptions = { "from-dataset": { dsn: fromDataSetName, member: fromMemberName } }; } - return zosfiles.Copy.dataSet(this.getSession(), { dsn: toDataSetName, member: toMemberName }, newOptions); + return zosfiles.Copy.dataSet( + this.getSession(), + { dsn: toDataSetName, member: toMemberName }, + { + responseTimeout: this.profile?.profile?.responseTimeout, + ...newOptions, + } + ); } public renameDataSet(currentDataSetName: string, newDataSetName: string): Promise { - return zosfiles.Rename.dataSet(this.getSession(), currentDataSetName, newDataSetName); + return zosfiles.Rename.dataSet(this.getSession(), currentDataSetName, newDataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + }); } public renameDataSetMember(dataSetName: string, oldMemberName: string, newMemberName: string): Promise { - return zosfiles.Rename.dataSetMember(this.getSession(), dataSetName, oldMemberName, newMemberName); + return zosfiles.Rename.dataSetMember(this.getSession(), dataSetName, oldMemberName, newMemberName, { + responseTimeout: this.profile?.profile?.responseTimeout, + }); } public hMigrateDataSet(dataSetName: string): Promise { - return zosfiles.HMigrate.dataSet(this.getSession(), dataSetName); + return zosfiles.HMigrate.dataSet(this.getSession(), dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + }); } public hRecallDataSet(dataSetName: string): Promise { - return zosfiles.HRecall.dataSet(this.getSession(), dataSetName); + return zosfiles.HRecall.dataSet(this.getSession(), dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + }); } public deleteDataSet(dataSetName: string, options?: zosfiles.IDeleteDatasetOptions): Promise { - return zosfiles.Delete.dataSet(this.getSession(), dataSetName, options); + return zosfiles.Delete.dataSet(this.getSession(), dataSetName, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public dataSetsMatchingPattern(filter: string[], options?: zosfiles.IDsmListOptions): Promise { - return zosfiles.List.dataSetsMatchingPattern(this.getSession(), filter, options); + return zosfiles.List.dataSetsMatchingPattern(this.getSession(), filter, { + responseTimeout: this.profile?.profile?.responseTimeout, + ...options, + }); } public copyDataSet(fromDataSetName: string, toDataSetName: string, enq?: string, replace?: boolean): Promise { - return zosfiles.Copy.dataSet(this.getSession(), { dsn: toDataSetName }, { "from-dataset": { dsn: fromDataSetName }, enq, replace }); + return zosfiles.Copy.dataSet( + this.getSession(), + { dsn: toDataSetName }, + { "from-dataset": { dsn: fromDataSetName }, enq, replace, responseTimeout: this.profile?.profile?.responseTimeout } + ); } } diff --git a/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts b/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts index bdcc061f9..65e4e2bb1 100644 --- a/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts +++ b/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts @@ -101,21 +101,8 @@ export class ZoweTreeNode extends vscode.TreeItem { * @param {imperative.IProfileLoaded} The profile you will set the node to use */ public setProfileToChoice(aProfile: imperative.IProfileLoaded, fsProvider?: BaseProvider): void { - if (this.profile == null) { - this.profile = aProfile; - } else { - // Don't reassign profile, we want to keep object reference shared across nodes - this.profile.profile = aProfile.profile; - } - if (this.resourceUri != null) { - const fsEntry = fsProvider?.lookup(this.resourceUri, true); - if (fsEntry != null) { - fsEntry.metadata.profile = aProfile; - } - } - for (const child of this.children) { - (child as unknown as ZoweTreeNode).setProfileToChoice(aProfile, fsProvider); - } + // Don't reassign profile if its already defined, as we want to keep the reference valid for other nodes and filesystems + this.profile = Object.assign(this.profile ?? {}, aProfile); } /** * Sets the session for this node to the one chosen in parameters. diff --git a/packages/zowe-explorer-ftp-extension/CHANGELOG.md b/packages/zowe-explorer-ftp-extension/CHANGELOG.md index 43628f808..a6537e335 100644 --- a/packages/zowe-explorer-ftp-extension/CHANGELOG.md +++ b/packages/zowe-explorer-ftp-extension/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to the "zowe-explorer-ftp-extension" extension will be docum ### Bug fixes +- Fixed issue where the MVS API `putContents` function did not support PDS members when the member was not specified in the data set name. [#3305](https://github.com/zowe/zowe-explorer-vscode/issues/3305) + ## `3.0.2` ### New features and enhancements diff --git a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts index cca7a1954..932ebf3fe 100644 --- a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts +++ b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts @@ -21,6 +21,7 @@ import { Gui, imperative } from "@zowe/zowe-explorer-api"; import * as globals from "../../../src/globals"; import { ZoweFtpExtensionError } from "../../../src/ZoweFtpExtensionError"; import { mocked } from "../../../__mocks__/mockUtils"; +import { ZosFilesUtils } from "@zowe/zos-files-for-zowe-sdk"; // two methods to mock modules: create a __mocks__ file for zowe-explorer-api.ts and direct mock for extension.ts jest.mock("../../../__mocks__/@zowe/zowe-explorer-api.ts"); @@ -100,7 +101,7 @@ describe("FtpMvsApi", () => { expect((response._readableState.buffer.head?.data ?? response._readableState.buffer).toString()).toContain("Hello world"); }); - it("should upload content to dataset.", async () => { + it("should upload content to dataset - sequential data set", async () => { const localFile = tmp.tmpNameSync({ tmpdir: "/tmp" }); const tmpNameSyncSpy = jest.spyOn(tmp, "tmpNameSync"); const rmSyncSpy = jest.spyOn(fs, "rmSync"); @@ -114,7 +115,7 @@ describe("FtpMvsApi", () => { const mockParams = { inputFilePath: localFile, - dataSetName: " (IBMUSER).DS2", + dataSetName: "IBMUSER.DS2", options: { encoding: "", returnEtag: true, etag: "utf8" }, }; jest.spyOn(MvsApi as any, "getContents").mockResolvedValueOnce({ apiResponse: { etag: "utf8" } }); @@ -130,6 +131,38 @@ describe("FtpMvsApi", () => { expect(rmSyncSpy).toHaveBeenCalled(); }); + it("should generate a member name for PDS upload if one wasn't provided", async () => { + const localFile = tmp.tmpNameSync({ tmpdir: "/tmp" }); + const tmpNameSyncSpy = jest.spyOn(tmp, "tmpNameSync"); + const rmSyncSpy = jest.spyOn(fs, "rmSync"); + + fs.writeFileSync(localFile, "helloPdsMember"); + const response = TestUtils.getSingleLineStream(); + const response2 = { success: true, commandResponse: "", apiResponse: { items: [{ dsname: "IBMUSER.PDS", dsorg: "PO", lrecl: 255 }] } }; + const dataSetMock = jest.spyOn(MvsApi, "dataSet").mockResolvedValue(response2 as any); + const uploadDataSetMock = jest.spyOn(DataSetUtils, "uploadDataSet").mockResolvedValue(response); + jest.spyOn(MvsApi, "getContents").mockResolvedValue({ apiResponse: { etag: "123" } } as any); + + const mockParams = { + inputFilePath: localFile, + dataSetName: "IBMUSER.PDS", + options: { encoding: "", returnEtag: true, etag: "utf8" }, + }; + const generateMemberNameSpy = jest.spyOn(ZosFilesUtils, "generateMemberName"); + jest.spyOn(MvsApi as any, "getContents").mockResolvedValueOnce({ apiResponse: { etag: "utf8" } }); + jest.spyOn(fs, "readFileSync").mockReturnValue("test"); + jest.spyOn(Gui, "warningMessage").mockImplementation(); + const result = await MvsApi.putContents(mockParams.inputFilePath, mockParams.dataSetName, mockParams.options); + expect(generateMemberNameSpy).toHaveBeenCalledWith(localFile); + expect(result.commandResponse).toContain("Data set uploaded successfully."); + expect(dataSetMock).toHaveBeenCalledTimes(1); + expect(uploadDataSetMock).toHaveBeenCalledTimes(1); + expect(MvsApi.releaseConnection).toHaveBeenCalled(); + // check that correct function is called from node-tmp + expect(tmpNameSyncSpy).toHaveBeenCalled(); + expect(rmSyncSpy).toHaveBeenCalled(); + }); + it("should upload single space to dataset when secureFtp is true and contents are empty", async () => { const localFile = tmp.tmpNameSync({ tmpdir: "/tmp" }); diff --git a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts index a8f39a0f5..3c91a13d8 100644 --- a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts +++ b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts @@ -128,12 +128,26 @@ export class FtpMvsApi extends AbstractFtpApi implements MainframeInteraction.IM const result = this.getDefaultResponse(); const profile = this.checkedProfile(); + const dsorg = dsAtrribute.apiResponse.items[0]?.dsorg; + const isPds = dsorg === "PO" || dsorg === "PO-E"; + + /** + * Determine the data set name for uploading. + * + * For PDS: When the input is a file path and the provided data set name doesn't include the member name, + * we'll need to generate a member name. + */ + const uploadName = + isPds && openParens == -1 && typeof input === "string" + ? `${dataSetName}(${zosfiles.ZosFilesUtils.generateMemberName(input)})` + : dataSetName; + const inputIsBuffer = input instanceof Buffer; // Save-Save with FTP requires loading the file first // (moved this block above connection request so only one connection is active at a time) if (options.returnEtag && options.etag) { - const contentsTag = await this.getContentsTag(dataSetName, inputIsBuffer); + const contentsTag = await this.getContentsTag(uploadName, inputIsBuffer); if (contentsTag && contentsTag !== options.etag) { throw Error("Rest API failure with HTTP(S) status 412: Save conflict"); } @@ -174,13 +188,13 @@ export class FtpMvsApi extends AbstractFtpApi implements MainframeInteraction.IM return result; } } - await DataSetUtils.uploadDataSet(connection, dataSetName, transferOptions); + await DataSetUtils.uploadDataSet(connection, uploadName, transferOptions); result.success = true; if (options.returnEtag) { // release this connection instance because a new one will be made with getContentsTag this.releaseConnection(connection); connection = null; - const etag = await this.getContentsTag(dataSetName, inputIsBuffer); + const etag = await this.getContentsTag(uploadName, inputIsBuffer); result.apiResponse = { etag, }; diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 41e7bfbfd..45134fbcc 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -8,6 +8,20 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen ### Bug fixes +- `DatasetFSProvider.stat()` will now throw a `FileNotFound` error for extenders trying to fetch an MVS resource that does not exist. [#3252](https://github.com/zowe/zowe-explorer-vscode/issues/3252) +- Fixed an issue where renaming or deleting a USS file or data set did not update the opened editor. [#3260](https://github.com/zowe/zowe-explorer-vscode/issues/3260) +- Fixed an issue during initialization where a broken team configuration file caused the "Show Config" action in the error dialog to stop working. [#3273](https://github.com/zowe/zowe-explorer-vscode/issues/3273) +- Fixed issue where switching the authentication methods would cause `Cannot read properties of undefined` error. [#3142](https://github.com/zowe/zowe-explorer-vscode/issues/3142) +- Fixed an issue where calling `vscode.workspace.fs.readFile` with a PDS member URI would throw an error when the PDS already existed as a filesystem entry. [#3267](https://github.com/zowe/zowe-explorer-vscode/issues/3267) +- Fixed issue where Zowe Explorer would present the "No configs detected" notification when initialized in a workspace without a Zowe team configuration. [#3280](https://github.com/zowe/zowe-explorer-vscode/issues/3280) +- Reduced the number of MVS API calls performed by `vscode.workspace.fs.readFile` when fetching the contents of a data set entry. [#3278](https://github.com/zowe/zowe-explorer-vscode/issues/3278) +- Fixed an issue to review inconsistent capitalization across translation strings. [#2935](https://github.com/zowe/zowe-explorer-vscode/issues/2935) +- Updated the test for the default credential manager for better compatibility with Cloud-based platforms such as Eclipse Che and Red Hat OpenShift Dev Spaces. [#3297](https://github.com/zowe/zowe-explorer-vscode/pull/3297) +- Fixed issue where users were not prompted to enter credentials if a 401 error was encountered when opening files, data sets or spools in the editor. [#3197](https://github.com/zowe/zowe-explorer-vscode/issues/3197) +- Fixed issue where profile credential updates or token changes were not reflected within the filesystem. [#3289](https://github.com/zowe/zowe-explorer-vscode/issues/3289) +- Fixed issue to update the success message when changing authentication from token to basic through the 'Change Authentication' option. +- Fixed an issue where fetching a USS file using `UssFSProvider.stat()` with a `fetch=true` query would cause Zowe Explorer to get stuck in an infinite loop. + ## `3.0.2` ### New features and enhancements diff --git a/packages/zowe-explorer/__tests__/__integration__/bdd/features/dialogs/ShowConfigErrorDialog.feature b/packages/zowe-explorer/__tests__/__integration__/bdd/features/dialogs/ShowConfigErrorDialog.feature new file mode 100644 index 000000000..3a68bf267 --- /dev/null +++ b/packages/zowe-explorer/__tests__/__integration__/bdd/features/dialogs/ShowConfigErrorDialog.feature @@ -0,0 +1,7 @@ +Feature: Show Config Error Dialog + +Scenario: Initializing Zowe Explorer with a broken profile + When a user opens Zowe Explorer + Then the Show Config dialog should appear + When the user clicks on the "Show Config" button + Then the config should appear in the editor \ No newline at end of file diff --git a/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/dialogs/ShowConfigErrorDialog.steps.ts b/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/dialogs/ShowConfigErrorDialog.steps.ts new file mode 100644 index 000000000..6fc99ad51 --- /dev/null +++ b/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/dialogs/ShowConfigErrorDialog.steps.ts @@ -0,0 +1,54 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { Then, When } from "@cucumber/cucumber"; +import { getZoweExplorerContainer } from "../../../../__common__/shared.wdio"; +import { Notification, Workbench } from "wdio-vscode-service"; + +When("a user opens Zowe Explorer", async function () { + this.zoweExplorerPane = await getZoweExplorerContainer(); + await expect(this.zoweExplorerPane).toBeDefined(); +}); + +Then("the Show Config dialog should appear", async function () { + this.workbench = await browser.getWorkbench(); + let notification: Notification; + const notificationCenter = await (this.workbench as Workbench).openNotificationsCenter(); + await notificationCenter.wait(60000); + await browser.waitUntil(async () => { + const notifications: Notification[] = await notificationCenter.getNotifications("error" as any); + for (const n of notifications) { + if ((await n.getMessage()).startsWith("Error encountered when loading your Zowe config.")) { + notification = n; + return true; + } + } + + return false; + }); + await expect(notification).toBeDefined(); + this.configErrorDialog = notification; + await (this.configErrorDialog as Notification).wait(); +}); + +When('the user clicks on the "Show Config" button', async function () { + const button = await this.configErrorDialog.elem.$("a[role='button']"); + await expect(button).toBeClickable(); + await button.click(); +}); + +Then("the config should appear in the editor", async function () { + const editorView = (this.workbench as Workbench).getEditorView(); + await editorView.wait(); + await browser.waitUntil(async () => (await editorView.getOpenEditorTitles()).length > 0); + const editorTitles = await editorView.getOpenEditorTitles(); + await expect(editorTitles.some((editorTitle) => editorTitle.includes("zowe.config.json"))).toBe(true); +}); diff --git a/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/profiles/UpdateCredentials.steps.ts b/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/profiles/UpdateCredentials.steps.ts index cb0cd76fb..c8b80ee65 100644 --- a/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/profiles/UpdateCredentials.steps.ts +++ b/packages/zowe-explorer/__tests__/__integration__/bdd/step_definitions/profiles/UpdateCredentials.steps.ts @@ -13,7 +13,6 @@ import * as fs from "fs"; import * as path from "path"; import { AfterAll, Then, When } from "@cucumber/cucumber"; import { paneDivForTree } from "../../../../__common__/shared.wdio"; -import { TreeItem } from "wdio-vscode-service"; import quickPick from "../../../../__pageobjects__/QuickPick"; import { Key } from "webdriverio"; @@ -54,9 +53,17 @@ When(/a user who has profile with (.*) auth in team config/, function (authType: }); When("the user has a profile in their Data Sets tree", async function () { this.treePane = await paneDivForTree("Data Sets"); - this.profileNode = (await this.treePane.getVisibleItems()).pop() as TreeItem; - await expect(this.profileNode).toBeDefined(); - await expect(await this.profileNode.getLabel()).toContain(this.authType); + await browser.waitUntil(async () => { + const visibleItems = await this.treePane.getVisibleItems(); + for (const item of visibleItems) { + if ((await item.getLabel()) === `zosmf_${this.authType as string}`) { + this.profileNode = item; + return true; + } + } + + return false; + }); }); When("a user clicks search button for the profile", async function () { await this.profileNode.elem.moveTo(); diff --git a/packages/zowe-explorer/__tests__/__integration__/bdd/wdio.conf.ts b/packages/zowe-explorer/__tests__/__integration__/bdd/wdio.conf.ts index f3aa0a4ca..a0ff1bcaa 100644 --- a/packages/zowe-explorer/__tests__/__integration__/bdd/wdio.conf.ts +++ b/packages/zowe-explorer/__tests__/__integration__/bdd/wdio.conf.ts @@ -13,6 +13,7 @@ import type { Options } from "@wdio/types"; import { join as joinPath, resolve as resolvePath } from "path"; import { emptyDirSync } from "fs-extra"; import { baseConfig } from "../../__common__/base.wdio.conf"; +import { renameSync, rmSync, writeFileSync } from "fs"; const dataDir = joinPath(__dirname, "..", "..", "__common__", ".wdio-vscode-service", "data"); const screenshotDir = joinPath(__dirname, "results", "screenshots"); @@ -152,6 +153,24 @@ export const config: Options.Testrunner = { emptyDirSync(screenshotDir); }, + beforeFeature: async function (uri, feature) { + if (feature.name === "Show Config Error Dialog") { + const configPath = joinPath(process.env["ZOWE_CLI_HOME"], "zowe.config.json"); + const backupConfigPath = joinPath(process.env["ZOWE_CLI_HOME"], "zowe.config.bkp"); + renameSync(configPath, backupConfigPath); + writeFileSync(configPath, "invalidjson"); + } + }, + + afterFeature: async function (uri, feature) { + if (feature.name === "Show Config Error Dialog") { + const backupConfigPath = joinPath(process.env["ZOWE_CLI_HOME"], "zowe.config.bkp"); + const configPath = joinPath(process.env["ZOWE_CLI_HOME"], "zowe.config.json"); + rmSync(configPath); + renameSync(backupConfigPath, configPath); + } + }, + afterStep: async function (step, scenario, result, context) { if (!result.passed) { await browser.saveScreenshot(joinPath(screenshotDir, `${scenario.name} - ${step.text}.png`)); diff --git a/packages/zowe-explorer/__tests__/__mocks__/vscode.ts b/packages/zowe-explorer/__tests__/__mocks__/vscode.ts index 8ab63f761..5c32ad980 100644 --- a/packages/zowe-explorer/__tests__/__mocks__/vscode.ts +++ b/packages/zowe-explorer/__tests__/__mocks__/vscode.ts @@ -1498,6 +1498,11 @@ export namespace workspace { } } +// We need to do this since "delete" is a reserved keyword and cannot be defined as a function name. +Object.defineProperty(workspace.fs, "delete", { + value: jest.fn(), +}); + export interface InputBoxOptions { placeholder?: 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 813d265b7..7c627681c 100644 --- a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts @@ -1050,7 +1050,7 @@ describe("Profiles Unit Tests - function checkCurrentProfile", () => { jest.spyOn(AuthUtils, "isUsingTokenAuth").mockResolvedValueOnce(true); environmentSetup(globalMocks); setupProfilesCheck(globalMocks); - const ssoLoginSpy = jest.spyOn(Profiles.getInstance(), "ssoLogin").mockResolvedValueOnce(); + const ssoLoginSpy = jest.spyOn(Profiles.getInstance(), "ssoLogin").mockResolvedValueOnce(true); jest.spyOn(Profiles.getInstance(), "loadNamedProfile").mockReturnValueOnce(globalMocks.testProfile); await expect(Profiles.getInstance().checkCurrentProfile(globalMocks.testProfile)).resolves.toEqual({ name: "sestest", status: "active" }); expect(ssoLoginSpy).toHaveBeenCalledTimes(1); @@ -2281,4 +2281,171 @@ describe("Profiles Unit Tests - function basicAuthClearSecureArray", () => { getProfileInfoMock.mockRestore(); getProfileFromConfigMock.mockRestore(); }); + + it("does not call Config.delete when user and password arg's are missing in mergeArgsForProfile", async () => { + const teamCfgMock = { + delete: jest.fn(), + save: jest.fn(), + set: jest.fn(), + }; + const profAttrsMock = { + isDefaultProfile: false, + profName: "example_profile", + profType: "zosmf", + profLoc: { + jsonLoc: undefined, + }, + }; + const mergeArgsMock = { + knownArgs: [], + }; + const getProfileInfoMock = jest.spyOn(Profiles.getInstance(), "getProfileInfo").mockResolvedValue({ + getTeamConfig: jest.fn().mockReturnValue(teamCfgMock), + mergeArgsForProfile: jest.fn().mockReturnValue(mergeArgsMock), + } as any); + const getProfileFromConfigMock = jest.spyOn(Profiles.getInstance(), "getProfileFromConfig").mockResolvedValue(profAttrsMock); + + await Profiles.getInstance().basicAuthClearSecureArray("example_profile"); + expect(teamCfgMock.delete).not.toHaveBeenCalled(); + expect(teamCfgMock.set).not.toHaveBeenCalled(); + expect(teamCfgMock.save).toHaveBeenCalled(); + getProfileInfoMock.mockRestore(); + getProfileFromConfigMock.mockRestore(); + }); +}); + +describe("Profiles Unit Tests - function tokenAuthClearSecureArray", () => { + it("calls Config APIs when profLoc.jsonLoc is valid, no loginTokenType provided", async () => { + const teamCfgMock = { + delete: jest.fn(), + save: jest.fn(), + set: jest.fn(), + }; + const profAttrsMock = { + isDefaultProfile: false, + profName: "example_profile", + profType: "zosmf", + profLoc: { + jsonLoc: "/user/path/to/zowe.config.json", + locType: imperative.ProfLocType.TEAM_CONFIG, + }, + }; + const mergeArgsMock = { + knownArgs: [ + { + argName: "tokenType", + argLoc: { + jsonLoc: "profiles.example_profile.properties.tokenType", + }, + }, + { + argName: "tokenValue", + argLoc: { + jsonLoc: "profiles.example_profile.properties.tokenValue", + }, + }, + { + argName: "tokenExpiration", + argLoc: { + jsonLoc: "profiles.example_profile.properties.tokenExpiration", + }, + }, + ], + }; + const getProfileInfoMock = jest.spyOn(Profiles.getInstance(), "getProfileInfo").mockResolvedValue({ + getTeamConfig: jest.fn().mockReturnValue(teamCfgMock), + mergeArgsForProfile: jest.fn().mockReturnValue(mergeArgsMock), + } as any); + const getProfileFromConfigMock = jest.spyOn(Profiles.getInstance(), "getProfileFromConfig").mockResolvedValue(profAttrsMock); + + await Profiles.getInstance().tokenAuthClearSecureArray("example_profile"); + expect(teamCfgMock.delete).toHaveBeenCalledWith(mergeArgsMock.knownArgs[0].argLoc.jsonLoc); + expect(teamCfgMock.delete).toHaveBeenCalledWith(mergeArgsMock.knownArgs[1].argLoc.jsonLoc); + expect(teamCfgMock.delete).toHaveBeenCalledWith(mergeArgsMock.knownArgs[2].argLoc.jsonLoc); + expect(teamCfgMock.set).toHaveBeenCalledWith(`${profAttrsMock.profLoc.jsonLoc}.secure`, ["user", "password"]); + expect(teamCfgMock.save).toHaveBeenCalled(); + getProfileInfoMock.mockRestore(); + getProfileFromConfigMock.mockRestore(); + }); + it("calls Config APIs when profLoc.jsonLoc is valid, loginTokenType provided", async () => { + const teamCfgMock = { + delete: jest.fn(), + save: jest.fn(), + set: jest.fn(), + }; + const profAttrsMock = { + isDefaultProfile: false, + profName: "example_profile", + profType: "zosmf", + profLoc: { + jsonLoc: "/user/path/to/zowe.config.json", + locType: imperative.ProfLocType.TEAM_CONFIG, + }, + }; + const mergeArgsMock = { + knownArgs: [ + { + argName: "tokenType", + argLoc: { + jsonLoc: "profiles.example_profile.properties.tokenType", + }, + }, + { + argName: "tokenValue", + argLoc: { + jsonLoc: "profiles.example_profile.properties.tokenValue", + }, + }, + { + argName: "tokenExpiration", + argLoc: { + jsonLoc: "profiles.example_profile.properties.tokenExpiration", + }, + }, + ], + }; + const getProfileInfoMock = jest.spyOn(Profiles.getInstance(), "getProfileInfo").mockResolvedValue({ + getTeamConfig: jest.fn().mockReturnValue(teamCfgMock), + mergeArgsForProfile: jest.fn().mockReturnValue(mergeArgsMock), + } as any); + const getProfileFromConfigMock = jest.spyOn(Profiles.getInstance(), "getProfileFromConfig").mockResolvedValue(profAttrsMock); + + await Profiles.getInstance().tokenAuthClearSecureArray("example_profile", "apimlAuthenticationToken"); + expect(teamCfgMock.delete).toHaveBeenCalledWith(mergeArgsMock.knownArgs[0].argLoc.jsonLoc); + expect(teamCfgMock.delete).toHaveBeenCalledWith(mergeArgsMock.knownArgs[1].argLoc.jsonLoc); + expect(teamCfgMock.delete).toHaveBeenCalledWith(mergeArgsMock.knownArgs[2].argLoc.jsonLoc); + expect(teamCfgMock.set).toHaveBeenCalledWith(`${profAttrsMock.profLoc.jsonLoc}.secure`, []); + expect(teamCfgMock.save).toHaveBeenCalled(); + getProfileInfoMock.mockRestore(); + getProfileFromConfigMock.mockRestore(); + }); + it("does not call Config.delete when tokenType, tokenValue, tokenExpiration arg's are missing in mergeArgsForProfile", async () => { + const teamCfgMock = { + delete: jest.fn(), + save: jest.fn(), + set: jest.fn(), + }; + const profAttrsMock = { + isDefaultProfile: false, + profName: "example_profile", + profType: "zosmf", + profLoc: { + jsonLoc: undefined, + }, + }; + const mergeArgsMock = { + knownArgs: [], + }; + const getProfileInfoMock = jest.spyOn(Profiles.getInstance(), "getProfileInfo").mockResolvedValue({ + getTeamConfig: jest.fn().mockReturnValue(teamCfgMock), + mergeArgsForProfile: jest.fn().mockReturnValue(mergeArgsMock), + } as any); + const getProfileFromConfigMock = jest.spyOn(Profiles.getInstance(), "getProfileFromConfig").mockResolvedValue(profAttrsMock); + await Profiles.getInstance().tokenAuthClearSecureArray("example_profile"); + expect(teamCfgMock.delete).not.toHaveBeenCalled(); + expect(teamCfgMock.set).not.toHaveBeenCalled(); + expect(teamCfgMock.save).toHaveBeenCalled(); + getProfileInfoMock.mockRestore(); + getProfileFromConfigMock.mockRestore(); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerZosmfApi.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerZosmfApi.unit.test.ts index f206d9969..1f2b445f0 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerZosmfApi.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerZosmfApi.unit.test.ts @@ -30,7 +30,9 @@ describe("Zosmf API tests", () => { const api = new ZoweExplorerZosmf.MvsApi(); api.getSession = jest.fn(); - await api.copyDataSetMember({ dsn: "IBM.FROM", member: "IEFBR14" }, { dsn: "IBM.TO", member: "IEFBR15" }); + await api.copyDataSetMember({ dsn: "IBM.FROM", member: "IEFBR14" }, { dsn: "IBM.TO", member: "IEFBR15" }, { + responseTimeout: undefined, + } as any); }); it("should test that copy data set uses enq", async () => { @@ -46,7 +48,7 @@ describe("Zosmf API tests", () => { await api.copyDataSetMember( { dsn: "IBM.FROM", member: "IEFBR14" }, { dsn: "IBM.TO", member: "IEFBR15" }, - { enq: "SHR", "from-dataset": { dsn: "BROADCOM.FROM" } } + { enq: "SHR", "from-dataset": { dsn: "BROADCOM.FROM" }, responseTimeout: undefined } ); }); @@ -62,6 +64,7 @@ describe("Zosmf API tests", () => { api.getSession = jest.fn(); await api.copyDataSetMember({ dsn: "IBM.FROM", member: "IEFBR14" }, { dsn: "IBM.TO", member: "IEFBR15" }, { enq: "SHR", + responseTimeout: undefined, } as any); }); @@ -80,6 +83,7 @@ describe("Zosmf API tests", () => { await api.putContent("someLocalFile.txt", "/some/remote", { encoding: "285", + responseTimeout: undefined, }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/extending/__snapshots__/ZoweExplorerZosmfApi.unit.test.ts.snap b/packages/zowe-explorer/__tests__/__unit__/extending/__snapshots__/ZoweExplorerZosmfApi.unit.test.ts.snap index c3eed47e8..b8a072080 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extending/__snapshots__/ZoweExplorerZosmfApi.unit.test.ts.snap +++ b/packages/zowe-explorer/__tests__/__unit__/extending/__snapshots__/ZoweExplorerZosmfApi.unit.test.ts.snap @@ -3,6 +3,7 @@ exports[`Zosmf API tests should test putContent method passes all options to Zowe api method 1`] = ` { "encoding": "285", + "responseTimeout": undefined, } `; @@ -12,6 +13,7 @@ exports[`Zosmf API tests should test that copy data set uses default options 1`] "dsn": "IBM.FROM", "member": "IEFBR14", }, + "responseTimeout": undefined, } `; @@ -21,6 +23,7 @@ exports[`Zosmf API tests should test that copy data set uses enq 1`] = ` "from-dataset": { "dsn": "BROADCOM.FROM", }, + "responseTimeout": undefined, } `; @@ -31,5 +34,6 @@ exports[`Zosmf API tests should test that copy data set uses enq only 1`] = ` "dsn": "IBM.FROM", "member": "IEFBR14", }, + "responseTimeout": undefined, } `; diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts index dcfa23e15..9352efd1f 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts @@ -66,7 +66,7 @@ function createGlobalMocks() { testFavoritesNode: createDatasetFavoritesNode(), testDatasetTree: null, getContentsSpy: null, - fspDelete: jest.spyOn(DatasetFSProvider.prototype, "delete").mockImplementation(), + fspDelete: jest.spyOn(vscode.workspace.fs, "delete").mockImplementation(), statusBarMsgSpy: null, mvsApi: null, mockShowWarningMessage: jest.fn(), @@ -96,7 +96,6 @@ function createGlobalMocks() { value: newMocks.mockShowWarningMessage, configurable: true, }); - Object.defineProperty(vscode.workspace.fs, "delete", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.window, "showInputBox", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.workspace, "openTextDocument", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.workspace, "getConfiguration", { value: jest.fn(), configurable: true }); @@ -487,7 +486,7 @@ describe("Dataset Actions Unit Tests - Function deleteDatasetPrompt", () => { blockMocks.testDatasetTree.getTreeView.mockReturnValueOnce(treeView); globalMocks.mockShowWarningMessage.mockResolvedValueOnce("Delete"); - jest.spyOn(DatasetFSProvider.instance, "delete").mockImplementation(); + jest.spyOn(vscode.workspace.fs, "delete").mockImplementation(); await DatasetActions.deleteDatasetPrompt(blockMocks.testDatasetTree); expect(mocked(Gui.showMessage)).toHaveBeenCalledWith( @@ -721,7 +720,7 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { }); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(DatasetFSProvider.instance, "delete").mockImplementation(); + const deleteSpy = jest.spyOn(vscode.workspace.fs, "delete").mockImplementation(); await DatasetActions.deleteDataset(node, blockMocks.testDatasetTree); expect(deleteSpy).toHaveBeenCalledWith(node.resourceUri, { recursive: false }); }); @@ -737,7 +736,7 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { }); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(DatasetFSProvider.instance, "delete").mockImplementation(); + const deleteSpy = jest.spyOn(vscode.workspace.fs, "delete").mockImplementation(); await DatasetActions.deleteDataset(node, blockMocks.testDatasetTree); expect(deleteSpy).toHaveBeenCalledWith(node.resourceUri, { recursive: false }); }); @@ -753,7 +752,7 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { }); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - jest.spyOn(DatasetFSProvider.instance, "delete").mockRejectedValueOnce(Error("not found")); + jest.spyOn(vscode.workspace.fs, "delete").mockRejectedValueOnce(Error("not found")); await expect(DatasetActions.deleteDataset(node, blockMocks.testDatasetTree)).rejects.toThrow("not found"); expect(mocked(Gui.showMessage)).toHaveBeenCalledWith("Unable to find file " + node.label?.toString()); }); @@ -769,7 +768,7 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { }); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - jest.spyOn(DatasetFSProvider.instance, "delete").mockRejectedValueOnce(Error("")); + jest.spyOn(vscode.workspace.fs, "delete").mockRejectedValueOnce(Error("")); await expect(DatasetActions.deleteDataset(node, blockMocks.testDatasetTree)).rejects.toThrow(""); expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error"); }); @@ -792,7 +791,7 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { node.contextValue = Constants.DS_PDS_CONTEXT + Constants.FAV_SUFFIX; mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(DatasetFSProvider.instance, "delete"); + const deleteSpy = jest.spyOn(vscode.workspace.fs, "delete"); await DatasetActions.deleteDataset(node, blockMocks.testDatasetTree); @@ -812,7 +811,7 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { const child = new ZoweDatasetNode({ label: "child", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: parent }); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(DatasetFSProvider.instance, "delete").mockImplementation(); + const deleteSpy = jest.spyOn(vscode.workspace.fs, "delete").mockImplementation(); await DatasetActions.deleteDataset(child, blockMocks.testDatasetTree); expect(deleteSpy).toHaveBeenCalledWith(child.resourceUri, { recursive: false }); @@ -841,7 +840,7 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { blockMocks.testDatasetTree.mFavorites[0].children.push(child); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(DatasetFSProvider.instance, "delete").mockImplementation(); + const deleteSpy = jest.spyOn(vscode.workspace.fs, "delete").mockImplementation(); await DatasetActions.deleteDataset(child, blockMocks.testDatasetTree); expect(deleteSpy).toHaveBeenCalledWith(child.resourceUri, { recursive: false }); expect(blockMocks.testDatasetTree.removeFavorite).toHaveBeenCalledWith(child); @@ -864,7 +863,7 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { }); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(DatasetFSProvider.instance, "delete").mockImplementation(); + const deleteSpy = jest.spyOn(vscode.workspace.fs, "delete").mockImplementation(); deleteSpy.mockClear(); await expect(DatasetActions.deleteDataset(child, blockMocks.testDatasetTree)).rejects.toThrow("Cannot delete, item invalid."); expect(deleteSpy).not.toHaveBeenCalled(); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index 2af16a725..54eb2acf0 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -298,7 +298,7 @@ describe("readFile", () => { throw FileSystemError.FileNotFound(uri as Uri); }); const lookupParentDir = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(null); - const remoteLookupForResourceMock = jest.spyOn(DatasetFSProvider.instance, "remoteLookupForResource").mockResolvedValue(testEntries.pds); + const fetchDatasetAtUriMock = jest.spyOn(DatasetFSProvider.instance, "fetchDatasetAtUri").mockResolvedValue(testEntries.pds); let err; try { @@ -311,7 +311,7 @@ describe("readFile", () => { expect(_lookupAsFileMock).toHaveBeenCalledWith(testUris.ps); _lookupAsFileMock.mockRestore(); lookupParentDir.mockRestore(); - remoteLookupForResourceMock.mockRestore(); + fetchDatasetAtUriMock.mockRestore(); }); it("throws an error if the entry does not exist and the error is not FileNotFound", async () => { @@ -339,7 +339,7 @@ describe("readFile", () => { profile: testProfile, path: "/USER.DATA.PS", }); - const fetchDatasetAtUriMock = jest.spyOn(DatasetFSProvider.instance, "fetchDatasetAtUri").mockImplementation(); + const fetchDatasetAtUriMock = jest.spyOn(DatasetFSProvider.instance, "fetchDatasetAtUri").mockResolvedValueOnce(new DsEntry("USER.DATA.PS")); await DatasetFSProvider.instance.readFile(testUris.ps); expect(_lookupAsFileMock).toHaveBeenCalledWith(testUris.ps); @@ -348,53 +348,44 @@ describe("readFile", () => { _getInfoFromUriMock.mockRestore(); }); - it("checks if parent dir exists when lookup fails & calls remoteLookupForResource if parent dir doesn't exist", async () => { + it("calls fetchDatasetAtUri if entry does not exist locally", async () => { const _lookupAsFileMock = jest .spyOn(DatasetFSProvider.instance as any, "_lookupAsFile") .mockImplementationOnce(() => { throw FileSystemError.FileNotFound(testUris.pdsMember); }) .mockReturnValue(testEntries.pdsMember); - - const fetchDatasetAtUriMock = jest.spyOn(DatasetFSProvider.instance, "fetchDatasetAtUri").mockImplementation(); - const _lookupParentDirectoryMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(null); + const fetchDatasetAtUriMock = jest + .spyOn(DatasetFSProvider.instance, "fetchDatasetAtUri") + .mockResolvedValueOnce(new DsEntry("USER.DATA.PDS(MEMBER)")); const _getInfoFromUriMock = jest.spyOn(DatasetFSProvider.instance as any, "_getInfoFromUri").mockReturnValueOnce({ profile: testProfile, path: "/USER.DATA.PS", }); - const remoteLookupForResourceMock = jest - .spyOn(DatasetFSProvider.instance, "remoteLookupForResource") - .mockResolvedValue(testEntries.pdsMember); await DatasetFSProvider.instance.readFile(testUris.pdsMember); expect(_lookupAsFileMock).toHaveBeenCalledWith(testUris.pdsMember); - expect(_lookupParentDirectoryMock).toHaveBeenCalledWith(testUris.pdsMember, true); - expect(remoteLookupForResourceMock).toHaveBeenCalledWith(testUris.pdsMember); expect(fetchDatasetAtUriMock).toHaveBeenCalledWith(testUris.pdsMember, { isConflict: false }); _getInfoFromUriMock.mockRestore(); + fetchDatasetAtUriMock.mockRestore(); }); it("throws error if parent exists and file cannot be found", async () => { const _lookupAsFileMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsFile").mockImplementationOnce(() => { throw FileSystemError.FileNotFound(testUris.pdsMember); }); - const _lookupParentDirectoryMock = jest - .spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory") - .mockReturnValueOnce(testEntries.pds); const _getInfoFromUriMock = jest.spyOn(DatasetFSProvider.instance as any, "_getInfoFromUri").mockReturnValueOnce({ profile: testProfile, path: "/USER.DATA.PS", }); - const remoteLookupForResourceMock = jest - .spyOn(DatasetFSProvider.instance, "remoteLookupForResource") - .mockReset() - .mockResolvedValue(testEntries.pdsMember); - + const fetchDatasetAtUriMock = jest.spyOn(DatasetFSProvider.instance, "fetchDatasetAtUri").mockResolvedValueOnce(null); await expect(DatasetFSProvider.instance.readFile(testUris.pdsMember)).rejects.toThrow(); expect(_lookupAsFileMock).toHaveBeenCalledWith(testUris.pdsMember); - expect(_lookupParentDirectoryMock).toHaveBeenCalledWith(testUris.pdsMember, true); - expect(remoteLookupForResourceMock).not.toHaveBeenCalledWith(testUris.pdsMember); + expect(fetchDatasetAtUriMock).toHaveBeenCalledWith(testUris.pdsMember, { isConflict: false }); + _getInfoFromUriMock.mockRestore(); + _lookupAsFileMock.mockRestore(); + fetchDatasetAtUriMock.mockRestore(); }); it("returns the data for an entry", async () => { @@ -734,6 +725,33 @@ describe("fetchEntriesForDataset", () => { describe("fetchDataset", () => { describe("calls dataSet to verify that the data set exists on the mainframe", () => { describe("PS", () => { + it("non-existent PS URI", async () => { + const dataSetMock = jest.fn().mockResolvedValue({ + success: true, + apiResponse: { + items: [], + }, + commandResponse: "", + }); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ + dataSet: dataSetMock, + } as any); + try { + await (DatasetFSProvider.instance as any).fetchDataset(testUris.ps, { + isRoot: false, + slashAfterProfilePos: testUris.ps.path.indexOf("/", 1), + profileName: "sestest", + profile: testProfile, + }); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe(testUris.ps.toString(true)); + } + expect(dataSetMock).toHaveBeenCalled(); + mvsApiMock.mockRestore(); + }); + it("non-existent URI", async () => { const dataSetMock = jest.fn().mockResolvedValue({ success: true, @@ -747,7 +765,7 @@ describe("fetchDataset", () => { } as any); await (DatasetFSProvider.instance as any).fetchDataset(testUris.ps, { isRoot: false, - slashAfterProfilePos: testUris.pds.path.indexOf("/", 1), + slashAfterProfilePos: testUris.ps.path.indexOf("/", 1), profileName: "sestest", profile: testProfile, }); @@ -772,6 +790,36 @@ describe("fetchDataset", () => { }); describe("PDS", () => { + it("non-existent PDS URI", async () => { + const dataSetMock = jest.fn().mockResolvedValue({ + success: true, + apiResponse: { + items: [], + }, + commandResponse: "", + }); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ + dataSet: dataSetMock, + } as any); + const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup"); + lookupMock.mockImplementation(() => { + throw FileSystemError.FileNotFound(testUris.pds); + }); + try { + await (DatasetFSProvider.instance as any).fetchDataset(testUris.pds, { + isRoot: false, + slashAfterProfilePos: testUris.pds.path.indexOf("/", 1), + profileName: "sestest", + profile: testProfile, + }); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe(testUris.pds.toString(true)); + } + lookupMock.mockRestore(); + mvsApiMock.mockRestore(); + }); it("non-existent URI", async () => { const dataSetMock = jest.fn().mockResolvedValue({ success: true, @@ -807,6 +855,63 @@ describe("fetchDataset", () => { fetchEntriesForDatasetMock.mockRestore(); }); }); + + describe("PDS member", () => { + it("non-existent member URI", async () => { + const allMembersMockNoMatch = jest.fn().mockResolvedValue({ + success: true, + apiResponse: { + items: [ + { + member: "NOMATCH", + }, + ], + }, + commandResponse: "", + }); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ + allMembers: allMembersMockNoMatch, + } as any); + try { + await (DatasetFSProvider.instance as any).fetchDataset(testUris.pdsMember, { + isRoot: false, + slashAfterProfilePos: testUris.pds.path.indexOf("/", 1), + profileName: "sestest", + profile: testProfile, + }); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe(testUris.pdsMember.toString(true)); + } + expect(allMembersMockNoMatch).toHaveBeenCalledWith("USER.DATA.PDS"); + mvsApiMock.mockRestore(); + }); + it("existing member URI", async () => { + const allMembersMock = jest.fn().mockResolvedValue({ + success: true, + apiResponse: { + items: [ + { + member: "MEMBER1", + }, + ], + }, + commandResponse: "", + }); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ + allMembers: allMembersMock, + } as any); + await (DatasetFSProvider.instance as any).fetchDataset(testUris.pdsMember, { + isRoot: false, + slashAfterProfilePos: testUris.pdsMember.path.indexOf("/", 1), + profileName: "sestest", + profile: testProfile, + }); + expect(allMembersMock).toHaveBeenCalledWith("USER.DATA.PDS"); + mvsApiMock.mockRestore(); + }); + }); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetInit.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetInit.unit.test.ts index 16a83c6c8..a0d97af00 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetInit.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetInit.unit.test.ts @@ -17,6 +17,7 @@ import { SharedContext } from "../../../../src/trees/shared/SharedContext"; import { DatasetActions } from "../../../../src/trees/dataset/DatasetActions"; import { DatasetInit } from "../../../../src/trees/dataset/DatasetInit"; import { SharedInit } from "../../../../src/trees/shared/SharedInit"; +import { ProfilesUtils } from "../../../../src/utils/ProfilesUtils"; describe("Test src/dataset/extension", () => { describe("initDatasetProvider", () => { @@ -39,7 +40,9 @@ describe("Test src/dataset/extension", () => { filterPrompt: jest.fn(), rename: jest.fn(), onDidChangeConfiguration: jest.fn(), - getTreeView: jest.fn(), + getTreeView: jest.fn().mockReturnValue({ + onDidChangeVisibility: jest.fn(), + }), sortPdsMembersDialog: jest.fn(), filterPdsMembersDialog: jest.fn(), openWithEncoding: jest.fn(), @@ -144,7 +147,11 @@ describe("Test src/dataset/extension", () => { name: "zowe.ds.pasteDataSets:1", parm: [false], mock: [ - { spy: jest.spyOn(dsProvider, "getTreeView"), arg: [], ret: { reveal: jest.fn(), selection: [test.value] } }, + { + spy: jest.spyOn(dsProvider, "getTreeView"), + arg: [], + ret: { reveal: jest.fn(), onDidChangeVisibility: jest.fn(), selection: [test.value] }, + }, { spy: jest.spyOn(DatasetActions, "pasteDataSetMembers"), arg: [dsProvider, test.value] }, { spy: jest.spyOn(DatasetActions, "refreshDataset"), arg: ["test", dsProvider] }, ], @@ -152,7 +159,11 @@ describe("Test src/dataset/extension", () => { { name: "zowe.ds.pasteDataSets:2", mock: [ - { spy: jest.spyOn(dsProvider, "getTreeView"), arg: [], ret: { reveal: jest.fn(), selection: [test.value] } }, + { + spy: jest.spyOn(dsProvider, "getTreeView"), + arg: [], + ret: { reveal: jest.fn(), onDidChangeVisibility: jest.fn(), selection: [test.value] }, + }, { spy: jest.spyOn(DatasetActions, "pasteDataSetMembers"), arg: [dsProvider, test.value] }, { spy: jest.spyOn(DatasetActions, "refreshDataset"), arg: ["test", dsProvider] }, ], @@ -204,12 +215,11 @@ describe("Test src/dataset/extension", () => { onDidChangeConfiguration = (fun: () => void) => { return { onDidChangeConfiguration: fun }; }; - spyCreateDatasetTree = jest.spyOn(DatasetInit, "createDatasetTree"); + spyCreateDatasetTree = jest.spyOn(DatasetInit, "createDatasetTree").mockResolvedValue(dsProvider as any); jest.spyOn(SharedInit, "initSubscribers").mockImplementation(jest.fn()); Object.defineProperty(vscode.commands, "registerCommand", { value: registerCommand }); Object.defineProperty(vscode.workspace, "onDidChangeConfiguration", { value: onDidChangeConfiguration }); - spyCreateDatasetTree.mockResolvedValue(dsProvider as any); await DatasetInit.initDatasetProvider(test.context); }); beforeEach(() => { @@ -227,4 +237,12 @@ describe("Test src/dataset/extension", () => { expect(myProvider).toBe(null); }); }); + + describe("datasetTreeVisibilityChanged", () => { + it("calls ProfilesUtils.promptUserWithNoConfigs if visible", async () => { + const promptUserWithNoConfigsMock = jest.spyOn(ProfilesUtils, "promptUserWithNoConfigs").mockImplementation(); + await DatasetInit.datasetTreeVisibilityChanged({ visible: true }); + expect(promptUserWithNoConfigsMock).toHaveBeenCalled(); + }); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts index 1cb7e8149..2ea97e539 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts @@ -2232,7 +2232,7 @@ describe("Dataset Tree Unit Tests - Function rename", () => { mvsApi, profileInstance, mockCheckCurrentProfile, - rename: jest.spyOn(DatasetFSProvider.instance, "rename").mockImplementation(), + rename: jest.spyOn(vscode.workspace.fs, "rename").mockImplementation(), }; } @@ -2479,7 +2479,7 @@ describe("Dataset Tree Unit Tests - Function rename", () => { favProfileNode.children.push(favParent); testTree.mFavorites.push(favProfileNode); const renameDataSetMemberSpy = jest.spyOn((DatasetTree as any).prototype, "renameDataSetMember"); - const renameMock = jest.spyOn(DatasetFSProvider.instance, "rename").mockImplementation(); + const renameMock = jest.spyOn(vscode.workspace.fs, "rename").mockImplementation(); await testTree.rename(child); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts index 4a8a58897..ab8c496a6 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts @@ -10,12 +10,13 @@ */ import { Disposable, FilePermission, FileType, Uri, window } from "vscode"; -import { FsJobsUtils, FilterEntry, Gui, JobEntry, SpoolEntry, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { FsJobsUtils, FilterEntry, Gui, JobEntry, SpoolEntry, ZoweScheme, imperative } from "@zowe/zowe-explorer-api"; import { createIProfile } from "../../../__mocks__/mockCreators/shared"; import { createIJobFile, createIJobObject } from "../../../__mocks__/mockCreators/jobs"; import { ZoweExplorerApiRegister } from "../../../../src/extending/ZoweExplorerApiRegister"; import { JobFSProvider } from "../../../../src/trees/job/JobFSProvider"; import { MockedProperty } from "../../../__mocks__/mockUtils"; +import { AuthUtils } from "../../../../src/utils/AuthUtils"; const testProfile = createIProfile(); @@ -222,6 +223,47 @@ describe("fetchSpoolAtUri", () => { jesApiMock.mockRestore(); lookupAsFileMock.mockRestore(); }); + it("fetches the spool contents for a given URI - getSpoolContentById", async () => { + const lookupAsFileMock = jest + .spyOn(JobFSProvider.instance as any, "_lookupAsFile") + .mockReturnValueOnce({ ...testEntries.spool, data: new Uint8Array() }); + const lookupParentDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce({ ...testEntries.job }); + const mockJesApi = { + getSpoolContentById: jest.fn((opts) => { + return "spool contents"; + }), + }; + const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); + const entry = await JobFSProvider.instance.fetchSpoolAtUri(testUris.spool); + expect(lookupAsFileMock).toHaveBeenCalledWith(testUris.spool); + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.spool); + expect(mockJesApi.getSpoolContentById).toHaveBeenCalled(); + expect(entry.data.toString()).toStrictEqual("spool contents"); + jesApiMock.mockRestore(); + lookupAsFileMock.mockRestore(); + }); + + it("calls AuthUtils.promptForAuthError when an error occurs", async () => { + const lookupAsFileMock = jest + .spyOn(JobFSProvider.instance as any, "_lookupAsFile") + .mockReturnValueOnce({ ...testEntries.spool, data: new Uint8Array() }); + const mockJesApi = { + downloadSingleSpool: jest.fn((opts) => { + throw new imperative.ImperativeError({ + msg: "Failed to download spool", + errorCode: "401" + }); + }), + }; + const promptForAuthErrorMock = jest.spyOn(AuthUtils, "promptForAuthError").mockImplementation(); + const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); + await expect(JobFSProvider.instance.fetchSpoolAtUri(testUris.spool)).rejects.toThrow(); + expect(promptForAuthErrorMock).toHaveBeenCalled(); + expect(lookupAsFileMock).toHaveBeenCalledWith(testUris.spool); + expect(mockJesApi.downloadSingleSpool).toHaveBeenCalled(); + jesApiMock.mockRestore(); + lookupAsFileMock.mockRestore(); + }); }); describe("readFile", () => { @@ -321,7 +363,7 @@ describe("delete", () => { const lookupParentDirMock = jest .spyOn(JobFSProvider.instance as any, "_lookupParentDirectory") .mockReturnValueOnce({ ...testEntries.session }); - await JobFSProvider.instance.delete(testUris.job, { recursive: true, deleteRemote: true }); + await JobFSProvider.instance.delete(testUris.job, { recursive: true }); const jobInfo = testEntries.job.job; expect(jobInfo).not.toBeUndefined(); expect(mockUssApi.deleteJob).toHaveBeenCalledWith(jobInfo?.jobname || "TESTJOB", jobInfo?.jobid || "JOB12345"); @@ -341,7 +383,7 @@ describe("delete", () => { fakeJob.job = testEntries.job.job; const lookupMock = jest.spyOn(JobFSProvider.instance as any, "lookup").mockReturnValueOnce(fakeSpool); const lookupParentDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(fakeJob); - await JobFSProvider.instance.delete(testUris.spool, { recursive: true, deleteRemote: true }); + await JobFSProvider.instance.delete(testUris.spool, { recursive: true }); expect(mockUssApi.deleteJob).not.toHaveBeenCalled(); expect(lookupParentDirMock).not.toHaveBeenCalled(); ussApiMock.mockRestore(); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobTree.unit.test.ts index 5e573cdbe..3cbd4eba2 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobTree.unit.test.ts @@ -135,7 +135,7 @@ async function createGlobalMocks() { }; jest.spyOn(JobFSProvider.instance, "createDirectory").mockImplementation(globalMocks.FileSystemProvider.createDirectory); - jest.spyOn(JobFSProvider.instance, "delete").mockImplementation(globalMocks.FileSystemProvider.delete); + jest.spyOn(vscode.workspace.fs, "delete").mockImplementation(globalMocks.FileSystemProvider.delete); jest.spyOn(Gui, "createTreeView").mockImplementation(globalMocks.createTreeView); Object.defineProperty(ProfilesCache, "getConfigInstance", { value: jest.fn(() => { diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts index 54f98b38d..ebdceae45 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/ZoweJobNode.unit.test.ts @@ -821,7 +821,11 @@ describe("ZosJobsProvider - Function searchPrompt", () => { const globalMocks = await createGlobalMocks(); jest.spyOn(globalMocks.testJobsProvider, "applySavedFavoritesSearchLabel").mockReturnValue(undefined); const applySearchLabelToNode = jest.spyOn(globalMocks.testJobsProvider, "applySearchLabelToNode"); - const jobSessionNode = new ZoweJobNode({ label: "sestest", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed }); + const jobSessionNode = new ZoweJobNode({ + label: "sestest", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + profile: createIProfile(), + }); jobSessionNode.contextValue = Constants.JOBS_SESSION_CONTEXT + Constants.FAV_SUFFIX; await globalMocks.testJobsProvider.searchPrompt(jobSessionNode); expect(applySearchLabelToNode).toHaveBeenCalled(); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts index 17cfac602..42b7e41da 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts @@ -103,7 +103,7 @@ function createGlobalMocks() { }; jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(globalMocks.FileSystemProvider.createDirectory); - jest.spyOn(UssFSProvider.instance, "rename").mockImplementation(globalMocks.FileSystemProvider.rename); + jest.spyOn(vscode.workspace.fs, "rename").mockImplementation(globalMocks.FileSystemProvider.rename); globalMocks.mockTextDocuments.push(globalMocks.mockTextDocumentDirty); globalMocks.mockTextDocuments.push(globalMocks.mockTextDocumentClean); @@ -1687,7 +1687,7 @@ describe("USSTree Unit Tests - Function crossLparMove", () => { ]; ussDirNode.dirty = false; - const deleteMock = jest.spyOn(UssFSProvider.instance, "delete").mockResolvedValue(undefined); + const deleteMock = jest.spyOn(vscode.workspace.fs, "delete").mockResolvedValue(undefined); const readFileMock = jest.spyOn(UssFSProvider.instance, "readFile").mockResolvedValue(new Uint8Array([1, 2, 3])); const writeFileMock = jest.spyOn(UssFSProvider.instance, "writeFile").mockResolvedValue(undefined); const existsMock = jest.spyOn(UssFSProvider.instance, "exists").mockReturnValueOnce(false); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index 7e53ac169..362823061 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -10,12 +10,13 @@ */ import { Disposable, FilePermission, FileSystemError, FileType, TextEditor, Uri, workspace } from "vscode"; -import { BaseProvider, DirEntry, FileEntry, Gui, UssDirectory, UssFile, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { BaseProvider, DirEntry, FileEntry, Gui, imperative, UssDirectory, UssFile, ZoweScheme } from "@zowe/zowe-explorer-api"; import { Profiles } from "../../../../src/configuration/Profiles"; import { createIProfile } from "../../../__mocks__/mockCreators/shared"; import { ZoweExplorerApiRegister } from "../../../../src/extending/ZoweExplorerApiRegister"; import { UssFSProvider } from "../../../../src/trees/uss/UssFSProvider"; import { USSFileStructure } from "../../../../src/trees/uss/USSFileStructure"; +import { AuthUtils } from "../../../../src/utils/AuthUtils"; const testProfile = createIProfile(); @@ -322,6 +323,28 @@ describe("fetchFileAtUri", () => { expect(fileEntry.data?.byteLength).toBe(exampleData.length); autoDetectEncodingMock.mockRestore(); }); + it("returns early if it failed to fetch contents", async () => { + const fileEntry = { ...testEntries.file }; + fileEntry.wasAccessed = false; + const _fireSoonSpy = jest.spyOn((UssFSProvider as any).prototype, "_fireSoon"); + const lookupAsFileMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsFile").mockReturnValueOnce(fileEntry); + const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockImplementation(); + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + getContents: jest.fn().mockRejectedValue(new imperative.ImperativeError({ msg: "Error fetching contents" })), + } as any); + const promptForAuthErrorMock = jest.spyOn(AuthUtils, "promptForAuthError").mockImplementation(); + + await expect(UssFSProvider.instance.fetchFileAtUri(testUris.file)).resolves.not.toThrow(); + + expect(lookupAsFileMock).toHaveBeenCalledWith(testUris.file); + expect(promptForAuthErrorMock).toHaveBeenCalled(); + expect(autoDetectEncodingMock).toHaveBeenCalledWith(fileEntry); + expect(_fireSoonSpy).not.toHaveBeenCalled(); + autoDetectEncodingMock.mockRestore(); + promptForAuthErrorMock.mockRestore(); + ussApiMock.mockRestore(); + lookupAsFileMock.mockRestore(); + }); it("calls getContents to get the data for a file entry with encoding", async () => { const fileEntry = { ...testEntries.file }; const lookupAsFileMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsFile").mockReturnValueOnce(fileEntry); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts index 5d00e1ac6..56f052c5b 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts @@ -430,7 +430,7 @@ describe("ZoweUSSNode Unit Tests - Function node.rename()", () => { uss: { addSingleSession: jest.fn(), mSessionNodes: [], refresh: jest.fn() } as any, job: { addSingleSession: jest.fn(), mSessionNodes: [], refresh: jest.fn() } as any, }), - renameSpy: jest.spyOn(UssFSProvider.instance, "rename").mockImplementation(), + renameSpy: jest.spyOn(vscode.workspace.fs, "rename").mockImplementation(), getEncodingForFile: jest.spyOn(UssFSProvider.instance as any, "getEncodingForFile").mockReturnValue(undefined), }; newMocks.ussDir.contextValue = Constants.USS_DIR_CONTEXT; @@ -443,7 +443,7 @@ describe("ZoweUSSNode Unit Tests - Function node.rename()", () => { const newFullPath = "/u/user/newName"; const errMessageMock = jest.spyOn(Gui, "errorMessage").mockImplementation(); - const renameMock = jest.spyOn(UssFSProvider.instance, "rename").mockRejectedValueOnce(new Error("Rename error: file is busy")); + const renameMock = jest.spyOn(vscode.workspace.fs, "rename").mockRejectedValueOnce(new Error("Rename error: file is busy")); await blockMocks.ussDir.rename(newFullPath); expect(errMessageMock).toHaveBeenCalledWith("Rename error: file is busy"); @@ -631,7 +631,7 @@ describe("ZoweUSSNode Unit Tests - Function node.deleteUSSNode()", () => { session: globalMocks.session, profile: globalMocks.profileOne, }), - fspDelete: jest.spyOn(UssFSProvider.instance, "delete").mockImplementation(), + fspDelete: jest.spyOn(vscode.workspace.fs, "delete").mockImplementation(), }; newMocks.ussNode = new ZoweUSSNode({ @@ -679,7 +679,7 @@ describe("ZoweUSSNode Unit Tests - Function node.deleteUSSNode()", () => { const globalMocks = createGlobalMocks(); const blockMocks = createBlockMocks(globalMocks); globalMocks.mockShowWarningMessage.mockResolvedValueOnce("Delete"); - jest.spyOn(UssFSProvider.instance, "delete").mockImplementationOnce(() => { + jest.spyOn(vscode.workspace.fs, "delete").mockImplementationOnce(() => { throw Error("testError"); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/AuthUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/AuthUtils.unit.test.ts new file mode 100644 index 000000000..2eacb3358 --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/utils/AuthUtils.unit.test.ts @@ -0,0 +1,30 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { imperative } from "@zowe/zowe-explorer-api"; +import { AuthUtils } from "../../../src/utils/AuthUtils"; + +describe("AuthUtils", () => { + describe("promptForAuthError", () => { + it("should prompt for authentication", async () => { + const errorDetails = new imperative.ImperativeError({ + errorCode: 401 as unknown as string, + msg: "All configured authentication methods failed", + }); + const profile = { type: "zosmf" } as any; + const promptForAuthenticationMock = jest + .spyOn(AuthUtils, "promptForAuthentication") + .mockImplementation(async () => Promise.resolve(true)); + AuthUtils.promptForAuthError(errorDetails, profile); + expect(promptForAuthenticationMock).toHaveBeenCalledWith(errorDetails, profile); + }); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts index 2f20c7d61..10abf1c9f 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts @@ -65,6 +65,7 @@ describe("ProfilesUtils unit tests", () => { Object.defineProperty(ZoweLogger, "info", { value: jest.fn(), configurable: true }); Object.defineProperty(SettingsConfig, "getDirectValue", { value: newMocks.mockGetDirectValue, configurable: true }); Object.defineProperty(ProfilesUtils, "PROFILE_SECURITY", { value: Constants.ZOWE_CLI_SCM, configurable: true }); + Object.defineProperty(ProfilesUtils, "checkDefaultCredentialManager", { value: jest.fn(), configurable: true }); return newMocks; } @@ -232,7 +233,6 @@ describe("ProfilesUtils unit tests", () => { await AuthUtils.errorHandling(errorDetails, label, moreInfo); expect(showErrorSpy).toHaveBeenCalledTimes(1); expect(promptCredentialsSpy).not.toHaveBeenCalled(); - expect(showMsgSpy).toHaveBeenCalledWith("Operation Cancelled"); showErrorSpy.mockClear(); showMsgSpy.mockClear(); promptCredentialsSpy.mockClear(); @@ -460,6 +460,7 @@ describe("ProfilesUtils unit tests", () => { describe("initializeZoweFolder", () => { it("should create directories and files that do not exist", async () => { const blockMocks = createBlockMocks(); + jest.spyOn(ProfilesUtils, "checkDefaultCredentialManager").mockReturnValue(true); blockMocks.mockGetDirectValue.mockReturnValue(true); blockMocks.mockExistsSync.mockReturnValue(false); jest.spyOn(fs, "readFileSync").mockReturnValue(Buffer.from(JSON.stringify({ overrides: { credentialManager: "@zowe/cli" } }), "utf-8")); @@ -472,6 +473,7 @@ describe("ProfilesUtils unit tests", () => { it("should skip creating directories and files that already exist", async () => { const blockMocks = createBlockMocks(); + jest.spyOn(ProfilesUtils, "checkDefaultCredentialManager").mockReturnValue(true); jest.spyOn(ProfilesUtils, "getCredentialManagerOverride").mockReturnValue("@zowe/cli"); blockMocks.mockGetDirectValue.mockReturnValue("@zowe/cli"); blockMocks.mockExistsSync.mockReturnValue(true); @@ -569,7 +571,7 @@ describe("ProfilesUtils unit tests", () => { it("should handle Imperative error thrown on read config from disk", async () => { const testError = new imperative.ImperativeError({ msg: "readConfigFromDisk failed" }); - const initZoweFolderSpy = jest.spyOn(ProfilesUtils, "initializeZoweFolder").mockReturnValueOnce(); + const initZoweFolderSpy = jest.spyOn(ProfilesUtils, "initializeZoweFolder").mockResolvedValueOnce(); const readConfigFromDiskSpy = jest.spyOn(ProfilesUtils, "readConfigFromDisk").mockRejectedValueOnce(testError); await ProfilesUtils.initializeZoweProfiles((msg) => ZoweExplorerExtender.showZoweConfigError(msg)); expect(initZoweFolderSpy).toHaveBeenCalledTimes(1); @@ -579,7 +581,7 @@ describe("ProfilesUtils unit tests", () => { it("should handle JSON parse error thrown on read config from disk", async () => { const testError = new Error("readConfigFromDisk failed"); - const initZoweFolderSpy = jest.spyOn(ProfilesUtils, "initializeZoweFolder").mockReturnValueOnce(); + const initZoweFolderSpy = jest.spyOn(ProfilesUtils, "initializeZoweFolder").mockResolvedValueOnce(); const readConfigFromDiskSpy = jest.spyOn(ProfilesUtils, "readConfigFromDisk").mockRejectedValueOnce(testError); const showZoweConfigErrorSpy = jest.spyOn(ZoweExplorerExtender, "showZoweConfigError").mockReturnValueOnce(); await ProfilesUtils.initializeZoweProfiles((msg) => ZoweExplorerExtender.showZoweConfigError(msg)); @@ -652,6 +654,7 @@ describe("ProfilesUtils unit tests", () => { it("should update the credential manager setting if secure value is true", () => { jest.spyOn(SettingsConfig, "isConfigSettingSetByUser").mockReturnValue(false); jest.spyOn(SettingsConfig, "getDirectValue").mockReturnValueOnce(true); + jest.spyOn(ProfilesUtils, "checkDefaultCredentialManager").mockReturnValue(true); const loggerInfoSpy = jest.spyOn(ZoweLogger, "info"); const recordCredMgrInConfigSpy = jest.spyOn(imperative.CredentialManagerOverride, "recordCredMgrInConfig"); ProfilesUtils.updateCredentialManagerSetting(); @@ -682,7 +685,8 @@ describe("ProfilesUtils unit tests", () => { let getCredentialManagerMapSpy: jest.SpyInstance; let setupCustomCredentialManagerSpy: jest.SpyInstance; let readProfilesFromDiskSpy: jest.SpyInstance; - let promptAndDisableCredentialManagementSpy: jest.SpyInstance; + let disableCredentialManagementSpy: jest.SpyInstance; + let checkDefaultCredentialManagerSpy: jest.SpyInstance; beforeEach(() => { jest.clearAllMocks(); @@ -695,7 +699,8 @@ describe("ProfilesUtils unit tests", () => { getCredentialManagerMapSpy = jest.spyOn(ProfilesUtils, "getCredentialManagerMap"); setupCustomCredentialManagerSpy = jest.spyOn(ProfilesUtils, "setupCustomCredentialManager"); readProfilesFromDiskSpy = jest.spyOn(imperative.ProfileInfo.prototype, "readProfilesFromDisk"); - promptAndDisableCredentialManagementSpy = jest.spyOn(ProfilesUtils, "promptAndDisableCredentialManagement"); + disableCredentialManagementSpy = jest.spyOn(ProfilesUtils, "disableCredentialManagement"); + checkDefaultCredentialManagerSpy = jest.spyOn(ProfilesUtils, "checkDefaultCredentialManager"); }); it("should retrieve the custom credential manager", async () => { @@ -723,10 +728,11 @@ describe("ProfilesUtils unit tests", () => { await expect(ProfilesUtils.getProfileInfo()).resolves.toEqual({}); }); - it("should retrieve the default credential manager and prompt to disable credential management if environment not supported", async () => { + it("should throw exception of readProfilesFromDiskSpy fails", async () => { const expectedErrMsg = // eslint-disable-next-line max-len "Failed to load credential manager. This may be related to Zowe Explorer being unable to use the default credential manager in a browser based environment."; + checkDefaultCredentialManagerSpy.mockReturnValue(false); getDirectValueSpy.mockReturnValueOnce(false); getCredentialManagerOverrideSpy.mockReturnValue("@zowe/cli"); isVSCodeCredentialPluginInstalledSpy.mockReturnValueOnce(false); @@ -744,11 +750,11 @@ describe("ProfilesUtils unit tests", () => { throw err; }); await expect(ProfilesUtils.getProfileInfo()).rejects.toThrow(expectedErrMsg); - expect(promptAndDisableCredentialManagementSpy).toHaveBeenCalledTimes(1); }); it("should ignore error if it is not an instance of ProfInfoErr", async () => { const expectedErrorMsg = "Another error unrelated to credential management"; + checkDefaultCredentialManagerSpy.mockReturnValue(true); getDirectValueSpy.mockReturnValueOnce(false); getCredentialManagerOverrideSpy.mockReturnValue("@zowe/cli"); isVSCodeCredentialPluginInstalledSpy.mockReturnValueOnce(false); @@ -759,7 +765,7 @@ describe("ProfilesUtils unit tests", () => { throw new Error(expectedErrorMsg); }); await expect(ProfilesUtils.getProfileInfo()).resolves.not.toThrow(); - expect(promptAndDisableCredentialManagementSpy).toHaveBeenCalledTimes(0); + expect(disableCredentialManagementSpy).toHaveBeenCalledTimes(0); }); }); @@ -963,10 +969,11 @@ describe("ProfilesUtils unit tests", () => { }); }); - describe("promptAndDisableCredentialManagement", () => { + describe("disableCredentialManagement", () => { let setDirectValueSpy: jest.SpyInstance; let warningMessageSpy: jest.SpyInstance; let executeCommandSpy: jest.SpyInstance; + let getDirectValueSpy: jest.SpyInstance; beforeEach(() => { jest.clearAllMocks(); @@ -975,33 +982,16 @@ describe("ProfilesUtils unit tests", () => { setDirectValueSpy = jest.spyOn(SettingsConfig, "setDirectValue"); warningMessageSpy = jest.spyOn(Gui, "warningMessage"); executeCommandSpy = jest.spyOn(vscode.commands, "executeCommand"); + getDirectValueSpy = jest.spyOn(SettingsConfig, "getDirectValue"); }); - it("should prompt whether to disable credential management, and disable globally if 'Yes, globally' selected", async () => { + it("should show warning that credential management was disabled", async () => { warningMessageSpy.mockResolvedValue("Yes, globally"); - await expect(ProfilesUtils.promptAndDisableCredentialManagement()).resolves.not.toThrow(); + getDirectValueSpy.mockReturnValueOnce(true); + await expect(ProfilesUtils.disableCredentialManagement()).resolves.not.toThrow(); expect(setDirectValueSpy).toHaveBeenCalledWith(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED, false, vscode.ConfigurationTarget.Global); expect(executeCommandSpy).toHaveBeenCalledWith("workbench.action.reloadWindow"); }); - - it("should prompt whether to disable credential management, and disable on workspace if 'Only for this workspace' selected", async () => { - warningMessageSpy.mockResolvedValue("Only for this workspace"); - await expect(ProfilesUtils.promptAndDisableCredentialManagement()).resolves.not.toThrow(); - expect(setDirectValueSpy).toHaveBeenCalledWith( - Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED, - false, - vscode.ConfigurationTarget.Workspace - ); - expect(executeCommandSpy).toHaveBeenCalledWith("workbench.action.reloadWindow"); - }); - - it("should prompt whether to disable credential management, and throw error if 'No'", async () => { - warningMessageSpy.mockResolvedValue("No"); - await expect(ProfilesUtils.promptAndDisableCredentialManagement()).rejects.toThrow( - // eslint-disable-next-line max-len - "Failed to load credential manager. This may be related to Zowe Explorer being unable to use the default credential manager in a browser based environment." - ); - }); }); describe("v1ProfileOptions", () => { @@ -1108,6 +1098,20 @@ describe("ProfilesUtils unit tests", () => { }; } + it("should return early if profileInfo is nullish", async () => { + const blockMocks = getBlockMocks(); + blockMocks.getValueMock.mockReturnValueOnce(Definitions.V1MigrationStatus.JustMigrated); + blockMocks.setValueMock.mockImplementation(); + const getProfInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue(undefined as any); + const onlyV1ProfilesExistMock = new MockedProperty(imperative.ProfileInfo, "onlyV1ProfilesExist", { get: () => true }); + await ProfilesUtils.handleV1MigrationStatus(); + expect(getProfInfoMock).toHaveBeenCalled(); + expect(onlyV1ProfilesExistMock.mock).not.toHaveBeenCalled(); + blockMocks.getValueMock.mockRestore(); + blockMocks.setValueMock.mockRestore(); + onlyV1ProfilesExistMock[Symbol.dispose](); + }); + it("should call executeCommand with zowe.ds.addSession if the migration status is CreateConfigSelected", async () => { const blockMocks = getBlockMocks(); const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockImplementation(); @@ -1256,7 +1260,22 @@ describe("ProfilesUtils unit tests", () => { }); describe("promptUserWithNoConfigs", () => { + it("returns early if user was already prompted in this session", async () => { + const noConfigDialogShownMock = new MockedProperty(ProfilesUtils, "noConfigDialogShown", { value: true }); + const getProfInfoSpy = jest.spyOn(ProfilesUtils, "getProfileInfo"); + await ProfilesUtils.promptUserWithNoConfigs(); + expect(getProfInfoSpy).not.toHaveBeenCalled(); + noConfigDialogShownMock[Symbol.dispose](); + }); + it("returns early if profileInfo is nullish", async () => { + const profInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue(undefined as any); + const showMessageSpy = jest.spyOn(Gui, "showMessage"); + await ProfilesUtils.promptUserWithNoConfigs(); + expect(showMessageSpy).not.toHaveBeenCalled(); + profInfoMock.mockRestore(); + }); it("prompts the user if they don't have any Zowe client configs", async () => { + const noConfigDialogShownMock = new MockedProperty(ProfilesUtils, "noConfigDialogShown", { value: false }); const profInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue({ getTeamConfig: () => ({ exists: false }), } as any); @@ -1273,11 +1292,16 @@ describe("ProfilesUtils unit tests", () => { expect(profInfoMock).toHaveBeenCalled(); profInfoMock.mockRestore(); onlyV1ProfsExistMock[Symbol.dispose](); + noConfigDialogShownMock[Symbol.dispose](); }); it("executes zowe.ds.addSession if the user selects 'Create New' in the prompt", async () => { const profInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue({ getTeamConfig: () => ({ exists: false }), } as any); + const noConfigDialogShownMock = new MockedProperty(ProfilesUtils, "noConfigDialogShown", { + configurable: true, + value: false, + }); const onlyV1ProfsExistMock = new MockedProperty(imperative.ProfileInfo, "onlyV1ProfilesExist", { configurable: true, get: () => false, @@ -1294,11 +1318,16 @@ describe("ProfilesUtils unit tests", () => { executeCommandMock.mockRestore(); profInfoMock.mockRestore(); onlyV1ProfsExistMock[Symbol.dispose](); + noConfigDialogShownMock[Symbol.dispose](); }); it("does not prompt the user if they have a Zowe team config", async () => { const profInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue({ getTeamConfig: () => ({ exists: true }), } as any); + const noConfigDialogShownMock = new MockedProperty(ProfilesUtils, "noConfigDialogShown", { + configurable: true, + value: false, + }); const onlyV1ProfsExistMock = new MockedProperty(imperative.ProfileInfo, "onlyV1ProfilesExist", { configurable: true, get: () => false, @@ -1312,11 +1341,16 @@ describe("ProfilesUtils unit tests", () => { expect(profInfoMock).toHaveBeenCalled(); profInfoMock.mockRestore(); onlyV1ProfsExistMock[Symbol.dispose](); + noConfigDialogShownMock[Symbol.dispose](); }); it("does not prompt the user if they have v1 profiles", async () => { const profInfoMock = jest.spyOn(ProfilesUtils, "getProfileInfo").mockResolvedValue({ getTeamConfig: () => ({ exists: false }), } as any); + const noConfigDialogShownMock = new MockedProperty(ProfilesUtils, "noConfigDialogShown", { + configurable: true, + value: false, + }); const onlyV1ProfsExistMock = new MockedProperty(imperative.ProfileInfo, "onlyV1ProfilesExist", { configurable: true, get: () => true, @@ -1330,6 +1364,7 @@ describe("ProfilesUtils unit tests", () => { expect(profInfoMock).toHaveBeenCalled(); profInfoMock.mockRestore(); onlyV1ProfsExistMock[Symbol.dispose](); + noConfigDialogShownMock[Symbol.dispose](); }); }); diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index ef955fbf6..987ad22af 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -57,10 +57,9 @@ "Credential manager display name" ] }, - "Yes, globally": "Yes, globally", - "Only for this workspace": "Only for this workspace", - "Zowe Explorer failed to activate since the default credential manager is not supported in your environment.": "Zowe Explorer failed to activate since the default credential manager is not supported in your environment.", - "Do you wish to disable credential management? (VS Code window reload will be triggered)": "Do you wish to disable credential management? (VS Code window reload will be triggered)", + "Zowe Explorer's default credential manager is not supported in your environment. Consider installing a custom solution for your platform. Click Reload to start Zowe Explorer without a credential manager.": "Zowe Explorer's default credential manager is not supported in your environment. Consider installing a custom solution for your platform. Click Reload to start Zowe Explorer without a credential manager.", + "Reload window": "Reload window", + "Default Zowe credentials manager not found on current platform. This is typically the case when running in container-based environments or Linux systems that miss required security libraries or user permissions.": "Default Zowe credentials manager not found on current platform. This is typically the case when running in container-based environments or Linux systems that miss required security libraries or user permissions.", "No custom credential managers found, using the default instead.": "No custom credential managers found, using the default instead.", "Custom credential manager {0} found/Credential manager display name": { "message": "Custom credential manager {0} found", @@ -159,7 +158,6 @@ "Enter the path to the certificate key for authenticating the connection.": "Enter the path to the certificate key for authenticating the connection.", "Certificate Keys": "Certificate Keys", "Select Certificate Key": "Select Certificate Key", - "Required parameter 'host' must not be blank.": "Required parameter 'host' must not be blank.", "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out./Label": { "message": "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.", "comment": [ @@ -173,6 +171,7 @@ ] }, "Update Credentials": "Update Credentials", + "Required parameter 'host' must not be blank.": "Required parameter 'host' must not be blank.", "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct./Profile name": { "message": "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", "comment": [ diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index c5ed995fc..3dcc072f7 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -463,10 +463,9 @@ "Zowe explorer profiles are being set as secured.": "", "Custom credential manager failed to activate": "", "Custom credential manager {0} found, attempting to activate.": "", - "Yes, globally": "", - "Only for this workspace": "", - "Zowe Explorer failed to activate since the default credential manager is not supported in your environment.": "", - "Do you wish to disable credential management? (VS Code window reload will be triggered)": "", + "Zowe Explorer's default credential manager is not supported in your environment. Consider installing a custom solution for your platform. Click Reload to start Zowe Explorer without a credential manager.": "", + "Reload window": "", + "Default Zowe credentials manager not found on current platform. This is typically the case when running in container-based environments or Linux systems that miss required security libraries or user permissions.": "", "No custom credential managers found, using the default instead.": "", "Custom credential manager {0} found": "", "Do you wish to use this credential manager instead?": "", @@ -509,10 +508,10 @@ "Enter the path to the certificate key for authenticating the connection.": "", "Certificate Keys": "", "Select Certificate Key": "", - "Required parameter 'host' must not be blank.": "", "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.": "", "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.": "", "Update Credentials": "", + "Required parameter 'host' must not be blank.": "", "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.": "", "Use the search button to list USS files": "", "Invalid node": "", diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index de3fd1b59..d657f7f55 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -44,11 +44,11 @@ export class Profiles extends ProfilesCache { Constants.PROFILES_CACHE = Profiles.loader; try { await Profiles.loader.refresh(ZoweExplorerApiRegister.getInstance()); + await Profiles.getInstance().getProfileInfo(); } catch (err) { ZoweLogger.error(err); ZoweExplorerExtender.showZoweConfigError(err.message); } - await Profiles.getInstance().getProfileInfo(); return Profiles.loader; } @@ -834,8 +834,14 @@ export class Profiles extends ProfilesCache { if (profAttrs.profLoc.jsonLoc) { configApi.set(`${profAttrs.profLoc.jsonLoc}.secure`, loginTokenType?.startsWith("apimlAuthenticationToken") ? [] : ["tokenValue"]); } - configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "user")?.argLoc.jsonLoc); - configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "password")?.argLoc.jsonLoc); + const userArgJsonLoc = profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "user")?.argLoc.jsonLoc; + if (userArgJsonLoc) { + configApi.delete(userArgJsonLoc); + } + const passwordArgJsonLoc = profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "password")?.argLoc.jsonLoc; + if (passwordArgJsonLoc) { + configApi.delete(passwordArgJsonLoc); + } await configApi.save(); } @@ -848,11 +854,22 @@ export class Profiles extends ProfilesCache { // Otherwise, we want to keep `tokenValue` in the secure array of the parent profile to avoid disconnecting child profiles if (profAttrs?.profLoc.jsonLoc) { configApi.set(`${profAttrs.profLoc.jsonLoc}.secure`, usingApimlToken ? [] : ["user", "password"]); - configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "tokenType")?.argLoc.jsonLoc); - configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "tokenValue")?.argLoc.jsonLoc); - configApi.delete(profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "tokenExpiration")?.argLoc.jsonLoc); - await configApi.save(); + const tokenTypeArgJsonLoc = profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "tokenType")?.argLoc.jsonLoc; + if (tokenTypeArgJsonLoc) { + configApi.delete(tokenTypeArgJsonLoc); + } + const tokenValueArgJsonLoc = profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "tokenValue") + ?.argLoc.jsonLoc; + if (tokenValueArgJsonLoc) { + configApi.delete(tokenValueArgJsonLoc); + } + const tokenExpirationArgJsonLoc = profInfo.mergeArgsForProfile(profAttrs).knownArgs.find((arg) => arg.argName === "tokenExpiration") + ?.argLoc.jsonLoc; + if (tokenExpirationArgJsonLoc) { + configApi.delete(tokenExpirationArgJsonLoc); + } } + await configApi.save(); } public async handleSwitchAuthentication(node: Types.IZoweNodeType): Promise { diff --git a/packages/zowe-explorer/src/extension.ts b/packages/zowe-explorer/src/extension.ts index 8176f3df8..8bc9d2b22 100644 --- a/packages/zowe-explorer/src/extension.ts +++ b/packages/zowe-explorer/src/extension.ts @@ -56,7 +56,6 @@ export async function activate(context: vscode.ExtensionContext): Promise respItem.member === memberName) + ) { + throw vscode.FileSystemError.FileNotFound(uri); + } } else { - throw vscode.FileSystemError.FileNotFound(uri); + const resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(uriPath[0], { + attributes: true, + }); + if (resp.success && resp.apiResponse?.items?.length > 0) { + entryIsDir = resp.apiResponse.items[0].dsorg?.startsWith("PO"); + } else { + throw vscode.FileSystemError.FileNotFound(uri); + } } } - if (entryIsDir) { if (!entryExists) { this.createDirectory(uri); @@ -328,40 +341,66 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem * @param uri The URI pointing to a valid file to fetch from the remote system * @param editor (optional) An editor instance to reload if the URI is already open */ - public async fetchDatasetAtUri(uri: vscode.Uri, options?: { editor?: vscode.TextEditor | null; isConflict?: boolean }): Promise { + public async fetchDatasetAtUri( + uri: vscode.Uri, + options?: { editor?: vscode.TextEditor | null; isConflict?: boolean } + ): Promise { ZoweLogger.trace(`[DatasetFSProvider] fetchDatasetAtUri called with ${uri.toString()}`); - const file = this._lookupAsFile(uri) as DsEntry; - // we need to fetch the contents from the mainframe since the file hasn't been accessed yet + let dsEntry = this._lookupAsFile(uri, { silent: true }) as DsEntry | undefined; const bufBuilder = new BufferBuilder(); - const metadata = file.metadata ?? this._getInfoFromUri(uri); - const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; - const resp = await ZoweExplorerApiRegister.getMvsApi(metadata.profile).getContents(metadata.dsName, { - binary: file.encoding?.kind === "binary", - encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, - responseTimeout: metadata.profile.profile?.responseTimeout, - returnEtag: true, - stream: bufBuilder, - }); - const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); - - if (options?.isConflict) { - file.conflictData = { - contents: data, - etag: resp.apiResponse.etag, - size: data.byteLength, - }; - } else { - file.data = data; - file.etag = resp.apiResponse.etag; - file.size = file.data.byteLength; - file.mtime = Date.now(); - } + const metadata = dsEntry?.metadata ?? this._getInfoFromUri(uri); + const profileEncoding = dsEntry?.encoding ? null : dsEntry?.metadata.profile.profile?.encoding; + try { + const resp = await ZoweExplorerApiRegister.getMvsApi(metadata.profile).getContents(metadata.dsName, { + binary: dsEntry?.encoding?.kind === "binary", + encoding: dsEntry?.encoding?.kind === "other" ? dsEntry?.encoding.codepage : profileEncoding, + responseTimeout: metadata.profile.profile?.responseTimeout, + returnEtag: true, + stream: bufBuilder, + }); + const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); + //if an entry does not exist for the dataset, create it + if (!dsEntry) { + const uriInfo = FsAbstractUtils.getInfoForUri(uri, Profiles.getInstance()); + const uriPath = uri.path.substring(uriInfo.slashAfterProfilePos + 1).split("/"); + const pdsMember = uriPath.length === 2; + this.createDirectory(uri.with({ path: path.posix.join(uri.path, "..") })); + const parentDir = this._lookupParentDirectory(uri); + const dsname = uriPath[Number(pdsMember)]; + const ds = new DsEntry(dsname, pdsMember); + ds.metadata = new DsEntryMetadata({ path: path.posix.join(parentDir.metadata.path, dsname), profile: parentDir.metadata.profile }); + parentDir.entries.set(dsname, ds); + dsEntry = parentDir.entries.get(dsname) as DsEntry; + } + //update entry's contents, attributes + if (options?.isConflict) { + dsEntry.conflictData = { + contents: data, + etag: resp.apiResponse.etag, + size: data.byteLength, + }; + } else { + dsEntry.data = data; + dsEntry.etag = resp.apiResponse.etag; + dsEntry.size = dsEntry.data.byteLength; + dsEntry.mtime = Date.now(); + } - ZoweLogger.trace(`[DatasetFSProvider] fetchDatasetAtUri fired a change event for ${uri.toString()}`); - this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); + ZoweLogger.trace(`[DatasetFSProvider] fetchDatasetAtUri fired a change event for ${uri.toString()}`); + this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); - if (options?.editor) { - await this._updateResourceInEditor(uri); + if (options?.editor) { + await this._updateResourceInEditor(uri); + } + return dsEntry; + } catch (error) { + //Response will error if the file is not found + //Callers of fetchDatasetAtUri() do not expect it to throw an error + if (error instanceof imperative.ImperativeError) { + AuthUtils.promptForAuthError(error, metadata.profile); + dsEntry.wasAccessed = false; + } + return null; } } @@ -372,44 +411,40 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem */ public async readFile(uri: vscode.Uri): Promise { let ds: DsEntry | DirEntry; + const urlQuery = new URLSearchParams(uri.query); + const isConflict = urlQuery.has("conflict"); try { ds = this._lookupAsFile(uri) as DsEntry; } catch (err) { if (!(err instanceof vscode.FileSystemError) || err.code !== "FileNotFound") { throw err; } + } - // check if parent directory exists; if not, do a remote lookup - const parent = this._lookupParentDirectory(uri, true); - if (parent == null) { - ds = await this.remoteLookupForResource(uri); + // we need to fetch the contents from the mainframe if the file hasn't been accessed yet + if (!ds || (!ds.wasAccessed && !urlQuery.has("inDiff")) || isConflict) { + //try and fetch its contents from remote + ds = (await this.fetchDatasetAtUri(uri, { isConflict })) as DsEntry; + if (!isConflict && ds) { + ds.wasAccessed = true; } } - if (ds == null) { - throw vscode.FileSystemError.FileNotFound(uri); - } if (FsAbstractUtils.isDirectoryEntry(ds)) { throw vscode.FileSystemError.FileIsADirectory(uri); } + //not found on remote, throw error + if (ds == null) { + throw vscode.FileSystemError.FileNotFound(uri); + } + const profInfo = this._getInfoFromUri(uri); if (profInfo.profile == null) { throw vscode.FileSystemError.FileNotFound(vscode.l10n.t("Profile does not exist for this file.")); } - const urlQuery = new URLSearchParams(uri.query); - const isConflict = urlQuery.has("conflict"); - - // we need to fetch the contents from the mainframe if the file hasn't been accessed yet - if ((!ds.wasAccessed && !urlQuery.has("inDiff")) || isConflict) { - await this.fetchDatasetAtUri(uri, { isConflict }); - if (!isConflict) { - ds.wasAccessed = true; - } - } - return isConflict ? ds.conflictData.contents : ds.data; } diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetInit.ts b/packages/zowe-explorer/src/trees/dataset/DatasetInit.ts index 309aa722c..ae95b35b9 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetInit.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetInit.ts @@ -20,6 +20,7 @@ import { SharedActions } from "../shared/SharedActions"; import { SharedContext } from "../shared/SharedContext"; import { SharedInit } from "../shared/SharedInit"; import { SharedUtils } from "../shared/SharedUtils"; +import { ProfilesUtils } from "../../utils/ProfilesUtils"; export class DatasetInit { public static async createDatasetTree(log: imperative.Logger): Promise { @@ -30,6 +31,12 @@ export class DatasetInit { return tree; } + public static async datasetTreeVisibilityChanged(this: void, e: vscode.TreeViewVisibilityChangeEvent): Promise { + if (e.visible) { + await ProfilesUtils.promptUserWithNoConfigs(); + } + } + public static async initDatasetProvider(context: vscode.ExtensionContext): Promise { ZoweLogger.trace("DatasetInit.initDatasetProvider called."); context.subscriptions.push(vscode.workspace.registerFileSystemProvider(ZoweScheme.DS, DatasetFSProvider.instance, { isCaseSensitive: true })); @@ -38,6 +45,8 @@ export class DatasetInit { return null; } + datasetProvider.getTreeView().onDidChangeVisibility(DatasetInit.datasetTreeVisibilityChanged); + context.subscriptions.push( vscode.commands.registerCommand("zowe.all.config.init", async () => { await datasetProvider.createZoweSchema(datasetProvider); diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts index 236900202..5e18da8f3 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts @@ -1167,7 +1167,7 @@ export class DatasetTree extends ZoweTreeProvider implemen const newUri = node.resourceUri.with({ path: path.posix.join(path.posix.dirname(node.resourceUri.path), afterMemberName), }); - await DatasetFSProvider.instance.rename(node.resourceUri, newUri, { overwrite: false }); + await vscode.workspace.fs.rename(node.resourceUri, newUri, { overwrite: false }); node.resourceUri = newUri; node.label = afterMemberName; node.tooltip = afterMemberName; @@ -1222,7 +1222,7 @@ export class DatasetTree extends ZoweTreeProvider implemen const newUri = node.resourceUri.with({ path: path.posix.join(path.posix.dirname(node.resourceUri.path), afterDataSetName), }); - await DatasetFSProvider.instance.rename(node.resourceUri, newUri, { overwrite: false }); + await vscode.workspace.fs.rename(node.resourceUri, newUri, { overwrite: false }); // Rename corresponding node in Sessions or Favorites section (whichever one Rename wasn't called from) if (SharedContext.isFavorite(node)) { diff --git a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts index cb998c092..e1728c53e 100644 --- a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts +++ b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts @@ -25,11 +25,13 @@ import { ZoweScheme, FsJobsUtils, FsAbstractUtils, + imperative, } from "@zowe/zowe-explorer-api"; import { IJob, IJobFile } from "@zowe/zos-jobs-for-zowe-sdk"; import { Profiles } from "../../configuration/Profiles"; import { ZoweExplorerApiRegister } from "../../extending/ZoweExplorerApiRegister"; import { SharedContext } from "../shared/SharedContext"; +import { AuthUtils } from "../../utils/AuthUtils"; export class JobFSProvider extends BaseProvider implements vscode.FileSystemProvider { private static _instance: JobFSProvider; @@ -193,14 +195,22 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv const jesApi = ZoweExplorerApiRegister.getJesApi(spoolEntry.metadata.profile); - if (jesApi.downloadSingleSpool) { - await jesApi.downloadSingleSpool({ - jobFile: spoolEntry.spool, - stream: bufBuilder, - }); - } else { - const jobEntry = this._lookupParentDirectory(uri) as JobEntry; - bufBuilder.write(await jesApi.getSpoolContentById(jobEntry.job.jobname, jobEntry.job.jobid, spoolEntry.spool.id)); + try { + if (jesApi.downloadSingleSpool) { + await jesApi.downloadSingleSpool({ + jobFile: spoolEntry.spool, + stream: bufBuilder, + }); + } else { + const jobEntry = this._lookupParentDirectory(uri) as JobEntry; + bufBuilder.write(await jesApi.getSpoolContentById(jobEntry.job.jobname, jobEntry.job.jobid, spoolEntry.spool.id)); + } + } catch (err) { + if (err instanceof imperative.ImperativeError) { + AuthUtils.promptForAuthError(err, spoolEntry.metadata.profile); + spoolEntry.wasAccessed = false; + } + throw err; } this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); @@ -281,7 +291,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv * @param options Options for deleting the spool file or job * - `deleteRemote` - Deletes the job from the remote system if set to true. */ - public async delete(uri: vscode.Uri, options: { readonly recursive: boolean; readonly deleteRemote: boolean }): Promise { + public async delete(uri: vscode.Uri, options: { readonly recursive: boolean }): Promise { const entry = this.lookup(uri, false); const isJob = FsJobsUtils.isJobEntry(entry); if (!isJob) { @@ -291,10 +301,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv const parent = this._lookupParentDirectory(uri, false); const profInfo = FsAbstractUtils.getInfoForUri(uri, Profiles.getInstance()); - - if (options.deleteRemote) { - await ZoweExplorerApiRegister.getJesApi(profInfo.profile).deleteJob(entry.job.jobname, entry.job.jobid); - } + await ZoweExplorerApiRegister.getJesApi(profInfo.profile).deleteJob(entry.job.jobname, entry.job.jobid); parent.entries.delete(entry.name); this._fireSoon({ type: vscode.FileChangeType.Deleted, uri }); } diff --git a/packages/zowe-explorer/src/trees/job/JobTree.ts b/packages/zowe-explorer/src/trees/job/JobTree.ts index 3fa17e221..8a950c2ff 100644 --- a/packages/zowe-explorer/src/trees/job/JobTree.ts +++ b/packages/zowe-explorer/src/trees/job/JobTree.ts @@ -218,7 +218,7 @@ export class JobTree extends ZoweTreeProvider implements Types public async delete(node: IZoweJobTreeNode): Promise { ZoweLogger.trace("JobTree.delete called."); - await JobFSProvider.instance.delete(node.resourceUri, { recursive: false, deleteRemote: true }); + await vscode.workspace.fs.delete(node.resourceUri, { recursive: false }); const favNode = this.relabelFavoritedJob(node); favNode.contextValue = SharedContext.asFavorite(favNode); await this.removeFavorite(favNode); diff --git a/packages/zowe-explorer/src/trees/uss/USSTree.ts b/packages/zowe-explorer/src/trees/uss/USSTree.ts index 3e574829f..cc741e35a 100644 --- a/packages/zowe-explorer/src/trees/uss/USSTree.ts +++ b/packages/zowe-explorer/src/trees/uss/USSTree.ts @@ -126,7 +126,7 @@ export class USSTree extends ZoweTreeProvider implements Types true ); } - await UssFSProvider.instance.delete(sourceUri, { recursive: true }); + await vscode.workspace.fs.delete(sourceUri, { recursive: true }); } else { // create a file on the remote system for writing try { @@ -160,7 +160,7 @@ export class USSTree extends ZoweTreeProvider implements Types if (!recursiveCall) { // Delete any files from the selection on the source LPAR - await UssFSProvider.instance.delete(sourceNode.resourceUri, { recursive: false }); + await vscode.workspace.fs.delete(sourceNode.resourceUri, { recursive: false }); } } } diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 693807248..ab7f8722b 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -29,6 +29,7 @@ import { USSFileStructure } from "./USSFileStructure"; import { Profiles } from "../../configuration/Profiles"; import { ZoweExplorerApiRegister } from "../../extending/ZoweExplorerApiRegister"; import { ZoweLogger } from "../../tools/ZoweLogger"; +import { AuthUtils } from "../../utils/AuthUtils"; export class UssFSProvider extends BaseProvider implements vscode.FileSystemProvider { // Event objects for provider @@ -136,7 +137,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const response = await ZoweExplorerApiRegister.getUssApi(profile).fileList(ussPath); // If request was successful, create directories for the path if it doesn't exist if (response.success && !keepRelative && response.apiResponse.items?.[0]?.mode?.startsWith("d") && !this.exists(uri)) { - await vscode.workspace.fs.createDirectory(uri); + await vscode.workspace.fs.createDirectory(uri.with({ query: "" })); } return { @@ -168,7 +169,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv let parentDir = this._lookupParentDirectory(uri, true); if (parentDir == null) { const parentPath = path.posix.join(uri.path, ".."); - const parentUri = uri.with({ path: parentPath }); + const parentUri = uri.with({ path: parentPath, query: "" }); await vscode.workspace.fs.createDirectory(parentUri); parentDir = this._lookupParentDirectory(uri, false); parentDir.metadata = this._getInfoFromUri(parentUri); @@ -260,15 +261,32 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const bufBuilder = new BufferBuilder(); const filePath = uri.path.substring(uriInfo.slashAfterProfilePos); const metadata = file.metadata; - await this.autoDetectEncoding(file as UssFile); - const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; - const resp = await ZoweExplorerApiRegister.getUssApi(metadata.profile).getContents(filePath, { - binary: file.encoding?.kind === "binary", - encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, - responseTimeout: metadata.profile.profile?.responseTimeout, - returnEtag: true, - stream: bufBuilder, - }); + + let resp: IZosFilesResponse; + try { + await this.autoDetectEncoding(file as UssFile); + const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; + resp = await ZoweExplorerApiRegister.getUssApi(metadata.profile).getContents(filePath, { + binary: file.encoding?.kind === "binary", + encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, + responseTimeout: metadata.profile.profile?.responseTimeout, + returnEtag: true, + stream: bufBuilder, + }); + } catch (err) { + if (err instanceof Error) { + ZoweLogger.error(err.message); + } + if (err instanceof imperative.ImperativeError) { + AuthUtils.promptForAuthError(err, metadata.profile); + file.wasAccessed = false; + } + return; + } + + if (!options?.isConflict) { + file.wasAccessed = true; + } const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); if (options?.isConflict) { @@ -357,9 +375,6 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv // - fetching a conflict from the remote FS if ((!file.wasAccessed && !urlQuery.has("inDiff")) || isConflict) { await this.fetchFileAtUri(uri, { isConflict }); - if (!isConflict) { - file.wasAccessed = true; - } } return isConflict ? file.conflictData.contents : file.data; diff --git a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts index f7f795113..364532e9a 100644 --- a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts +++ b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts @@ -364,7 +364,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { }); try { - await UssFSProvider.instance.rename(oldUri, newUri, { overwrite: false }); + await vscode.workspace.fs.rename(oldUri, newUri, { overwrite: false }); } catch (err) { Gui.errorMessage(err.message); return; @@ -416,7 +416,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { return; } try { - await UssFSProvider.instance.delete(this.resourceUri, { recursive: this.isFolder }); + await vscode.workspace.fs.delete(this.resourceUri, { recursive: this.isFolder }); } catch (err) { ZoweLogger.error(err); if (err instanceof Error) { diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index 0f6fa561c..cad5c189e 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -17,6 +17,68 @@ import { ZoweLogger } from "../tools/ZoweLogger"; import { SharedTreeProviders } from "../trees/shared/SharedTreeProviders"; export class AuthUtils { + public static async promptForAuthentication( + imperativeError: imperative.ImperativeError, + profile: string | imperative.IProfileLoaded + ): Promise { + let profileName = typeof profile === "string" ? profile : profile.name; + const errMsg = vscode.l10n.t({ + message: "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.", + args: [profileName], + comment: ["Label"], + }); + const errToken = vscode.l10n.t({ + message: + // eslint-disable-next-line max-len + "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.", + args: [profileName], + comment: ["Label"], + }); + if (profileName.includes("[")) { + profileName = profileName.substring(0, profileName.indexOf(" [")).trim(); + } + + if (imperativeError.mDetails.additionalDetails) { + const tokenError: string = imperativeError.mDetails.additionalDetails; + const isTokenAuth = await AuthUtils.isUsingTokenAuth(profileName); + + if (tokenError.includes("Token is not valid or expired.") || isTokenAuth) { + const message = vscode.l10n.t("Log in to Authentication Service"); + const success = await Gui.showMessage(errToken, { items: [message] }).then(async (selection) => { + if (selection) { + return Constants.PROFILES_CACHE.ssoLogin(null, profileName); + } + + return false; + }); + return success; + } + } + const checkCredsButton = vscode.l10n.t("Update Credentials"); + const creds = await Gui.errorMessage(errMsg, { + items: [checkCredsButton], + vsCodeOpts: { modal: true }, + }).then(async (selection) => { + if (selection !== checkCredsButton) { + return; + } + return Constants.PROFILES_CACHE.promptCredentials(profile, true); + }); + return creds != null ? true : false; + } + + public static promptForAuthError(imperativeError: imperative.ImperativeError, profile: string | imperative.IProfileLoaded): void { + const httpErrorCode = Number(imperativeError.mDetails.errorCode); + if ( + httpErrorCode === imperative.RestConstants.HTTP_STATUS_401 || + imperativeError.message.includes("All configured authentication methods failed") + ) { + void AuthUtils.promptForAuthentication(imperativeError, profile).catch( + (error) => error instanceof Error && ZoweLogger.error(error.message) + ); + } + } + /************************************************************************************************************* * Error Handling * @param {errorDetails} - string or error object @@ -46,49 +108,7 @@ export class AuthUtils { httpErrorCode === imperative.RestConstants.HTTP_STATUS_401 || imperativeError.message.includes("All configured authentication methods failed") ) { - const errMsg = vscode.l10n.t({ - message: - "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.", - args: [label], - comment: ["Label"], - }); - const errToken = vscode.l10n.t({ - message: - // eslint-disable-next-line max-len - "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.", - args: [label], - comment: ["Label"], - }); - if (label.includes("[")) { - label = label.substring(0, label.indexOf(" [")).trim(); - } - - if (imperativeError.mDetails.additionalDetails) { - const tokenError: string = imperativeError.mDetails.additionalDetails; - const isTokenAuth = await AuthUtils.isUsingTokenAuth(label); - - if (tokenError.includes("Token is not valid or expired.") || isTokenAuth) { - const message = vscode.l10n.t("Log in to Authentication Service"); - const success = Gui.showMessage(errToken, { items: [message] }).then(async (selection) => { - if (selection) { - return Constants.PROFILES_CACHE.ssoLogin(null, label); - } - }); - return success; - } - } - const checkCredsButton = vscode.l10n.t("Update Credentials"); - const creds = await Gui.errorMessage(errMsg, { - items: [checkCredsButton], - vsCodeOpts: { modal: true }, - }).then(async (selection) => { - if (selection !== checkCredsButton) { - Gui.showMessage(vscode.l10n.t("Operation Cancelled")); - return; - } - return Constants.PROFILES_CACHE.promptCredentials(label.trim(), true); - }); - return creds != null ? true : false; + return AuthUtils.promptForAuthentication(imperativeError, label); } } if (errorDetails.toString().includes("Could not find profile")) { diff --git a/packages/zowe-explorer/src/utils/ProfilesUtils.ts b/packages/zowe-explorer/src/utils/ProfilesUtils.ts index a7369eaef..6dcef1d18 100644 --- a/packages/zowe-explorer/src/utils/ProfilesUtils.ts +++ b/packages/zowe-explorer/src/utils/ProfilesUtils.ts @@ -28,6 +28,7 @@ export enum ProfilesConvertStatus { export class ProfilesUtils { public static PROFILE_SECURITY: string | boolean = Constants.ZOWE_CLI_SCM; + private static noConfigDialogShown = false; /** * Check if the credential manager's vsix is installed for use @@ -81,18 +82,22 @@ export class ProfilesUtils { */ public static updateCredentialManagerSetting(credentialManager?: string | false): void { ZoweLogger.trace("ProfilesUtils.updateCredentialManagerSetting called."); + const currentProfileSecurity = this.PROFILE_SECURITY; const settingEnabled: boolean = SettingsConfig.getDirectValue(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED, true); + const defaultCredentialManagerFound = this.checkDefaultCredentialManager(); if (settingEnabled && credentialManager) { this.PROFILE_SECURITY = credentialManager; return; - } else if (!settingEnabled) { + } else if (!settingEnabled || !defaultCredentialManagerFound) { this.PROFILE_SECURITY = false; ZoweLogger.info(vscode.l10n.t(`Zowe explorer profiles are being set as unsecured.`)); } else { this.PROFILE_SECURITY = Constants.ZOWE_CLI_SCM; ZoweLogger.info(vscode.l10n.t(`Zowe explorer profiles are being set as secured.`)); } - imperative.CredentialManagerOverride.recordCredMgrInConfig(this.PROFILE_SECURITY); + if (currentProfileSecurity !== this.PROFILE_SECURITY) { + imperative.CredentialManagerOverride.recordCredMgrInConfig(this.PROFILE_SECURITY); + } } /** @@ -148,40 +153,44 @@ export class ProfilesUtils { * Prompt whether to disable credential management setting * * This will disable credential management on all settings - * scopes since order presedence can be hard to predict based on the user's setup + * scopes since order precedence can be hard to predict based on the user's setup */ - public static async promptAndDisableCredentialManagement(): Promise { - ZoweLogger.trace("ProfilesUtils.promptAndDisableCredentialManagement called."); - const noButton = vscode.l10n.t("No"); - const yesGloballyButton = vscode.l10n.t("Yes, globally"); - const yesWorkspaceButton = vscode.l10n.t("Only for this workspace"); - const response = await Gui.warningMessage( - vscode.l10n.t("Zowe Explorer failed to activate since the default credential manager is not supported in your environment."), - { - items: [noButton, yesGloballyButton, yesWorkspaceButton], - vsCodeOpts: { - modal: true, - detail: vscode.l10n.t("Do you wish to disable credential management? (VS Code window reload will be triggered)"), - }, - } - ); - if (response === yesGloballyButton || response === yesWorkspaceButton) { - const scope = response === yesGloballyButton ? vscode.ConfigurationTarget.Global : vscode.ConfigurationTarget.Workspace; - await SettingsConfig.setDirectValue(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED, false, scope); - await vscode.commands.executeCommand("workbench.action.reloadWindow"); - } else { - throw new imperative.ImperativeError({ - msg: vscode.l10n.t( - // eslint-disable-next-line max-len - "Failed to load credential manager. This may be related to Zowe Explorer being unable to use the default credential manager in a browser based environment." + public static async disableCredentialManagement(): Promise { + ZoweLogger.trace("ProfilesUtils.disableCredentialManagement called."); + const settingEnabled: boolean = SettingsConfig.getDirectValue(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED, true); + if (settingEnabled) { + this.PROFILE_SECURITY = false; + await SettingsConfig.setDirectValue(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED, false, vscode.ConfigurationTarget.Global); + await Gui.infoMessage( + vscode.l10n.t( + "Zowe Explorer's default credential manager is not supported in your environment. Consider installing a custom solution for your platform. Click Reload to start Zowe Explorer without a credential manager." ), - }); + { + items: [vscode.l10n.t("Reload window")], + } + ); + await vscode.commands.executeCommand("workbench.action.reloadWindow"); + } + } + + public static checkDefaultCredentialManager(): boolean { + try { + ProfilesCache.requireKeyring(); + } catch (_error) { + ZoweLogger.info( + vscode.l10n.t( + "Default Zowe credentials manager not found on current platform. This is typically the case when running in container-based environments or Linux systems that miss required security libraries or user permissions." + ) + ); + return false; } + return true; } /** * Use the default credential manager in Zowe Explorer and setup before use - * @returns Promise the object of profileInfo using the default credential manager + * @returns {imperative.ProfileInfo} a ProfileInfo instance using the default credential manager, + * or undefined if an error occurred unrelated to credential manager initialization */ public static async setupDefaultCredentialManager(): Promise { try { @@ -202,7 +211,10 @@ export class ProfilesUtils { return profileInfo; } catch (err) { if (err instanceof imperative.ProfInfoErr && err.errorCode === imperative.ProfInfoErr.LOAD_CRED_MGR_FAILED) { - await ProfilesUtils.promptAndDisableCredentialManagement(); + await ProfilesUtils.disableCredentialManagement(); + } + if (err instanceof Error) { + ZoweLogger.error(err.message); } // Ignore other types of errors since they will be handled later } @@ -295,10 +307,14 @@ export class ProfilesUtils { ); } + /** + * Creates an instance of ProfileInfo and calls `readProfilesFromDisk` to load profiles. + * @returns An instance of `ProfileInfo`, or `undefined` if there was an error. + */ public static async getProfileInfo(): Promise { ZoweLogger.trace("ProfilesUtils.getProfileInfo called."); - const hasSecureCredentialManagerEnabled: boolean = SettingsConfig.getDirectValue(Constants.SETTINGS_SECURE_CREDENTIALS_ENABLED); + const hasSecureCredentialManagerEnabled: boolean = this.checkDefaultCredentialManager(); if (hasSecureCredentialManagerEnabled) { const shouldCheckForCustomCredentialManagers = SettingsConfig.getDirectValue( Constants.SETTINGS_CHECK_FOR_CUSTOM_CREDENTIAL_MANAGERS @@ -318,9 +334,17 @@ export class ProfilesUtils { if (credentialManagerMap && isVSCodeCredentialPluginInstalled) { return this.setupCustomCredentialManager(credentialManagerMap); } + return this.setupDefaultCredentialManager(); } - return this.setupDefaultCredentialManager(); + const profileInfo = new imperative.ProfileInfo("zowe", {}); + const workspacePath = ZoweVsCodeExtension.workspaceRoot?.uri.fsPath; + // Trigger initialize() function of credential manager to throw an error early if failed to load + await profileInfo.readProfilesFromDisk({ + homeDir: FileManagement.getZoweDir(), + projectDir: workspacePath ? FileManagement.getFullPath(workspacePath) : undefined, + }); + return profileInfo; } public static async readConfigFromDisk(warnForMissingSchema?: boolean): Promise { @@ -367,13 +391,17 @@ export class ProfilesUtils { // VS Code registers our updated TreeView IDs. Otherwise, VS Code's "Refresh Extensions" option will break v3 init. const ussPersistentSettings = vscode.workspace.getConfiguration("Zowe-USS-Persistent"); const upgradingFromV1 = ZoweLocalStorage.getValue(Definitions.LocalStorageKey.V1_MIGRATION_STATUS); - const mProfileInfo = await ProfilesUtils.getProfileInfo(); + const profileInfo = await ProfilesUtils.getProfileInfo(); + if (profileInfo == null) { + return; + } + if (ussPersistentSettings != null && upgradingFromV1 == null && imperative.ProfileInfo.onlyV1ProfilesExist) { await ZoweLocalStorage.setValue(Definitions.LocalStorageKey.V1_MIGRATION_STATUS, Definitions.V1MigrationStatus.JustMigrated); await vscode.commands.executeCommand("workbench.action.reloadWindow"); } - if (upgradingFromV1 == null || mProfileInfo.getTeamConfig().exists || !imperative.ProfileInfo.onlyV1ProfilesExist) { + if (upgradingFromV1 == null || profileInfo.getTeamConfig().exists || !imperative.ProfileInfo.onlyV1ProfilesExist) { return; } const userSelection = await this.v1ProfileOptions(); @@ -390,7 +418,15 @@ export class ProfilesUtils { * This aims to help direct new Zowe Explorer users to create a new team configuration. */ public static async promptUserWithNoConfigs(): Promise { + if (ProfilesUtils.noConfigDialogShown) { + return; + } + const profInfo = await ProfilesUtils.getProfileInfo(); + if (profInfo == null) { + return; + } + if (!profInfo.getTeamConfig().exists && !imperative.ProfileInfo.onlyV1ProfilesExist) { Gui.showMessage( vscode.l10n.t("No Zowe client configurations were detected. Click 'Create New' to create a new Zowe team configuration."), @@ -402,6 +438,7 @@ export class ProfilesUtils { await vscode.commands.executeCommand("zowe.ds.addSession"); } }); + ProfilesUtils.noConfigDialogShown = true; } } @@ -444,7 +481,7 @@ export class ProfilesUtils { } } - public static initializeZoweFolder(): void { + public static async initializeZoweFolder(): Promise { ZoweLogger.trace("ProfilesUtils.initializeZoweFolder called."); // Ensure that ~/.zowe folder exists const zoweDir = FileManagement.getZoweDir(); @@ -455,6 +492,11 @@ export class ProfilesUtils { if (!fs.existsSync(settingsPath)) { fs.mkdirSync(settingsPath); } + + if (!this.checkDefaultCredentialManager()) { + await this.disableCredentialManagement(); + } + ProfilesUtils.writeOverridesFile(); // set global variable of security value to existing override // this will later get reverted to default in getProfilesInfo.ts if user chooses to @@ -531,7 +573,7 @@ export class ProfilesUtils { public static async initializeZoweProfiles(errorCallback: (msg: string) => unknown): Promise { ZoweLogger.trace("ProfilesUtils.initializeZoweProfiles called."); try { - ProfilesUtils.initializeZoweFolder(); + await ProfilesUtils.initializeZoweFolder(); } catch (err) { ZoweLogger.error(err); Gui.errorMessage(