Skip to content
This repository was archived by the owner on Apr 13, 2020. It is now read-only.

Commit d61c19e

Browse files
[REFACTOR] commands/hld/init.ts and add tests (#197)
* replace any with interface * correct the type definition for IVariableGroupDataVariable[] * [REFACTOR] making code compact * add test to create-variable-group command * breaking larger function in scaffold class into smaller one, so we can add tests * simplier commander interface * using type guard in hasValue function * incorporate Evan's feedback * fix jsdoc * fixed the problem where definition.yaml file is not created * [REFACTOR] commands/hld/init.ts and add tests 1. move the command decoration out to a JSON file 2. standardized on command decorator function name, initCommandDecorator -> commandDecorator 3. added support for having default values in command builder 4. added tests to boost the coverage from 59% to 89% 5. create helper functions in tests so test code is more manage-able * modified code to make sure that log is flushed before the command exit * adding missing Promise<void> return type Co-authored-by: Yvonne Radsmikham <yvonne.radsmikham@gmail.com>
1 parent 17a1e2f commit d61c19e

File tree

7 files changed

+154
-83
lines changed

7 files changed

+154
-83
lines changed

src/commands/hld/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Command } from "../command";
2-
import { initCommandDecorator } from "./init";
2+
import { commandDecorator as initCommandDecorator } from "./init";
33
import { installHldToManifestPipelineDecorator } from "./pipeline";
44
import { reconcileHldDecorator } from "./reconcile";
55
export const hldCommand = Command(
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"command": "init",
3+
"alias": "i",
4+
"description": "Initialize High Level Definition repository. Add manifest-generation.yaml file to working directory/repository if it does not already exist.",
5+
"options": [
6+
{
7+
"arg": "--git-push",
8+
"description": "SPK CLI will try to commit and push these changes to a new origin/branch.",
9+
"required": false,
10+
"defaultValue": false
11+
}
12+
]
13+
}

src/commands/hld/init.test.ts

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
enableVerboseLogging,
99
logger
1010
} from "../../logger";
11-
import { initialize } from "./init";
11+
import { execute, initialize } from "./init";
1212
jest.mock("../../lib/gitutils");
1313

1414
beforeAll(() => {
@@ -19,53 +19,54 @@ afterAll(() => {
1919
disableVerboseLogging();
2020
});
2121

22-
describe("Initializing an HLD Repository.", () => {
23-
test("New directory is created under root directory with required service files.", async () => {
24-
// Create random directory to initialize
25-
const randomTmpDir = path.join(os.tmpdir(), uuid());
26-
fs.mkdirSync(randomTmpDir);
27-
28-
logger.info(`creating randomTmpDir ${randomTmpDir}`);
29-
30-
// addService call
31-
await initialize(randomTmpDir, false);
32-
33-
// Check temp test directory exists
34-
expect(fs.existsSync(randomTmpDir)).toBe(true);
35-
36-
// Verify new azure-pipelines created
37-
const filepaths = ["manifest-generation.yaml", "component.yaml"].map(
38-
filename => path.join(randomTmpDir, filename)
39-
);
40-
41-
for (const filepath of filepaths) {
42-
expect(fs.existsSync(filepath)).toBe(true);
43-
}
22+
const testExecuteFn = async (
23+
projectRootPath: string,
24+
gitPush: boolean,
25+
expectedExitCode: number
26+
) => {
27+
const exitFn = jest.fn();
28+
await execute(projectRootPath, gitPush, exitFn);
29+
expect(exitFn).toBeCalledTimes(1);
30+
expect(exitFn.mock.calls).toEqual([[expectedExitCode]]);
31+
};
32+
33+
describe("Test execute function", () => {
34+
it("simulate an error: missing project path", async () => {
35+
await testExecuteFn("", false, 1);
4436
});
37+
it("positive test", async () => {
38+
await testExecuteFn(os.tmpdir(), false, 0);
39+
});
40+
});
4541

46-
test("New directory is created and git push is enabled.", async () => {
47-
// Create random directory to initialize
48-
49-
const randomTmpDir = path.join(os.tmpdir(), uuid());
50-
fs.mkdirSync(randomTmpDir);
42+
const testRepoInitialization = async (gitPush: boolean) => {
43+
// Create random directory to initialize
44+
const randomTmpDir = path.join(os.tmpdir(), uuid());
45+
fs.mkdirSync(randomTmpDir);
5146

52-
logger.info(`creating randomTmpDir ${randomTmpDir}`);
47+
logger.info(`creating randomTmpDir ${randomTmpDir}`);
5348

54-
// addService call
55-
await initialize(randomTmpDir, true);
49+
// addService call
50+
await initialize(randomTmpDir, gitPush);
5651

57-
// Check temp test directory exists
58-
expect(fs.existsSync(randomTmpDir)).toBe(true);
52+
// Check temp test directory exists
53+
expect(fs.existsSync(randomTmpDir)).toBe(true);
5954

60-
// Verify new azure-pipelines created
61-
const filepaths = ["manifest-generation.yaml", "component.yaml"].map(
62-
filename => path.join(randomTmpDir, filename)
63-
);
55+
// Verify new azure-pipelines created
56+
["manifest-generation.yaml", "component.yaml"]
57+
.map(filename => path.join(randomTmpDir, filename))
58+
.forEach(filePath => {
59+
expect(fs.existsSync(filePath)).toBe(true);
60+
});
61+
};
6462

65-
for (const filepath of filepaths) {
66-
expect(fs.existsSync(filepath)).toBe(true);
67-
}
63+
describe("Initializing an HLD Repository.", () => {
64+
test("New directory is created under root directory with required service files.", async () => {
65+
await testRepoInitialization(false);
66+
});
6867

68+
test("New directory is created and git push is enabled.", async () => {
69+
await testRepoInitialization(true);
6970
expect(checkoutCommitPushCreatePRLink).toHaveBeenCalled();
7071
});
7172
});

src/commands/hld/init.ts

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,51 @@
11
import commander from "commander";
22

3+
import { build as buildCmd, exit as exitCmd } from "../../lib/commandBuilder";
34
import {
45
generateDefaultHldComponentYaml,
56
generateGitIgnoreFile,
67
generateHldAzurePipelinesYaml
78
} from "../../lib/fileutils";
89
import { checkoutCommitPushCreatePRLink } from "../../lib/gitutils";
10+
import { hasValue } from "../../lib/validator";
911
import { logger } from "../../logger";
12+
import decorator from "./init.decorator.json";
1013

11-
/**
12-
* Adds the init command to the hld command object
13-
*
14-
* @param command Commander command object to decorate
15-
*/
16-
export const initCommandDecorator = (command: commander.Command): void => {
17-
command
18-
.command("init")
19-
.alias("i")
20-
.description(
21-
"Initialize your hld repository. Will add the manifest-generation.yaml file to your working directory/repository if it does not already exist."
22-
)
23-
.option(
24-
"--git-push",
25-
"SPK CLI will try to commit and push these changes to a new origin/branch.",
26-
false
27-
)
28-
.action(async opts => {
29-
const { gitPush = false } = opts;
30-
const projectPath = process.cwd();
31-
try {
32-
// Type check all parsed command line args here.
33-
if (typeof gitPush !== "boolean") {
34-
throw new Error(
35-
`gitPush must be of type boolean, ${typeof gitPush} given.`
36-
);
37-
}
38-
await initialize(projectPath, gitPush);
39-
} catch (err) {
40-
logger.error(
41-
`Error occurred while initializing hld repository ${projectPath}`
42-
);
43-
logger.error(err);
44-
}
14+
// values that we need to pull out from command operator
15+
interface ICommandOptions {
16+
gitPush: boolean;
17+
}
18+
19+
export const execute = async (
20+
projectPath: string,
21+
gitPush: boolean,
22+
exitFn: (status: number) => Promise<void>
23+
) => {
24+
try {
25+
if (!hasValue(projectPath)) {
26+
throw new Error("project path is not provided");
27+
}
28+
await initialize(projectPath, gitPush);
29+
await exitFn(0);
30+
} catch (err) {
31+
logger.error(
32+
`Error occurred while initializing hld repository ${projectPath}`
33+
);
34+
logger.error(err);
35+
await exitFn(1);
36+
}
37+
};
38+
39+
export const commandDecorator = (command: commander.Command): void => {
40+
buildCmd(command, decorator).action(async (opts: ICommandOptions) => {
41+
const projectPath = process.cwd();
42+
// gitPush will is always true or false. It shall not be
43+
// undefined because default value is set in the commander decorator
44+
await execute(projectPath, opts.gitPush, async (status: number) => {
45+
await exitCmd(logger);
46+
process.exit(status);
4547
});
48+
});
4649
};
4750

4851
export const initialize = async (rootProjectPath: string, gitPush: boolean) => {

src/commands/project/create-variable-group.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { echo } from "shelljs";
66
import { Bedrock, Config, readYaml, write } from "../../config";
77
import {
88
build as buildCmd,
9+
exit as exitCmd,
910
validateForRequiredValues
1011
} from "../../lib/commandBuilder";
1112
import { IAzureDevOpsOpts } from "../../lib/git";
@@ -72,7 +73,7 @@ export const validateRequiredArguments = (
7273
export const execute = async (
7374
variableGroupName: string,
7475
opts: ICommandOptions,
75-
exitFn: (status: number) => void
76+
exitFn: (status: number) => Promise<void>
7677
) => {
7778
if (!hasValue(variableGroupName)) {
7879
exitFn(1);
@@ -109,7 +110,7 @@ export const execute = async (
109110
);
110111

111112
if (errors.length !== 0) {
112-
exitFn(1);
113+
await exitFn(1);
113114
} else {
114115
const variableGroup = await create(
115116
variableGroupName,
@@ -134,12 +135,12 @@ export const execute = async (
134135
logger.info(
135136
"Successfully created a variable group in Azure DevOps project!"
136137
);
137-
exitFn(0);
138+
await exitFn(0);
138139
}
139140
} catch (err) {
140141
logger.error(`Error occurred while creating variable group`);
141142
logger.error(err);
142-
exitFn(1);
143+
await exitFn(1);
143144
}
144145
}
145146
};
@@ -152,7 +153,10 @@ export const execute = async (
152153
export const commandDecorator = (command: commander.Command): void => {
153154
buildCmd(command, decorator).action(
154155
async (variableGroupName: string, opts: ICommandOptions) => {
155-
await execute(variableGroupName, opts, process.exit);
156+
await execute(variableGroupName, opts, async (status: number) => {
157+
await exitCmd(logger);
158+
process.exit(status);
159+
});
156160
}
157161
);
158162
};

src/lib/commandBuilder.test.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import commander from "commander";
2+
import { logger } from "../logger";
23
import {
34
build,
5+
exit as exitCmd,
46
ICommandBuildElements,
57
validateForRequiredValues
68
} from "./commandBuilder";
79

810
interface ICommandOption {
911
flags: string;
1012
description: string;
13+
defaultValue: string | boolean;
1114
}
1215

1316
describe("Tests Command Builder's build function", () => {
@@ -31,6 +34,18 @@ describe("Tests Command Builder's build function", () => {
3134
arg: "-c, --option-c <optionC>",
3235
description: "description for optionC",
3336
required: false
37+
},
38+
{
39+
arg: "-d, --option-d <optionD>",
40+
defaultValue: false,
41+
description: "description for optionD",
42+
required: false
43+
},
44+
{
45+
arg: "-e, --option-d <optionE>",
46+
defaultValue: "test",
47+
description: "description for optionE",
48+
required: false
3449
}
3550
]
3651
};
@@ -39,9 +54,15 @@ describe("Tests Command Builder's build function", () => {
3954

4055
expect(cmd.description()).toBe("description of command");
4156
expect(cmd.alias()).toBe("cbt");
57+
4258
cmd.options.forEach((opt: ICommandOption, i: number) => {
43-
expect(opt.flags).toBe(descriptor.options[i].arg);
44-
expect(opt.description).toBe(descriptor.options[i].description);
59+
const declared = descriptor.options[i];
60+
expect(opt.flags).toBe(declared.arg);
61+
expect(opt.description).toBe(declared.description);
62+
63+
if (declared.defaultValue !== undefined) {
64+
expect(opt.defaultValue).toBe(declared.defaultValue);
65+
}
4566
});
4667
});
4768
});
@@ -82,3 +103,12 @@ describe("Tests Command Builder's validation function", () => {
82103
expect(errors[0]).toBe("-c --option-c <optionC>");
83104
});
84105
});
106+
107+
describe("Tests Command Builder's exit function", () => {
108+
it("calling exit function", () => {
109+
jest.spyOn(logger, "info");
110+
exitCmd(logger).then(() => {
111+
expect(logger.info).toBeCalledTimes(1);
112+
});
113+
});
114+
});

src/lib/commandBuilder.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import commander from "commander";
2+
import { Logger } from "winston";
23
import { logger } from "../logger";
34
import { hasValue } from "./validator";
45

@@ -9,6 +10,7 @@ export interface ICommandOption {
910
arg: string;
1011
description: string;
1112
required: boolean;
13+
defaultValue?: string | boolean;
1214
}
1315

1416
/**
@@ -43,7 +45,11 @@ export const build = (
4345
.description(decorator.description);
4446

4547
decorator.options.forEach(opt => {
46-
cmd.option(opt.arg, opt.description);
48+
if (opt.defaultValue !== undefined) {
49+
cmd.option(opt.arg, opt.description, opt.defaultValue);
50+
} else {
51+
cmd.option(opt.arg, opt.description);
52+
}
4753
});
4854

4955
return cmd;
@@ -96,3 +102,17 @@ export const validateForRequiredValues = (
96102
}
97103
return errors;
98104
};
105+
106+
/**
107+
* Flushs the log, ready to exit the command.
108+
* In future there may be other housekeeper tasks.
109+
*
110+
* @param log Logger instance
111+
*/
112+
export const exit = (log: Logger): Promise<void> => {
113+
return new Promise(resolve => {
114+
log.info("", null, () => {
115+
resolve();
116+
});
117+
});
118+
};

0 commit comments

Comments
 (0)