feat: test integration connection (#669)
* feat: add test integration for pi-hole * refactor: test integration for pi-hole * fix: multiple secrets of same type could be used for integration creation * fix: remove integration test connection test and add mock for test-connection function * fix: add missing onUpdateFn to mysql integration secrets * fix: format issues * feat: add home assistant test connection * fix: deepsource issues * test: add system integration tests for test connection * fix: add before all for pulling home assistant image * test: add unit tests for handleTestConnectionResponseAsync * test: add unit test for testConnectionAsync * test: add mroe tests to integration-test-connection * fix: deepsource issues * fix: deepsource issue * chore: address pull request feedback
This commit is contained in:
@@ -4,7 +4,7 @@ import { dockerRouter } from "./router/docker/docker-router";
|
||||
import { groupRouter } from "./router/group";
|
||||
import { homeRouter } from "./router/home";
|
||||
import { iconsRouter } from "./router/icons";
|
||||
import { integrationRouter } from "./router/integration";
|
||||
import { integrationRouter } from "./router/integration/integration-router";
|
||||
import { inviteRouter } from "./router/invite";
|
||||
import { locationRouter } from "./router/location";
|
||||
import { logRouter } from "./router/log";
|
||||
|
||||
@@ -5,10 +5,11 @@ import type { Database } from "@homarr/db";
|
||||
import { and, createId, eq } from "@homarr/db";
|
||||
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions, integrationKinds, integrationSecretKindObject } from "@homarr/definitions";
|
||||
import { integrationKinds, integrationSecretKindObject } from "@homarr/definitions";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
import { testConnectionAsync } from "./integration-test-connection";
|
||||
|
||||
export const integrationRouter = createTRPCRouter({
|
||||
all: publicProcedure.query(async ({ ctx }) => {
|
||||
@@ -60,6 +61,14 @@ export const integrationRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
create: publicProcedure.input(validation.integration.create).mutation(async ({ ctx, input }) => {
|
||||
await testConnectionAsync({
|
||||
id: "new",
|
||||
name: input.name,
|
||||
url: input.url,
|
||||
kind: input.kind,
|
||||
secrets: input.secrets,
|
||||
});
|
||||
|
||||
const integrationId = createId();
|
||||
await ctx.db.insert(integrations).values({
|
||||
id: integrationId,
|
||||
@@ -68,13 +77,14 @@ export const integrationRouter = createTRPCRouter({
|
||||
kind: input.kind,
|
||||
});
|
||||
|
||||
for (const secret of input.secrets) {
|
||||
await ctx.db.insert(integrationSecrets).values({
|
||||
kind: secret.kind,
|
||||
value: encryptSecret(secret.value),
|
||||
updatedAt: new Date(),
|
||||
integrationId,
|
||||
});
|
||||
if (input.secrets.length >= 1) {
|
||||
await ctx.db.insert(integrationSecrets).values(
|
||||
input.secrets.map((secret) => ({
|
||||
kind: secret.kind,
|
||||
value: encryptSecret(secret.value),
|
||||
integrationId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}),
|
||||
update: publicProcedure.input(validation.integration.update).mutation(async ({ ctx, input }) => {
|
||||
@@ -92,6 +102,17 @@ export const integrationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await testConnectionAsync(
|
||||
{
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
url: input.url,
|
||||
kind: integration.kind,
|
||||
secrets: input.secrets,
|
||||
},
|
||||
integration.secrets,
|
||||
);
|
||||
|
||||
await ctx.db
|
||||
.update(integrations)
|
||||
.set({
|
||||
@@ -100,15 +121,14 @@ export const integrationRouter = createTRPCRouter({
|
||||
})
|
||||
.where(eq(integrations.id, input.id));
|
||||
|
||||
const decryptedSecrets = integration.secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
}));
|
||||
|
||||
const changedSecrets = input.secrets.filter(
|
||||
(secret): secret is { kind: IntegrationSecretKind; value: string } =>
|
||||
secret.value !== null && // only update secrets that have a value
|
||||
!decryptedSecrets.find((dSecret) => dSecret.kind === secret.kind && dSecret.value === secret.value),
|
||||
!integration.secrets.find(
|
||||
// Checked above
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
(dbSecret) => dbSecret.kind === secret.kind && dbSecret.value === encryptSecret(secret.value!),
|
||||
),
|
||||
);
|
||||
|
||||
if (changedSecrets.length > 0) {
|
||||
@@ -118,7 +138,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
value: changedSecret.value,
|
||||
kind: changedSecret.kind,
|
||||
};
|
||||
if (!decryptedSecrets.some((secret) => secret.kind === changedSecret.kind)) {
|
||||
if (!integration.secrets.some((secret) => secret.kind === changedSecret.kind)) {
|
||||
await addSecretAsync(ctx.db, secretInput);
|
||||
} else {
|
||||
await updateSecretAsync(ctx.db, secretInput);
|
||||
@@ -140,71 +160,6 @@ export const integrationRouter = createTRPCRouter({
|
||||
|
||||
await ctx.db.delete(integrations).where(eq(integrations.id, input.id));
|
||||
}),
|
||||
testConnection: publicProcedure.input(validation.integration.testConnection).mutation(async ({ ctx, input }) => {
|
||||
const secrets = input.secrets.filter((secret): secret is { kind: IntegrationSecretKind; value: string } =>
|
||||
Boolean(secret.value),
|
||||
);
|
||||
|
||||
// Find any matching secret kinds
|
||||
let secretKinds = getAllSecretKindOptions(input.kind).find((secretKinds) =>
|
||||
secretKinds.every((secretKind) => secrets.some((secret) => secret.kind === secretKind)),
|
||||
);
|
||||
|
||||
if (!secretKinds && input.id === null) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "SECRETS_NOT_DEFINED",
|
||||
});
|
||||
}
|
||||
|
||||
if (!secretKinds && input.id !== null) {
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: eq(integrations.id, input.id),
|
||||
with: {
|
||||
secrets: true,
|
||||
},
|
||||
});
|
||||
if (!integration) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "SECRETS_NOT_DEFINED",
|
||||
});
|
||||
}
|
||||
const decryptedSecrets = integration.secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
}));
|
||||
|
||||
// Add secrets that are not defined in the input from the database
|
||||
for (const dbSecret of decryptedSecrets) {
|
||||
if (!secrets.find((secret) => secret.kind === dbSecret.kind)) {
|
||||
secrets.push({
|
||||
kind: dbSecret.kind,
|
||||
value: dbSecret.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
secretKinds = getAllSecretKindOptions(input.kind).find((secretKinds) =>
|
||||
secretKinds.every((secretKind) => secrets.some((secret) => secret.kind === secretKind)),
|
||||
);
|
||||
|
||||
if (!secretKinds) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "SECRETS_NOT_DEFINED",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: actually test the connection
|
||||
// Probably by calling a function on the integration class
|
||||
// getIntegration(input.kind).testConnection(secrets)
|
||||
// getIntegration(kind: IntegrationKind): Integration
|
||||
// interface Integration {
|
||||
// testConnection(): Promise<void>;
|
||||
// }
|
||||
}),
|
||||
});
|
||||
|
||||
interface UpdateSecretInput {
|
||||
@@ -217,7 +172,6 @@ const updateSecretAsync = async (db: Database, input: UpdateSecretInput) => {
|
||||
.update(integrationSecrets)
|
||||
.set({
|
||||
value: encryptSecret(input.value),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(integrationSecrets.integrationId, input.integrationId), eq(integrationSecrets.kind, input.kind)));
|
||||
};
|
||||
@@ -231,7 +185,6 @@ const addSecretAsync = async (db: Database, input: AddSecretInput) => {
|
||||
await db.insert(integrationSecrets).values({
|
||||
kind: input.kind,
|
||||
value: encryptSecret(input.value),
|
||||
updatedAt: new Date(),
|
||||
integrationId: input.integrationId,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { decryptSecret } from "@homarr/common";
|
||||
import type { Integration } from "@homarr/db/schema/sqlite";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions } from "@homarr/definitions";
|
||||
import { integrationCreatorByKind, IntegrationTestConnectionError } from "@homarr/integrations";
|
||||
|
||||
type FormIntegration = Integration & {
|
||||
secrets: {
|
||||
kind: IntegrationSecretKind;
|
||||
value: string | null;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const testConnectionAsync = async (
|
||||
integration: FormIntegration,
|
||||
dbSecrets: {
|
||||
kind: IntegrationSecretKind;
|
||||
value: `${string}.${string}`;
|
||||
}[] = [],
|
||||
) => {
|
||||
const formSecrets = integration.secrets
|
||||
.filter((secret) => secret.value !== null)
|
||||
.map((secret) => ({
|
||||
...secret,
|
||||
// We ensured above that the value is not null
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
value: secret.value!,
|
||||
source: "form" as const,
|
||||
}));
|
||||
|
||||
const decryptedDbSecrets = dbSecrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
source: "db" as const,
|
||||
}));
|
||||
|
||||
const sourcedSecrets = [...formSecrets, ...decryptedDbSecrets];
|
||||
const secretKinds = getSecretKindOption(integration.kind, sourcedSecrets);
|
||||
|
||||
const filteredSecrets = secretKinds.map((kind) => {
|
||||
const secrets = sourcedSecrets.filter((secret) => secret.kind === kind);
|
||||
// Will never be undefined because of the check before
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
if (secrets.length === 1) return secrets[0]!;
|
||||
|
||||
// There will always be a matching secret because of the getSecretKindOption function
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return secrets.find((secret) => secret.source === "form") ?? secrets[0]!;
|
||||
});
|
||||
|
||||
const integrationInstance = integrationCreatorByKind(integration.kind, {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
url: integration.url,
|
||||
decryptedSecrets: filteredSecrets,
|
||||
});
|
||||
|
||||
await integrationInstance.testConnectionAsync();
|
||||
};
|
||||
|
||||
interface SourcedIntegrationSecret {
|
||||
kind: IntegrationSecretKind;
|
||||
value: string;
|
||||
source: "db" | "form";
|
||||
}
|
||||
|
||||
const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedIntegrationSecret[]) => {
|
||||
const matchingSecretKindOptions = getAllSecretKindOptions(kind).filter((secretKinds) =>
|
||||
secretKinds.every((kind) => sourcedSecrets.some((secret) => secret.kind === kind)),
|
||||
);
|
||||
|
||||
if (matchingSecretKindOptions.length === 0) {
|
||||
throw new IntegrationTestConnectionError("secretNotDefined");
|
||||
}
|
||||
|
||||
if (matchingSecretKindOptions.length === 1) {
|
||||
// Will never be undefined because of the check above
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return matchingSecretKindOptions[0]!;
|
||||
}
|
||||
|
||||
const onlyFormSecretsKindOptions = matchingSecretKindOptions.filter((secretKinds) =>
|
||||
sourcedSecrets.filter((secret) => secretKinds.includes(secret.kind)).every((secret) => secret.source === "form"),
|
||||
);
|
||||
|
||||
if (onlyFormSecretsKindOptions.length >= 1) {
|
||||
// Will never be undefined because of the check above
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return onlyFormSecretsKindOptions[0]!;
|
||||
}
|
||||
|
||||
// Will never be undefined because of the check above
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return matchingSecretKindOptions[0]!;
|
||||
};
|
||||
@@ -7,12 +7,14 @@ import { createId } from "@homarr/db";
|
||||
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import type { RouterInputs } from "../..";
|
||||
import { integrationRouter } from "../integration";
|
||||
import { expectToBeDefined } from "./helper";
|
||||
import { integrationRouter } from "../../integration/integration-router";
|
||||
import { expectToBeDefined } from "../helper";
|
||||
|
||||
// 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(undefined),
|
||||
}));
|
||||
|
||||
describe("all should return all integrations", () => {
|
||||
it("should return all integrations", async () => {
|
||||
@@ -290,199 +292,3 @@ describe("delete should delete an integration", () => {
|
||||
expect(dbSecrets.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("testConnection should test the connection to an integration", () => {
|
||||
it.each([
|
||||
[
|
||||
"nzbGet" as const,
|
||||
[
|
||||
{ kind: "username" as const, value: null },
|
||||
{ kind: "password" as const, value: "Password123!" },
|
||||
],
|
||||
],
|
||||
[
|
||||
"nzbGet" as const,
|
||||
[
|
||||
{ kind: "username" as const, value: "exampleUser" },
|
||||
{ kind: "password" as const, value: null },
|
||||
],
|
||||
],
|
||||
["sabNzbd" as const, [{ kind: "apiKey" as const, value: null }]],
|
||||
[
|
||||
"sabNzbd" as const,
|
||||
[
|
||||
{ kind: "username" as const, value: "exampleUser" },
|
||||
{ kind: "password" as const, value: "Password123!" },
|
||||
],
|
||||
],
|
||||
])("should fail when a required secret is missing when creating %s integration", async (kind, secrets) => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
});
|
||||
|
||||
const input: RouterInputs["integration"]["testConnection"] = {
|
||||
id: null,
|
||||
kind,
|
||||
url: `http://${kind}.local`,
|
||||
secrets,
|
||||
};
|
||||
|
||||
const actAsync = async () => await caller.testConnection(input);
|
||||
await expect(actAsync()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"nzbGet" as const,
|
||||
[
|
||||
{ kind: "username" as const, value: "exampleUser" },
|
||||
{ kind: "password" as const, value: "Password123!" },
|
||||
],
|
||||
],
|
||||
["sabNzbd" as const, [{ kind: "apiKey" as const, value: "1234567890" }]],
|
||||
])(
|
||||
"should be successful when all required secrets are defined for creation of %s integration",
|
||||
async (kind, secrets) => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
});
|
||||
|
||||
const input: RouterInputs["integration"]["testConnection"] = {
|
||||
id: null,
|
||||
kind,
|
||||
url: `http://${kind}.local`,
|
||||
secrets,
|
||||
};
|
||||
|
||||
const actAsync = async () => await caller.testConnection(input);
|
||||
await expect(actAsync()).resolves.toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("should be successful when all required secrets are defined for updating an nzbGet integration", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
});
|
||||
|
||||
const input: RouterInputs["integration"]["testConnection"] = {
|
||||
id: createId(),
|
||||
kind: "nzbGet",
|
||||
url: "http://nzbGet.local",
|
||||
secrets: [
|
||||
{ kind: "username", value: "exampleUser" },
|
||||
{ kind: "password", value: "Password123!" },
|
||||
],
|
||||
};
|
||||
|
||||
const actAsync = async () => await caller.testConnection(input);
|
||||
await expect(actAsync()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should be successful when overriding one of the secrets for an existing nzbGet integration", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
});
|
||||
|
||||
const integrationId = createId();
|
||||
await db.insert(integrations).values({
|
||||
id: integrationId,
|
||||
name: "NZBGet",
|
||||
kind: "nzbGet",
|
||||
url: "http://nzbGet.local",
|
||||
});
|
||||
|
||||
await db.insert(integrationSecrets).values([
|
||||
{
|
||||
kind: "username",
|
||||
value: encryptSecret("exampleUser"),
|
||||
integrationId,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
kind: "password",
|
||||
value: encryptSecret("Password123!"),
|
||||
integrationId,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
]);
|
||||
|
||||
const input: RouterInputs["integration"]["testConnection"] = {
|
||||
id: integrationId,
|
||||
kind: "nzbGet",
|
||||
url: "http://nzbGet.local",
|
||||
secrets: [
|
||||
{ kind: "username", value: "newUser" },
|
||||
{ kind: "password", value: null },
|
||||
],
|
||||
};
|
||||
|
||||
const actAsync = async () => await caller.testConnection(input);
|
||||
await expect(actAsync()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail when a required secret is missing for an existing nzbGet integration", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
});
|
||||
|
||||
const integrationId = createId();
|
||||
await db.insert(integrations).values({
|
||||
id: integrationId,
|
||||
name: "NZBGet",
|
||||
kind: "nzbGet",
|
||||
url: "http://nzbGet.local",
|
||||
});
|
||||
|
||||
await db.insert(integrationSecrets).values([
|
||||
{
|
||||
kind: "username",
|
||||
value: encryptSecret("exampleUser"),
|
||||
integrationId,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
]);
|
||||
|
||||
const input: RouterInputs["integration"]["testConnection"] = {
|
||||
id: integrationId,
|
||||
kind: "nzbGet",
|
||||
url: "http://nzbGet.local",
|
||||
secrets: [
|
||||
{ kind: "username", value: "newUser" },
|
||||
{ kind: "apiKey", value: "1234567890" },
|
||||
],
|
||||
};
|
||||
|
||||
const actAsync = async () => await caller.testConnection(input);
|
||||
await expect(actAsync()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
||||
});
|
||||
|
||||
it("should fail when the updating integration does not exist", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
});
|
||||
|
||||
const actAsync = async () =>
|
||||
await caller.testConnection({
|
||||
id: createId(),
|
||||
kind: "nzbGet",
|
||||
url: "http://nzbGet.local",
|
||||
secrets: [
|
||||
{ kind: "username", value: null },
|
||||
{ kind: "password", value: "Password123!" },
|
||||
],
|
||||
});
|
||||
await expect(actAsync()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,253 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import * as homarrDefinitions from "@homarr/definitions";
|
||||
import * as homarrIntegrations from "@homarr/integrations";
|
||||
|
||||
import { testConnectionAsync } from "../../integration/integration-test-connection";
|
||||
|
||||
vi.mock("@homarr/common", async (importActual) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const actual = await importActual<typeof import("@homarr/common")>();
|
||||
|
||||
return {
|
||||
...actual,
|
||||
decryptSecret: (value: string) => value.split(".")[0],
|
||||
};
|
||||
});
|
||||
|
||||
describe("testConnectionAsync should run test connection of integration", () => {
|
||||
test("with input of only form secrets matching api key kind it should use form apiKey", async () => {
|
||||
// Arrange
|
||||
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreatorByKind");
|
||||
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
||||
factorySpy.mockReturnValue({
|
||||
testConnectionAsync: async () => await Promise.resolve(),
|
||||
} as homarrIntegrations.PiHoleIntegration);
|
||||
optionsSpy.mockReturnValue([["apiKey"]]);
|
||||
|
||||
const integration = {
|
||||
id: "new",
|
||||
name: "Pi Hole",
|
||||
url: "http://pi.hole",
|
||||
kind: "piHole" as const,
|
||||
secrets: [
|
||||
{
|
||||
kind: "apiKey" as const,
|
||||
value: "secret",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Act
|
||||
await testConnectionAsync(integration);
|
||||
|
||||
// Assert
|
||||
expect(factorySpy).toHaveBeenCalledWith("piHole", {
|
||||
id: "new",
|
||||
name: "Pi Hole",
|
||||
url: "http://pi.hole",
|
||||
decryptedSecrets: [
|
||||
expect.objectContaining({
|
||||
kind: "apiKey",
|
||||
value: "secret",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("with input of only null form secrets and the required db secrets matching api key kind it should use db apiKey", async () => {
|
||||
// Arrange
|
||||
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreatorByKind");
|
||||
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
||||
factorySpy.mockReturnValue({
|
||||
testConnectionAsync: async () => await Promise.resolve(),
|
||||
} as homarrIntegrations.PiHoleIntegration);
|
||||
optionsSpy.mockReturnValue([["apiKey"]]);
|
||||
|
||||
const integration = {
|
||||
id: "new",
|
||||
name: "Pi Hole",
|
||||
url: "http://pi.hole",
|
||||
kind: "piHole" as const,
|
||||
secrets: [
|
||||
{
|
||||
kind: "apiKey" as const,
|
||||
value: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dbSecrets = [
|
||||
{
|
||||
kind: "apiKey" as const,
|
||||
value: "dbSecret.encrypted" as const,
|
||||
},
|
||||
];
|
||||
|
||||
// Act
|
||||
await testConnectionAsync(integration, dbSecrets);
|
||||
|
||||
// Assert
|
||||
expect(factorySpy).toHaveBeenCalledWith("piHole", {
|
||||
id: "new",
|
||||
name: "Pi Hole",
|
||||
url: "http://pi.hole",
|
||||
decryptedSecrets: [
|
||||
expect.objectContaining({
|
||||
kind: "apiKey",
|
||||
value: "dbSecret",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("with input of form and db secrets matching api key kind it should use form apiKey", async () => {
|
||||
// Arrange
|
||||
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreatorByKind");
|
||||
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
||||
factorySpy.mockReturnValue({
|
||||
testConnectionAsync: async () => await Promise.resolve(),
|
||||
} as homarrIntegrations.PiHoleIntegration);
|
||||
optionsSpy.mockReturnValue([["apiKey"]]);
|
||||
|
||||
const integration = {
|
||||
id: "new",
|
||||
name: "Pi Hole",
|
||||
url: "http://pi.hole",
|
||||
kind: "piHole" as const,
|
||||
secrets: [
|
||||
{
|
||||
kind: "apiKey" as const,
|
||||
value: "secret",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dbSecrets = [
|
||||
{
|
||||
kind: "apiKey" as const,
|
||||
value: "dbSecret.encrypted" as const,
|
||||
},
|
||||
];
|
||||
|
||||
// Act
|
||||
await testConnectionAsync(integration, dbSecrets);
|
||||
|
||||
// Assert
|
||||
expect(factorySpy).toHaveBeenCalledWith("piHole", {
|
||||
id: "new",
|
||||
name: "Pi Hole",
|
||||
url: "http://pi.hole",
|
||||
decryptedSecrets: [
|
||||
expect.objectContaining({
|
||||
kind: "apiKey",
|
||||
value: "secret",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("with input of form apiKey and db secrets for username and password it should use form apiKey when both is allowed", async () => {
|
||||
// Arrange
|
||||
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreatorByKind");
|
||||
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
||||
factorySpy.mockReturnValue({
|
||||
testConnectionAsync: async () => await Promise.resolve(),
|
||||
} as homarrIntegrations.PiHoleIntegration);
|
||||
optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]);
|
||||
|
||||
const integration = {
|
||||
id: "new",
|
||||
name: "Pi Hole",
|
||||
url: "http://pi.hole",
|
||||
kind: "piHole" as const,
|
||||
secrets: [
|
||||
{
|
||||
kind: "apiKey" as const,
|
||||
value: "secret",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dbSecrets = [
|
||||
{
|
||||
kind: "username" as const,
|
||||
value: "dbUsername.encrypted" as const,
|
||||
},
|
||||
{
|
||||
kind: "password" as const,
|
||||
value: "dbPassword.encrypted" as const,
|
||||
},
|
||||
];
|
||||
|
||||
// Act
|
||||
await testConnectionAsync(integration, dbSecrets);
|
||||
|
||||
// Assert
|
||||
expect(factorySpy).toHaveBeenCalledWith("piHole", {
|
||||
id: "new",
|
||||
name: "Pi Hole",
|
||||
url: "http://pi.hole",
|
||||
decryptedSecrets: [
|
||||
expect.objectContaining({
|
||||
kind: "apiKey",
|
||||
value: "secret",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("with input of null form apiKey and db secrets for username and password it should use db username and password when both is allowed", async () => {
|
||||
// Arrange
|
||||
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreatorByKind");
|
||||
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
||||
factorySpy.mockReturnValue({
|
||||
testConnectionAsync: async () => await Promise.resolve(),
|
||||
} as homarrIntegrations.PiHoleIntegration);
|
||||
optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]);
|
||||
|
||||
const integration = {
|
||||
id: "new",
|
||||
name: "Pi Hole",
|
||||
url: "http://pi.hole",
|
||||
kind: "piHole" as const,
|
||||
secrets: [
|
||||
{
|
||||
kind: "apiKey" as const,
|
||||
value: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dbSecrets = [
|
||||
{
|
||||
kind: "username" as const,
|
||||
value: "dbUsername.encrypted" as const,
|
||||
},
|
||||
{
|
||||
kind: "password" as const,
|
||||
value: "dbPassword.encrypted" as const,
|
||||
},
|
||||
];
|
||||
|
||||
// Act
|
||||
await testConnectionAsync(integration, dbSecrets);
|
||||
|
||||
// Assert
|
||||
expect(factorySpy).toHaveBeenCalledWith("piHole", {
|
||||
id: "new",
|
||||
name: "Pi Hole",
|
||||
url: "http://pi.hole",
|
||||
decryptedSecrets: [
|
||||
expect.objectContaining({
|
||||
kind: "username",
|
||||
value: "dbUsername",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "password",
|
||||
value: "dbPassword",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { FlattenError } from "@homarr/common";
|
||||
import { db } from "@homarr/db";
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
@@ -52,6 +53,7 @@ const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||
data: {
|
||||
...shape.data,
|
||||
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||
error: error.cause instanceof FlattenError ? error.cause.flatten() : null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user