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:
Meier Lukas
2024-06-22 21:02:04 +02:00
committed by GitHub
parent 92afd82d22
commit f92aeba403
30 changed files with 1138 additions and 550 deletions

View File

@@ -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");
});
});

View File

@@ -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",
}),
],
});
});
});