Files
homarr/packages/api/src/router/test/docker/docker-router.spec.ts
T
Thomas Camlong c4a664b7ac fix: address PR review — bugs, dead code, error handling
Critical fixes:
- Smart Home: invalidate entity state query after toggle mutation
- Beszel live mode: use 5s refetchInterval instead of global 30s
- Fix unit test assertions for 5-arg setQueryCacheAsync signature
- Fix serialize/deserialize mismatch: JSON.parse on read (matches persister's JSON.stringify)
- Remove render-time setActiveQueryCacheBoardId race (keep only useEffect)

Error handling:
- Log persistence errors instead of swallowing with catch(() => {})
- Log QueryCacheHydration failures with ErrorWithMetadata
- Beszel: log partial failures (system details, container stats)

Dead code removal:
- Remove dead queryKey from all 39 request handlers + lib files
- Delete unused Redis APIs: createCacheChannel, createChannelWithLatestAndEvents,
  createChannelEventHistory, history-channel.ts
- Remove stale createCacheChannel mock from docker test

Cleanup:
- Seed system-resources chart from initial query data (no empty start)
- Remove redundant refetchOnWindowFocus/refetchOnReconnect from BoardProvider
2026-06-25 11:25:09 +02:00

108 lines
3.3 KiB
TypeScript

import { TRPCError } from "@trpc/server";
import { describe, expect, test, vi } from "vitest";
import type { Session } from "@homarr/auth";
import { objectKeys } from "@homarr/common";
import type { Database } from "@homarr/db";
import type { GroupPermissionKey } from "@homarr/definitions";
import { getPermissionsWithChildren } from "@homarr/definitions";
import type { RouterInputs } from "../../..";
import { dockerRouter } from "../../docker/docker-router";
// Mock the auth module to return an empty session
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
vi.mock("@homarr/request-handler/docker", () => ({
getContainerLogsAsync: async () => {
await Promise.resolve();
return "logs";
},
streamContainerLogsAsync: async () => {
await Promise.resolve();
return () => undefined;
},
dockerContainersRequestHandler: {
handler: () => ({
getDataAsync: async () => {
return await Promise.resolve({ data: [], timestamp: new Date() });
},
}),
},
}));
vi.mock("@homarr/docker/env", () => ({
env: {
ENABLE_DOCKER: true,
},
}));
const createSessionWithPermissions = (...permissions: GroupPermissionKey[]) =>
({
user: {
id: "1",
permissions,
colorScheme: "light",
},
expires: new Date().toISOString(),
}) satisfies Session;
const procedureKeys = objectKeys(dockerRouter._def.procedures);
const validInputs: {
[key in (typeof procedureKeys)[number]]: RouterInputs["docker"][key];
} = {
getContainers: undefined,
startAll: { ids: ["1"] },
stopAll: { ids: ["1"] },
restartAll: { ids: ["1"] },
removeAll: { ids: ["1"] },
logs: { id: "1", tail: 200 },
subscribeLogs: { id: "1", tail: 200 },
};
describe("All procedures should only be accessible for users with admin permission", () => {
test.each(procedureKeys)("Procedure %s should be accessible for users with admin permission", async (procedure) => {
// Arrange
const caller = dockerRouter.createCaller({
db: null as unknown as Database,
deviceType: undefined,
session: createSessionWithPermissions("admin"),
});
// Act
const act = () => caller[procedure](validInputs[procedure] as never);
await expect(act()).resolves.not.toThrow();
});
test.each(procedureKeys)("Procedure %s should not be accessible with other permissions", async (procedure) => {
// Arrange
const groupPermissionsWithoutAdmin = getPermissionsWithChildren(["admin"]).filter(
(permission) => permission !== "admin",
);
const caller = dockerRouter.createCaller({
db: null as unknown as Database,
deviceType: undefined,
session: createSessionWithPermissions(...groupPermissionsWithoutAdmin),
});
// Act
const act = () => caller[procedure](validInputs[procedure] as never);
await expect(act()).rejects.toThrow(new TRPCError({ code: "FORBIDDEN", message: "Permission denied" }));
});
test.each(procedureKeys)("Procedure %s should not be accessible without session", async (procedure) => {
// Arrange
const caller = dockerRouter.createCaller({
db: null as unknown as Database,
deviceType: undefined,
session: null,
});
// Act
const act = () => caller[procedure](validInputs[procedure] as never);
await expect(act()).rejects.toThrow(new TRPCError({ code: "UNAUTHORIZED" }));
});
});