From 91d4aafc7aa8be552477334f8c5d7a2db3e34158 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 15 Nov 2024 13:36:07 -0500 Subject: [PATCH] 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> --- .../zowe-explorer-ftp-extension/CHANGELOG.md | 2 + .../Mvs/ZoweExplorerFtpMvsApi.unit.test.ts | 37 ++++++++++++++++++- .../src/ZoweExplorerFtpMvsApi.ts | 20 ++++++++-- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/packages/zowe-explorer-ftp-extension/CHANGELOG.md b/packages/zowe-explorer-ftp-extension/CHANGELOG.md index 05e045d4f..36f267f9a 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` ## `3.0.1` 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, };