fix: Argument validation targets the bindings (fixes checkboxes, etc)
This commit is contained in:
parent
5d061eb1a0
commit
09e1f0f984
|
|
@ -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)
|
// @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
@ -686,6 +686,16 @@ export declare type ValidateArgumentTypeRequest = Message<"olivetin.api.v1.Valid
|
||||||
* @generated from field: string type = 2;
|
* @generated from field: string type = 2;
|
||||||
*/
|
*/
|
||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string binding_id = 3;
|
||||||
|
*/
|
||||||
|
bindingId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string argument_name = 4;
|
||||||
|
*/
|
||||||
|
argumentName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -116,8 +116,8 @@ async function setup() {
|
||||||
// Run initial validation on all fields after DOM is updated
|
// Run initial validation on all fields after DOM is updated
|
||||||
await nextTick()
|
await nextTick()
|
||||||
for (const arg of actionArguments.value) {
|
for (const arg of actionArguments.value) {
|
||||||
if (arg.type && !arg.type.startsWith('regex:') && arg.type !== 'select' && arg.type !== '' && arg.type !== 'confirmation') {
|
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])
|
await validateArgument(arg, argValues.value[arg.name] || '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -212,10 +212,22 @@ async function validateArgument(arg, value) {
|
||||||
return
|
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 {
|
try {
|
||||||
const validateArgumentTypeArgs = {
|
const validateArgumentTypeArgs = {
|
||||||
value: value,
|
value: value,
|
||||||
type: arg.type
|
type: arg.type,
|
||||||
|
bindingId: props.bindingId,
|
||||||
|
argumentName: arg.name
|
||||||
}
|
}
|
||||||
|
|
||||||
const validation = await window.client.validateArgumentType(validateArgumentTypeArgs)
|
const validation = await window.client.validateArgumentType(validateArgumentTypeArgs)
|
||||||
|
|
|
||||||
|
|
@ -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/')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -164,6 +164,8 @@ message GetActionLogsResponse {
|
||||||
message ValidateArgumentTypeRequest {
|
message ValidateArgumentTypeRequest {
|
||||||
string value = 1;
|
string value = 1;
|
||||||
string type = 2;
|
string type = 2;
|
||||||
|
string binding_id = 3;
|
||||||
|
string argument_name = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ValidateArgumentTypeResponse {
|
message ValidateArgumentTypeResponse {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.10
|
// protoc-gen-go v1.36.11
|
||||||
// protoc (unknown)
|
// protoc (unknown)
|
||||||
// source: olivetin/api/v1/olivetin.proto
|
// source: olivetin/api/v1/olivetin.proto
|
||||||
|
|
||||||
|
|
@ -1509,6 +1509,8 @@ type ValidateArgumentTypeRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
|
Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
|
||||||
Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
@ -1557,6 +1559,20 @@ func (x *ValidateArgumentTypeRequest) GetType() string {
|
||||||
return ""
|
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 {
|
type ValidateArgumentTypeResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"`
|
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" +
|
"\tpage_size\x18\x03 \x01(\x03R\bpageSize\x12\x1f\n" +
|
||||||
"\vtotal_count\x18\x04 \x01(\x03R\n" +
|
"\vtotal_count\x18\x04 \x01(\x03R\n" +
|
||||||
"totalCount\x12!\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" +
|
"\x1bValidateArgumentTypeRequest\x12\x14\n" +
|
||||||
"\x05value\x18\x01 \x01(\tR\x05value\x12\x12\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" +
|
"\x1cValidateArgumentTypeResponse\x12\x14\n" +
|
||||||
"\x05valid\x18\x01 \x01(\bR\x05valid\x12 \n" +
|
"\x05valid\x18\x01 \x01(\bR\x05valid\x12 \n" +
|
||||||
"\vdescription\x18\x02 \x01(\tR\vdescription\"K\n" +
|
"\vdescription\x18\x02 \x01(\tR\vdescription\"K\n" +
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
on the StartAction -> Executor chain. This is here basically to provide helpful
|
||||||
error messages more quickly before starting the action.
|
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) {
|
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 := ""
|
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 {
|
if err != nil {
|
||||||
desc = err.Error()
|
desc = err.Error()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,17 @@ func typecheckActionArgument(arg *config.ActionArgument, value string, action *c
|
||||||
return typecheckActionArgumentFound(value, action, arg)
|
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 {
|
func typecheckActionArgumentFound(value string, action *config.Action, arg *config.ActionArgument) error {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return typecheckNull(arg)
|
return typecheckNull(arg)
|
||||||
|
|
@ -198,6 +209,8 @@ func TypeSafetyCheck(name string, value string, argumentType string) error {
|
||||||
return nil
|
return nil
|
||||||
case "raw_string_multiline":
|
case "raw_string_multiline":
|
||||||
return nil
|
return nil
|
||||||
|
case "checkbox":
|
||||||
|
return nil
|
||||||
case "email":
|
case "email":
|
||||||
return typeSafetyCheckEmail(value)
|
return typeSafetyCheckEmail(value)
|
||||||
case "url":
|
case "url":
|
||||||
|
|
@ -369,3 +382,42 @@ func mangleInvalidDatetimeValues(req *ExecutionRequest, arg *config.ActionArgume
|
||||||
req.Arguments[arg.Name] = timestamp.Format("2006-01-02T15:04:05")
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue