feat(integration): improve integration test connection (#3005)

This commit is contained in:
Meier Lukas
2025-05-16 20:59:12 +02:00
committed by GitHub
parent 3daf1c8341
commit ef9a5e9895
111 changed files with 7168 additions and 976 deletions

View File

@@ -0,0 +1,30 @@
import { ResponseError } from "@homarr/common/server";
import type { IIntegrationErrorHandler } from "../base/errors/handler";
import { integrationFetchHttpErrorHandler } from "../base/errors/http";
import { IntegrationResponseError } from "../base/errors/http/integration-response-error";
import type { IntegrationError, IntegrationErrorData } from "../base/errors/integration-error";
export class ProxmoxApiErrorHandler implements IIntegrationErrorHandler {
handleError(error: unknown, integration: IntegrationErrorData): IntegrationError | undefined {
if (!(error instanceof Error)) return undefined;
if (error.cause && error.cause instanceof TypeError) {
return integrationFetchHttpErrorHandler.handleError(error.cause, integration);
}
if (error.message.includes(" return Error 400 "))
return new IntegrationResponseError(integration, { cause: new ResponseError({ status: 400 }, { cause: error }) });
if (error.message.includes(" return Error 500 "))
return new IntegrationResponseError(integration, { cause: new ResponseError({ status: 500 }, { cause: error }) });
if (error.message.includes(" return Error 401 "))
return new IntegrationResponseError(integration, { cause: new ResponseError({ status: 401 }, { cause: error }) });
const otherStatusCode = /connection failed with (\d{3})/.exec(error.message)?.at(1);
if (!otherStatusCode) return undefined;
const statusCode = parseInt(otherStatusCode, 10);
return new IntegrationResponseError(integration, {
cause: new ResponseError({ status: statusCode }, { cause: error }),
});
}
}

View File

@@ -2,11 +2,13 @@ import type { Proxmox } from "proxmox-api";
import proxmoxApi from "proxmox-api";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { extractErrorMessage } from "@homarr/common";
import { logger } from "@homarr/log";
import { HandleIntegrationErrors } from "../base/errors/decorator";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { IntegrationTestConnectionError } from "../base/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import { ProxmoxApiErrorHandler } from "./proxmox-error-handler";
import type {
ComputeResourceBase,
LxcResource,
@@ -16,12 +18,12 @@ import type {
StorageResource,
} from "./proxmox-types";
@HandleIntegrationErrors([new ProxmoxApiErrorHandler()])
export class ProxmoxIntegration extends Integration {
public async testConnectionAsync(): Promise<void> {
const proxmox = this.getPromoxApi();
await proxmox.nodes.$get().catch((error) => {
throw new IntegrationTestConnectionError("internalServerError", extractErrorMessage(error));
});
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const proxmox = this.getPromoxApi(input.fetchAsync);
await proxmox.nodes.$get();
return { success: true };
}
public async getClusterInfoAsync() {
@@ -41,12 +43,12 @@ export class ProxmoxIntegration extends Integration {
};
}
private getPromoxApi() {
private getPromoxApi(fetchAsync = fetchWithTrustedCertificatesAsync) {
return proxmoxApi({
host: this.url("/").host,
tokenID: `${this.getSecretValue("username")}@${this.getSecretValue("realm")}!${this.getSecretValue("tokenId")}`,
tokenSecret: this.getSecretValue("apiKey"),
fetch: fetchWithTrustedCertificatesAsync,
fetch: fetchAsync,
});
}
}

View File

@@ -0,0 +1,89 @@
import proxmoxApi from "proxmox-api";
import type { fetch as undiciFetch } from "undici";
import { Response } from "undici";
import { describe, expect, test } from "vitest";
import { IntegrationRequestError } from "../../base/errors/http/integration-request-error";
import { IntegrationResponseError } from "../../base/errors/http/integration-response-error";
import { ProxmoxApiErrorHandler } from "../proxmox-error-handler";
describe("ProxmoxApiErrorHandler handleError should handle the provided error accordingly", () => {
test.each([400, 401, 500])("should handle %s error", async (statusCode) => {
// Arrange
// eslint-disable-next-line no-restricted-syntax
const mockedFetch: typeof undiciFetch = async () => {
return Promise.resolve(createFakeResponse(statusCode));
};
// Act
const result = await runWithAsync(mockedFetch);
// Assert
expect(result).toBeInstanceOf(IntegrationResponseError);
const error = result as unknown as IntegrationResponseError;
expect(error.cause.statusCode).toBe(statusCode);
});
test("should handle other non successful status codes", async () => {
// Arrange
// eslint-disable-next-line no-restricted-syntax
const mockedFetch: typeof undiciFetch = async () => {
return Promise.resolve(createFakeResponse(404));
};
// Act
const result = await runWithAsync(mockedFetch);
// Assert
expect(result).toBeInstanceOf(IntegrationResponseError);
const error = result as unknown as IntegrationResponseError;
expect(error.cause.statusCode).toBe(404);
});
test("should handle request error", async () => {
// Arrange
const mockedFetch: typeof undiciFetch = () => {
const errorWithCode = new Error("Inner error") as Error & { code: string };
errorWithCode.code = "ECONNREFUSED";
throw new TypeError("Outer error", { cause: errorWithCode });
};
// Act
const result = await runWithAsync(mockedFetch);
// Assert
// In the end in should have the structure IntegrationRequestError -> RequestError -> TypeError -> Error (with code)
expect(result).toBeInstanceOf(IntegrationRequestError);
const error = result as unknown as IntegrationRequestError;
expect(error.cause.cause).toBeInstanceOf(TypeError);
expect(error.cause.cause?.message).toBe("Outer error");
expect(error.cause.cause?.cause).toBeInstanceOf(Error);
const cause = error.cause.cause?.cause as Error & { code: string };
expect(cause.message).toBe("Inner error");
expect(cause.code).toBe("ECONNREFUSED");
});
});
const createFakeResponse = (statusCode: number) => {
return new Response(JSON.stringify({ data: {} }), {
status: statusCode,
// It expects a content-type and valid json response
// https://github.com/UrielCh/proxmox-api/blob/master/api/src/ProxmoxEngine.ts#L258
headers: { "content-type": "application/json;charset=UTF-8" },
});
};
const runWithAsync = async (mockedFetch: typeof undiciFetch) => {
const integration = { id: "test", name: "test", url: "http://proxmox.example.com" };
const client = createProxmoxClient(mockedFetch);
const handler = new ProxmoxApiErrorHandler();
return await client.nodes.$get().catch((error) => handler.handleError(error, integration));
};
const createProxmoxClient = (fetch: typeof undiciFetch) => {
return proxmoxApi({
host: "proxmox.example.com",
tokenID: "username@realm!tokenId",
tokenSecret: crypto.randomUUID(),
fetch,
});
};