mirror of
https://github.com/gethomepage/homepage.git
synced 2026-10-15 15:09:56 -07:00
Fix: use x-total-count header for correct gitea totals (#7227)
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+16
-11
@@ -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: {
|
||||||
pulls: items.filter((issue) => issue.pull_request),
|
endpoint: "repos/issues/search?type=pulls&limit=1",
|
||||||
issues: items.filter((issue) => !issue.pull_request),
|
map: totalCount,
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
repositories: {
|
repositories: {
|
||||||
endpoint: "repos/search",
|
endpoint: "repos/search?limit=1",
|
||||||
|
map: totalCount,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user