Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/adapters/base-class.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,4 +685,71 @@ describe('BaseClass', () => {
expect(exitMock).toHaveBeenCalledWith(1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely, We would need to bump a minor version in the package.json on this, as there's no breaking change.

});
});

describe('detectFramework', () => {
it('should scope the framework query to the selected GitHub connection namespace', async () => {
const apolloClient = {
query: jest.fn().mockResolvedValueOnce({ data: { framework: { framework: 'GATSBY' } } }),
} as any;
baseClass = new BaseClass({
log: logMock,
exit: exitMock,
apolloClient,
config: {
provider: 'GitHub',
listOfFrameWorks: [],
repository: { fullName: 'test-user/eleventy-sample', defaultBranch: 'main' },
userConnection: { provider: 'GitHub', namespace: 'org-account' },
},
} as any);
(ux.inquire as jest.Mock).mockResolvedValueOnce('GATSBY');

await baseClass.detectFramework();

expect(apolloClient.query).toHaveBeenCalledWith({
query: expect.anything(),
variables: {
query: {
provider: 'GitHub',
repoName: 'test-user/eleventy-sample',
branchName: 'main',
namespace: 'org-account',
},
},
});
});
});

describe('selectBranch', () => {
it('should scope the branches query to the selected GitHub connection namespace', async () => {
const apolloClient = {
query: jest.fn().mockResolvedValueOnce({
data: { branches: { edges: [], pageData: { page: 1 }, pageInfo: { hasNextPage: false } } },
}),
} as any;
baseClass = new BaseClass({
log: logMock,
exit: exitMock,
apolloClient,
config: {
provider: 'GitHub',
flags: {},
repository: { fullName: 'test-user/eleventy-sample' },
userConnection: { provider: 'GitHub', namespace: 'org-account' },
},
} as any);
(ux.inquire as jest.Mock).mockResolvedValueOnce('main');

await baseClass.selectBranch();

expect(apolloClient.query).toHaveBeenCalledWith({
query: expect.anything(),
variables: {
page: 1,
first: 100,
query: { provider: 'GitHub', repoName: 'test-user/eleventy-sample', namespace: 'org-account' },
},
});
});
});
});
4 changes: 4 additions & 0 deletions src/adapters/base-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export default class BaseClass {
*/
async detectFramework(): Promise<void> {
const { fullName, defaultBranch } = this.config.repository || {};
const namespace = this.config.userConnection?.namespace;
const query = this.config.provider === 'FileUpload' ? fileFrameworkQuery : frameworkQuery;
const variables =
this.config.provider === 'FileUpload'
Expand All @@ -171,6 +172,7 @@ export default class BaseClass {
provider: this.config.provider,
repoName: fullName,
branchName: defaultBranch,
...(namespace ? { namespace } : {}),
},
};
this.config.framework = (await this.apolloClient
Expand Down Expand Up @@ -464,12 +466,14 @@ export default class BaseClass {
* @memberof BaseClass
*/
async selectBranch(): Promise<void> {
const namespace = this.config.userConnection?.namespace;
const variables = {
page: 1,
first: 100,
query: {
provider: this.config.provider,
repoName: this.config.repository?.fullName,
...(namespace ? { namespace } : {}),
},
};

Expand Down
173 changes: 173 additions & 0 deletions src/adapters/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,83 @@ describe('GitHub Adapter', () => {
expect(connectToAdapterOnUiMock).toHaveBeenCalled();
expect(githubAdapterInstance.config.userConnection).toEqual(undefined);
});

it('should prompt the user to select a connection when multiple GitHub connections exist', async () => {
const multipleUserConnections = [
{ __typename: 'UserConnection', userUid: 'testuser1', provider: 'GitHub', namespace: 'personal-account' },
{ __typename: 'UserConnection', userUid: 'testuser1', provider: 'GitHub', namespace: 'org-account' },
];
const userConnectionResponse = { data: { userConnections: multipleUserConnections } };
const apolloClient = {
query: jest.fn().mockResolvedValueOnce(userConnectionResponse),
} as any;
const githubAdapterInstance = new GitHub({
config: { projectBasePath: '/home/project1', provider: 'GitHub' },
apolloClient: apolloClient,
log: logMock,
} as any);
(ux.inquire as jest.Mock).mockResolvedValueOnce('org-account');

await githubAdapterInstance.checkGitHubConnected();

expect(ux.inquire).toHaveBeenCalledWith({
type: 'search-list',
name: 'userConnection',
message: 'Choose a GitHub Namespace',
choices: ['personal-account', 'org-account'],
});
expect(githubAdapterInstance.config.userConnection).toEqual(multipleUserConnections[1]);
});

it('should use the --namespace flag to select a connection without prompting', async () => {
const multipleUserConnections = [
{ __typename: 'UserConnection', userUid: 'testuser1', provider: 'GitHub', namespace: 'personal-account' },
{ __typename: 'UserConnection', userUid: 'testuser1', provider: 'GitHub', namespace: 'org-account' },
];
const userConnectionResponse = { data: { userConnections: multipleUserConnections } };
const apolloClient = {
query: jest.fn().mockResolvedValueOnce(userConnectionResponse),
} as any;
const githubAdapterInstance = new GitHub({
config: { projectBasePath: '/home/project1', provider: 'GitHub', flags: { namespace: 'org-account' } },
apolloClient: apolloClient,
log: logMock,
} as any);

await githubAdapterInstance.checkGitHubConnected();

expect(ux.inquire).not.toHaveBeenCalled();
expect(githubAdapterInstance.config.userConnection).toEqual(multipleUserConnections[1]);
});

it('should log an error and exit if the --namespace flag does not match any connection', async () => {
const multipleUserConnections = [
{ __typename: 'UserConnection', userUid: 'testuser1', provider: 'GitHub', namespace: 'personal-account' },
{ __typename: 'UserConnection', userUid: 'testuser1', provider: 'GitHub', namespace: 'org-account' },
];
const userConnectionResponse = { data: { userConnections: multipleUserConnections } };
const apolloClient = {
query: jest.fn().mockResolvedValueOnce(userConnectionResponse),
} as any;
const githubAdapterInstance = new GitHub({
config: { projectBasePath: '/home/project1', provider: 'GitHub', flags: { namespace: 'unknown-account' } },
apolloClient: apolloClient,
log: logMock,
exit: exitMock,
} as any);

let err;
try {
await githubAdapterInstance.checkGitHubConnected();
} catch (error: any) {
err = error;
}

expect(ux.inquire).not.toHaveBeenCalled();
expect(logMock).toHaveBeenCalledWith('GitHub connection namespace not found!', 'error');
expect(exitMock).toHaveBeenCalledWith(1);
expect(err).toEqual(new Error('1'));
});
});

describe('checkGitRemoteAvailableAndValid', () => {
Expand Down Expand Up @@ -212,6 +289,31 @@ describe('GitHub Adapter', () => {
expect(result).toBe(true);
});

it('should scope the repositories query to the selected GitHub connection namespace', async () => {
(existsSync as jest.Mock).mockReturnValueOnce(true);
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
origin: 'https://github.com/test-user/eleventy-sample.git',
});
const apolloClient = {
query: jest.fn().mockResolvedValueOnce(repositoriesResponse),
} as any;
const githubAdapterInstance = new GitHub({
config: {
projectBasePath: '/home/project1',
provider: 'GitHub',
userConnection: { provider: 'GitHub', namespace: 'org-account' },
},
apolloClient: apolloClient,
} as any);

await githubAdapterInstance.checkGitRemoteAvailableAndValid();

expect(apolloClient.query).toHaveBeenCalledWith({
query: repositoriesQuery,
variables: { page: 1, first: 100, query: { provider: 'GitHub', namespace: 'org-account' } },
});
});

it('should log an error and exit if git config file does not exists', async () => {
(existsSync as jest.Mock).mockReturnValueOnce(false);
const githubAdapterInstance = new GitHub({
Expand Down Expand Up @@ -298,6 +400,37 @@ describe('GitHub Adapter', () => {
expect(err).toEqual(new Error('1'));
});

it('should reference the selected connection namespace when the GitHub app is uninstalled', async () => {
(existsSync as jest.Mock).mockReturnValueOnce(true);
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
origin: 'https://github.com/test-user/eleventy-sample.git',
});
const apolloClient = {
query: jest.fn().mockRejectedValue(new Error('GitHub app error')),
} as any;
jest.spyOn(BaseClass.prototype, 'connectToAdapterOnUi').mockResolvedValueOnce();
const githubAdapterInstance = new GitHub({
config: {
projectBasePath: '/home/project1',
userConnection: { provider: 'GitHub', namespace: 'org-account' },
},
apolloClient: apolloClient,
log: logMock,
exit: exitMock,
} as any);

try {
await githubAdapterInstance.checkGitRemoteAvailableAndValid();
} catch {
// exitMock throws to halt the flow under test, same as sibling tests in this file
}

expect(logMock).toHaveBeenCalledWith(
'GitHub app uninstalled for the "org-account" connection. Please reconnect the app and try again',
'error',
);
});

it('should log an error and exit if repository is not found in the list of available repositories', async () => {
(existsSync as jest.Mock).mockReturnValueOnce(true);
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
Expand Down Expand Up @@ -464,6 +597,46 @@ describe('GitHub Adapter', () => {
expect(exitMock).toHaveBeenCalledWith(1);
expect(err).toEqual(new Error('1'));
});

it('should reference the selected connection namespace when the repository is beyond the checked pages', async () => {
(existsSync as jest.Mock).mockReturnValueOnce(true);
(getRemoteUrls as jest.Mock).mockResolvedValueOnce({
origin: 'https://github.com/test-user/missing-repo.git',
});
const apolloClient = {
query: jest.fn().mockImplementation(({ variables }) => {
const { page, first } = variables;
const edges = Array.from({ length: first }, (_, i) => ({
node: {
__typename: 'GitRepository',
id: `${(page - 1) * first + i}`,
url: `https://github.com/test-user/repo-${(page - 1) * first + i}`,
name: `repo-${(page - 1) * first + i}`,
fullName: `test-user/repo-${(page - 1) * first + i}`,
defaultBranch: 'main',
},
}));
return Promise.resolve({ data: { repositories: { edges, pageData: { page }, pageInfo: { hasNextPage: true } } } });
}),
} as any;
const githubAdapterInstance = new GitHub({
config: {
projectBasePath: '/home/project1',
userConnection: { provider: 'GitHub', namespace: 'org-account' },
},
log: logMock,
exit: exitMock,
apolloClient: apolloClient,
} as any);

try {
await githubAdapterInstance.checkGitRemoteAvailableAndValid();
} catch {
// exitMock throws to halt the flow under test, same as sibling tests in this file
}

expect(logMock).toHaveBeenCalledWith(expect.stringContaining('the "org-account" connection'), 'error');
});
});

describe('runGitHubFlow', () => {
Expand Down
46 changes: 41 additions & 5 deletions src/adapters/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { resolve } from 'path';
import omit from 'lodash/omit';
import find from 'lodash/find';
import split from 'lodash/split';
import filter from 'lodash/filter';
import { exec } from 'child_process';
import includes from 'lodash/includes';
import { configHandler, cliux as ux } from '@contentstack/cli-utilities';
Expand Down Expand Up @@ -240,7 +241,7 @@ export default class GitHub extends BaseClass {
});
if (serverCommandInput) {
this.config.serverCommand = serverCommandInput;
}
}
} else {
this.config.serverCommand = serverCommand;
}
Expand Down Expand Up @@ -296,10 +297,33 @@ export default class GitHub extends BaseClass {
.then(({ data: { userConnections } }) => userConnections)
.catch((error) => this.log(error, 'error'));

const userConnection = find(userConnections, {
const matchingConnections = filter(userConnections, {
provider: this.config.provider,
});

const namespaceFlag = this.config.flags?.namespace;
let userConnection;
if (namespaceFlag) {
userConnection = find(matchingConnections, { namespace: namespaceFlag });
if (!userConnection) {
this.log('GitHub connection namespace not found!', 'error');
this.exit(1);
}
} else if (matchingConnections.length > 1) {
const selectedNamespace = await ux.inquire({
type: 'search-list',
name: 'userConnection',
message: 'Choose a GitHub Namespace',
choices: map(matchingConnections, (connection) => connection.namespace || connection.userUid),
});
userConnection = find(
matchingConnections,
(connection) => (connection.namespace || connection.userUid) === selectedNamespace,
);
} else {
userConnection = matchingConnections[0];
}

if (userConnection) {
this.log('GitHub connection identified!', 'info');
this.config.userConnection = userConnection;
Expand Down Expand Up @@ -353,11 +377,20 @@ export default class GitHub extends BaseClass {
this.exit(1);
}

const namespace = this.config.userConnection?.namespace;
let repositories: Repository[] = [];
try {
repositories = await this.queryRepositories({ page: 1, first: REPOSITORY_PAGE_SIZE });
repositories = await this.queryRepositories({
page: 1,
first: REPOSITORY_PAGE_SIZE,
...(namespace ? { query: { provider: this.config.provider, namespace } } : {}),
});
} catch {
this.log('GitHub app uninstalled. Please reconnect the app and try again', 'error');
this.log(
`GitHub app uninstalled${namespace ? ` for the "${namespace}" connection` : ''}. ` +
'Please reconnect the app and try again',
'error',
);
await this.connectToAdapterOnUi();
this.exit(1);
}
Expand All @@ -379,9 +412,12 @@ export default class GitHub extends BaseClass {
if (!this.config.repository) {
const checkedCount = MAX_REPOSITORY_PAGES * REPOSITORY_PAGE_SIZE;
if (repositories.length >= checkedCount) {
const installationSettingsLocation = namespace
? `In the GitHub App installation settings for the "${namespace}" connection`
: 'In your GitHub App\'s installation settings';
this.log(
`"${repoFullName}" is beyond the first ${checkedCount} repositories the GitHub App can access. ` +
'In your GitHub App\'s installation settings, under "Repository access", select ' +
`${installationSettingsLocation}, under "Repository access", select ` +
'"Only select repositories" and add the repository you want to deploy. Then re-run the command.',
'error',
);
Expand Down
Loading
Loading