Commit 69a73762 authored by Enrique Alcántara's avatar Enrique Alcántara Committed by Natalia Tepluhina

Submit content changes service

Implement service that uses GitLab REST API
to submit changes in the Static Site Editor
parent 445ad717
......@@ -188,6 +188,15 @@ const Api = {
return axios.get(url, { params });
},
createProjectMergeRequest(projectPath, options) {
const url = Api.buildUrl(Api.projectMergeRequestsPath).replace(
':id',
encodeURIComponent(projectPath),
);
return axios.post(url, options);
},
// Return Merge Request for project
projectMergeRequest(projectPath, mergeRequestId, params = {}) {
const url = Api.buildUrl(Api.projectMergeRequestPath)
......
import { s__ } from '~/locale';
export const BRANCH_SUFFIX_COUNT = 8;
export const DEFAULT_TARGET_BRANCH = 'master';
export const SUBMIT_CHANGES_BRANCH_ERROR = s__('StaticSiteEditor|Branch could not be created.');
export const SUBMIT_CHANGES_COMMIT_ERROR = s__(
'StaticSiteEditor|Could not commit the content changes.',
);
export const SUBMIT_CHANGES_MERGE_REQUEST_ERROR = s__(
'StaticSiteEditor|Could not create merge request.',
);
import { BRANCH_SUFFIX_COUNT, DEFAULT_TARGET_BRANCH } from '../constants';
const generateBranchSuffix = () => `${Date.now()}`.substr(BRANCH_SUFFIX_COUNT);
const generateBranchName = (username, targetBranch = DEFAULT_TARGET_BRANCH) =>
`${username}-${targetBranch}-patch-${generateBranchSuffix()}`;
export default generateBranchName;
// TODO implement
const submitContentChanges = () => new Promise(resolve => setTimeout(resolve, 1000));
import Api from '~/api';
import { s__, sprintf } from '~/locale';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
import {
DEFAULT_TARGET_BRANCH,
SUBMIT_CHANGES_BRANCH_ERROR,
SUBMIT_CHANGES_COMMIT_ERROR,
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
} from '../constants';
const createBranch = (projectId, branch) =>
Api.createBranch(projectId, {
ref: DEFAULT_TARGET_BRANCH,
branch,
}).catch(() => {
throw new Error(SUBMIT_CHANGES_BRANCH_ERROR);
});
const commitContent = (projectId, message, branch, sourcePath, content) =>
Api.commitMultiple(
projectId,
convertObjectPropsToSnakeCase({
branch,
commitMessage: message,
actions: [
convertObjectPropsToSnakeCase({
action: 'update',
filePath: sourcePath,
content,
}),
],
}),
).catch(() => {
throw new Error(SUBMIT_CHANGES_COMMIT_ERROR);
});
const createMergeRequest = (projectId, title, sourceBranch, targetBranch = DEFAULT_TARGET_BRANCH) =>
Api.createProjectMergeRequest(
projectId,
convertObjectPropsToSnakeCase({
title,
sourceBranch,
targetBranch,
}),
).catch(() => {
throw new Error(SUBMIT_CHANGES_MERGE_REQUEST_ERROR);
});
const submitContentChanges = ({ username, projectId, sourcePath, content }) => {
const branch = generateBranchName(username);
const mergeRequestTitle = sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), {
sourcePath,
});
const meta = {};
return createBranch(projectId, branch)
.then(() => {
Object.assign(meta, { branch: { label: branch } });
return commitContent(projectId, mergeRequestTitle, branch, sourcePath, content);
})
.then(({ data: { short_id: label, web_url: url } }) => {
Object.assign(meta, { commit: { label, url } });
return createMergeRequest(projectId, mergeRequestTitle, branch);
})
.then(({ data: { iid: label, web_url: url } }) => {
Object.assign(meta, { mergeRequest: { label, url } });
return meta;
});
};
export default submitContentChanges;
......@@ -10,6 +10,8 @@ const createState = (initialState = {}) => ({
content: '',
title: '',
savedContentMeta: null,
...initialState,
});
......
---
title: Save changes in Static Site Editor using REST GitLab API
merge_request: 29286
author:
type: added
......@@ -19359,6 +19359,15 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
msgid "StaticSiteEditor|Could not commit the content changes."
msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
msgid "StaticSiteEditor|Return to site"
msgstr ""
......@@ -19368,6 +19377,9 @@ msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
msgid "StaticSiteEditor|View merge request"
msgstr ""
......
......@@ -651,7 +651,7 @@ describe('Api', () => {
describe('when an error occurs while getting a raw file', () => {
it('rejects the Promise', () => {
mock.onDelete(expectedUrl).replyOnce(500);
mock.onPost(expectedUrl).replyOnce(500);
return Api.getRawFile(dummyProjectPath, dummyFilePath).catch(() => {
expect(mock.history.get).toHaveLength(1);
......@@ -659,4 +659,36 @@ describe('Api', () => {
});
});
});
describe('createProjectMergeRequest', () => {
const dummyProjectPath = 'gitlab-org/gitlab';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
dummyProjectPath,
)}/merge_requests`;
const options = {
source_branch: 'feature',
target_branch: 'master',
title: 'Add feature',
};
describe('when the merge request is successfully created', () => {
it('resolves the Promise', () => {
mock.onPost(expectedUrl, options).replyOnce(201);
return Api.createProjectMergeRequest(dummyProjectPath, options).then(() => {
expect(mock.history.post).toHaveLength(1);
});
});
});
describe('when an error occurs while getting a raw file', () => {
it('rejects the Promise', () => {
mock.onPost(expectedUrl).replyOnce(500);
return Api.createProjectMergeRequest(dummyProjectPath).catch(() => {
expect(mock.history.post).toHaveLength(1);
});
});
});
});
});
......@@ -34,3 +34,11 @@ export const savedContentMeta = {
};
export const submitChangesError = 'Could not save changes';
export const commitMultipleResponse = {
short_id: 'ed899a2f4b5',
web_url: '/commit/ed899a2f4b5',
};
export const createMergeRequestResponse = {
iid: '123',
web_url: '/merge_requests/123',
};
import { DEFAULT_TARGET_BRANCH, BRANCH_SUFFIX_COUNT } from '~/static_site_editor/constants';
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
import { username } from '../mock_data';
describe('generateBranchName', () => {
const timestamp = 12345678901234;
beforeEach(() => {
jest.spyOn(Date, 'now').mockReturnValueOnce(timestamp);
});
it('generates a name that includes the username and target branch', () => {
expect(generateBranchName(username)).toMatch(`${username}-${DEFAULT_TARGET_BRANCH}`);
});
it(`adds the first ${BRANCH_SUFFIX_COUNT} numbers of the current timestamp`, () => {
expect(generateBranchName(username)).toMatch(
timestamp.toString().substring(BRANCH_SUFFIX_COUNT),
);
});
});
import Api from '~/api';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import {
DEFAULT_TARGET_BRANCH,
SUBMIT_CHANGES_BRANCH_ERROR,
SUBMIT_CHANGES_COMMIT_ERROR,
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
} from '~/static_site_editor/constants';
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
import {
username,
projectId,
commitMultipleResponse,
createMergeRequestResponse,
sourcePath,
sourceContent as content,
} from '../mock_data';
jest.mock('~/static_site_editor/services/generate_branch_name');
describe('submitContentChanges', () => {
const mergeRequestTitle = `Update ${sourcePath} file`;
const branch = 'branch-name';
beforeEach(() => {
jest.spyOn(Api, 'createBranch').mockResolvedValue();
jest.spyOn(Api, 'commitMultiple').mockResolvedValue({ data: commitMultipleResponse });
jest
.spyOn(Api, 'createProjectMergeRequest')
.mockResolvedValue({ data: createMergeRequestResponse });
generateBranchName.mockReturnValue(branch);
});
it('creates a branch named after the username and target branch', () => {
return submitContentChanges({ username, projectId }).then(() => {
expect(Api.createBranch).toHaveBeenCalledWith(projectId, {
ref: DEFAULT_TARGET_BRANCH,
branch,
});
});
});
it('notifies error when branch could not be created', () => {
Api.createBranch.mockRejectedValueOnce();
expect(submitContentChanges({ username, projectId })).rejects.toThrow(
SUBMIT_CHANGES_BRANCH_ERROR,
);
});
it('commits the content changes to the branch when creating branch succeeds', () => {
return submitContentChanges({ username, projectId, sourcePath, content }).then(() => {
expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, {
branch,
commit_message: mergeRequestTitle,
actions: [
{
action: 'update',
file_path: sourcePath,
content,
},
],
});
});
});
it('notifies error when content could not be committed', () => {
Api.commitMultiple.mockRejectedValueOnce();
expect(submitContentChanges({ username, projectId })).rejects.toThrow(
SUBMIT_CHANGES_COMMIT_ERROR,
);
});
it('creates a merge request when commiting changes succeeds', () => {
return submitContentChanges({ username, projectId, sourcePath, content }).then(() => {
expect(Api.createProjectMergeRequest).toHaveBeenCalledWith(
projectId,
convertObjectPropsToSnakeCase({
title: mergeRequestTitle,
targetBranch: DEFAULT_TARGET_BRANCH,
sourceBranch: branch,
}),
);
});
});
it('notifies error when merge request could not be created', () => {
Api.createProjectMergeRequest.mockRejectedValueOnce();
expect(submitContentChanges({ username, projectId })).rejects.toThrow(
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
);
});
describe('when changes are submitted successfully', () => {
let result;
beforeEach(() => {
return submitContentChanges({ username, projectId, sourcePath, content }).then(_result => {
result = _result;
});
});
it('returns the branch name', () => {
expect(result).toMatchObject({ branch: { label: branch } });
});
it('returns commit short id and web url', () => {
expect(result).toMatchObject({
commit: {
label: commitMultipleResponse.short_id,
url: commitMultipleResponse.web_url,
},
});
});
it('returns merge request iid and web url', () => {
expect(result).toMatchObject({
mergeRequest: {
label: createMergeRequestResponse.iid,
url: createMergeRequestResponse.web_url,
},
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment