feat(SKY-8558): add Test Connection button to custom credential service settings (#5299)
Some checks are pending
Run tests and pre-commit / Run tests and pre-commit hooks (push) Waiting to run
Run tests and pre-commit / Frontend Lint and Build (push) Waiting to run
Publish Fern Docs / run (push) Waiting to run

This commit is contained in:
Celal Zamanoğlu 2026-03-31 01:53:13 +03:00 committed by GitHub
parent b5c3703eb7
commit 1d1d9c8e65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 141 additions and 1 deletions

View file

@ -42,6 +42,8 @@ export function CustomCredentialServiceConfigForm() {
isLoading,
createOrUpdateConfig,
isUpdating,
testConnection,
isTesting,
} = useCustomCredentialServiceConfig();
const form = useForm<FormData>({
@ -58,6 +60,13 @@ export function CustomCredentialServiceConfigForm() {
createOrUpdateConfig(data);
};
const onTestConnection = async () => {
const valid = await form.trigger();
if (!valid) return;
const values = form.getValues();
testConnection(values);
};
const toggleApiTokenVisibility = () => {
setShowApiToken((v) => !v);
};
@ -159,9 +168,20 @@ export function CustomCredentialServiceConfigForm() {
/>
<div className="flex items-center gap-4">
<Button type="submit" disabled={isLoading || isUpdating}>
<Button
type="submit"
disabled={isLoading || isUpdating || isTesting}
>
{isUpdating ? "Updating..." : "Update Configuration"}
</Button>
<Button
type="button"
variant="outline"
disabled={isLoading || isUpdating || isTesting}
onClick={onTestConnection}
>
{isTesting ? "Testing..." : "Test Connection"}
</Button>
{customCredentialServiceAuthToken && (
<div className="text-sm text-muted-foreground">

View file

@ -82,11 +82,47 @@ export function useCustomCredentialServiceConfig() {
},
});
const testConnectionMutation = useMutation({
mutationFn: async (data: CreateCustomCredentialServiceConfigRequest) => {
const client = await getClient(credentialGetter, "sans-api-v1");
return await client
.post("/credentials/custom_credential/test_connection", data)
.then((response) => response.data as { success: boolean });
},
onSuccess: () => {
toast({
title: "Success",
description: "Connection successful",
variant: "success",
});
},
onError: (error: unknown) => {
const resp = (error as { response?: { data?: { detail?: unknown } } })
?.response;
const detail = resp?.data?.detail;
let message: string;
if (typeof detail === "string") {
message = detail;
} else if (Array.isArray(detail)) {
message = detail.map((d) => d.msg ?? JSON.stringify(d)).join("; ");
} else {
message = (error as Error)?.message || "Connection test failed";
}
toast({
title: "Error",
description: message,
variant: "destructive",
});
},
});
return {
customCredentialServiceAuthToken,
parsedConfig,
isLoading,
createOrUpdateConfig: createOrUpdateConfigMutation.mutate,
isUpdating: createOrUpdateConfigMutation.isPending,
testConnection: testConnectionMutation.mutate,
isTesting: testConnectionMutation.isPending,
};
}

View file

@ -34,7 +34,9 @@ from fastapi import BackgroundTasks, Body, Depends, HTTPException, Path, Query
from skyvern.config import settings
from skyvern.exceptions import HttpException as SkyvernHttpException
from skyvern.exceptions import SkyvernHTTPException
from skyvern.forge import app
from skyvern.forge.sdk.core.aiohttp_helper import aiohttp_request
from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType
from skyvern.forge.sdk.executor.factory import AsyncExecutorFactory
from skyvern.forge.sdk.routes.code_samples import (
@ -83,6 +85,7 @@ from skyvern.forge.sdk.schemas.organizations import (
CreateOnePasswordTokenResponse,
CustomCredentialServiceConfigResponse,
Organization,
TestConnectionResponse,
)
from skyvern.forge.sdk.schemas.totp_codes import OTPType, TOTPCode, TOTPCodeCreate
from skyvern.forge.sdk.services import org_auth_service
@ -99,6 +102,7 @@ from skyvern.schemas.workflows import (
)
from skyvern.services.otp_service import OTPValue, parse_otp_login
from skyvern.services.run_service import cancel_workflow_run
from skyvern.utils.url_validators import validate_url
LOG = structlog.get_logger()
@ -1871,6 +1875,80 @@ async def update_custom_credential_service_config(
) from e
@base_router.post(
"/credentials/custom_credential/test_connection",
summary="Test Custom Credential Service Connection",
description="Tests connectivity to the custom credential service API.",
include_in_schema=False,
)
@base_router.post(
"/credentials/custom_credential/test_connection/",
include_in_schema=False,
)
async def test_custom_credential_service_connection(
request: CreateCustomCredentialServiceConfigRequest,
current_org: Organization = Depends(org_auth_service.get_current_org),
) -> TestConnectionResponse:
"""
Test connectivity to the custom credential service API.
Makes a GET request to the api_base_url with the provided Bearer token
to verify the service is reachable and the token is valid.
Uses the shared URL validator for scheme/host validation (respects ALLOWED_HOSTS / BLOCKED_HOSTS).
"""
api_base_url = request.config.api_base_url
api_token = request.config.api_token
try:
validated_url = validate_url(api_base_url)
except SkyvernHTTPException as e:
raise HTTPException(status_code=e.status_code, detail=str(e)) from e
if not validated_url:
raise HTTPException(status_code=400, detail="Invalid URL")
try:
status_code, _, _ = await aiohttp_request(
method="GET",
url=validated_url,
headers={"Authorization": f"Bearer {api_token}"},
timeout=10,
)
if 200 <= status_code < 300:
LOG.info(
"Custom credential service connection test succeeded",
organization_id=current_org.organization_id,
api_base_url=api_base_url,
status_code=status_code,
)
return TestConnectionResponse(success=True)
LOG.warning(
"Custom credential service returned non-2xx status",
organization_id=current_org.organization_id,
api_base_url=api_base_url,
status_code=status_code,
)
raise HTTPException(
status_code=400,
detail=f"Connection test failed: server returned HTTP {status_code}",
)
except HTTPException:
raise
except Exception as e:
LOG.warning(
"Custom credential service connection test failed",
organization_id=current_org.organization_id,
api_base_url=api_base_url,
error=str(e),
)
raise HTTPException(
status_code=400,
detail="Connection test failed: could not reach the specified URL",
) from e
async def _get_credential_vault_service(
vault_type_override: CredentialVaultType | None = None,
) -> CredentialVaultService:

View file

@ -146,6 +146,12 @@ class CustomCredentialServiceConfigResponse(BaseModel):
)
class TestConnectionResponse(BaseModel):
"""Response model for the custom credential service connection test."""
success: bool
class CreateCustomCredentialServiceConfigRequest(BaseModel):
"""Request model for creating or updating custom credential service configuration."""