Skip to content

Commit f3a0707

Browse files
Merge pull request #47 from contentstack/CL-1637
fix: error handling for creating new GitHub projects
2 parents 132b6d0 + 3f72e2c commit f3a0707

File tree

7 files changed

+322
-117
lines changed

7 files changed

+322
-117
lines changed

src/adapters/base-class.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import BaseClass from './base-class';
22
import { cliux as ux, ContentstackClient } from '@contentstack/cli-utilities';
33
import config from '../config';
4-
import exp from 'constants';
54

65
jest.mock('@contentstack/cli-utilities', () => ({
76
cliux: {

src/adapters/base-class.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { ApolloClient } from '@apollo/client/core';
1818
import { writeFileSync, existsSync, readFileSync } from 'fs';
1919
import { cliux as ux, ContentstackClient } from '@contentstack/cli-utilities';
2020

21-
import config from '../config';
2221
import { print, GraphqlApiClient, LogPolling, getOrganizations } from '../util';
2322
import {
2423
branchesQuery,
@@ -31,7 +30,6 @@ import {
3130
import {
3231
LogFn,
3332
ExitFn,
34-
Providers,
3533
ConfigType,
3634
AdapterConstructorInputs,
3735
EmitMessage,
@@ -148,31 +146,6 @@ export default class BaseClass {
148146
await this.initApolloClient();
149147
}
150148

151-
/**
152-
* @method selectProjectType - select project type/provider/adapter
153-
*
154-
* @return {*} {Promise<void>}
155-
* @memberof BaseClass
156-
*/
157-
async selectProjectType(): Promise<void> {
158-
const choices = [
159-
...map(config.supportedAdapters, (provider) => ({
160-
value: provider,
161-
name: `Continue with ${provider}`,
162-
})),
163-
{ value: 'FileUpload', name: 'Continue with FileUpload' },
164-
];
165-
166-
const selectedProvider: Providers = await ux.inquire({
167-
choices: choices,
168-
type: 'search-list',
169-
name: 'projectType',
170-
message: 'Choose a project type to proceed',
171-
});
172-
173-
this.config.provider = selectedProvider;
174-
}
175-
176149
/**
177150
* @method detectFramework - detect the project framework
178151
*
@@ -427,7 +400,6 @@ export default class BaseClass {
427400
* @memberof BaseClass
428401
*/
429402
async connectToAdapterOnUi(emit = true): Promise<void> {
430-
await this.selectProjectType();
431403

432404
if (includes(this.config.supportedAdapters, this.config.provider)) {
433405
const baseUrl = this.config.host.startsWith('http') ? this.config.host : `https://${this.config.host}`;
@@ -862,4 +834,4 @@ export default class BaseClass {
862834
});
863835
}
864836
}
865-
}
837+
}

src/adapters/github.test.ts

Lines changed: 151 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import GitHub from './github';
22
import { getRemoteUrls } from '../util/create-git-meta';
3-
import { repositoriesQuery } from '../graphql';
3+
import { repositoriesQuery, userConnectionsQuery } from '../graphql';
44
import BaseClass from './base-class';
5+
import { existsSync } from 'fs';
56

67
jest.mock('../util/create-git-meta');
8+
jest.mock('fs', () => ({
9+
...jest.requireActual('fs'),
10+
existsSync: jest.fn(),
11+
}));
712

13+
const userConnections = [
14+
{
15+
__typename: 'UserConnection',
16+
userUid: 'testuser1',
17+
provider: 'GitHub',
18+
},
19+
];
820
const repositories = [
921
{
1022
__typename: 'GitRepository',
@@ -33,24 +45,70 @@ const repositories = [
3345
];
3446

3547
describe('GitHub Adapter', () => {
36-
describe('checkGitRemoteAvailableAndValid', () => {
37-
const repositoriesResponse = { data: { repositories } };
38-
let logMock: jest.Mock;
39-
let exitMock: jest.Mock;
48+
let logMock: jest.Mock;
49+
let exitMock: jest.Mock;
4050

41-
beforeEach(() => {
42-
logMock = jest.fn();
43-
exitMock = jest.fn().mockImplementationOnce(() => {
44-
throw new Error('1');
45-
});
51+
beforeEach(() => {
52+
logMock = jest.fn();
53+
exitMock = jest.fn().mockImplementationOnce(() => {
54+
throw new Error('1');
4655
});
56+
});
57+
58+
afterEach(() => {
59+
jest.resetAllMocks();
60+
});
61+
62+
describe('checkGitHubConnected', () => {
63+
it('should return true if GitHub is connected', async () => {
64+
const userConnectionResponse = { data: { userConnections } };
65+
const apolloClient = {
66+
query: jest.fn().mockResolvedValueOnce(userConnectionResponse),
67+
} as any;
68+
const githubAdapterInstance = new GitHub({
69+
config: { projectBasePath: '/home/project1', provider: 'GitHub' },
70+
apolloClient: apolloClient,
71+
log: logMock,
72+
} as any);
73+
const connectToAdapterOnUiMock = jest
74+
.spyOn(BaseClass.prototype, 'connectToAdapterOnUi')
75+
.mockResolvedValueOnce(undefined);
76+
77+
await githubAdapterInstance.checkGitHubConnected();
78+
79+
expect(apolloClient.query).toHaveBeenCalledWith({ query: userConnectionsQuery });
80+
expect(logMock).toHaveBeenCalledWith('GitHub connection identified!', 'info');
81+
expect(githubAdapterInstance.config.userConnection).toEqual(userConnections[0]);
82+
expect(connectToAdapterOnUiMock).not.toHaveBeenCalled();
83+
});
84+
85+
it('should log an error and exit if GitHub is not connected', async () => {
86+
const userConnectionResponse = { data: { userConnections: [] } };
87+
const connectToAdapterOnUiMock = jest.spyOn(BaseClass.prototype, 'connectToAdapterOnUi').mockResolvedValueOnce();
88+
const apolloClient = {
89+
query: jest.fn().mockResolvedValueOnce(userConnectionResponse),
90+
} as any;
91+
const githubAdapterInstance = new GitHub({
92+
config: { projectBasePath: '/home/project1' },
93+
apolloClient: apolloClient,
94+
log: logMock,
95+
} as any);
96+
97+
await githubAdapterInstance.checkGitHubConnected();
4798

48-
afterEach(() => {
49-
jest.resetAllMocks();
99+
expect(apolloClient.query).toHaveBeenCalledWith({ query: userConnectionsQuery });
100+
expect(logMock).toHaveBeenCalledWith('GitHub connection not found!', 'error');
101+
expect(connectToAdapterOnUiMock).toHaveBeenCalled();
102+
expect(githubAdapterInstance.config.userConnection).toEqual(undefined);
50103
});
104+
});
105+
106+
describe('checkGitRemoteAvailableAndValid', () => {
107+
const repositoriesResponse = { data: { repositories } };
51108

52109
it(`should successfully check if the git remote is available and valid
53110
when the github remote URL is HTTPS based`, async () => {
111+
(existsSync as jest.Mock).mockReturnValueOnce(true);
54112
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
55113
origin: 'https://github.com/test-user/eleventy-sample.git',
56114
});
@@ -64,6 +122,7 @@ describe('GitHub Adapter', () => {
64122

65123
const result = await githubAdapterInstance.checkGitRemoteAvailableAndValid();
66124

125+
expect(existsSync).toHaveBeenCalledWith('/home/project1/.git');
67126
expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
68127
expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery });
69128
expect(githubAdapterInstance.config.repository).toEqual({
@@ -79,6 +138,7 @@ describe('GitHub Adapter', () => {
79138

80139
it(`should successfully check if the git remote is available and valid
81140
when the github remote URL is SSH based`, async () => {
141+
(existsSync as jest.Mock).mockReturnValueOnce(true);
82142
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
83143
origin: 'git@github.com:test-user/eleventy-sample.git',
84144
});
@@ -92,6 +152,7 @@ describe('GitHub Adapter', () => {
92152

93153
const result = await githubAdapterInstance.checkGitRemoteAvailableAndValid();
94154

155+
expect(existsSync).toHaveBeenCalledWith('/home/project1/.git');
95156
expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
96157
expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery });
97158
expect(githubAdapterInstance.config.repository).toEqual({
@@ -105,15 +166,39 @@ describe('GitHub Adapter', () => {
105166
expect(result).toBe(true);
106167
});
107168

108-
it(`should log an error and proceed to connection via UI
109-
if git repo remote url is unavailable and exit`, async () => {
169+
it('should log an error and exit if git config file does not exists', async () => {
170+
(existsSync as jest.Mock).mockReturnValueOnce(false);
171+
const githubAdapterInstance = new GitHub({
172+
config: { projectBasePath: '/home/project1' },
173+
log: logMock,
174+
exit: exitMock,
175+
} as any);
176+
let err;
177+
178+
try {
179+
await githubAdapterInstance.checkGitRemoteAvailableAndValid();
180+
} catch (error: any) {
181+
err = error;
182+
}
183+
184+
expect(getRemoteUrls as jest.Mock).not.toHaveBeenCalled();
185+
expect(logMock).toHaveBeenCalledWith('No Git repository configuration found at /home/project1.', 'error');
186+
expect(logMock).toHaveBeenCalledWith(
187+
'Please initialize a Git repository and try again, or use the File Upload option.',
188+
'error',
189+
);
190+
expect(exitMock).toHaveBeenCalledWith(1);
191+
expect(err).toEqual(new Error('1'));
192+
});
193+
194+
it(`should log an error if git repo remote url
195+
is unavailable and exit`, async () => {
196+
(existsSync as jest.Mock).mockReturnValueOnce(true);
110197
(getRemoteUrls as jest.Mock).mockResolvedValueOnce(undefined);
111-
const connectToAdapterOnUiMock
112-
= jest.spyOn(BaseClass.prototype, 'connectToAdapterOnUi').mockResolvedValueOnce(undefined);
113-
const githubAdapterInstance = new GitHub({
114-
config: { projectBasePath: '/home/project1' },
115-
log: logMock,
116-
exit: exitMock
198+
const githubAdapterInstance = new GitHub({
199+
config: { projectBasePath: '/home/project1' },
200+
log: logMock,
201+
exit: exitMock,
117202
} as any);
118203
let err;
119204

@@ -123,25 +208,61 @@ describe('GitHub Adapter', () => {
123208
err = error;
124209
}
125210

211+
expect(existsSync).toHaveBeenCalledWith('/home/project1/.git');
212+
expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
213+
expect(logMock).toHaveBeenCalledWith(
214+
`No Git remote origin URL found for the repository at /home/project1.
215+
Please add a git remote origin url and try again`,
216+
'error',
217+
);
218+
expect(exitMock).toHaveBeenCalledWith(1);
219+
expect(err).toEqual(new Error('1'));
220+
expect(githubAdapterInstance.config.repository).toBeUndefined();
221+
});
222+
223+
it('should log an error and exit if GitHub app is uninstalled', async () => {
224+
(existsSync as jest.Mock).mockReturnValueOnce(true);
225+
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
226+
origin: 'https://github.com/test-user/eleventy-sample.git',
227+
});
228+
const apolloClient = {
229+
query: jest.fn().mockRejectedValue(new Error('GitHub app error')),
230+
} as any;
231+
const connectToAdapterOnUiMock = jest.spyOn(BaseClass.prototype, 'connectToAdapterOnUi').mockResolvedValueOnce();
232+
const githubAdapterInstance = new GitHub({
233+
config: { projectBasePath: '/home/project1' },
234+
apolloClient: apolloClient,
235+
log: logMock,
236+
exit: exitMock,
237+
} as any);
238+
let err;
239+
240+
try {
241+
await githubAdapterInstance.checkGitRemoteAvailableAndValid();
242+
} catch (error: any) {
243+
err = error;
244+
}
126245

246+
expect(existsSync).toHaveBeenCalledWith('/home/project1/.git');
127247
expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
128-
expect(logMock).toHaveBeenCalledWith('GitHub project not identified!', 'error');
248+
expect(apolloClient.query).toHaveBeenCalled();
129249
expect(connectToAdapterOnUiMock).toHaveBeenCalled();
250+
expect(logMock).toHaveBeenCalledWith('GitHub app uninstalled. Please reconnect the app and try again', 'error');
130251
expect(exitMock).toHaveBeenCalledWith(1);
131252
expect(err).toEqual(new Error('1'));
132-
expect(githubAdapterInstance.config.repository).toBeUndefined();
133253
});
134254

135255
it('should log an error and exit if repository is not found in the list of available repositories', async () => {
256+
(existsSync as jest.Mock).mockReturnValueOnce(true);
136257
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
137258
origin: 'https://github.com/test-user/test-repo-2.git',
138259
});
139260
const apolloClient = {
140261
query: jest.fn().mockResolvedValueOnce(repositoriesResponse),
141262
} as any;
142-
const githubAdapterInstance = new GitHub({
143-
config: { projectBasePath: '/home/project1' },
144-
log: logMock,
263+
const githubAdapterInstance = new GitHub({
264+
config: { projectBasePath: '/home/project1' },
265+
log: logMock,
145266
exit: exitMock,
146267
apolloClient: apolloClient,
147268
} as any);
@@ -153,9 +274,13 @@ describe('GitHub Adapter', () => {
153274
err = error;
154275
}
155276

277+
expect(existsSync).toHaveBeenCalledWith('/home/project1/.git');
156278
expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
157279
expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery });
158-
expect(logMock).toHaveBeenCalledWith('Repository not found in the list!', 'error');
280+
expect(logMock).toHaveBeenCalledWith(
281+
'Repository not added to the GitHub app. Please add it to the app’s repository access list and try again.',
282+
'error',
283+
);
159284
expect(exitMock).toHaveBeenCalledWith(1);
160285
expect(err).toEqual(new Error('1'));
161286
expect(githubAdapterInstance.config.repository).toBeUndefined();

0 commit comments

Comments
 (0)