mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-26 14:01:16 -07:00
Docker CI / Docker Build & Push (push) Canceled after 0s
Lint / Linting Checks (push) Canceled after 0s
Release Drafter / Update Release Draft (push) Canceled after 0s
Release Drafter / Auto Label PR (push) Canceled after 0s
Tests / vitest (1) (push) Canceled after 0s
Tests / vitest (2) (push) Canceled after 0s
Tests / vitest (3) (push) Canceled after 0s
Tests / vitest (4) (push) Canceled after 0s
184 lines
6.5 KiB
JavaScript
184 lines
6.5 KiB
JavaScript
import cache from "memory-cache";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import createMockRes from "test-utils/create-mock-res";
|
|
|
|
const { httpProxy, getServiceWidget, logger } = vi.hoisted(() => ({
|
|
httpProxy: vi.fn(),
|
|
getServiceWidget: vi.fn(),
|
|
logger: {
|
|
debug: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("utils/logger", () => ({
|
|
default: () => logger,
|
|
}));
|
|
|
|
vi.mock("utils/config/service-helpers", () => ({
|
|
default: getServiceWidget,
|
|
}));
|
|
|
|
vi.mock("utils/proxy/http", () => ({
|
|
httpProxy,
|
|
}));
|
|
|
|
import feedProxyHandler from "./proxy";
|
|
|
|
const items = Array.from(
|
|
{ length: 7 },
|
|
(_, i) => `<item><title>Item ${i}</title><link>https://example.com/${i}</link>
|
|
<media:thumbnail url="https://example.com/${i}.jpg" /></item>`,
|
|
).join("");
|
|
const feed = `<rss xmlns:media="http://search.yahoo.com/mrss/"><channel>${items}</channel></rss>`;
|
|
|
|
const req = { query: { group: "g", service: "svc", index: "0" } };
|
|
|
|
describe("widgets/feed/proxy", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
cache.clear();
|
|
});
|
|
|
|
it("returns 400 when the url is missing or invalid", async () => {
|
|
getServiceWidget.mockResolvedValueOnce({ type: "feed" });
|
|
let res = createMockRes();
|
|
await feedProxyHandler(req, res);
|
|
expect(res.statusCode).toBe(400);
|
|
expect(res.body).toEqual({ error: "Missing feed URL" });
|
|
|
|
getServiceWidget.mockResolvedValueOnce({ type: "feed", url: "nope" });
|
|
res = createMockRes();
|
|
await feedProxyHandler(req, res);
|
|
expect(res.statusCode).toBe(400);
|
|
expect(res.body).toEqual({ error: "Invalid feed URL" });
|
|
|
|
getServiceWidget.mockResolvedValueOnce({ type: "feed", url: "file:///etc/passwd" });
|
|
res = createMockRes();
|
|
await feedProxyHandler(req, res);
|
|
expect(res.statusCode).toBe(400);
|
|
expect(res.body).toEqual({ error: "Invalid feed URL" });
|
|
expect(httpProxy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns the first 5 items by default", async () => {
|
|
getServiceWidget.mockResolvedValue({ type: "feed", url: "https://example.com/feed.xml" });
|
|
httpProxy.mockResolvedValueOnce([200, "application/rss+xml", Buffer.from(feed)]);
|
|
|
|
const res = createMockRes();
|
|
await feedProxyHandler(req, res);
|
|
|
|
expect(httpProxy.mock.calls[0][0].href).toBe("https://example.com/feed.xml");
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.items).toHaveLength(5);
|
|
expect(res.body.items[0]).toEqual({
|
|
title: "Item 0",
|
|
link: "https://example.com/0",
|
|
date: null,
|
|
image: "https://example.com/0.jpg",
|
|
});
|
|
});
|
|
|
|
it.each([-1, 0, "abc"])("falls back to 5 items for maxItems %j", async (maxItems) => {
|
|
getServiceWidget.mockResolvedValue({ type: "feed", url: "https://example.com/feed.xml", maxItems });
|
|
httpProxy.mockResolvedValueOnce([200, "application/rss+xml", Buffer.from(feed)]);
|
|
|
|
const res = createMockRes();
|
|
await feedProxyHandler(req, res);
|
|
|
|
expect(res.body.items).toHaveLength(5);
|
|
});
|
|
|
|
it("respects maxItems and drops images when disabled", async () => {
|
|
getServiceWidget.mockResolvedValue({
|
|
type: "feed",
|
|
url: "https://example.com/feed.xml",
|
|
maxItems: "2",
|
|
images: false,
|
|
});
|
|
httpProxy.mockResolvedValueOnce([200, "application/rss+xml", Buffer.from(feed)]);
|
|
|
|
const res = createMockRes();
|
|
await feedProxyHandler(req, res);
|
|
|
|
expect(res.body.items).toEqual([
|
|
{ title: "Item 0", link: "https://example.com/0", date: null },
|
|
{ title: "Item 1", link: "https://example.com/1", date: null },
|
|
]);
|
|
});
|
|
|
|
it("passes through http errors without leaking the url", async () => {
|
|
getServiceWidget.mockResolvedValue({ type: "feed", url: "https://example.com/feed.xml?token=secret" });
|
|
httpProxy.mockResolvedValueOnce([404, "text/html", Buffer.from("Not found")]);
|
|
|
|
const res = createMockRes();
|
|
await feedProxyHandler(req, res);
|
|
|
|
expect(res.statusCode).toBe(404);
|
|
expect(res.body).toEqual({ error: { message: "HTTP Error", url: "example.com (see logs for details)" } });
|
|
});
|
|
|
|
it("never includes any part of the configured url in the response", async () => {
|
|
getServiceWidget.mockResolvedValue({
|
|
type: "feed",
|
|
url: "https://user:hunter2@nas.lan/feeds/s3cr3t/rss.xml?token=abc123",
|
|
});
|
|
const relative = `<rss><channel>
|
|
<item><title>Path</title><link>item/1</link><enclosure url="img.jpg" type="image/jpeg" /></item>
|
|
<item><title>Root</title><link>/x</link></item>
|
|
<item><title>Fragment</title><link>#top</link></item>
|
|
</channel></rss>`;
|
|
httpProxy.mockResolvedValueOnce([200, "application/rss+xml", Buffer.from(relative)]);
|
|
|
|
const res = createMockRes();
|
|
await feedProxyHandler(req, res);
|
|
|
|
expect(res.body.items.map((item) => item.link)).toEqual([null, null, null]);
|
|
expect(JSON.stringify(res.body)).not.toMatch(/user|hunter2|nas\.lan|s3cr3t|token|abc123/);
|
|
});
|
|
|
|
it("returns 500 for unparseable feeds", async () => {
|
|
getServiceWidget.mockResolvedValue({ type: "feed", url: "https://example.com/feed.xml" });
|
|
httpProxy.mockResolvedValueOnce([200, "text/html", Buffer.from("<html></html>")]);
|
|
|
|
const res = createMockRes();
|
|
await feedProxyHandler(req, res);
|
|
|
|
expect(res.statusCode).toBe(500);
|
|
expect(res.body).toEqual({ error: { message: "Invalid feed", url: "example.com (see logs for details)" } });
|
|
});
|
|
|
|
it("shares one cached fetch across widgets with different options", async () => {
|
|
getServiceWidget
|
|
.mockResolvedValueOnce({ type: "feed", url: "https://example.com/feed.xml", maxItems: 1 })
|
|
.mockResolvedValueOnce({ type: "feed", url: "https://example.com/feed.xml", images: false });
|
|
httpProxy.mockResolvedValue([200, "application/rss+xml", Buffer.from(feed)]);
|
|
|
|
const first = createMockRes();
|
|
await feedProxyHandler(req, first);
|
|
const second = createMockRes();
|
|
await feedProxyHandler(req, second);
|
|
|
|
expect(httpProxy).toHaveBeenCalledTimes(1);
|
|
expect(first.body.items).toHaveLength(1);
|
|
expect(second.body.items).toHaveLength(5);
|
|
expect(second.body.items[0].image).toBeUndefined();
|
|
});
|
|
|
|
it("does not cache failures", async () => {
|
|
getServiceWidget.mockResolvedValue({ type: "feed", url: "https://example.com/feed.xml" });
|
|
httpProxy
|
|
.mockResolvedValueOnce([404, "text/html", Buffer.from("Not found")])
|
|
.mockResolvedValueOnce([200, "application/rss+xml", Buffer.from(feed)]);
|
|
|
|
const failed = createMockRes();
|
|
await feedProxyHandler(req, failed);
|
|
const recovered = createMockRes();
|
|
await feedProxyHandler(req, recovered);
|
|
|
|
expect(failed.statusCode).toBe(404);
|
|
expect(recovered.statusCode).toBe(200);
|
|
expect(httpProxy).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|