test(web): gitea issue creation — wire api coverage, module docs, section comment
The Gitea create-issue path (server route, client, wire api, facade, UI
button/dialog) landed with the forge user-lookup work; close the remaining
verification/documentation gaps:
- wire: gitea.test.ts covers issueCreate (POST /api/gitea/issues/create,
full/optional body, error throw)
- docs: DOCUMENTATION.md lists createIssue client method and the
POST /repos/{owner}/{repo}/issues endpoint (labels as names)
- ui: fix stale 'read-only by design' JSDoc in GiteaIssuesSection — creation
is offered here and the detail view provides edit/close/reopen
This commit is contained in:
@@ -18,7 +18,9 @@ const issueLabelBadgeClass =
|
||||
* Open Gitea issues for the context panel's PR view. The list is fetched
|
||||
* lazily (the parent only mounts this component while the Issues tab is
|
||||
* active); selecting a row mounts the shared `ForgeEntityDetailView` for the
|
||||
* issue detail. Read-only by design — no create, update, or close actions.
|
||||
* issue detail. Issue creation is supported here via the "new issue" button
|
||||
* and `ForgeCreateIssueDialog`; the detail view also offers edit, close/reopen,
|
||||
* comments, and metadata editing through the Gitea provider's write methods.
|
||||
*/
|
||||
export const GiteaIssuesSection: React.FC<{ directory: string }> = ({ directory }) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
- This module owns Gitea/Forgejo auth (Personal Access Token), raw REST v1 client access, remote-URL repo resolution, and Gitea issue / pull-request (PR) APIs for OpenChamber, including PR create/update/merge writes.
|
||||
- This module owns Gitea/Forgejo auth (Personal Access Token), raw REST v1 client access, remote-URL repo resolution, and Gitea issue / pull-request (PR) APIs for OpenChamber, including issue create/update and PR create/update/merge writes.
|
||||
- From a user perspective, this is the layer that lets the app show Gitea issues and pull requests for a local project, including comments and per-file diffs, and create, edit, and merge pull requests.
|
||||
- Gitea and Forgejo share the same GitHub-style REST v1 API, so this module serves both. Gitea calls remote work **pull requests** (PR), not merge requests. Gitea repos are flat `owner/repo` — there are no multi-segment namespaces.
|
||||
- The module mirrors `packages/web/server/lib/gitlab/` (PAT auth + raw-fetch client) but uses a **Personal Access Token** against the `Authorization: token <pat>` header and a **user-supplied base URL** (Gitea is self-hosted; there is no default instance).
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
### Client (`client.js`)
|
||||
|
||||
- `createGiteaClient({ token, baseUrl })`: raw-fetch REST v1 client with `request(path, { method, query, body, signal, raw })` plus convenience methods `user()`, `repo(owner, repo)`, `issues(owner, repo, params)`, `issue(owner, repo, number)`, `issueComments(owner, repo, number, params)`, `createIssueComment(owner, repo, number, body)`, `updateIssue(owner, repo, number, params)` (PATCH), `milestones(owner, repo, params)`, `repoLabels(owner, repo, params)`, `pullRequests(owner, repo, params)`, `pullRequest(owner, repo, number)`, `pullRequestDiff(owner, repo, number)` (raw `.diff` text via the `raw` option), `pullRequestFiles(owner, repo, number, params)`, `pullRequestCommits(owner, repo, number, params)`, `pullRequestReviews(owner, repo, number, params)`, `createPullReview(owner, repo, number, params)` (POST), `commitStatuses(owner, repo, sha, params)`, `createPullRequest(owner, repo, body)`, `updatePullRequest(owner, repo, number, body)` (PATCH), `mergePullRequest(owner, repo, number, body)` (POST), `branches(owner, repo, params)`.
|
||||
- `createGiteaClient({ token, baseUrl })`: raw-fetch REST v1 client with `request(path, { method, query, body, signal, raw })` plus convenience methods `user()`, `repo(owner, repo)`, `issues(owner, repo, params)`, `issue(owner, repo, number)`, `issueComments(owner, repo, number, params)`, `createIssueComment(owner, repo, number, body)`, `createIssue(owner, repo, params)` (POST), `updateIssue(owner, repo, number, params)` (PATCH), `milestones(owner, repo, params)`, `repoLabels(owner, repo, params)`, `pullRequests(owner, repo, params)`, `pullRequest(owner, repo, number)`, `pullRequestDiff(owner, repo, number)` (raw `.diff` text via the `raw` option), `pullRequestFiles(owner, repo, number, params)`, `pullRequestCommits(owner, repo, number, params)`, `pullRequestReviews(owner, repo, number, params)`, `createPullReview(owner, repo, number, params)` (POST), `commitStatuses(owner, repo, sha, params)`, `createPullRequest(owner, repo, body)`, `updatePullRequest(owner, repo, number, body)` (PATCH), `mergePullRequest(owner, repo, number, body)` (POST), `branches(owner, repo, params)`.
|
||||
- `getGiteaClientOrNull()`: client for the current account, or `null`.
|
||||
- `isGiteaRateLimited()` / `noteGiteaRateLimit(error)`: own module-level rate-limit cooldown (not shared with the GitHub/GitLab modules).
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
- User: `GET /user` -> `{ id, login, full_name, avatar_url, html_url, email, ... }`.
|
||||
- Issue list: `GET /repos/{owner}/{repo}/issues?type=issues&state=open&limit=50&page=N&q=<query>` (`type=issues` excludes pull requests; entries carrying a `pull_request` field are skipped client-side as a backstop).
|
||||
- Issue detail: `GET /repos/{owner}/{repo}/issues/{number}`.
|
||||
- Issue create: `POST /repos/{owner}/{repo}/issues` with `{ title, body?, labels? }` (labels are label **names**; `body` omitted when absent).
|
||||
- Issue/PR comments: `GET /repos/{owner}/{repo}/issues/{number}/comments`.
|
||||
- PR list: `GET /repos/{owner}/{repo}/pulls?state=open&limit=50&page=N&q=<query>`. Gitea has no server-side source-branch filter, so when `sourceBranch` is requested the route scans `state=all` pages (cap 10 pages) and filters by `head.ref === sourceBranch` client-side, returning all matching states (open and merged).
|
||||
- PR detail: `GET /repos/{owner}/{repo}/pulls/{number}`.
|
||||
|
||||
@@ -260,6 +260,83 @@ describe('createWebGiteaAPI', () => {
|
||||
await expect(api.prMerge({ directory: '/workspace', number: 12 })).rejects.toThrow('Bad Gateway');
|
||||
});
|
||||
|
||||
it('posts to /api/gitea/issues/create with the input body and returns the created issue', async () => {
|
||||
const created = {
|
||||
connected: true,
|
||||
repo: null,
|
||||
issue: {
|
||||
number: 42,
|
||||
title: 'Broken build',
|
||||
url: 'https://gitea.example/owner/repo/issues/42',
|
||||
state: 'open',
|
||||
author: { username: 'octocat', id: 1 },
|
||||
labels: ['bug'],
|
||||
},
|
||||
};
|
||||
runtimeFetchMock.mockResolvedValueOnce(Response.json(created));
|
||||
|
||||
const api = await createAPI();
|
||||
await expect(api.issueCreate!({
|
||||
directory: '/workspace',
|
||||
title: 'Broken build',
|
||||
body: 'The build is failing',
|
||||
labels: ['bug'],
|
||||
owner: 'group',
|
||||
repo: 'repo',
|
||||
})).resolves.toEqual(created);
|
||||
|
||||
expect(runtimeFetchMock).toHaveBeenCalledWith('/api/gitea/issues/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({
|
||||
directory: '/workspace',
|
||||
title: 'Broken build',
|
||||
body: 'The build is failing',
|
||||
labels: ['bug'],
|
||||
owner: 'group',
|
||||
repo: 'repo',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('omits optional body/labels/owner/repo from the issueCreate body when absent', async () => {
|
||||
const created = {
|
||||
connected: true,
|
||||
repo: null,
|
||||
issue: {
|
||||
number: 43,
|
||||
title: 'Title only',
|
||||
url: 'https://gitea.example/owner/repo/issues/43',
|
||||
state: 'open',
|
||||
author: { username: 'octocat', id: 1 },
|
||||
labels: [],
|
||||
},
|
||||
};
|
||||
runtimeFetchMock.mockResolvedValueOnce(Response.json(created));
|
||||
|
||||
const api = await createAPI();
|
||||
await expect(api.issueCreate!({ directory: '/workspace', title: 'Title only' })).resolves.toEqual(created);
|
||||
|
||||
expect(runtimeFetchMock).toHaveBeenCalledWith('/api/gitea/issues/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ directory: '/workspace', title: 'Title only' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws the server error when issueCreate fails', async () => {
|
||||
runtimeFetchMock.mockResolvedValueOnce(Response.json(
|
||||
{ error: 'Your Gitea token needs write:repository scope to create issues' },
|
||||
{ status: 400 },
|
||||
));
|
||||
|
||||
const api = await createAPI();
|
||||
await expect(api.issueCreate!({
|
||||
directory: '/workspace',
|
||||
title: 'Broken build',
|
||||
})).rejects.toThrow('Your Gitea token needs write:repository scope to create issues');
|
||||
});
|
||||
|
||||
it('parses branches and the default branch from repoBranches', async () => {
|
||||
runtimeFetchMock.mockResolvedValueOnce(Response.json({ branches: ['main', 'feat/api'], defaultBranch: 'main' }));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user