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

@@ -1,7 +1,11 @@
import path from "path";
import type { fetch as undiciFetch } from "undici";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ResponseError } from "@homarr/common/server";
import type { IntegrationTestingInput } from "../../base/integration";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
@@ -91,12 +95,15 @@ export class Aria2Integration extends DownloadClientIntegration {
}
}
public async testConnectionAsync(): Promise<void> {
const client = this.getClient();
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const client = this.getClient(input.fetchAsync);
await client.getVersion();
return {
success: true,
};
}
private getClient() {
private getClient(fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync) {
const url = this.url("/jsonrpc");
return new Proxy(
@@ -114,21 +121,24 @@ export class Aria2Integration extends DownloadClientIntegration {
method: `aria2.${method}`,
params,
});
return await fetchWithTrustedCertificatesAsync(url, { method: "POST", body })
return await fetchAsync(url, { method: "POST", body })
.then(async (response) => {
const responseBody = (await response.json()) as { result: ReturnType<Aria2GetClient[typeof method]> };
if (!response.ok) {
throw new Error(response.statusText);
throw new ResponseError(response);
}
return responseBody.result;
})
.catch((error) => {
if (error instanceof Error) {
throw new Error(error.message);
} else {
throw new Error("Error communicating with Aria2");
throw error;
}
throw new Error("Error communicating with Aria2", {
cause: error,
});
});
};
},

View File

@@ -1,17 +1,32 @@
import { Deluge } from "@ctrl/deluge";
import dayjs from "dayjs";
import type { Dispatcher } from "undici";
import { createCertificateAgentAsync } from "@homarr/certificates/server";
import { HandleIntegrationErrors } from "../../base/errors/decorator";
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
import type { IntegrationTestingInput } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
export class DelugeIntegration extends DownloadClientIntegration {
public async testConnectionAsync(): Promise<void> {
const client = await this.getClientAsync();
await client.login();
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const client = await this.getClientAsync(input.dispatcher);
const isSuccess = await client.login();
if (!isSuccess) {
return TestConnectionError.UnauthorizedResult(401);
}
return {
success: true,
};
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
@@ -93,11 +108,11 @@ export class DelugeIntegration extends DownloadClientIntegration {
await client.removeTorrent(id, fromDisk);
}
private async getClientAsync() {
private async getClientAsync(dispatcher?: Dispatcher) {
return new Deluge({
baseUrl: this.url("/").toString(),
password: this.getSecretValue("password"),
dispatcher: await createCertificateAgentAsync(),
dispatcher: dispatcher ?? (await createCertificateAgentAsync()),
});
}

View File

@@ -1,7 +1,11 @@
import dayjs from "dayjs";
import type { fetch as undiciFetch } from "undici";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ResponseError } from "@homarr/common/server";
import type { IntegrationTestingInput } from "../../base/integration";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
@@ -9,8 +13,11 @@ import type { DownloadClientStatus } from "../../interfaces/downloads/download-c
import type { NzbGetClient } from "./nzbget-types";
export class NzbGetIntegration extends DownloadClientIntegration {
public async testConnectionAsync(): Promise<void> {
await this.nzbGetApiCallAsync("version");
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
await this.nzbGetApiCallWithCustomFetchAsync(input.fetchAsync, "version");
return {
success: true,
};
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
@@ -93,24 +100,34 @@ export class NzbGetIntegration extends DownloadClientIntegration {
private async nzbGetApiCallAsync<CallType extends keyof NzbGetClient>(
method: CallType,
...params: Parameters<NzbGetClient[CallType]>
): Promise<ReturnType<NzbGetClient[CallType]>> {
return await this.nzbGetApiCallWithCustomFetchAsync(fetchWithTrustedCertificatesAsync, method, ...params);
}
private async nzbGetApiCallWithCustomFetchAsync<CallType extends keyof NzbGetClient>(
fetchAsync: typeof undiciFetch,
method: CallType,
...params: Parameters<NzbGetClient[CallType]>
): Promise<ReturnType<NzbGetClient[CallType]>> {
const username = this.getSecretValue("username");
const password = this.getSecretValue("password");
const url = this.url(`/${encodeURIComponent(username)}:${encodeURIComponent(password)}/jsonrpc`);
const body = JSON.stringify({ method, params });
return await fetchWithTrustedCertificatesAsync(url, { method: "POST", body })
return await fetchAsync(url, { method: "POST", body })
.then(async (response) => {
if (!response.ok) {
throw new Error(response.statusText);
throw new ResponseError(response);
}
return ((await response.json()) as { result: ReturnType<NzbGetClient[CallType]> }).result;
})
.catch((error) => {
if (error instanceof Error) {
throw new Error(error.message);
} else {
throw new Error("Error communicating with NzbGet");
throw error;
}
throw new Error("Error communicating with NzbGet", {
cause: error,
});
});
}

View File

@@ -1,17 +1,29 @@
import { QBittorrent } from "@ctrl/qbittorrent";
import dayjs from "dayjs";
import type { Dispatcher } from "undici";
import { createCertificateAgentAsync } from "@homarr/certificates/server";
import { HandleIntegrationErrors } from "../../base/errors/decorator";
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
import type { IntegrationTestingInput } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
export class QBitTorrentIntegration extends DownloadClientIntegration {
public async testConnectionAsync(): Promise<void> {
const client = await this.getClientAsync();
await client.login();
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const client = await this.getClientAsync(input.dispatcher);
const isSuccess = await client.login();
if (!isSuccess) return TestConnectionError.UnauthorizedResult(401);
return {
success: true,
};
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
@@ -76,12 +88,12 @@ export class QBitTorrentIntegration extends DownloadClientIntegration {
await client.removeTorrent(id, fromDisk);
}
private async getClientAsync() {
private async getClientAsync(dispatcher?: Dispatcher) {
return new QBittorrent({
baseUrl: this.url("/").toString(),
username: this.getSecretValue("username"),
password: this.getSecretValue("password"),
dispatcher: await createCertificateAgentAsync(),
dispatcher: dispatcher ?? (await createCertificateAgentAsync()),
});
}

View File

@@ -1,8 +1,12 @@
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import type { fetch as undiciFetch } from "undici";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ResponseError } from "@homarr/common/server";
import type { IntegrationTestingInput } from "../../base/integration";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
@@ -12,9 +16,10 @@ import { historySchema, queueSchema } from "./sabnzbd-schema";
dayjs.extend(duration);
export class SabnzbdIntegration extends DownloadClientIntegration {
public async testConnectionAsync(): Promise<void> {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
//This is the one call that uses the least amount of data while requiring the api key
await this.sabNzbApiCallAsync("translate", { value: "ping" });
await this.sabNzbApiCallWithCustomFetchAsync(input.fetchAsync, "translate", { value: "ping" });
return { success: true };
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
@@ -101,6 +106,13 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
}
private async sabNzbApiCallAsync(mode: string, searchParams?: Record<string, string>): Promise<unknown> {
return await this.sabNzbApiCallWithCustomFetchAsync(fetchWithTrustedCertificatesAsync, mode, searchParams);
}
private async sabNzbApiCallWithCustomFetchAsync(
fetchAsync: typeof undiciFetch,
mode: string,
searchParams?: Record<string, string>,
): Promise<unknown> {
const url = this.url("/api", {
...searchParams,
output: "json",
@@ -108,19 +120,21 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
apikey: this.getSecretValue("apiKey"),
});
return await fetchWithTrustedCertificatesAsync(url)
return await fetchAsync(url)
.then((response) => {
if (!response.ok) {
throw new Error(response.statusText);
throw new ResponseError(response);
}
return response.json();
})
.catch((error) => {
if (error instanceof Error) {
throw new Error(error.message);
} else {
throw new Error("Error communicating with SABnzbd");
throw error;
}
throw new Error("Error communicating with SABnzbd", {
cause: error,
});
});
}

View File

@@ -1,17 +1,26 @@
import { Transmission } from "@ctrl/transmission";
import dayjs from "dayjs";
import type { Dispatcher } from "undici";
import { createCertificateAgentAsync } from "@homarr/certificates/server";
import { HandleIntegrationErrors } from "../../base/errors/decorator";
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
import type { IntegrationTestingInput } from "../../base/integration";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
export class TransmissionIntegration extends DownloadClientIntegration {
public async testConnectionAsync(): Promise<void> {
const client = await this.getClientAsync();
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const client = await this.getClientAsync(input.dispatcher);
await client.getSession();
return {
success: true,
};
}
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
@@ -76,12 +85,12 @@ export class TransmissionIntegration extends DownloadClientIntegration {
await client.removeTorrent(id, fromDisk);
}
private async getClientAsync() {
private async getClientAsync(dispatcher?: Dispatcher) {
return new Transmission({
baseUrl: this.url("/").toString(),
username: this.getSecretValue("username"),
password: this.getSecretValue("password"),
dispatcher: await createCertificateAgentAsync(),
dispatcher: dispatcher ?? (await createCertificateAgentAsync()),
});
}