Files
upstream-mattermost/server/public/model/property_field_attrs_validation_test.go
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

398 lines
16 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidatePropertyFieldVisibility(t *testing.T) {
tests := []struct {
name string
attrs StringInterface
wantErr bool
}{
{name: "nil attrs", attrs: nil},
{name: "no visibility key", attrs: StringInterface{"other": "val"}},
{name: "empty string", attrs: StringInterface{PropertyFieldAttrVisibility: ""}},
{name: "hidden", attrs: StringInterface{PropertyFieldAttrVisibility: "hidden"}},
{name: "when_set", attrs: StringInterface{PropertyFieldAttrVisibility: "when_set"}},
{name: "always", attrs: StringInterface{PropertyFieldAttrVisibility: "always"}},
{name: "invalid", attrs: StringInterface{PropertyFieldAttrVisibility: "public"}, wantErr: true},
{name: "non-string type", attrs: StringInterface{PropertyFieldAttrVisibility: 42}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
field := &PropertyField{Attrs: tt.attrs}
err := ValidatePropertyFieldVisibility(field)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidatePropertyFieldSortOrder(t *testing.T) {
tests := []struct {
name string
attrs StringInterface
wantErr bool
}{
{name: "nil attrs", attrs: nil},
{name: "no sort_order key", attrs: StringInterface{"other": "val"}},
{name: "float64", attrs: StringInterface{PropertyFieldAttrSortOrder: float64(1.5)}},
{name: "int", attrs: StringInterface{PropertyFieldAttrSortOrder: 1}},
{name: "int64", attrs: StringInterface{PropertyFieldAttrSortOrder: int64(42)}},
{name: "json.Number", attrs: StringInterface{PropertyFieldAttrSortOrder: json.Number("3.14")}},
{name: "string", attrs: StringInterface{PropertyFieldAttrSortOrder: "not_a_number"}, wantErr: true},
{name: "bool", attrs: StringInterface{PropertyFieldAttrSortOrder: true}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
field := &PropertyField{Attrs: tt.attrs}
err := ValidatePropertyFieldSortOrder(field)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestSanitizeAndValidatePropertyFieldBoolAttr(t *testing.T) {
tests := []struct {
name string
key string
attrs StringInterface
want bool
wantKey bool
wantError string
}{
{name: "nil attrs", key: PropertyFieldAttrRequired, attrs: nil},
{name: "no key", key: PropertyFieldAttrRequired, attrs: StringInterface{"other": "value"}},
{name: "explicit nil clears the key", key: PropertyFieldAttrRequired, attrs: StringInterface{PropertyFieldAttrRequired: nil}},
{name: "required true", key: PropertyFieldAttrRequired, attrs: StringInterface{PropertyFieldAttrRequired: true}, want: true, wantKey: true},
{name: "required false is kept", key: PropertyFieldAttrRequired, attrs: StringInterface{PropertyFieldAttrRequired: false}, want: false, wantKey: true},
{name: "editable false is kept", key: PropertyFieldAttrEditable, attrs: StringInterface{PropertyFieldAttrEditable: false}, want: false, wantKey: true},
{name: "string is not coerced", key: PropertyFieldAttrRequired, attrs: StringInterface{PropertyFieldAttrRequired: "true"}, wantError: "required must be a boolean"},
{name: "numeric is rejected", key: PropertyFieldAttrEditable, attrs: StringInterface{PropertyFieldAttrEditable: 1}, wantError: "editable must be a boolean"},
{name: "array is rejected", key: PropertyFieldAttrRequired, attrs: StringInterface{PropertyFieldAttrRequired: []any{true}}, wantError: "required must be a boolean"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
field := &PropertyField{Attrs: tt.attrs}
err := SanitizeAndValidatePropertyFieldBoolAttr(field, tt.key)
if tt.wantError != "" {
require.ErrorContains(t, err, tt.wantError)
return
}
require.NoError(t, err)
if field.Attrs == nil {
return
}
actual, ok := field.Attrs[tt.key]
require.Equal(t, tt.wantKey, ok)
if tt.wantKey {
require.Equal(t, tt.want, actual)
}
})
}
}
// The two keys are independent: validating one must not disturb the other, and
// neither may touch a key the hook doesn't own.
func TestSanitizeAndValidatePropertyFieldBoolAttrLeavesOtherKeys(t *testing.T) {
field := &PropertyField{Attrs: StringInterface{
PropertyFieldAttrRequired: true,
PropertyFieldAttrEditable: nil,
PropertyFieldAttrSortOrder: 3,
PropertyFieldAttrVisibility: PropertyFieldVisibilityAlways,
}}
require.NoError(t, SanitizeAndValidatePropertyFieldBoolAttr(field, PropertyFieldAttrRequired))
require.NoError(t, SanitizeAndValidatePropertyFieldBoolAttr(field, PropertyFieldAttrEditable))
require.Equal(t, true, field.Attrs[PropertyFieldAttrRequired])
require.NotContains(t, field.Attrs, PropertyFieldAttrEditable)
require.Equal(t, 3, field.Attrs[PropertyFieldAttrSortOrder])
require.Equal(t, PropertyFieldVisibilityAlways, field.Attrs[PropertyFieldAttrVisibility])
}
func TestIsPropertyFieldRequired(t *testing.T) {
tests := []struct {
name string
field *PropertyField
want bool
}{
{name: "nil field", field: nil, want: false},
{name: "nil attrs", field: &PropertyField{}, want: false},
{name: "absent key", field: &PropertyField{Attrs: StringInterface{}}, want: false},
{name: "true", field: &PropertyField{Attrs: StringInterface{PropertyFieldAttrRequired: true}}, want: true},
{name: "false", field: &PropertyField{Attrs: StringInterface{PropertyFieldAttrRequired: false}}, want: false},
// A typo must not silently block channel creation.
{name: "stringly true", field: &PropertyField{Attrs: StringInterface{PropertyFieldAttrRequired: "true"}}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, IsPropertyFieldRequired(tt.field))
})
}
}
func TestIsEmptyPropertyValue(t *testing.T) {
empty := []string{"", " ", "null", `""`, "[]", " [] "}
for _, raw := range empty {
require.True(t, IsEmptyPropertyValue(json.RawMessage(raw)), raw)
}
set := []string{`"x"`, `["a"]`, "0", "false", `{"a":1}`}
for _, raw := range set {
require.False(t, IsEmptyPropertyValue(json.RawMessage(raw)), raw)
}
}
func TestSanitizeAndValidatePropertyFieldChangePolicy(t *testing.T) {
tests := []struct {
name string
fieldType PropertyFieldType
attrs StringInterface
want string
wantKey bool
wantError string
}{
{name: "nil attrs", fieldType: PropertyFieldTypeSelect, attrs: nil},
{name: "no key", fieldType: PropertyFieldTypeSelect, attrs: StringInterface{"other": "value"}},
{name: "explicit nil clears the key", fieldType: PropertyFieldTypeSelect, attrs: StringInterface{PropertyFieldAttrChangePolicy: nil}},
{name: "empty clears the key", fieldType: PropertyFieldTypeSelect, attrs: StringInterface{PropertyFieldAttrChangePolicy: " "}},
{name: "any is the default and is not stored", fieldType: PropertyFieldTypeSelect, attrs: StringInterface{PropertyFieldAttrChangePolicy: PropertyFieldChangePolicyAny}},
{
name: "never is kept on any type",
fieldType: PropertyFieldTypeText,
attrs: StringInterface{PropertyFieldAttrChangePolicy: PropertyFieldChangePolicyNever},
want: PropertyFieldChangePolicyNever,
wantKey: true,
},
{
name: "raise_only is kept on a rank field",
fieldType: PropertyFieldTypeRank,
attrs: StringInterface{PropertyFieldAttrChangePolicy: " raise_only "},
want: PropertyFieldChangePolicyRaiseOnly,
wantKey: true,
},
{
name: "raise_only is stripped off a select field, which has no ranks to compare",
fieldType: PropertyFieldTypeSelect,
attrs: StringInterface{PropertyFieldAttrChangePolicy: PropertyFieldChangePolicyRaiseOnly},
},
{
name: "unknown policy is rejected",
fieldType: PropertyFieldTypeRank,
attrs: StringInterface{PropertyFieldAttrChangePolicy: "frozen"},
wantError: `invalid change_policy "frozen"`,
},
{
name: "non-string is rejected",
fieldType: PropertyFieldTypeRank,
attrs: StringInterface{PropertyFieldAttrChangePolicy: true},
wantError: "change_policy must be a string",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
field := &PropertyField{Type: tt.fieldType, Attrs: tt.attrs}
err := SanitizeAndValidatePropertyFieldChangePolicy(field)
if tt.wantError != "" {
require.ErrorContains(t, err, tt.wantError)
return
}
require.NoError(t, err)
if field.Attrs == nil {
return
}
actual, ok := field.Attrs[PropertyFieldAttrChangePolicy]
require.Equal(t, tt.wantKey, ok)
if tt.wantKey {
require.Equal(t, tt.want, actual)
}
})
}
}
// editable predates change_policy, so a field carrying only editable=false must
// still read as locked.
func TestGetPropertyFieldChangePolicy(t *testing.T) {
require.Equal(t, PropertyFieldChangePolicyAny, GetPropertyFieldChangePolicy(&PropertyField{}))
require.Equal(t, PropertyFieldChangePolicyAny, GetPropertyFieldChangePolicy(&PropertyField{Attrs: StringInterface{PropertyFieldAttrEditable: true}}))
require.Equal(t, PropertyFieldChangePolicyNever, GetPropertyFieldChangePolicy(&PropertyField{Attrs: StringInterface{PropertyFieldAttrEditable: false}}))
require.Equal(t, PropertyFieldChangePolicyRaiseOnly, GetPropertyFieldChangePolicy(&PropertyField{Attrs: StringInterface{
PropertyFieldAttrChangePolicy: PropertyFieldChangePolicyRaiseOnly,
}}))
// change_policy wins: it is the more expressive key, and the pair is only ever
// written together for "never".
require.Equal(t, PropertyFieldChangePolicyNever, GetPropertyFieldChangePolicy(&PropertyField{Attrs: StringInterface{
PropertyFieldAttrChangePolicy: PropertyFieldChangePolicyNever,
PropertyFieldAttrEditable: false,
}}))
}
func TestSanitizeAndValidatePropertyFieldActions(t *testing.T) {
tests := []struct {
name string
attrs StringInterface
want []any
wantKey bool
wantError string
}{
{name: "nil attrs", attrs: nil},
{name: "no actions key", attrs: StringInterface{"other": "value"}},
{name: "nil actions", attrs: StringInterface{PropertyFieldAttrActions: nil}},
{name: "empty typed actions", attrs: StringInterface{PropertyFieldAttrActions: []string{}}},
{name: "empty decoded actions", attrs: StringInterface{PropertyFieldAttrActions: []any{}}},
{
name: "classification banner actions",
attrs: StringInterface{PropertyFieldAttrActions: []any{PropertyFieldActionDisplayBannerTop, PropertyFieldActionDisplayBannerBottom}},
want: []any{PropertyFieldActionDisplayBannerTop, PropertyFieldActionDisplayBannerBottom},
wantKey: true,
},
{
name: "smart label actions are trimmed and canonicalized",
attrs: StringInterface{PropertyFieldAttrActions: []string{" " + PropertyFieldActionDisplayLabelHeader + " ", PropertyFieldActionDisplayLabelInfo}},
want: []any{PropertyFieldActionDisplayLabelHeader, PropertyFieldActionDisplayLabelInfo},
wantKey: true,
},
{name: "non-array", attrs: StringInterface{PropertyFieldAttrActions: "display_label_header"}, wantError: "actions must be an array"},
{name: "non-string element", attrs: StringInterface{PropertyFieldAttrActions: []any{42}}, wantError: "actions[0] must be a string"},
{name: "empty action", attrs: StringInterface{PropertyFieldAttrActions: []any{" "}}, wantError: "actions must not contain empty strings"},
{name: "unknown action", attrs: StringInterface{PropertyFieldAttrActions: []any{"unknown"}}, wantError: "unknown action"},
{
name: "duplicate action",
attrs: StringInterface{PropertyFieldAttrActions: []any{PropertyFieldActionDisplayLabelHeader, PropertyFieldActionDisplayLabelHeader}},
wantError: "duplicate action",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
field := &PropertyField{Attrs: tt.attrs}
err := SanitizeAndValidatePropertyFieldActions(field)
if tt.wantError != "" {
require.ErrorContains(t, err, tt.wantError)
return
}
require.NoError(t, err)
if field.Attrs == nil {
return
}
actual, ok := field.Attrs[PropertyFieldAttrActions]
require.Equal(t, tt.wantKey, ok)
if tt.wantKey && tt.want != nil {
require.Equal(t, tt.want, actual)
}
})
}
}
func TestValidatePropertyValueForValueType(t *testing.T) {
tests := []struct {
name string
valueType string
value string
wantErr bool
}{
{name: "empty value type", valueType: "", value: `"anything"`},
{name: "valid email", valueType: "email", value: `"test@example.com"`},
{name: "invalid email", valueType: "email", value: `"not-an-email"`, wantErr: true},
{name: "empty email string", valueType: "email", value: `""`},
{name: "valid url", valueType: "url", value: `"https://example.com"`},
{name: "valid url with path", valueType: "url", value: `"https://example.com/path?q=1"`},
{name: "invalid url - plain string", valueType: "url", value: `"not a url"`, wantErr: true},
{name: "invalid url - relative path", valueType: "url", value: `"/relative/path"`, wantErr: true},
{name: "invalid url - missing host", valueType: "url", value: `"http://"`, wantErr: true},
{name: "invalid url - missing scheme", valueType: "url", value: `"example.com"`, wantErr: true},
{name: "phone (any string)", valueType: "phone", value: `"+1-555-0123"`},
{name: "unknown value type", valueType: "fax", value: `"test"`, wantErr: true},
{name: "non-string json", valueType: "email", value: `42`, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidatePropertyValueForValueType(tt.valueType, json.RawMessage(tt.value))
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestIsPropertyFieldSynced(t *testing.T) {
assert.False(t, IsPropertyFieldSynced(&PropertyField{}))
assert.False(t, IsPropertyFieldSynced(&PropertyField{Attrs: StringInterface{}}))
assert.True(t, IsPropertyFieldSynced(&PropertyField{Attrs: StringInterface{PropertyFieldAttrLDAP: "attr"}}))
assert.True(t, IsPropertyFieldSynced(&PropertyField{Attrs: StringInterface{PropertyFieldAttrSAML: "attr"}}))
assert.True(t, IsPropertyFieldSynced(&PropertyField{Attrs: StringInterface{PropertyFieldAttrLDAP: "a", PropertyFieldAttrSAML: "b"}}))
}
func TestGetPropertyFieldSyncSource(t *testing.T) {
assert.Equal(t, "", GetPropertyFieldSyncSource(&PropertyField{}))
assert.Equal(t, "ldap", GetPropertyFieldSyncSource(&PropertyField{Attrs: StringInterface{PropertyFieldAttrLDAP: "attr"}}))
assert.Equal(t, "saml", GetPropertyFieldSyncSource(&PropertyField{Attrs: StringInterface{PropertyFieldAttrSAML: "attr"}}))
// ldap takes priority
assert.Equal(t, "ldap", GetPropertyFieldSyncSource(&PropertyField{Attrs: StringInterface{PropertyFieldAttrLDAP: "a", PropertyFieldAttrSAML: "b"}}))
}
func TestIsValidPropertyFieldVisibility(t *testing.T) {
assert.True(t, IsValidPropertyFieldVisibility("hidden"))
assert.True(t, IsValidPropertyFieldVisibility("when_set"))
assert.True(t, IsValidPropertyFieldVisibility("always"))
assert.False(t, IsValidPropertyFieldVisibility(""))
assert.False(t, IsValidPropertyFieldVisibility("public"))
}
func TestIsValidPropertyFieldValueType(t *testing.T) {
assert.True(t, IsValidPropertyFieldValueType("email"))
assert.True(t, IsValidPropertyFieldValueType("url"))
assert.True(t, IsValidPropertyFieldValueType("phone"))
assert.False(t, IsValidPropertyFieldValueType(""))
assert.False(t, IsValidPropertyFieldValueType("fax"))
}
func TestGetPropertyFieldValueType(t *testing.T) {
assert.Equal(t, "", GetPropertyFieldValueType(&PropertyField{}))
assert.Equal(t, "", GetPropertyFieldValueType(&PropertyField{Attrs: StringInterface{}}))
assert.Equal(t, "email", GetPropertyFieldValueType(&PropertyField{Attrs: StringInterface{PropertyFieldAttrValueType: "email"}}))
assert.Equal(t, "email", GetPropertyFieldValueType(&PropertyField{Attrs: StringInterface{PropertyFieldAttrValueType: " email "}}))
}
func TestCallerIDConstants(t *testing.T) {
require.NotEmpty(t, CallerIDLDAPSync)
require.NotEmpty(t, CallerIDSAMLSync)
require.NotEqual(t, CallerIDLDAPSync, CallerIDSAMLSync)
// The sync caller IDs must not be valid plugin IDs, otherwise an
// admin-installed plugin could set its manifest ID to one of these
// values and bypass the sync-lock check for LDAP/SAML-managed fields.
require.False(t, IsValidPluginId(CallerIDLDAPSync),
"CallerIDLDAPSync must not be a valid plugin ID")
require.False(t, IsValidPluginId(CallerIDSAMLSync),
"CallerIDSAMLSync must not be a valid plugin ID")
}