Skip to content

Commit 5d6b77b

Browse files
committed
Refactor setup process for improved clarity: Consolidate file copying and directory creation into a single function, enhancing logging to include details on skipped files for better user feedback.
1 parent 55aff4e commit 5d6b77b

18 files changed

Lines changed: 1619 additions & 0 deletions
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { CreateReleaseUseCase } from '../create_release_use_case';
2+
import { Result } from '../../../data/model/result';
3+
import { INPUT_KEYS } from '../../../utils/constants';
4+
import type { Execution } from '../../../data/model/execution';
5+
6+
jest.mock('../../../utils/logger', () => ({
7+
logInfo: jest.fn(),
8+
logError: jest.fn(),
9+
}));
10+
11+
jest.mock('../../../utils/task_emoji', () => ({
12+
getTaskEmoji: jest.fn(() => '🚀'),
13+
}));
14+
15+
const mockCreateRelease = jest.fn();
16+
jest.mock('../../../data/repository/project_repository', () => ({
17+
ProjectRepository: jest.fn().mockImplementation(() => ({
18+
createRelease: mockCreateRelease,
19+
})),
20+
}));
21+
22+
function baseParam(overrides: Record<string, unknown> = {}): Execution {
23+
return {
24+
owner: 'owner',
25+
repo: 'repo',
26+
tokens: { token: 'token' },
27+
singleAction: {
28+
version: '1.0.0',
29+
title: 'Release title',
30+
changelog: '- Fix bug',
31+
},
32+
currentConfiguration: {},
33+
...overrides,
34+
} as unknown as Execution;
35+
}
36+
37+
describe('CreateReleaseUseCase', () => {
38+
let useCase: CreateReleaseUseCase;
39+
40+
beforeEach(() => {
41+
useCase = new CreateReleaseUseCase();
42+
mockCreateRelease.mockReset();
43+
});
44+
45+
it('returns failure when version is empty', async () => {
46+
const param = baseParam({ singleAction: { version: '', title: 't', changelog: 'c' } });
47+
const results = await useCase.invoke(param);
48+
expect(results).toHaveLength(1);
49+
expect(results[0].success).toBe(false);
50+
expect(results[0].errors?.some((e) => String(e).includes(INPUT_KEYS.SINGLE_ACTION_VERSION))).toBe(true);
51+
});
52+
53+
it('returns failure when title is empty', async () => {
54+
const param = baseParam({ singleAction: { version: '1.0.0', title: '', changelog: 'c' } });
55+
const results = await useCase.invoke(param);
56+
expect(results.length).toBeGreaterThanOrEqual(1);
57+
expect(results.some((r) => r.errors?.some((e) => String(e).includes(`${INPUT_KEYS.SINGLE_ACTION_TITLE} is not set.`)))).toBe(true);
58+
});
59+
60+
it('returns failure when changelog is empty', async () => {
61+
const param = baseParam({ singleAction: { version: '1.0.0', title: 't', changelog: '' } });
62+
const results = await useCase.invoke(param);
63+
expect(results.length).toBeGreaterThanOrEqual(1);
64+
expect(results.some((r) => r.errors?.some((e) => String(e).includes(`${INPUT_KEYS.SINGLE_ACTION_CHANGELOG} is not set.`)))).toBe(true);
65+
});
66+
67+
it('returns success with release URL when createRelease succeeds', async () => {
68+
mockCreateRelease.mockResolvedValue('https://github.com/owner/repo/releases/tag/v1.0.0');
69+
const param = baseParam();
70+
const results = await useCase.invoke(param);
71+
expect(results).toHaveLength(1);
72+
expect(results[0]).toBeInstanceOf(Result);
73+
expect(results[0].success).toBe(true);
74+
expect(results[0].steps?.some((s) => s.includes('Created release'))).toBe(true);
75+
expect(mockCreateRelease).toHaveBeenCalledWith(
76+
'owner',
77+
'repo',
78+
'1.0.0',
79+
'Release title',
80+
'- Fix bug',
81+
'token'
82+
);
83+
});
84+
85+
it('returns failure when createRelease returns null', async () => {
86+
mockCreateRelease.mockResolvedValue(null);
87+
const param = baseParam();
88+
const results = await useCase.invoke(param);
89+
expect(results[0].success).toBe(false);
90+
expect(results[0].errors).toContain('Failed to create release.');
91+
});
92+
93+
it('returns failure and catches error when createRelease throws', async () => {
94+
mockCreateRelease.mockRejectedValue(new Error('API error'));
95+
const param = baseParam();
96+
const results = await useCase.invoke(param);
97+
expect(results[0].success).toBe(false);
98+
expect(results[0].errors?.length).toBeGreaterThan(0);
99+
});
100+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { CreateTagUseCase } from '../create_tag_use_case';
2+
import { Result } from '../../../data/model/result';
3+
import { INPUT_KEYS } from '../../../utils/constants';
4+
import type { Execution } from '../../../data/model/execution';
5+
6+
jest.mock('../../../utils/logger', () => ({
7+
logInfo: jest.fn(),
8+
logError: jest.fn(),
9+
}));
10+
11+
jest.mock('../../../utils/task_emoji', () => ({
12+
getTaskEmoji: jest.fn(() => '🏷️'),
13+
}));
14+
15+
const mockCreateTag = jest.fn();
16+
jest.mock('../../../data/repository/project_repository', () => ({
17+
ProjectRepository: jest.fn().mockImplementation(() => ({
18+
createTag: mockCreateTag,
19+
})),
20+
}));
21+
22+
function baseParam(overrides: Record<string, unknown> = {}): Execution {
23+
return {
24+
owner: 'owner',
25+
repo: 'repo',
26+
tokens: { token: 'token' },
27+
singleAction: { version: '1.0.0' },
28+
currentConfiguration: { releaseBranch: 'release/1.0.0' },
29+
...overrides,
30+
} as unknown as Execution;
31+
}
32+
33+
describe('CreateTagUseCase', () => {
34+
let useCase: CreateTagUseCase;
35+
36+
beforeEach(() => {
37+
useCase = new CreateTagUseCase();
38+
mockCreateTag.mockReset();
39+
});
40+
41+
it('returns failure when version is empty', async () => {
42+
const param = baseParam({ singleAction: { version: '' } });
43+
const results = await useCase.invoke(param);
44+
expect(results).toHaveLength(1);
45+
expect(results[0].success).toBe(false);
46+
expect(results[0].errors).toContain(`${INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`);
47+
});
48+
49+
it('returns failure when releaseBranch is undefined', async () => {
50+
const param = baseParam({ currentConfiguration: { releaseBranch: undefined } });
51+
const results = await useCase.invoke(param);
52+
expect(results).toHaveLength(1);
53+
expect(results[0].success).toBe(false);
54+
expect(results[0].errors).toContain('Release branch not found in issue configuration.');
55+
});
56+
57+
it('returns success with step when createTag succeeds', async () => {
58+
mockCreateTag.mockResolvedValue('abc123');
59+
const param = baseParam();
60+
const results = await useCase.invoke(param);
61+
expect(results).toHaveLength(1);
62+
expect(results[0]).toBeInstanceOf(Result);
63+
expect(results[0].success).toBe(true);
64+
expect(results[0].steps?.some((s) => s.includes('1.0.0') && s.includes('abc123'))).toBe(true);
65+
expect(mockCreateTag).toHaveBeenCalledWith(
66+
'owner',
67+
'repo',
68+
'release/1.0.0',
69+
'1.0.0',
70+
'token'
71+
);
72+
});
73+
74+
it('returns failure when createTag returns null', async () => {
75+
mockCreateTag.mockResolvedValue(null);
76+
const param = baseParam();
77+
const results = await useCase.invoke(param);
78+
expect(results[0].success).toBe(false);
79+
expect(results[0].errors?.some((e) => String(e).includes('Failed to create tag'))).toBe(true);
80+
});
81+
82+
it('returns failure when createTag throws', async () => {
83+
mockCreateTag.mockRejectedValue(new Error('API error'));
84+
const param = baseParam();
85+
const results = await useCase.invoke(param);
86+
expect(results[0].success).toBe(false);
87+
});
88+
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { InitialSetupUseCase } from '../initial_setup_use_case';
2+
import { Result } from '../../../data/model/result';
3+
import type { Execution } from '../../../data/model/execution';
4+
5+
jest.mock('../../../utils/logger', () => ({
6+
logInfo: jest.fn(),
7+
logError: jest.fn(),
8+
}));
9+
10+
jest.mock('../../../utils/task_emoji', () => ({
11+
getTaskEmoji: jest.fn(() => '📋'),
12+
}));
13+
14+
const mockEnsureGitHubDirs = jest.fn();
15+
const mockCopySetupFiles = jest.fn();
16+
jest.mock('../../../utils/setup_files', () => ({
17+
ensureGitHubDirs: (...args: unknown[]) => mockEnsureGitHubDirs(...args),
18+
copySetupFiles: (...args: unknown[]) => mockCopySetupFiles(...args),
19+
}));
20+
21+
const mockGetUserFromToken = jest.fn();
22+
jest.mock('../../../data/repository/project_repository', () => ({
23+
ProjectRepository: jest.fn().mockImplementation(() => ({
24+
getUserFromToken: mockGetUserFromToken,
25+
})),
26+
}));
27+
28+
const mockEnsureLabels = jest.fn();
29+
const mockEnsureProgressLabels = jest.fn();
30+
const mockEnsureIssueTypes = jest.fn();
31+
jest.mock('../../../data/repository/issue_repository', () => ({
32+
IssueRepository: jest.fn().mockImplementation(() => ({
33+
ensureLabels: mockEnsureLabels,
34+
ensureProgressLabels: mockEnsureProgressLabels,
35+
ensureIssueTypes: mockEnsureIssueTypes,
36+
})),
37+
}));
38+
39+
function baseParam(overrides: Record<string, unknown> = {}): Execution {
40+
return {
41+
owner: 'owner',
42+
repo: 'repo',
43+
tokens: { token: 'token' },
44+
labels: {},
45+
issueTypes: {},
46+
singleAction: {},
47+
currentConfiguration: {},
48+
branches: {},
49+
release: {},
50+
hotfix: {},
51+
issue: {},
52+
pullRequest: {},
53+
workflows: {},
54+
project: { getProjects: () => [], getProjectColumnIssueCreated: () => '', getProjectColumnIssueInProgress: () => '' },
55+
commit: {},
56+
commitPrefixBuilder: '',
57+
emoji: {},
58+
images: {},
59+
ai: {},
60+
locale: {},
61+
sizeThresholds: {},
62+
...overrides,
63+
} as unknown as Execution;
64+
}
65+
66+
describe('InitialSetupUseCase', () => {
67+
let useCase: InitialSetupUseCase;
68+
69+
beforeEach(() => {
70+
useCase = new InitialSetupUseCase();
71+
mockEnsureGitHubDirs.mockClear();
72+
mockCopySetupFiles.mockReturnValue({ copied: 2, skipped: 0 });
73+
mockGetUserFromToken.mockResolvedValue('test-user');
74+
mockEnsureLabels.mockResolvedValue({ success: true, created: 0, existing: 5, errors: [] });
75+
mockEnsureProgressLabels.mockResolvedValue({ created: 0, existing: 21, errors: [] });
76+
mockEnsureIssueTypes.mockResolvedValue({ success: true, created: 0, existing: 3, errors: [] });
77+
});
78+
79+
it('calls ensureGitHubDirs and copySetupFiles with process.cwd()', async () => {
80+
const param = baseParam();
81+
await useCase.invoke(param);
82+
expect(mockEnsureGitHubDirs).toHaveBeenCalledWith(process.cwd());
83+
expect(mockCopySetupFiles).toHaveBeenCalledWith(process.cwd());
84+
});
85+
86+
it('returns success and steps including setup files when all steps succeed', async () => {
87+
const param = baseParam();
88+
const results = await useCase.invoke(param);
89+
expect(results).toHaveLength(1);
90+
expect(results[0]).toBeInstanceOf(Result);
91+
expect(results[0].success).toBe(true);
92+
expect(results[0].steps?.some((s) => s.includes('Setup files'))).toBe(true);
93+
expect(results[0].steps?.some((s) => s.includes('GitHub access verified'))).toBe(true);
94+
expect(results[0].steps?.some((s) => s.includes('Labels checked'))).toBe(true);
95+
expect(results[0].steps?.some((s) => s.includes('Progress labels'))).toBe(true);
96+
expect(results[0].steps?.some((s) => s.includes('Issue types'))).toBe(true);
97+
});
98+
99+
it('returns failure when verifyGitHubAccess fails', async () => {
100+
mockGetUserFromToken.mockRejectedValue(new Error('Invalid token'));
101+
const param = baseParam();
102+
const results = await useCase.invoke(param);
103+
expect(results).toHaveLength(1);
104+
expect(results[0].success).toBe(false);
105+
expect(results[0].errors?.length).toBeGreaterThan(0);
106+
});
107+
108+
it('continues and reports errors when ensureLabels fails', async () => {
109+
mockEnsureLabels.mockResolvedValue({ success: false, created: 0, existing: 0, errors: ['Label error'] });
110+
const param = baseParam();
111+
const results = await useCase.invoke(param);
112+
expect(results).toHaveLength(1);
113+
expect(results[0].success).toBe(false);
114+
expect(results[0].errors).toContain('Label error');
115+
});
116+
117+
it('continues and reports errors when ensureProgressLabels has errors', async () => {
118+
mockEnsureProgressLabels.mockResolvedValue({ created: 0, existing: 0, errors: ['Progress error'] });
119+
const param = baseParam();
120+
const results = await useCase.invoke(param);
121+
expect(results).toHaveLength(1);
122+
expect(results[0].success).toBe(false);
123+
expect(results[0].errors).toContain('Progress error');
124+
});
125+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { PublishGithubActionUseCase } from '../publish_github_action_use_case';
2+
import { Result } from '../../../data/model/result';
3+
import { INPUT_KEYS } from '../../../utils/constants';
4+
import type { Execution } from '../../../data/model/execution';
5+
6+
jest.mock('../../../utils/logger', () => ({
7+
logInfo: jest.fn(),
8+
logError: jest.fn(),
9+
}));
10+
11+
jest.mock('../../../utils/task_emoji', () => ({
12+
getTaskEmoji: jest.fn(() => '📦'),
13+
}));
14+
15+
const mockUpdateTag = jest.fn();
16+
const mockUpdateRelease = jest.fn();
17+
jest.mock('../../../data/repository/project_repository', () => ({
18+
ProjectRepository: jest.fn().mockImplementation(() => ({
19+
updateTag: mockUpdateTag,
20+
updateRelease: mockUpdateRelease,
21+
})),
22+
}));
23+
24+
function baseParam(overrides: Record<string, unknown> = {}): Execution {
25+
return {
26+
owner: 'owner',
27+
repo: 'repo',
28+
tokens: { token: 'token' },
29+
singleAction: { version: '1.2.3' },
30+
...overrides,
31+
} as unknown as Execution;
32+
}
33+
34+
describe('PublishGithubActionUseCase', () => {
35+
let useCase: PublishGithubActionUseCase;
36+
37+
beforeEach(() => {
38+
useCase = new PublishGithubActionUseCase();
39+
mockUpdateTag.mockResolvedValue(undefined);
40+
mockUpdateRelease.mockReset();
41+
});
42+
43+
it('returns failure when version is empty', async () => {
44+
const param = baseParam({ singleAction: { version: '' } });
45+
const results = await useCase.invoke(param);
46+
expect(results).toHaveLength(1);
47+
expect(results[0].success).toBe(false);
48+
expect(results[0].errors).toContain(`${INPUT_KEYS.SINGLE_ACTION_VERSION} is not set.`);
49+
});
50+
51+
it('calls updateTag with v{version} and major segment, then updateRelease', async () => {
52+
mockUpdateRelease.mockResolvedValue(12345);
53+
const param = baseParam({ singleAction: { version: '1.2.3' } });
54+
await useCase.invoke(param);
55+
expect(mockUpdateTag).toHaveBeenCalledWith('owner', 'repo', 'v1.2.3', 'v1', 'token');
56+
expect(mockUpdateRelease).toHaveBeenCalledWith('owner', 'repo', 'v1.2.3', 'v1', 'token');
57+
});
58+
59+
it('returns success when updateRelease returns truthy', async () => {
60+
mockUpdateRelease.mockResolvedValue(999);
61+
const param = baseParam();
62+
const results = await useCase.invoke(param);
63+
expect(results).toHaveLength(1);
64+
expect(results[0]).toBeInstanceOf(Result);
65+
expect(results[0].success).toBe(true);
66+
expect(results[0].steps?.some((s) => s.includes('v1.2.3') && s.includes('v1'))).toBe(true);
67+
});
68+
69+
it('returns failure when updateRelease returns falsy', async () => {
70+
mockUpdateRelease.mockResolvedValue(null);
71+
const param = baseParam();
72+
const results = await useCase.invoke(param);
73+
expect(results[0].success).toBe(false);
74+
expect(results[0].errors?.some((e) => String(e).includes('Failed to update release'))).toBe(true);
75+
});
76+
77+
it('returns failure when updateTag or updateRelease throws', async () => {
78+
mockUpdateTag.mockRejectedValue(new Error('Tag error'));
79+
const param = baseParam();
80+
const results = await useCase.invoke(param);
81+
expect(results[0].success).toBe(false);
82+
});
83+
});

0 commit comments

Comments
 (0)