feat(web): create, update and merge GitLab merge requests
This commit is contained in:
@@ -145,6 +145,124 @@ describe('createWebGitLabAPI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('posts to /api/gitlab/mrs/create with the input body and returns the created MR', async () => {
|
||||
const created = {
|
||||
connected: true,
|
||||
repo: null,
|
||||
mr: {
|
||||
number: 12,
|
||||
title: 'Add feature',
|
||||
url: 'https://gitlab.com/group/sub/-/merge_requests/12',
|
||||
state: 'opened',
|
||||
draft: false,
|
||||
author: { username: 'octocat', id: 1 },
|
||||
sourceBranch: 'feat/add',
|
||||
targetBranch: 'main',
|
||||
},
|
||||
};
|
||||
runtimeFetchMock.mockResolvedValueOnce(Response.json(created));
|
||||
|
||||
const api = await createAPI();
|
||||
await expect(api.mrCreate({
|
||||
directory: '/workspace',
|
||||
title: 'Add feature',
|
||||
sourceBranch: 'feat/add',
|
||||
targetBranch: 'main',
|
||||
removeSourceBranch: true,
|
||||
})).resolves.toEqual(created.mr);
|
||||
|
||||
expect(runtimeFetchMock).toHaveBeenCalledWith('/api/gitlab/mrs/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({
|
||||
directory: '/workspace',
|
||||
title: 'Add feature',
|
||||
sourceBranch: 'feat/add',
|
||||
targetBranch: 'main',
|
||||
removeSourceBranch: true,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws the server error when mrCreate fails', async () => {
|
||||
runtimeFetchMock.mockResolvedValueOnce(Response.json(
|
||||
{ error: 'Your GitLab token needs the api scope to create merge requests' },
|
||||
{ status: 400 },
|
||||
));
|
||||
|
||||
const api = await createAPI();
|
||||
await expect(api.mrCreate({
|
||||
directory: '/workspace',
|
||||
title: 'Add feature',
|
||||
sourceBranch: 'feat/add',
|
||||
targetBranch: 'main',
|
||||
})).rejects.toThrow('Your GitLab token needs the api scope to create merge requests');
|
||||
});
|
||||
|
||||
it('PUTs to /api/gitlab/mrs/update with the input body and returns the updated MR', async () => {
|
||||
const updated = {
|
||||
connected: true,
|
||||
repo: null,
|
||||
mr: { number: 12, title: 'Renamed', url: 'u', state: 'opened', draft: false, author: { username: 'octocat', id: 1 }, sourceBranch: 'feat/add', targetBranch: 'main' },
|
||||
};
|
||||
runtimeFetchMock.mockResolvedValueOnce(Response.json(updated));
|
||||
|
||||
const api = await createAPI();
|
||||
await expect(api.mrUpdate({ directory: '/workspace', number: 12, title: 'Renamed', description: 'New body' })).resolves.toEqual(updated.mr);
|
||||
|
||||
expect(runtimeFetchMock).toHaveBeenCalledWith('/api/gitlab/mrs/update', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ directory: '/workspace', number: 12, title: 'Renamed', description: 'New body' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns merged:false without throwing when the server rejects a merge', async () => {
|
||||
runtimeFetchMock.mockResolvedValueOnce(Response.json(
|
||||
{ connected: true, merged: false, message: '405 Method Not Allowed: not open' },
|
||||
{ status: 405 },
|
||||
));
|
||||
|
||||
const api = await createAPI();
|
||||
await expect(api.mrMerge({ directory: '/workspace', number: 12, squash: true })).resolves.toEqual({
|
||||
connected: true,
|
||||
merged: false,
|
||||
message: '405 Method Not Allowed: not open',
|
||||
});
|
||||
|
||||
expect(runtimeFetchMock).toHaveBeenCalledWith('/api/gitlab/mrs/merge', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ directory: '/workspace', number: 12, squash: true }),
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves merged:true on a successful merge', async () => {
|
||||
runtimeFetchMock.mockResolvedValueOnce(Response.json({ connected: true, merged: true }));
|
||||
|
||||
const api = await createAPI();
|
||||
await expect(api.mrMerge({ directory: '/workspace', number: 12 })).resolves.toEqual({ connected: true, merged: true });
|
||||
});
|
||||
|
||||
it('throws the server error when mrMerge hits a real error payload', async () => {
|
||||
runtimeFetchMock.mockResolvedValueOnce(Response.json(
|
||||
{ error: 'Your GitLab token needs the api scope to create merge requests' },
|
||||
{ status: 400 },
|
||||
));
|
||||
|
||||
const api = await createAPI();
|
||||
await expect(api.mrMerge({ directory: '/workspace', number: 12 })).rejects.toThrow(
|
||||
'Your GitLab token needs the api scope to create merge requests',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws the response status text when mrMerge has no parseable payload', async () => {
|
||||
runtimeFetchMock.mockResolvedValueOnce(new Response('upstream gone', { status: 502, statusText: 'Bad Gateway' }));
|
||||
|
||||
const api = await createAPI();
|
||||
await expect(api.mrMerge({ directory: '/workspace', number: 12 })).rejects.toThrow('Bad Gateway');
|
||||
});
|
||||
|
||||
it('throws the server error message on {error} payloads', async () => {
|
||||
runtimeFetchMock.mockResolvedValueOnce(Response.json({ error: 'Not connected to GitLab' }, { status: 401 }));
|
||||
|
||||
|
||||
@@ -5,8 +5,15 @@ import type {
|
||||
GitLabIssueCommentsResult,
|
||||
GitLabIssueGetResult,
|
||||
GitLabIssuesListResult,
|
||||
GitLabMergeRequest,
|
||||
GitLabMergeRequestContextResult,
|
||||
GitLabMergeRequestCreateInput,
|
||||
GitLabMergeRequestCreateResult,
|
||||
GitLabMergeRequestMergeInput,
|
||||
GitLabMergeRequestMergeResult,
|
||||
GitLabMergeRequestsListResult,
|
||||
GitLabMergeRequestUpdateInput,
|
||||
GitLabMergeRequestUpdateResult,
|
||||
GitLabUserSummary,
|
||||
} from '@openchamber/ui/lib/api/types';
|
||||
import { runtimeFetch } from '@openchamber/ui/lib/runtime-fetch';
|
||||
@@ -175,6 +182,56 @@ export const createWebGitLabAPI = ({ urls }: WebGitLabAPIOptions): GitLabAPI =>
|
||||
return payload;
|
||||
},
|
||||
|
||||
async mrCreate(input: GitLabMergeRequestCreateInput): Promise<GitLabMergeRequest> {
|
||||
const response = await runtimeFetch('/api/gitlab/mrs/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const payload = await jsonOrNull<GitLabMergeRequestCreateResult & { error?: string }>(response);
|
||||
if (!response.ok || !payload?.mr) {
|
||||
throw new Error(payload?.error || response.statusText || 'Failed to create GitLab merge request');
|
||||
}
|
||||
return payload.mr;
|
||||
},
|
||||
|
||||
async mrUpdate(input: GitLabMergeRequestUpdateInput): Promise<GitLabMergeRequest> {
|
||||
const response = await runtimeFetch('/api/gitlab/mrs/update', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const payload = await jsonOrNull<GitLabMergeRequestUpdateResult & { error?: string }>(response);
|
||||
if (!response.ok || !payload?.mr) {
|
||||
throw new Error(payload?.error || response.statusText || 'Failed to update GitLab merge request');
|
||||
}
|
||||
return payload.mr;
|
||||
},
|
||||
|
||||
async mrMerge(input: GitLabMergeRequestMergeInput): Promise<GitLabMergeRequestMergeResult> {
|
||||
const response = await runtimeFetch('/api/gitlab/mrs/merge', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const payload = await jsonOrNull<GitLabMergeRequestMergeResult & { error?: string }>(response);
|
||||
// The server rejects non-mergeable requests with 405/409/422 and a
|
||||
// `{ connected, merged: false, message }` body — parse it and return it
|
||||
// instead of throwing. Only throw when there is no parseable payload
|
||||
// (network failure) or the server surfaced a real `{ error }`.
|
||||
if (!payload) {
|
||||
throw new Error(response.statusText || 'Failed to merge GitLab merge request');
|
||||
}
|
||||
if (payload.error) {
|
||||
throw new Error(payload.error);
|
||||
}
|
||||
return {
|
||||
connected: Boolean(payload.connected),
|
||||
merged: Boolean(payload.merged),
|
||||
...(payload.message ? { message: payload.message } : {}),
|
||||
};
|
||||
},
|
||||
|
||||
async repoBranches(namespace: string, project: string): Promise<string[]> {
|
||||
const response = await runtimeFetch(
|
||||
`/api/gitlab/repo/branches?namespace=${encodeURIComponent(namespace)}&project=${encodeURIComponent(project)}`,
|
||||
|
||||
Reference in New Issue
Block a user