fix: address remaining widget review feedback

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-14 11:07:08 +02:00
1 parent 5553c4e0c5
commit f00f5b83b5
13 files changed
+149 -26

No files matched your search

@@ -21,7 +21,7 @@ vi.hoisted(() => {
const createHandler = <T>(integration: { name: string }, data: T) => ({
getDataAsync: async () => {
if (integration.name === "Offline") throw new Error("offline");
if (integration.name === "Offline") throw new Error("offline https://internal.example?token=secret");
return { data, timestamp: new Date() };
},
});
@@ -119,9 +119,13 @@ describe("partial integration failures", () => {
});
expect(calendarResults).toEqual(
expect.arrayContaining([
expect.objectContaining({ integration: expect.objectContaining({ id: calendar.offlineId }), error: "offline" }),
expect.objectContaining({
integration: expect.objectContaining({ id: calendar.offlineId }),
error: "Integration request failed",
}),
]),
);
expect(JSON.stringify(calendarResults)).not.toContain("secret");
const mediaServer = await setupAsync("plex");
const streamResults = await mediaServerRouter
@@ -129,9 +133,14 @@ describe("partial integration failures", () => {
.getCurrentStreams({ integrationIds: mediaServer.integrationIds, showOnlyPlaying: false });
expect(streamResults).toEqual(
expect.arrayContaining([
expect.objectContaining({ integrationId: mediaServer.offlineId, sessions: [], error: "offline" }),
expect.objectContaining({
integrationId: mediaServer.offlineId,
sessions: [],
error: "Integration request failed",
}),
]),
);
expect(JSON.stringify(streamResults)).not.toContain("secret");
});
test("media request responses expose failed integrations without dropping successful data", async () => {
@@ -156,13 +165,15 @@ describe("partial integration failures", () => {
}),
]);
expect(latest.failedIntegrations).toEqual([
expect.objectContaining({ integrationId: mediaRequests.offlineId, error: "offline" }),
expect.objectContaining({ integrationId: mediaRequests.offlineId, error: "Integration request failed" }),
]);
expect(JSON.stringify(latest)).not.toContain("secret");
const stats = await caller.getStats({ integrationIds: mediaRequests.integrationIds });
expect(stats.failedIntegrations).toEqual([
expect.objectContaining({ integrationId: mediaRequests.offlineId, error: "offline" }),
expect.objectContaining({ integrationId: mediaRequests.offlineId, error: "Integration request failed" }),
]);
expect(JSON.stringify(stats)).not.toContain("secret");
expect(stats.stats).toHaveLength(1);
expect(stats.users).toEqual([
expect.objectContaining({
@@ -1,4 +1,6 @@
import { stringify } from "superjson";
// @vitest-environment node
import { parse, stringify } from "superjson";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Session } from "@homarr/auth";
@@ -8,6 +10,8 @@ import { eq } from "@homarr/db";
import { boards, items, users } from "@homarr/db/schema";
import { createDb } from "@homarr/db/test";
import { boardRouter } from "../../board";
import { optionsRouter } from "../../widgets/options";
import { timetableRouter } from "../../widgets/timetable";
const mocks = vi.hoisted(() => ({ getTimetable: vi.fn(), searchStations: vi.fn(), lookup: vi.fn() }));
@@ -123,7 +127,7 @@ describe("timetableRouter source authorization", () => {
"http://timetable.internal:8080",
"http://[::1]:8080",
"http://[::ffff:127.0.0.1]:8080",
"http://[fd00::1]:8080",
"https://[fd00::1]:8443",
"http://[64:ff9b::7f00:1]:8080",
"http://[2002:7f00:1::]:8080",
])("blocks an unsaved private endpoint %s even for board modifiers", async (baseUrl) => {
@@ -182,6 +186,66 @@ describe("timetableRouter source authorization", () => {
);
});
it("blocks changing a saved timetable to a private endpoint through item options", async () => {
const db = createDb();
const { boardId, itemId, ownerId } = await createTimetableItemAsync(db, {
baseUrl: "https://search.ch",
isPublic: false,
});
const caller = optionsRouter.createCaller({ db, deviceType: undefined, session: createSession(ownerId) });
await expect(
caller.saveItemOptions({ boardId, itemId, newOptions: { baseUrl: "http://127.0.0.1:8080" } }),
).rejects.toMatchObject({ code: "BAD_REQUEST" });
const item = await db.query.items.findFirst({ where: eq(items.id, itemId) });
expect(parse<Record<string, unknown>>(item?.options ?? "{}")).toMatchObject({ baseUrl: "https://search.ch" });
});
it("blocks changing a saved timetable to a private endpoint through a board save", async () => {
const db = createDb();
const { boardId, itemId, ownerId } = await createTimetableItemAsync(db, {
baseUrl: "https://search.ch",
isPublic: false,
});
const caller = boardRouter.createCaller({ db, deviceType: undefined, session: createSession(ownerId) });
await expect(
caller.saveBoard({
id: boardId,
sections: [],
items: [
{
id: itemId,
kind: "timetable",
options: { baseUrl: "http://127.0.0.1:8080" },
integrationIds: [],
layouts: [],
advancedOptions: { title: null, customCssClasses: [], borderColor: "" },
},
],
}),
).rejects.toMatchObject({ code: "BAD_REQUEST" });
});
it("blocks adding a timetable with a private endpoint", async () => {
const db = createDb();
const { boardId, ownerId } = await createTimetableItemAsync(db, {
baseUrl: "https://search.ch",
isPublic: false,
});
const caller = boardRouter.createCaller({ db, deviceType: undefined, session: createSession(ownerId) });
await expect(
caller.addItem({
boardId,
kind: "timetable",
options: { baseUrl: "http://127.0.0.1:8080" },
integrationIds: [],
}),
).rejects.toMatchObject({ code: "BAD_REQUEST" });
});
it("rejects an item ID that does not belong to the requested board", async () => {
const db = createDb();
const first = await createTimetableItemAsync(db, { baseUrl: "http://timetable.internal:8080", isPublic: false });