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; let resultData = data;
@@ -63,7 +63,7 @@ export default async function genericProxyHandler(req, res, map) {
.status(status) .status(status)
.json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } }); .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); 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.statusCode).toBe(200);
expect(res.body).toEqual({ mapped: true }); 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: giteaNotifications, error: giteaNotificationsError } = useWidgetAPI(widget, "notifications");
const { data: giteaIssues, error: giteaIssuesError } = useWidgetAPI(widget, "issues"); const { data: giteaIssues, error: giteaIssuesError } = useWidgetAPI(widget, "issues");
const { data: giteaPulls, error: giteaPullsError } = useWidgetAPI(widget, "pulls");
const { data: giteaRepositories, error: giteaRepositoriesError } = useWidgetAPI(widget, "repositories"); const { data: giteaRepositories, error: giteaRepositoriesError } = useWidgetAPI(widget, "repositories");
if (giteaNotificationsError || giteaIssuesError || giteaRepositoriesError) { const error = giteaNotificationsError ?? giteaIssuesError ?? giteaPullsError ?? giteaRepositoriesError;
return ( if (error) {
<Container service={service} error={giteaNotificationsError ?? giteaIssuesError ?? giteaRepositoriesError} /> return <Container service={service} error={error} />;
);
} }
if (!giteaNotifications || !giteaIssues || !giteaRepositories) { if (!giteaNotifications || !giteaIssues || !giteaPulls || !giteaRepositories) {
return ( return (
<Container service={service}> <Container service={service}>
<Block label="gitea.notifications" /> <Block label="gitea.notifications" />
@@ -28,10 +28,10 @@ export default function Component({ service }) {
return ( return (
<Container service={service}> <Container service={service}>
<Block label="gitea.notifications" value={giteaNotifications.length} /> <Block label="gitea.notifications" value={giteaNotifications.count} />
<Block label="gitea.issues" value={giteaIssues.issues.length} /> <Block label="gitea.issues" value={giteaIssues.count} />
<Block label="gitea.pulls" value={giteaIssues.pulls.length} /> <Block label="gitea.pulls" value={giteaPulls.count} />
<Block label="gitea.repositories" value={giteaRepositories.data.length} /> <Block label="gitea.repositories" value={giteaRepositories.count} />
</Container> </Container>
); );
} }
+10 -10
View File
@@ -20,6 +20,7 @@ describe("widgets/gitea/component", () => {
useWidgetAPI useWidgetAPI
.mockReturnValueOnce({ data: undefined, error: undefined }) // notifications .mockReturnValueOnce({ data: undefined, error: undefined }) // notifications
.mockReturnValueOnce({ data: undefined, error: undefined }) // issues .mockReturnValueOnce({ data: undefined, error: undefined }) // issues
.mockReturnValueOnce({ data: undefined, error: undefined }) // pulls
.mockReturnValueOnce({ data: undefined, error: undefined }); // repositories .mockReturnValueOnce({ data: undefined, error: undefined }); // repositories
const { container } = renderWithProviders(<Component service={{ widget: { type: "gitea", url: "http://x" } }} />, { const { container } = renderWithProviders(<Component service={{ widget: { type: "gitea", url: "http://x" } }} />, {
@@ -38,6 +39,7 @@ describe("widgets/gitea/component", () => {
useWidgetAPI useWidgetAPI
.mockReturnValueOnce({ data: undefined, error: undefined }) .mockReturnValueOnce({ data: undefined, error: undefined })
.mockReturnValueOnce({ data: undefined, error: { message: "nope" } }) .mockReturnValueOnce({ data: undefined, error: { message: "nope" } })
.mockReturnValueOnce({ data: undefined, error: undefined })
.mockReturnValueOnce({ data: undefined, error: undefined }); .mockReturnValueOnce({ data: undefined, error: undefined });
renderWithProviders(<Component service={{ widget: { type: "gitea", url: "http://x" } }} />, { renderWithProviders(<Component service={{ widget: { type: "gitea", url: "http://x" } }} />, {
@@ -48,22 +50,20 @@ describe("widgets/gitea/component", () => {
expect(screen.getByText("nope")).toBeInTheDocument(); expect(screen.getByText("nope")).toBeInTheDocument();
}); });
it("renders computed counts when loaded", () => { it("renders total counts when loaded", () => {
useWidgetAPI useWidgetAPI
.mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }], error: undefined }) .mockReturnValueOnce({ data: { count: 2 }, error: undefined })
.mockReturnValueOnce({ .mockReturnValueOnce({ data: { count: 1234 }, error: undefined })
data: { issues: [{ id: 1 }], pulls: [{ id: 1 }, { id: 2 }, { id: 3 }] }, .mockReturnValueOnce({ data: { count: 0 }, error: undefined })
error: undefined, .mockReturnValueOnce({ data: { count: 56 }, error: undefined });
})
.mockReturnValueOnce({ data: { data: [{ id: 1 }] }, error: undefined });
const { container } = renderWithProviders(<Component service={{ widget: { type: "gitea", url: "http://x" } }} />, { const { container } = renderWithProviders(<Component service={{ widget: { type: "gitea", url: "http://x" } }} />, {
settings: { hideErrors: false }, settings: { hideErrors: false },
}); });
expectBlockValue(container, "gitea.notifications", 2); expectBlockValue(container, "gitea.notifications", 2);
expectBlockValue(container, "gitea.issues", 1); expectBlockValue(container, "gitea.issues", 1234);
expectBlockValue(container, "gitea.pulls", 3); expectBlockValue(container, "gitea.pulls", 0);
expectBlockValue(container, "gitea.repositories", 1); expectBlockValue(container, "gitea.repositories", 56);
}); });
}); });
+15 -10
View File
@@ -1,26 +1,31 @@
import { asJson } from "utils/proxy/api-helpers";
import genericProxyHandler from "utils/proxy/handlers/generic"; 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 = { const widget = {
api: "{url}/api/v1/{endpoint}?access_token={key}", api: "{url}/api/v1/{endpoint}?access_token={key}",
proxyHandler: genericProxyHandler, proxyHandler: genericProxyHandler,
mappings: { mappings: {
notifications: { notifications: {
endpoint: "notifications", endpoint: "notifications?limit=1",
map: totalCount,
}, },
issues: { issues: {
endpoint: "repos/issues/search", endpoint: "repos/issues/search?type=issues&limit=1",
map: (data) => { map: totalCount,
const items = asJson(data);
return {
pulls: items.filter((issue) => issue.pull_request),
issues: items.filter((issue) => !issue.pull_request),
};
}, },
pulls: {
endpoint: "repos/issues/search?type=pulls&limit=1",
map: totalCount,
}, },
repositories: { 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 { expectWidgetConfigShape } from "test-utils/widget-config";
import widget from "./widget"; import widget, { totalCount } from "./widget";
describe("gitea widget config", () => { describe("gitea widget config", () => {
it("exports a valid widget config", () => { it("exports a valid widget config", () => {
expectWidgetConfigShape(widget); expectWidgetConfigShape(widget);
}); });
it("splits issues and pull requests", () => { it("reads the total count from the X-Total-Count header", () => {
const result = widget.mappings.issues.map( expect(totalCount(Buffer.from("[]"), { "x-total-count": "1234" })).toEqual({ count: 1234 });
Buffer.from(JSON.stringify([{ id: 1, pull_request: {} }, { id: 2 }, { id: 3 }])), expect(totalCount(Buffer.from("[]"), { "x-total-count": "0" })).toEqual({ count: 0 });
); });
expect(result.pulls.map((i) => i.id)).toEqual([1]); it("returns a null count when the header is missing", () => {
expect(result.issues.map((i) => i.id)).toEqual([2, 3]); expect(totalCount(Buffer.from("[{}]"), {})).toEqual({ count: null });
expect(totalCount(Buffer.from("[{}]"), undefined)).toEqual({ count: null });
}); });
}); });