diff --git a/package-lock.json b/package-lock.json index f93e4d9..52855e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/cli-launch", - "version": "1.8.0", + "version": "1.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/cli-launch", - "version": "1.8.0", + "version": "1.9.1", "license": "MIT", "dependencies": { "@apollo/client": "^3.11.8", diff --git a/package.json b/package.json index c02e1ea..855a00a 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/cli-launch", - "version": "1.8.0", + "version": "1.9.1", "description": "Launch related operations", "author": "Contentstack CLI", "bin": { diff --git a/src/adapters/github.test.ts b/src/adapters/github.test.ts new file mode 100644 index 00000000..001e701 --- /dev/null +++ b/src/adapters/github.test.ts @@ -0,0 +1,164 @@ +import GitHub from './github'; +import { getRemoteUrls } from '../util/create-git-meta'; +import { repositoriesQuery } from '../graphql'; +import BaseClass from './base-class'; + +jest.mock('../util/create-git-meta'); + +const repositories = [ + { + __typename: 'GitRepository', + id: '495370701', + url: 'https://github.com/test-user/nextjs-ssr-isr-demo', + name: 'nextjs-ssr-isr-demo', + fullName: 'test-user/nextjs-ssr-isr-demo', + defaultBranch: 'main', + }, + { + __typename: 'GitRepository', + id: '555341263', + url: 'https://github.com/test-user/static-site-demo', + name: 'static-site-demo', + fullName: 'test-user/static-site-demo', + defaultBranch: 'main', + }, + { + __typename: 'GitRepository', + id: '647250661', + url: 'https://github.com/test-user/eleventy-sample', + name: 'eleventy-sample', + fullName: 'test-user/eleventy-sample', + defaultBranch: 'main', + }, +]; + +describe('GitHub Adapter', () => { + describe('checkGitRemoteAvailableAndValid', () => { + const repositoriesResponse = { data: { repositories } }; + let logMock: jest.Mock; + let exitMock: jest.Mock; + + beforeEach(() => { + logMock = jest.fn(); + exitMock = jest.fn().mockImplementationOnce(() => { + throw new Error('1'); + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it(`should successfully check if the git remote is available and valid + when the github remote URL is HTTPS based`, async () => { + (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' }, + apolloClient: apolloClient, + } as any); + + const result = await githubAdapterInstance.checkGitRemoteAvailableAndValid(); + + expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config'); + expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery }); + expect(githubAdapterInstance.config.repository).toEqual({ + __typename: 'GitRepository', + id: '647250661', + url: 'https://github.com/test-user/eleventy-sample', + name: 'eleventy-sample', + fullName: 'test-user/eleventy-sample', + defaultBranch: 'main', + }); + expect(result).toBe(true); + }); + + it(`should successfully check if the git remote is available and valid + when the github remote URL is SSH based`, async () => { + (getRemoteUrls as jest.Mock).mockResolvedValueOnce({ + origin: 'git@github.com:test-user/eleventy-sample.git', + }); + const apolloClient = { + query: jest.fn().mockResolvedValueOnce(repositoriesResponse), + } as any; + const githubAdapterInstance = new GitHub({ + config: { projectBasePath: '/home/project1' }, + apolloClient: apolloClient, + } as any); + + const result = await githubAdapterInstance.checkGitRemoteAvailableAndValid(); + + expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config'); + expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery }); + expect(githubAdapterInstance.config.repository).toEqual({ + __typename: 'GitRepository', + id: '647250661', + url: 'https://github.com/test-user/eleventy-sample', + name: 'eleventy-sample', + fullName: 'test-user/eleventy-sample', + defaultBranch: 'main', + }); + expect(result).toBe(true); + }); + + it(`should log an error and proceed to connection via UI + if git repo remote url is unavailable and exit`, async () => { + (getRemoteUrls as jest.Mock).mockResolvedValueOnce(undefined); + const connectToAdapterOnUiMock + = jest.spyOn(BaseClass.prototype, 'connectToAdapterOnUi').mockResolvedValueOnce(undefined); + const githubAdapterInstance = new GitHub({ + config: { projectBasePath: '/home/project1' }, + log: logMock, + exit: exitMock + } as any); + let err; + + try { + await githubAdapterInstance.checkGitRemoteAvailableAndValid(); + } catch (error: any) { + err = error; + } + + + expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config'); + expect(logMock).toHaveBeenCalledWith('GitHub project not identified!', 'error'); + expect(connectToAdapterOnUiMock).toHaveBeenCalled(); + expect(exitMock).toHaveBeenCalledWith(1); + expect(err).toEqual(new Error('1')); + expect(githubAdapterInstance.config.repository).toBeUndefined(); + }); + + it('should log an error and exit if repository is not found in the list of available repositories', async () => { + (getRemoteUrls as jest.Mock).mockResolvedValueOnce({ + origin: 'https://github.com/test-user/test-repo-2.git', + }); + const apolloClient = { + query: jest.fn().mockResolvedValueOnce(repositoriesResponse), + } as any; + const githubAdapterInstance = new GitHub({ + config: { projectBasePath: '/home/project1' }, + log: logMock, + exit: exitMock, + apolloClient: apolloClient, + } as any); + let err; + + try { + await githubAdapterInstance.checkGitRemoteAvailableAndValid(); + } catch (error: any) { + err = error; + } + + expect(getRemoteUrls as jest.Mock).toHaveBeenCalledWith('/home/project1/.git/config'); + expect(apolloClient.query).toHaveBeenCalledWith({ query: repositoriesQuery }); + expect(logMock).toHaveBeenCalledWith('Repository not found in the list!', 'error'); + expect(exitMock).toHaveBeenCalledWith(1); + expect(err).toEqual(new Error('1')); + expect(githubAdapterInstance.config.repository).toBeUndefined(); + }); + }); +}); diff --git a/src/adapters/github.ts b/src/adapters/github.ts index 9be46b7..dd242cf 100755 --- a/src/adapters/github.ts +++ b/src/adapters/github.ts @@ -4,7 +4,6 @@ import omit from 'lodash/omit'; import find from 'lodash/find'; import split from 'lodash/split'; import { exec } from 'child_process'; -import replace from 'lodash/replace'; import includes from 'lodash/includes'; import { configHandler, cliux as ux } from '@contentstack/cli-utilities'; @@ -267,6 +266,23 @@ export default class GitHub extends BaseClass { return this.config.userConnection; } + private extractRepoFullNameFromGithubRemoteURL(url: string) { + let match; + + // HTTPS format: https://github.com/owner/repo.git + match = url.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)(\.git)?$/); + if (match) { + return `${match[1]}/${match[2].replace(/\.git$/, '')}`; + } + + // SSH format: git@github.com:owner/repo.git + match = url.match(/^git@github\.com:([^/]+)\/([^/]+)(\.git)?$/); + if (match) { + return `${match[1]}/${match[2].replace(/\.git$/, '')}`; + } + } + + /** * @method checkGitRemoteAvailableAndValid - GitHub repository verification * @@ -274,11 +290,14 @@ export default class GitHub extends BaseClass { * @memberof GitHub */ async checkGitRemoteAvailableAndValid(): Promise { - const localRemoteUrl = (await getRemoteUrls(resolve(this.config.projectBasePath, '.git/config')))?.origin || ''; + const gitConfigFilePath = resolve(this.config.projectBasePath, '.git/config'); + const remoteUrls = await getRemoteUrls(gitConfigFilePath); + const localRemoteUrl = remoteUrls?.origin || ''; if (!localRemoteUrl) { this.log('GitHub project not identified!', 'error'); await this.connectToAdapterOnUi(); + this.exit(1); } const repositories = await this.apolloClient @@ -286,8 +305,10 @@ export default class GitHub extends BaseClass { .then(({ data: { repositories } }) => repositories) .catch((error) => this.log(error, 'error')); + const repoFullName = this.extractRepoFullNameFromGithubRemoteURL(localRemoteUrl); + this.config.repository = find(repositories, { - url: replace(localRemoteUrl, '.git', ''), + fullName: repoFullName, }); if (!this.config.repository) { @@ -306,6 +327,7 @@ export default class GitHub extends BaseClass { */ async checkUserGitHubAccess(): Promise { return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; const defaultBranch = this.config.repository?.defaultBranch; if (!defaultBranch) return reject('Branch not found');