[MM-62427] Add message attachments validation (#30180)

* Add message attachments validation

* Add props validation

* Validate slack attachment fields

* Update tests and library usage

* Improve interactive dialog error for length checks

* Allow predefined colors for slack attachments

* Fix TestPostAction

* Use const for data source

* Add tests

* Cleanup unused props

* Add happy path tests

* lint fixes

* Add validation for PostActionOptions
Этот коммит содержится в:
Ben Schumacher
2025-03-20 12:53:50 +01:00
коммит произвёл GitHub
родитель 5609489e86
Коммит 9b5d8d52bf
47 изменённых файлов: 1402 добавлений и 355 удалений

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

@@ -6,9 +6,15 @@ package model
import (
"fmt"
"regexp"
"slices"
"github.com/hashicorp/go-multierror"
)
var linkWithTextRegex = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
var (
linkWithTextRegex = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
hexColorRegex = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
)
type SlackAttachment struct {
Id int64 `json:"id"`
@@ -30,6 +36,78 @@ type SlackAttachment struct {
Actions []*PostAction `json:"actions,omitempty"`
}
func (s *SlackAttachment) IsValid() error {
var multiErr *multierror.Error
if s.Color != "" {
validStyles := []string{"good", "warning", "danger"}
// If not a predefined style, check if it's a hex color
if !slices.Contains(validStyles, s.Color) && !hexColorRegex.MatchString(s.Color) {
multiErr = multierror.Append(multiErr, fmt.Errorf("invalid style '%s' - must be one of [good, warning, danger] or a hex color", s.Color))
}
}
if s.AuthorLink != "" {
if s.AuthorName == "" {
multiErr = multierror.Append(multiErr, fmt.Errorf("author link cannot be set without author name"))
}
if !IsValidHTTPURL(s.AuthorLink) {
multiErr = multierror.Append(multiErr, fmt.Errorf("invalid author link URL"))
}
}
if s.AuthorIcon != "" && !IsValidHTTPURL(s.AuthorIcon) {
multiErr = multierror.Append(multiErr, fmt.Errorf("invalid author icon URL"))
}
if s.TitleLink != "" {
if s.Title == "" {
multiErr = multierror.Append(multiErr, fmt.Errorf("title link cannot be set without title"))
}
if !IsValidHTTPURL(s.TitleLink) {
multiErr = multierror.Append(multiErr, fmt.Errorf("invalid title link URL"))
}
}
for _, field := range s.Fields {
if err := field.IsValid(); err != nil {
multiErr = multierror.Append(multiErr, err)
}
}
if s.ImageURL != "" && !IsValidHTTPURL(s.ImageURL) {
multiErr = multierror.Append(multiErr, fmt.Errorf("invalid image URL"))
}
if s.ThumbURL != "" && !IsValidHTTPURL(s.ThumbURL) {
multiErr = multierror.Append(multiErr, fmt.Errorf("invalid thumb URL"))
}
if s.FooterIcon != "" && !IsValidHTTPURL(s.FooterIcon) {
multiErr = multierror.Append(multiErr, fmt.Errorf("invalid footer icon URL"))
}
// Validate timestamp is either string or int64
if s.Timestamp != nil {
switch s.Timestamp.(type) {
case string, int64:
// Valid types
default:
multiErr = multierror.Append(multiErr, fmt.Errorf("timestamp must be either a string or int64"))
}
}
for i, action := range s.Actions {
if err := action.IsValid(); err != nil {
multiErr = multierror.Append(multiErr, multierror.Prefix(err, fmt.Sprintf("action at index %d is invalid:", i)))
}
}
return multiErr.ErrorOrNil()
}
func (s *SlackAttachment) Equals(input *SlackAttachment) bool {
// Direct comparison of simple types
@@ -120,6 +198,21 @@ type SlackAttachmentField struct {
Short SlackCompatibleBool `json:"short"`
}
func (s *SlackAttachmentField) IsValid() error {
var multiErr *multierror.Error
if s.Value != nil {
switch s.Value.(type) {
case string, int:
// Valid types
default:
multiErr = multierror.Append(multiErr, fmt.Errorf("value must be either a string or int"))
}
}
return multiErr.ErrorOrNil()
}
func (s *SlackAttachmentField) Equals(input *SlackAttachmentField) bool {
if s.Title != input.Title {
return false
@@ -188,7 +281,7 @@ func ParseSlackAttachment(post *Post, attachments []*SlackAttachment) {
}
postAttachments = append(postAttachments, attachment)
}
post.AddProp("attachments", postAttachments)
post.AddProp(PostPropsAttachments, postAttachments)
}
func ParseSlackLinksToMarkdown(text string) string {