From b5293004b578fc1f785912ee1ddc66ee576a0c98 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Tue, 26 Nov 2019 16:50:00 +0100 Subject: [PATCH 01/26] making use of Etag - created new `node` property called etag + setter + getter - using the value of etag to check if remote file has changed - triggering a merge conflict if above condition is true - removed safe save functionality Signed-off-by: Alexandru-Paul Dumitru --- i18n/sample/package.i18n.json | 2 - i18n/sample/src/extension.i18n.json | 8 +- package.json | 74 ---------- package.nls.json | 2 - src/ZoweNode.ts | 27 +++- src/ZoweUSSNode.ts | 22 ++- src/extension.ts | 209 ++++++++++++++-------------- 7 files changed, 154 insertions(+), 190 deletions(-) diff --git a/i18n/sample/package.i18n.json b/i18n/sample/package.i18n.json index 91e8517f23..1bb6161cba 100644 --- a/i18n/sample/package.i18n.json +++ b/i18n/sample/package.i18n.json @@ -26,7 +26,6 @@ "uss.removeFavorite": "Remove Favorite", "removeSavedSearch": "Remove Search", "removeSession": "Remove Profile", - "safeSave": "Safe Save, Merge if Necessary", "saveSearch": "Save Search", "submitJcl": "Submit JCL", "submitMember": "Submit Job", @@ -38,7 +37,6 @@ "uss.fullPath": "Search Unix System Services (USS) by Entering a Path", "uss.refreshUSS": "Pull from Mainframe", "uss.removeSession": "Remove Profile", - "uss.safeSaveUSS": "Safe Save, Merge if Necessary", "uss.binary": "Toggle Binary", "uss.uploadDialog": "Upload Files...", "uss.text": "Toggle Text", diff --git a/i18n/sample/src/extension.i18n.json b/i18n/sample/src/extension.i18n.json index 0fb6a5c5a8..4d0ecd8217 100644 --- a/i18n/sample/src/extension.i18n.json +++ b/i18n/sample/src/extension.i18n.json @@ -79,12 +79,6 @@ "refreshUSS.error.notFound": "not found", "refreshUSS.file1": "Unable to find file: ", "refreshUSS.file2": " was probably deleted.", - "safeSave.log.debug.request": "safe save requested for node: ", - "safeSave.error.invalidNode": "safeSave() called from invalid node.", - "safeSave.log.debug.invoke": "Invoking safesave for data set ", - "safeSave.error.notFound": "not found", - "safeSave.file1": "Unable to find file: ", - "safeSave.file2": " was probably deleted.", "saveFile.log.debug.request": "requested to save data set: ", "saveFile.log.debug.path": "path.relative returned a non-blank directory.", "saveFile.log.debug.directory": "Assuming we are not in the DS_DIR directory: ", @@ -93,6 +87,8 @@ "saveFile.log.error.session": "Couldn't locate session when saving data set!", "saveFile.log.debug.saving": "Saving file ", "saveFile.error.saveFailed": "Data set failed to save. Data set may have been deleted on mainframe.", + "saveFile.error.ZosmfEtagMismatchError": "Rest API failure with HTTP(S) status 412", + "saveFile.error.etagMismatch": "Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict.", "saveFile.response.save.title": "Saving data set...", "saveUSSFile.log.debug.saveRequest": "save requested for USS file ", "saveUSSFile.response.title": "Saving file...", diff --git a/package.json b/package.json index 71da322771..e0e50a6dba 100644 --- a/package.json +++ b/package.json @@ -169,14 +169,6 @@ "command": "zowe.removeSession", "title": "%removeSession%" }, - { - "command": "zowe.safeSave", - "title": "%safeSave%", - "icon": { - "light": "./resources/light/upload.svg", - "dark": "./resources/dark/upload.svg" - } - }, { "command": "zowe.saveSearch", "title": "%saveSearch%" @@ -249,14 +241,6 @@ "command": "zowe.uss.removeSession", "title": "%uss.removeSession%" }, - { - "command": "zowe.uss.safeSaveUSS", - "title": "%uss.safeSaveUSS%", - "icon": { - "light": "./resources/light/upload.svg", - "dark": "./resources/dark/upload.svg" - } - }, { "command": "zowe.uss.saveSearch", "title": "%saveSearch%" @@ -443,21 +427,11 @@ "command": "zowe.uss.refreshUSS", "group": "inline" }, - { - "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", - "command": "zowe.uss.safeSaveUSS", - "group": "inline" - }, { "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", "command": "zowe.uss.refreshUSS", "group": "0_mainframeInteraction" }, - { - "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", - "command": "zowe.uss.safeSaveUSS", - "group": "0_mainframeInteraction" - }, { "when": "view == zowe.uss.explorer && viewItem != uss_session && viewItem != favorite && viewItem != textFile_fav && viewItem != binaryFile_fav && viewItem != directory_fav && viewItem != uss_session_fav", "command": "zowe.uss.deleteNode", @@ -538,21 +512,11 @@ "command": "zowe.refreshNode", "group": "inline" }, - { - "when": "view == zowe.explorer && viewItem == member", - "command": "zowe.safeSave", - "group": "inline" - }, { "when": "view == zowe.explorer && viewItem == member", "command": "zowe.refreshNode", "group": "0_mainframeInteraction" }, - { - "when": "view == zowe.explorer && viewItem == member", - "command": "zowe.safeSave", - "group": "0_mainframeInteraction" - }, { "when": "view == zowe.explorer && viewItem == member", "command": "zowe.deleteMember", @@ -573,21 +537,11 @@ "command": "zowe.refreshNode", "group": "inline" }, - { - "when": "view == zowe.explorer && viewItem == member_fav", - "command": "zowe.safeSave", - "group": "inline" - }, { "when": "view == zowe.explorer && viewItem == member_fav", "command": "zowe.refreshNode", "group": "0_mainframeInteraction" }, - { - "when": "view == zowe.explorer && viewItem == member_fav", - "command": "zowe.safeSave", - "group": "0_mainframeInteraction" - }, { "when": "view == zowe.explorer && viewItem == member_fav", "command": "zowe.deleteMember", @@ -603,21 +557,11 @@ "command": "zowe.refreshNode", "group": "inline" }, - { - "when": "view == zowe.explorer && viewItem == ds", - "command": "zowe.safeSave", - "group": "inline" - }, { "when": "view == zowe.explorer && viewItem == ds", "command": "zowe.refreshNode", "group": "0_mainframeInteraction" }, - { - "when": "view == zowe.explorer && viewItem == ds", - "command": "zowe.safeSave", - "group": "0_mainframeInteraction" - }, { "when": "view == zowe.explorer && viewItem == ds", "command": "zowe.deleteDataset", @@ -643,21 +587,11 @@ "command": "zowe.refreshNode", "group": "inline" }, - { - "when": "view == zowe.explorer && viewItem == ds_fav", - "command": "zowe.safeSave", - "group": "inline" - }, { "when": "view == zowe.explorer && viewItem == ds_fav", "command": "zowe.refreshNode", "group": "0_mainframeInteraction" }, - { - "when": "view == zowe.explorer && viewItem == ds_fav", - "command": "zowe.safeSave", - "group": "0_mainframeInteraction" - }, { "when": "view == zowe.explorer && viewItem == ds_fav", "command": "zowe.showDSAttributes", @@ -890,10 +824,6 @@ "command": "zowe.removeSession", "when": "never" }, - { - "command": "zowe.safeSave", - "when": "never" - }, { "command": "zowe.saveSearch", "when": "never" @@ -914,10 +844,6 @@ "command": "zowe.uss.refreshUSS", "when": "never" }, - { - "command": "zowe.uss.safeSaveUSS", - "when": "never" - }, { "command": "zowe.uss.saveSearch", "when": "never" diff --git a/package.nls.json b/package.nls.json index d5553c2e39..51d0a6f0b5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -26,7 +26,6 @@ "uss.removeFavorite": "Remove Favorite", "removeSavedSearch": "Remove Search", "removeSession": "Remove Profile", - "safeSave": "Safe Save, Merge if Necessary", "saveSearch": "Save Search", "submitJcl": "Submit JCL", "submitMember": "Submit Job", @@ -38,7 +37,6 @@ "uss.fullPath": "Search Unix System Services (USS) by Entering a Path", "uss.refreshUSS": "Pull from Mainframe", "uss.removeSession": "Remove Profile", - "uss.safeSaveUSS": "Safe Save, Merge if Necessary", "uss.binary": "Toggle Binary", "uss.uploadDialog": "Upload Files...", "uss.text": "Toggle Text", diff --git a/src/ZoweNode.ts b/src/ZoweNode.ts index c495192fa4..538d061a2c 100644 --- a/src/ZoweNode.ts +++ b/src/ZoweNode.ts @@ -38,8 +38,12 @@ export class ZoweNode extends vscode.TreeItem { * @param {ZoweNode} mParent * @param {Session} session */ - constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, - public mParent: ZoweNode, private session: Session, contextOverride?: string) { + constructor(label: string, + collapsibleState: vscode.TreeItemCollapsibleState, + public mParent: ZoweNode, + private session: Session, + contextOverride?: string, + private etag?: string) { super(label, collapsibleState); if (contextOverride) { this.contextValue = contextOverride; @@ -51,6 +55,7 @@ export class ZoweNode extends vscode.TreeItem { this.contextValue = extension.DS_DS_CONTEXT; } this.tooltip = this.label; + this.etag = etag ? etag : ""; utils.applyIcons(this); } @@ -164,4 +169,22 @@ export class ZoweNode extends vscode.TreeItem { public getSessionNode(): ZoweNode { return this.session ? this : this.mParent.getSessionNode(); } + + /** + * Returns the [etag] for this node + * + * @returns {string} + */ + public getEtag(): string { + return this.etag; + } + + /** + * Set the [etag] for this node + * + * @returns {void} + */ + public setEtag(etagValue): void { + this.etag = etagValue; + } } diff --git a/src/ZoweUSSNode.ts b/src/ZoweUSSNode.ts index 69e8ab7c24..63fdf24b9d 100644 --- a/src/ZoweUSSNode.ts +++ b/src/ZoweUSSNode.ts @@ -49,7 +49,8 @@ export class ZoweUSSNode extends vscode.TreeItem { private session: Session, private parentPath: string, public binary = false, - public mProfileName?: string) { + public mProfileName?: string, + private etag?: string) { super(label, collapsibleState); if (collapsibleState !== vscode.TreeItemCollapsibleState.None) { this.contextValue = extension.USS_DIR_CONTEXT; @@ -74,6 +75,7 @@ export class ZoweUSSNode extends vscode.TreeItem { this.label = this.profileName + this.shortLabel; this.tooltip = this.profileName + this.fullPath; } + this.etag = etag ? etag : ""; utils.applyIcons(this); } @@ -205,4 +207,22 @@ export class ZoweUSSNode extends vscode.TreeItem { utils.applyIcons(this); this.dirty = true; } + + /** + * Returns the [etag] for this node + * + * @returns {string} + */ + public getEtag(): string { + return this.etag; + } + + /** + * Set the [etag] for this node + * + * @returns {void} + */ + public setEtag(etagValue): void { + this.etag = etagValue; + } } diff --git a/src/extension.ts b/src/extension.ts index b089feb465..fc9453d33c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -24,7 +24,7 @@ import { ZoweUSSNode } from "./ZoweUSSNode"; import * as ussActions from "./uss/ussNodeActions"; import * as mvsActions from "./mvs/mvsNodeActions"; // tslint:disable-next-line: no-duplicate-imports -import { IJobFile } from "@brightside/core"; +import { IJobFile, IUploadOptions } from "@brightside/core"; import { Profiles } from "./Profiles"; import * as nls from "vscode-nls"; import * as utils from "./utils"; @@ -159,7 +159,6 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("zowe.editMember", (node) => openPS(node, false)); vscode.commands.registerCommand("zowe.removeSession", async (node) => datasetProvider.deleteSession(node)); vscode.commands.registerCommand("zowe.removeFavorite", async (node) => datasetProvider.removeFavorite(node)); - vscode.commands.registerCommand("zowe.safeSave", async (node) => safeSave(node)); vscode.commands.registerCommand("zowe.saveSearch", async (node) => datasetProvider.addFavorite(node)); vscode.commands.registerCommand("zowe.removeSavedSearch", async (node) => datasetProvider.removeFavorite(node)); vscode.commands.registerCommand("zowe.submitJcl", async () => submitJcl(datasetProvider)); @@ -197,7 +196,6 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("zowe.uss.addSession", async () => addUSSSession(ussFileProvider)); vscode.commands.registerCommand("zowe.uss.refreshAll", () => ussActions.refreshAllUSS(ussFileProvider)); vscode.commands.registerCommand("zowe.uss.refreshUSS", (node) => refreshUSS(node)); - vscode.commands.registerCommand("zowe.uss.safeSaveUSS", async (node) => safeSaveUSS(node)); vscode.commands.registerCommand("zowe.uss.fullPath", (node) => ussFileProvider.ussFilterPrompt(node)); vscode.commands.registerCommand("zowe.uss.ZoweUSSNode.open", (node) => openUSS(node, false, true)); vscode.commands.registerCommand("zowe.uss.removeSession", async (node) => ussFileProvider.deleteSession(node)); @@ -300,7 +298,7 @@ export async function activate(context: vscode.ExtensionContext) { } /** - * Allow the user to subbmit a TSO command to the selected server. Response is written + * Allow the user to submit a TSO command to the selected server. Response is written * to the output channel. * @param outputChannel The Output Channel to write the command and response to */ @@ -1126,17 +1124,20 @@ export async function openPS(node: ZoweNode, previewMember: boolean) { } log.debug(localize("openPS.log.debug.openDataSet", "opening physical sequential data set from label ") + label); // if local copy exists, open that instead of pulling from mainframe - if (!fs.existsSync(getDocumentFilePath(label, node))) { - await vscode.window.withProgress({ + const documentFilePath = getDocumentFilePath(label, node); + if (!fs.existsSync(documentFilePath)) { + const response = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: "Opening data set..." }, function downloadDataset() { return zowe.Download.dataSet(node.getSession(), label, { // TODO MISSED TESTING - file: getDocumentFilePath(label, node) + file: documentFilePath, + returnEtag: true }); }); + node.setEtag(response.apiResponse.etag); } - const document = await vscode.workspace.openTextDocument(getDocumentFilePath(label, node)); + const document = await vscode.workspace.openTextDocument(documentFilePath); if (previewMember === true) { await vscode.window.showTextDocument(document); } @@ -1192,10 +1193,14 @@ export async function refreshPS(node: ZoweNode) { default: throw Error(localize("refreshPS.error.invalidNode", "refreshPS() called from invalid node.")); } - await zowe.Download.dataSet(node.getSession(), label, { - file: getDocumentFilePath(label, node) + const documentFilePath = getDocumentFilePath(label, node); + const response = await zowe.Download.dataSet(node.getSession(), label, { + file: documentFilePath, + returnEtag: true }); - const document = await vscode.workspace.openTextDocument(getDocumentFilePath(label, node)); + node.setEtag(response.apiResponse.etag); + + const document = await vscode.workspace.openTextDocument(documentFilePath); vscode.window.showTextDocument(document); // if there are unsaved changes, vscode won't automatically display the updates, so close and reopen if (document.isDirty) { @@ -1235,10 +1240,14 @@ export async function refreshUSS(node: ZoweUSSNode) { throw Error(localize("refreshUSS.error.invalidNode", "refreshPS() called from invalid node.")); } try { - await zowe.Download.ussFile(node.getSession(), node.fullPath, { - file: getUSSDocumentFilePath(node) + const ussDocumentFilePath = getUSSDocumentFilePath(node); + const response = await zowe.Download.ussFile(node.getSession(), node.fullPath, { + file: ussDocumentFilePath, + returnEtag: true }); - const document = await vscode.workspace.openTextDocument(getUSSDocumentFilePath(node)); + node.setEtag(response.apiResponse.etag); + + const document = await vscode.workspace.openTextDocument(ussDocumentFilePath); vscode.window.showTextDocument(document); // if there are unsaved changes, vscode won't automatically display the updates, so close and reopen if (document.isDirty) { @@ -1255,77 +1264,6 @@ export async function refreshUSS(node: ZoweUSSNode) { } } -/** - * Checks if there are changes on the mainframe before pushing changes - * - * @export - * @param {ZoweNode} node The node which represents the dataset - */ -export async function safeSave(node: ZoweNode) { - - log.debug(localize("safeSave.log.debug.request", "safe save requested for node: ") + node.label); - let label; - try { - switch (node.mParent.contextValue) { - case (FAVORITE_CONTEXT): - label = node.label.trim().substring(node.label.indexOf(":") + 1).trim(); - break; - case (DS_PDS_CONTEXT + FAV_SUFFIX): - label = node.mParent.label.substring(node.mParent.label.indexOf(":") + 1).trim() + "(" + node.label.trim()+ ")"; - break; - case (DS_SESSION_CONTEXT): - label = node.label.trim(); - break; - case (DS_PDS_CONTEXT): - label = node.mParent.label.trim() + "(" + node.label.trim()+ ")"; - break; - default: - throw Error(localize("safeSave.error.invalidNode", "safeSave() called from invalid node.")); - } - log.debug(localize("safeSave.log.debug.invoke", "Invoking safesave for data set ") + label); - await zowe.Download.dataSet(node.getSession(), label, { - file: getDocumentFilePath(label, node) - }); - const document = await vscode.workspace.openTextDocument(getDocumentFilePath(label, node)); - await vscode.window.showTextDocument(document); - await vscode.window.activeTextEditor.document.save(); - } catch (err) { - if (err.message.includes(localize("safeSave.error.notFound", "not found"))) { - vscode.window.showInformationMessage(localize("safeSave.file1", "Unable to find file: ") + label + - localize("safeSave.file2", " was probably deleted.")); - } else { - vscode.window.showErrorMessage(err.message); - } - } -} - -/** - * Checks if there are changes on the mainframe before pushing changes - * - * @export - * @param {ZoweUSSNode} node The node which represents the file - */ -export async function safeSaveUSS(node: ZoweUSSNode) { - - log.debug("safe save requested for node: " + node.label); - try { - // Switch case from `safeSave` not needed, as we will only ever receive a file - log.debug("Invoking safesave for USS file " + node.fullPath); - await zowe.Download.ussFile(node.getSession(), node.fullPath, { - file: getUSSDocumentFilePath(node) - }); - const document = await vscode.workspace.openTextDocument(getUSSDocumentFilePath(node)); - await vscode.window.showTextDocument(document); - await vscode.window.activeTextEditor.document.save(); - } catch (err) { - if (err.message.includes("not found")) { - vscode.window.showInformationMessage(`Unable to find file: ${node.fullPath} was probably deleted.`); - } else { - vscode.window.showErrorMessage(err.message); - } - } -} - function checkForAddedSuffix(filename: string): boolean { // identify how close to the end of the string the last . is const dotPos = filename.length - ( 1 + filename.lastIndexOf(".") ); @@ -1386,17 +1324,50 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase vscode.window.showErrorMessage(err.message + "\n" + err.stack); } } + // Get specific node based on label + const node = (await sesNode.getChildren()).find((child) => + child.label.trim() === label); + const uploadEtag = node.getEtag(); + // define upload options + const uploadOptions: IUploadOptions = { + etag: uploadEtag, + returnEtag: true + } try { - const response = await vscode.window.withProgress({ + const uploadResponse = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: localize("saveFile.response.save.title", "Saving data set...") }, () => { - return zowe.Upload.pathToDataSet(documentSession, doc.fileName, label); // TODO MISSED TESTING + return zowe.Upload.pathToDataSet(documentSession, doc.fileName, label, uploadOptions); // TODO MISSED TESTING }); - if (response.success) { - vscode.window.showInformationMessage(response.commandResponse); // TODO MISSED TESTING + if (uploadResponse.success) { + vscode.window.showInformationMessage(uploadResponse.commandResponse); // TODO MISSED TESTING + // setting local etag with the new etag from the updated file on mainframe + node.setEtag(uploadResponse.apiResponse[0].etag); + } else if (!uploadResponse.success && uploadResponse.commandResponse.includes(localize("saveFile.error.ZosmfEtagMismatchError", "Rest API failure with HTTP(S) status 412"))) { + const downloadResponse = await zowe.Download.dataSet(node.getSession(),label, { + file: doc.fileName, + returnEtag: true}); + // re-assign etag, so that it can be used with subsequent requests + const downloadEtag = downloadResponse.apiResponse.etag; + if (downloadEtag != uploadEtag) { + node.setEtag(downloadEtag); + } + vscode.window.showWarningMessage(localize("saveFile.error.etagMismatch","Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict.")) + // Store document in a separate variable, to be used on merge conflict + const oldDoc = doc; + const oldDocText = oldDoc.getText(); + const startPosition = new vscode.Position(0,0); + const endPosition = new vscode.Position(oldDoc.lineCount,0); + const deleteRange = new vscode.Range(startPosition, endPosition); + await vscode.window.activeTextEditor.edit(editBuilder => { + // re-write the old content in the editor view + editBuilder.delete(deleteRange); + editBuilder.insert(startPosition, oldDocText); + }); + await vscode.window.activeTextEditor.document.save(); } else { - vscode.window.showErrorMessage(response.commandResponse); + vscode.window.showErrorMessage(uploadResponse.commandResponse); } } catch (err) { vscode.window.showErrorMessage(err.message); // TODO MISSED TESTING @@ -1425,22 +1396,51 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS documentSession = sesNode.getSession(); binary = Object.keys(sesNode.binaryFiles).find((child) => child === remote) !== undefined; } - + // Get specific node based on label + const node = (await sesNode.getChildren()).find((child) => + child.fullPath.trim() === remote); + const uploadEtag = node.getEtag(); try { - const response = await vscode.window.withProgress({ + const uploadResponse = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: localize("saveUSSFile.response.title", "Saving file...") }, () => { - return zowe.Upload.fileToUSSFile(documentSession, doc.fileName, remote, binary); // TODO MISSED TESTING + return zowe.Upload.fileToUSSFile(documentSession, doc.fileName, remote, binary, null, uploadEtag, true); // TODO MISSED TESTING }); - if (response.success) { - vscode.window.showInformationMessage(response.commandResponse); + if (uploadResponse.success) { + vscode.window.showInformationMessage(uploadResponse.commandResponse); + node.setEtag(uploadResponse.apiResponse.etag); + // this part never runs! zowe.Upload.fileToUSSFile doesn't return success: false, it just throws the error which is caught below!!!!! } else { - vscode.window.showErrorMessage(response.commandResponse); + vscode.window.showErrorMessage(uploadResponse.commandResponse); } } catch (err) { - log.error(localize("saveUSSFile.log.error.save", "Error encountered when saving USS file: ") + JSON.stringify(err)); - vscode.window.showErrorMessage(err.message); + if (err.message.includes(localize("saveFile.error.ZosmfEtagMismatchError", "Rest API failure with HTTP(S) status 412"))) { + const downloadResponse = await zowe.Download.ussFile(node.getSession(), node.fullPath, { + file: getUSSDocumentFilePath(node), + returnEtag: true}); + // re-assign etag, so that it can be used with subsequent requests + const downloadEtag = downloadResponse.apiResponse.etag; + if (downloadEtag != uploadEtag) { + node.setEtag(downloadEtag); + } + vscode.window.showWarningMessage(localize("saveFile.error.etagMismatch","Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict.")) + // Store document in a separate variable, to be used on merge conflict + const oldDoc = doc; + const oldDocText = oldDoc.getText(); + const startPosition = new vscode.Position(0,0); + const endPosition = new vscode.Position(oldDoc.lineCount,0); + const deleteRange = new vscode.Range(startPosition, endPosition); + await vscode.window.activeTextEditor.edit(editBuilder => { + // re-write the old content in the editor view + editBuilder.delete(deleteRange); + editBuilder.insert(startPosition, oldDocText); + }); + await vscode.window.activeTextEditor.document.save(); + } else { + log.error(localize("saveUSSFile.log.error.save", "Error encountered when saving USS file: ") + JSON.stringify(err)); + vscode.window.showErrorMessage(err.message); + } } } @@ -1470,20 +1470,23 @@ export async function openUSS(node: ZoweUSSNode, download = false, previewFile: } log.debug(localize("openUSS.log.debug.request", "requesting to open a uss file ") + label); // if local copy exists, open that instead of pulling from mainframe - if (download || !fs.existsSync(getUSSDocumentFilePath(node))) { + const documentFilePath = getUSSDocumentFilePath(node); + if (download || !fs.existsSync(documentFilePath)) { const chooseBinary = node.binary || await zowe.Utilities.isFileTagBinOrAscii(node.getSession(), node.fullPath); - await vscode.window.withProgress({ + let response = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: "Opening USS file...", }, function downloadUSSFile() { return zowe.Download.ussFile(node.getSession(), node.fullPath, { // TODO MISSED TESTING - file: getUSSDocumentFilePath(node), - binary: chooseBinary + file: documentFilePath, + binary: chooseBinary, + returnEtag: true }); } ); + node.setEtag(response.apiResponse.etag); } - const document = await vscode.workspace.openTextDocument(getUSSDocumentFilePath(node)); + const document = await vscode.workspace.openTextDocument(documentFilePath); if (previewFile === true) { await vscode.window.showTextDocument(document); } From 61402436afbb68d1aecd5498227ec81ae4f05708 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Tue, 26 Nov 2019 17:32:02 +0100 Subject: [PATCH 02/26] merge master into branch Signed-off-by: Alexandru-Paul Dumitru --- CHANGELOG.md | 6 + README.md | 4 +- .../extension.integration.test.ts | 303 ++++++++++++++ __tests__/__unit__/DatasetTree.unit.test.ts | 51 +++ __tests__/__unit__/Profiles.unit.test.ts | 138 ++++++- .../__snapshots__/extension.unit.test.ts.snap | 60 +++ __tests__/__unit__/extension.unit.test.ts | 377 ++++++++++++------ .../__unit__/uss/ussNodeActions.unit.test.ts | 35 +- i18n/sample/package.i18n.json | 10 +- i18n/sample/src/ProfileLoader.i18n.json | 7 +- i18n/sample/src/extension.i18n.json | 18 +- package-lock.json | 50 ++- package.json | 76 +++- package.nls.json | 3 + src/DatasetTree.ts | 57 +++ src/Profiles.ts | 104 +++-- src/USSTree.ts | 22 +- src/ZoweUSSNode.ts | 13 + src/extension.ts | 143 +++++++ src/uss/ussNodeActions.ts | 44 +- 20 files changed, 1275 insertions(+), 246 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 344a887c52..df2824f767 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documented in this file. +## 0.29.0 + +- Provide ability to rename datasets. Thanks @CForrest97 +- Fix URL parsing. @MarkAckert +- Fixed `AppSettings` error message. @jellypuno + ## 0.28.0 - Provide ability to add new profiles in explorer. Thanks @crawr, @jellypuno diff --git a/README.md b/README.md index 73f4f609fc..016ff1e592 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ Zowe™ Explorer extension modernizes the way developers and system administ Ensure that you meet the following prerequisites before using the extension: -* Access to z/OSMF. -* At least, one Zowe CLI `zosmf` profile. +* Configured TSO/E address space services, z/OS data set and file REST interface, and z/OS jobs REST interface. For more information, see [z/OS Requirements](https://docs.zowe.org/stable/user-guide/systemrequirements-zosmf.html#z-os-requirements). +* Zowe CLI `zosmf` profile. **Notes:** diff --git a/__tests__/__integration__/extension.integration.test.ts b/__tests__/__integration__/extension.integration.test.ts index df3956ba67..b9a9350353 100644 --- a/__tests__/__integration__/extension.integration.test.ts +++ b/__tests__/__integration__/extension.integration.test.ts @@ -383,6 +383,309 @@ describe("Extension Integration Tests", () => { // TODO add tests for saving data set from favorites }); + describe("Renaming a Data Set", () => { + const beforeDataSetName = `${pattern}.RENAME.BEFORE.TEST`; + const afterDataSetName = `${pattern}.RENAME.AFTER.TEST`; + + describe("Success Scenarios", () => { + afterEach(async () => { + await Promise.all([ + zowe.Delete.dataSet(sessionNode.getSession(), beforeDataSetName), + zowe.Delete.dataSet(sessionNode.getSession(), afterDataSetName), + ].map((p) => p.catch((err) => err))); + }); + describe("Rename Sequential Data Set", () => { + beforeEach(async () => { + await zowe.Create.dataSet( + sessionNode.getSession(), + zowe.CreateDataSetTypeEnum.DATA_SET_SEQUENTIAL, + beforeDataSetName + ).catch((err) => err); + }); + it("should rename a data set", async () => { + let error; + let beforeList; + let afterList; + + try { + const testNode = new ZoweNode(beforeDataSetName, vscode.TreeItemCollapsibleState.None, sessionNode, session); + const inputBoxStub = sandbox.stub(vscode.window, "showInputBox"); + inputBoxStub.returns(afterDataSetName); + + await extension.renameDataSet(testNode, testTree); + beforeList = await zowe.List.dataSet(sessionNode.getSession(), beforeDataSetName); + afterList = await zowe.List.dataSet(sessionNode.getSession(), afterDataSetName); + } catch (err) { + error = err; + } + + expect(error).to.be.equal(undefined); + + expect(beforeList.apiResponse.returnedRows).to.equal(0); + expect(afterList.apiResponse.returnedRows).to.equal(1); + }).timeout(TIMEOUT); + }); + describe("Rename Partitioned Data Set", () => { + beforeEach(async () => { + await zowe.Create.dataSet( + sessionNode.getSession(), + zowe.CreateDataSetTypeEnum.DATA_SET_PARTITIONED, + beforeDataSetName + ).catch((err) => err); + }); + it("should rename a data set", async () => { + let error; + let beforeList; + let afterList; + + try { + const testNode = new ZoweNode(beforeDataSetName, vscode.TreeItemCollapsibleState.None, sessionNode, session); + + const inputBoxStub = sandbox.stub(vscode.window, "showInputBox"); + inputBoxStub.returns(afterDataSetName); + + await extension.renameDataSet(testNode, testTree); + beforeList = await zowe.List.dataSet(sessionNode.getSession(), beforeDataSetName); + afterList = await zowe.List.dataSet(sessionNode.getSession(), afterDataSetName); + } catch (err) { + error = err; + } + + expect(error).to.be.equal(undefined); + + expect(beforeList.apiResponse.returnedRows).to.equal(0); + expect(afterList.apiResponse.returnedRows).to.equal(1); + }).timeout(TIMEOUT); + }); + }); + describe("Failure Scenarios", () => { + describe("Rename Sequential Data Set", () => { + it("should throw an error if a missing data set name is provided", async () => { + let error; + + try { + const testNode = new ZoweNode(beforeDataSetName, vscode.TreeItemCollapsibleState.None, sessionNode, session); + const inputBoxStub = sandbox.stub(vscode.window, "showInputBox"); + inputBoxStub.returns("MISSING.DATA.SET"); + + await extension.renameDataSet(testNode, testTree); + } catch (err) { + error = err; + } + + expect(error).not.to.be.equal(undefined); + }).timeout(TIMEOUT); + }); + }); + }); + + describe("Copying data sets", () => { + describe("Success Scenarios", () => { + describe("Sequential > Sequential", () => { + const fromDataSetName = `${pattern}.COPY.FROM.SET`; + const toDataSetName = `${pattern}.COPY.TO.SET`; + + beforeEach(async () => { + await Promise.all([ + zowe.Create.dataSet( + sessionNode.getSession(), + zowe.CreateDataSetTypeEnum.DATA_SET_SEQUENTIAL, + fromDataSetName, + ), + zowe.Create.dataSet( + sessionNode.getSession(), + zowe.CreateDataSetTypeEnum.DATA_SET_SEQUENTIAL, + toDataSetName, + ), + ].map((p) => p.catch((err) => err))); + + await zowe.Upload.bufferToDataSet(sessionNode.getSession(), Buffer.from("1234"), fromDataSetName).catch((err) => err); + }); + afterEach(async () => { + await Promise.all([ + zowe.Delete.dataSet(sessionNode.getSession(), fromDataSetName), + zowe.Delete.dataSet(sessionNode.getSession(), toDataSetName), + ].map((p) => p.catch((err) => err))); + }); + + it("Should copy a data set", async () => { + let error; + let contents; + + try { + const fromNode = new ZoweNode(fromDataSetName, vscode.TreeItemCollapsibleState.None, sessionNode, session); + const toNode = new ZoweNode(toDataSetName, vscode.TreeItemCollapsibleState.None, sessionNode, session); + + await extension.copyDataSet(fromNode); + await extension.pasteDataSet(toNode, testTree); + + contents = await zowe.Get.dataSet(sessionNode.getSession(), fromDataSetName); + } catch (err) { + error = err; + } + + expect(error).to.be.equal(undefined); + + expect(contents.toString()).to.equal("1234\n"); + }).timeout(TIMEOUT); + }); + describe("Member > Member", () => { + const dataSetName = `${pattern}.COPY.DATA.SET`; + const fromMemberName = "file1"; + const toMemberName = "file2"; + + beforeEach(async () => { + await zowe.Create.dataSet( + sessionNode.getSession(), + zowe.CreateDataSetTypeEnum.DATA_SET_PARTITIONED, + dataSetName, + ).catch((err) => err); + + await zowe.Upload.bufferToDataSet(sessionNode.getSession(), Buffer.from("1234"), `${dataSetName}(${fromMemberName})`); + }); + afterEach(async () => { + await zowe.Delete.dataSet(sessionNode.getSession(), dataSetName).catch((err) => err); + }); + it("Should copy a data set", async () => { + let error; + let contents; + + try { + const parentNode = new ZoweNode(dataSetName, vscode.TreeItemCollapsibleState.None, sessionNode, session); + const fromNode = new ZoweNode(fromMemberName, vscode.TreeItemCollapsibleState.None, parentNode, session); + parentNode.contextValue = extension.DS_PDS_CONTEXT; + fromNode.contextValue = extension.DS_MEMBER_CONTEXT; + + const inputBoxStub = sandbox.stub(vscode.window, "showInputBox"); + inputBoxStub.returns(toMemberName); + + await extension.copyDataSet(fromNode); + await extension.pasteDataSet(parentNode, testTree); + + contents = await zowe.Get.dataSet(sessionNode.getSession(), `${dataSetName}(${toMemberName})`); + } catch (err) { + error = err; + } + + expect(error).to.be.equal(undefined); + + expect(contents.toString()).to.equal("1234\n"); + }).timeout(TIMEOUT); + }); + describe("Sequential > Member", () => { + const fromDataSetName = `${pattern}.COPY.FROM.SET`; + const toDataSetName = `${pattern}.COPY.TO.SET`; + const toMemberName = "file2"; + + beforeEach(async () => { + await Promise.all([ + zowe.Create.dataSet( + sessionNode.getSession(), + zowe.CreateDataSetTypeEnum.DATA_SET_SEQUENTIAL, + fromDataSetName, + ), + zowe.Create.dataSet( + sessionNode.getSession(), + zowe.CreateDataSetTypeEnum.DATA_SET_PARTITIONED, + toDataSetName, + ), + ].map((p) => p.catch((err) => err))); + + await zowe.Upload.bufferToDataSet(sessionNode.getSession(), Buffer.from("1234"), fromDataSetName).catch((err) => err); + }); + afterEach(async () => { + await Promise.all([ + zowe.Delete.dataSet(sessionNode.getSession(), fromDataSetName), + zowe.Delete.dataSet(sessionNode.getSession(), toDataSetName), + ].map((p) => p.catch((err) => err))); + }); + + it("Should copy a data set", async () => { + let error; + let contents; + + try { + const fromNode = new ZoweNode(fromDataSetName, vscode.TreeItemCollapsibleState.None, sessionNode, session); + const toNode = new ZoweNode(toDataSetName, vscode.TreeItemCollapsibleState.None, sessionNode, session); + fromNode.contextValue = extension.DS_DS_CONTEXT; + toNode.contextValue = extension.DS_PDS_CONTEXT; + + const inputBoxStub = sandbox.stub(vscode.window, "showInputBox"); + inputBoxStub.returns(toMemberName); + + await extension.copyDataSet(fromNode); + await extension.pasteDataSet(toNode, testTree); + + contents = await zowe.Get.dataSet(sessionNode.getSession(), `${toDataSetName}(${toMemberName})`); + } catch (err) { + error = err; + } + + expect(error).to.be.equal(undefined); + + expect(contents.toString()).to.equal("1234\n"); + }).timeout(TIMEOUT); + }); + describe("Member > Sequential", () => { + const fromDataSetName = `${pattern}.COPY.FROM.SET`; + const toDataSetName = `${pattern}.COPY.TO.SET`; + const fromMemberName = "file1"; + + beforeEach(async () => { + await Promise.all([ + zowe.Create.dataSet( + sessionNode.getSession(), + zowe.CreateDataSetTypeEnum.DATA_SET_PARTITIONED, + fromDataSetName, + ), + zowe.Create.dataSet( + sessionNode.getSession(), + zowe.CreateDataSetTypeEnum.DATA_SET_SEQUENTIAL, + toDataSetName, + ), + ].map((p) => p.catch((err) => err))); + + await zowe.Upload.bufferToDataSet( + sessionNode.getSession(), + Buffer.from("1234"), + `${fromDataSetName}(${fromMemberName})`, + ).catch((err) => err); + }); + afterEach(async () => { + await Promise.all([ + zowe.Delete.dataSet(sessionNode.getSession(), fromDataSetName), + zowe.Delete.dataSet(sessionNode.getSession(), toDataSetName), + ].map((p) => p.catch((err) => err))); + }); + + it("Should copy a data set", async () => { + let error; + let contents; + + try { + const fromParentNode = new ZoweNode(fromDataSetName, vscode.TreeItemCollapsibleState.None, sessionNode, session); + const fromMemberNode = new ZoweNode(fromMemberName, vscode.TreeItemCollapsibleState.None, fromParentNode, session); + const toNode = new ZoweNode(toDataSetName, vscode.TreeItemCollapsibleState.None, sessionNode, session); + fromParentNode.contextValue = extension.DS_PDS_CONTEXT; + fromMemberNode.contextValue = extension.DS_MEMBER_CONTEXT; + toNode.contextValue = extension.DS_DS_CONTEXT; + + await extension.copyDataSet(fromMemberNode); + await extension.pasteDataSet(toNode, testTree); + + contents = await zowe.Get.dataSet(sessionNode.getSession(), toDataSetName); + } catch (err) { + error = err; + } + + expect(error).to.be.equal(undefined); + + expect(contents.toString()).to.equal("1234\n"); + }).timeout(TIMEOUT); + }); + }); + }); + describe("Updating Temp Folder", () => { // define paths const testingPath = path.join(__dirname, "..", "..", "..", "test"); diff --git a/__tests__/__unit__/DatasetTree.unit.test.ts b/__tests__/__unit__/DatasetTree.unit.test.ts index 1bce7470b9..c143447ef6 100644 --- a/__tests__/__unit__/DatasetTree.unit.test.ts +++ b/__tests__/__unit__/DatasetTree.unit.test.ts @@ -551,4 +551,55 @@ describe("DatasetTree Unit Tests", () => { expect(getConfiguration.mock.calls.length).toBe(2); }); + it("Should rename a favorited node", async () => { + const sessionNode = testTree.mSessionNodes[1]; + const newLabel = "USER.NEW.LABEL"; + testTree.mFavorites = []; + const node = new ZoweNode("node", vscode.TreeItemCollapsibleState.Collapsed, sessionNode, null); + + testTree.addFavorite(node); + node.label = `[${sessionNode.label.trim()}]: ${node.label}`; + testTree.renameFavorite(node, newLabel); + + expect(testTree.mFavorites.length).toEqual(1); + expect(testTree.mFavorites[0].label).toBe(`[${sessionNode.label.trim()}]: ${newLabel}`); + }); + + it("Should rename a node", async () => { + const sessionNode = testTree.mSessionNodes[1]; + const newLabel = "USER.NEW.LABEL"; + const node = new ZoweNode("node", vscode.TreeItemCollapsibleState.Collapsed, sessionNode, null); + sessionNode.children.push(node); + testTree.renameNode(sessionNode.label.trim(), "node", newLabel); + + expect(sessionNode.children[sessionNode.children.length-1].label).toBe(newLabel); + sessionNode.children.pop(); + }); + + it("Should find a favorited node", async () => { + testTree.mFavorites = []; + const sessionNode = testTree.mSessionNodes[1]; + const nonFavoritedNode = new ZoweNode("node", vscode.TreeItemCollapsibleState.Collapsed, sessionNode, null); + const favoritedNode = new ZoweNode("[testSess]: node", vscode.TreeItemCollapsibleState.Collapsed, sessionNode, null); + favoritedNode.contextValue = extension.DS_PDS_CONTEXT + extension.FAV_SUFFIX; + + testTree.mFavorites.push(favoritedNode); + const foundNode = testTree.findFavoritedNode(nonFavoritedNode); + + expect(foundNode).toBe(favoritedNode); + testTree.mFavorites.pop(); + }); + + it("Should find a non-favorited node", async () => { + const sessionNode = testTree.mSessionNodes[1]; + const nonFavoritedNode = new ZoweNode("node", vscode.TreeItemCollapsibleState.Collapsed, sessionNode, null); + const favoritedNode = new ZoweNode("[testSess]: node", vscode.TreeItemCollapsibleState.Collapsed, sessionNode, null); + + sessionNode.children.push(nonFavoritedNode); + + const foundNode = testTree.findNonFavoritedNode(favoritedNode); + + expect(foundNode).toBe(nonFavoritedNode); + sessionNode.children.pop(); + }); }); diff --git a/__tests__/__unit__/Profiles.unit.test.ts b/__tests__/__unit__/Profiles.unit.test.ts index a57d1f781f..a9305d428f 100644 --- a/__tests__/__unit__/Profiles.unit.test.ts +++ b/__tests__/__unit__/Profiles.unit.test.ts @@ -11,11 +11,11 @@ jest.mock("vscode"); jest.mock("child_process"); -import * as vscode from "vscode"; +import { Logger } from "@brightside/imperative"; import * as child_process from "child_process"; -import { Logger, profileLoadError } from "@brightside/imperative"; -import { Profiles } from "../../src/Profiles"; +import * as vscode from "vscode"; import * as loader from "../../src/ProfileLoader"; +import { Profiles } from "../../src/Profiles"; describe("Profile class unit tests", () => { // Mocking log.debug @@ -34,7 +34,7 @@ describe("Profile class unit tests", () => { }); }); afterEach(() => { - jest.resetAllMocks(); + jest.resetAllMocks(); }); it("should create an instance", async () => { @@ -81,8 +81,8 @@ describe("Profile class unit tests", () => { Object.defineProperty(vscode.window, "showInformationMessage", { value: showInformationMessage }); Object.defineProperty(vscode.window, "showErrorMessage", { value: showErrorMessage }); - Object.defineProperty(vscode.window, "showInputBox", {value: showInputBox}); - Object.defineProperty(vscode.window, "showQuickPick", {value: showQuickPick}); + Object.defineProperty(vscode.window, "showInputBox", { value: showInputBox }); + Object.defineProperty(vscode.window, "showQuickPick", { value: showQuickPick }); beforeEach(async () => { profiles = await Profiles.createInstance(log); @@ -168,16 +168,134 @@ describe("Profile class unit tests", () => { } }); + it("should create profile https+443", async () => { + showInputBox.mockResolvedValueOnce("fake"); + showInputBox.mockResolvedValueOnce("https://fake:443"); + showInputBox.mockResolvedValueOnce("fake"); + showInputBox.mockResolvedValueOnce("fake"); + showQuickPick.mockReset(); + showQuickPick.mockResolvedValueOnce("False - Accept connections with self-signed certificates"); + let success = true; + // Do more test + try { + await profiles.createNewConnection(); + } catch (error) { + success = false; + } + expect(success).toBe(true); + if (success) { + expect(showInformationMessage.mock.calls.length).toBe(1); + expect(showInformationMessage.mock.calls[0][0]).toBe("Profile fake was created."); + } else { + expect(showInformationMessage.mock.calls.length).toBe(0); + // expect(showInformationMessage.mock.calls[0][0]).toBe("Operation Cancelled"); + } + }); + + it("should create 2 consecutive profiles", async () => { + let success = true; + showInputBox.mockResolvedValueOnce("fake1"); + showInputBox.mockResolvedValueOnce("https://fake1:143"); + showInputBox.mockResolvedValueOnce("fake1"); + showInputBox.mockResolvedValueOnce("fake1"); + showQuickPick.mockReset(); + showQuickPick.mockResolvedValueOnce("False - Accept connections with self-signed certificates"); + // Do more test + try { + await profiles.createNewConnection(); + } catch (error) { + success = false; + } + expect(success).toBe(true); + if (success) { + expect(showInformationMessage.mock.calls.length).toBe(1); + expect(showInformationMessage.mock.calls[0][0]).toBe("Profile fake1 was created."); + } else { + expect(showInformationMessage.mock.calls.length).toBe(0); + // expect(showInformationMessage.mock.calls[0][0]).toBe("Operation Cancelled"); + } + + showInputBox.mockReset(); + showInformationMessage.mockReset(); + showInputBox.mockResolvedValueOnce("fake2"); + showInputBox.mockResolvedValueOnce("https://fake2:143"); + showInputBox.mockResolvedValueOnce("fake2"); + showInputBox.mockResolvedValueOnce("fake2"); + showQuickPick.mockReset(); + showQuickPick.mockResolvedValueOnce("False - Accept connections with self-signed certificates"); + success = true; + // Do more test + try { + await profiles.createNewConnection(); + } catch (error) { + success = false; + } + expect(success).toBe(true); + if (success) { + expect(showInformationMessage.mock.calls.length).toBe(1); + expect(showInformationMessage.mock.calls[0][0]).toBe("Profile fake2 was created."); + } else { + expect(showInformationMessage.mock.calls.length).toBe(0); + // expect(showInformationMessage.mock.calls[0][0]).toBe("Operation Cancelled"); + } + }); + it("should validate URL", async () => { - const res = await Profiles.getInstance().validateUrl("fake/url"); - expect(res).toBe(false); + const res = await Profiles.getInstance().validateAndParseUrl("fake/url"); + expect(res.valid).toBe(false); }); it("should validate URL", async () => { - const res = await Profiles.getInstance().validateUrl("https://fake:143"); - expect(res).toBe(true); + const res = await Profiles.getInstance().validateAndParseUrl("https://fake:143"); + expect(res.valid).toBe(true); + expect(res.host).toBe("fake"); + // tslint:disable-next-line + expect(res.port).toBe(143); + + }); + + it("should validate https: url", async () => { + const res = await Profiles.getInstance().validateAndParseUrl("https://10.142.0.23/some/path"); + expect(res.valid).toBe(true); + expect(res.host).toBe("10.142.0.23"); + // tslint:disable-next-line + expect(res.port).toBe(443); + }); + + it("should validate https:443 url", async () => { + const res = await Profiles.getInstance().validateAndParseUrl("https://10.142.0.23:443"); + expect(res.valid).toBe(true); + expect(res.host).toBe("10.142.0.23"); + // tslint:disable-next-line + expect(res.port).toBe(443); + }); + + it("should reject http: url", async () => { + const res = await Profiles.getInstance().validateAndParseUrl("http://10.142.0.23/some/path"); + expect(res.valid).toBe(false); }); + it("should reject out of range port url", async () => { + const res = await Profiles.getInstance().validateAndParseUrl("http://10.142.0.23:9999999999/some/path"); + expect(res.valid).toBe(false); + }); + + it("should reject http:80 url", async () => { + const res = await Profiles.getInstance().validateAndParseUrl("http://fake:80"); + expect(res.valid).toBe(false); + }); + + it("should reject ftp protocol url", async () => { + const res = await Profiles.getInstance().validateAndParseUrl("ftp://fake:80"); + expect(res.valid).toBe(false); + }); + + it("should reject invalid url syntax", async () => { + const res = await Profiles.getInstance().validateAndParseUrl("https://fake::80"); + expect(res.valid).toBe(false); + }); + + it("it should validate duplicate profiles", async () => { showInputBox.mockResolvedValueOnce("profile1"); showInputBox.mockResolvedValueOnce("https://fake:143"); diff --git a/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap b/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap index 9130cd2adc..094fcb9889 100644 --- a/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap +++ b/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap @@ -162,6 +162,11 @@ Array [ "group": "3_systemSpecific", "when": "view == zowe.explorer && viewItem == member", }, + Object { + "command": "zowe.copyDataSet", + "group": "9_cutcopypaste", + "when": "view == zowe.explorer && viewItem == member", + }, Object { "command": "zowe.refreshNode", "group": "inline", @@ -232,6 +237,21 @@ Array [ "group": "2_information", "when": "view == zowe.explorer && viewItem == ds", }, + Object { + "command": "zowe.renameDataSet", + "group": "6_modification@2", + "when": "view == zowe.explorer && viewItem == ds", + }, + Object { + "command": "zowe.copyDataSet", + "group": "9_cutcopypaste", + "when": "view == zowe.explorer && viewItem == ds", + }, + Object { + "command": "zowe.pasteDataSet", + "group": "9_cutcopypaste", + "when": "view == zowe.explorer && viewItem == ds", + }, Object { "command": "zowe.refreshNode", "group": "inline", @@ -272,6 +292,21 @@ Array [ "group": "4_workspace", "when": "view == zowe.explorer && viewItem == ds_fav", }, + Object { + "command": "zowe.renameDataSet", + "group": "6_modification@2", + "when": "view == zowe.explorer && viewItem == ds_fav", + }, + Object { + "command": "zowe.copyDataSet", + "group": "9_cutcopypaste", + "when": "view == zowe.explorer && viewItem == ds_fav", + }, + Object { + "command": "zowe.pasteDataSet", + "group": "9_cutcopypaste", + "when": "view == zowe.explorer && viewItem == ds_fav", + }, Object { "command": "zowe.createMember", "group": "1_create", @@ -297,6 +332,16 @@ Array [ "group": "1_create", "when": "view == zowe.explorer && viewItem == pds", }, + Object { + "command": "zowe.renameDataSet", + "group": "6_modification@2", + "when": "view == zowe.explorer && viewItem == pds", + }, + Object { + "command": "zowe.pasteDataSet", + "group": "9_cutcopypaste", + "when": "view == zowe.explorer && viewItem == pds", + }, Object { "command": "zowe.createMember", "group": "1_create", @@ -322,6 +367,21 @@ Array [ "group": "4_workspace", "when": "view == zowe.explorer && viewItem == pds_fav", }, + Object { + "command": "zowe.renameDataSet", + "group": "6_modification@2", + "when": "view == zowe.explorer && viewItem == pds_fav", + }, + Object { + "command": "zowe.copyDataSet", + "group": "9_cutcopypaste", + "when": "view == zowe.explorer && viewItem == pds_fav", + }, + Object { + "command": "zowe.pasteDataSet", + "group": "9_cutcopypaste", + "when": "view == zowe.explorer && viewItem == pds_fav", + }, Object { "command": "zowe.showDSAttributes", "group": "2_information", diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index 277b6d3f28..d5c83bac72 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -142,6 +142,8 @@ describe("Extension Unit Tests", () => { const dataSet = jest.fn(); const ussFile = jest.fn(); const List = jest.fn(); + const Get = jest.fn(); + const dataSetGet = jest.fn(); const fileToUSSFile = jest.fn(); const dataSetList = jest.fn(); const fileList = jest.fn(); @@ -190,6 +192,21 @@ describe("Extension Unit Tests", () => { const mockInitializeUSS = jest.fn(); const ussPattern = jest.fn(); const mockPattern = jest.fn(); + const Rename = jest.fn(); + const renameDataSet = jest.fn(); + const mockRenameFavorite = jest.fn(); + const mockUpdateFavorites = jest.fn(); + const mockRenameNode = jest.fn(); + const Copy = jest.fn(); + const copyDataSet = jest.fn(); + const findFavoritedNode = jest.fn(); + const findNonFavoritedNode = jest.fn(); + let mockClipboardData: string; + const clipboard = { + writeText: jest.fn().mockImplementation((value) => mockClipboardData = value), + readText: jest.fn().mockImplementation(() => mockClipboardData), + }; + const ProgressLocation = jest.fn().mockImplementation(() => { return { Notification: 15 @@ -209,7 +226,12 @@ describe("Extension Unit Tests", () => { getChildren: mockGetChildren, removeFavorite: mockRemoveFavorite, enterPattern: mockPattern, - initializeFavorites: mockInitialize + initializeFavorites: mockInitialize, + renameFavorite: mockRenameFavorite, + updateFavorites: mockUpdateFavorites, + renameNode: mockRenameNode, + findFavoritedNode, + findNonFavoritedNode, }; }); const USSTree = jest.fn().mockImplementation(() => { @@ -301,6 +323,8 @@ describe("Extension Unit Tests", () => { Object.defineProperty(Upload, "fileToUSSFile", {value: fileToUSSFile}); Object.defineProperty(brightside, "Create", {value: Create}); Object.defineProperty(Create, "dataSet", {value: dataSetCreate}); + Object.defineProperty(brightside, "Get", {value: Get}); + Object.defineProperty(Get, "dataSet", {value: dataSetGet}); Object.defineProperty(brightside, "List", {value: List}); Object.defineProperty(List, "dataSet", {value: dataSetList}); Object.defineProperty(List, "fileList", {value: fileList}); @@ -335,7 +359,11 @@ describe("Extension Unit Tests", () => { Object.defineProperty(vscode.workspace, "registerTextDocumentContentProvider", { value: registerTextDocumentContentProvider}); Object.defineProperty(vscode.Disposable, "from", {value: from}); Object.defineProperty(vscode.Uri, "parse", {value: parse}); - + Object.defineProperty(brightside, "Rename", {value: Rename}); + Object.defineProperty(Rename, "dataSet", { value: renameDataSet }); + Object.defineProperty(brightside, "Copy", {value: Copy}); + Object.defineProperty(Copy, "dataSet", { value: copyDataSet }); + Object.defineProperty(vscode.env, "clipboard", { value: clipboard }); beforeEach(() => { mockLoadNamedProfile.mockReturnValue({profile: {name:"aProfile", type:"zosmf"}}); @@ -494,7 +522,7 @@ describe("Extension Unit Tests", () => { expect(createTreeView.mock.calls[0][0]).toBe("zowe.explorer"); expect(createTreeView.mock.calls[1][0]).toBe("zowe.uss.explorer"); // tslint:disable-next-line: no-magic-numbers - expect(registerCommand.mock.calls.length).toBe(61); + expect(registerCommand.mock.calls.length).toBe(59); registerCommand.mock.calls.forEach((call, i ) => { expect(registerCommand.mock.calls[i][1]).toBeInstanceOf(Function); }); @@ -518,18 +546,19 @@ describe("Extension Unit Tests", () => { "zowe.editMember", "zowe.removeSession", "zowe.removeFavorite", - "zowe.safeSave", "zowe.saveSearch", "zowe.removeSavedSearch", "zowe.submitJcl", "zowe.submitMember", "zowe.showDSAttributes", + "zowe.renameDataSet", + "zowe.copyDataSet", + "zowe.pasteDataSet", "zowe.uss.addFavorite", "zowe.uss.removeFavorite", "zowe.uss.addSession", "zowe.uss.refreshAll", "zowe.uss.refreshUSS", - "zowe.uss.safeSaveUSS", "zowe.uss.fullPath", "zowe.uss.ZoweUSSNode.open", "zowe.uss.removeSession", @@ -718,7 +747,8 @@ describe("Extension Unit Tests", () => { expect(dataSet.mock.calls[0][0]).toBe(node.getSession()); expect(dataSet.mock.calls[0][1]).toBe(node.label); expect(dataSet.mock.calls[0][2]).toEqual({ - file: path.join(extension.DS_DIR, node.getSessionNode().label, node.label ) + file: path.join(extension.DS_DIR, node.getSessionNode().label, node.label), + returnEtag: true }); expect(openTextDocument.mock.calls.length).toBe(1); expect(openTextDocument.mock.calls[0][0]).toBe(path.join(extension.DS_DIR, @@ -784,12 +814,6 @@ describe("Extension Unit Tests", () => { showErrorMessage.mockReset(); dataSet.mockReset(); openTextDocument.mockReset(); - parent.contextValue = "turnip"; - await extension.safeSave(child); - expect(openTextDocument.mock.calls.length).toBe(0); - expect(dataSet.mock.calls.length).toBe(0); - expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual("safeSave() called from invalid node."); }); @@ -1409,126 +1433,6 @@ describe("Extension Unit Tests", () => { showErrorMessage.mockReset(); }); - it("Testing that safeSave is executed successfully", async () => { - dataSet.mockReset(); - openTextDocument.mockReset(); - showTextDocument.mockReset(); - showInformationMessage.mockReset(); - - const node = new ZoweNode("node", vscode.TreeItemCollapsibleState.None, sessNode, null); - const parent = new ZoweNode("parent", vscode.TreeItemCollapsibleState.Collapsed, sessNode, null); - const child = new ZoweNode("child", vscode.TreeItemCollapsibleState.None, parent, null); - - openTextDocument.mockResolvedValueOnce("test"); - - await extension.safeSave(node); - - expect(dataSet.mock.calls.length).toBe(1); - expect(dataSet.mock.calls[0][0]).toBe(session); - expect(dataSet.mock.calls[0][1]).toBe(node.label); - expect(dataSet.mock.calls[0][2]).toEqual({file: extension.getDocumentFilePath(node.label, node)}); - expect(openTextDocument.mock.calls.length).toBe(1); - expect(openTextDocument.mock.calls[0][0]).toBe(path.join(extension.DS_DIR, - node.getSessionNode().label.trim(), node.label )); - expect(showTextDocument.mock.calls.length).toBe(1); - expect(showTextDocument.mock.calls[0][0]).toBe("test"); - expect(save.mock.calls.length).toBe(1); - - dataSet.mockReset(); - dataSet.mockRejectedValueOnce(Error("not found")); - - await extension.safeSave(node); - - expect(showInformationMessage.mock.calls.length).toBe(1); - expect(showInformationMessage.mock.calls[0][0]).toBe("Unable to find file: " + node.label + " was probably deleted."); - - dataSet.mockReset(); - showErrorMessage.mockReset(); - dataSet.mockRejectedValueOnce(Error("")); - - await extension.safeSave(child); - - expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual(""); - - openTextDocument.mockResolvedValueOnce("test"); - openTextDocument.mockResolvedValueOnce("test"); - - dataSet.mockReset(); - openTextDocument.mockReset(); - node.contextValue = extension.DS_PDS_CONTEXT + extension.FAV_SUFFIX; - await extension.safeSave(node); - expect(openTextDocument.mock.calls.length).toBe(1); - expect(dataSet.mock.calls.length).toBe(1); - - dataSet.mockReset(); - openTextDocument.mockReset(); - parent.contextValue = extension.DS_PDS_CONTEXT + extension.FAV_SUFFIX; - await extension.safeSave(child); - expect(openTextDocument.mock.calls.length).toBe(1); - expect(dataSet.mock.calls.length).toBe(1); - - dataSet.mockReset(); - openTextDocument.mockReset(); - parent.contextValue = extension.FAVORITE_CONTEXT; - await extension.safeSave(child); - expect(openTextDocument.mock.calls.length).toBe(1); - expect(dataSet.mock.calls.length).toBe(1); - - showErrorMessage.mockReset(); - dataSet.mockReset(); - openTextDocument.mockReset(); - parent.contextValue = "turnip"; - await extension.safeSave(child); - expect(openTextDocument.mock.calls.length).toBe(0); - expect(dataSet.mock.calls.length).toBe(0); - expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual("safeSave() called from invalid node."); - }); - - it("Testing that safeSaveUSS is executed successfully", async () => { - ussFile.mockReset(); - openTextDocument.mockReset(); - showTextDocument.mockReset(); - showInformationMessage.mockReset(); - save.mockReset(); - - const node = new ZoweUSSNode("node", vscode.TreeItemCollapsibleState.None, ussNode, null, null); - const parent = new ZoweUSSNode("parent", vscode.TreeItemCollapsibleState.Collapsed, ussNode, null, null); - const child = new ZoweUSSNode("child", vscode.TreeItemCollapsibleState.None, parent, null, null); - - openTextDocument.mockResolvedValueOnce("test"); - - await extension.safeSaveUSS(node); - - expect(ussFile.mock.calls.length).toBe(1); - expect(ussFile.mock.calls[0][0]).toBe(node.getSession()); - expect(ussFile.mock.calls[0][1]).toBe(node.fullPath); - expect(ussFile.mock.calls[0][2]).toEqual({file: extension.getUSSDocumentFilePath(node)}); - expect(openTextDocument.mock.calls.length).toBe(1); - expect(openTextDocument.mock.calls[0][0]).toBe(path.join(extension.getUSSDocumentFilePath(node))); - expect(showTextDocument.mock.calls.length).toBe(1); - expect(showTextDocument.mock.calls[0][0]).toBe("test"); - expect(save.mock.calls.length).toBe(1); - - ussFile.mockReset(); - ussFile.mockRejectedValueOnce(Error("not found")); - - await extension.safeSaveUSS(node); - - expect(showInformationMessage.mock.calls.length).toBe(1); - expect(showInformationMessage.mock.calls[0][0]).toBe("Unable to find file: " + node.fullPath + " was probably deleted."); - - ussFile.mockReset(); - showErrorMessage.mockReset(); - ussFile.mockRejectedValueOnce(Error("")); - - await extension.safeSaveUSS(child); - - expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual(""); - }); - it("Testing that refreshUSS correctly executes with and without error", async () => { const node = new ZoweUSSNode("test-node", vscode.TreeItemCollapsibleState.None, ussNode, null, "/"); const parent = new ZoweUSSNode("parent", vscode.TreeItemCollapsibleState.Collapsed, node, null, "/"); @@ -1688,11 +1592,20 @@ describe("Extension Unit Tests", () => { showErrorMessage.mockReset(); existsSync.mockReset(); withProgress.mockReset(); - + const node = new ZoweUSSNode("node", vscode.TreeItemCollapsibleState.None, ussNode, null, "/"); const parent = new ZoweUSSNode("parent", vscode.TreeItemCollapsibleState.Collapsed, ussNode, null, "/"); const child = new ZoweUSSNode("child", vscode.TreeItemCollapsibleState.None, parent, null, "/parent"); + downloadUSSFile.mockReturnValueOnce({ + success: true, + commandResponse: "", + apiResponse: { + data: "", + etag: "123" + } + }); + isFileTagBinOrAscii.mockReturnValue(false); existsSync.mockReturnValue(null); openTextDocument.mockResolvedValueOnce("test.doc"); @@ -2385,6 +2298,204 @@ describe("Extension Unit Tests", () => { expect(showInputBox.mock.calls.length).toBe(1); }); + describe("Renaming Data Sets", () => { + it("Should rename the node", async () => { + showInputBox.mockReset(); + renameDataSet.mockReset(); + + const child = new ZoweNode("HLQ.TEST.DELETE.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + + showInputBox.mockResolvedValueOnce("HLQ.TEST.DELETE.NODE.NEW"); + await extension.renameDataSet(child, testTree); + + expect(renameDataSet.mock.calls.length).toBe(1); + expect(renameDataSet).toHaveBeenLastCalledWith(child.getSession(), "HLQ.TEST.DELETE.NODE", "HLQ.TEST.DELETE.NODE.NEW"); + }); + it("Should rename a favorited node", async () => { + showInputBox.mockReset(); + renameDataSet.mockReset(); + + const child = new ZoweNode("[sessNode]: HLQ.TEST.DELETE.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + child.contextValue = "ds_fav"; + showInputBox.mockResolvedValueOnce("HLQ.TEST.DELETE.NODE.NEW"); + await extension.renameDataSet(child, testTree); + + expect(renameDataSet.mock.calls.length).toBe(1); + expect(renameDataSet).toHaveBeenLastCalledWith(child.getSession(), "HLQ.TEST.DELETE.NODE", "HLQ.TEST.DELETE.NODE.NEW"); + }); + it("Should throw an error if zowe.Rename.dataSet throws", async () => { + let error; + const defaultError = new Error("Default error message"); + + showInputBox.mockReset(); + renameDataSet.mockReset(); + renameDataSet.mockImplementation(() => { throw defaultError; }); + + const child = new ZoweNode("[sessNode]: HLQ.TEST.DELETE.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + child.contextValue = "ds_fav"; + showInputBox.mockResolvedValueOnce("HLQ.TEST.DELETE.NODE.NEW"); + try { + await extension.renameDataSet(child, testTree); + } catch (err) { + error = err; + } + + expect(renameDataSet.mock.calls.length).toBe(1); + expect(renameDataSet).toHaveBeenLastCalledWith(child.getSession(), "HLQ.TEST.DELETE.NODE", "HLQ.TEST.DELETE.NODE.NEW"); + expect(error).toBe(defaultError); + }); + }); + describe("Copying Data Sets", () => { + it("Should copy the label of a node to the clipboard", async () => { + renameDataSet.mockReset(); + + const node = new ZoweNode("HLQ.TEST.DELETE.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + node.contextValue = extension.DS_SESSION_CONTEXT; + + await extension.copyDataSet(node); + expect(clipboard.readText()).toBe(JSON.stringify({ profileName: "sestest", dataSetName: "HLQ.TEST.DELETE.NODE" })); + }); + it("Should copy the label of a favourited node to the clipboard", async () => { + renameDataSet.mockReset(); + + const node = new ZoweNode("[sestest]: HLQ.TEST.DELETE.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + node.contextValue = "ds_fav"; + + await extension.copyDataSet(node); + expect(clipboard.readText()).toBe(JSON.stringify({ profileName: "sestest", dataSetName: "HLQ.TEST.DELETE.NODE" })); + }); + it("Should copy the label of a member to the clipboard", async () => { + renameDataSet.mockReset(); + + const parent = new ZoweNode("HLQ.TEST.PARENT.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + const child = new ZoweNode("child", vscode.TreeItemCollapsibleState.None, parent, null); + parent.contextValue = extension.DS_PDS_CONTEXT; + child.contextValue = extension.DS_MEMBER_CONTEXT; + await extension.copyDataSet(child); + expect(clipboard.readText()).toBe(JSON.stringify({ profileName: "sestest", dataSetName: "HLQ.TEST.PARENT.NODE", memberName: "child" })); + }); + it("Should copy the label of a favourited member to the clipboard", async () => { + renameDataSet.mockReset(); + + const parent = new ZoweNode("[sestest]: HLQ.TEST.PARENT.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + const child = new ZoweNode("child", vscode.TreeItemCollapsibleState.None, parent, null); + parent.contextValue = extension.DS_PDS_CONTEXT + extension.FAV_SUFFIX; + child.contextValue = extension.DS_MEMBER_CONTEXT; + await extension.copyDataSet(child); + expect(clipboard.readText()).toBe(JSON.stringify({ profileName: "sestest", dataSetName: "HLQ.TEST.PARENT.NODE", memberName: "child" })); + }); + }); + describe("Pasting Data Sets", () => { + it("Should call zowe.Copy.dataSet when pasting to sequential data set", async () => { + const node = new ZoweNode("HLQ.TEST.TO.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + node.contextValue = extension.DS_SESSION_CONTEXT; + + clipboard.writeText(JSON.stringify({ dataSetName: "HLQ.TEST.BEFORE.NODE", profileName: "sestest" })); + await extension.pasteDataSet(node, testTree); + + expect(copyDataSet.mock.calls.length).toBe(1); + expect(copyDataSet).toHaveBeenLastCalledWith( + node.getSession(), + { dataSetName: "HLQ.TEST.BEFORE.NODE" }, + { dataSetName: "HLQ.TEST.TO.NODE" }, + ); + }); + it("Should throw an error if invalid clipboard data is supplied when pasting to sequential data set", async () => { + let error; + const node = new ZoweNode("HLQ.TEST.TO.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + node.contextValue = extension.DS_SESSION_CONTEXT; + clipboard.writeText("INVALID"); + try { + await extension.pasteDataSet(node, testTree); + } catch(err) { + error = err; + } + + expect(error).toBeTruthy(); + expect(error.message).toContain("Invalid clipboard. Copy from data set first"); + expect(copyDataSet.mock.calls.length).toBe(0); + }); + it("Should not call zowe.Copy.dataSet when pasting to partitioned data set with no member name", async () => { + dataSetGet.mockImplementation(() => { + throw Error("Member not found"); + }); + const node = new ZoweNode("HLQ.TEST.TO.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + node.contextValue = extension.DS_PDS_CONTEXT; + + clipboard.writeText(JSON.stringify({ dataSetName: "HLQ.TEST.BEFORE.NODE", profileName: "sestest" })); + await extension.pasteDataSet(node, testTree); + + expect(copyDataSet.mock.calls.length).toBe(0); + }); + it("Should call zowe.Copy.dataSet when pasting to partitioned data set", async () => { + dataSetGet.mockImplementation(() => { + throw Error("Member not found"); + }); + const node = new ZoweNode("HLQ.TEST.TO.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + node.contextValue = extension.DS_PDS_CONTEXT; + showInputBox.mockResolvedValueOnce("mem1"); + + clipboard.writeText(JSON.stringify({ dataSetName: "HLQ.TEST.BEFORE.NODE", profileName: "sestest" })); + await extension.pasteDataSet(node, testTree); + + expect(copyDataSet.mock.calls.length).toBe(1); + expect(findFavoritedNode).toHaveBeenLastCalledWith( + node, + ); + expect(copyDataSet).toHaveBeenLastCalledWith( + node.getSession(), + { dataSetName: "HLQ.TEST.BEFORE.NODE" }, + { dataSetName: "HLQ.TEST.TO.NODE", memberName: "mem1" }, + ); + }); + it("Should throw an error when pasting to a member that already exists", async () => { + let error; + dataSetGet.mockImplementation(() => "DATA"); + const node = new ZoweNode("HLQ.TEST.TO.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + node.contextValue = extension.DS_PDS_CONTEXT; + showInputBox.mockResolvedValueOnce("mem1"); + + clipboard.writeText(JSON.stringify({ dataSetName: "HLQ.TEST.BEFORE.NODE", profileName: "sestest" })); + + try { + await extension.pasteDataSet(node, testTree); + } catch(err) { + error = err; + } + + expect(error).toBeTruthy(); + expect(error.message).toBe("HLQ.TEST.TO.NODE(mem1) already exists. You cannot replace a member"); + expect(copyDataSet.mock.calls.length).toBe(0); + dataSetGet.mockReset(); + }); + it("Should call zowe.Copy.dataSet when pasting to a favorited partitioned data set", async () => { + dataSetGet.mockImplementation(() => { + throw Error("Member not found"); + }); + const favoritedNode = new ZoweNode("[sestest]: HLQ.TEST.TO.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + favoritedNode.contextValue = extension.DS_PDS_CONTEXT + extension.FAV_SUFFIX; + const nonFavoritedNode = new ZoweNode("HLQ.TEST.TO.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + findNonFavoritedNode.mockImplementation(() => nonFavoritedNode); + + showInputBox.mockResolvedValueOnce("mem1"); + clipboard.writeText(JSON.stringify({ dataSetName: "HLQ.TEST.BEFORE.NODE", profileName: "sestest" })); + await extension.pasteDataSet(favoritedNode, testTree); + + expect(copyDataSet.mock.calls.length).toBe(1); + expect(findNonFavoritedNode).toHaveBeenLastCalledWith( + favoritedNode, + ); + expect(mockRefreshElement).toHaveBeenLastCalledWith( + nonFavoritedNode, + ); + expect(copyDataSet).toHaveBeenLastCalledWith( + favoritedNode.getSession(), + { dataSetName: "HLQ.TEST.BEFORE.NODE" }, + { dataSetName: "HLQ.TEST.TO.NODE", memberName: "mem1" }, + ); + }); + }); + it("tests the issueTsoCommand function", async () => { showQuickPick.mockReset(); showInputBox.mockReset(); diff --git a/__tests__/__unit__/uss/ussNodeActions.unit.test.ts b/__tests__/__unit__/uss/ussNodeActions.unit.test.ts index bffc45b4a2..4edeee03a4 100644 --- a/__tests__/__unit__/uss/ussNodeActions.unit.test.ts +++ b/__tests__/__unit__/uss/ussNodeActions.unit.test.ts @@ -29,6 +29,7 @@ const mockUSSRefresh = jest.fn(); const mockUSSRefreshElement = jest.fn(); const mockGetUSSChildren = jest.fn(); const mockRemoveUSSFavorite = jest.fn(); +const mockAddUSSFavorite = jest.fn(); const mockInitializeFavorites = jest.fn(); const showInputBox = jest.fn(); const showErrorMessage = jest.fn(); @@ -51,17 +52,30 @@ function getUSSNode() { return ussNode1; } +function getFavoriteUSSNode() { + const ussNodeF = new ZoweUSSNode("[profile]: usstest", vscode.TreeItemCollapsibleState.Expanded, null, session, null); + const mParent = new ZoweUSSNode("Favorites", vscode.TreeItemCollapsibleState.Expanded, null, session, null); + mParent.contextValue = extension.FAVORITE_CONTEXT; + ussNodeF.contextValue = extension.DS_TEXT_FILE_CONTEXT + extension.FAV_SUFFIX; + ussNodeF.fullPath = "/u/myuser/usstest"; + ussNodeF.tooltip = "/u/myuser/usstest"; + ussNodeF.mParent = mParent; + return ussNodeF; +} + function getUSSTree() { const ussNode1= getUSSNode(); + const ussNodeFav= getFavoriteUSSNode(); const USSTree = jest.fn().mockImplementation(() => { return { mSessionNodes: [], - mFavorites: [], + mFavorites: [ussNodeFav], addSession: mockAddUSSSession, refresh: mockUSSRefresh, refreshAll: mockUSSRefresh, refreshElement: mockUSSRefreshElement, getChildren: mockGetUSSChildren, + addUSSFavorite: mockAddUSSFavorite, removeUSSFavorite: mockRemoveUSSFavorite, initializeUSSFavorites: mockInitializeFavorites }; @@ -81,6 +95,7 @@ const session = new brtimperative.Session({ }); const ussNode = getUSSNode(); +const ussFavNode = getFavoriteUSSNode(); const testUSSTree = getUSSTree(); Object.defineProperty(zowe, "Create", { value: Create }); @@ -210,7 +225,7 @@ describe("ussNodeActions", () => { showInputBox.mockReturnValueOnce("new name"); ussNode.contextValue = extension.USS_DIR_CONTEXT; await ussNodeActions.renameUSSNode(ussNode, testUSSTree, extension.DS_SESSION_CONTEXT); - expect(testUSSTree.refreshAll).toHaveBeenCalled(); + // expect(testUSSTree.refreshAll).toHaveBeenCalled(); expect(showErrorMessage.mock.calls.length).toBe(0); expect(renameUSSFile.mock.calls.length).toBe(1); }); @@ -232,6 +247,15 @@ describe("ussNodeActions", () => { } expect(showErrorMessage.mock.calls.length).toBe(1); }); + it("should execute rename favorite USS file", async () => { + showInputBox.mockReturnValueOnce("new name"); + await ussNodeActions.renameUSSNode(ussFavNode, testUSSTree, "file"); + expect(testUSSTree.refresh).toHaveBeenCalled(); + expect(showErrorMessage.mock.calls.length).toBe(0); + expect(renameUSSFile.mock.calls.length).toBe(1); + expect(mockRemoveUSSFavorite.mock.calls.length).toBe(1); + expect(mockAddUSSFavorite.mock.calls.length).toBe(1); + }); }); describe("uploadFile", () => { Object.defineProperty(zowe, "Upload", {value: Upload}); @@ -310,5 +334,12 @@ describe("ussNodeActions", () => { await ussNodeActions.copyPath(ussNode); expect(writeText).toBeCalledWith(ussNode.fullPath); }); + it("should not copy the node's full path to the system clipboard if theia", async () => { + let theia = true; + Object.defineProperty(extension, "ISTHEIA", { get: () => theia }); + await ussNodeActions.copyPath(ussNode); + expect(writeText).not.toBeCalled(); + theia = false; + }); }); }); diff --git a/i18n/sample/package.i18n.json b/i18n/sample/package.i18n.json index 1bb6161cba..b01f9d8bec 100644 --- a/i18n/sample/package.i18n.json +++ b/i18n/sample/package.i18n.json @@ -64,5 +64,13 @@ "Zowe-Default-USS-Persistent-Favorites": "Toggle if USS favorite files persist locally", "Zowe-Default-Jobs-Persistent-Favorites": "Toggle if Jobs favorite files persist locally", "Zowe-Environment": "Environment where the extension is running, default is VSCode", - "issueTsoCmd": "Zowe: Issue TSO Command..." + "issueTsoCmd": "Zowe: Issue TSO Command...", + "renameDataSet": "Rename Data Set", + "copyDataSet": "Copy Data Set", + "pasteDataSet": "Paste Data Set", + "profile.configuration.title": "Zowe z/OSMF Profiles", + "profile.configuration.name": "Profile Name", + "profile.configuration.url": "z/OSMF URL", + "profile.configuration.user": "z/OSMF User Name", + "profile.configuration.ru": "Profile Reject Unauthorize" } \ No newline at end of file diff --git a/i18n/sample/src/ProfileLoader.i18n.json b/i18n/sample/src/ProfileLoader.i18n.json index aaf74b0fbe..a565b67952 100644 --- a/i18n/sample/src/ProfileLoader.i18n.json +++ b/i18n/sample/src/ProfileLoader.i18n.json @@ -4,8 +4,9 @@ "loadAllProfiles.error.loadAll2": "Please ensure that you have created at least one profile with Zowe CLI before attempting to use this extension. Error text:", "loadDefaultProfile.error.spawnProcess": "Failed to spawn process to retrieve default profile contents!\n", "loadDefaultProfile.error.profile1": "No default zosmf profile found for Zowe CLI.", - "loadDefaultProfile.error.profile2": " A default zosmf profile created with Zowe CLI is required to use the Zowe extension.", - "loadDefaultProfile.error.profile3": " Please [create at least one profile with Zowe CLI]", - "loadDefaultProfile.error.profile4": "(https://docs.zowe.org/stable/user-guide/cli-configuringcli.html#creating-zowe-cli-profiles).", + "loadDefaultProfile.error.profile2": " A default zosmf profile is required to use the Zowe extension.", + "loadDefaultProfile.error.profile3": " Please create at least one using the plus (+) sign in the", + "loadDefaultProfile.error.profile4": " Dataset, Jobs or USS tree or create a profile using ZOWE CLI ", + "loadDefaultProfile.error.profile5": "(https://docs.zowe.org/stable/user-guide/cli-configuringcli.html#creating-zowe-cli-profiles).", "loadDefaultProfile.debug.errorText": "Error text:" } \ No newline at end of file diff --git a/i18n/sample/src/extension.i18n.json b/i18n/sample/src/extension.i18n.json index 4d0ecd8217..ab16d0c370 100644 --- a/i18n/sample/src/extension.i18n.json +++ b/i18n/sample/src/extension.i18n.json @@ -22,16 +22,12 @@ "submitMember.error.invalidNode": "submitMember() called from invalid node.", "submitMember.jobSubmitted": "Job submitted ", "submitMember.jobSubmissionFailed": "Job submission failed\n", - "addSession.noProfile": "No profiles detected", - "addSession.quickPickOption": "Select a Profile to Add to the Data Set Explorer", + "addSession.quickPickOption": "Choose \"Create new...\" to define a new profile or select an existing profile to Add to the Jobs Explorer", + "addSession.log.debug.createNewProfile": "User created a new profile", "addSession.log.debug.selectedProfile": "User selected profile ", "addSession.log.debug.cancelledSelection": "User cancelled profile selection", - "addSession.noProfilesAdd": "No more profiles to add", - "addUSSSession.noProfile": "No profiles detected", - "addUSSSession.quickPickOption": "Select a Profile to Add to the USS Explorer", "addUSSSession.log.debug.selectProfile": "User selected profile ", "addUSSSession.log.debug.cancelledSelection": "User cancelled profile selection", - "addUSSSession.noProfileAdd": "No more profiles to add", "createFile.quickPickOption.dataSetType": "Type of Data Set to be Created", "createFile.dataSetBinary": "Data Set Binary", "createFile.dataSetC": "Data Set C", @@ -51,6 +47,10 @@ "showDSAttributes.log.error": "Error encountered when listing attributes! ", "showDSAttributes.error": "Unable to list attributes: ", "attributes.title": "Attributes", + "renameDataSet.log.debug": "Renaming data set ", + "renameDataSet.log.error": "Error encountered when renaming data set! ", + "renameDataSet.error": "Unable to rename data set: ", + "renameDataSet.name": "Name of Data Set Member", "deactivate.error": "Unable to delete temporary folder. ", "deleteDataset.log.debug": "Deleting data set ", "deleteDataset.quickPickOption": "Are you sure you want to delete ", @@ -101,10 +101,6 @@ "stopCommand.response": "Command response: ", "setOwner.newOwner.prompt.owner": "Owner", "setOwner.newOwner.prompt.prefix": "Prefix", - "addJobsSession.error.load": "Unable to load all profiles: ", - "addJobsSession.noProfilesDetected": "No profiles detected", - "addJobsSession.quickPickOptions.profileAdd": "Select a Profile to Add to the Jobs Explorer", "addJobsSession.log.debug.selectedProfile": "User selected profile ", - "addJobsSession.log.debug.cancelledProfile": "User cancelled profile selection", - "addJobsSession.noProfilesAdd": "No more profiles to add" + "addJobsSession.log.debug.cancelledProfile": "User cancelled profile selection" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 989c880ae4..803de6fd1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vscode-extension-for-zowe", - "version": "0.27.0", + "version": "0.29.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -178,21 +178,29 @@ } }, "@brightside/core": { - "version": "2.32.1", - "resolved": "https://dl.bintray.com//ca/brightside/@brightside/core/-/@brightside/core-2.32.1.tgz", - "integrity": "sha1-jNMh4v3l4ELoIHFLpvqCmS9crv0=", + "version": "2.34.1", + "resolved": "https://dl.bintray.com//ca/brightside/@brightside/core/-/@brightside/core-2.34.1.tgz", + "integrity": "sha1-BvuhnNr/ZeDh1uTzU3kr5FQhEzc=", "requires": { - "@brightside/imperative": "2.7.3", + "@brightside/imperative": "2.7.4", + "get-stdin": "7.0.0", "js-yaml": "^3.13.1", "minimatch": "^3.0.4", "ssh2": "^0.8.2", "string-width": "2.1.1" + }, + "dependencies": { + "get-stdin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", + "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==" + } } }, "@brightside/imperative": { - "version": "2.7.3", - "resolved": "https://dl.bintray.com//ca/brightside/@brightside/imperative/-/@brightside/imperative-2.7.3.tgz", - "integrity": "sha1-q3vMuEl8g78q9XJ/p1BOxhACkmA=", + "version": "2.7.4", + "resolved": "https://dl.bintray.com//ca/brightside/@brightside/imperative/-/@brightside/imperative-2.7.4.tgz", + "integrity": "sha1-Cg1zFMbHQeVCUgDUtmNWzRVUMD4=", "requires": { "@types/lodash-deep": "^2.0.0", "@types/yargs": "8.0.2", @@ -670,9 +678,9 @@ "dev": true }, "@types/lodash": { - "version": "4.14.144", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.144.tgz", - "integrity": "sha512-ogI4g9W5qIQQUhXAclq6zhqgqNUr7UlFaqDHbch7WLSLeeM/7d3CRaw7GLajxvyFvhJqw4Rpcz5bhoaYtIx6Tg==" + "version": "4.14.149", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", + "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==" }, "@types/lodash-deep": { "version": "2.0.0", @@ -3799,9 +3807,9 @@ } }, "handlebars": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.2.tgz", - "integrity": "sha512-29Zxv/cynYB7mkT1rVWQnV7mGX6v7H/miQ6dbEpYTKq5eJBN7PsRB+ViYJlcT6JINTSu4dVB9kOqEun78h6Exg==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -7697,17 +7705,17 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "ssh2": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.5.tgz", - "integrity": "sha512-TkvzxSYYUSQ8jb//HbHnJVui4fVEW7yu/zwBxwro/QaK2EGYtwB+8gdEChwHHuj142c5+250poMC74aJiwApPw==", + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.6.tgz", + "integrity": "sha512-T0cPmEtmtC8WxSupicFDjx3vVUdNXO8xu2a/D5bjt8ixOUCe387AgvxU3mJgEHpu7+Sq1ZYx4d3P2pl/yxMH+w==", "requires": { - "ssh2-streams": "~0.4.4" + "ssh2-streams": "~0.4.7" } }, "ssh2-streams": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.6.tgz", - "integrity": "sha512-jXq/nk2K82HuueO9CTCdas/a0ncX3fvYzEPKt1+ftKwE5RXTX25GyjcpjBh2lwVUYbk0c9yq6cBczZssWmU3Tw==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.7.tgz", + "integrity": "sha512-JhF8BNfeguOqVHOLhXjzLlRKlUP8roAEhiT/y+NcBQCqpRUupLNrRf2M+549OPNVGx21KgKktug4P3MY/IvTig==", "requires": { "asn1": "~0.2.0", "bcrypt-pbkdf": "^1.0.2", diff --git a/package.json b/package.json index e0e50a6dba..043dc28c1b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-extension-for-zowe", "displayName": "%displayName%", "description": "%description%", - "version": "0.28.0", + "version": "0.29.0", "publisher": "Zowe", "repository": { "url": "https://github.com/zowe/vscode-extension-for-zowe" @@ -105,6 +105,18 @@ "command": "zowe.showDSAttributes", "title": "%showDSAttributes%" }, + { + "command": "zowe.renameDataSet", + "title": "%renameDataSet%" + }, + { + "command": "zowe.copyDataSet", + "title": "%copyDataSet%" + }, + { + "command": "zowe.pasteDataSet", + "title": "%pasteDataSet%" + }, { "command": "zowe.deleteDataset", "title": "%deleteDataset%" @@ -532,6 +544,11 @@ "command": "zowe.submitMember", "group": "3_systemSpecific" }, + { + "when": "view == zowe.explorer && viewItem == member", + "command": "zowe.copyDataSet", + "group": "9_cutcopypaste" + }, { "when": "view == zowe.explorer && viewItem == member_fav", "command": "zowe.refreshNode", @@ -582,6 +599,21 @@ "command": "zowe.showDSAttributes", "group": "2_information" }, + { + "when": "view == zowe.explorer && viewItem == ds", + "command": "zowe.renameDataSet", + "group": "6_modification@2" + }, + { + "when": "view == zowe.explorer && viewItem == ds", + "command": "zowe.copyDataSet", + "group": "9_cutcopypaste" + }, + { + "when": "view == zowe.explorer && viewItem == ds", + "command": "zowe.pasteDataSet", + "group": "9_cutcopypaste" + }, { "when": "view == zowe.explorer && viewItem == ds_fav", "command": "zowe.refreshNode", @@ -612,6 +644,21 @@ "command": "zowe.removeFavorite", "group": "4_workspace" }, + { + "when": "view == zowe.explorer && viewItem == ds_fav", + "command": "zowe.renameDataSet", + "group": "6_modification@2" + }, + { + "when": "view == zowe.explorer && viewItem == ds_fav", + "command": "zowe.copyDataSet", + "group": "9_cutcopypaste" + }, + { + "when": "view == zowe.explorer && viewItem == ds_fav", + "command": "zowe.pasteDataSet", + "group": "9_cutcopypaste" + }, { "when": "view == zowe.explorer && viewItem == pds", "command": "zowe.createMember", @@ -637,6 +684,16 @@ "command": "zowe.uploadDialog", "group": "1_create" }, + { + "when": "view == zowe.explorer && viewItem == pds", + "command": "zowe.renameDataSet", + "group": "6_modification@2" + }, + { + "when": "view == zowe.explorer && viewItem == pds", + "command": "zowe.pasteDataSet", + "group": "9_cutcopypaste" + }, { "when": "view == zowe.explorer && viewItem == pds_fav", "command": "zowe.createMember", @@ -662,6 +719,21 @@ "command": "zowe.removeFavorite", "group": "4_workspace" }, + { + "when": "view == zowe.explorer && viewItem == pds_fav", + "command": "zowe.renameDataSet", + "group": "6_modification@2" + }, + { + "when": "view == zowe.explorer && viewItem == pds_fav", + "command": "zowe.copyDataSet", + "group": "9_cutcopypaste" + }, + { + "when": "view == zowe.explorer && viewItem == pds_fav", + "command": "zowe.pasteDataSet", + "group": "9_cutcopypaste" + }, { "when": "view == zowe.explorer && viewItem == migr", "command": "zowe.showDSAttributes", @@ -1067,7 +1139,7 @@ "vscode-nls-dev": "^3.2.6" }, "dependencies": { - "@brightside/core": "2.32.1", + "@brightside/core": "^2.33.0", "@types/fs-extra": "^7.0.0", "fs-extra": "^8.0.1", "gulp-cli": "^2.2.0", diff --git a/package.nls.json b/package.nls.json index 51d0a6f0b5..593700c6f7 100644 --- a/package.nls.json +++ b/package.nls.json @@ -65,6 +65,9 @@ "Zowe-Default-Jobs-Persistent-Favorites": "Toggle if Jobs favorite files persist locally", "Zowe-Environment": "Environment where the extension is running, default is VSCode", "issueTsoCmd" : "Zowe: Issue TSO Command...", + "renameDataSet" : "Rename Data Set", + "copyDataSet" : "Copy Data Set", + "pasteDataSet" : "Paste Data Set", "profile.configuration.title": "Zowe z/OSMF Profiles", "profile.configuration.name": "Profile Name", "profile.configuration.url": "z/OSMF URL", diff --git a/src/DatasetTree.ts b/src/DatasetTree.ts index 4d19564681..43473b18f8 100644 --- a/src/DatasetTree.ts +++ b/src/DatasetTree.ts @@ -263,6 +263,63 @@ export class DatasetTree implements vscode.TreeDataProvider { } } + /** + * Renames a node based on the profile and it's label + * + * @param {string} profileLabel + * @param {string} beforeLabel + * @param {string} afterLabel + */ + + public async renameNode(profileLabel: string, beforeLabel: string, afterLabel: string) { + const sessionNode = this.mSessionNodes.find((session) => session.label === `${profileLabel} `); + if (sessionNode) { + const matchingNode = sessionNode.children.find((node) => node.label === beforeLabel); + if (matchingNode) { + matchingNode.label = afterLabel; + this.refreshElement(matchingNode); + } + } + } + + /** + * Renames a node from the favorites list + * + * @param {ZoweNode} node + */ + public async renameFavorite(node: ZoweNode, newLabel: string) { + const matchingNode = this.mFavorites.find( + (temp) => (temp.label === node.label) && (temp.contextValue.startsWith(node.contextValue)) + ); + if (matchingNode) { + const prefix = matchingNode.label.substring(0, matchingNode.label.indexOf(":") + 2); + matchingNode.label = prefix + newLabel; + this.refreshElement(matchingNode); + } + } + + /** + * Finds the equivalent node as a favorite + * + * @param {ZoweNode} node + */ + public findFavoritedNode(node: ZoweNode) { + return this.mFavorites.find( + (temp) => (temp.label === `[${node.mParent.label.trim()}]: ${node.label}`) && (temp.contextValue.includes(node.contextValue)) + ); + } + /** + * Finds the equivalent node not as a favorite + * + * @param {ZoweNode} node + */ + public findNonFavoritedNode(node: ZoweNode) { + const profileLabel = node.label.substring(1, node.label.indexOf("]")); + const nodeLabel = node.label.substring(node.label.indexOf(":") + 2); + const sessionNode = this.mSessionNodes.find((session) => session.label.trim() === profileLabel); + return sessionNode.children.find((temp) => temp.label === nodeLabel); + } + /** * Removes a node from the favorites list * diff --git a/src/Profiles.ts b/src/Profiles.ts index cc838b738c..6e84881d9f 100644 --- a/src/Profiles.ts +++ b/src/Profiles.ts @@ -9,21 +9,22 @@ * * */ -import { IProfileLoaded, Logger, CliProfileManager, Imperative, ImperativeConfig, IProfile } from "@brightside/imperative"; -import * as nls from "vscode-nls"; -import * as os from "os"; +import * as zowe from "@brightside/core"; +import { CliProfileManager, Imperative, ImperativeConfig, IProfile, IProfileLoaded, Logger } from "@brightside/imperative"; import * as fs from "fs"; +import * as os from "os"; import * as path from "path"; -import * as ProfileLoader from "./ProfileLoader"; import { URL } from "url"; import * as vscode from "vscode"; -import * as zowe from "@brightside/core"; -import { DatasetTree } from "./DatasetTree"; -import { addSession } from "./extension"; +import * as nls from "vscode-nls"; +import * as ProfileLoader from "./ProfileLoader"; const localize = nls.config({ messageFormat: nls.MessageFormat.file })(); -let url: URL; -let validURL: string; -let validPort: number; +interface IUrlValidator { + valid: boolean; + host: string; + port: number; +} + let IConnection: { name: string; host: string; @@ -48,6 +49,7 @@ export class Profiles { // Processing stops if there are no profiles detected public defaultProfile: IProfileLoaded; private spawnValue: number = -1; + private initValue: number = -1; private constructor(public log: Logger) {} public loadNamedProfile(name: string): IProfileLoaded { @@ -66,11 +68,11 @@ export class Profiles { // Processing stops if there are no profiles detected if (this.isSpawnReqd() === 0) { this.allProfiles = ProfileLoader.loadAllProfiles(); try { - this.defaultProfile = ProfileLoader.loadDefaultProfile(this.log); + this.defaultProfile = ProfileLoader.loadDefaultProfile(this.log); } catch (err) { // Unable to load a default profile this.log.warn(localize("loadNamedProfile.warn.noDefaultProfile", - "Unable to locate a default profile. CLI may not be installed. ")+err.message); + "Unable to locate a default profile. CLI may not be installed. ") + err.message); } } else { const profileManager = new CliProfileManager({ @@ -80,7 +82,7 @@ export class Profiles { // Processing stops if there are no profiles detected this.allProfiles = (await profileManager.loadAll()).filter((profile) => { return profile.type === "zosmf"; }); - if (this.allProfiles.length > 0 ) { + if (this.allProfiles.length > 0) { this.defaultProfile = (await profileManager.load({ loadDefault: true })); } else { ProfileLoader.loadDefaultProfile(this.log); @@ -104,15 +106,40 @@ export class Profiles { // Processing stops if there are no profiles detected return this.allProfiles; } - public validateUrl = (newUrl: string) => { + public validateAndParseUrl = (newUrl: string): IUrlValidator => { + + let url: URL; + const validProtocols: string[] = ["https"]; + const DEFAULT_HTTPS_PORT: number = 443; + + const validationResult: IUrlValidator = { + valid: false, + host: null, + port: null + }; + try { url = new URL(newUrl); } catch (error) { - return false; + return validationResult; + } + + // overkill with only one valid protocol, but we may expand profile types and protocols in the future? + if (!validProtocols.some((validProtocol: string) => url.protocol.includes(validProtocol))) { + return validationResult; } - validURL = url.hostname; - validPort = Number(url.port); - return url.port ? true : false; + + // if port is empty, set https defaults + if (!url.port.trim()) { + validationResult.port = DEFAULT_HTTPS_PORT; + } + else { + validationResult.port = Number(url.port); + } + + validationResult.host = url.hostname; + validationResult.valid = true; + return validationResult; } public async createNewConnection() { @@ -131,25 +158,27 @@ export class Profiles { // Processing stops if there are no profiles detected profileName = await vscode.window.showInputBox(options); if (!profileName) { vscode.window.showInformationMessage(localize("createNewConnection.enterprofileName", - "Profile Name was not supplied. Operation Cancelled")); + "Profile Name was not supplied. Operation Cancelled")); return; } zosmfURL = await vscode.window.showInputBox({ ignoreFocusOut: true, - placeHolder: localize("createNewConnection.option.prompt.url.placeholder", "http(s)://url:port"), + placeHolder: localize("createNewConnection.option.prompt.url.placeholder", "https://url:port"), prompt: localize("createNewConnection.option.prompt.url", - "Enter a z/OSMF URL in the format 'http(s)://url:port'."), - validateInput: (text: string) => (this.validateUrl(text) ? "" : "Please enter a valid URL."), + "Enter a z/OSMF URL in the format 'https://url:port'."), + validateInput: (text: string) => (this.validateAndParseUrl(text).valid ? "" : "Please enter a valid URL."), value: zosmfURL }); if (!zosmfURL) { vscode.window.showInformationMessage(localize("createNewConnection.enterzosmfURL", - "No valid value for z/OSMF URL. Operation Cancelled")); + "No valid value for z/OSMF URL. Operation Cancelled")); return; } + const zosmfUrlParsed = this.validateAndParseUrl(zosmfURL); + options = { placeHolder: localize("createNewConnection.option.prompt.userName.placeholder", "User Name"), prompt: localize("createNewConnection.option.prompt.userName", "Enter the user name for the connection"), @@ -159,7 +188,7 @@ export class Profiles { // Processing stops if there are no profiles detected if (!userName) { vscode.window.showInformationMessage(localize("createNewConnection.enterzosmfURL", - "Please enter your z/OS username. Operation Cancelled")); + "Please enter your z/OS username. Operation Cancelled")); return; } @@ -173,7 +202,7 @@ export class Profiles { // Processing stops if there are no profiles detected if (!passWord) { vscode.window.showInformationMessage(localize("createNewConnection.enterzosmfURL", - "Please enter your z/OS password. Operation Cancelled")); + "Please enter your z/OS password. Operation Cancelled")); return; } @@ -183,8 +212,8 @@ export class Profiles { // Processing stops if there are no profiles detected canPickMany: false }; - const selectRU = [ "True - Reject connections with self-signed certificates", - "False - Accept connections with self-signed certificates" ]; + const selectRU = ["True - Reject connections with self-signed certificates", + "False - Accept connections with self-signed certificates"]; const ruOptions = Array.from(selectRU); @@ -196,22 +225,22 @@ export class Profiles { // Processing stops if there are no profiles detected rejectUnauthorize = false; } else { vscode.window.showInformationMessage(localize("createNewConnection.rejectUnauthorize", - "Operation Cancelled")); + "Operation Cancelled")); return; } for (const profile of this.allProfiles) { if (profile.name === profileName) { vscode.window.showErrorMessage(localize("createNewConnection.duplicateProfileName", - "Profile name already exists. Please create a profile using a different name")); + "Profile name already exists. Please create a profile using a different name")); return; } } IConnection = { name: profileName, - host: validURL, - port: validPort, + host: zosmfUrlParsed.host, + port: zosmfUrlParsed.port, user: userName, password: passWord, rejectUnauthorized: rejectUnauthorize, @@ -242,18 +271,21 @@ export class Profiles { // Processing stops if there are no profiles detected } catch (error) { vscode.window.showErrorMessage(error.message); } - try { + if (this.initValue === -1) { + try { // we need to call Imperative.init so that any installed credential manager plugins are loaded - await Imperative.init({ configurationModule: require.resolve("@brightside/core/lib/imperative.js") }); - } catch (error) { - vscode.window.showErrorMessage(error.message); + await Imperative.init({ configurationModule: require.resolve("@brightside/core/lib/imperative.js") }); + } catch (error) { + vscode.window.showErrorMessage(error.message); + } + this.initValue = 0; } let zosmfProfile: IProfile; try { zosmfProfile = await new CliProfileManager({ profileRootDirectory: path.join(ImperativeConfig.instance.cliHome, "profiles"), type: "zosmf" - }).save({profile: ProfileInfo, name: ProfileName, type: ProfileType}); + }).save({ profile: ProfileInfo, name: ProfileName, type: ProfileType }); } catch (error) { vscode.window.showErrorMessage(error.message); } diff --git a/src/USSTree.ts b/src/USSTree.ts index d862c8707c..7883efcd01 100644 --- a/src/USSTree.ts +++ b/src/USSTree.ts @@ -389,17 +389,17 @@ export class USSTree implements vscode.TreeDataProvider { node.contextValue += extension.FAV_SUFFIX; node.iconPath = applyIcons(node); this.mFavorites.push(node); - } catch(e) { - vscode.window.showErrorMessage( - localize("initializeUSSFavorites.error.profile1", - "Error: You have Zowe USS favorites that refer to a non-existent CLI profile named: ") + profileName + - localize("intializeUSSFavorites.error.profile2", - ". To resolve this, you can create a profile with this name, ") + - localize("initializeUSSFavorites.error.profile3", - "or remove the favorites with this profile name from the Zowe-USS-Persistent setting, ") + - localize("initializeUSSFavorites.error.profile4", "which can be found in your VS Code user settings.")); - return; - } + } catch(e) { + vscode.window.showErrorMessage( + localize("initializeUSSFavorites.error.profile1", + "Error: You have Zowe USS favorites that refer to a non-existent CLI profile named: ") + profileName + + localize("intializeUSSFavorites.error.profile2", + ". To resolve this, you can create a profile with this name, ") + + localize("initializeUSSFavorites.error.profile3", + "or remove the favorites with this profile name from the Zowe-USS-Persistent setting, ") + + localize("initializeUSSFavorites.error.profile4", "which can be found in your VS Code user settings.")); + return; + } }); } diff --git a/src/ZoweUSSNode.ts b/src/ZoweUSSNode.ts index 63fdf24b9d..5ab69d4712 100644 --- a/src/ZoweUSSNode.ts +++ b/src/ZoweUSSNode.ts @@ -208,6 +208,14 @@ export class ZoweUSSNode extends vscode.TreeItem { this.dirty = true; } + public rename(newFullPath: string) { + const oldReference = this.shortLabel; + this.fullPath = newFullPath; + this.shortLabel = newFullPath.substring(newFullPath.lastIndexOf("/")); + this.label = this.label.replace(oldReference, this.shortLabel); + this.tooltip = this.tooltip.replace(oldReference, this.shortLabel); + } + /** * Returns the [etag] for this node * @@ -224,5 +232,10 @@ export class ZoweUSSNode extends vscode.TreeItem { */ public setEtag(etagValue): void { this.etag = etagValue; + /** + * helper method to change the node names in one go + * @param oldReference string + * @param revision string + */ } } diff --git a/src/extension.ts b/src/extension.ts index fc9453d33c..f9819b384d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -164,6 +164,9 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("zowe.submitJcl", async () => submitJcl(datasetProvider)); vscode.commands.registerCommand("zowe.submitMember", async (node) => submitMember(node)); vscode.commands.registerCommand("zowe.showDSAttributes", (node) => showDSAttributes(node, datasetProvider)); + vscode.commands.registerCommand("zowe.renameDataSet", (node) => renameDataSet(node, datasetProvider)); + vscode.commands.registerCommand("zowe.copyDataSet", (node) => copyDataSet(node)); + vscode.commands.registerCommand("zowe.pasteDataSet", (node) => pasteDataSet(node, datasetProvider)); vscode.workspace.onDidChangeConfiguration(async (e) => { datasetProvider.onDidChangeConfiguration(e); }); @@ -827,6 +830,146 @@ export async function showDSAttributes(parent: ZoweNode, datasetProvider: Datase } +/** + * Rename data sets + * + * @export + * @param {ZoweNode} node - The node + * @param {DatasetTree} datasetProvider - the tree which contains the nodes + */ +export async function renameDataSet(node: ZoweNode, datasetProvider: DatasetTree) { + let beforeDataSetName = node.label.trim(); + let favPrefix; + let isFavourite; + + if (node.contextValue.includes(FAV_SUFFIX)) { + isFavourite = true; + favPrefix = node.label.substring(0, node.label.indexOf(":") + 2); + beforeDataSetName = node.label.substring(node.label.indexOf(":") + 2); + } + const afterDataSetName = await vscode.window.showInputBox({ value: beforeDataSetName }); + + log.debug(localize("renameDataSet.log.debug", "Renaming data set ") + afterDataSetName); + if (afterDataSetName) { + try { + await zowe.Rename.dataSet(node.getSession(), beforeDataSetName, afterDataSetName); + node.label = `${favPrefix}${afterDataSetName}`; + } catch (err) { + log.error(localize("renameDataSet.log.error", "Error encountered when renaming data set! ") + JSON.stringify(err)); + vscode.window.showErrorMessage(localize("renameDataSet.error", "Unable to rename data set: ") + err.message); + throw err; + } + if (isFavourite) { + const profile = favPrefix.substring(1, favPrefix.indexOf("]")); + datasetProvider.renameNode(profile, beforeDataSetName, afterDataSetName); + } else { + const temp = node.label; + node.label = "[" + node.getSessionNode().label.trim() + "]: " + beforeDataSetName; + datasetProvider.renameFavorite(node, afterDataSetName); + node.label = temp; + } + datasetProvider.refreshElement(node.mParent); + datasetProvider.updateFavorites(); + } +} + +function getProfileAndDataSetName(node: ZoweNode) { + let profileName; + let dataSetName; + if (node.contextValue.includes(FAV_SUFFIX)) { + profileName = node.label.substring(1, node.label.indexOf("]")); + dataSetName = node.label.substring(node.label.indexOf(":") + 2); + } else { + profileName = node.mParent.label.trim(); + dataSetName = node.label.trim(); + } + + return { profileName, dataSetName }; +} + +function getNodeLabels(node: ZoweNode) { + if (node.contextValue.includes(DS_MEMBER_CONTEXT)) { + return { ...getProfileAndDataSetName(node.mParent), memberName: node.label.trim() }; + } else { + return getProfileAndDataSetName(node); + } +} + +/** + * Copy data sets + * + * @export + * @param {ZoweNode} node - The node to copy + */ +export async function copyDataSet(node: ZoweNode) { + return vscode.env.clipboard.writeText(JSON.stringify(getNodeLabels(node))); +} + +/** + * Paste data sets + * + * @export + * @param {ZoweNode} node - The node to paste to + * @param {DatasetTree} datasetProvider - the tree which contains the nodes + */ +export async function pasteDataSet(node: ZoweNode, datasetProvider: DatasetTree) { + const { profileName, dataSetName } = getNodeLabels(node); + let memberName; + let beforeDataSetName; + let beforeProfileName; + let beforeMemberName; + + if (node.contextValue.includes(DS_PDS_CONTEXT)) { + memberName = await vscode.window.showInputBox({ placeHolder: localize("renameDataSet.name", "Name of Data Set Member") }); + if(!memberName) { + return; + } + } + + try { + ({ + dataSetName: beforeDataSetName, + memberName: beforeMemberName, + profileName: beforeProfileName, + } = JSON.parse(await vscode.env.clipboard.readText())); + } catch (err) { + throw Error("Invalid clipboard. Copy from data set first"); + } + + if(beforeProfileName === profileName) { + if(memberName) { + try { + await zowe.Get.dataSet(node.getSession(), `${dataSetName}(${memberName})`); + throw Error(`${dataSetName}(${memberName}) already exists. You cannot replace a member`); + } catch(err) { + if (!err.message.includes("Member not found")) { + throw err; + } + } + } + await zowe.Copy.dataSet( + node.getSession(), + { dataSetName: beforeDataSetName, memberName: beforeMemberName }, + { dataSetName, memberName }, + ); + + if (memberName) { + datasetProvider.refreshElement(node); + let node2; + if (node.contextValue.includes(FAV_SUFFIX)) { + node2 = datasetProvider.findNonFavoritedNode(node); + } else { + node2 = datasetProvider.findFavoritedNode(node); + } + if (node2) { + datasetProvider.refreshElement(node2); + } + } else { + refreshPS(node); + } + } +} + /** * Recursively deletes directory * diff --git a/src/uss/ussNodeActions.ts b/src/uss/ussNodeActions.ts index 01d58861f6..4e2dd2fd6a 100644 --- a/src/uss/ussNodeActions.ts +++ b/src/uss/ussNodeActions.ts @@ -106,23 +106,39 @@ export async function refreshAllUSS(ussFileProvider: USSTree) { return Profiles.getInstance().refresh(); } -export async function renameUSSNode(node: ZoweUSSNode, ussFileProvider: USSTree, filePath: string) { - const newName = await vscode.window.showInputBox({value: node.label}); - if (!newName) { - return; - } - try { - const newNamePath = node.mParent.fullPath + "/" + newName; - await zowe.Utilities.renameUSSFile(node.getSession(), node.fullPath, newNamePath); - if (node.contextValue === extension.USS_DIR_CONTEXT || node.mParent.contextValue === extension.USS_SESSION_CONTEXT) { - refreshAllUSS(ussFileProvider); - } else { +/** + * Process for renaming a USS Node. This could be a Favorite Node + * + * @param {ZoweUSSNode} originalNode + * @param {USSTree} ussFileProvider + * @param {string} filePath + */ +export async function renameUSSNode(originalNode: ZoweUSSNode, ussFileProvider: USSTree, filePath: string) { + // Could be a favorite or regular entry always deal with the regular entry + const isFav = originalNode.contextValue.endsWith(extension.FAV_SUFFIX); + const oldLabel = isFav ? originalNode.shortLabel : originalNode.label; + const parentPath = originalNode.fullPath.substr(0, originalNode.fullPath.indexOf(oldLabel)); + // Check if an old favorite exists for this node + const oldFavorite = isFav ? originalNode : ussFileProvider.mFavorites.find((temp: ZoweUSSNode) => + (temp.shortLabel === oldLabel) && (temp.fullPath.substr(0, temp.fullPath.indexOf(oldLabel)) === parentPath) + ); + const newName = await vscode.window.showInputBox({value: oldLabel}); + if (newName && newName !== oldLabel) { + try { + const newNamePath = path.join(parentPath + newName); + await zowe.Utilities.renameUSSFile(originalNode.getSession(), originalNode.fullPath, newNamePath); ussFileProvider.refresh(); + if (oldFavorite) { + ussFileProvider.removeUSSFavorite(oldFavorite); + oldFavorite.rename(newNamePath); + ussFileProvider.addUSSFavorite(oldFavorite); + } + } catch (err) { + vscode.window.showErrorMessage(localize("renameUSSNode.error", "Unable to rename node: ") + err.message); + throw (err); } - } catch (err) { - vscode.window.showErrorMessage(localize("renameUSSNode.error", "Unable to rename node: ") + err.message); - throw (err); } + } /** From 1fdbdf71adfb199976170e4a62b9eb07534b3e45 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Tue, 26 Nov 2019 18:39:26 +0100 Subject: [PATCH 03/26] WIP - fixing tests Signed-off-by: Alexandru-Paul Dumitru --- .../__snapshots__/extension.unit.test.ts.snap | 52 ++++--------------- __tests__/__unit__/extension.unit.test.ts | 14 +++-- 2 files changed, 22 insertions(+), 44 deletions(-) diff --git a/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap b/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap index 094fcb9889..21bff14a14 100644 --- a/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap +++ b/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap @@ -37,21 +37,11 @@ Array [ "group": "inline", "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", }, - Object { - "command": "zowe.uss.safeSaveUSS", - "group": "inline", - "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", - }, Object { "command": "zowe.uss.refreshUSS", "group": "0_mainframeInteraction", "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", }, - Object { - "command": "zowe.uss.safeSaveUSS", - "group": "0_mainframeInteraction", - "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", - }, Object { "command": "zowe.uss.deleteNode", "group": "6_modification@2", @@ -132,21 +122,11 @@ Array [ "group": "inline", "when": "view == zowe.explorer && viewItem == member", }, - Object { - "command": "zowe.safeSave", - "group": "inline", - "when": "view == zowe.explorer && viewItem == member", - }, Object { "command": "zowe.refreshNode", "group": "0_mainframeInteraction", "when": "view == zowe.explorer && viewItem == member", }, - Object { - "command": "zowe.safeSave", - "group": "0_mainframeInteraction", - "when": "view == zowe.explorer && viewItem == member", - }, Object { "command": "zowe.deleteMember", "group": "6_modification@2", @@ -173,7 +153,7 @@ Array [ "when": "view == zowe.explorer && viewItem == member_fav", }, Object { - "command": "zowe.safeSave", + "command": "zowe.refreshNode", "group": "inline", "when": "view == zowe.explorer && viewItem == member_fav", }, @@ -182,11 +162,6 @@ Array [ "group": "0_mainframeInteraction", "when": "view == zowe.explorer && viewItem == member_fav", }, - Object { - "command": "zowe.safeSave", - "group": "0_mainframeInteraction", - "when": "view == zowe.explorer && viewItem == member_fav", - }, Object { "command": "zowe.deleteMember", "group": "6_modification@2", @@ -202,21 +177,11 @@ Array [ "group": "inline", "when": "view == zowe.explorer && viewItem == ds", }, - Object { - "command": "zowe.safeSave", - "group": "inline", - "when": "view == zowe.explorer && viewItem == ds", - }, Object { "command": "zowe.refreshNode", "group": "0_mainframeInteraction", "when": "view == zowe.explorer && viewItem == ds", }, - Object { - "command": "zowe.safeSave", - "group": "0_mainframeInteraction", - "when": "view == zowe.explorer && viewItem == ds", - }, Object { "command": "zowe.deleteDataset", "group": "6_modification@2", @@ -258,17 +223,22 @@ Array [ "when": "view == zowe.explorer && viewItem == ds_fav", }, Object { - "command": "zowe.safeSave", - "group": "inline", - "when": "view == zowe.explorer && viewItem == ds_fav", + "command": "zowe.copyDataSet", + "group": "9_cutcopypaste", + "when": "view == zowe.explorer && viewItem == ds", + }, + Object { + "command": "zowe.pasteDataSet", + "group": "9_cutcopypaste", + "when": "view == zowe.explorer && viewItem == ds", }, Object { "command": "zowe.refreshNode", - "group": "0_mainframeInteraction", + "group": "inline", "when": "view == zowe.explorer && viewItem == ds_fav", }, Object { - "command": "zowe.safeSave", + "command": "zowe.refreshNode", "group": "0_mainframeInteraction", "when": "view == zowe.explorer && viewItem == ds_fav", }, diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index 67c70c2f25..a3e118fef6 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -201,6 +201,8 @@ describe("Extension Unit Tests", () => { const copyDataSet = jest.fn(); const findFavoritedNode = jest.fn(); const findNonFavoritedNode = jest.fn(); + const mockSetEtag = jest.fn(); + const mockGetEtag = jest.fn(); let mockClipboardData: string; const clipboard = { writeText: jest.fn().mockImplementation((value) => mockClipboardData = value), @@ -230,6 +232,8 @@ describe("Extension Unit Tests", () => { renameFavorite: mockRenameFavorite, updateFavorites: mockUpdateFavorites, renameNode: mockRenameNode, + getEtag: mockGetEtag, + setEtag: mockSetEtag, findFavoritedNode, findNonFavoritedNode, }; @@ -243,7 +247,9 @@ describe("Extension Unit Tests", () => { refreshElement: mockUSSRefreshElement, getChildren: mockGetUSSChildren, initializeUSSFavorites: mockInitializeUSS, - ussFilterPrompt: ussPattern + ussFilterPrompt: ussPattern, + getEtag: mockGetEtag, + setEtag: mockSetEtag, }; }); const JobsTree = jest.fn().mockImplementation(() => { @@ -522,7 +528,7 @@ describe("Extension Unit Tests", () => { expect(createTreeView.mock.calls[0][0]).toBe("zowe.explorer"); expect(createTreeView.mock.calls[1][0]).toBe("zowe.uss.explorer"); // tslint:disable-next-line: no-magic-numbers - expect(registerCommand.mock.calls.length).toBe(64); + expect(registerCommand.mock.calls.length).toBe(62); registerCommand.mock.calls.forEach((call, i ) => { expect(registerCommand.mock.calls[i][1]).toBeInstanceOf(Function); }); @@ -740,6 +746,7 @@ describe("Extension Unit Tests", () => { openTextDocument.mockResolvedValueOnce({isDirty: true}); dataSet.mockReset(); showTextDocument.mockReset(); + await extension.refreshPS(node); @@ -1454,7 +1461,8 @@ describe("Extension Unit Tests", () => { expect(ussFile.mock.calls[0][0]).toBe(node.getSession()); expect(ussFile.mock.calls[0][1]).toBe(node.fullPath); expect(ussFile.mock.calls[0][2]).toEqual({ - file: extension.getUSSDocumentFilePath(node) + file: extension.getUSSDocumentFilePath(node), + returnEtag: true, }); expect(openTextDocument.mock.calls.length).toBe(1); expect(openTextDocument.mock.calls[0][0]).toBe(path.join(extension.getUSSDocumentFilePath(node))); From 5a3ce276226f387bc73127b3d00fbb0349326d80 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Wed, 27 Nov 2019 02:36:14 +0100 Subject: [PATCH 04/26] WIP2 - 2 more tests to fix Signed-off-by: Alexandru-Paul Dumitru --- .../ZoweUSSNode.unit.test.ts.snap | 3 + __tests__/__unit__/extension.unit.test.ts | 65 ++++++++++++++----- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/__tests__/__unit__/__snapshots__/ZoweUSSNode.unit.test.ts.snap b/__tests__/__unit__/__snapshots__/ZoweUSSNode.unit.test.ts.snap index 7579cad107..06f30977ed 100644 --- a/__tests__/__unit__/__snapshots__/ZoweUSSNode.unit.test.ts.snap +++ b/__tests__/__unit__/__snapshots__/ZoweUSSNode.unit.test.ts.snap @@ -8,6 +8,7 @@ ZoweUSSNode { "collapsibleState": 0, "contextValue": "textFile", "dirty": false, + "etag": "", "fullPath": "", "iconPath": "Ref: 'document.svg'", "label": "testFile", @@ -18,6 +19,7 @@ ZoweUSSNode { "collapsibleState": 1, "contextValue": "directory", "dirty": false, + "etag": "", "fullPath": "", "iconPath": "Ref: 'folder.svg'", "label": "testDir", @@ -28,6 +30,7 @@ ZoweUSSNode { "collapsibleState": 1, "contextValue": "uss_session", "dirty": true, + "etag": "", "fullPath": "", "iconPath": "Ref: 'folder.svg'", "label": "root", diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index a3e118fef6..dd1044731f 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -24,6 +24,7 @@ import * as profileLoader from "../../src/Profiles"; import * as ussNodeActions from "../../src/uss/ussNodeActions"; import { Job } from "../../src/ZoweJobNode"; import * as utils from "../../src/utils"; +import { IZosFilesResponse } from "@brightside/core"; jest.mock("vscode"); jest.mock("Session"); @@ -201,8 +202,6 @@ describe("Extension Unit Tests", () => { const copyDataSet = jest.fn(); const findFavoritedNode = jest.fn(); const findNonFavoritedNode = jest.fn(); - const mockSetEtag = jest.fn(); - const mockGetEtag = jest.fn(); let mockClipboardData: string; const clipboard = { writeText: jest.fn().mockImplementation((value) => mockClipboardData = value), @@ -232,8 +231,6 @@ describe("Extension Unit Tests", () => { renameFavorite: mockRenameFavorite, updateFavorites: mockUpdateFavorites, renameNode: mockRenameNode, - getEtag: mockGetEtag, - setEtag: mockSetEtag, findFavoritedNode, findNonFavoritedNode, }; @@ -248,8 +245,6 @@ describe("Extension Unit Tests", () => { getChildren: mockGetUSSChildren, initializeUSSFavorites: mockInitializeUSS, ussFilterPrompt: ussPattern, - getEtag: mockGetEtag, - setEtag: mockSetEtag, }; }); const JobsTree = jest.fn().mockImplementation(() => { @@ -747,7 +742,14 @@ describe("Extension Unit Tests", () => { dataSet.mockReset(); showTextDocument.mockReset(); - + const response: IZosFilesResponse = { + success: true, + commandResponse: null, + apiResponse: { + etag: "123" + } + }; + dataSet.mockReturnValueOnce(response); await extension.refreshPS(node); expect(dataSet.mock.calls.length).toBe(1); @@ -796,6 +798,7 @@ describe("Extension Unit Tests", () => { openTextDocument.mockResolvedValueOnce({isDirty: true}); dataSet.mockReset(); showTextDocument.mockReset(); + dataSet.mockReturnValueOnce(response); node.contextValue = extension.DS_PDS_CONTEXT + extension.FAV_SUFFIX; await extension.refreshPS(node); @@ -804,6 +807,7 @@ describe("Extension Unit Tests", () => { dataSet.mockReset(); openTextDocument.mockReset(); + dataSet.mockReturnValueOnce(response); parent.contextValue = extension.DS_PDS_CONTEXT + extension.FAV_SUFFIX; await extension.refreshPS(child); @@ -812,6 +816,7 @@ describe("Extension Unit Tests", () => { dataSet.mockReset(); openTextDocument.mockReset(); + dataSet.mockReturnValueOnce(response); parent.contextValue = extension.FAVORITE_CONTEXT; await extension.refreshPS(child); @@ -1365,6 +1370,14 @@ describe("Extension Unit Tests", () => { const child = new ZoweNode("child", vscode.TreeItemCollapsibleState.None, parent, null); existsSync.mockReturnValue(null); + const response: IZosFilesResponse = { + success: true, + commandResponse: null, + apiResponse: { + etag: "123" + } + }; + withProgress.mockReturnValue(response); openTextDocument.mockResolvedValueOnce("test doc"); await extension.openPS(node, true); @@ -1454,7 +1467,14 @@ describe("Extension Unit Tests", () => { ussFile.mockReset(); showTextDocument.mockReset(); executeCommand.mockReset(); - + const response: IZosFilesResponse = { + success: true, + commandResponse: null, + apiResponse: { + etag: "132" + } + } + ussFile.mockReturnValueOnce(response); await extension.refreshUSS(node); expect(ussFile.mock.calls.length).toBe(1); @@ -1605,18 +1625,18 @@ describe("Extension Unit Tests", () => { const parent = new ZoweUSSNode("parent", vscode.TreeItemCollapsibleState.Collapsed, ussNode, null, "/"); const child = new ZoweUSSNode("child", vscode.TreeItemCollapsibleState.None, parent, null, "/parent"); - downloadUSSFile.mockReturnValueOnce({ + isFileTagBinOrAscii.mockReturnValue(false); + existsSync.mockReturnValue(null); + openTextDocument.mockResolvedValueOnce("test.doc"); + + const response: IZosFilesResponse = { success: true, - commandResponse: "", + commandResponse: null, apiResponse: { - data: "", etag: "123" } - }); - - isFileTagBinOrAscii.mockReturnValue(false); - existsSync.mockReturnValue(null); - openTextDocument.mockResolvedValueOnce("test.doc"); + }; + withProgress.mockReturnValue(response); await extension.openUSS(node, false, true); @@ -1691,7 +1711,7 @@ describe("Extension Unit Tests", () => { expect(showErrorMessage.mock.calls[1][0]).toBe("open() called from invalid node."); }); - it("Tests that openUSS executes successfully with favorited files", async () => { + it("Tests that openUSS executes successfully with favored files", async () => { ussFile.mockReset(); openTextDocument.mockReset(); showTextDocument.mockReset(); @@ -1707,7 +1727,7 @@ describe("Extension Unit Tests", () => { favoriteFile.contextValue = extension.DS_TEXT_FILE_CONTEXT + extension.FAV_SUFFIX; const favoriteParent = new ZoweUSSNode("favParent", vscode.TreeItemCollapsibleState.Collapsed, favoriteSession, null, "/"); favoriteParent.contextValue = extension.USS_DIR_CONTEXT + extension.FAV_SUFFIX; - // Set up child of favoriteDir - make sure we can open the child of a favorited directory + // Set up child of favoriteDir - make sure we can open the child of a favored directory const child = new ZoweUSSNode("favChild", vscode.TreeItemCollapsibleState.Collapsed, favoriteParent, null, "/favDir"); child.contextValue = extension.DS_TEXT_FILE_CONTEXT; @@ -1737,6 +1757,15 @@ describe("Extension Unit Tests", () => { existsSync.mockReturnValue(null); openTextDocument.mockResolvedValueOnce("test.doc"); + const response: IZosFilesResponse = { + success: true, + commandResponse: null, + apiResponse: { + etag: "123" + } + }; + withProgress.mockReturnValue(response); + await extension.openUSS(node, false, true); expect(existsSync.mock.calls.length).toBe(1); From 4b32ed236e3891fbf259e5f9c4f8d618839ac64d Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Wed, 27 Nov 2019 02:46:29 +0100 Subject: [PATCH 05/26] WIP - fix linting Signed-off-by: Alexandru-Paul Dumitru --- src/ZoweNode.ts | 6 +++--- src/extension.ts | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ZoweNode.ts b/src/ZoweNode.ts index 538d061a2c..34a695efd6 100644 --- a/src/ZoweNode.ts +++ b/src/ZoweNode.ts @@ -38,10 +38,10 @@ export class ZoweNode extends vscode.TreeItem { * @param {ZoweNode} mParent * @param {Session} session */ - constructor(label: string, + constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, - public mParent: ZoweNode, - private session: Session, + public mParent: ZoweNode, + private session: Session, contextOverride?: string, private etag?: string) { super(label, collapsibleState); diff --git a/src/extension.ts b/src/extension.ts index f9819b384d..2be02b59af 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1475,7 +1475,7 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase const uploadOptions: IUploadOptions = { etag: uploadEtag, returnEtag: true - } + }; try { const uploadResponse = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, @@ -1493,17 +1493,17 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase returnEtag: true}); // re-assign etag, so that it can be used with subsequent requests const downloadEtag = downloadResponse.apiResponse.etag; - if (downloadEtag != uploadEtag) { + if (downloadEtag !== uploadEtag) { node.setEtag(downloadEtag); } - vscode.window.showWarningMessage(localize("saveFile.error.etagMismatch","Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict.")) + vscode.window.showWarningMessage(localize("saveFile.error.etagMismatch","Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict.")); // Store document in a separate variable, to be used on merge conflict const oldDoc = doc; const oldDocText = oldDoc.getText(); const startPosition = new vscode.Position(0,0); const endPosition = new vscode.Position(oldDoc.lineCount,0); const deleteRange = new vscode.Range(startPosition, endPosition); - await vscode.window.activeTextEditor.edit(editBuilder => { + await vscode.window.activeTextEditor.edit((editBuilder) => { // re-write the old content in the editor view editBuilder.delete(deleteRange); editBuilder.insert(startPosition, oldDocText); @@ -1564,17 +1564,17 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS returnEtag: true}); // re-assign etag, so that it can be used with subsequent requests const downloadEtag = downloadResponse.apiResponse.etag; - if (downloadEtag != uploadEtag) { + if (downloadEtag !== uploadEtag) { node.setEtag(downloadEtag); } - vscode.window.showWarningMessage(localize("saveFile.error.etagMismatch","Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict.")) + vscode.window.showWarningMessage(localize("saveFile.error.etagMismatch","Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict.")); // Store document in a separate variable, to be used on merge conflict const oldDoc = doc; const oldDocText = oldDoc.getText(); const startPosition = new vscode.Position(0,0); const endPosition = new vscode.Position(oldDoc.lineCount,0); const deleteRange = new vscode.Range(startPosition, endPosition); - await vscode.window.activeTextEditor.edit(editBuilder => { + await vscode.window.activeTextEditor.edit((editBuilder) => { // re-write the old content in the editor view editBuilder.delete(deleteRange); editBuilder.insert(startPosition, oldDocText); @@ -1616,7 +1616,7 @@ export async function openUSS(node: ZoweUSSNode, download = false, previewFile: const documentFilePath = getUSSDocumentFilePath(node); if (download || !fs.existsSync(documentFilePath)) { const chooseBinary = node.binary || await zowe.Utilities.isFileTagBinOrAscii(node.getSession(), node.fullPath); - let response = await vscode.window.withProgress({ + const response = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: "Opening USS file...", }, function downloadUSSFile() { From a13c99bfe984ad1d01e3a6bc56c6001750ee85e5 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Wed, 27 Nov 2019 02:52:02 +0100 Subject: [PATCH 06/26] WIP - another lint fix Signed-off-by: Alexandru-Paul Dumitru --- __tests__/__unit__/extension.unit.test.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index dd1044731f..088d0be15f 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -24,7 +24,6 @@ import * as profileLoader from "../../src/Profiles"; import * as ussNodeActions from "../../src/uss/ussNodeActions"; import { Job } from "../../src/ZoweJobNode"; import * as utils from "../../src/utils"; -import { IZosFilesResponse } from "@brightside/core"; jest.mock("vscode"); jest.mock("Session"); @@ -741,8 +740,8 @@ describe("Extension Unit Tests", () => { openTextDocument.mockResolvedValueOnce({isDirty: true}); dataSet.mockReset(); showTextDocument.mockReset(); - - const response: IZosFilesResponse = { + + const response: brightside.IZosFilesResponse = { success: true, commandResponse: null, apiResponse: { @@ -1370,7 +1369,7 @@ describe("Extension Unit Tests", () => { const child = new ZoweNode("child", vscode.TreeItemCollapsibleState.None, parent, null); existsSync.mockReturnValue(null); - const response: IZosFilesResponse = { + const response: brightside.IZosFilesResponse = { success: true, commandResponse: null, apiResponse: { @@ -1467,13 +1466,13 @@ describe("Extension Unit Tests", () => { ussFile.mockReset(); showTextDocument.mockReset(); executeCommand.mockReset(); - const response: IZosFilesResponse = { + const response: brightside.IZosFilesResponse = { success: true, commandResponse: null, apiResponse: { etag: "132" } - } + }; ussFile.mockReturnValueOnce(response); await extension.refreshUSS(node); @@ -1620,7 +1619,7 @@ describe("Extension Unit Tests", () => { showErrorMessage.mockReset(); existsSync.mockReset(); withProgress.mockReset(); - + const node = new ZoweUSSNode("node", vscode.TreeItemCollapsibleState.None, ussNode, null, "/"); const parent = new ZoweUSSNode("parent", vscode.TreeItemCollapsibleState.Collapsed, ussNode, null, "/"); const child = new ZoweUSSNode("child", vscode.TreeItemCollapsibleState.None, parent, null, "/parent"); @@ -1629,7 +1628,7 @@ describe("Extension Unit Tests", () => { existsSync.mockReturnValue(null); openTextDocument.mockResolvedValueOnce("test.doc"); - const response: IZosFilesResponse = { + const response: brightside.IZosFilesResponse = { success: true, commandResponse: null, apiResponse: { @@ -1757,7 +1756,7 @@ describe("Extension Unit Tests", () => { existsSync.mockReturnValue(null); openTextDocument.mockResolvedValueOnce("test.doc"); - const response: IZosFilesResponse = { + const response: brightside.IZosFilesResponse = { success: true, commandResponse: null, apiResponse: { From 4c98864768f4cc0b4c50c7c0dc7f7684eacc1931 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Wed, 27 Nov 2019 02:56:37 +0100 Subject: [PATCH 07/26] WIP - update dependency on brightside/core Signed-off-by: Alexandru-Paul Dumitru --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 618f559847..c926086a35 100644 --- a/package.json +++ b/package.json @@ -1159,7 +1159,7 @@ "vscode-nls-dev": "^3.2.6" }, "dependencies": { - "@brightside/core": "^2.33.0", + "@brightside/core": "^2.36.0", "@types/fs-extra": "^7.0.0", "fs-extra": "^8.0.1", "gulp-cli": "^2.2.0", From dcc50a06d34a1982b998c86bf0472109baeaf969 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Wed, 27 Nov 2019 17:05:36 +0100 Subject: [PATCH 08/26] WIP - fixing tests Signed-off-by: Alexandru-Paul Dumitru --- __tests__/__unit__/extension.unit.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index 088d0be15f..be1256b4d4 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -101,7 +101,7 @@ describe("Extension Unit Tests", () => { sessNode.contextValue = extension.DS_SESSION_CONTEXT; sessNode.pattern = "test hlq"; - const ussNode = new ZoweUSSNode("usstest", vscode.TreeItemCollapsibleState.Expanded, null, session, null); + const ussNode = new ZoweUSSNode("usstest", vscode.TreeItemCollapsibleState.Expanded, null, session, null, null, null, "123"); ussNode.contextValue = extension.USS_SESSION_CONTEXT; ussNode.fullPath = "/u/myuser"; @@ -1263,7 +1263,6 @@ describe("Extension Unit Tests", () => { showErrorMessage.mockReset(); dataSetList.mockResolvedValueOnce(testResponse); - await extension.saveFile(testDoc, testTree); expect(dataSetList.mock.calls.length).toBe(1); @@ -1279,6 +1278,7 @@ describe("Extension Unit Tests", () => { testTree.getChildren.mockReturnValueOnce([sessNode]); dataSetList.mockResolvedValueOnce(testResponse); + dataSetList.mockResolvedValueOnce(testResponse); testResponse.success = true; pathToDataSet.mockResolvedValueOnce(testResponse); @@ -1811,8 +1811,9 @@ describe("Extension Unit Tests", () => { items: [] } }; + ussNode.mProfileName = "usstest"; testUSSTree.getChildren.mockReturnValueOnce([ - new ZoweUSSNode("testFile", vscode.TreeItemCollapsibleState.None, ussNode, null, "/"), sessNode]); + new ZoweUSSNode("testFile", vscode.TreeItemCollapsibleState.None, ussNode, session, "/")]); testResponse.apiResponse.items = ["Item1"]; fileToUSSFile.mockReset(); From a9335a034c5a9cefe9a5301c5ad2b091a2d9dfa1 Mon Sep 17 00:00:00 2001 From: Colin Stone <30794003+Colin-Stone@users.noreply.github.com> Date: Thu, 28 Nov 2019 09:10:13 +0000 Subject: [PATCH 09/26] Fix up unit test cases Signed-off-by: Colin Stone <30794003+Colin-Stone@users.noreply.github.com> --- __tests__/__unit__/extension.unit.test.ts | 68 ++++------------------- 1 file changed, 10 insertions(+), 58 deletions(-) diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index be1256b4d4..dcc5e37769 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -1271,7 +1271,7 @@ describe("Extension Unit Tests", () => { expect(showErrorMessage.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls[0][0]).toBe("Data set failed to save. Data set may have been deleted on mainframe."); - testResponse.apiResponse.items = ["Item1"]; + testResponse.apiResponse.items = [{dsname: "HLQ.TEST.AFILE"}, {dsname: "HLQ.TEST.AFILE(mem)"}]; dataSetList.mockReset(); pathToDataSet.mockReset(); showErrorMessage.mockReset(); @@ -1784,8 +1784,10 @@ describe("Extension Unit Tests", () => { }); it("Testing that saveUSSFile is executed successfully", async () => { + withProgress.mockReset(); + const testDoc: vscode.TextDocument = { - fileName: path.join(extension.USS_DIR, ussNode.label, "testFile"), + fileName: path.join(extension.USS_DIR, "usstest", "/u/myuser/testFile"), uri: null, isUntitled: null, languageId: null, @@ -1811,16 +1813,19 @@ describe("Extension Unit Tests", () => { items: [] } }; + + fileList.mockResolvedValueOnce(testResponse); ussNode.mProfileName = "usstest"; + ussNode.dirty = true; testUSSTree.getChildren.mockReturnValueOnce([ - new ZoweUSSNode("testFile", vscode.TreeItemCollapsibleState.None, ussNode, session, "/")]); + new ZoweUSSNode("testFile", vscode.TreeItemCollapsibleState.None, ussNode, null, "/"), sessNode]); - testResponse.apiResponse.items = ["Item1"]; + testResponse.apiResponse.items = [{name: "testFile", mode: "-rwxrwx"}]; fileToUSSFile.mockReset(); showErrorMessage.mockReset(); testResponse.success = true; - fileToUSSFile.mockResolvedValueOnce(testResponse); + fileToUSSFile.mockResolvedValue(testResponse); withProgress.mockReturnValueOnce(testResponse); await extension.saveUSSFile(testDoc, testUSSTree); @@ -1838,59 +1843,6 @@ describe("Extension Unit Tests", () => { await extension.saveUSSFile(testDoc, testUSSTree); expect(showErrorMessage.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls[0][0]).toBe("Test Error"); - - const testDoc2: vscode.TextDocument = { - fileName: path.normalize("/sestest/HLQ.TEST.AFILE"), - uri: null, - isUntitled: null, - languageId: null, - version: null, - isDirty: null, - isClosed: null, - save: null, - eol: null, - lineCount: null, - lineAt: null, - offsetAt: null, - positionAt: null, - getText: null, - getWordRangeAtPosition: null, - validateRange: null, - validatePosition: null - }; - - testUSSTree.getChildren.mockReturnValueOnce([sessNode]); - - await extension.saveUSSFile(testDoc2, testUSSTree); - - const testDoc3: vscode.TextDocument = { - fileName: path.join(extension.DS_DIR, "/sestest/HLQ.TEST.AFILE(mem)"), - uri: null, - isUntitled: null, - languageId: null, - version: null, - isDirty: null, - isClosed: null, - save: null, - eol: null, - lineCount: null, - lineAt: null, - offsetAt: null, - positionAt: null, - getText: null, - getWordRangeAtPosition: null, - validateRange: null, - validatePosition: null - }; - - fileToUSSFile.mockReset(); - showErrorMessage.mockReset(); - - testUSSTree.getChildren.mockReturnValueOnce([sessNode]); - testResponse.success = true; - fileToUSSFile.mockResolvedValueOnce(testResponse); - - await extension.saveUSSFile(testDoc3, testUSSTree); }); it("tests that the prefix is set correctly on the job", async () => { From 0c2937c2f4f90b7b3728eb9b5ebb728dfe7ba571 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Thu, 28 Nov 2019 11:09:03 +0100 Subject: [PATCH 10/26] add conditional execution in case `node` is not defined - safeguarding `.getEtag()` to run on non-existing node Signed-off-by: Alexandru-Paul Dumitru --- src/extension.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 2be02b59af..28fc766b74 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1470,12 +1470,18 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase // Get specific node based on label const node = (await sesNode.getChildren()).find((child) => child.label.trim() === label); - const uploadEtag = node.getEtag(); - // define upload options - const uploadOptions: IUploadOptions = { - etag: uploadEtag, - returnEtag: true - }; + // define upload options + let uploadOptions: IUploadOptions; + if (node) { + uploadOptions = { + etag: node.getEtag(), + returnEtag: true + }; + } else { + uploadOptions = { + returnEtag: true + }; + } try { const uploadResponse = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, @@ -1542,7 +1548,12 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS // Get specific node based on label const node = (await sesNode.getChildren()).find((child) => child.fullPath.trim() === remote); - const uploadEtag = node.getEtag(); + let uploadEtag: string; + if (node) { + uploadEtag = node.getEtag(); + } else { + uploadEtag = null; + } try { const uploadResponse = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, From 96467a77b09981b7f3dae895e43c1937c13edf34 Mon Sep 17 00:00:00 2001 From: Colin Stone <30794003+Colin-Stone@users.noreply.github.com> Date: Tue, 26 Nov 2019 14:13:57 +0000 Subject: [PATCH 11/26] Removes unused code Signed-off-by: Colin Stone <30794003+Colin-Stone@users.noreply.github.com> --- .../__unit__/jes/jesNodeActions.unit.test.ts | 61 ------------------- src/jes/jesNodeActions.ts | 29 --------- 2 files changed, 90 deletions(-) delete mode 100644 __tests__/__unit__/jes/jesNodeActions.unit.test.ts delete mode 100644 src/jes/jesNodeActions.ts diff --git a/__tests__/__unit__/jes/jesNodeActions.unit.test.ts b/__tests__/__unit__/jes/jesNodeActions.unit.test.ts deleted file mode 100644 index c2575fd110..0000000000 --- a/__tests__/__unit__/jes/jesNodeActions.unit.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* -* This program and the accompanying materials are made available under the terms of the * -* Eclipse Public License v2.0 which accompanies this distribution, and is available at * -* https://www.eclipse.org/legal/epl-v20.html * -* * -* SPDX-License-Identifier: EPL-2.0 * -* * -* Copyright Contributors to the Zowe Project. * -* * -*/ - -jest.mock("@brightside/core"); -import * as jesNodeActions from "../../../src/jes/jesNodeActions"; -import * as brightside from "@brightside/core"; - -describe("jesNodeActions", () => { - - afterAll(() => { - jest.resetAllMocks(); - }); - - describe("getSpoolLanguage", () => { - const JESMSGLG = "JESMSGLG"; - const JESJCL = "JESJCL"; - const JESYSMSG = "JESYSMSG"; - const iJobFile: brightside.IJobFile = { - "byte-count": 128, - "job-correlator": "", - "record-count": 1, - "records-url": "fake/records", - "class": "A", - "ddname": "STDOUT", - "id": 100, - "jobid": "100", - "jobname": "TESTJOB", - "lrecl": 80, - "procstep": "", - "recfm": "FB", - "stepname": "", - "subsystem": "" - }; - afterEach(() => { - jest.resetAllMocks(); - }); - it("should return undefined language", () => { - expect(jesNodeActions.getSpoolLanguage(iJobFile)).toEqual(undefined); - }); - it("should return undefined language", () => { - iJobFile.ddname = JESMSGLG; - expect(jesNodeActions.getSpoolLanguage(iJobFile)).toEqual("jesmsglg"); - }); - it("should return undefined language", () => { - iJobFile.ddname = JESJCL; - expect(jesNodeActions.getSpoolLanguage(iJobFile)).toEqual("jesjcl"); - }); - it("should return undefined language", () => { - iJobFile.ddname = JESYSMSG; - expect(jesNodeActions.getSpoolLanguage(iJobFile)).toEqual("jesysmsg"); - }); - }); -}); diff --git a/src/jes/jesNodeActions.ts b/src/jes/jesNodeActions.ts deleted file mode 100644 index eb01583e8e..0000000000 --- a/src/jes/jesNodeActions.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -* This program and the accompanying materials are made available under the terms of the * -* Eclipse Public License v2.0 which accompanies this distribution, and is available at * -* https://www.eclipse.org/legal/epl-v20.html * -* * -* SPDX-License-Identifier: EPL-2.0 * -* * -* Copyright Contributors to the Zowe Project. * -* * -*/ - -import { IJobFile } from "@brightside/core"; - -export function getSpoolLanguage(spool: IJobFile) { - const name = spool.ddname.toLocaleUpperCase(); - const JESMSGLG = "JESMSGLG"; - const JESJCL = "JESJCL"; - const JESYSMSG = "JESYSMSG"; - if (name === JESMSGLG) { - return JESMSGLG.toLowerCase(); - } - if (name === JESJCL) { - return JESJCL.toLowerCase(); - } - if (name === JESYSMSG) { - return JESYSMSG.toLowerCase(); - } - return undefined; -} From bc991cd8abf3e7ec632e753193bc3b16df5b29c1 Mon Sep 17 00:00:00 2001 From: CForrest97 Date: Wed, 27 Nov 2019 13:40:36 +0000 Subject: [PATCH 12/26] rename data set member Signed-off-by: CForrest97 --- .../extension.integration.test.ts | 53 +++++++++++++ .../__snapshots__/extension.unit.test.ts.snap | 10 +++ __tests__/__unit__/extension.unit.test.ts | 77 ++++++++++++++++--- i18n/sample/package.i18n.json | 1 + package.json | 14 ++++ package.nls.json | 1 + src/extension.ts | 54 +++++++++++++ 7 files changed, 200 insertions(+), 10 deletions(-) diff --git a/__tests__/__integration__/extension.integration.test.ts b/__tests__/__integration__/extension.integration.test.ts index b9a9350353..e1b74e8763 100644 --- a/__tests__/__integration__/extension.integration.test.ts +++ b/__tests__/__integration__/extension.integration.test.ts @@ -425,6 +425,41 @@ describe("Extension Integration Tests", () => { expect(afterList.apiResponse.returnedRows).to.equal(1); }).timeout(TIMEOUT); }); + describe("Rename Member", () => { + beforeEach(async () => { + await zowe.Create.dataSet( + sessionNode.getSession(), + zowe.CreateDataSetTypeEnum.DATA_SET_PARTITIONED, + beforeDataSetName + ).catch((err) => err); + await zowe.Upload.bufferToDataSet( + sessionNode.getSession(), + new Buffer("abc"), + `${beforeDataSetName}(mem1)` + ); + }); + it("should rename a data set member", async () => { + let error; + let list; + + try { + const parentNode = new ZoweNode(beforeDataSetName, vscode.TreeItemCollapsibleState.None, sessionNode, session); + const childNode = new ZoweNode("mem1", vscode.TreeItemCollapsibleState.None, parentNode, session); + const inputBoxStub = sandbox.stub(vscode.window, "showInputBox"); + inputBoxStub.returns("mem2"); + + await extension.renameDataSetMember(childNode, testTree); + list = await zowe.List.allMembers(sessionNode.getSession(), beforeDataSetName); + } catch (err) { + error = err; + } + + expect(error).to.be.equal(undefined); + + expect(list.apiResponse.returnedRows).to.equal(1); + expect(list.apiResponse.items[0].member).to.equal("MEM2"); + }).timeout(TIMEOUT); + }); describe("Rename Partitioned Data Set", () => { beforeEach(async () => { await zowe.Create.dataSet( @@ -473,6 +508,24 @@ describe("Extension Integration Tests", () => { error = err; } + expect(error).not.to.be.equal(undefined); + }).timeout(TIMEOUT); + }); + describe("Rename Data Set Member", () => { + it("should throw an error if a missing data set name is provided", async () => { + let error; + + try { + const parentNode = new ZoweNode(beforeDataSetName, vscode.TreeItemCollapsibleState.None, sessionNode, session); + const childNode = new ZoweNode("mem1", vscode.TreeItemCollapsibleState.None, parentNode, session); + const inputBoxStub = sandbox.stub(vscode.window, "showInputBox"); + inputBoxStub.returns("mem2"); + + await extension.renameDataSetMember(childNode, testTree); + } catch (err) { + error = err; + } + expect(error).not.to.be.equal(undefined); }).timeout(TIMEOUT); }); diff --git a/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap b/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap index 21bff14a14..81092abeba 100644 --- a/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap +++ b/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap @@ -172,6 +172,16 @@ Array [ "group": "6_modification@0", "when": "view == zowe.explorer && viewItem == member", }, + Object { + "command": "zowe.renameDataSetMember", + "group": "6_modification@2", + "when": "view == zowe.explorer && viewItem == member", + }, + Object { + "command": "zowe.renameDataSetMember", + "group": "6_modification@2", + "when": "view == zowe.explorer && viewItem == member_fav", + }, Object { "command": "zowe.refreshNode", "group": "inline", diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index dcc5e37769..7b53f522cf 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -194,6 +194,7 @@ describe("Extension Unit Tests", () => { const mockPattern = jest.fn(); const Rename = jest.fn(); const renameDataSet = jest.fn(); + const renameDataSetMember = jest.fn(); const mockRenameFavorite = jest.fn(); const mockUpdateFavorites = jest.fn(); const mockRenameNode = jest.fn(); @@ -364,6 +365,8 @@ describe("Extension Unit Tests", () => { Object.defineProperty(brightside, "Copy", {value: Copy}); Object.defineProperty(Copy, "dataSet", { value: copyDataSet }); Object.defineProperty(vscode.env, "clipboard", { value: clipboard }); + Object.defineProperty(Rename, "dataSetMember", { value: renameDataSetMember }); + beforeEach(() => { mockLoadNamedProfile.mockReturnValue({profile: {name:"aProfile", type:"zosmf"}}); @@ -522,7 +525,7 @@ describe("Extension Unit Tests", () => { expect(createTreeView.mock.calls[0][0]).toBe("zowe.explorer"); expect(createTreeView.mock.calls[1][0]).toBe("zowe.uss.explorer"); // tslint:disable-next-line: no-magic-numbers - expect(registerCommand.mock.calls.length).toBe(62); + expect(registerCommand.mock.calls.length).toBe(63); registerCommand.mock.calls.forEach((call, i ) => { expect(registerCommand.mock.calls[i][1]).toBeInstanceOf(Function); }); @@ -554,6 +557,7 @@ describe("Extension Unit Tests", () => { "zowe.renameDataSet", "zowe.copyDataSet", "zowe.pasteDataSet", + "zowe.renameDataSetMember", "zowe.uss.addFavorite", "zowe.uss.removeFavorite", "zowe.uss.addSession", @@ -2292,25 +2296,25 @@ describe("Extension Unit Tests", () => { showInputBox.mockReset(); renameDataSet.mockReset(); - const child = new ZoweNode("HLQ.TEST.DELETE.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + const child = new ZoweNode("HLQ.TEST.RENAME.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); - showInputBox.mockResolvedValueOnce("HLQ.TEST.DELETE.NODE.NEW"); + showInputBox.mockResolvedValueOnce("HLQ.TEST.RENAME.NODE.NEW"); await extension.renameDataSet(child, testTree); expect(renameDataSet.mock.calls.length).toBe(1); - expect(renameDataSet).toHaveBeenLastCalledWith(child.getSession(), "HLQ.TEST.DELETE.NODE", "HLQ.TEST.DELETE.NODE.NEW"); + expect(renameDataSet).toHaveBeenLastCalledWith(child.getSession(), "HLQ.TEST.RENAME.NODE", "HLQ.TEST.RENAME.NODE.NEW"); }); it("Should rename a favorited node", async () => { showInputBox.mockReset(); renameDataSet.mockReset(); - const child = new ZoweNode("[sessNode]: HLQ.TEST.DELETE.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + const child = new ZoweNode("[sessNode]: HLQ.TEST.RENAME.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); child.contextValue = "ds_fav"; - showInputBox.mockResolvedValueOnce("HLQ.TEST.DELETE.NODE.NEW"); + showInputBox.mockResolvedValueOnce("HLQ.TEST.RENAME.NODE.NEW"); await extension.renameDataSet(child, testTree); expect(renameDataSet.mock.calls.length).toBe(1); - expect(renameDataSet).toHaveBeenLastCalledWith(child.getSession(), "HLQ.TEST.DELETE.NODE", "HLQ.TEST.DELETE.NODE.NEW"); + expect(renameDataSet).toHaveBeenLastCalledWith(child.getSession(), "HLQ.TEST.RENAME.NODE", "HLQ.TEST.RENAME.NODE.NEW"); }); it("Should throw an error if zowe.Rename.dataSet throws", async () => { let error; @@ -2320,9 +2324,9 @@ describe("Extension Unit Tests", () => { renameDataSet.mockReset(); renameDataSet.mockImplementation(() => { throw defaultError; }); - const child = new ZoweNode("[sessNode]: HLQ.TEST.DELETE.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + const child = new ZoweNode("[sessNode]: HLQ.TEST.RENAME.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); child.contextValue = "ds_fav"; - showInputBox.mockResolvedValueOnce("HLQ.TEST.DELETE.NODE.NEW"); + showInputBox.mockResolvedValueOnce("HLQ.TEST.RENAME.NODE.NEW"); try { await extension.renameDataSet(child, testTree); } catch (err) { @@ -2330,7 +2334,60 @@ describe("Extension Unit Tests", () => { } expect(renameDataSet.mock.calls.length).toBe(1); - expect(renameDataSet).toHaveBeenLastCalledWith(child.getSession(), "HLQ.TEST.DELETE.NODE", "HLQ.TEST.DELETE.NODE.NEW"); + expect(renameDataSet).toHaveBeenLastCalledWith(child.getSession(), "HLQ.TEST.RENAME.NODE", "HLQ.TEST.RENAME.NODE.NEW"); + expect(error).toBe(defaultError); + }); + it("Should rename the member", async () => { + showInputBox.mockReset(); + renameDataSet.mockReset(); + + const parent = new ZoweNode("HLQ.TEST.RENAME.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + const child = new ZoweNode("mem1", vscode.TreeItemCollapsibleState.None, parent, null); + + showInputBox.mockResolvedValueOnce("mem2"); + await extension.renameDataSetMember(child, testTree); + + expect(renameDataSetMember.mock.calls.length).toBe(1); + expect(renameDataSetMember).toHaveBeenLastCalledWith(child.getSession(), "HLQ.TEST.RENAME.NODE", "mem1", "mem2"); + }); + it("Should rename a favorited member", async () => { + showInputBox.mockReset(); + renameDataSet.mockReset(); + + const parent = new ZoweNode("[sesstest]: HLQ.TEST.RENAME.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + const child = new ZoweNode("mem1", vscode.TreeItemCollapsibleState.None, parent, null); + + parent.contextValue = extension.DS_PDS_CONTEXT + extension.FAV_SUFFIX; + child.contextValue = extension.DS_MEMBER_CONTEXT; + + showInputBox.mockResolvedValueOnce("mem2"); + await extension.renameDataSetMember(child, testTree); + + expect(renameDataSetMember.mock.calls.length).toBe(1); + expect(renameDataSetMember).toHaveBeenLastCalledWith(child.getSession(), "HLQ.TEST.RENAME.NODE", "mem1", "mem2"); + }); + it("Should throw an error if zowe.Rename.dataSetMember throws", async () => { + let error; + const defaultError = new Error("Default error message"); + + showInputBox.mockReset(); + renameDataSetMember.mockReset(); + renameDataSetMember.mockImplementation(() => { throw defaultError; }); + + const parent = new ZoweNode("HLQ.TEST.RENAME.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); + const child = new ZoweNode("mem1", vscode.TreeItemCollapsibleState.None, parent, null); + + child.contextValue = extension.DS_MEMBER_CONTEXT; + + showInputBox.mockResolvedValueOnce("mem2"); + try { + await extension.renameDataSetMember(child, testTree); + } catch (err) { + error = err; + } + + expect(renameDataSetMember.mock.calls.length).toBe(1); + expect(renameDataSetMember).toHaveBeenLastCalledWith(child.getSession(), "HLQ.TEST.RENAME.NODE", "mem1", "mem2"); expect(error).toBe(defaultError); }); }); diff --git a/i18n/sample/package.i18n.json b/i18n/sample/package.i18n.json index b01f9d8bec..544acef3b5 100644 --- a/i18n/sample/package.i18n.json +++ b/i18n/sample/package.i18n.json @@ -68,6 +68,7 @@ "renameDataSet": "Rename Data Set", "copyDataSet": "Copy Data Set", "pasteDataSet": "Paste Data Set", + "renameDataSetMember": "Rename Member", "profile.configuration.title": "Zowe z/OSMF Profiles", "profile.configuration.name": "Profile Name", "profile.configuration.url": "z/OSMF URL", diff --git a/package.json b/package.json index c926086a35..a3ca07d395 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,10 @@ "command": "zowe.pasteDataSet", "title": "%pasteDataSet%" }, + { + "command": "zowe.renameDataSetMember", + "title": "%renameDataSetMember%" + }, { "command": "zowe.deleteDataset", "title": "%deleteDataset%" @@ -574,6 +578,16 @@ "command": "zowe.editMember", "group": "6_modification@0" }, + { + "when": "view == zowe.explorer && viewItem == member", + "command": "zowe.renameDataSetMember", + "group": "6_modification@2" + }, + { + "when": "view == zowe.explorer && viewItem == member_fav", + "command": "zowe.renameDataSetMember", + "group": "6_modification@2" + }, { "when": "view == zowe.explorer && viewItem == ds", "command": "zowe.refreshNode", diff --git a/package.nls.json b/package.nls.json index 593700c6f7..2e79693a6e 100644 --- a/package.nls.json +++ b/package.nls.json @@ -68,6 +68,7 @@ "renameDataSet" : "Rename Data Set", "copyDataSet" : "Copy Data Set", "pasteDataSet" : "Paste Data Set", + "renameDataSetMember" : "Rename Member", "profile.configuration.title": "Zowe z/OSMF Profiles", "profile.configuration.name": "Profile Name", "profile.configuration.url": "z/OSMF URL", diff --git a/src/extension.ts b/src/extension.ts index 28fc766b74..8ad339b968 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -167,6 +167,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("zowe.renameDataSet", (node) => renameDataSet(node, datasetProvider)); vscode.commands.registerCommand("zowe.copyDataSet", (node) => copyDataSet(node)); vscode.commands.registerCommand("zowe.pasteDataSet", (node) => pasteDataSet(node, datasetProvider)); + vscode.commands.registerCommand("zowe.renameDataSetMember", (node) => renameDataSetMember(node, datasetProvider)); vscode.workspace.onDidChangeConfiguration(async (e) => { datasetProvider.onDidChangeConfiguration(e); }); @@ -970,6 +971,59 @@ export async function pasteDataSet(node: ZoweNode, datasetProvider: DatasetTree) } } +/** + * Rename data set members + * + * @export + * @param {ZoweNode} node - The node + * @param {DatasetTree} datasetProvider - the tree which contains the nodes + */ +export async function renameDataSetMember(node: ZoweNode, datasetProvider: DatasetTree) { + const beforeMemberName = node.label.trim(); + let dataSetName; + let profileLabel; + + if (node.mParent.contextValue.includes(FAV_SUFFIX)) { + profileLabel = node.mParent.label.substring(0, node.mParent.label.indexOf(":") + 2); + dataSetName = node.mParent.label.substring(node.mParent.label.indexOf(":") + 2); + } else { + dataSetName = node.mParent.label.trim(); + } + const afterMemberName = await vscode.window.showInputBox({ value: beforeMemberName }); + + log.debug(localize("renameDataSet.log.debug", "Renaming data set ") + afterMemberName); + if (afterMemberName) { + try { + await zowe.Rename.dataSetMember(node.getSession(), dataSetName, beforeMemberName, afterMemberName); + node.label = `${profileLabel}${afterMemberName}`; + } catch (err) { + log.error(localize("renameDataSet.log.error", "Error encountered when renaming data set! ") + JSON.stringify(err)); + vscode.window.showErrorMessage(localize("renameDataSet.error", "Unable to rename data set: ") + err.message); + throw err; + } + if (node.mParent.contextValue.includes(FAV_SUFFIX)) { + const nonFavoritedParent = datasetProvider.findNonFavoritedNode(node.mParent); + if (nonFavoritedParent) { + const nonFavoritedMember = nonFavoritedParent.children.find(child => child.label === beforeMemberName); + if (nonFavoritedMember) { + nonFavoritedMember.label = afterMemberName; + datasetProvider.refreshElement(nonFavoritedParent); + } + } + } else { + const favoritedParent = datasetProvider.findFavoritedNode(node.mParent); + if (favoritedParent) { + const favoritedMember = favoritedParent.children.find(child => child.label === beforeMemberName); + if (favoritedMember) { + favoritedMember.label = afterMemberName; + datasetProvider.refreshElement(favoritedParent); + } + } + } + datasetProvider.refreshElement(node.mParent); + } +} + /** * Recursively deletes directory * From 9da2994f980773890b8187b5f85ab939bea07a67 Mon Sep 17 00:00:00 2001 From: CForrest97 Date: Wed, 27 Nov 2019 13:46:48 +0000 Subject: [PATCH 13/26] resolve linter Signed-off-by: CForrest97 --- __tests__/__unit__/extension.unit.test.ts | 2 +- src/extension.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index 7b53f522cf..6cf2c1175c 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -2376,7 +2376,7 @@ describe("Extension Unit Tests", () => { const parent = new ZoweNode("HLQ.TEST.RENAME.NODE", vscode.TreeItemCollapsibleState.None, sessNode, null); const child = new ZoweNode("mem1", vscode.TreeItemCollapsibleState.None, parent, null); - + child.contextValue = extension.DS_MEMBER_CONTEXT; showInputBox.mockResolvedValueOnce("mem2"); diff --git a/src/extension.ts b/src/extension.ts index 8ad339b968..73c2d5e57a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1004,7 +1004,7 @@ export async function renameDataSetMember(node: ZoweNode, datasetProvider: Datas if (node.mParent.contextValue.includes(FAV_SUFFIX)) { const nonFavoritedParent = datasetProvider.findNonFavoritedNode(node.mParent); if (nonFavoritedParent) { - const nonFavoritedMember = nonFavoritedParent.children.find(child => child.label === beforeMemberName); + const nonFavoritedMember = nonFavoritedParent.children.find((child) => child.label === beforeMemberName); if (nonFavoritedMember) { nonFavoritedMember.label = afterMemberName; datasetProvider.refreshElement(nonFavoritedParent); @@ -1013,7 +1013,7 @@ export async function renameDataSetMember(node: ZoweNode, datasetProvider: Datas } else { const favoritedParent = datasetProvider.findFavoritedNode(node.mParent); if (favoritedParent) { - const favoritedMember = favoritedParent.children.find(child => child.label === beforeMemberName); + const favoritedMember = favoritedParent.children.find((child) => child.label === beforeMemberName); if (favoritedMember) { favoritedMember.label = afterMemberName; datasetProvider.refreshElement(favoritedParent); From 06a545d3ee9ad2b0338bac6c7c5a856a14158d52 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Thu, 28 Nov 2019 13:46:17 +0100 Subject: [PATCH 14/26] fix missing variable Signed-off-by: Alexandru-Paul Dumitru --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 73c2d5e57a..2f3e99206c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1553,7 +1553,7 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase returnEtag: true}); // re-assign etag, so that it can be used with subsequent requests const downloadEtag = downloadResponse.apiResponse.etag; - if (downloadEtag !== uploadEtag) { + if (downloadEtag !== node.getEtag()) { node.setEtag(downloadEtag); } vscode.window.showWarningMessage(localize("saveFile.error.etagMismatch","Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict.")); From a8b771bd1d3bc6d29e9b42f3491e0e05b59f497d Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Fri, 29 Nov 2019 15:19:09 +0100 Subject: [PATCH 15/26] fix errors + add tests on etag Signed-off-by: Alexandru-Paul Dumitru --- __tests__/__unit__/extension.unit.test.ts | 46 +++++++++++++++++++- src/extension.ts | 53 +++++++++++++---------- 2 files changed, 73 insertions(+), 26 deletions(-) diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index 3f0586b1d3..c41c0cfb7f 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -124,6 +124,7 @@ describe("Extension Unit Tests", () => { const rmdirSync = jest.fn(); const readFileSync = jest.fn(); const showErrorMessage = jest.fn(); + const showWarningMessage = jest.fn(); const showInputBox = jest.fn(); const showOpenDialog = jest.fn(); const showQuickBox = jest.fn(); @@ -312,6 +313,7 @@ describe("Extension Unit Tests", () => { Object.defineProperty(fs, "readFileSync", {value: readFileSync}); Object.defineProperty(fsextra, "moveSync", {value: moveSync}); Object.defineProperty(vscode.window, "showErrorMessage", {value: showErrorMessage}); + Object.defineProperty(vscode.window, "showWarningMessage", {value: showWarningMessage}); Object.defineProperty(vscode.window, "showInputBox", {value: showInputBox}); Object.defineProperty(vscode.window, "showQuickBox", {value: showQuickBox}); Object.defineProperty(vscode.window, "activeTextEditor", {value: activeTextEditor}); @@ -335,7 +337,6 @@ describe("Extension Unit Tests", () => { Object.defineProperty(vscode.workspace, "openTextDocument", {value: openTextDocument}); Object.defineProperty(vscode.window, "showInformationMessage", {value: showInformationMessage}); Object.defineProperty(vscode.window, "showTextDocument", {value: showTextDocument}); - Object.defineProperty(vscode.window, "showErrorMessage", {value: showErrorMessage}); Object.defineProperty(vscode.window, "showOpenDialog", {value: showOpenDialog}); Object.defineProperty(vscode.window, "showQuickPick", {value: showQuickPick}); Object.defineProperty(vscode.window, "withProgress", {value: withProgress}); @@ -1479,6 +1480,7 @@ describe("Extension Unit Tests", () => { items: [] } }; + sessNode.children.push(new ZoweNode("node", vscode.TreeItemCollapsibleState.None, sessNode, null)); testTree.getChildren.mockReturnValueOnce([new ZoweNode("node", vscode.TreeItemCollapsibleState.None, sessNode, null), sessNode]); dataSetList.mockReset(); showErrorMessage.mockReset(); @@ -1571,6 +1573,24 @@ describe("Extension Unit Tests", () => { testTree.getChildren.mockReturnValueOnce([new ZoweNode("node", vscode.TreeItemCollapsibleState.None, sessNode, null), sessNode]); dataSetList.mockReset(); showErrorMessage.mockReset(); + + testTree.getChildren.mockReturnValueOnce([sessNode]); + dataSetList.mockResolvedValueOnce(testResponse); + testResponse.success = false; + testResponse.commandResponse = "Rest API failure with HTTP(S) status 412"; + withProgress.mockResolvedValueOnce(testResponse); + dataSet.mockReset(); + const downloadResponse = { + success: true, + commandResponse: "", + apiResponse: { + etag: "" + } + }; + dataSet.mockResolvedValue(downloadResponse); + + await extension.saveFile(testDoc, testTree); + expect(showWarningMessage.mock.calls[0][0]).toBe("Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict."); }); it("Testing that refreshAll is executed successfully", async () => { @@ -2240,9 +2260,9 @@ describe("Extension Unit Tests", () => { fileList.mockResolvedValueOnce(testResponse); ussNode.mProfileName = "usstest"; ussNode.dirty = true; + ussNode.children.push(new ZoweUSSNode("/u/myuser/testFile", vscode.TreeItemCollapsibleState.None, ussNode, null, "/")); testUSSTree.getChildren.mockReturnValueOnce([ new ZoweUSSNode("testFile", vscode.TreeItemCollapsibleState.None, ussNode, null, "/"), sessNode]); - testResponse.apiResponse.items = [{name: "testFile", mode: "-rwxrwx"}]; fileToUSSFile.mockReset(); showErrorMessage.mockReset(); @@ -2266,6 +2286,28 @@ describe("Extension Unit Tests", () => { await extension.saveUSSFile(testDoc, testUSSTree); expect(showErrorMessage.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls[0][0]).toBe("Test Error"); + + showWarningMessage.mockReset(); + testResponse.success = false; + testResponse.commandResponse = "Rest API failure with HTTP(S) status 412"; + testDoc.getText = jest.fn(); + ussFile.mockReset(); + withProgress.mockRejectedValueOnce(Error("Rest API failure with HTTP(S) status 412")); + const downloadResponse = { + success: true, + commandResponse: "", + apiResponse: { + etag: "" + } + }; + ussFile.mockResolvedValueOnce(downloadResponse); + try { + await extension.saveUSSFile(testDoc, testUSSTree); + } catch (e) { + // this is OK. We are interested in the next expect (showWarninMessage) to fullfil + expect(e.message).toBe("vscode.Position is not a constructor"); + } + expect(showWarningMessage.mock.calls[0][0]).toBe("Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict."); }); describe("Add Jobs Session Unit Test", () => { diff --git a/src/extension.ts b/src/extension.ts index 82a05d9283..a3d7bd2357 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1598,6 +1598,7 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase // get session from session name let documentSession; + let node; const sesNode = (await datasetProvider.getChildren()).find((child) => child.label.trim() === sesName); if (sesNode) { @@ -1628,21 +1629,20 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase vscode.window.showErrorMessage(err.message + "\n" + err.stack); } } - // Get specific node based on label - const node = (await sesNode.getChildren()).find((child) => - child.label.trim() === label); - // define upload options - let uploadOptions: IUploadOptions; - if (node) { - uploadOptions = { - etag: node.getEtag(), - returnEtag: true - }; + // Get specific node based on label and parent tree (session / favorites) + if (sesNode.children.length === 0) { + node = datasetProvider.mFavorites.find((zNode) => (zNode.label === `[${sesName}]: ${label}`)); } else { - uploadOptions = { - returnEtag: true - }; + node = (await sesNode.getChildren()).find((child) => + child.label.trim() === label); } + + // define upload options + const uploadOptions: IUploadOptions = { + etag: node.getEtag(), + returnEtag: true + }; + try { const uploadResponse = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, @@ -1652,10 +1652,10 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase }); if (uploadResponse.success) { vscode.window.showInformationMessage(uploadResponse.commandResponse); // TODO MISSED TESTING - // setting local etag with the new etag from the updated file on mainframe + // set local etag with the new etag from the updated file on mainframe node.setEtag(uploadResponse.apiResponse[0].etag); } else if (!uploadResponse.success && uploadResponse.commandResponse.includes(localize("saveFile.error.ZosmfEtagMismatchError", "Rest API failure with HTTP(S) status 412"))) { - const downloadResponse = await zowe.Download.dataSet(node.getSession(),label, { + const downloadResponse = await zowe.Download.dataSet(node.getSession(), label, { file: doc.fileName, returnEtag: true}); // re-assign etag, so that it can be used with subsequent requests @@ -1701,29 +1701,34 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS // get session from session name let documentSession; let binary; + let node; const sesNode = (await ussFileProvider.mSessionNodes.find((child) => child.mProfileName && child.mProfileName.trim()=== sesName.trim())); if (sesNode) { documentSession = sesNode.getSession(); binary = Object.keys(sesNode.binaryFiles).find((child) => child === remote) !== undefined; } - // Get specific node based on label - const node = (await sesNode.getChildren()).find((child) => - child.fullPath.trim() === remote); - let uploadEtag: string; - if (node) { - uploadEtag = node.getEtag(); + // Get specific node based on label and parent tree (session / favorites) + if (sesNode.children.length === 0) { + node = ussFileProvider.mFavorites.find((zNode) => (zNode.label === `[${sesName}]: ${remote}`)); } else { - uploadEtag = null; + node = (await sesNode.getChildren()).find((child) => + child.fullPath.trim() === remote); } + + // define upload options + const etagToUpload: string = node.getEtag(); + const returnEtag: boolean = true; + try { const uploadResponse = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: localize("saveUSSFile.response.title", "Saving file...") }, () => { - return zowe.Upload.fileToUSSFile(documentSession, doc.fileName, remote, binary, null, uploadEtag, true); // TODO MISSED TESTING + return zowe.Upload.fileToUSSFile(documentSession, doc.fileName, remote, binary, null, etagToUpload, returnEtag); // TODO MISSED TESTING }); if (uploadResponse.success) { vscode.window.showInformationMessage(uploadResponse.commandResponse); + // set local etag with the new etag from the updated file on mainframe node.setEtag(uploadResponse.apiResponse.etag); // this part never runs! zowe.Upload.fileToUSSFile doesn't return success: false, it just throws the error which is caught below!!!!! } else { @@ -1736,7 +1741,7 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS returnEtag: true}); // re-assign etag, so that it can be used with subsequent requests const downloadEtag = downloadResponse.apiResponse.etag; - if (downloadEtag !== uploadEtag) { + if (downloadEtag !== etagToUpload) { node.setEtag(downloadEtag); } vscode.window.showWarningMessage(localize("saveFile.error.etagMismatch","Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict.")); From 9d51471ed012a0d4ef2deced08a7cebfe0895b3a Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Sun, 1 Dec 2019 13:48:07 +0100 Subject: [PATCH 16/26] fix bug - integration test discovered that nodes past depth=1 were not taken into consideration for USS files. - added recursive functions to extract and analyze the nodes - added some variable types here and there Signed-off-by: Alexandru-Paul Dumitru --- src/extension.ts | 39 +++++++++++++++++++++++---------------- src/utils.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index a3d7bd2357..78532f5371 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1597,8 +1597,8 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase const sesName = ending.substring(0, ending.indexOf(path.sep)); // get session from session name - let documentSession; - let node; + let documentSession: Session; + let node: ZoweNode; const sesNode = (await datasetProvider.getChildren()).find((child) => child.label.trim() === sesName); if (sesNode) { @@ -1633,15 +1633,18 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase if (sesNode.children.length === 0) { node = datasetProvider.mFavorites.find((zNode) => (zNode.label === `[${sesName}]: ${label}`)); } else { - node = (await sesNode.getChildren()).find((child) => - child.label.trim() === label); + const nodes = await utils.getAllNodes([sesNode]); + node = await nodes.find((child) => child.label.trim() === label); } // define upload options - const uploadOptions: IUploadOptions = { - etag: node.getEtag(), - returnEtag: true - }; + let uploadOptions: IUploadOptions; + if (node) { + uploadOptions = { + etag: node.getEtag(), + returnEtag: true + }; + } try { const uploadResponse = await vscode.window.withProgress({ @@ -1655,7 +1658,7 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase // set local etag with the new etag from the updated file on mainframe node.setEtag(uploadResponse.apiResponse[0].etag); } else if (!uploadResponse.success && uploadResponse.commandResponse.includes(localize("saveFile.error.ZosmfEtagMismatchError", "Rest API failure with HTTP(S) status 412"))) { - const downloadResponse = await zowe.Download.dataSet(node.getSession(), label, { + const downloadResponse = await zowe.Download.dataSet(documentSession, label, { file: doc.fileName, returnEtag: true}); // re-assign etag, so that it can be used with subsequent requests @@ -1699,9 +1702,9 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS const remote = ending.substring(sesName.length).replace(/\\/g, "/"); // get session from session name - let documentSession; + let documentSession: Session; let binary; - let node; + let node: ZoweUSSNode; const sesNode = (await ussFileProvider.mSessionNodes.find((child) => child.mProfileName && child.mProfileName.trim()=== sesName.trim())); if (sesNode) { documentSession = sesNode.getSession(); @@ -1711,13 +1714,17 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS if (sesNode.children.length === 0) { node = ussFileProvider.mFavorites.find((zNode) => (zNode.label === `[${sesName}]: ${remote}`)); } else { - node = (await sesNode.getChildren()).find((child) => - child.fullPath.trim() === remote); + const nodes = await utils.getAllUSSNodes([sesNode]); + node = await nodes.find((child) => child.fullPath.trim() === remote); } // define upload options - const etagToUpload: string = node.getEtag(); - const returnEtag: boolean = true; + let etagToUpload: string; + let returnEtag: boolean; + if (node) { + etagToUpload = node.getEtag(); + returnEtag = true; + } try { const uploadResponse = await vscode.window.withProgress({ @@ -1736,7 +1743,7 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS } } catch (err) { if (err.message.includes(localize("saveFile.error.ZosmfEtagMismatchError", "Rest API failure with HTTP(S) status 412"))) { - const downloadResponse = await zowe.Download.ussFile(node.getSession(), node.fullPath, { + const downloadResponse = await zowe.Download.ussFile(documentSession, node.fullPath, { file: getUSSDocumentFilePath(node), returnEtag: true}); // re-assign etag, so that it can be used with subsequent requests diff --git a/src/utils.ts b/src/utils.ts index 7ebd50ca0a..d353c9bc3c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -16,6 +16,8 @@ import { CliProfileManager } from "@brightside/imperative"; import { TreeItem, QuickPickItem, QuickPick } from "vscode"; import * as extension from "../src/extension"; import * as nls from "vscode-nls"; +import { ZoweUSSNode } from "./ZoweUSSNode"; +import { ZoweNode } from "./ZoweNode"; const localize = nls.config({ messageFormat: nls.MessageFormat.file })(); /* @@ -142,3 +144,31 @@ export class JobIdFilterDescriptor extends FilterDescriptor { "Job Id search")); } } + +/************************************************************************************************************* + * Returns array of all subnodes of given node + *************************************************************************************************************/ +export async function getAllUSSNodes(nodes: ZoweUSSNode[]) { + let allNodes = new Array(); + + for (const node of nodes) { + allNodes = allNodes.concat(await getAllUSSNodes(await node.getChildren())); + allNodes.push(node); + } + + return allNodes; +} + +/************************************************************************************************************* + * Returns array of all subnodes of given node + *************************************************************************************************************/ +export async function getAllNodes(nodes: ZoweNode[]) { + let allNodes = new Array(); + + for (const node of nodes) { + allNodes = allNodes.concat(await getAllNodes(await node.getChildren())); + allNodes.push(node); + } + + return allNodes; +} From c0f90e8d84408e905991980bf910273c8c12aa9d Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Mon, 2 Dec 2019 11:34:58 +0100 Subject: [PATCH 17/26] change function names Signed-off-by: Alexandru-Paul Dumitru --- src/extension.ts | 4 ++-- src/utils.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 78532f5371..eb639b00ce 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1633,7 +1633,7 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase if (sesNode.children.length === 0) { node = datasetProvider.mFavorites.find((zNode) => (zNode.label === `[${sesName}]: ${label}`)); } else { - const nodes = await utils.getAllNodes([sesNode]); + const nodes = await utils.concatChildNodes([sesNode]); node = await nodes.find((child) => child.label.trim() === label); } @@ -1714,7 +1714,7 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS if (sesNode.children.length === 0) { node = ussFileProvider.mFavorites.find((zNode) => (zNode.label === `[${sesName}]: ${remote}`)); } else { - const nodes = await utils.getAllUSSNodes([sesNode]); + const nodes = await utils.concatUSSChildNodes([sesNode]); node = await nodes.find((child) => child.fullPath.trim() === remote); } diff --git a/src/utils.ts b/src/utils.ts index d353c9bc3c..55bf60758e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -148,11 +148,11 @@ export class JobIdFilterDescriptor extends FilterDescriptor { /************************************************************************************************************* * Returns array of all subnodes of given node *************************************************************************************************************/ -export async function getAllUSSNodes(nodes: ZoweUSSNode[]) { +export async function concatUSSChildNodes(nodes: ZoweUSSNode[]) { let allNodes = new Array(); for (const node of nodes) { - allNodes = allNodes.concat(await getAllUSSNodes(await node.getChildren())); + allNodes = allNodes.concat(await concatUSSChildNodes(await node.getChildren())); allNodes.push(node); } @@ -162,11 +162,11 @@ export async function getAllUSSNodes(nodes: ZoweUSSNode[]) { /************************************************************************************************************* * Returns array of all subnodes of given node *************************************************************************************************************/ -export async function getAllNodes(nodes: ZoweNode[]) { +export async function concatChildNodes(nodes: ZoweNode[]) { let allNodes = new Array(); for (const node of nodes) { - allNodes = allNodes.concat(await getAllNodes(await node.getChildren())); + allNodes = allNodes.concat(await concatChildNodes(await node.getChildren())); allNodes.push(node); } From 56c3bf3ccbf0069c6ca3535817cc8a96f1127935 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Mon, 2 Dec 2019 12:05:10 +0100 Subject: [PATCH 18/26] bug fix - changed comparison elements while saving USS files, from label to fullPath Signed-off-by: Alexandru-Paul Dumitru --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index eb639b00ce..69e0626eee 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1712,7 +1712,7 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS } // Get specific node based on label and parent tree (session / favorites) if (sesNode.children.length === 0) { - node = ussFileProvider.mFavorites.find((zNode) => (zNode.label === `[${sesName}]: ${remote}`)); + node = ussFileProvider.mFavorites.find((zNode) => (zNode.fullPath.trim() === `${remote}`)); } else { const nodes = await utils.concatUSSChildNodes([sesNode]); node = await nodes.find((child) => child.fullPath.trim() === remote); From 5e0142471815cf1e0e6e9afafb9683fdaaa5bd63 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Mon, 2 Dec 2019 14:12:42 +0100 Subject: [PATCH 19/26] refactor the way node is getting resolved Signed-off-by: Alexandru-Paul Dumitru --- src/extension.ts | 55 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 69e0626eee..0456f742bf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -29,6 +29,7 @@ import { Profiles } from "./Profiles"; import * as nls from "vscode-nls"; import * as utils from "./utils"; import SpoolProvider, { encodeJobFile } from "./SpoolProvider"; +import { booleanLiteral } from "@babel/types"; const localize = nls.config({ messageFormat: nls.MessageFormat.file })(); @@ -40,15 +41,19 @@ export let ISTHEIA: boolean = false; // set during activate export const FAV_SUFFIX = "_fav"; export const INFORMATION_CONTEXT = "information"; export const FAVORITE_CONTEXT = "favorite"; +export const DS_FAV_CONTEXT = "ds_fav"; +export const PDS_FAV_CONTEXT = "pds_fav"; export const DS_SESSION_CONTEXT = "session"; export const DS_PDS_CONTEXT = "pds"; export const DS_DS_CONTEXT = "ds"; export const DS_MEMBER_CONTEXT = "member"; export const DS_TEXT_FILE_CONTEXT = "textFile"; +export const DS_FAV_TEXT_FILE_CONTEXT = "textFile_fav"; export const DS_BINARY_FILE_CONTEXT = "binaryFile"; export const DS_MIGRATED_FILE_CONTEXT = "migr"; export const USS_SESSION_CONTEXT = "uss_session"; export const USS_DIR_CONTEXT = "directory"; +export const USS_FAV_DIR_CONTEXT = "directory_fav"; export const JOBS_SESSION_CONTEXT = "server"; export const JOBS_JOB_CONTEXT = "job"; export const JOBS_SPOOL_CONTEXT = "spool"; @@ -1630,13 +1635,35 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase } } // Get specific node based on label and parent tree (session / favorites) + let nodes: ZoweNode[]; + let isFromFavorites: boolean; if (sesNode.children.length === 0) { - node = datasetProvider.mFavorites.find((zNode) => (zNode.label === `[${sesName}]: ${label}`)); + // saving from favorites + nodes = await utils.concatChildNodes(datasetProvider.mFavorites); + isFromFavorites = true; } else { - const nodes = await utils.concatChildNodes([sesNode]); - node = await nodes.find((child) => child.label.trim() === label); - } - + // saving from session + nodes = await utils.concatChildNodes([sesNode]); + isFromFavorites = false; + } + node = await nodes.find((zNode) => { + // dataset in Favorites + if (zNode.contextValue === DS_FAV_CONTEXT) { + return (zNode.label === `[${sesName}]: ${label}`) + // member in Favorites + } else if (zNode.contextValue === DS_MEMBER_CONTEXT && isFromFavorites) { + const zNodeDetails = getProfileAndDataSetName(zNode); + return (`${zNodeDetails.profileName}(${zNodeDetails.dataSetName})` === `[${sesName}]: ${label}`); + } else if (zNode.contextValue === DS_MEMBER_CONTEXT && !isFromFavorites) { + const zNodeDetails = getProfileAndDataSetName(zNode); + return (`${zNodeDetails.profileName}(${zNodeDetails.dataSetName})` === `${label}`); + } else if (zNode.contextValue === DS_DS_CONTEXT) { + return (zNode.label.trim() === label); + } else { + return false; + } + }); + // define upload options let uploadOptions: IUploadOptions; if (node) { @@ -1711,12 +1738,24 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS binary = Object.keys(sesNode.binaryFiles).find((child) => child === remote) !== undefined; } // Get specific node based on label and parent tree (session / favorites) + let nodes: ZoweUSSNode[]; + let isFromFavorites: boolean; if (sesNode.children.length === 0) { - node = ussFileProvider.mFavorites.find((zNode) => (zNode.fullPath.trim() === `${remote}`)); + // saving from favorites + nodes = await utils.concatUSSChildNodes(ussFileProvider.mFavorites); + isFromFavorites = true; } else { - const nodes = await utils.concatUSSChildNodes([sesNode]); - node = await nodes.find((child) => child.fullPath.trim() === remote); + // saving from session + nodes = await utils.concatUSSChildNodes([sesNode]); + isFromFavorites = false; } + node = await nodes.find((zNode) => { + if (zNode.contextValue === DS_FAV_TEXT_FILE_CONTEXT || zNode.contextValue === DS_TEXT_FILE_CONTEXT) { + return (zNode.fullPath.trim() === remote); + } else { + return false; + } + }); // define upload options let etagToUpload: string; From afb9c8491bf3befef2c67723b6ca7e3d920c44c0 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Mon, 2 Dec 2019 14:13:44 +0100 Subject: [PATCH 20/26] remove unused variable Signed-off-by: Alexandru-Paul Dumitru --- src/extension.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 0456f742bf..d91447f16b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1739,15 +1739,12 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS } // Get specific node based on label and parent tree (session / favorites) let nodes: ZoweUSSNode[]; - let isFromFavorites: boolean; if (sesNode.children.length === 0) { // saving from favorites nodes = await utils.concatUSSChildNodes(ussFileProvider.mFavorites); - isFromFavorites = true; } else { // saving from session nodes = await utils.concatUSSChildNodes([sesNode]); - isFromFavorites = false; } node = await nodes.find((zNode) => { if (zNode.contextValue === DS_FAV_TEXT_FILE_CONTEXT || zNode.contextValue === DS_TEXT_FILE_CONTEXT) { From 39d77e77d02d7a3f3eced6335ce717873b738271 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Mon, 2 Dec 2019 14:21:06 +0100 Subject: [PATCH 21/26] fix linting :) Signed-off-by: Alexandru-Paul Dumitru --- src/extension.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index d91447f16b..0304aec981 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -29,7 +29,6 @@ import { Profiles } from "./Profiles"; import * as nls from "vscode-nls"; import * as utils from "./utils"; import SpoolProvider, { encodeJobFile } from "./SpoolProvider"; -import { booleanLiteral } from "@babel/types"; const localize = nls.config({ messageFormat: nls.MessageFormat.file })(); @@ -1649,7 +1648,7 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase node = await nodes.find((zNode) => { // dataset in Favorites if (zNode.contextValue === DS_FAV_CONTEXT) { - return (zNode.label === `[${sesName}]: ${label}`) + return (zNode.label === `[${sesName}]: ${label}`); // member in Favorites } else if (zNode.contextValue === DS_MEMBER_CONTEXT && isFromFavorites) { const zNodeDetails = getProfileAndDataSetName(zNode); @@ -1663,7 +1662,7 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase return false; } }); - + // define upload options let uploadOptions: IUploadOptions; if (node) { From fcf2a9735ce2be819c35e149c194a963c5c0a33f Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Tue, 3 Dec 2019 11:43:30 +0100 Subject: [PATCH 22/26] change from node.getChildren() to node.children - fixed mocked values to refrect expected reality Signed-off-by: Alexandru-Paul Dumitru --- __tests__/__unit__/extension.unit.test.ts | 4 ++-- src/utils.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index c41c0cfb7f..654207b789 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -1480,7 +1480,7 @@ describe("Extension Unit Tests", () => { items: [] } }; - sessNode.children.push(new ZoweNode("node", vscode.TreeItemCollapsibleState.None, sessNode, null)); + sessNode.children.push(new ZoweNode("HLQ.TEST.AFILE", vscode.TreeItemCollapsibleState.None, sessNode, null)); testTree.getChildren.mockReturnValueOnce([new ZoweNode("node", vscode.TreeItemCollapsibleState.None, sessNode, null), sessNode]); dataSetList.mockReset(); showErrorMessage.mockReset(); @@ -2260,7 +2260,7 @@ describe("Extension Unit Tests", () => { fileList.mockResolvedValueOnce(testResponse); ussNode.mProfileName = "usstest"; ussNode.dirty = true; - ussNode.children.push(new ZoweUSSNode("/u/myuser/testFile", vscode.TreeItemCollapsibleState.None, ussNode, null, "/")); + ussNode.children.push(new ZoweUSSNode("u/myuser/testFile", vscode.TreeItemCollapsibleState.None, ussNode, null, "/")); testUSSTree.getChildren.mockReturnValueOnce([ new ZoweUSSNode("testFile", vscode.TreeItemCollapsibleState.None, ussNode, null, "/"), sessNode]); testResponse.apiResponse.items = [{name: "testFile", mode: "-rwxrwx"}]; diff --git a/src/utils.ts b/src/utils.ts index 55bf60758e..2b2d153ba3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -152,7 +152,7 @@ export async function concatUSSChildNodes(nodes: ZoweUSSNode[]) { let allNodes = new Array(); for (const node of nodes) { - allNodes = allNodes.concat(await concatUSSChildNodes(await node.getChildren())); + allNodes = allNodes.concat(await concatUSSChildNodes(await node.children)); allNodes.push(node); } @@ -166,7 +166,7 @@ export async function concatChildNodes(nodes: ZoweNode[]) { let allNodes = new Array(); for (const node of nodes) { - allNodes = allNodes.concat(await concatChildNodes(await node.getChildren())); + allNodes = allNodes.concat(await concatChildNodes(await node.children)); allNodes.push(node); } From 23fac2998ac54c024274383acb56376d0aaf79b8 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Tue, 3 Dec 2019 12:04:56 +0100 Subject: [PATCH 23/26] remove unnecessary `await` Signed-off-by: Alexandru-Paul Dumitru --- src/extension.ts | 8 ++++---- src/utils.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 0304aec981..04b21dd3a5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1638,11 +1638,11 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase let isFromFavorites: boolean; if (sesNode.children.length === 0) { // saving from favorites - nodes = await utils.concatChildNodes(datasetProvider.mFavorites); + nodes = utils.concatChildNodes(datasetProvider.mFavorites); isFromFavorites = true; } else { // saving from session - nodes = await utils.concatChildNodes([sesNode]); + nodes = utils.concatChildNodes([sesNode]); isFromFavorites = false; } node = await nodes.find((zNode) => { @@ -1740,10 +1740,10 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS let nodes: ZoweUSSNode[]; if (sesNode.children.length === 0) { // saving from favorites - nodes = await utils.concatUSSChildNodes(ussFileProvider.mFavorites); + nodes = utils.concatUSSChildNodes(ussFileProvider.mFavorites); } else { // saving from session - nodes = await utils.concatUSSChildNodes([sesNode]); + nodes = utils.concatUSSChildNodes([sesNode]); } node = await nodes.find((zNode) => { if (zNode.contextValue === DS_FAV_TEXT_FILE_CONTEXT || zNode.contextValue === DS_TEXT_FILE_CONTEXT) { diff --git a/src/utils.ts b/src/utils.ts index 2b2d153ba3..85739fec82 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -148,11 +148,11 @@ export class JobIdFilterDescriptor extends FilterDescriptor { /************************************************************************************************************* * Returns array of all subnodes of given node *************************************************************************************************************/ -export async function concatUSSChildNodes(nodes: ZoweUSSNode[]) { +export function concatUSSChildNodes(nodes: ZoweUSSNode[]) { let allNodes = new Array(); for (const node of nodes) { - allNodes = allNodes.concat(await concatUSSChildNodes(await node.children)); + allNodes = allNodes.concat(concatUSSChildNodes(node.children)); allNodes.push(node); } @@ -162,11 +162,11 @@ export async function concatUSSChildNodes(nodes: ZoweUSSNode[]) { /************************************************************************************************************* * Returns array of all subnodes of given node *************************************************************************************************************/ -export async function concatChildNodes(nodes: ZoweNode[]) { +export function concatChildNodes(nodes: ZoweNode[]) { let allNodes = new Array(); for (const node of nodes) { - allNodes = allNodes.concat(await concatChildNodes(await node.children)); + allNodes = allNodes.concat(concatChildNodes(node.children)); allNodes.push(node); } From 98e21adf53ffcc52504fbdb10b69bd1caadc24c6 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Tue, 3 Dec 2019 17:46:35 +0100 Subject: [PATCH 24/26] adding more tests Signed-off-by: Alexandru-Paul Dumitru --- __tests__/__unit__/extension.unit.test.ts | 57 ++++++++++++++++++++--- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index 654207b789..0bd3e5af79 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -205,6 +205,8 @@ describe("Extension Unit Tests", () => { const copyDataSet = jest.fn(); const findFavoritedNode = jest.fn(); const findNonFavoritedNode = jest.fn(); + const concatChildNodes = jest.fn(); + const concatUSSChildNodes = jest.fn(); let mockClipboardData: string; const clipboard = { writeText: jest.fn().mockImplementation((value) => mockClipboardData = value), @@ -293,6 +295,8 @@ describe("Extension Unit Tests", () => { }) }); + Object.defineProperty(utils, "concatChildNodes", {value: concatChildNodes}); + Object.defineProperty(utils, "concatUSSChildNodes", {value: concatUSSChildNodes}); Object.defineProperty(fs, "mkdirSync", {value: mkdirSync}); Object.defineProperty(brtimperative, "CliProfileManager", {value: CliProfileManager}); Object.defineProperty(vscode.window, "createTreeView", {value: createTreeView}); @@ -1480,11 +1484,9 @@ describe("Extension Unit Tests", () => { items: [] } }; - sessNode.children.push(new ZoweNode("HLQ.TEST.AFILE", vscode.TreeItemCollapsibleState.None, sessNode, null)); testTree.getChildren.mockReturnValueOnce([new ZoweNode("node", vscode.TreeItemCollapsibleState.None, sessNode, null), sessNode]); dataSetList.mockReset(); showErrorMessage.mockReset(); - dataSetList.mockResolvedValueOnce(testResponse); await extension.saveFile(testDoc, testTree); @@ -1494,19 +1496,39 @@ describe("Extension Unit Tests", () => { expect(showErrorMessage.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls[0][0]).toBe("Data set failed to save. Data set may have been deleted on mainframe."); + const node = new ZoweNode("HLQ.TEST.AFILE", vscode.TreeItemCollapsibleState.None, sessNode, null); + sessNode.children.push(node); testResponse.apiResponse.items = [{dsname: "HLQ.TEST.AFILE"}, {dsname: "HLQ.TEST.AFILE(mem)"}]; dataSetList.mockReset(); pathToDataSet.mockReset(); showErrorMessage.mockReset(); - + concatChildNodes.mockReset(); + const mockSetEtag = jest.spyOn(node, "setEtag").mockImplementation(() => null); + mockSetEtag.mockReset(); + const uploadResponse: brightside.IZosFilesResponse = { + success: true, + commandResponse: "success", + apiResponse: [{ + etag: "123" + }] + }; + concatChildNodes.mockReturnValueOnce([sessNode.children[0]]); testTree.getChildren.mockReturnValueOnce([sessNode]); dataSetList.mockResolvedValueOnce(testResponse); dataSetList.mockResolvedValueOnce(testResponse); + withProgress.mockResolvedValueOnce(uploadResponse); testResponse.success = true; pathToDataSet.mockResolvedValueOnce(testResponse); await extension.saveFile(testDoc, testTree); + expect(concatChildNodes.mock.calls.length).toBe(1); + expect(showInformationMessage.mock.calls.length).toBe(1); + expect(showInformationMessage.mock.calls[0][0]).toBe("success"); + expect(mockSetEtag).toHaveBeenCalledTimes(1); + expect(mockSetEtag).toHaveBeenCalledWith("123"); + + concatChildNodes.mockReturnValueOnce([sessNode.children[0]]); testTree.getChildren.mockReturnValueOnce([sessNode]); dataSetList.mockResolvedValueOnce(testResponse); testResponse.success = false; @@ -1565,17 +1587,24 @@ describe("Extension Unit Tests", () => { dataSetList.mockReset(); showErrorMessage.mockReset(); + sessNode.children.push(new ZoweNode("HLQ.TEST.AFILE(mem)", vscode.TreeItemCollapsibleState.None, sessNode, null)); testTree.getChildren.mockReturnValueOnce([sessNode]); dataSetList.mockResolvedValueOnce(testResponse); testResponse.success = true; + concatChildNodes.mockReset(); + concatChildNodes.mockReturnValueOnce(sessNode.children); await extension.saveFile(testDoc3, testTree); + expect(concatChildNodes.mock.calls.length).toBe(1); + testTree.getChildren.mockReturnValueOnce([new ZoweNode("node", vscode.TreeItemCollapsibleState.None, sessNode, null), sessNode]); dataSetList.mockReset(); showErrorMessage.mockReset(); testTree.getChildren.mockReturnValueOnce([sessNode]); dataSetList.mockResolvedValueOnce(testResponse); + concatChildNodes.mockReset(); + concatChildNodes.mockReturnValueOnce(sessNode.children); testResponse.success = false; testResponse.commandResponse = "Rest API failure with HTTP(S) status 412"; withProgress.mockResolvedValueOnce(testResponse); @@ -1591,6 +1620,7 @@ describe("Extension Unit Tests", () => { await extension.saveFile(testDoc, testTree); expect(showWarningMessage.mock.calls[0][0]).toBe("Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict."); + expect(concatChildNodes.mock.calls.length).toBe(1); }); it("Testing that refreshAll is executed successfully", async () => { @@ -2260,19 +2290,27 @@ describe("Extension Unit Tests", () => { fileList.mockResolvedValueOnce(testResponse); ussNode.mProfileName = "usstest"; ussNode.dirty = true; - ussNode.children.push(new ZoweUSSNode("u/myuser/testFile", vscode.TreeItemCollapsibleState.None, ussNode, null, "/")); + const node = new ZoweUSSNode("u/myuser/testFile", vscode.TreeItemCollapsibleState.None, ussNode, null, "/"); + ussNode.children.push(node); testUSSTree.getChildren.mockReturnValueOnce([ new ZoweUSSNode("testFile", vscode.TreeItemCollapsibleState.None, ussNode, null, "/"), sessNode]); testResponse.apiResponse.items = [{name: "testFile", mode: "-rwxrwx"}]; fileToUSSFile.mockReset(); showErrorMessage.mockReset(); - + concatUSSChildNodes.mockReset(); + const mockGetEtag = jest.spyOn(node, "getEtag").mockImplementation(() => "123"); testResponse.success = true; fileToUSSFile.mockResolvedValue(testResponse); withProgress.mockReturnValueOnce(testResponse); - + concatUSSChildNodes.mockReturnValueOnce([ussNode.children[0]]); await extension.saveUSSFile(testDoc, testUSSTree); + expect(concatUSSChildNodes.mock.calls.length).toBe(1); + expect(mockGetEtag).toBeCalledTimes(1); + expect(mockGetEtag).toReturnWith("123"); + + concatUSSChildNodes.mockReset(); + concatUSSChildNodes.mockReturnValueOnce([ussNode.children[0]]); testResponse.success = false; testResponse.commandResponse = "Save failed"; fileToUSSFile.mockResolvedValueOnce(testResponse); @@ -2280,6 +2318,11 @@ describe("Extension Unit Tests", () => { await extension.saveUSSFile(testDoc, testUSSTree); + expect(showErrorMessage.mock.calls.length).toBe(1); + expect(showErrorMessage.mock.calls[0][0]).toBe("Save failed"); + + concatUSSChildNodes.mockReset(); + concatUSSChildNodes.mockReturnValueOnce([ussNode.children[0]]); showErrorMessage.mockReset(); withProgress.mockRejectedValueOnce(Error("Test Error")); @@ -2287,6 +2330,8 @@ describe("Extension Unit Tests", () => { expect(showErrorMessage.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls[0][0]).toBe("Test Error"); + concatUSSChildNodes.mockReset(); + concatUSSChildNodes.mockReturnValueOnce([ussNode.children[0]]); showWarningMessage.mockReset(); testResponse.success = false; testResponse.commandResponse = "Rest API failure with HTTP(S) status 412"; From 6b94c23584a6e72e814dd793b224868a8563c416 Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Wed, 4 Dec 2019 03:03:12 +0100 Subject: [PATCH 25/26] another atempt to increase test coverage - also included a `return` with error if `documentSession` is not found. This should be a show stopper - fix bug where `sesNode` was not defined but trying to extract the nodes Signed-off-by: Alexandru-Paul Dumitru --- __tests__/__unit__/extension.unit.test.ts | 50 ++++++++++++++++++++++- src/extension.ts | 5 ++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index 75bbed9079..cc02bef55b 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -294,7 +294,6 @@ describe("Extension Unit Tests", () => { }; }) }); - Object.defineProperty(utils, "concatChildNodes", {value: concatChildNodes}); Object.defineProperty(utils, "concatUSSChildNodes", {value: concatUSSChildNodes}); Object.defineProperty(fs, "mkdirSync", {value: mkdirSync}); @@ -1537,6 +1536,25 @@ describe("Extension Unit Tests", () => { validateRange: null, validatePosition: null }; + const testDoc0: vscode.TextDocument = { + fileName: path.join(extension.DS_DIR, "HLQ.TEST.AFILE"), + uri: null, + isUntitled: null, + languageId: null, + version: null, + isDirty: null, + isClosed: null, + save: null, + eol: null, + lineCount: null, + lineAt: null, + offsetAt: null, + positionAt: null, + getText: null, + getWordRangeAtPosition: null, + validateRange: null, + validatePosition: null + }; const testResponse = { success: true, @@ -1545,6 +1563,36 @@ describe("Extension Unit Tests", () => { items: [] } }; + // If session node is not defined, it should take the session from Profile + const sessionwocred = new brtimperative.Session({ + user: "", + password: "", + hostname: "fake", + protocol: "https", + type: "basic", + }); + // testing if no session is defined (can happen while saving from favorites) + const nodeWitoutSession = new ZoweNode("HLQ.TEST.AFILE", vscode.TreeItemCollapsibleState.None, null, null); + testTree.getChildren.mockReturnValueOnce([nodeWitoutSession]); + concatChildNodes.mockReturnValueOnce([nodeWitoutSession]); + createBasicZosmfSession.mockReturnValueOnce(sessionwocred); + await extension.saveFile(testDoc0, testTree); + expect(createBasicZosmfSession.mock.calls.length).toBe(1); + expect(createBasicZosmfSession.mock.results[0].value).toEqual(sessionwocred); + + // testing if no documentSession is found (no session + no profile) + createBasicZosmfSession.mockReset(); + testTree.getChildren.mockReset(); + showErrorMessage.mockReset(); + testTree.getChildren.mockReturnValueOnce([nodeWitoutSession]); + createBasicZosmfSession.mockReturnValueOnce(null); + await extension.saveFile(testDoc0, testTree); + expect(showErrorMessage.mock.calls.length).toBe(1); + expect(showErrorMessage.mock.calls[0][0]).toBe("Couldn't locate session when saving data set!"); + + testTree.getChildren.mockReset(); + createBasicZosmfSession.mockReset(); + testTree.getChildren.mockReturnValueOnce([new ZoweNode("node", vscode.TreeItemCollapsibleState.None, sessNode, null), sessNode]); dataSetList.mockReset(); showErrorMessage.mockReset(); diff --git a/src/extension.ts b/src/extension.ts index 618d76c617..a457a9fdb5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1657,6 +1657,7 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase } if (documentSession == null) { log.error(localize("saveFile.log.error.session", "Couldn't locate session when saving data set!")); + return vscode.window.showErrorMessage(localize("saveFile.log.error.session", "Couldn't locate session when saving data set!")); } // If not a member const label = doc.fileName.substring(doc.fileName.lastIndexOf(path.sep) + 1, @@ -1677,7 +1678,7 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase // Get specific node based on label and parent tree (session / favorites) let nodes: ZoweNode[]; let isFromFavorites: boolean; - if (sesNode.children.length === 0) { + if (!sesNode || sesNode.children.length === 0) { // saving from favorites nodes = utils.concatChildNodes(datasetProvider.mFavorites); isFromFavorites = true; @@ -1779,7 +1780,7 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS } // Get specific node based on label and parent tree (session / favorites) let nodes: ZoweUSSNode[]; - if (sesNode.children.length === 0) { + if (!sesNode || sesNode.children.length === 0) { // saving from favorites nodes = utils.concatUSSChildNodes(ussFileProvider.mFavorites); } else { From c55603b615a057d482dc9abb81688d7d8b98ffdb Mon Sep 17 00:00:00 2001 From: Alexandru-Paul Dumitru Date: Wed, 4 Dec 2019 13:49:34 +0100 Subject: [PATCH 26/26] add unit test for getEtag and setEtag in ZoweUSSNode Signed-off-by: Alexandru-Paul Dumitru --- __tests__/__unit__/ZoweUSSNode.unit.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/__tests__/__unit__/ZoweUSSNode.unit.test.ts b/__tests__/__unit__/ZoweUSSNode.unit.test.ts index 781f7e1151..6b50ff3510 100644 --- a/__tests__/__unit__/ZoweUSSNode.unit.test.ts +++ b/__tests__/__unit__/ZoweUSSNode.unit.test.ts @@ -199,4 +199,22 @@ describe("Unit Tests (Jest)", () => { utils.labelHack(rootNode); expect(rootNode.label === "gappy"); }); + + /************************************************************************************************************* + * Checks that getEtag() returns a value + *************************************************************************************************************/ + it("Checks that getEtag() returns a value", async () => { + const rootNode = new ZoweUSSNode("gappy", vscode.TreeItemCollapsibleState.Collapsed, null, session, null, null, null, "123"); + expect(rootNode.getEtag() === "123"); + }); + + /************************************************************************************************************* + * Checks that setEtag() assigns a value + *************************************************************************************************************/ + it("Checks that setEtag() assigns a value", async () => { + const rootNode = new ZoweUSSNode("gappy", vscode.TreeItemCollapsibleState.Collapsed, null, session, null, null, null, "123"); + expect(rootNode.getEtag() === "123"); + rootNode.setEtag("ABC"); + expect(rootNode.getEtag() === "ABC"); + }); });