feat(integration): improve integration test connection (#3005)
This commit is contained in:
30
packages/integrations/src/proxmox/proxmox-error-handler.ts
Normal file
30
packages/integrations/src/proxmox/proxmox-error-handler.ts
Normal 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 }),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user