diff --git a/internal/handler/connector.go b/internal/handler/connector.go index dcb32f46b1..549c258fad 100644 --- a/internal/handler/connector.go +++ b/internal/handler/connector.go @@ -391,8 +391,9 @@ func (h *ConnectorHandler) TestConnector(c *gin.Context) { var ( valErr *syncerconnector.ConnectorValidationError credErr *syncerconnector.ConnectorMissingCredentialError + rateErr *syncerconnector.RateLimitTriedTooManyTimesError ) - if errors.As(err, &valErr) || errors.As(err, &credErr) { + if errors.As(err, &valErr) || errors.As(err, &credErr) || errors.As(err, &rateErr) { common.ResponseWithCodeData(c, common.CodeDataError, false, err.Error()) return } diff --git a/internal/handler/connector_test.go b/internal/handler/connector_test.go index bd137f706c..b2c7a67174 100644 --- a/internal/handler/connector_test.go +++ b/internal/handler/connector_test.go @@ -275,6 +275,11 @@ func TestConnectorHandlerTestConnector(t *testing.T) { err: &syncerconnector.ConnectorMissingCredentialError{Message: "REST API (bearer) requires 'token' in credentials"}, wantCode: common.CodeDataError, }, + { + name: "rate limit failure", + err: &syncerconnector.RateLimitTriedTooManyTimesError{Message: "REST API rate limited"}, + wantCode: common.CodeDataError, + }, { name: "unexpected failure", err: fmt.Errorf("boom"), diff --git a/internal/service/connector.go b/internal/service/connector.go index ec77827170..b81d29387e 100644 --- a/internal/service/connector.go +++ b/internal/service/connector.go @@ -428,7 +428,22 @@ func (s *ConnectorService) TestConnector(ctx context.Context, connectorID, userI if !ok { return ErrConnectorTestUnsupported } - return validator.ValidateConnectorSetting(ctx, connectorConfig) + return wrapConnectorValidationError(validator.ValidateConnectorSetting(ctx, connectorConfig)) +} + +func wrapConnectorValidationError(err error) error { + if err == nil { + return nil + } + var ( + valErr *syncerconnector.ConnectorValidationError + credErr *syncerconnector.ConnectorMissingCredentialError + rateErr *syncerconnector.RateLimitTriedTooManyTimesError + ) + if errors.As(err, &valErr) || errors.As(err, &credErr) || errors.As(err, &rateErr) { + return err + } + return &syncerconnector.ConnectorValidationError{Message: err.Error()} } func testConnectorSettings(stored *entity.Connector, request entity.JSONMap) (string, entity.JSONMap, error) { diff --git a/internal/service/connector_test_connection_test.go b/internal/service/connector_test_connection_test.go index 925bcbbe09..291f0d367b 100644 --- a/internal/service/connector_test_connection_test.go +++ b/internal/service/connector_test_connection_test.go @@ -73,6 +73,30 @@ func TestConnectorServiceTestConnectorAllowsUnsavedConnectorWithSource(t *testin } } +func TestConnectorServiceTestConnectorSurfacesRawValidationError(t *testing.T) { + db := setupServiceTestDB(t) + pushServiceDB(t, db) + if err := db.AutoMigrate(&entity.Connector{}, &entity.UserTenant{}); err != nil { + t.Fatalf("migrate connector tables: %v", err) + } + + registry := syncerconnector.NewRegistry() + registry.RegisterConfigFactory("mock", func(config map[string]any) (syncerconnector.Connector, error) { + return &connectormock.Connector{ValidateConnectorSettingErr: errors.New("raw validation failure")}, nil + }) + svc := NewConnectorService() + svc.connectorRegistry = registry + + err := svc.TestConnector(context.Background(), "missing", "tenant-1", entity.JSONMap{ + "source": "mock", + "config": entity.JSONMap{"ok": true}, + }) + var valErr *syncerconnector.ConnectorValidationError + if !errors.As(err, &valErr) || !strings.Contains(valErr.Message, "raw validation failure") { + t.Fatalf("error = %v, want *ConnectorValidationError with raw message", err) + } +} + func TestConnectorServiceTestConnectorRejectsMissingConfigForMissingConnector(t *testing.T) { db := setupServiceTestDB(t) pushServiceDB(t, db) diff --git a/internal/syncer/connector/webdav.go b/internal/syncer/connector/webdav.go index a797bd3dd8..91a43a1a18 100644 --- a/internal/syncer/connector/webdav.go +++ b/internal/syncer/connector/webdav.go @@ -134,6 +134,13 @@ func (c *WebDAVConnector) Validate(ctx context.Context) error { return fmt.Errorf("WebDAV validation failed for path '%s': %v", testPath, err) } +// ValidateConnectorSetting validates WebDAV settings from an unsaved config. +func (c *WebDAVConnector) ValidateConnectorSetting(ctx context.Context, request map[string]any) error { + ctx, cancel := context.WithTimeout(ctx, connectorSettingValidationTimeout) + defer cancel() + return c.Validate(ctx) +} + // OpenSync opens one WebDAV sync session. func (c *WebDAVConnector) OpenSync(ctx context.Context, request SyncRequest) (SyncSession, error) { files, err := c.listFiles(ctx, c.remotePath) diff --git a/internal/syncer/connector/webdav_test.go b/internal/syncer/connector/webdav_test.go index 2043398f51..328fcac7ce 100644 --- a/internal/syncer/connector/webdav_test.go +++ b/internal/syncer/connector/webdav_test.go @@ -219,6 +219,27 @@ func TestWebDAVConnectorValidate(t *testing.T) { } } +func TestWebDAVConnectorValidateConnectorSetting(t *testing.T) { + server, _, _ := newWebDAVTestServer(t, webDAVTestTree()) + + connector := webDAVTestConnector(t, server.URL, false, 2) + if err := connector.ValidateConnectorSetting(context.Background(), nil); err != nil { + t.Fatalf("ValidateConnectorSetting failed: %v", err) + } + + missingCredentials, err := NewWebDAVConnector(map[string]any{ + "base_url": server.URL, + "credentials": map[string]any{}, + }) + if err != nil { + t.Fatalf("NewWebDAVConnector failed: %v", err) + } + err = missingCredentials.ValidateConnectorSetting(context.Background(), nil) + if err == nil || !strings.Contains(err.Error(), "username and password") { + t.Fatalf("missing credentials error = %v", err) + } +} + func TestWebDAVConnectorOpenSyncFull(t *testing.T) { t.Setenv("BLOB_STORAGE_SIZE_THRESHOLD", "20") server, authHeaders, _ := newWebDAVTestServer(t, webDAVTestTree())