/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { describe, expect, test, vi } from "vitest"; import type { Session } from "@homarr/auth"; import { encryptSecret } from "@homarr/common/server"; import { createId } from "@homarr/db"; import { integrations, integrationSecrets } from "@homarr/db/schema"; import { createDb } from "@homarr/db/test"; import type { GroupPermissionKey } from "@homarr/definitions"; import { integrationRouter } from "../../integration/integration-router"; import { expectToBeDefined } from "../helper"; const defaultUserId = createId(); const defaultSessionWithPermissions = (permissions: GroupPermissionKey[] = []) => ({ user: { id: defaultUserId, permissions, colorScheme: "light", }, expires: new Date().toISOString(), }) satisfies Session; // Mock the auth module to return an empty session vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session })); vi.mock("../../integration/integration-test-connection", () => ({ testConnectionAsync: async () => await Promise.resolve({ success: true }), })); describe("all should return all integrations", () => { test("with any session should return all integrations", async () => { const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(), }); await db.insert(integrations).values([ { id: "1", name: "Home assistant", kind: "homeAssistant", url: "http://homeassist.local", }, { id: "2", name: "Home plex server", kind: "plex", url: "http://plex.local", }, ]); const result = await caller.all(); expect(result.length).toBe(2); expect(result[0]!.kind).toBe("plex"); expect(result[1]!.kind).toBe("homeAssistant"); }); }); describe("byId should return an integration by id", () => { test("with full access should return an integration by id", async () => { const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(["integration-full-all"]), }); await db.insert(integrations).values([ { id: "1", name: "Home assistant", kind: "homeAssistant", url: "http://homeassist.local", }, { id: "2", name: "Home plex server", kind: "plex", url: "http://plex.local", }, ]); const result = await caller.byId({ id: "2" }); expect(result.kind).toBe("plex"); }); test("with full access should throw an error if the integration does not exist", async () => { const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(["integration-full-all"]), }); const actAsync = async () => await caller.byId({ id: "2" }); await expect(actAsync()).rejects.toThrow("Integration not found"); }); test("with full access should only return the public secret values", async () => { const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(["integration-full-all"]), }); await db.insert(integrations).values([ { id: "1", name: "Home assistant", kind: "homeAssistant", url: "http://homeassist.local", }, ]); await db.insert(integrationSecrets).values([ { kind: "username", value: encryptSecret("musterUser"), integrationId: "1", updatedAt: new Date(), }, { kind: "password", value: encryptSecret("Password123!"), integrationId: "1", updatedAt: new Date(), }, { kind: "apiKey", value: encryptSecret("1234567890"), integrationId: "1", updatedAt: new Date(), }, ]); const result = await caller.byId({ id: "1" }); expect(result.secrets.length).toBe(3); const username = expectToBeDefined(result.secrets.find((secret) => secret.kind === "username")); expect(username.value).not.toBeNull(); const password = expectToBeDefined(result.secrets.find((secret) => secret.kind === "password")); expect(password.value).toBeNull(); const apiKey = expectToBeDefined(result.secrets.find((secret) => secret.kind === "apiKey")); expect(apiKey.value).toBeNull(); }); test("without full access should throw integration not found error", async () => { // Arrange const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(["integration-interact-all"]), }); await db.insert(integrations).values([ { id: "1", name: "Home assistant", kind: "homeAssistant", url: "http://homeassist.local", }, ]); // Act const actAsync = async () => await caller.byId({ id: "1" }); // Assert await expect(actAsync()).rejects.toThrow("Integration not found"); }); }); describe("create should create a new integration", () => { test("with create integration access should create a new integration", async () => { const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(["integration-create"]), }); const input = { name: "Jellyfin", kind: "jellyfin" as const, url: "http://jellyfin.local", secrets: [{ kind: "apiKey" as const, value: "1234567890" }], attemptSearchEngineCreation: false, }; const fakeNow = new Date("2023-07-01T00:00:00Z"); vi.useFakeTimers(); vi.setSystemTime(fakeNow); await caller.create(input); vi.useRealTimers(); const dbIntegration = await db.query.integrations.findFirst(); const dbSecret = await db.query.integrationSecrets.findFirst(); expect(dbIntegration).toBeDefined(); expect(dbIntegration!.name).toBe(input.name); expect(dbIntegration!.kind).toBe(input.kind); expect(dbIntegration!.url).toBe(input.url); expect(dbSecret!.integrationId).toBe(dbIntegration!.id); expect(dbSecret).toBeDefined(); expect(dbSecret!.kind).toBe(input.secrets[0]!.kind); expect(dbSecret!.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/); expect(dbSecret!.updatedAt).toEqual(fakeNow); }); test("with create integration access should create a new integration when creating search engine", async () => { const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(["integration-create"]), }); const input = { name: "Jellyseerr", kind: "jellyseerr" as const, url: "http://jellyseerr.local", secrets: [{ kind: "apiKey" as const, value: "1234567890" }], attemptSearchEngineCreation: true, }; const fakeNow = new Date("2023-07-01T00:00:00Z"); vi.useFakeTimers(); vi.setSystemTime(fakeNow); await caller.create(input); vi.useRealTimers(); const dbIntegration = await db.query.integrations.findFirst(); const dbSecret = await db.query.integrationSecrets.findFirst(); const dbSearchEngine = await db.query.searchEngines.findFirst(); expect(dbIntegration).toBeDefined(); expect(dbIntegration!.name).toBe(input.name); expect(dbIntegration!.kind).toBe(input.kind); expect(dbIntegration!.url).toBe(input.url); expect(dbSecret!.integrationId).toBe(dbIntegration!.id); expect(dbSecret).toBeDefined(); expect(dbSecret!.kind).toBe(input.secrets[0]!.kind); expect(dbSecret!.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/); expect(dbSecret!.updatedAt).toEqual(fakeNow); expect(dbSearchEngine!.integrationId).toBe(dbIntegration!.id); expect(dbSearchEngine!.short).toBe("j"); expect(dbSearchEngine!.name).toBe(input.name); expect(dbSearchEngine!.iconUrl).toBe( "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg", ); }); test("without create integration access should throw permission error", async () => { // Arrange const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(["integration-interact-all"]), }); const input = { name: "Jellyfin", kind: "jellyfin" as const, url: "http://jellyfin.local", secrets: [{ kind: "apiKey" as const, value: "1234567890" }], attemptSearchEngineCreation: false, }; // Act const actAsync = async () => await caller.create(input); // Assert await expect(actAsync()).rejects.toThrow("Permission denied"); }); }); describe("update should update an integration", () => { test("with full access should update an integration", async () => { const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(["integration-full-all"]), }); const lastWeek = new Date("2023-06-24T00:00:00Z"); const integrationId = createId(); const toInsert = { id: integrationId, name: "Pi Hole", kind: "piHole" as const, url: "http://hole.local", }; await db.insert(integrations).values(toInsert); const usernameToInsert = { kind: "username" as const, value: encryptSecret("musterUser"), integrationId, updatedAt: lastWeek, }; const passwordToInsert = { kind: "password" as const, value: encryptSecret("Password123!"), integrationId, updatedAt: lastWeek, }; await db.insert(integrationSecrets).values([usernameToInsert, passwordToInsert]); const input = { id: integrationId, name: "Milky Way Pi Hole", kind: "piHole" as const, url: "http://milkyway.local", secrets: [ { kind: "username" as const, value: "newUser" }, { kind: "password" as const, value: null }, { kind: "apiKey" as const, value: "1234567890" }, ], }; const fakeNow = new Date("2023-07-01T00:00:00Z"); vi.useFakeTimers(); vi.setSystemTime(fakeNow); await caller.update(input); vi.useRealTimers(); const dbIntegration = await db.query.integrations.findFirst(); const dbSecrets = await db.query.integrationSecrets.findMany(); expect(dbIntegration).toBeDefined(); expect(dbIntegration!.name).toBe(input.name); expect(dbIntegration!.kind).toBe(input.kind); expect(dbIntegration!.url).toBe(input.url); expect(dbSecrets.length).toBe(3); const username = expectToBeDefined(dbSecrets.find((secret) => secret.kind === "username")); const password = expectToBeDefined(dbSecrets.find((secret) => secret.kind === "password")); const apiKey = expectToBeDefined(dbSecrets.find((secret) => secret.kind === "apiKey")); expect(username.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/); expect(password.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/); expect(apiKey.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/); expect(username.updatedAt).toEqual(fakeNow); expect(password.updatedAt).toEqual(lastWeek); expect(apiKey.updatedAt).toEqual(fakeNow); expect(username.value).not.toEqual(usernameToInsert.value); expect(password.value).toEqual(passwordToInsert.value); expect(apiKey.value).not.toEqual(input.secrets[2]!.value); }); test("with full access should throw an error if the integration does not exist", async () => { const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(["integration-full-all"]), }); const actAsync = async () => await caller.update({ id: createId(), name: "Pi Hole", url: "http://hole.local", secrets: [], }); await expect(actAsync()).rejects.toThrow("Integration not found"); }); test("without full access should throw permission error", async () => { // Arrange const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(["integration-interact-all"]), }); // Act const actAsync = async () => await caller.update({ id: createId(), name: "Pi Hole", url: "http://hole.local", secrets: [], }); // Assert await expect(actAsync()).rejects.toThrow("Integration not found"); }); }); describe("delete should delete an integration", () => { test("with full access should delete an integration", async () => { const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(["integration-full-all"]), }); const integrationId = createId(); await db.insert(integrations).values({ id: integrationId, name: "Home assistant", kind: "homeAssistant", url: "http://homeassist.local", }); await db.insert(integrationSecrets).values([ { kind: "username", value: encryptSecret("example"), integrationId, updatedAt: new Date(), }, ]); await caller.delete({ id: integrationId }); const dbIntegration = await db.query.integrations.findFirst(); expect(dbIntegration).toBeUndefined(); const dbSecrets = await db.query.integrationSecrets.findMany(); expect(dbSecrets.length).toBe(0); }); test("without full access should throw permission error", async () => { // Arrange const db = createDb(); const caller = integrationRouter.createCaller({ db, deviceType: undefined, session: defaultSessionWithPermissions(["integration-interact-all"]), }); // Act const actAsync = async () => await caller.delete({ id: createId() }); // Assert await expect(actAsync()).rejects.toThrow("Integration not found"); }); });