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:
@@ -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",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user