fix(watchsync): close connection validation gaps

This commit is contained in:
Quick104
2026-08-06 16:21:42 -04:00
parent 2dd4069009
commit f1a909e7ad
2 changed files with 51 additions and 3 deletions
+30 -1
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"unicode"
@@ -774,17 +775,24 @@ func validateRequiredConnectionSchemaIsRenderable(schema *pluginv1.ConfigSchema)
}
}
for key, property := range document.Properties {
field := explicit[key]
switch property.Type {
case "string", watchSyncJSONSchemaNumberType, "integer", "boolean":
continue
case "array":
field := explicit[key]
if field != nil && field.GetControl() == pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_MULTI_SELECT &&
(property.Items == nil || property.Items.Type == "string" || property.Items.Type == watchSyncJSONSchemaNumberType ||
property.Items.Type == "integer" || property.Items.Type == "boolean") {
continue
}
default:
// A property whose shape comes from enum/const/$ref cannot be
// inferred from type alone, but an explicit scalar form control is
// still a complete input mechanism. Direct object properties remain
// unsupported because none of these controls produces an object.
if property.Type != "object" && connectionAdminFieldRendersValue(field) {
continue
}
}
return fmt.Errorf(
"required connection config %q property %q needs a renderable admin_form field because type %q cannot be inferred",
@@ -796,6 +804,24 @@ func validateRequiredConnectionSchemaIsRenderable(schema *pluginv1.ConfigSchema)
return nil
}
func connectionAdminFieldRendersValue(field *pluginv1.AdminFormField) bool {
if field == nil {
return false
}
switch field.GetControl() {
case pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_TEXT,
pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_TEXTAREA,
pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_PASSWORD,
pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_NUMBER,
pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_SWITCH,
pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_SELECT,
pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_MULTI_SELECT:
return true
default:
return false
}
}
func validateConnectionAdminFormValue(schema *pluginv1.ConfigSchema, value map[string]any) error {
if schema == nil || schema.GetAdminForm() == nil {
return nil
@@ -868,6 +894,9 @@ func connectionConfigNumber(value any) (float64, bool) {
case json.Number:
number, err := typed.Float64()
return number, err == nil
case string:
number, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
return number, err == nil
default:
return 0, false
}
+21 -2
View File
@@ -398,6 +398,9 @@ func TestPluginProviderRejectsRequiredConnectionConfigTheWebCannotRender(t *test
ConnectionConfigSchema: []*pluginv1.ConfigSchema{{
Key: "server", Required: true,
JsonSchema: `{"type":"object","properties":{"headers":{"type":"object"}}}`,
AdminForm: &pluginv1.AdminFormDescriptor{Fields: []*pluginv1.AdminFormField{{
Key: "headers", Control: pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_TEXTAREA,
}}},
}},
ResolveClient: func(context.Context, int, string) (WatchSyncPluginClient, error) {
return &fakeWatchSyncPluginClient{}, nil
@@ -408,7 +411,7 @@ func TestPluginProviderRejectsRequiredConnectionConfigTheWebCannotRender(t *test
}
}
func TestPluginProviderAcceptsRequiredScalarFieldsMissingFromPartialAdminForm(t *testing.T) {
func TestPluginProviderAcceptsRenderableRequiredConnectionConfig(t *testing.T) {
_, err := NewPluginProvider(PluginProviderOptions{
InstallationID: 4, ProviderKey: testPluginProviderKey, CapabilityID: testPluginCapabilityID,
Descriptor: &pluginv1.WatchSyncProviderDescriptor{AuthMethods: []pluginv1.WatchSyncAuthMethod{
@@ -430,6 +433,21 @@ func TestPluginProviderAcceptsRequiredScalarFieldsMissingFromPartialAdminForm(t
Options: []*pluginv1.AdminFormOption{{Value: "true", Label: "Enabled"}, {Value: "false", Label: "Disabled"}},
}}},
},
{
Key: "mode", Required: true,
JsonSchema: `{"type":"object","properties":{"value":{"enum":["standard","anime"]}}}`,
AdminForm: &pluginv1.AdminFormDescriptor{Fields: []*pluginv1.AdminFormField{{
Key: "value", Control: pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_SELECT,
Options: []*pluginv1.AdminFormOption{{Value: "standard", Label: "Standard"}, {Value: "anime", Label: "Anime"}},
}}},
},
{
Key: "reference", Required: true,
JsonSchema: `{"type":"object","properties":{"endpoint":{"$ref":"#/$defs/endpoint"}},"$defs":{"endpoint":{"type":"string","format":"uri"}}}`,
AdminForm: &pluginv1.AdminFormDescriptor{Fields: []*pluginv1.AdminFormField{{
Key: "endpoint", Control: pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_TEXT,
}}},
},
},
ResolveClient: func(context.Context, int, string) (WatchSyncPluginClient, error) {
return &fakeWatchSyncPluginClient{}, nil
@@ -452,7 +470,7 @@ func TestPluginProviderEnforcesConnectionAdminFormValidation(t *testing.T) {
}},
ConnectionConfigSchema: []*pluginv1.ConfigSchema{{
Key: "server", Required: true,
JsonSchema: `{"type":"object","properties":{"name":{"type":"string"},"port":{"type":"number"},"password":{"type":"string","format":"password"}},"required":["name","port","password"]}`,
JsonSchema: `{"type":"object","properties":{"name":{"type":"string"},"port":{},"password":{"type":"string","format":"password"}},"required":["name","port","password"]}`,
AdminForm: &pluginv1.AdminFormDescriptor{Fields: []*pluginv1.AdminFormField{
{Key: "name", Control: pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_TEXT, Validation: &pluginv1.AdminFormValidation{Pattern: `^[a-z]+$`, MinLength: 3, MaxLength: 8}},
{Key: "port", Control: pluginv1.AdminFormControl_ADMIN_FORM_CONTROL_NUMBER, Validation: &pluginv1.AdminFormValidation{HasMin: true, Min: 1, HasMax: true, Max: 65535}},
@@ -474,6 +492,7 @@ func TestPluginProviderEnforcesConnectionAdminFormValidation(t *testing.T) {
}{
{name: "pattern", values: map[string]any{"name": "Bad", "port": 443.0, "password": "long-enough"}, message: "is invalid"},
{name: "number", values: map[string]any{"name": "good", "port": 70000.0, "password": "long-enough"}, message: "at most 65535"},
{name: "numeric string", values: map[string]any{"name": "good", "port": "70000", "password": "long-enough"}, message: "at most 65535"},
{name: "secret length", values: map[string]any{"name": "good", "port": 443.0, "password": "leaky"}, message: "at least 8 characters"},
}
for _, tt := range tests {