diff --git a/src/api-sdk/github/github.ts b/src/api-sdk/github/github.ts index a49b979..2283546 100644 --- a/src/api-sdk/github/github.ts +++ b/src/api-sdk/github/github.ts @@ -5,7 +5,8 @@ import { GitHubMembersPerTeam, GitHubReposPerTeam, GitHubCollaboratorsPerRepo, - GitHubIssueRequest + GitHubIssueRequest, + GitHubPullRequest } from './type'; import { reposMapping, @@ -16,6 +17,7 @@ import { collaboratorsPerRepoMapping } from './mapping'; +import { extractBaseShaHelper, extractShaHelper, getShaParams, createBranchParams, createBlobParams, createTreeParams, createCommitParams, updateBranchReferenceParams, createPullRequestParams } from './utils'; import { ApiResponse, ApiErrorResponse } from '../response'; import { HttpRequest } from '../../http-request'; @@ -78,6 +80,29 @@ export class Github { return this.responseHandler(response); } + public async postPullRequest (repoUrl: string, body: GitHubPullRequest): Promise | ApiErrorResponse> { + const shaResponse = await this.getData(getShaParams(repoUrl, body.baseBranch)); + const baseSha = extractBaseShaHelper(shaResponse); + + const { branchUrl, branchBody } = createBranchParams(repoUrl, body.branchName, baseSha); + await this.postData(branchUrl, branchBody); + + const { blobUrl, blobBody } = createBlobParams(repoUrl, body.prContent); + const blobSha = extractShaHelper(await this.postData(blobUrl, blobBody)); + + const { treeUrl, treeBody } = createTreeParams(repoUrl, baseSha, body.filePath, blobSha); + const treeSha = extractShaHelper(await this.postData(treeUrl, treeBody)); + + const { commitUrl, commitBody } = createCommitParams(repoUrl, body.prTitle, treeSha, baseSha); + const commitSha = extractShaHelper(await this.postData(commitUrl, commitBody)); + + const { refUrl, refBody } = updateBranchReferenceParams(repoUrl, body.branchName, commitSha); + await this.postData(refUrl, refBody); + + const { prUrl, prPostbody } = createPullRequestParams(repoUrl, body.prTitle, body.prBody, body.branchName, body.baseBranch); + return this.postData(prUrl, prPostbody); + } + private responseHandler( response: any, responseMap?: (body: any) => T diff --git a/src/api-sdk/github/type.ts b/src/api-sdk/github/type.ts index ba5986d..c2bae5b 100644 --- a/src/api-sdk/github/type.ts +++ b/src/api-sdk/github/type.ts @@ -47,3 +47,12 @@ export interface GitHubIssueRequest { assignees: string[], labels: string[] } + +export interface GitHubPullRequest { + prTitle: string, + prBody: string, + filePath: string, + prContent: string, + branchName: string, + baseBranch: string +} diff --git a/src/api-sdk/github/utils.ts b/src/api-sdk/github/utils.ts new file mode 100644 index 0000000..93911be --- /dev/null +++ b/src/api-sdk/github/utils.ts @@ -0,0 +1,85 @@ +import { ApiResponse, ApiErrorResponse } from '../response'; + +const defaultBranch = 'main'; + +export const extractBaseShaHelper = (response: ApiResponse | ApiErrorResponse) => { + if ('resource' in response && 'object' in response.resource){ + return response.resource.object.sha; + } + throw new Error(`Error: ${JSON.stringify(response)}`); +}; + +export const extractShaHelper = (response: ApiResponse | ApiErrorResponse) => { + if ('resource' in response){ + return response.resource.sha; + } + throw new Error(`Error: ${JSON.stringify(response)}`); +}; + +export const getShaParams = (repoUrl: string, baseBranch: string = defaultBranch) => { + const shaUrl = `${repoUrl}/git/refs/heads/${baseBranch}`; + return shaUrl; +}; + +export const createBranchParams = (repoUrl: string, branchName: string, baseSha: string) => { + const branchUrl = `${repoUrl}/git/refs`; + const branchBody = { + ref: `refs/heads/${branchName}`, + sha: baseSha + }; + return { branchUrl, branchBody }; +}; + +export const createBlobParams = (repoUrl: string, content: string) => { + const blobUrl = `${repoUrl}/git/blobs`; + const blobBody = { + content: Buffer.from(content).toString('base64'), + encoding: 'base64' + }; + return { blobUrl, blobBody }; +}; + +export const createTreeParams = (repoUrl: string, baseTreeSha: string, path: string, blobSha: string) => { + const treeUrl = `${repoUrl}/git/trees`; + const treeBody = { + base_tree: baseTreeSha, + tree: [ + { + path: path, + mode: '100644', + type: 'blob', + sha: blobSha + } + ] + }; + return { treeUrl, treeBody }; +}; + +export const createCommitParams = (repoUrl: string, message: string, treeSha: string, parentSha: string) => { + const commitUrl = `${repoUrl}/git/commits`; + const commitBody = { + message: message, + tree: treeSha, + parents: [parentSha] + }; + return { commitUrl, commitBody }; +}; + +export const updateBranchReferenceParams = (repoUrl: string, branch: string, commitSha: string) => { + const refUrl = `${repoUrl}/git/refs/heads/${branch}`; + const refBody = { + sha: commitSha + }; + return { refUrl, refBody }; +}; + +export const createPullRequestParams = (repoUrl: string, prTitle: string, prBody: string, branchName: string, baseBranch: string = defaultBranch) => { + const prUrl = `${repoUrl}/pulls`; + const prPostbody = { + title: prTitle, + body: prBody, + head: branchName, + base: baseBranch + }; + return { prUrl, prPostbody }; +}; diff --git a/test/mock/data.mock.ts b/test/mock/data.mock.ts index 9cdc778..c6b5229 100644 --- a/test/mock/data.mock.ts +++ b/test/mock/data.mock.ts @@ -197,3 +197,48 @@ export const MOCK_POST_RESPONSE = { httpStatusCode: 200, resource: MOCK_POST }; + +export const MOCK_SHA_RESPONSE = { + object: { sha: 'ABC12345678' } +}; + +export const MOCK_INVALID_SHA_RESPONSE = { + httpStatusCode: 404, + object: ['Resource not found'] +}; + +export const MOCK_API_ERROR = new Error(`Error: ${JSON.stringify(MOCK_INVALID_SHA_RESPONSE)}`); + +export const MOCK_POST_BRANCH = { ref: 'refs/heads/test-branch', sha: 'ABC12345678' }; + +export const MOCK_POST_BLOB = { + content: Buffer.from('test content').toString('base64'), + encoding: 'base64' +}; + +export const MOCK_POST_TREE = { + base_tree: 'ABC12345678', + tree: [{ path: 'terraform/account-1.tf', mode: '100644', type: 'blob', sha: 'ABC12345678' }] +}; + +export const MOCK_POST_COMMIT = { + message: 'commit message', + tree: 'ABC12345678', + parents: ['ABC12345678'] +}; + +export const MOCK_POST_PR = { + title: 'PR Title', + body: 'PR Body', + head: 'test-branch', + base: 'main' +}; + +export const MOCK_PR_RESPONSE = { + branchName: 'new-feature', + prContent: 'some content', + filePath: 'file.txt', + prTitle: 'PR Title', + prBody: 'PR Body', + baseBranch: 'main' +}; diff --git a/test/mock/text.mock.ts b/test/mock/text.mock.ts new file mode 100644 index 0000000..b31351d --- /dev/null +++ b/test/mock/text.mock.ts @@ -0,0 +1,15 @@ +export const MOCK_REPO_URL = 'https://api.github.com/repos/test-repo'; + +export const MOCK_BASE_SHA = 'ABC12345678'; +export const MOCK_BLOB_SHA = 'ABC12345678'; +export const MOCK_TREE_SHA = 'ABC12345678'; +export const MOCK_COMMIT_SHA = 'ABC12345678'; + +export const MOCK_COMMIT_MESSAGE = 'commit message'; +export const MOCK_BRANCH_NAME = 'test-branch'; + +export const MOCK_PATH = 'terraform/account-1.tf'; + +export const MOCK_PR_TITLE = 'PR Title'; +export const MOCK_PR_BODY = 'PR Body'; + diff --git a/test/unit/api-sdk/github/github.spec.ts b/test/unit/api-sdk/github/github.spec.ts index 24849fe..5837f03 100644 --- a/test/unit/api-sdk/github/github.spec.ts +++ b/test/unit/api-sdk/github/github.spec.ts @@ -1,6 +1,7 @@ import { jest, beforeEach, afterEach, describe, test, expect } from '@jest/globals'; import { Github } from '../../../../src/api-sdk/github/github'; import { HttpRequest } from '../../../../src/http-request/index'; +import { MOCK_REPO_URL, MOCK_BASE_SHA, MOCK_BLOB_SHA, MOCK_TREE_SHA, MOCK_COMMIT_SHA } from '../../../mock/text.mock'; import { MOCK_REPO_FETCH_RESPONSE, MOCK_MEMBER_FETCH_RESPONSE, @@ -27,7 +28,8 @@ import { MOCK_GET, MOCK_GET_RESPONSE, MOCK_POST, - MOCK_POST_RESPONSE + MOCK_POST_RESPONSE, + MOCK_PR_RESPONSE } from '../../../mock/data.mock'; import { HttpResponse } from '../../../../src/http-request/type'; @@ -163,6 +165,25 @@ describe('Github sdk module test suites', () => { expect(result).toEqual(MOCK_POST_RESPONSE); }); + test('should successfully post a pull request and return the response', async () => { + httpRequestMock.httpGet.mockResolvedValue(createMockHttpResponse({ object: { sha: MOCK_BASE_SHA } })); + httpRequestMock.httpPost + .mockResolvedValueOnce(createMockHttpResponse({})) + .mockResolvedValueOnce(createMockHttpResponse({ sha: MOCK_BLOB_SHA })) + .mockResolvedValueOnce(createMockHttpResponse({ sha: MOCK_TREE_SHA })) + .mockResolvedValueOnce(createMockHttpResponse({ sha: MOCK_COMMIT_SHA })) + .mockResolvedValueOnce(createMockHttpResponse({})) + .mockResolvedValue(createMockHttpResponse(MOCK_PR_RESPONSE)); + + const result = await github.postPullRequest(MOCK_REPO_URL, MOCK_PR_RESPONSE); + + expect(result).toEqual({ httpStatusCode: 200, resource: MOCK_PR_RESPONSE }); + + expect(httpRequestMock.httpGet).toHaveBeenCalledTimes(1); + + expect(httpRequestMock.httpPost).toHaveBeenCalledTimes(6); + }); + test('Should return an object with an error property', async () => { httpRequestMock.httpGet.mockResolvedValue(createMockHttpResponse(MOCK_TEAMS, 500, MOCK_ERROR)); diff --git a/test/unit/api-sdk/github/utils.spec.ts b/test/unit/api-sdk/github/utils.spec.ts new file mode 100644 index 0000000..acf68a0 --- /dev/null +++ b/test/unit/api-sdk/github/utils.spec.ts @@ -0,0 +1,96 @@ +import { jest, afterEach, describe, test, expect } from '@jest/globals'; + +import { + extractBaseShaHelper, + extractShaHelper, + getShaParams, + createBranchParams, + createBlobParams, + createTreeParams, + createCommitParams, + updateBranchReferenceParams, + createPullRequestParams +} from '../../../../src/api-sdk/github/utils'; +import { MOCK_REPO_URL, MOCK_BASE_SHA, MOCK_BLOB_SHA, MOCK_TREE_SHA, MOCK_COMMIT_SHA, MOCK_BRANCH_NAME, MOCK_PATH, MOCK_COMMIT_MESSAGE, MOCK_PR_TITLE, MOCK_PR_BODY } from '../../../mock/text.mock'; +import { MOCK_POST_BLOB, MOCK_INVALID_SHA_RESPONSE, MOCK_POST_BRANCH, MOCK_POST_TREE, MOCK_POST_COMMIT, MOCK_POST_PR, MOCK_API_ERROR } from '../../../mock/data.mock'; + +describe('Github utils test suites', () => { + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('extractBaseShaHelper should return sha if it exists', () => { + const mockBaseShaResponse = { + httpStatusCode: 200, + resource: { object: { sha: MOCK_BASE_SHA } } + }; + const result = extractBaseShaHelper(mockBaseShaResponse); + expect(result).toBe(MOCK_BASE_SHA); + }); + + test('extractBaseShaHelper should return response if resource does not exist', () => { + jest.spyOn(global, 'Error').mockImplementationOnce(() => MOCK_API_ERROR); + + expect(() => extractBaseShaHelper(MOCK_INVALID_SHA_RESPONSE)).toThrowError(MOCK_API_ERROR); + }); + + test('extractShaHelper should return sha if it exists', () => { + const mockShaResponse = { + httpStatusCode: 200, + resource: { sha: MOCK_BLOB_SHA } + }; + const result = extractShaHelper(mockShaResponse); + expect(result).toBe(MOCK_BLOB_SHA); + }); + + test('extractShaHelper should return response if sha does not exist', () => { + jest.spyOn(global, 'Error').mockImplementationOnce(() => MOCK_API_ERROR); + + expect(() => extractShaHelper(MOCK_INVALID_SHA_RESPONSE)).toThrowError(MOCK_API_ERROR); + }); + + test('getShaParams should return the correct sha URL', () => { + const result = getShaParams(MOCK_REPO_URL); + expect(result).toBe(`${MOCK_REPO_URL}/git/refs/heads/main`); + }); + + test('createBranchParams should return the correct branch URL and body', () => { + const { branchUrl, branchBody } = createBranchParams(MOCK_REPO_URL, MOCK_BRANCH_NAME, MOCK_BASE_SHA); + + expect(branchUrl).toBe(`${MOCK_REPO_URL}/git/refs`); + expect(branchBody).toEqual(MOCK_POST_BRANCH); + }); + + test('createBlobParams should return the correct blob URL and body', () => { + const { blobUrl, blobBody } = createBlobParams(MOCK_REPO_URL, 'test content'); + + expect(blobUrl).toBe(`${MOCK_REPO_URL}/git/blobs`); + expect(blobBody).toEqual(MOCK_POST_BLOB); + }); + + test('createTreeParams should return the correct tree URL and body', () => { + const { treeUrl, treeBody } = createTreeParams(MOCK_REPO_URL, MOCK_BASE_SHA, MOCK_PATH, MOCK_BLOB_SHA); + + expect(treeUrl).toBe(`${MOCK_REPO_URL}/git/trees`); + expect(treeBody).toEqual(MOCK_POST_TREE); + }); + + test('createCommitParams should return the correct commit URL and body', () => { + const { commitUrl, commitBody } = createCommitParams(MOCK_REPO_URL, MOCK_COMMIT_MESSAGE, MOCK_TREE_SHA, MOCK_BASE_SHA); + expect(commitUrl).toBe(`${MOCK_REPO_URL}/git/commits`); + expect(commitBody).toEqual(MOCK_POST_COMMIT); + }); + + test('updateBranchReferenceParams should return the correct ref URL and body', () => { + const { refUrl, refBody } = updateBranchReferenceParams(MOCK_REPO_URL, MOCK_BRANCH_NAME, MOCK_COMMIT_SHA); + expect(refUrl).toBe(`${MOCK_REPO_URL}/git/refs/heads/${MOCK_BRANCH_NAME}`); + expect(refBody).toEqual({ sha: MOCK_COMMIT_SHA }); + }); + + test('createPullRequestParams should return the correct PR URL and body', () => { + const { prUrl, prPostbody } = createPullRequestParams(MOCK_REPO_URL, MOCK_PR_TITLE, MOCK_PR_BODY, MOCK_BRANCH_NAME); + expect(prUrl).toBe(`${MOCK_REPO_URL}/pulls`); + expect(prPostbody).toEqual(MOCK_POST_PR); + }); +});