[GH-16199] add IsValid to model OpenDialogRequest (#26526)

* [GH-16199] add IsValid to model OpenDialogRequest

* [GH-16199] add IsValid to model OpenDialogRequest, revert remove of translations

* [GH-16199] fix tests after revert

* [GH-16199] add IsValid to model OpenDialogRequest

* [GH-16199] revert validation of icon url

* [GH-16199] update go sum

* [GH-16199] log warning for invalid dialog

* [GH-16199] log error for invalid dialog

* [GH-16199] log warning for invalid dialog

* [GH-16199] fix golang-ci

---------

Co-authored-by: Lukas Eipert <git@leipert.io>
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Anna Os
2024-04-30 18:27:07 +02:00
коммит произвёл GitHub
родитель b70e657271
Коммит 48b047aa0c
10 изменённых файлов: 408 добавлений и 8 удалений

Просмотреть файл

@@ -20,11 +20,22 @@ import (
"strconv"
"strings"
"time"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
)
const (
PostActionTypeButton = "button"
PostActionTypeSelect = "select"
PostActionTypeButton = "button"
PostActionTypeSelect = "select"
DialogTitleMaxLength = 24
DialogElementDisplayNameMaxLength = 24
DialogElementNameMaxLength = 300
DialogElementHelpTextMaxLength = 150
DialogElementTextMaxLength = 150
DialogElementTextareaMaxLength = 3000
DialogElementSelectMaxLength = 3000
DialogElementBoolMaxLength = 150
)
var PostActionRetainPropKeys = []string{"from_webhook", "override_username", "override_icon_url"}
@@ -335,6 +346,148 @@ func (r *OpenDialogRequest) DecodeAndVerifyTriggerId(s *ecdsa.PrivateKey, timeou
return DecodeAndVerifyTriggerId(r.TriggerId, s, timeout)
}
func (r *OpenDialogRequest) IsValid() error {
var multiErr *multierror.Error
if r.URL == "" {
multiErr = multierror.Append(multiErr, errors.New("empty URL"))
}
if r.TriggerId == "" {
multiErr = multierror.Append(multiErr, errors.New("empty trigger id"))
}
err := r.Dialog.IsValid()
if err != nil {
multiErr = multierror.Append(multiErr, err)
}
return multiErr.ErrorOrNil()
}
func (d *Dialog) IsValid() error {
var multiErr *multierror.Error
if d.Title == "" || len(d.Title) > DialogTitleMaxLength {
multiErr = multierror.Append(multiErr, errors.Errorf("invalid dialog title %q", d.Title))
}
if d.IconURL != "" && !IsValidHTTPURL(d.IconURL) {
multiErr = multierror.Append(multiErr, errors.New("invalid icon url"))
}
if len(d.Elements) != 0 {
elementMap := make(map[string]bool)
for _, element := range d.Elements {
if elementMap[element.Name] {
multiErr = multierror.Append(multiErr, errors.Errorf("duplicate dialog element %q", element.Name))
}
elementMap[element.Name] = true
err := element.IsValid()
if err != nil {
multiErr = multierror.Append(multiErr, errors.Wrapf(err, "%q field is not valid", element.Name))
}
}
}
return multiErr.ErrorOrNil()
}
func (e *DialogElement) IsValid() error {
var multiErr *multierror.Error
textSubTypes := map[string]bool{
"": true,
"text": true,
"email": true,
"number": true,
"tel": true,
"url": true,
"password": true,
}
if e.MinLength < 0 {
multiErr = multierror.Append(multiErr, errors.Errorf("min length cannot be a negative number, got %d", e.MinLength))
}
if e.MinLength > e.MaxLength {
multiErr = multierror.Append(multiErr, errors.Errorf("min length should be less then max length, got %d > %d", e.MinLength, e.MaxLength))
}
multiErr = multierror.Append(multiErr, checkMaxLength("DisplayName", e.DisplayName, DialogElementDisplayNameMaxLength))
multiErr = multierror.Append(multiErr, checkMaxLength("Name", e.Name, DialogElementNameMaxLength))
multiErr = multierror.Append(multiErr, checkMaxLength("HelpText", e.HelpText, DialogElementHelpTextMaxLength))
switch e.Type {
case "text":
multiErr = multierror.Append(multiErr, checkMaxLength("Default", e.Default, DialogElementTextMaxLength))
multiErr = multierror.Append(multiErr, checkMaxLength("Placeholder", e.Placeholder, DialogElementTextMaxLength))
if _, ok := textSubTypes[e.SubType]; !ok {
multiErr = multierror.Append(multiErr, errors.Errorf("invalid subtype %q", e.Type))
}
case "textarea":
multiErr = multierror.Append(multiErr, checkMaxLength("Default", e.Default, DialogElementTextareaMaxLength))
multiErr = multierror.Append(multiErr, checkMaxLength("Placeholder", e.Placeholder, DialogElementTextareaMaxLength))
if _, ok := textSubTypes[e.SubType]; !ok {
multiErr = multierror.Append(multiErr, errors.Errorf("invalid subtype %q", e.Type))
}
case "select":
multiErr = multierror.Append(multiErr, checkMaxLength("Default", e.Default, DialogElementSelectMaxLength))
multiErr = multierror.Append(multiErr, checkMaxLength("Placeholder", e.Placeholder, DialogElementSelectMaxLength))
if e.DataSource != "" && e.DataSource != "users" && e.DataSource != "channels" {
multiErr = multierror.Append(multiErr, errors.Errorf("invalid data source %q, allowed are 'users' or 'channels'", e.DataSource))
}
if e.DataSource == "" && !isDefaultInOptions(e.Default, e.Options) {
multiErr = multierror.Append(multiErr, errors.Errorf("default value %q doesn't exist in options ", e.Default))
}
case "bool":
if e.Default != "" && e.Default != "true" && e.Default != "false" {
multiErr = multierror.Append(multiErr, errors.New("invalid default of bool"))
}
multiErr = multierror.Append(multiErr, checkMaxLength("Placeholder", e.Placeholder, DialogElementBoolMaxLength))
case "radio":
if !isDefaultInOptions(e.Default, e.Options) {
multiErr = multierror.Append(multiErr, errors.Errorf("default value %q doesn't exist in options ", e.Default))
}
default:
multiErr = multierror.Append(multiErr, errors.Errorf("invalid element type: %q", e.Type))
}
return multiErr.ErrorOrNil()
}
func isDefaultInOptions(defaultValue string, options []*PostActionOptions) bool {
if defaultValue == "" {
return true
}
for _, option := range options {
if defaultValue == option.Value {
return true
}
}
return false
}
func checkMaxLength(fieldName string, field string, length int) error {
var valid bool
// DisplayName and Name are required fields
if fieldName == "DisplayName" || fieldName == "Name" {
valid = len(field) > 0 && len(field) > length
} else {
valid = len(field) > length
}
if valid {
return errors.Errorf("%v cannot be longer than %d characters", fieldName, length)
}
return nil
}
func (o *Post) StripActionIntegrations() {
attachments := o.Attachments()
if o.GetProp("attachments") != nil {

Просмотреть файл

@@ -8,6 +8,7 @@ import (
"crypto/elliptic"
"crypto/rand"
"encoding/base64"
"strings"
"testing"
"time"
@@ -180,3 +181,190 @@ func TestPostActionIntegrationEquals(t *testing.T) {
require.True(t, pa1.Equals(pa2))
})
}
func TestOpenDialogRequestIsValid(t *testing.T) {
getBaseOpenDialogRequest := func() OpenDialogRequest {
return OpenDialogRequest{
TriggerId: "triggerId",
URL: "http://localhost:8065",
Dialog: Dialog{
CallbackId: "callbackid",
Title: "Some Title",
Elements: []DialogElement{
{
DisplayName: "Element Name",
Name: "element_name",
Type: "text",
Placeholder: "Enter a value",
},
},
SubmitLabel: "Submit",
NotifyOnCancel: false,
State: "somestate",
},
}
}
t.Run("should pass validation", func(t *testing.T) {
request := getBaseOpenDialogRequest()
err := request.IsValid()
assert.NoError(t, err)
})
t.Run("should fail on empty url", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.URL = ""
err := request.IsValid()
assert.ErrorContains(t, err, "empty URL")
})
t.Run("should fail on empty trigger", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.TriggerId = ""
err := request.IsValid()
assert.ErrorContains(t, err, "empty trigger id")
})
t.Run("should fail on empty dialog title", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.Title = ""
err := request.IsValid()
assert.ErrorContains(t, err, "invalid dialog title")
})
t.Run("should fail on wrong subtype and long dialog title", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.Elements[0].SubType = "wrong SubType"
request.Dialog.Title = "Very very long Dialog Name"
err := request.IsValid()
assert.ErrorContains(t, err, "invalid subtype")
assert.ErrorContains(t, err, "invalid dialog title")
t.Cleanup(func() {
request.Dialog.Elements[0].SubType = ""
request.Dialog.Title = "Some Title"
})
})
t.Run("should fail on wrong dialog icon url", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.IconURL = "wrong url"
err := request.IsValid()
assert.ErrorContains(t, err, "invalid icon url")
})
t.Run("should pass on empty dialog icon url", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.IconURL = ""
err := request.IsValid()
assert.NoError(t, err)
})
t.Run("should fail on wrong minimal and maximal element length", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.Elements[0].MinLength = 10
request.Dialog.Elements[0].MaxLength = 9
err := request.IsValid()
assert.ErrorContains(t, err, "field is not valid")
assert.ErrorContains(t, err, "min length should be less then max length")
})
t.Run("should fail on wrong element type", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.Elements[0].Type = "wrong type"
err := request.IsValid()
assert.ErrorContains(t, err, "invalid element type")
})
t.Run("should fail on duplicate element name", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
DisplayName: "Radio element name",
Name: "element_name",
Type: "radio",
})
err := request.IsValid()
assert.ErrorContains(t, err, "duplicate dialog element")
})
t.Run("should fail on wrong bool default value", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
DisplayName: "Bool element name",
Name: "bool_element_name",
Type: "bool",
Default: "wrong default",
})
err := request.IsValid()
assert.ErrorContains(t, err, "invalid default of bool")
})
t.Run("should pass on bool default value", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
DisplayName: "Bool element name",
Name: "bool_element_name",
Type: "bool",
Default: "true",
})
err := request.IsValid()
assert.NoError(t, err)
})
t.Run("should fail on wrong select datasource value", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
DisplayName: "Select element name",
Name: "select_element_name",
Type: "select",
DataSource: "wrong DataSource",
})
err := request.IsValid()
assert.ErrorContains(t, err, "invalid data source")
})
t.Run("should fail on wrong radio default value", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
DisplayName: "Radio element name",
Name: "radio_element_name",
Type: "radio",
Default: "default",
Options: []*PostActionOptions{
{
Text: "Text 1",
Value: "value 1",
},
},
})
err := request.IsValid()
assert.ErrorContains(t, err, "default value \"default\" doesn't exist in options")
})
t.Run("should pass radio default value", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
DisplayName: "Radio element name",
Name: "radio_element_name",
Type: "radio",
Default: "default",
Options: []*PostActionOptions{
{
Text: "Text 1",
Value: "value 1",
},
{
Text: "Text 2",
Value: "default",
},
},
})
err := request.IsValid()
assert.NoError(t, err)
})
t.Run("should fail on too long text placeholder", func(t *testing.T) {
request := getBaseOpenDialogRequest()
request.Dialog.Elements[0].Placeholder = strings.Repeat("x", 151)
err := request.IsValid()
assert.ErrorContains(t, err, "Placeholder cannot be longer than 150 characters")
})
}