Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Identical member names - PDS enhancement #2427

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to the Zowe CLI package will be documented in this file.

## Recent Changes

- Enhancement: When copying PDSs with like-named members, the user is now prompted to confirm before the operation occurs in case of data loss. [#2349] (https://github.com/zowe/zowe-cli/issues/2349)
- Enhancement: Added `--recordRange` flag to `zowe jobs download output` command to allow users to select a specific range of records to output from a spool file. [#2411](https://github.com/zowe/zowe-cli/pull/2411)
- BugFix: The `zowe zos-files copy data-set` command overwrites the contents of the target data set without user confirmation. A `--safe-replace` option was added which prompts the user to confirm before overwriting the contents of the target data set. [#2369] (https://github.com/zowe/zowe-cli/issues/2369)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@

},
response: {
console: { promptFn: jest.fn() }
console: { promptFn: jest.fn(), promptForLikeNamedMembers: jest.fn() }
}
};

Expand All @@ -68,7 +68,8 @@
"replace": commandParameters.arguments.replace,
"responseTimeout": commandParameters.arguments.responseTimeout,
"safeReplace": commandParameters.arguments.safeReplace,
"promptFn": expect.any(Function)
"promptFn": expect.any(Function),
"promptForLikeNamedMembers": expect.any(Function)
}
);
expect(response).toBe(defaultReturn);
Expand Down Expand Up @@ -98,7 +99,7 @@
responseTimeout
},
response: {
console: { promptFn: jest.fn() }
console: { promptFn: jest.fn(), promptForLikeNamedMembers: jest.fn() }
}
};

Expand All @@ -116,7 +117,8 @@
"replace": commandParameters.arguments.replace,
"responseTimeout": commandParameters.arguments.responseTimeout,
"safeReplace": commandParameters.arguments.safeReplace,
"promptFn": expect.any(Function)
"promptFn": expect.any(Function),
"promptForLikeNamedMembers": expect.any(Function)
}
);
expect(response).toBe(defaultReturn);
Expand Down Expand Up @@ -162,7 +164,8 @@
"replace": commandParameters.arguments.replace,
"responseTimeout": commandParameters.arguments.responseTimeout,
"safeReplace": commandParameters.arguments.safeReplace,
"promptFn": expect.any(Function)
"promptFn": expect.any(Function),
"promptForLikeNamedMembers": expect.any(Function)
}
);
expect(response).toBe(defaultReturn);
Expand Down Expand Up @@ -198,7 +201,7 @@
const result = await promptFn(commandParameters.arguments.toDataSetName);

expect(promptMock).toHaveBeenCalledWith(
`The dataset '${toDataSetName}' exists on the target system. This copy will result in data loss.` +
`The dataset '${toDataSetName}' exists on the target system. This copy may result in data loss.` +
pujal0909 marked this conversation as resolved.
Show resolved Hide resolved
` Are you sure you want to continue? [y/N]: `
);
expect(result).toBe(true);
Expand Down Expand Up @@ -234,7 +237,79 @@
const result = await promptFn(commandParameters.arguments.toDataSetName);

expect(promptMock).toHaveBeenCalledWith(
`The dataset '${toDataSetName}' exists on the target system. This copy will result in data loss.` +
`The dataset '${toDataSetName}' exists on the target system. This copy may result in data loss.` +
pujal0909 marked this conversation as resolved.
Show resolved Hide resolved
` Are you sure you want to continue? [y/N]: `
);
expect(result).toBe(false);
});
it("should prompt the user about duplicate member names and return true when input is 'y", async () => {
const handler = new DsHandler();

expect(handler).toBeInstanceOf(ZosFilesBaseHandler);
const fromDataSetName = "ABCD";
const toDataSetName = "EFGH";
const enq = "SHR";
const replace = false;
const safeReplace = false;
const responseTimeout: any = undefined;

const commandParameters: any = {
Dismissed Show dismissed Hide dismissed
arguments: {
fromDataSetName,
toDataSetName,
enq,
replace,
safeReplace,
responseTimeout
},
response: {
console: { promptFn: jest.fn() }
}
};
const promptMock = jest.fn();
promptMock.mockResolvedValue("y");

const promptForDuplicates = (handler as any)["promptForLikeNamedMembers"]({ prompt: promptMock });
const result = await promptForDuplicates();

expect(promptMock).toHaveBeenCalledWith(
`The source and target data sets have like named member names. The contents of those members will be overwritten.` +
pujal0909 marked this conversation as resolved.
Show resolved Hide resolved
` Are you sure you want to continue? [y/N]: `
);
expect(result).toBe(true);
});
it("should prompt the user about duplicate member names and return false when input is 'N'", async () => {
const handler = new DsHandler();

expect(handler).toBeInstanceOf(ZosFilesBaseHandler);
const fromDataSetName = "ABCD";
const toDataSetName = "EFGH";
const enq = "SHR";
const replace = false;
const safeReplace = false;
const responseTimeout: any = undefined;

const commandParameters: any = {
Dismissed Show dismissed Hide dismissed
arguments: {
fromDataSetName,
toDataSetName,
enq,
replace,
safeReplace,
responseTimeout
},
response: {
console: { promptFn: jest.fn() }
}
};
const promptMock = jest.fn();
promptMock.mockResolvedValue("N");

const promptForDuplicates = (handler as any)["promptForLikeNamedMembers"]({ prompt: promptMock });
const result = await promptForDuplicates();

expect(promptMock).toHaveBeenCalledWith(
`The source and target data sets have like named member names. The contents of those members will be overwritten.` +
pujal0909 marked this conversation as resolved.
Show resolved Hide resolved
` Are you sure you want to continue? [y/N]: `
);
expect(result).toBe(false);
Expand Down
15 changes: 13 additions & 2 deletions packages/cli/src/zosfiles/copy/ds/Ds.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export default class DsHandler extends ZosFilesBaseHandler {
replace: commandParameters.arguments.replace,
responseTimeout: commandParameters.arguments.responseTimeout,
safeReplace: commandParameters.arguments.safeReplace,
promptFn: this.promptForSafeReplace(commandParameters.response.console)
promptFn: this.promptForSafeReplace(commandParameters.response.console),
promptForLikeNamedMembers: this.promptForLikeNamedMembers(commandParameters.response.console)
};

return Copy.dataSet(session, toDataSet, options);
Expand All @@ -35,10 +36,20 @@ export default class DsHandler extends ZosFilesBaseHandler {
private promptForSafeReplace(console: IHandlerResponseConsoleApi) {
return async (targetDSN: string) => {
const answer: string = await console.prompt(
`The dataset '${targetDSN}' exists on the target system. This copy will result in data loss.` +
`The dataset '${targetDSN}' exists on the target system. This copy may result in data loss.` +
pujal0909 marked this conversation as resolved.
Show resolved Hide resolved
` Are you sure you want to continue? [y/N]: `
);
return answer != null && (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
};
}

private promptForLikeNamedMembers(console: IHandlerResponseConsoleApi) {
return async() => {
const answer: string = await console.prompt (
`The source and target data sets have like named member names. The contents of those members will be overwritten.` +
pujal0909 marked this conversation as resolved.
Show resolved Hide resolved
` Are you sure you want to continue? [y/N]: `
)
return answer != null && (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
};
}
}
1 change: 1 addition & 0 deletions packages/zosfiles/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to the Zowe z/OS files SDK package will be documented in thi

## Recent Changes

- Enhancement: When copying PDSs with like-named members, the user is now prompted to confirm before the operation occurs in case of data loss. [#2349] (https://github.com/zowe/zowe-cli/issues/2349)
- BugFix: Fixed an issue in the `Copy.dataSetCrossLPAR()` function where the `spacu` attribute of the copied data set was always set to `TRK`, regardless of the source data set's attributes. [#2412](https://github.com/zowe/zowe-cli/issues/2412)
- BugFix: The `Copy.data.set` function now prompts the user to confirm before overwriting the contents of the target data set with the addition of the `--safe-replace` option. [#2369] (https://github.com/zowe/zowe-cli/issues/2369)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,40 @@ describe("Copy", () => {
});
});

describe("hasLikeNamedMembers", () => {
beforeEach(async () => {
try {
await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_PARTITIONED, fromDataSetName);
await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_PARTITIONED, toDataSetName);
await Upload.fileToDataset(REAL_SESSION, fileLocation, fromDataSetName);
await Upload.fileToDataset(REAL_SESSION, fileLocation, toDataSetName);
}
catch (err) {
Imperative.console.info(`Error: ${inspect(err)}`);
}
});
afterEach(async () => {
try {
await Delete.dataSet(REAL_SESSION, fromDataSetName);
await Delete.dataSet(REAL_SESSION, toDataSetName);
} catch (err) {
Imperative.console.info(`Error: ${inspect(err)}`);
}
});
it("should return true if the source and target data sets have like-named members", async () => {
const response = await Copy["hasLikeNamedMembers"](REAL_SESSION, fromDataSetName, toDataSetName);
expect(response).toBe(true);
});

it("should return false if the source and target data sets do not have like-named members", async () => {
await Delete.dataSet(REAL_SESSION, toDataSetName);
await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_PARTITIONED, toDataSetName);

const response = await Copy["hasLikeNamedMembers"](REAL_SESSION, fromDataSetName, toDataSetName);
expect(response).toBe(false);
});
});

describe("Data Set Cross LPAR", () => {
describe("Common Failures", () => {
it("should fail if no fromDataSet data set name is supplied", async () => {
Expand Down
118 changes: 116 additions & 2 deletions packages/zosfiles/__tests__/__unit__/methods/copy/Copy.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,18 @@ describe("Copy", () => {
const toDataSetName = "USER.DATA.TO";
const toMemberName = "mem2";
const isPDSSpy = jest.spyOn(Copy as any, "isPDS");
const hasLikeNamedMembers = jest.spyOn(Copy as any, "hasLikeNamedMembers");
let dataSetExistsSpy: jest.SpyInstance;
const promptFn = jest.fn();
const promptForLikeNamedMembers = jest.fn();

beforeEach(() => {
copyPDSSpy.mockClear();
copyExpectStringSpy.mockClear().mockImplementation(async () => { return ""; });
isPDSSpy.mockClear().mockResolvedValue(false);
dataSetExistsSpy = jest.spyOn(Copy as any, "dataSetExists").mockResolvedValue(true);

hasLikeNamedMembers.mockClear().mockResolvedValue(false);
promptForLikeNamedMembers.mockClear();
});
afterAll(() => {
isPDSSpy.mockRestore();
Expand Down Expand Up @@ -619,6 +622,49 @@ describe("Copy", () => {
commandResponse: ZosFilesMessages.datasetCopiedSuccessfully.message
});
});
it("should display a prompt for like named members if there are duplicate member names and --safe-replace and --replace flags are not used", async () => {
pujal0909 marked this conversation as resolved.
Show resolved Hide resolved
hasLikeNamedMembers.mockResolvedValue(true);
promptForLikeNamedMembers.mockClear().mockResolvedValue(true);

const response = await Copy.dataSet(
Fixed Show fixed Hide fixed
dummySession,
{ dsn: toDataSetName },
{ "from-dataset": { dsn: fromDataSetName },
safeReplace: false,
replace: false,
promptForLikeNamedMembers }
);
expect(promptForLikeNamedMembers).toHaveBeenCalledWith();
expect(response.success).toEqual(true);

});
it("should not display a prompt for like named members if there are no duplicate member names", async () => {
const response = await Copy.dataSet(
Fixed Show fixed Hide fixed
dummySession,
{ dsn: toDataSetName },
{ "from-dataset": { dsn: fromDataSetName },
safeReplace: false,
replace: false,
promptForLikeNamedMembers }
);
expect(response.success).toEqual(true);
expect(promptForLikeNamedMembers).not.toHaveBeenCalled();
});
it("should throw error if user declines to replace the dataset", async () => {
hasLikeNamedMembers.mockResolvedValue(true);
promptForLikeNamedMembers.mockClear().mockResolvedValue(false);

await expect(Copy.dataSet(
dummySession,
{ dsn: toDataSetName },
{ "from-dataset": { dsn: fromDataSetName },
safeReplace: false,
replace: false,
promptForLikeNamedMembers }
)).rejects.toThrow(new ImperativeError({ msg: ZosFilesMessages.datasetCopiedAborted.message }));

expect(promptForLikeNamedMembers).toHaveBeenCalled();
});
});
it("should return early if the source and target data sets are identical", async () => {
const response = await Copy.dataSet(
Expand Down Expand Up @@ -711,7 +757,7 @@ describe("Copy", () => {
});
});

describe("Copy Partitioned Data Set", () => {
describe("Partitioned Data Set", () => {
const listAllMembersSpy = jest.spyOn(List, "allMembers");
const downloadAllMembersSpy = jest.spyOn(Download, "allMembers");
const uploadSpy = jest.spyOn(Upload, "streamToDataSet");
Expand All @@ -722,6 +768,11 @@ describe("Copy", () => {
const readStream = jest.spyOn(IO, "createReadStream");
const rmSync = jest.spyOn(fs, "rmSync");
const listDatasetSpy = jest.spyOn(List, "dataSet");
const hasLikeNamedMembers = jest.spyOn(Copy as any, "hasLikeNamedMembers");

beforeEach(() => {
hasLikeNamedMembers.mockRestore();
});

const dsPO = {
dsname: fromDataSetName,
Expand Down Expand Up @@ -849,6 +900,69 @@ describe("Copy", () => {
commandResponse: ZosFilesMessages.datasetCopiedSuccessfully.message,
});
});

describe("hasLikeNamedMembers", () => {
const listAllMembersSpy = jest.spyOn(List, "allMembers");

beforeEach(() => {
jest.clearAllMocks();
});
it("should return true if the source and target have like-named members", async () => {
listAllMembersSpy.mockImplementation(async (session, dsName): Promise<any> => {
if (dsName === fromDataSetName) {
return {
apiResponse: {
items: [
{ member: "mem1" },
{ member: "mem2" },
]
}
};
} else if (dsName === toDataSetName) {
return {
apiResponse: {
items: [{ member: "mem1" }]
}
};
}
});

const response = await Copy["hasLikeNamedMembers"](dummySession, fromDataSetName, toDataSetName);
expect(response).toBe(true);
expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, fromDataSetName);
expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, toDataSetName);
});
it("should return false if the source and target do not have like-named members", async () => {
const sourceResponse = {
apiResponse: {
items: [
{ member: "mem1" },
{ member: "mem2" },
]
}
};
const targetResponse = {
apiResponse: {
items: [
{ member: "mem3" },
]
}
};
listAllMembersSpy.mockImplementation(async (session, dsName): Promise<any> => {
if (dsName === fromDataSetName) {
return sourceResponse;
} else if (dsName === toDataSetName) {
return targetResponse;
}
});

const response = await Copy["hasLikeNamedMembers"](dummySession, fromDataSetName, toDataSetName);

expect(response).toBe(false);
expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, fromDataSetName);
expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, toDataSetName);
});
});
});

describe("Data Set Cross LPAR", () => {
Expand Down
1 change: 0 additions & 1 deletion packages/zosfiles/src/constants/ZosFiles.messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ export const ZosFilesMessages: { [key: string]: IMessageDefinition } = {
message: "Member(s) downloaded successfully."
},


/**
* Message indicating that the member was downloaded successfully
* @type {IMessageDefinition}
Expand Down
Loading