Fix: use x-total-count header for correct gitea totals (#7227)

This commit is contained in:
shamoon
2026-10-08 10:10:30 -07:00
committed by GitHub
parent fec0f5d8b2
commit 97db55ab36
6 changed files with 61 additions and 39 deletions
+2 -2
View File
@@ -49,7 +49,7 @@ export default async function genericProxyHandler(req, res, map) {
}
}
const [status, contentType, data] = await httpProxy(url, params);
const [status, contentType, data, responseHeaders] = await httpProxy(url, params);
let resultData = data;
@@ -63,7 +63,7 @@ export default async function genericProxyHandler(req, res, map) {
.status(status)
.json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } });
}
if (map) resultData = map(resultData);
if (map) resultData = map(resultData, responseHeaders);
}
if (contentType) res.setHeader("Content-Type", contentType);
+16
View File
@@ -253,4 +253,20 @@ describe("utils/proxy/handlers/generic", () => {
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ mapped: true });
});
it("passes the response headers to the mapping function", async () => {
getServiceWidget.mockResolvedValue({
type: "testservice",
url: "http://example",
});
httpProxy.mockResolvedValueOnce([200, "application/json", { ok: true }, { "x-total-count": "42" }]);
const req = { method: "GET", query: { group: "g", service: "svc", endpoint: "api", index: "0" } };
const res = createMockRes();
await genericProxyHandler(req, res, (data, headers) => ({ total: headers["x-total-count"] }));
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ total: "42" });
});
});
+9 -9
View File
@@ -7,15 +7,15 @@ export default function Component({ service }) {
const { data: giteaNotifications, error: giteaNotificationsError } = useWidgetAPI(widget, "notifications");
const { data: giteaIssues, error: giteaIssuesError } = useWidgetAPI(widget, "issues");
const { data: giteaPulls, error: giteaPullsError } = useWidgetAPI(widget, "pulls");
const { data: giteaRepositories, error: giteaRepositoriesError } = useWidgetAPI(widget, "repositories");
if (giteaNotificationsError || giteaIssuesError || giteaRepositoriesError) {
return (
<Container service={service} error={giteaNotificationsError ?? giteaIssuesError ?? giteaRepositoriesError} />
);
const error = giteaNotificationsError ?? giteaIssuesError ?? giteaPullsError ?? giteaRepositoriesError;
if (error) {
return <Container service={service} error={error} />;
}
if (!giteaNotifications || !giteaIssues || !giteaRepositories) {
if (!giteaNotifications || !giteaIssues || !giteaPulls || !giteaRepositories) {
return (
<Container service={service}>
<Block label="gitea.notifications" />
@@ -28,10 +28,10 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block label="gitea.notifications" value={giteaNotifications.length} />
<Block label="gitea.issues" value={giteaIssues.issues.length} />
<Block label="gitea.pulls" value={giteaIssues.pulls.length} />
<Block label="gitea.repositories" value={giteaRepositories.data.length} />
<Block label="gitea.notifications" value={giteaNotifications.count} />
<Block label="gitea.issues" value={giteaIssues.count} />
<Block label="gitea.pulls" value={giteaPulls.count} />
<Block label="gitea.repositories" value={giteaRepositories.count} />
</Container>
);
}
+10 -10
View File
@@ -20,6 +20,7 @@ describe("widgets/gitea/component", () => {
useWidgetAPI
.mockReturnValueOnce({ data: undefined, error: undefined }) // notifications
.mockReturnValueOnce({ data: undefined, error: undefined }) // issues
.mockReturnValueOnce({ data: undefined, error: undefined }) // pulls
.mockReturnValueOnce({ data: undefined, error: undefined }); // repositories
const { container } = renderWithProviders(<Component service={{ widget: { type: "gitea", url: "http://x" } }} />, {
@@ -38,6 +39,7 @@ describe("widgets/gitea/component", () => {
useWidgetAPI
.mockReturnValueOnce({ data: undefined, error: undefined })
.mockReturnValueOnce({ data: undefined, error: { message: "nope" } })
.mockReturnValueOnce({ data: undefined, error: undefined })
.mockReturnValueOnce({ data: undefined, error: undefined });
renderWithProviders(<Component service={{ widget: { type: "gitea", url: "http://x" } }} />, {
@@ -48,22 +50,20 @@ describe("widgets/gitea/component", () => {
expect(screen.getByText("nope")).toBeInTheDocument();
});
it("renders computed counts when loaded", () => {
it("renders total counts when loaded", () => {
useWidgetAPI
.mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }], error: undefined })
.mockReturnValueOnce({
data: { issues: [{ id: 1 }], pulls: [{ id: 1 }, { id: 2 }, { id: 3 }] },
error: undefined,
})
.mockReturnValueOnce({ data: { data: [{ id: 1 }] }, error: undefined });
.mockReturnValueOnce({ data: { count: 2 }, error: undefined })
.mockReturnValueOnce({ data: { count: 1234 }, error: undefined })
.mockReturnValueOnce({ data: { count: 0 }, error: undefined })
.mockReturnValueOnce({ data: { count: 56 }, error: undefined });
const { container } = renderWithProviders(<Component service={{ widget: { type: "gitea", url: "http://x" } }} />, {
settings: { hideErrors: false },
});
expectBlockValue(container, "gitea.notifications", 2);
expectBlockValue(container, "gitea.issues", 1);
expectBlockValue(container, "gitea.pulls", 3);
expectBlockValue(container, "gitea.repositories", 1);
expectBlockValue(container, "gitea.issues", 1234);
expectBlockValue(container, "gitea.pulls", 0);
expectBlockValue(container, "gitea.repositories", 56);
});
});
+16 -11
View File
@@ -1,26 +1,31 @@
import { asJson } from "utils/proxy/api-helpers";
import genericProxyHandler from "utils/proxy/handlers/generic";
// Get total from header to avoid pagination limit
export function totalCount(data, headers) {
const count = parseInt(headers?.["x-total-count"], 10);
return { count: Number.isNaN(count) ? null : count };
}
const widget = {
api: "{url}/api/v1/{endpoint}?access_token={key}",
proxyHandler: genericProxyHandler,
mappings: {
notifications: {
endpoint: "notifications",
endpoint: "notifications?limit=1",
map: totalCount,
},
issues: {
endpoint: "repos/issues/search",
map: (data) => {
const items = asJson(data);
return {
pulls: items.filter((issue) => issue.pull_request),
issues: items.filter((issue) => !issue.pull_request),
};
},
endpoint: "repos/issues/search?type=issues&limit=1",
map: totalCount,
},
pulls: {
endpoint: "repos/issues/search?type=pulls&limit=1",
map: totalCount,
},
repositories: {
endpoint: "repos/search",
endpoint: "repos/search?limit=1",
map: totalCount,
},
},
};
+8 -7
View File
@@ -2,19 +2,20 @@ import { describe, expect, it } from "vitest";
import { expectWidgetConfigShape } from "test-utils/widget-config";
import widget from "./widget";
import widget, { totalCount } from "./widget";
describe("gitea widget config", () => {
it("exports a valid widget config", () => {
expectWidgetConfigShape(widget);
});
it("splits issues and pull requests", () => {
const result = widget.mappings.issues.map(
Buffer.from(JSON.stringify([{ id: 1, pull_request: {} }, { id: 2 }, { id: 3 }])),
);
it("reads the total count from the X-Total-Count header", () => {
expect(totalCount(Buffer.from("[]"), { "x-total-count": "1234" })).toEqual({ count: 1234 });
expect(totalCount(Buffer.from("[]"), { "x-total-count": "0" })).toEqual({ count: 0 });
});
expect(result.pulls.map((i) => i.id)).toEqual([1]);
expect(result.issues.map((i) => i.id)).toEqual([2, 3]);
it("returns a null count when the header is missing", () => {
expect(totalCount(Buffer.from("[{}]"), {})).toEqual({ count: null });
expect(totalCount(Buffer.from("[{}]"), undefined)).toEqual({ count: null });
});
});