Skip to content

Commit c844431

Browse files
feat(commands): add command to delete project
1 parent 00397c7 commit c844431

File tree

21 files changed

+693
-41
lines changed

21 files changed

+693
-41
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import chalk from 'chalk';
2+
import * as inquirer from 'inquirer';
3+
import emoji from 'node-emoji';
4+
5+
import { DeleteAction } from '../../actions';
6+
import * as questionBuilder from '../../core/project/builder/questions.builder';
7+
import { Project } from '../../core/project/project.entity';
8+
import { ERROR_PREFIX } from '../../core/ui';
9+
10+
jest.mock('../../core/project/persistence/repository.factory');
11+
jest.mock('../../core/project/builder/questions.builder');
12+
jest.mock('inquirer');
13+
14+
const buildDeleteProjectConfirmationQuestion = questionBuilder.buildDeleteProjectConfirmationQuestion as jest.MockedFunction<
15+
typeof questionBuilder.buildDeleteProjectConfirmationQuestion
16+
>;
17+
const repositoryMock = {
18+
findOneOrFail: jest.fn(),
19+
delete: jest.fn(),
20+
};
21+
22+
const prompt = inquirer.prompt as jest.MockedFunction<inquirer.PromptModule>;
23+
24+
describe('With action', () => {
25+
describe('Setup', () => {
26+
it('should return a function calling handle with inputs', () => {
27+
const action = new DeleteAction(repositoryMock as any);
28+
action.handle = jest.fn();
29+
const setup = action.setup();
30+
31+
setup('alias');
32+
33+
expect(action.handle).toHaveBeenCalledTimes(1);
34+
expect(action.handle).toHaveBeenCalledWith([
35+
{ name: 'alias', value: 'alias' },
36+
]);
37+
});
38+
});
39+
40+
describe('Handle', () => {
41+
let action: DeleteAction;
42+
43+
beforeAll(() => {
44+
action = new DeleteAction(repositoryMock as any);
45+
});
46+
47+
describe('Without alias on inputs', () => {
48+
it('should throw error', () => {
49+
expect(() => action.handle([])).rejects.toThrowError(
50+
'No alias found in command input',
51+
);
52+
});
53+
});
54+
55+
describe('With alias', () => {
56+
const alias = 'funniest-project';
57+
58+
beforeAll(() => {
59+
global.console.error = jest.fn();
60+
global.console.info = jest.fn();
61+
});
62+
63+
describe('When project not exists', () => {
64+
const errorMessage = `\n${ERROR_PREFIX} Not found a project with alias: ${chalk.red(
65+
alias,
66+
)}\n`;
67+
beforeAll(async () => {
68+
repositoryMock.findOneOrFail.mockRejectedValue(
69+
new Error(errorMessage),
70+
);
71+
});
72+
73+
it('should throw error', async () => {
74+
action = new DeleteAction(repositoryMock as any);
75+
await expect(() =>
76+
action.handle([
77+
{
78+
name: 'alias',
79+
value: alias,
80+
},
81+
]),
82+
).rejects.toThrowError(errorMessage);
83+
});
84+
85+
it('should call repository passing alias', () => {
86+
expect(repositoryMock.findOneOrFail).toHaveBeenCalledTimes(1);
87+
expect(repositoryMock.findOneOrFail).toHaveBeenCalledWith(alias);
88+
});
89+
});
90+
91+
describe('For existent project', () => {
92+
const confirmQuestion: any[] = [];
93+
94+
let project: Project;
95+
96+
beforeAll(async () => {
97+
project = new Project(alias, 'test');
98+
99+
repositoryMock.findOneOrFail.mockResolvedValue(project);
100+
101+
buildDeleteProjectConfirmationQuestion.mockReturnValue(
102+
confirmQuestion,
103+
);
104+
prompt.mockResolvedValue({
105+
shouldDelete: true,
106+
});
107+
108+
action = new DeleteAction(repositoryMock as any);
109+
await action.handle([
110+
{
111+
name: 'alias',
112+
value: alias,
113+
},
114+
]);
115+
});
116+
117+
it('should ask confirmation', () => {
118+
expect(buildDeleteProjectConfirmationQuestion).toHaveBeenCalledTimes(
119+
1,
120+
);
121+
expect(buildDeleteProjectConfirmationQuestion).toHaveBeenCalledWith(
122+
project,
123+
);
124+
125+
expect(prompt).toHaveBeenCalledTimes(1);
126+
expect(prompt).toHaveBeenCalledWith(confirmQuestion);
127+
});
128+
129+
describe('When confirm deletion', () => {
130+
beforeAll(async () => {
131+
global.console.info = jest.fn();
132+
repositoryMock.delete.mockReset();
133+
134+
repositoryMock.findOneOrFail.mockResolvedValue(project);
135+
repositoryMock.delete.mockResolvedValue(true);
136+
buildDeleteProjectConfirmationQuestion.mockReturnValue(
137+
confirmQuestion,
138+
);
139+
140+
prompt.mockResolvedValue({
141+
shouldDelete: true,
142+
});
143+
144+
action = new DeleteAction(repositoryMock as any);
145+
await action.handle([
146+
{
147+
name: 'alias',
148+
value: alias,
149+
},
150+
]);
151+
});
152+
153+
it('should delete project', () => {
154+
expect(repositoryMock.delete).toHaveBeenCalledTimes(1);
155+
expect(repositoryMock.delete).toHaveBeenCalledWith(alias);
156+
});
157+
158+
describe('When delete with success', () => {
159+
it('should print success message', () => {
160+
expect(console.info).toHaveBeenNthCalledWith(
161+
1,
162+
`\n${emoji.get(
163+
'disappointed',
164+
)} The project "${alias}" is not fun anymore, it happens...\n`,
165+
);
166+
});
167+
});
168+
169+
describe('When delete fail', () => {
170+
beforeAll(async () => {
171+
global.console.info = jest.fn();
172+
173+
repositoryMock.findOneOrFail.mockResolvedValue(project);
174+
repositoryMock.delete.mockResolvedValue(false);
175+
buildDeleteProjectConfirmationQuestion.mockReturnValue(
176+
confirmQuestion,
177+
);
178+
179+
prompt.mockResolvedValue({
180+
shouldDelete: true,
181+
});
182+
183+
action = new DeleteAction(repositoryMock as any);
184+
await action.handle([
185+
{
186+
name: 'alias',
187+
value: alias,
188+
},
189+
]);
190+
});
191+
192+
it('should print error message', () => {
193+
expect(console.error).toHaveBeenNthCalledWith(
194+
1,
195+
`\n${emoji.get(
196+
'disappointed_relieved',
197+
)} Whoops, something went wrong! It's not fun...\n`,
198+
);
199+
});
200+
201+
it('should print a joke', () => {
202+
expect(console.info).toHaveBeenNthCalledWith(
203+
1,
204+
`\n${emoji.get(
205+
'thought_balloon',
206+
)} Be positive! At least you can have fun with "${project.getAlias()} for the last time..."\n`,
207+
);
208+
});
209+
});
210+
});
211+
212+
describe('When not confirm deletion', () => {
213+
beforeAll(async () => {
214+
global.console.info = jest.fn();
215+
repositoryMock.delete.mockReset();
216+
217+
repositoryMock.findOneOrFail.mockResolvedValue(project);
218+
repositoryMock.delete.mockResolvedValue(true);
219+
buildDeleteProjectConfirmationQuestion.mockReturnValue(
220+
confirmQuestion,
221+
);
222+
223+
prompt.mockResolvedValue({
224+
shouldDelete: false,
225+
});
226+
227+
action = new DeleteAction(repositoryMock as any);
228+
await action.handle([
229+
{
230+
name: 'alias',
231+
value: alias,
232+
},
233+
]);
234+
});
235+
236+
it('should not delete project', () => {
237+
expect(repositoryMock.delete).not.toHaveBeenCalled();
238+
});
239+
240+
it('should print info message', () => {
241+
expect(console.info).toHaveBeenNthCalledWith(
242+
1,
243+
`\n${emoji.get('sweat_smile')} Phew, that was close...\n`,
244+
);
245+
});
246+
});
247+
});
248+
});
249+
});
250+
});

src/__tests__/commands/command.factory.spec.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { AddCommand } from '../../commands/add.command';
1+
import {
2+
AddCommand,
3+
DeleteCommand,
4+
ListCommand,
5+
WithCommand,
6+
} from '../../commands';
27
import {
38
createAddCommand,
9+
createDeleteCommand,
410
createListCommand,
511
createWithCommand,
612
} from '../../commands/command.factory';
7-
import { ListCommand } from '../../commands/list.command';
8-
import { WithCommand } from '../../commands/with.command';
913

1014
describe('Command factory', () => {
1115
describe('createAddCommand', () => {
@@ -24,6 +28,14 @@ describe('Command factory', () => {
2428
});
2529
});
2630

31+
describe('createDeleteCommand', () => {
32+
it('should return an instance of DeleteCommand', () => {
33+
const command = createDeleteCommand();
34+
35+
expect(command).toBeInstanceOf(DeleteCommand);
36+
});
37+
});
38+
2739
describe('createWithCommand', () => {
2840
const command = createWithCommand();
2941

src/__tests__/commands/command.loader.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CommanderStatic } from 'commander';
44
import {
55
AddCommand,
66
CommandLoader,
7+
DeleteCommand,
78
ListCommand,
89
WithCommand,
910
} from '../../commands';
@@ -21,6 +22,9 @@ const createWithCommand = commandsFactory.createWithCommand as jest.MockedFuncti
2122
const createListCommand = commandsFactory.createListCommand as jest.MockedFunction<
2223
typeof commandsFactory.createListCommand
2324
>;
25+
const createDeleteCommand = commandsFactory.createDeleteCommand as jest.MockedFunction<
26+
typeof commandsFactory.createDeleteCommand
27+
>;
2428

2529
describe('Command loader', () => {
2630
const addCommand: AddCommand = {
@@ -35,6 +39,10 @@ describe('Command loader', () => {
3539
load: jest.fn(),
3640
} as any;
3741

42+
const deleteCommand: DeleteCommand = {
43+
load: jest.fn(),
44+
} as any;
45+
3846
const program: CommanderStatic = {
3947
on: jest.fn(),
4048
args: ['a', 'b'],
@@ -76,6 +84,7 @@ describe('Command loader', () => {
7684
createAddCommand.mockReturnValue(addCommand);
7785
createWithCommand.mockReturnValue(withCommand);
7886
createListCommand.mockReturnValue(listCommand);
87+
createDeleteCommand.mockReturnValue(deleteCommand);
7988

8089
CommandLoader.invalidCommandHandler = jest
8190
.fn()
@@ -98,6 +107,11 @@ describe('Command loader', () => {
98107
expect(listCommand.load).toHaveBeenCalledWith(program);
99108
});
100109

110+
it('should load DeleteCommand', () => {
111+
expect(deleteCommand.load).toHaveBeenCalledTimes(1);
112+
expect(deleteCommand.load).toHaveBeenCalledWith(program);
113+
});
114+
101115
it('should register listener for invalid commands', () => {
102116
expect(program.on).toHaveBeenCalledTimes(1);
103117
expect(program.on).toHaveBeenCalledWith(
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { CommanderStatic } from 'commander';
2+
3+
import { DeleteAction } from '../../actions';
4+
import { DeleteCommand } from '../../commands';
5+
6+
describe('Delete command', () => {
7+
describe('load', () => {
8+
const program: CommanderStatic = {
9+
command: jest.fn().mockReturnThis(),
10+
allowUnknownOption: jest.fn().mockReturnThis(),
11+
description: jest.fn().mockReturnThis(),
12+
requiredOption: jest.fn().mockReturnThis(),
13+
usage: jest.fn().mockReturnThis(),
14+
action: jest.fn().mockReturnThis(),
15+
} as any;
16+
const action: DeleteAction = {
17+
setup: jest.fn().mockReturnValue(jest.fn()),
18+
} as any;
19+
20+
beforeAll(() => {
21+
const command = new DeleteCommand(action);
22+
command.load(program);
23+
});
24+
25+
it('should define the command', () => {
26+
expect(program.command).toHaveBeenCalledTimes(1);
27+
expect(program.command).toHaveBeenCalledWith('delete <project-alias>');
28+
});
29+
30+
it('should not allow unknown options', () => {
31+
expect(program.allowUnknownOption).toHaveBeenCalledTimes(1);
32+
expect(program.allowUnknownOption).toHaveBeenCalledWith(false);
33+
});
34+
35+
it('should define the description', () => {
36+
expect(program.description).toHaveBeenCalledTimes(1);
37+
expect(program.description).toHaveBeenCalledWith(
38+
'Removes a probable not fun project.',
39+
);
40+
});
41+
42+
it('should define the usage info', () => {
43+
expect(program.usage).toHaveBeenCalledTimes(1);
44+
expect(program.usage).toHaveBeenCalledWith('<project-alias>');
45+
});
46+
47+
it('should define the action calling DeleteAction setup', () => {
48+
expect(program.action).toHaveBeenCalledTimes(1);
49+
expect(program.action).toHaveBeenCalledWith(action.setup());
50+
});
51+
});
52+
});

0 commit comments

Comments
 (0)