Skip to content

Commit 9160a8d

Browse files
Merge pull request #27 from cabinetoffice/IDP-442-add-call-to-submit-well-formatted-pr-object-to-terraform-repo
Idp 442 add call to submit well formatted pr object to terraform repo
2 parents 77d5b73 + 2878548 commit 9160a8d

7 files changed

Lines changed: 298 additions & 2 deletions

File tree

src/api-sdk/github/github.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
GitHubMembersPerTeam,
66
GitHubReposPerTeam,
77
GitHubCollaboratorsPerRepo,
8-
GitHubIssueRequest
8+
GitHubIssueRequest,
9+
GitHubPullRequest
910
} from './type';
1011
import {
1112
reposMapping,
@@ -16,6 +17,7 @@ import {
1617
collaboratorsPerRepoMapping
1718
} from './mapping';
1819

20+
import { extractBaseShaHelper, extractShaHelper, getShaParams, createBranchParams, createBlobParams, createTreeParams, createCommitParams, updateBranchReferenceParams, createPullRequestParams } from './utils';
1921
import { ApiResponse, ApiErrorResponse } from '../response';
2022
import { HttpRequest } from '../../http-request';
2123

@@ -78,6 +80,29 @@ export class Github {
7880
return this.responseHandler<any>(response);
7981
}
8082

83+
public async postPullRequest (repoUrl: string, body: GitHubPullRequest): Promise<ApiResponse<any> | ApiErrorResponse> {
84+
const shaResponse = await this.getData(getShaParams(repoUrl, body.baseBranch));
85+
const baseSha = extractBaseShaHelper(shaResponse);
86+
87+
const { branchUrl, branchBody } = createBranchParams(repoUrl, body.branchName, baseSha);
88+
await this.postData(branchUrl, branchBody);
89+
90+
const { blobUrl, blobBody } = createBlobParams(repoUrl, body.prContent);
91+
const blobSha = extractShaHelper(await this.postData(blobUrl, blobBody));
92+
93+
const { treeUrl, treeBody } = createTreeParams(repoUrl, baseSha, body.filePath, blobSha);
94+
const treeSha = extractShaHelper(await this.postData(treeUrl, treeBody));
95+
96+
const { commitUrl, commitBody } = createCommitParams(repoUrl, body.prTitle, treeSha, baseSha);
97+
const commitSha = extractShaHelper(await this.postData(commitUrl, commitBody));
98+
99+
const { refUrl, refBody } = updateBranchReferenceParams(repoUrl, body.branchName, commitSha);
100+
await this.postData(refUrl, refBody);
101+
102+
const { prUrl, prPostbody } = createPullRequestParams(repoUrl, body.prTitle, body.prBody, body.branchName, body.baseBranch);
103+
return this.postData(prUrl, prPostbody);
104+
}
105+
81106
private responseHandler<T>(
82107
response: any,
83108
responseMap?: (body: any) => T

src/api-sdk/github/type.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,12 @@ export interface GitHubIssueRequest {
4747
assignees: string[],
4848
labels: string[]
4949
}
50+
51+
export interface GitHubPullRequest {
52+
prTitle: string,
53+
prBody: string,
54+
filePath: string,
55+
prContent: string,
56+
branchName: string,
57+
baseBranch: string
58+
}

src/api-sdk/github/utils.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { ApiResponse, ApiErrorResponse } from '../response';
2+
3+
const defaultBranch = 'main';
4+
5+
export const extractBaseShaHelper = (response: ApiResponse<any> | ApiErrorResponse) => {
6+
if ('resource' in response && 'object' in response.resource){
7+
return response.resource.object.sha;
8+
}
9+
throw new Error(`Error: ${JSON.stringify(response)}`);
10+
};
11+
12+
export const extractShaHelper = (response: ApiResponse<any> | ApiErrorResponse) => {
13+
if ('resource' in response){
14+
return response.resource.sha;
15+
}
16+
throw new Error(`Error: ${JSON.stringify(response)}`);
17+
};
18+
19+
export const getShaParams = (repoUrl: string, baseBranch: string = defaultBranch) => {
20+
const shaUrl = `${repoUrl}/git/refs/heads/${baseBranch}`;
21+
return shaUrl;
22+
};
23+
24+
export const createBranchParams = (repoUrl: string, branchName: string, baseSha: string) => {
25+
const branchUrl = `${repoUrl}/git/refs`;
26+
const branchBody = {
27+
ref: `refs/heads/${branchName}`,
28+
sha: baseSha
29+
};
30+
return { branchUrl, branchBody };
31+
};
32+
33+
export const createBlobParams = (repoUrl: string, content: string) => {
34+
const blobUrl = `${repoUrl}/git/blobs`;
35+
const blobBody = {
36+
content: Buffer.from(content).toString('base64'),
37+
encoding: 'base64'
38+
};
39+
return { blobUrl, blobBody };
40+
};
41+
42+
export const createTreeParams = (repoUrl: string, baseTreeSha: string, path: string, blobSha: string) => {
43+
const treeUrl = `${repoUrl}/git/trees`;
44+
const treeBody = {
45+
base_tree: baseTreeSha,
46+
tree: [
47+
{
48+
path: path,
49+
mode: '100644',
50+
type: 'blob',
51+
sha: blobSha
52+
}
53+
]
54+
};
55+
return { treeUrl, treeBody };
56+
};
57+
58+
export const createCommitParams = (repoUrl: string, message: string, treeSha: string, parentSha: string) => {
59+
const commitUrl = `${repoUrl}/git/commits`;
60+
const commitBody = {
61+
message: message,
62+
tree: treeSha,
63+
parents: [parentSha]
64+
};
65+
return { commitUrl, commitBody };
66+
};
67+
68+
export const updateBranchReferenceParams = (repoUrl: string, branch: string, commitSha: string) => {
69+
const refUrl = `${repoUrl}/git/refs/heads/${branch}`;
70+
const refBody = {
71+
sha: commitSha
72+
};
73+
return { refUrl, refBody };
74+
};
75+
76+
export const createPullRequestParams = (repoUrl: string, prTitle: string, prBody: string, branchName: string, baseBranch: string = defaultBranch) => {
77+
const prUrl = `${repoUrl}/pulls`;
78+
const prPostbody = {
79+
title: prTitle,
80+
body: prBody,
81+
head: branchName,
82+
base: baseBranch
83+
};
84+
return { prUrl, prPostbody };
85+
};

test/mock/data.mock.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,48 @@ export const MOCK_POST_RESPONSE = {
197197
httpStatusCode: 200,
198198
resource: MOCK_POST
199199
};
200+
201+
export const MOCK_SHA_RESPONSE = {
202+
object: { sha: 'ABC12345678' }
203+
};
204+
205+
export const MOCK_INVALID_SHA_RESPONSE = {
206+
httpStatusCode: 404,
207+
object: ['Resource not found']
208+
};
209+
210+
export const MOCK_API_ERROR = new Error(`Error: ${JSON.stringify(MOCK_INVALID_SHA_RESPONSE)}`);
211+
212+
export const MOCK_POST_BRANCH = { ref: 'refs/heads/test-branch', sha: 'ABC12345678' };
213+
214+
export const MOCK_POST_BLOB = {
215+
content: Buffer.from('test content').toString('base64'),
216+
encoding: 'base64'
217+
};
218+
219+
export const MOCK_POST_TREE = {
220+
base_tree: 'ABC12345678',
221+
tree: [{ path: 'terraform/account-1.tf', mode: '100644', type: 'blob', sha: 'ABC12345678' }]
222+
};
223+
224+
export const MOCK_POST_COMMIT = {
225+
message: 'commit message',
226+
tree: 'ABC12345678',
227+
parents: ['ABC12345678']
228+
};
229+
230+
export const MOCK_POST_PR = {
231+
title: 'PR Title',
232+
body: 'PR Body',
233+
head: 'test-branch',
234+
base: 'main'
235+
};
236+
237+
export const MOCK_PR_RESPONSE = {
238+
branchName: 'new-feature',
239+
prContent: 'some content',
240+
filePath: 'file.txt',
241+
prTitle: 'PR Title',
242+
prBody: 'PR Body',
243+
baseBranch: 'main'
244+
};

test/mock/text.mock.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const MOCK_REPO_URL = 'https://api.github.com/repos/test-repo';
2+
3+
export const MOCK_BASE_SHA = 'ABC12345678';
4+
export const MOCK_BLOB_SHA = 'ABC12345678';
5+
export const MOCK_TREE_SHA = 'ABC12345678';
6+
export const MOCK_COMMIT_SHA = 'ABC12345678';
7+
8+
export const MOCK_COMMIT_MESSAGE = 'commit message';
9+
export const MOCK_BRANCH_NAME = 'test-branch';
10+
11+
export const MOCK_PATH = 'terraform/account-1.tf';
12+
13+
export const MOCK_PR_TITLE = 'PR Title';
14+
export const MOCK_PR_BODY = 'PR Body';
15+

test/unit/api-sdk/github/github.spec.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { jest, beforeEach, afterEach, describe, test, expect } from '@jest/globals';
22
import { Github } from '../../../../src/api-sdk/github/github';
33
import { HttpRequest } from '../../../../src/http-request/index';
4+
import { MOCK_REPO_URL, MOCK_BASE_SHA, MOCK_BLOB_SHA, MOCK_TREE_SHA, MOCK_COMMIT_SHA } from '../../../mock/text.mock';
45
import {
56
MOCK_REPO_FETCH_RESPONSE,
67
MOCK_MEMBER_FETCH_RESPONSE,
@@ -27,7 +28,8 @@ import {
2728
MOCK_GET,
2829
MOCK_GET_RESPONSE,
2930
MOCK_POST,
30-
MOCK_POST_RESPONSE
31+
MOCK_POST_RESPONSE,
32+
MOCK_PR_RESPONSE
3133
} from '../../../mock/data.mock';
3234
import { HttpResponse } from '../../../../src/http-request/type';
3335

@@ -163,6 +165,25 @@ describe('Github sdk module test suites', () => {
163165
expect(result).toEqual(MOCK_POST_RESPONSE);
164166
});
165167

168+
test('should successfully post a pull request and return the response', async () => {
169+
httpRequestMock.httpGet.mockResolvedValue(createMockHttpResponse({ object: { sha: MOCK_BASE_SHA } }));
170+
httpRequestMock.httpPost
171+
.mockResolvedValueOnce(createMockHttpResponse({}))
172+
.mockResolvedValueOnce(createMockHttpResponse({ sha: MOCK_BLOB_SHA }))
173+
.mockResolvedValueOnce(createMockHttpResponse({ sha: MOCK_TREE_SHA }))
174+
.mockResolvedValueOnce(createMockHttpResponse({ sha: MOCK_COMMIT_SHA }))
175+
.mockResolvedValueOnce(createMockHttpResponse({}))
176+
.mockResolvedValue(createMockHttpResponse(MOCK_PR_RESPONSE));
177+
178+
const result = await github.postPullRequest(MOCK_REPO_URL, MOCK_PR_RESPONSE);
179+
180+
expect(result).toEqual({ httpStatusCode: 200, resource: MOCK_PR_RESPONSE });
181+
182+
expect(httpRequestMock.httpGet).toHaveBeenCalledTimes(1);
183+
184+
expect(httpRequestMock.httpPost).toHaveBeenCalledTimes(6);
185+
});
186+
166187
test('Should return an object with an error property', async () => {
167188
httpRequestMock.httpGet.mockResolvedValue(createMockHttpResponse(MOCK_TEAMS, 500, MOCK_ERROR));
168189

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { jest, afterEach, describe, test, expect } from '@jest/globals';
2+
3+
import {
4+
extractBaseShaHelper,
5+
extractShaHelper,
6+
getShaParams,
7+
createBranchParams,
8+
createBlobParams,
9+
createTreeParams,
10+
createCommitParams,
11+
updateBranchReferenceParams,
12+
createPullRequestParams
13+
} from '../../../../src/api-sdk/github/utils';
14+
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';
15+
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';
16+
17+
describe('Github utils test suites', () => {
18+
19+
afterEach(() => {
20+
jest.restoreAllMocks();
21+
});
22+
23+
test('extractBaseShaHelper should return sha if it exists', () => {
24+
const mockBaseShaResponse = {
25+
httpStatusCode: 200,
26+
resource: { object: { sha: MOCK_BASE_SHA } }
27+
};
28+
const result = extractBaseShaHelper(mockBaseShaResponse);
29+
expect(result).toBe(MOCK_BASE_SHA);
30+
});
31+
32+
test('extractBaseShaHelper should return response if resource does not exist', () => {
33+
jest.spyOn(global, 'Error').mockImplementationOnce(() => MOCK_API_ERROR);
34+
35+
expect(() => extractBaseShaHelper(MOCK_INVALID_SHA_RESPONSE)).toThrowError(MOCK_API_ERROR);
36+
});
37+
38+
test('extractShaHelper should return sha if it exists', () => {
39+
const mockShaResponse = {
40+
httpStatusCode: 200,
41+
resource: { sha: MOCK_BLOB_SHA }
42+
};
43+
const result = extractShaHelper(mockShaResponse);
44+
expect(result).toBe(MOCK_BLOB_SHA);
45+
});
46+
47+
test('extractShaHelper should return response if sha does not exist', () => {
48+
jest.spyOn(global, 'Error').mockImplementationOnce(() => MOCK_API_ERROR);
49+
50+
expect(() => extractShaHelper(MOCK_INVALID_SHA_RESPONSE)).toThrowError(MOCK_API_ERROR);
51+
});
52+
53+
test('getShaParams should return the correct sha URL', () => {
54+
const result = getShaParams(MOCK_REPO_URL);
55+
expect(result).toBe(`${MOCK_REPO_URL}/git/refs/heads/main`);
56+
});
57+
58+
test('createBranchParams should return the correct branch URL and body', () => {
59+
const { branchUrl, branchBody } = createBranchParams(MOCK_REPO_URL, MOCK_BRANCH_NAME, MOCK_BASE_SHA);
60+
61+
expect(branchUrl).toBe(`${MOCK_REPO_URL}/git/refs`);
62+
expect(branchBody).toEqual(MOCK_POST_BRANCH);
63+
});
64+
65+
test('createBlobParams should return the correct blob URL and body', () => {
66+
const { blobUrl, blobBody } = createBlobParams(MOCK_REPO_URL, 'test content');
67+
68+
expect(blobUrl).toBe(`${MOCK_REPO_URL}/git/blobs`);
69+
expect(blobBody).toEqual(MOCK_POST_BLOB);
70+
});
71+
72+
test('createTreeParams should return the correct tree URL and body', () => {
73+
const { treeUrl, treeBody } = createTreeParams(MOCK_REPO_URL, MOCK_BASE_SHA, MOCK_PATH, MOCK_BLOB_SHA);
74+
75+
expect(treeUrl).toBe(`${MOCK_REPO_URL}/git/trees`);
76+
expect(treeBody).toEqual(MOCK_POST_TREE);
77+
});
78+
79+
test('createCommitParams should return the correct commit URL and body', () => {
80+
const { commitUrl, commitBody } = createCommitParams(MOCK_REPO_URL, MOCK_COMMIT_MESSAGE, MOCK_TREE_SHA, MOCK_BASE_SHA);
81+
expect(commitUrl).toBe(`${MOCK_REPO_URL}/git/commits`);
82+
expect(commitBody).toEqual(MOCK_POST_COMMIT);
83+
});
84+
85+
test('updateBranchReferenceParams should return the correct ref URL and body', () => {
86+
const { refUrl, refBody } = updateBranchReferenceParams(MOCK_REPO_URL, MOCK_BRANCH_NAME, MOCK_COMMIT_SHA);
87+
expect(refUrl).toBe(`${MOCK_REPO_URL}/git/refs/heads/${MOCK_BRANCH_NAME}`);
88+
expect(refBody).toEqual({ sha: MOCK_COMMIT_SHA });
89+
});
90+
91+
test('createPullRequestParams should return the correct PR URL and body', () => {
92+
const { prUrl, prPostbody } = createPullRequestParams(MOCK_REPO_URL, MOCK_PR_TITLE, MOCK_PR_BODY, MOCK_BRANCH_NAME);
93+
expect(prUrl).toBe(`${MOCK_REPO_URL}/pulls`);
94+
expect(prPostbody).toEqual(MOCK_POST_PR);
95+
});
96+
});

0 commit comments

Comments
 (0)