Explorar o código

fix: Argument validation targets the bindings (fixes checkboxes, etc)

jamesread hai 6 meses
pai
achega
09e1f0f984

+ 11 - 1
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts

@@ -1,4 +1,4 @@
-// @generated by protoc-gen-es v2.10.1
+// @generated by protoc-gen-es v2.10.2
 // @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
 /* eslint-disable */
 
@@ -686,6 +686,16 @@ export declare type ValidateArgumentTypeRequest = Message<"olivetin.api.v1.Valid
    * @generated from field: string type = 2;
    */
   type: string;
+
+  /**
+   * @generated from field: string binding_id = 3;
+   */
+  bindingId: string;
+
+  /**
+   * @generated from field: string argument_name = 4;
+   */
+  argumentName: string;
 };
 
 /**

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 1
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js


+ 15 - 3
frontend/resources/vue/views/ArgumentForm.vue

@@ -116,8 +116,8 @@ async function setup() {
   // Run initial validation on all fields after DOM is updated
   await nextTick()
   for (const arg of actionArguments.value) {
-    if (arg.type && !arg.type.startsWith('regex:') && arg.type !== 'select' && arg.type !== '' && arg.type !== 'confirmation') {
-      await validateArgument(arg, argValues.value[arg.name])
+    if (arg.type && !arg.type.startsWith('regex:') && arg.type !== 'select' && arg.type !== '' && arg.type !== 'confirmation' && arg.type !== 'checkbox') {
+      await validateArgument(arg, argValues.value[arg.name] || '')
     }
   }
 }
@@ -212,10 +212,22 @@ async function validateArgument(arg, value) {
     return
   }
 
+  // Skip validation for checkbox and confirmation - they're always valid
+  if (arg.type === 'checkbox' || arg.type === 'confirmation') {
+    const inputElement = document.getElementById(arg.name)
+    if (inputElement) {
+      inputElement.setCustomValidity('')
+    }
+    delete formErrors.value[arg.name]
+    return
+  }
+
   try {
     const validateArgumentTypeArgs = {
       value: value,
-      type: arg.type
+      type: arg.type,
+      bindingId: props.bindingId,
+      argumentName: arg.name
     }
 
     const validation = await window.client.validateArgumentType(validateArgumentTypeArgs)

+ 103 - 0
integration-tests/tests/checkbox/checkbox.mjs

@@ -0,0 +1,103 @@
+import { describe, it, before, after } from 'mocha'
+import { expect } from 'chai'
+import { By, Condition } from 'selenium-webdriver'
+import {
+  getRootAndWait,
+  getActionButton,
+  takeScreenshotOnFailure,
+} from '../../lib/elements.js'
+
+describe('config: checkbox', function () {
+  before(async function () {
+    await runner.start('checkbox')
+  })
+
+  after(async () => {
+    await runner.stop()
+  })
+
+  afterEach(function () {
+    takeScreenshotOnFailure(this.currentTest, webdriver)
+  })
+
+  it('Checkbox argument is rendered as a checkbox input', async function () {
+    await getRootAndWait()
+
+    const btn = await getActionButton(webdriver, 'Test checkbox argument')
+
+    await btn.click()
+
+    // Wait for navigation to argument form page
+    await webdriver.wait(
+      new Condition('wait for argument form page', async () => {
+        const url = await webdriver.getCurrentUrl()
+        return url.includes('/actionBinding/') && url.includes('/argumentForm')
+      }),
+      8000
+    )
+
+    // Find the checkbox input field
+    const checkboxInput = await webdriver.findElement(By.id('confirm'))
+
+    // Verify it's an input of type checkbox
+    const tagName = await checkboxInput.getTagName()
+    expect(tagName).to.equal('input')
+
+    const inputType = await checkboxInput.getAttribute('type')
+    expect(inputType).to.equal('checkbox')
+
+    // Verify the label is present
+    const label = await webdriver.findElement(By.css('label[for="confirm"]'))
+    expect(await label.getText()).to.contain('Confirm option')
+  })
+
+  it('Checkbox argument can be toggled and submitted', async function () {
+    await getRootAndWait()
+
+    const btn = await getActionButton(webdriver, 'Test checkbox argument')
+
+    await btn.click()
+
+    // Wait for navigation to argument form page
+    await webdriver.wait(
+      new Condition('wait for argument form page', async () => {
+        const url = await webdriver.getCurrentUrl()
+        return url.includes('/actionBinding/') && url.includes('/argumentForm')
+      }),
+      8000
+    )
+
+    const checkboxInput = await webdriver.findElement(By.id('confirm'))
+
+    // Toggle the checkbox
+    await checkboxInput.click()
+
+    // Small wait for Vue to process the change
+    await webdriver.sleep(100)
+
+    // Verify the checkbox is checked
+    const isChecked = await checkboxInput.isSelected()
+    expect(isChecked).to.be.true
+
+    // Find and click the submit button
+    const submitButton = await webdriver.findElement(
+      By.css('button[name="start"]')
+    )
+    await submitButton.click()
+
+    // Wait for navigation to logs page
+    await webdriver.wait(
+      new Condition('wait for logs page', async () => {
+        const url = await webdriver.getCurrentUrl()
+        return url.includes('/logs/')
+      }),
+      8000
+    )
+
+    // Verify we're on the logs page (action was executed)
+    const url = await webdriver.getCurrentUrl()
+    expect(url).to.include('/logs/')
+  })
+})
+
+

+ 18 - 0
integration-tests/tests/checkbox/config.yaml

@@ -0,0 +1,18 @@
+---
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
+
+logLevel: "DEBUG"
+checkForUpdates: false
+
+actions:
+  - title: Test checkbox argument
+    shell: "echo 'Checkbox value: {{ confirm }}'"
+    icon: ping
+    arguments:
+      - name: confirm
+        title: Confirm option
+        type: checkbox
+        description: "When checked: 1, when unchecked: 0"
+        default: "false"
+
+

+ 2 - 0
proto/olivetin/api/v1/olivetin.proto

@@ -164,6 +164,8 @@ message GetActionLogsResponse {
 message ValidateArgumentTypeRequest {
 	string value = 1;
 	string type = 2;
+	string binding_id = 3;
+	string argument_name = 4;
 }
 
 message ValidateArgumentTypeResponse {

+ 22 - 3
service/gen/olivetin/api/v1/olivetin.pb.go

@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.36.10
+// 	protoc-gen-go v1.36.11
 // 	protoc        (unknown)
 // source: olivetin/api/v1/olivetin.proto
 
@@ -1509,6 +1509,8 @@ type ValidateArgumentTypeRequest struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
 	Value         string                 `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
 	Type          string                 `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
+	BindingId     string                 `protobuf:"bytes,3,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`
+	ArgumentName  string                 `protobuf:"bytes,4,opt,name=argument_name,json=argumentName,proto3" json:"argument_name,omitempty"`
 	unknownFields protoimpl.UnknownFields
 	sizeCache     protoimpl.SizeCache
 }
@@ -1557,6 +1559,20 @@ func (x *ValidateArgumentTypeRequest) GetType() string {
 	return ""
 }
 
+func (x *ValidateArgumentTypeRequest) GetBindingId() string {
+	if x != nil {
+		return x.BindingId
+	}
+	return ""
+}
+
+func (x *ValidateArgumentTypeRequest) GetArgumentName() string {
+	if x != nil {
+		return x.ArgumentName
+	}
+	return ""
+}
+
 type ValidateArgumentTypeResponse struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
 	Valid         bool                   `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"`
@@ -3946,10 +3962,13 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
 	"\tpage_size\x18\x03 \x01(\x03R\bpageSize\x12\x1f\n" +
 	"\vtotal_count\x18\x04 \x01(\x03R\n" +
 	"totalCount\x12!\n" +
-	"\fstart_offset\x18\x05 \x01(\x03R\vstartOffset\"G\n" +
+	"\fstart_offset\x18\x05 \x01(\x03R\vstartOffset\"\x8b\x01\n" +
 	"\x1bValidateArgumentTypeRequest\x12\x14\n" +
 	"\x05value\x18\x01 \x01(\tR\x05value\x12\x12\n" +
-	"\x04type\x18\x02 \x01(\tR\x04type\"V\n" +
+	"\x04type\x18\x02 \x01(\tR\x04type\x12\x1d\n" +
+	"\n" +
+	"binding_id\x18\x03 \x01(\tR\tbindingId\x12#\n" +
+	"\rargument_name\x18\x04 \x01(\tR\fargumentName\"V\n" +
 	"\x1cValidateArgumentTypeResponse\x12\x14\n" +
 	"\x05valid\x18\x01 \x01(\bR\x05valid\x12 \n" +
 	"\vdescription\x18\x02 \x01(\tR\vdescription\"K\n" +

+ 38 - 1
service/internal/api/api.go

@@ -587,11 +587,48 @@ func paginate(total int64, size int64, start int64) pageInfo {
 This function is ONLY a helper for the UI - the arguments are validated properly
 on the StartAction -> Executor chain. This is here basically to provide helpful
 error messages more quickly before starting the action.
+
+It uses the same validation logic as the executor, including mangling argument
+values (e.g., datetime formatting, checkbox title-to-value conversion).
 */
 func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) {
-	err := executor.TypeSafetyCheck("", req.Msg.Value, req.Msg.Type)
+	var err error
 	desc := ""
 
+	// If binding_id and argument_name are provided, use the full validation path
+	if req.Msg.BindingId != "" && req.Msg.ArgumentName != "" {
+		binding := api.executor.FindBindingByID(req.Msg.BindingId)
+		if binding == nil || binding.Action == nil {
+			return connect.NewResponse(&apiv1.ValidateArgumentTypeResponse{
+				Valid:       false,
+				Description: "action binding not found",
+			}), nil
+		}
+
+		// Find the specific argument
+		var arg *config.ActionArgument
+		for i := range binding.Action.Arguments {
+			if binding.Action.Arguments[i].Name == req.Msg.ArgumentName {
+				arg = &binding.Action.Arguments[i]
+				break
+			}
+		}
+
+		if arg == nil {
+			return connect.NewResponse(&apiv1.ValidateArgumentTypeResponse{
+				Valid:       false,
+				Description: "argument not found",
+			}), nil
+		}
+
+		// Use the same validation path as the executor (includes mangling)
+		err = executor.ValidateArgument(arg, req.Msg.Value, binding.Action)
+	} else {
+		// Fallback to simple type check if binding_id/argument_name not provided
+		// (for backwards compatibility, though this path doesn't handle choices/null checks)
+		err = executor.TypeSafetyCheck("", req.Msg.Value, req.Msg.Type)
+	}
+
 	if err != nil {
 		desc = err.Error()
 	}

+ 52 - 0
service/internal/executor/arguments.go

@@ -175,6 +175,17 @@ func typecheckActionArgument(arg *config.ActionArgument, value string, action *c
 	return typecheckActionArgumentFound(value, action, arg)
 }
 
+// ValidateArgument validates a single argument value using the same logic as the executor.
+// It applies mangling transformations and performs full validation including null checks,
+// choice validation, and type safety checks.
+func ValidateArgument(arg *config.ActionArgument, value string, action *config.Action) error {
+	// Apply mangling transformations
+	mangledValue := MangleArgumentValue(arg, value, action.Title)
+
+	// Use the same validation path as the executor
+	return typecheckActionArgument(arg, mangledValue, action)
+}
+
 func typecheckActionArgumentFound(value string, action *config.Action, arg *config.ActionArgument) error {
 	if value == "" {
 		return typecheckNull(arg)
@@ -198,6 +209,8 @@ func TypeSafetyCheck(name string, value string, argumentType string) error {
 		return nil
 	case "raw_string_multiline":
 		return nil
+	case "checkbox":
+		return nil
 	case "email":
 		return typeSafetyCheckEmail(value)
 	case "url":
@@ -369,3 +382,42 @@ func mangleInvalidDatetimeValues(req *ExecutionRequest, arg *config.ActionArgume
 		req.Arguments[arg.Name] = timestamp.Format("2006-01-02T15:04:05")
 	}
 }
+
+// MangleArgumentValue applies mangling transformations to a single argument value.
+// This is used by the validation API to ensure the value matches what would be
+// used during actual execution.
+func MangleArgumentValue(arg *config.ActionArgument, value string, actionTitle string) string {
+	if arg.Type == "datetime" {
+		if value == "" {
+			return value
+		}
+
+		timestamp, err := time.Parse("2006-01-02T15:04", value)
+		if err == nil {
+			log.WithFields(log.Fields{
+				"arg":         arg.Name,
+				"value":       value,
+				"actionTitle": actionTitle,
+			}).Warnf("Mangled invalid datetime value without seconds to :00 seconds, this issue is commonly caused by Android browsers.")
+
+			return timestamp.Format("2006-01-02T15:04:05")
+		}
+	}
+
+	if arg.Type == "checkbox" {
+		for _, choice := range arg.Choices {
+			if value == choice.Title {
+				log.WithFields(log.Fields{
+					"arg":         arg.Name,
+					"oldValue":    value,
+					"newValue":    choice.Value,
+					"actionTitle": actionTitle,
+				}).Infof("Mangled checkbox value")
+
+				return choice.Value
+			}
+		}
+	}
+
+	return value
+}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio