feat(ui): rich forge entity views — commits, files/diff, timeline, checks, metadata chips
- server: new read routes for PR/MR commits, timeline, reviews, commit-statuses across github/gitlab/gitea; enrich PR/issue summaries with labels/assignees/milestone/commentsCount - ui: forge facade gains getCommits/getTimeline/getChecks; shared ForgeEntityDetailView + section components; mounted into PR/MR views and issue sections
This commit is contained in:
@@ -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)`, `pullRequests(owner, repo, params)`, `pullRequest(owner, repo, number)`, `pullRequestDiff(owner, repo, number)` (raw `.diff` text via the `raw` option), `pullRequestFiles(owner, repo, number, 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)`, `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)`, `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).
|
||||
|
||||
@@ -72,6 +72,9 @@
|
||||
- PR detail: `GET /repos/{owner}/{repo}/pulls/{number}`.
|
||||
- PR files: `GET /repos/{owner}/{repo}/pulls/{number}/files?patch=true` (capitalized JSON fields `Filename`/`Status`/`Additions`/`Deletions`/`Patch`; a `404` on older Gitea instances falls back to `files: []`).
|
||||
- PR diff: `GET /repos/{owner}/{repo}/pulls/{number}.diff` (raw text; falls back to concatenated per-file patches when it fails).
|
||||
- PR commits: `GET /repos/{owner}/{repo}/pulls/{number}/commits?limit=100` (mapped to `{ sha, message, summary, author, committedAt, parents }`).
|
||||
- PR reviews: `GET /repos/{owner}/{repo}/pulls/{number}/reviews?limit=100` (mapped to `{ id, state, author, submittedAt, body, commitSha }`; `state` passes through, e.g. `APPROVED`/`REQUEST_CHANGES`).
|
||||
- Commit statuses: `GET /repos/{owner}/{repo}/commits/{sha}/statuses?limit=100` (the `prs/statuses` route resolves the PR `head.sha` first, then maps statuses to `{ state, name, description, url, createdAt }` with `state` lowercased).
|
||||
- PR create: `POST /repos/{owner}/{repo}/pulls` with `{ title, head, base, body? }` (body omitted when absent).
|
||||
- PR update: `PATCH /repos/{owner}/{repo}/pulls/{number}` with `{ title?, body? }` (undefined fields omitted).
|
||||
- PR merge: `POST /repos/{owner}/{repo}/pulls/{number}/merge` with `{ Do: true, MergeMethod: 'merge' | 'squash' | 'rebase' }` (`method` defaults to `'merge'`).
|
||||
@@ -92,6 +95,9 @@
|
||||
| GET | `/api/gitea/issues/comments` | `?directory&number&owner&repo` -> `{ connected, repo?, comments[] }` |
|
||||
| GET | `/api/gitea/prs/list` | `?directory&page&query&sourceBranch` -> `{ connected, repo?, prs[], page, hasMore }` |
|
||||
| GET | `/api/gitea/pr/context` | `?directory&number&includeDiff&owner&repo` -> `{ connected, repo?, pr, comments[], files[], diff? }` |
|
||||
| GET | `/api/gitea/prs/commits` | `?directory&number&owner&repo` -> `{ connected, repo?, commits[] }` |
|
||||
| GET | `/api/gitea/prs/reviews` | `?directory&number&owner&repo` -> `{ connected, repo?, reviews[] }` |
|
||||
| GET | `/api/gitea/prs/statuses` | `?directory&number&owner&repo` -> `{ connected, repo?, statuses[] }` (resolves the PR `head.sha` first, then lists commit statuses for that SHA) |
|
||||
| POST | `/api/gitea/pr/create` | body `{ directory, title, sourceBranch, targetBranch, description? }` -> `{ connected, repo?, pr }`; `400` for missing fields or an unresolvable repo |
|
||||
| PATCH | `/api/gitea/pr/update` | body `{ directory, number, title?, description? }` -> `{ connected, repo?, pr }`; `404` when the PR does not exist |
|
||||
| POST | `/api/gitea/pr/merge` | body `{ directory, number, method? }` -> `{ connected, merged: true }` on success; non-mergeable PRs -> the Gitea status (`405`/`409`/`422`) with `{ connected, merged: false, message }` |
|
||||
|
||||
@@ -271,6 +271,12 @@ export function createGiteaClient({ token, baseUrl }) {
|
||||
request(`/repos/${owner}/${repo}/pulls/${number}.diff`, { raw: true }),
|
||||
pullRequestFiles: (owner, repo, number, params = {}) =>
|
||||
request(`/repos/${owner}/${repo}/pulls/${number}/files`, { query: params }),
|
||||
pullRequestCommits: (owner, repo, number, params = {}) =>
|
||||
request(`/repos/${owner}/${repo}/pulls/${number}/commits`, { query: params }),
|
||||
pullRequestReviews: (owner, repo, number, params = {}) =>
|
||||
request(`/repos/${owner}/${repo}/pulls/${number}/reviews`, { query: params }),
|
||||
commitStatuses: (owner, repo, sha, params = {}) =>
|
||||
request(`/repos/${owner}/${repo}/commits/${sha}/statuses`, { query: params }),
|
||||
createPullRequest: (owner, repo, body) =>
|
||||
request(`/repos/${owner}/${repo}/pulls`, { method: 'POST', body }),
|
||||
updatePullRequest: (owner, repo, number, body) =>
|
||||
|
||||
@@ -75,6 +75,16 @@ const mapGiteaIssueSummary = (item) => ({
|
||||
state: typeof item.state === 'string' ? item.state : 'open',
|
||||
author: mapGiteaAuthor(item.user) || {},
|
||||
labels: mapGiteaLabels(item.labels),
|
||||
assignees: Array.isArray(item.assignees)
|
||||
? item.assignees.map(mapGiteaAuthor).filter((user) => user?.username)
|
||||
: [],
|
||||
milestone: item.milestone && typeof item.milestone === 'object'
|
||||
? {
|
||||
title: typeof item.milestone.title === 'string' ? item.milestone.title : '',
|
||||
...(typeof item.milestone.state === 'string' ? { state: item.milestone.state } : {}),
|
||||
}
|
||||
: null,
|
||||
commentsCount: typeof item.comments === 'number' ? item.comments : undefined,
|
||||
});
|
||||
|
||||
const mapGiteaPullRequestSummary = (item) => {
|
||||
@@ -88,6 +98,16 @@ const mapGiteaPullRequestSummary = (item) => {
|
||||
draft: Boolean(item.draft),
|
||||
author: mapGiteaAuthor(item.user) || {},
|
||||
labels: mapGiteaLabels(item.labels),
|
||||
assignees: Array.isArray(item.assignees)
|
||||
? item.assignees.map(mapGiteaAuthor).filter((user) => user?.username)
|
||||
: [],
|
||||
milestone: item.milestone && typeof item.milestone === 'object'
|
||||
? {
|
||||
title: typeof item.milestone.title === 'string' ? item.milestone.title : '',
|
||||
...(typeof item.milestone.state === 'string' ? { state: item.milestone.state } : {}),
|
||||
}
|
||||
: null,
|
||||
commentsCount: typeof item.comments === 'number' ? item.comments : undefined,
|
||||
sourceBranch: typeof item.head?.ref === 'string' ? item.head.ref : '',
|
||||
targetBranch: typeof item.base?.ref === 'string' ? item.base.ref : '',
|
||||
};
|
||||
@@ -657,6 +677,203 @@ export function registerGiteaRoutes(app, options = {}) {
|
||||
}
|
||||
});
|
||||
|
||||
// ================= Gitea Pull Request Enrichment APIs =================
|
||||
|
||||
// Gitea commit objects carry a top-level `author` (the GitHub-style user,
|
||||
// with `login`) plus a `commit.author` (git identity: name/email/date).
|
||||
// Prefer the user when present, else fall back to the git identity so the
|
||||
// author chip still renders something useful.
|
||||
const mapGiteaCommitAuthor = (commit) => {
|
||||
const userAuthor = mapGiteaAuthor(commit?.author);
|
||||
if (userAuthor?.username) {
|
||||
return userAuthor;
|
||||
}
|
||||
const gitAuthor = commit?.commit?.author;
|
||||
if (gitAuthor && typeof gitAuthor.name === 'string' && gitAuthor.name) {
|
||||
return {
|
||||
username: gitAuthor.name,
|
||||
...(typeof gitAuthor.email === 'string' ? { email: gitAuthor.email } : {}),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
app.get('/api/gitea/prs/commits', async (req, res) => {
|
||||
try {
|
||||
const directory = asString(req.query?.directory);
|
||||
const requestedRepo = getRequestedRepo(req);
|
||||
const number = getRequiredNumber(req);
|
||||
if (!directory && !requestedRepo) {
|
||||
return res.status(400).json({ error: 'directory or owner/repo is required' });
|
||||
}
|
||||
if (!number) {
|
||||
return res.status(400).json({ error: 'number is required' });
|
||||
}
|
||||
|
||||
const client = await getClient();
|
||||
if (!client) {
|
||||
return res.json({ connected: false, commits: [] });
|
||||
}
|
||||
|
||||
const { owner, repo, repoRef } = await resolveRepoForRequest(directory, requestedRepo);
|
||||
if (!owner || !repo) {
|
||||
return res.json({ connected: true, repo: null, commits: [] });
|
||||
}
|
||||
|
||||
const resp = await withTimeout(
|
||||
client.pullRequestCommits(owner, repo, number, { limit: 100 }),
|
||||
ROUTE_TIMEOUT_MS,
|
||||
'gitea pr commits',
|
||||
);
|
||||
if (resp.status === 429) {
|
||||
return res.status(503).json({ error: 'Gitea rate limited' });
|
||||
}
|
||||
if (resp.status === 404) {
|
||||
return res.status(404).json({ error: 'Pull request not found' });
|
||||
}
|
||||
if (resp.status !== 200) {
|
||||
return res.status(502).json({ error: 'Gitea returned an error while fetching pull request commits' });
|
||||
}
|
||||
|
||||
const commits = (Array.isArray(resp.data) ? resp.data : []).map((commit) => {
|
||||
const message = typeof commit.commit?.message === 'string' ? commit.commit.message : '';
|
||||
return {
|
||||
sha: typeof commit.sha === 'string' ? commit.sha : '',
|
||||
message,
|
||||
...(message.split('\n')[0] ? { summary: message.split('\n')[0] } : {}),
|
||||
author: mapGiteaCommitAuthor(commit),
|
||||
...(typeof commit.commit?.author?.date === 'string' ? { committedAt: commit.commit.author.date } : {}),
|
||||
parents: Array.isArray(commit.parents) ? commit.parents.map((parent) => parent.sha) : [],
|
||||
};
|
||||
});
|
||||
|
||||
return res.json({ connected: true, repo: repoRef || repoRefFromOwnerRepo(owner, repo, client.baseUrl), commits });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch Gitea pull request commits:', error);
|
||||
return res.status(500).json({ error: error.message || 'Failed to fetch Gitea pull request commits' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/prs/reviews', async (req, res) => {
|
||||
try {
|
||||
const directory = asString(req.query?.directory);
|
||||
const requestedRepo = getRequestedRepo(req);
|
||||
const number = getRequiredNumber(req);
|
||||
if (!directory && !requestedRepo) {
|
||||
return res.status(400).json({ error: 'directory or owner/repo is required' });
|
||||
}
|
||||
if (!number) {
|
||||
return res.status(400).json({ error: 'number is required' });
|
||||
}
|
||||
|
||||
const client = await getClient();
|
||||
if (!client) {
|
||||
return res.json({ connected: false, reviews: [] });
|
||||
}
|
||||
|
||||
const { owner, repo, repoRef } = await resolveRepoForRequest(directory, requestedRepo);
|
||||
if (!owner || !repo) {
|
||||
return res.json({ connected: true, repo: null, reviews: [] });
|
||||
}
|
||||
|
||||
const resp = await withTimeout(
|
||||
client.pullRequestReviews(owner, repo, number, { limit: 100 }),
|
||||
ROUTE_TIMEOUT_MS,
|
||||
'gitea pr reviews',
|
||||
);
|
||||
if (resp.status === 429) {
|
||||
return res.status(503).json({ error: 'Gitea rate limited' });
|
||||
}
|
||||
if (resp.status === 404) {
|
||||
return res.status(404).json({ error: 'Pull request not found' });
|
||||
}
|
||||
if (resp.status !== 200) {
|
||||
return res.status(502).json({ error: 'Gitea returned an error while fetching pull request reviews' });
|
||||
}
|
||||
|
||||
const reviews = (Array.isArray(resp.data) ? resp.data : []).map((review) => ({
|
||||
id: String(review.id),
|
||||
state: typeof review.state === 'string' ? review.state : 'PENDING',
|
||||
author: mapGiteaAuthor(review.user) || null,
|
||||
...(typeof review.submitted_at === 'string' ? { submittedAt: review.submitted_at } : {}),
|
||||
body: typeof review.body === 'string' ? review.body : null,
|
||||
...(typeof review.commit_id === 'string' ? { commitSha: review.commit_id } : null),
|
||||
}));
|
||||
|
||||
return res.json({ connected: true, repo: repoRef || repoRefFromOwnerRepo(owner, repo, client.baseUrl), reviews });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch Gitea pull request reviews:', error);
|
||||
return res.status(500).json({ error: error.message || 'Failed to fetch Gitea pull request reviews' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/gitea/prs/statuses', async (req, res) => {
|
||||
try {
|
||||
const directory = asString(req.query?.directory);
|
||||
const requestedRepo = getRequestedRepo(req);
|
||||
const number = getRequiredNumber(req);
|
||||
if (!directory && !requestedRepo) {
|
||||
return res.status(400).json({ error: 'directory or owner/repo is required' });
|
||||
}
|
||||
if (!number) {
|
||||
return res.status(400).json({ error: 'number is required' });
|
||||
}
|
||||
|
||||
const client = await getClient();
|
||||
if (!client) {
|
||||
return res.json({ connected: false, statuses: [] });
|
||||
}
|
||||
|
||||
const { owner, repo, repoRef } = await resolveRepoForRequest(directory, requestedRepo);
|
||||
if (!owner || !repo) {
|
||||
return res.json({ connected: true, repo: null, statuses: [] });
|
||||
}
|
||||
|
||||
// Resolve the PR head SHA first — Gitea's commit-status endpoint is
|
||||
// keyed by commit SHA, not PR number. The PR payload's `head.sha` is the
|
||||
// head commit (same field family as `head.ref` used elsewhere).
|
||||
const prResp = await withTimeout(client.pullRequest(owner, repo, number), ROUTE_TIMEOUT_MS, 'gitea pr statuses');
|
||||
if (prResp.status === 429) {
|
||||
return res.status(503).json({ error: 'Gitea rate limited' });
|
||||
}
|
||||
if (prResp.status === 404) {
|
||||
return res.status(404).json({ error: 'Pull request not found' });
|
||||
}
|
||||
if (prResp.status !== 200 || !prResp.data) {
|
||||
return res.status(502).json({ error: 'Gitea returned an error while fetching the pull request' });
|
||||
}
|
||||
const headSha = typeof prResp.data.head?.sha === 'string' ? prResp.data.head.sha : null;
|
||||
if (!headSha) {
|
||||
return res.json({ connected: true, repo: repoRef || repoRefFromOwnerRepo(owner, repo, client.baseUrl), statuses: [] });
|
||||
}
|
||||
|
||||
const statusesResp = await withTimeout(
|
||||
client.commitStatuses(owner, repo, headSha, { limit: 100 }),
|
||||
ROUTE_TIMEOUT_MS,
|
||||
'gitea commit statuses',
|
||||
);
|
||||
if (statusesResp.status === 429) {
|
||||
return res.status(503).json({ error: 'Gitea rate limited' });
|
||||
}
|
||||
if (statusesResp.status !== 200) {
|
||||
return res.status(502).json({ error: 'Gitea returned an error while fetching commit statuses' });
|
||||
}
|
||||
|
||||
const statuses = (Array.isArray(statusesResp.data) ? statusesResp.data : []).map((status) => ({
|
||||
state: typeof status.state === 'string' ? status.state.toLowerCase() : 'unknown',
|
||||
name: typeof status.context === 'string' ? status.context : '',
|
||||
...(typeof status.description === 'string' ? { description: status.description } : null),
|
||||
...(typeof status.target_url === 'string' ? { url: status.target_url } : null),
|
||||
...(typeof status.created_at === 'string' ? { createdAt: status.created_at } : {}),
|
||||
}));
|
||||
|
||||
return res.json({ connected: true, repo: repoRef || repoRefFromOwnerRepo(owner, repo, client.baseUrl), statuses });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch Gitea pull request statuses:', error);
|
||||
return res.status(500).json({ error: error.message || 'Failed to fetch Gitea pull request statuses' });
|
||||
}
|
||||
});
|
||||
|
||||
// ================= Gitea Pull Request Write APIs =================
|
||||
|
||||
app.post('/api/gitea/pr/create', async (req, res) => {
|
||||
|
||||
@@ -921,6 +921,131 @@ describe('Gitea data routes', () => {
|
||||
expect(response.body).toEqual({ error: 'Your Gitea token needs write:repository scope to merge pull requests' });
|
||||
});
|
||||
|
||||
test('prs/commits maps pull request commits with summaries', async () => {
|
||||
scriptedFetch([
|
||||
(url) => (matches(/\/pulls\/9\/commits\?/)(url)
|
||||
? jsonResponse([
|
||||
{
|
||||
sha: 'abc123def4567890',
|
||||
commit: {
|
||||
message: 'Add the API\n\nAdds the public API',
|
||||
author: { name: 'Alice Example', email: 'alice@example.com', date: '2026-01-01T10:00:00Z' },
|
||||
},
|
||||
author: { id: 42, login: 'alice', full_name: 'Alice Example' },
|
||||
parents: [{ sha: 'parent-one' }, { sha: 'parent-two' }],
|
||||
},
|
||||
])
|
||||
: null),
|
||||
]);
|
||||
|
||||
const app = createApp();
|
||||
const response = await request(app).get('/api/gitea/prs/commits?directory=%2Ftmp%2Fwork&number=9');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toMatchObject({
|
||||
connected: true,
|
||||
repo: { owner: 'owner', repo: 'repo' },
|
||||
commits: [
|
||||
{
|
||||
sha: 'abc123def4567890',
|
||||
message: 'Add the API\n\nAdds the public API',
|
||||
summary: 'Add the API',
|
||||
author: { username: 'alice', id: 42 },
|
||||
committedAt: '2026-01-01T10:00:00Z',
|
||||
parents: ['parent-one', 'parent-two'],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('prs/reviews passes review state through and maps the author', async () => {
|
||||
scriptedFetch([
|
||||
(url) => (matches(/\/pulls\/9\/reviews\?/)(url)
|
||||
? jsonResponse([
|
||||
{
|
||||
id: 101,
|
||||
state: 'APPROVED',
|
||||
user: { id: 42, login: 'alice', full_name: 'Alice Example' },
|
||||
submitted_at: '2026-01-02T11:00:00Z',
|
||||
body: 'LGTM',
|
||||
commit_id: 'abc123def4567890',
|
||||
},
|
||||
])
|
||||
: null),
|
||||
]);
|
||||
|
||||
const app = createApp();
|
||||
const response = await request(app).get('/api/gitea/prs/reviews?directory=%2Ftmp%2Fwork&number=9');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toMatchObject({
|
||||
connected: true,
|
||||
reviews: [
|
||||
{
|
||||
id: '101',
|
||||
state: 'APPROVED',
|
||||
author: { username: 'alice', id: 42 },
|
||||
submittedAt: '2026-01-02T11:00:00Z',
|
||||
body: 'LGTM',
|
||||
commitSha: 'abc123def4567890',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('prs/statuses resolves the PR head SHA then maps commit statuses', async () => {
|
||||
const fetchMock = scriptedFetch([
|
||||
(url) => (matches(/\/pulls\/9$/)(url)
|
||||
? jsonResponse({
|
||||
number: 9,
|
||||
title: 'Add the API',
|
||||
html_url: 'u',
|
||||
state: 'open',
|
||||
merged: false,
|
||||
user: { login: 'alice' },
|
||||
head: { ref: 'feat/api', sha: 'abc123def4567890' },
|
||||
base: { ref: 'main' },
|
||||
})
|
||||
: null),
|
||||
(url) => (matches(/\/commits\/abc123def4567890\/statuses\?/)(url)
|
||||
? jsonResponse([
|
||||
{ id: 1, state: 'success', context: 'ci/test', description: 'All good', target_url: 'https://ci.example.com/run/1', created_at: '2026-01-02T12:00:00Z' },
|
||||
{ id: 2, state: 'error', context: 'lint' },
|
||||
{ id: 3, state: 'warning', context: 'docs' },
|
||||
])
|
||||
: null),
|
||||
]);
|
||||
|
||||
const app = createApp();
|
||||
const response = await request(app).get('/api/gitea/prs/statuses?directory=%2Ftmp%2Fwork&number=9');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toMatchObject({
|
||||
connected: true,
|
||||
repo: { owner: 'owner', repo: 'repo' },
|
||||
statuses: [
|
||||
{ state: 'success', name: 'ci/test', description: 'All good', url: 'https://ci.example.com/run/1', createdAt: '2026-01-02T12:00:00Z' },
|
||||
{ state: 'error', name: 'lint' },
|
||||
{ state: 'warning', name: 'docs' },
|
||||
],
|
||||
});
|
||||
|
||||
const requestedUrls = fetchMock.mock.calls.map(([url]) => String(url));
|
||||
expect(requestedUrls.some((url) => url.includes('/pulls/9'))).toBe(true);
|
||||
expect(requestedUrls.some((url) => url.includes('/commits/abc123def4567890/statuses'))).toBe(true);
|
||||
});
|
||||
|
||||
test('prs/commits, prs/reviews, and prs/statuses report connected:false when not authenticated', async () => {
|
||||
clearGiteaAuth();
|
||||
const app = createApp();
|
||||
const commits = await request(app).get('/api/gitea/prs/commits?directory=%2Ftmp%2Fwork&number=9');
|
||||
const reviews = await request(app).get('/api/gitea/prs/reviews?directory=%2Ftmp%2Fwork&number=9');
|
||||
const statuses = await request(app).get('/api/gitea/prs/statuses?directory=%2Ftmp%2Fwork&number=9');
|
||||
expect(commits.body).toMatchObject({ connected: false, commits: [] });
|
||||
expect(reviews.body).toMatchObject({ connected: false, reviews: [] });
|
||||
expect(statuses.body).toMatchObject({ connected: false, statuses: [] });
|
||||
});
|
||||
|
||||
test('data routes surface a 503 when Gitea rate limits', async () => {
|
||||
scriptedFetch([(url) => (matches(/\/issues\?/)(url) ? jsonResponse({}, { status: 429, headers: { 'retry-after': '30' } }) : null)]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user