fix: Argument validation targets the bindings (fixes checkboxes, etc) (#800)
This commit is contained in:
commit
365e52ae1f
|
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
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
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
string value = 1;
|
||||
string type = 2;
|
||||
string binding_id = 3;
|
||||
string argument_name = 4;
|
||||
}
|
||||
|
||||
message ValidateArgumentTypeResponse {
|
||||
|
|
|
|||
|
|
@ -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" +
|
||||
|
|
|
|||
|
|
@ -587,11 +587,13 @@ 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)
|
||||
err := api.validateArgumentTypeInternal(req.Msg)
|
||||
desc := ""
|
||||
|
||||
if err != nil {
|
||||
desc = err.Error()
|
||||
}
|
||||
|
|
@ -602,6 +604,38 @@ func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Reque
|
|||
}), nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) validateArgumentTypeInternal(msg *apiv1.ValidateArgumentTypeRequest) error {
|
||||
if msg.BindingId == "" || msg.ArgumentName == "" {
|
||||
return executor.TypeSafetyCheck("", msg.Value, msg.Type)
|
||||
}
|
||||
|
||||
arg, action := api.findArgumentForValidation(msg.BindingId, msg.ArgumentName)
|
||||
if arg == nil {
|
||||
return fmt.Errorf("argument not found")
|
||||
}
|
||||
|
||||
return executor.ValidateArgument(arg, msg.Value, action)
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) findArgumentForValidation(bindingId string, argumentName string) (*config.ActionArgument, *config.Action) {
|
||||
binding := api.executor.FindBindingByID(bindingId)
|
||||
if binding == nil || binding.Action == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
arg := api.findArgumentByName(binding.Action, argumentName)
|
||||
return arg, binding.Action
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) findArgumentByName(action *config.Action, name string) *config.ActionArgument {
|
||||
for i := range action.Arguments {
|
||||
if action.Arguments[i].Name == name {
|
||||
return &action.Arguments[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAmIRequest]) (*connect.Response[apiv1.WhoAmIResponse], error) {
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
|
|
|
|||
|
|
@ -175,6 +175,25 @@ 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 {
|
||||
if arg == nil {
|
||||
return fmt.Errorf("ValidateArgument: arg is nil")
|
||||
}
|
||||
|
||||
if action == nil {
|
||||
return fmt.Errorf("ValidateArgument: action is nil")
|
||||
}
|
||||
|
||||
// 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 +217,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 +390,69 @@ 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 == nil {
|
||||
log.Debugf("MangleArgumentValue called with nil arg, returning value unchanged")
|
||||
return value
|
||||
}
|
||||
|
||||
if arg.Type == "datetime" {
|
||||
return mangleDatetimeValue(arg, value, actionTitle)
|
||||
}
|
||||
|
||||
if arg.Type == "checkbox" {
|
||||
return mangleCheckboxValue(arg, value, actionTitle)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func mangleDatetimeValue(arg *config.ActionArgument, value string, actionTitle string) string {
|
||||
if arg == nil {
|
||||
log.Debugf("mangleDatetimeValue called with nil arg, returning value unchanged")
|
||||
return value
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
return value
|
||||
}
|
||||
|
||||
timestamp, err := time.Parse("2006-01-02T15:04", value)
|
||||
if err != nil {
|
||||
return value
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
func mangleCheckboxValue(arg *config.ActionArgument, value string, actionTitle string) string {
|
||||
if arg == nil {
|
||||
log.Debugf("mangleCheckboxValue called with nil arg, returning value unchanged")
|
||||
return value
|
||||
}
|
||||
|
||||
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