mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-30 17:06:34 +08:00
[MM-70188] Convert the os_platform session attribute to a select field (#37901)
This commit is contained in:
@@ -938,6 +938,51 @@ func mergeBoardsStatusColors(attrs model.StringInterface, colorByName map[string
|
||||
return out
|
||||
}
|
||||
|
||||
// syncSessionAttributeOptions replaces the persisted select options on an
|
||||
// already-seeded session attribute field with the ones the schema declares.
|
||||
// Each existing option's ID is reused by name so IDs stay stable across
|
||||
// restarts, while a field the schema has converted from text to select picks
|
||||
// up its options for the first time. Options are dropped entirely for fields
|
||||
// the schema declares without them, so a select → text conversion doesn't
|
||||
// leave the UI rendering a stale dropdown.
|
||||
func syncSessionAttributeOptions(current, expected *model.PropertyField) error {
|
||||
expectedOptions, ok := expected.Attrs[model.PropertyFieldAttributeOptions]
|
||||
if !ok {
|
||||
delete(current.Attrs, model.PropertyFieldAttributeOptions)
|
||||
return nil
|
||||
}
|
||||
|
||||
idByName := make(map[string]string)
|
||||
if raw, found := current.Attrs[model.PropertyFieldAttributeOptions]; found {
|
||||
currentOptions, err := model.NewPropertyOptionsFromFieldAttrs[*model.PluginPropertyOption](raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read persisted options: %w", err)
|
||||
}
|
||||
for _, option := range currentOptions {
|
||||
idByName[option.GetName()] = option.GetID()
|
||||
}
|
||||
}
|
||||
|
||||
options, err := model.NewPropertyOptionsFromFieldAttrs[*model.PluginPropertyOption](expectedOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read schema options: %w", err)
|
||||
}
|
||||
|
||||
// Written back as []any of map[string]any, the canonical attrs shape every
|
||||
// downstream reader of attrs["options"] expects.
|
||||
merged := make([]any, len(options))
|
||||
for i, option := range options {
|
||||
id := option.GetID()
|
||||
if existingID, found := idByName[option.GetName()]; found {
|
||||
id = existingID
|
||||
}
|
||||
merged[i] = map[string]any{"id": id, "name": option.GetName()}
|
||||
}
|
||||
current.Attrs[model.PropertyFieldAttributeOptions] = merged
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedSessionAttributeFields idempotently seeds the built-in session attribute property fields.
|
||||
func (s *Server) seedSessionAttributeFields(groupID string) error {
|
||||
existing, err := s.propertyService.SearchPropertyFields(nil, groupID, model.PropertyFieldSearchOpts{PerPage: 100})
|
||||
@@ -958,6 +1003,9 @@ func (s *Server) seedSessionAttributeFields(groupID string) error {
|
||||
current.Type = expected.Type
|
||||
current.Attrs["platforms"] = expected.Attrs["platforms"]
|
||||
current.Attrs[model.SAAttrDisplayName] = expected.Attrs[model.SAAttrDisplayName]
|
||||
if err := syncSessionAttributeOptions(current, expected); err != nil {
|
||||
return fmt.Errorf("failed to sync options for session attribute field %q: %w", expected.Name, err)
|
||||
}
|
||||
current.ObjectType = expected.ObjectType
|
||||
current.TargetType = expected.TargetType
|
||||
current.Protected = expected.Protected
|
||||
|
||||
@@ -362,6 +362,53 @@ func TestCPADisplayNameBackfill_BackfillsProtectedSourceOnlyField(t *testing.T)
|
||||
require.Equal(t, "true", data.Value)
|
||||
}
|
||||
|
||||
var expectedOSPlatformOptions = []string{"macos", "windows", "linux", "ios", "android"}
|
||||
|
||||
func sessionAttributeFieldByName(t *testing.T, th *TestHelper, groupID, name string) *model.PropertyField {
|
||||
t.Helper()
|
||||
|
||||
fields, appErr := th.App.SearchPropertyFields(th.Context, groupID, model.PropertyFieldSearchOpts{PerPage: 100})
|
||||
require.Nil(t, appErr)
|
||||
for _, field := range fields {
|
||||
if field.Name == name {
|
||||
return field
|
||||
}
|
||||
}
|
||||
|
||||
require.FailNowf(t, "session attribute field not found", "field %q was not seeded", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func sessionAttributeOptions(t *testing.T, field *model.PropertyField) model.PropertyOptions[*model.PluginPropertyOption] {
|
||||
t.Helper()
|
||||
|
||||
options, err := model.NewPropertyOptionsFromFieldAttrs[*model.PluginPropertyOption](field.Attrs[model.PropertyFieldAttributeOptions])
|
||||
require.NoError(t, err)
|
||||
return options
|
||||
}
|
||||
|
||||
func sessionAttributeOptionNames(t *testing.T, field *model.PropertyField) []string {
|
||||
t.Helper()
|
||||
|
||||
options := sessionAttributeOptions(t, field)
|
||||
names := make([]string, 0, len(options))
|
||||
for _, option := range options {
|
||||
names = append(names, option.GetName())
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func sessionAttributeOptionIDsByName(t *testing.T, field *model.PropertyField) map[string]string {
|
||||
t.Helper()
|
||||
|
||||
options := sessionAttributeOptions(t, field)
|
||||
idsByName := make(map[string]string, len(options))
|
||||
for _, option := range options {
|
||||
idsByName[option.GetName()] = option.GetID()
|
||||
}
|
||||
return idsByName
|
||||
}
|
||||
|
||||
func TestDoSetupSessionAttributesProperties(t *testing.T) {
|
||||
expectedFieldCount := len(model.SessionAttributeSystemFields("group-id"))
|
||||
|
||||
@@ -413,6 +460,58 @@ func TestDoSetupSessionAttributesProperties(t *testing.T) {
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("os_platform seeds as a select with the known platform values", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
|
||||
group, appErr := th.App.GetPropertyGroup(th.Context, model.SessionAttributesPropertyGroupName)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
field := sessionAttributeFieldByName(t, th, group.ID, model.SessionAttributesPropertyFieldOSPlatform)
|
||||
require.Equal(t, model.PropertyFieldTypeSelect, field.Type)
|
||||
require.Equal(t, expectedOSPlatformOptions, sessionAttributeOptionNames(t, field))
|
||||
require.True(t, model.IsValidSessionAttributeValue(field, "windows"))
|
||||
require.False(t, model.IsValidSessionAttributeValue(field, "darwin"))
|
||||
})
|
||||
|
||||
t.Run("converts a legacy text os_platform field into a select", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
|
||||
group, appErr := th.App.GetPropertyGroup(th.Context, model.SessionAttributesPropertyGroupName)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Restore the pre-conversion shape a server upgrade would find: free
|
||||
// text with no options. Written with a nil request context so
|
||||
// SessionAttributesHook treats it as a system caller, the same way the
|
||||
// seed itself does.
|
||||
field := sessionAttributeFieldByName(t, th, group.ID, model.SessionAttributesPropertyFieldOSPlatform)
|
||||
field.Type = model.PropertyFieldTypeText
|
||||
delete(field.Attrs, model.PropertyFieldAttributeOptions)
|
||||
_, _, _, err := th.Server.propertyService.UpdatePropertyFields(nil, group.ID, []*model.PropertyField{field})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, th.Server.doSetupSessionAttributesProperties())
|
||||
|
||||
converted := sessionAttributeFieldByName(t, th, group.ID, model.SessionAttributesPropertyFieldOSPlatform)
|
||||
require.Equal(t, model.PropertyFieldTypeSelect, converted.Type)
|
||||
require.Equal(t, expectedOSPlatformOptions, sessionAttributeOptionNames(t, converted))
|
||||
require.True(t, model.IsValidSessionAttributeValue(converted, "windows"))
|
||||
})
|
||||
|
||||
t.Run("re-seeding preserves select option IDs", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
|
||||
group, appErr := th.App.GetPropertyGroup(th.Context, model.SessionAttributesPropertyGroupName)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
before := sessionAttributeOptionIDsByName(t, sessionAttributeFieldByName(t, th, group.ID, model.SessionAttributesPropertyFieldOSPlatform))
|
||||
require.Len(t, before, len(expectedOSPlatformOptions))
|
||||
|
||||
require.NoError(t, th.Server.doSetupSessionAttributesProperties())
|
||||
|
||||
after := sessionAttributeOptionIDsByName(t, sessionAttributeFieldByName(t, th, group.ID, model.SessionAttributesPropertyFieldOSPlatform))
|
||||
require.Equal(t, before, after, "re-seeding must not regenerate option IDs")
|
||||
})
|
||||
|
||||
t.Run("re-running is idempotent", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
|
||||
|
||||
@@ -257,7 +257,15 @@ func SessionAttributeSystemFields(groupID string) []*PropertyField {
|
||||
|
||||
sessionAttributeField(groupID, SessionAttributesPropertyFieldMDMEnrolled, SessionAttributesDisplayNameMDMEnrolled, PropertyFieldTypeSelect, clientsOnly, SessionAttributeDefaultTTLPosture, SessionAttributeDefaultGracePosture, boolSelectOptions),
|
||||
sessionAttributeField(groupID, SessionAttributesPropertyFieldJailbreakDetected, SessionAttributesDisplayNameJailbreakDetected, PropertyFieldTypeSelect, mobileOnly, SessionAttributeDefaultTTLPosture, SessionAttributeDefaultGracePosture, boolSelectOptions),
|
||||
sessionAttributeField(groupID, SessionAttributesPropertyFieldOSPlatform, SessionAttributesDisplayNameOSPlatform, PropertyFieldTypeText, clientsOnly, SessionAttributeDefaultTTLPosture, SessionAttributeDefaultGracePosture, nil),
|
||||
sessionAttributeField(groupID, SessionAttributesPropertyFieldOSPlatform, SessionAttributesDisplayNameOSPlatform, PropertyFieldTypeSelect, clientsOnly, SessionAttributeDefaultTTLPosture, SessionAttributeDefaultGracePosture, StringInterface{
|
||||
PropertyFieldAttributeOptions: []map[string]string{
|
||||
{"name": "macos"},
|
||||
{"name": "windows"},
|
||||
{"name": "linux"},
|
||||
{"name": "ios"},
|
||||
{"name": "android"},
|
||||
},
|
||||
}),
|
||||
sessionAttributeField(groupID, SessionAttributesPropertyFieldOSVersion, SessionAttributesDisplayNameOSVersion, PropertyFieldTypeText, clientsOnly, SessionAttributeDefaultTTLPosture, SessionAttributeDefaultGracePosture, nil),
|
||||
sessionAttributeField(groupID, SessionAttributesPropertyFieldClientVersion, SessionAttributesDisplayNameClientVersion, PropertyFieldTypeText, clientsOnly, SessionAttributeDefaultTTLPosture, SessionAttributeDefaultGracePosture, nil),
|
||||
|
||||
|
||||
@@ -63,6 +63,32 @@ func TestSessionAttributeSystemFieldsDisplayNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionAttributeSystemFieldsOSPlatform(t *testing.T) {
|
||||
var field *PropertyField
|
||||
for _, f := range SessionAttributeSystemFields("group-id") {
|
||||
if f.Name == SessionAttributesPropertyFieldOSPlatform {
|
||||
field = f
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, field)
|
||||
require.Equal(t, PropertyFieldTypeSelect, field.Type)
|
||||
|
||||
options, err := NewPropertyOptionsFromFieldAttrs[*PluginPropertyOption](field.Attrs[PropertyFieldAttributeOptions])
|
||||
require.NoError(t, err)
|
||||
|
||||
names := make([]string, 0, len(options))
|
||||
for _, option := range options {
|
||||
names = append(names, option.GetName())
|
||||
}
|
||||
assert.Equal(t, []string{"macos", "windows", "linux", "ios", "android"}, names)
|
||||
|
||||
for _, name := range names {
|
||||
assert.True(t, IsValidSessionAttributeValue(field, name), "%q must be an accepted value", name)
|
||||
}
|
||||
assert.False(t, IsValidSessionAttributeValue(field, "darwin"))
|
||||
}
|
||||
|
||||
func TestSAFieldEnabledForPlatform(t *testing.T) {
|
||||
field := &PropertyField{
|
||||
Name: SessionAttributesPropertyFieldVPNActive,
|
||||
@@ -87,7 +113,7 @@ func TestSAFieldEnabledForPlatform(t *testing.T) {
|
||||
|
||||
func TestIsValidSessionAttributeValue(t *testing.T) {
|
||||
textField := &PropertyField{
|
||||
Name: SessionAttributesPropertyFieldOSPlatform,
|
||||
Name: SessionAttributesPropertyFieldOSVersion,
|
||||
Type: PropertyFieldTypeText,
|
||||
}
|
||||
selectField := &PropertyField{
|
||||
@@ -112,7 +138,7 @@ func TestIsValidSessionAttributeValue(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("text accepts string", func(t *testing.T) {
|
||||
assert.True(t, IsValidSessionAttributeValue(textField, "linux"))
|
||||
assert.True(t, IsValidSessionAttributeValue(textField, "15.1"))
|
||||
})
|
||||
|
||||
t.Run("text rejects non-string", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user