Skip to content

Commit 24e3cc0

Browse files
Merge pull request #50 from contentstack/staging
Error handling for creating new GitHub projects, Support for validating git remote url for SSH format and update version in package.json file
2 parents 0ced2f1 + e2e4694 commit 24e3cc0

File tree

10 files changed

+533
-93
lines changed

10 files changed

+533
-93
lines changed

.talismanrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ fileignoreconfig:
22
- filename: .github/workflows/secrets-scan.yml
33
ignore_detectors:
44
- filecontent
5+
- filename: src/commands/launch/index.test.ts
6+
checksum: 9db6c02ad35a0367343cd753b916dd64db4a9efd24838201d2e1113ed19c9b62
57
version: "1.0"

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/cli-launch",
3-
"version": "1.9.0",
3+
"version": "1.9.1",
44
"description": "Launch related operations",
55
"author": "Contentstack CLI",
66
"bin": {

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: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import GitHub from './github';
2+
import { getRemoteUrls } from '../util/create-git-meta';
3+
import { repositoriesQuery, userConnectionsQuery } from '../graphql';
4+
import BaseClass from './base-class';
5+
import { existsSync } from 'fs';
6+
7+
jest.mock('../util/create-git-meta');
8+
jest.mock('fs', () => ({
9+
...jest.requireActual('fs'),
10+
existsSync: jest.fn(),
11+
}));
12+
13+
const userConnections = [
14+
{
15+
__typename: 'UserConnection',
16+
userUid: 'testuser1',
17+
provider: 'GitHub',
18+
},
19+
];
20+
const repositories = [
21+
{
22+
__typename: 'GitRepository',
23+
id: '495370701',
24+
url: 'https://github.com/test-user/nextjs-ssr-isr-demo',
25+
name: 'nextjs-ssr-isr-demo',
26+
fullName: 'test-user/nextjs-ssr-isr-demo',
27+
defaultBranch: 'main',
28+
},
29+
{
30+
__typename: 'GitRepository',
31+
id: '555341263',
32+
url: 'https://github.com/test-user/static-site-demo',
33+
name: 'static-site-demo',
34+
fullName: 'test-user/static-site-demo',
35+
defaultBranch: 'main',
36+
},
37+
{
38+
__typename: 'GitRepository',
39+
id: '647250661',
40+
url: 'https://github.com/test-user/eleventy-sample',
41+
name: 'eleventy-sample',
42+
fullName: 'test-user/eleventy-sample',
43+
defaultBranch: 'main',
44+
},
45+
];
46+
47+
describe('GitHub Adapter', () => {
48+
let logMock: jest.Mock;
49+
let exitMock: jest.Mock;
50+
51+
beforeEach(() => {
52+
logMock = jest.fn();
53+
exitMock = jest.fn().mockImplementationOnce(() => {
54+
throw new Error('1');
55+
});
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();
98+
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);
103+
});
104+
});
105+
106+
describe('checkGitRemoteAvailableAndValid', () => {
107+
const repositoriesResponse = { data: { repositories } };
108+
109+
it(`should successfully check if the git remote is available and valid
110+
when the github remote URL is HTTPS based`, async () => {
111+
(existsSync as jest.Mock).mockReturnValueOnce(true);
112+
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
113+
origin: 'https://github.com/test-user/eleventy-sample.git',
114+
});
115+
const apolloClient = {
116+
query: jest.fn().mockResolvedValueOnce(repositoriesResponse),
117+
} as any;
118+
const githubAdapterInstance = new GitHub({
119+
config: { projectBasePath: '/home/project1' },
120+
apolloClient: apolloClient,
121+
} as any);
122+
123+
const result = await githubAdapterInstance.checkGitRemoteAvailableAndValid();
124+
125+
expect(existsSync).toHaveBeenCalledWith('/home/project1/.git');
126+
expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
127+
expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery });
128+
expect(githubAdapterInstance.config.repository).toEqual({
129+
__typename: 'GitRepository',
130+
id: '647250661',
131+
url: 'https://github.com/test-user/eleventy-sample',
132+
name: 'eleventy-sample',
133+
fullName: 'test-user/eleventy-sample',
134+
defaultBranch: 'main',
135+
});
136+
expect(result).toBe(true);
137+
});
138+
139+
it(`should successfully check if the git remote is available and valid
140+
when the github remote URL is SSH based`, async () => {
141+
(existsSync as jest.Mock).mockReturnValueOnce(true);
142+
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
143+
origin: 'git@github.com:test-user/eleventy-sample.git',
144+
});
145+
const apolloClient = {
146+
query: jest.fn().mockResolvedValueOnce(repositoriesResponse),
147+
} as any;
148+
const githubAdapterInstance = new GitHub({
149+
config: { projectBasePath: '/home/project1' },
150+
apolloClient: apolloClient,
151+
} as any);
152+
153+
const result = await githubAdapterInstance.checkGitRemoteAvailableAndValid();
154+
155+
expect(existsSync).toHaveBeenCalledWith('/home/project1/.git');
156+
expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
157+
expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery });
158+
expect(githubAdapterInstance.config.repository).toEqual({
159+
__typename: 'GitRepository',
160+
id: '647250661',
161+
url: 'https://github.com/test-user/eleventy-sample',
162+
name: 'eleventy-sample',
163+
fullName: 'test-user/eleventy-sample',
164+
defaultBranch: 'main',
165+
});
166+
expect(result).toBe(true);
167+
});
168+
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);
197+
(getRemoteUrls as jest.Mock).mockResolvedValueOnce(undefined);
198+
const githubAdapterInstance = new GitHub({
199+
config: { projectBasePath: '/home/project1' },
200+
log: logMock,
201+
exit: exitMock,
202+
} as any);
203+
let err;
204+
205+
try {
206+
await githubAdapterInstance.checkGitRemoteAvailableAndValid();
207+
} catch (error: any) {
208+
err = error;
209+
}
210+
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+
}
245+
246+
expect(existsSync).toHaveBeenCalledWith('/home/project1/.git');
247+
expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
248+
expect(apolloClient.query).toHaveBeenCalled();
249+
expect(connectToAdapterOnUiMock).toHaveBeenCalled();
250+
expect(logMock).toHaveBeenCalledWith('GitHub app uninstalled. Please reconnect the app and try again', 'error');
251+
expect(exitMock).toHaveBeenCalledWith(1);
252+
expect(err).toEqual(new Error('1'));
253+
});
254+
255+
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);
257+
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
258+
origin: 'https://github.com/test-user/test-repo-2.git',
259+
});
260+
const apolloClient = {
261+
query: jest.fn().mockResolvedValueOnce(repositoriesResponse),
262+
} as any;
263+
const githubAdapterInstance = new GitHub({
264+
config: { projectBasePath: '/home/project1' },
265+
log: logMock,
266+
exit: exitMock,
267+
apolloClient: apolloClient,
268+
} as any);
269+
let err;
270+
271+
try {
272+
await githubAdapterInstance.checkGitRemoteAvailableAndValid();
273+
} catch (error: any) {
274+
err = error;
275+
}
276+
277+
expect(existsSync).toHaveBeenCalledWith('/home/project1/.git');
278+
expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config');
279+
expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery });
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+
);
284+
expect(exitMock).toHaveBeenCalledWith(1);
285+
expect(err).toEqual(new Error('1'));
286+
expect(githubAdapterInstance.config.repository).toBeUndefined();
287+
});
288+
});
289+
});

0 commit comments

Comments
 (0)