Files
team/server/channels/app/property_field_test.go
zeekay 8feb24ea1f One name for this fork, and it is ours
The server module named github.com/hanzoai/team/server/v8 — a path that
resolves to the retired workspace's repository, not this one — while
server/public still published github.com/mattermost/mattermost/server/public.
Both are github.com/hanzoteam/server/... now, which is where this code
actually lives.

That second half is what made the binary unbootable. Upstream's public path
stayed claimed by upstream, mattermost-plugin-ai imported it, and the build
linked public/model twice; public/plugin's gob.Register then ran twice under
one name and panicked at init, before main. Reverting the rename traded the
brand for a working binary. Rebranding the plugin alongside it needs no such
trade: the agents fork is github.com/hanzoteam/agents/v2 against our public
module, so no module named mattermost/mattermost is in the graph at all.

Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and
it is ours.
2026-08-28 23:35:36 -07:00

1362 lines
50 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"testing"
"github.com/hanzoteam/server/server/public/model"
"github.com/hanzoteam/server/server/public/shared/request"
"github.com/hanzoteam/server/server/v8/einterfaces/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
// registerTestPropertyGroup creates a fresh, unmanaged PSAv2 property group
// for tests that exercise generic PropertyField CRUD.
func registerTestPropertyGroup(tb testing.TB, th *TestHelper) string {
tb.Helper()
group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{
Name: "test_" + model.NewId(),
Version: model.PropertyGroupVersionV2,
})
require.Nil(tb, appErr)
return group.ID
}
func TestCreatePropertyField(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
t.Run("should create a non-protected field without bypass", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Non-Protected Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
assert.NotEmpty(t, created.ID)
assert.Equal(t, "Non-Protected Field", created.Name)
assert.False(t, created.Protected)
})
t.Run("should reject creating protected field without bypass", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Protected Field No Bypass",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.NotNil(t, appErr)
assert.Nil(t, created)
assert.Equal(t, "app.property_field.create.protected.app_error", appErr.Id)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("should allow creating protected field with bypass", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Protected Field With Bypass",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, true, "")
require.Nil(t, appErr)
assert.NotEmpty(t, created.ID)
assert.Equal(t, "Protected Field With Bypass", created.Name)
assert.True(t, created.Protected)
})
t.Run("should reject creating an invalid protected field even with bypass", func(t *testing.T) {
// Protected field without permissions (validation requires permissions.field = "none")
field := &model.PropertyField{
GroupID: groupID,
Name: "Invalid Protected Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
// Missing Permissions - validation should fail
}
created, appErr := th.App.CreatePropertyField(th.Context, field, true, "")
require.NotNil(t, appErr)
assert.Nil(t, created)
})
t.Run("should reject creating protected field with wrong permission level even with bypass", func(t *testing.T) {
// Protected field with permissions.field != "none" (validation should fail)
field := &model.PropertyField{
GroupID: groupID,
Name: "Invalid Protected Field Permissions",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelSysadmin), // Should be "none" for protected fields
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, true, "")
require.NotNil(t, appErr)
assert.Nil(t, created)
})
}
func TestUpdatePropertyField(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
t.Run("should update a non-protected field without bypass", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Field to Update",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
created.Name = "Updated Field Name"
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, created, false, "")
require.Nil(t, appErr)
assert.Equal(t, "Updated Field Name", updated.Name)
})
t.Run("should reject updating protected field without bypass", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Protected Field to Update",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, true, "")
require.Nil(t, appErr)
created.Name = "Attempted Update"
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, created, false, "")
require.NotNil(t, appErr)
assert.Nil(t, updated)
assert.Equal(t, "app.property_field.update.protected.app_error", appErr.Id)
assert.Equal(t, http.StatusForbidden, appErr.StatusCode)
})
t.Run("should allow updating protected field with bypass", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Protected Field Bypass Update",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, true, "")
require.Nil(t, appErr)
created.Name = "Successfully Updated Protected"
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, created, true, "")
require.Nil(t, appErr)
assert.Equal(t, "Successfully Updated Protected", updated.Name)
})
t.Run("should reject an invalid update", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Field for Invalid Update",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
// Try to update with empty name (invalid)
created.Name = ""
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, created, false, "")
require.NotNil(t, appErr)
assert.Nil(t, updated)
})
}
func TestUpdatePropertyFields(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
t.Run("should update multiple non-protected fields without bypass", func(t *testing.T) {
field1 := &model.PropertyField{
GroupID: groupID,
Name: "Batch Field 1",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
field2 := &model.PropertyField{
GroupID: groupID,
Name: "Batch Field 2",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created1, appErr := th.App.CreatePropertyField(th.Context, field1, false, "")
require.Nil(t, appErr)
created2, appErr := th.App.CreatePropertyField(th.Context, field2, false, "")
require.Nil(t, appErr)
created1.Name = "Updated Batch 1"
created2.Name = "Updated Batch 2"
updated, _, appErr := th.App.UpdatePropertyFields(th.Context, groupID, []*model.PropertyField{created1, created2}, false, "")
require.Nil(t, appErr)
require.Len(t, updated, 2)
})
t.Run("should reject batch update if any field is protected without bypass", func(t *testing.T) {
nonProtected := &model.PropertyField{
GroupID: groupID,
Name: "Non-Protected in Batch",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
protected := &model.PropertyField{
GroupID: groupID,
Name: "Protected in Batch",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
}
createdNonProtected, appErr := th.App.CreatePropertyField(th.Context, nonProtected, false, "")
require.Nil(t, appErr)
createdProtected, appErr := th.App.CreatePropertyField(th.Context, protected, true, "")
require.Nil(t, appErr)
createdNonProtected.Name = "Updated Non-Protected"
createdProtected.Name = "Updated Protected"
updated, _, appErr := th.App.UpdatePropertyFields(th.Context, groupID, []*model.PropertyField{createdNonProtected, createdProtected}, false, "")
require.NotNil(t, appErr)
assert.Nil(t, updated)
assert.Equal(t, "app.property_field.update.protected.app_error", appErr.Id)
assert.Equal(t, http.StatusForbidden, appErr.StatusCode)
// Verify neither field was updated
fetchedNonProtected, appErr := th.App.GetPropertyField(th.Context, groupID, createdNonProtected.ID)
require.Nil(t, appErr)
assert.Equal(t, "Non-Protected in Batch", fetchedNonProtected.Name)
fetchedProtected, appErr := th.App.GetPropertyField(th.Context, groupID, createdProtected.ID)
require.Nil(t, appErr)
assert.Equal(t, "Protected in Batch", fetchedProtected.Name)
})
t.Run("should allow batch update with protected fields when bypass is true", func(t *testing.T) {
nonProtected := &model.PropertyField{
GroupID: groupID,
Name: "Non-Protected Bypass Batch",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
protected := &model.PropertyField{
GroupID: groupID,
Name: "Protected Bypass Batch",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
}
createdNonProtected, appErr := th.App.CreatePropertyField(th.Context, nonProtected, false, "")
require.Nil(t, appErr)
createdProtected, appErr := th.App.CreatePropertyField(th.Context, protected, true, "")
require.Nil(t, appErr)
createdNonProtected.Name = "Bypass Updated Non-Protected"
createdProtected.Name = "Bypass Updated Protected"
updated, _, appErr := th.App.UpdatePropertyFields(th.Context, groupID, []*model.PropertyField{createdNonProtected, createdProtected}, true, "")
require.Nil(t, appErr)
require.Len(t, updated, 2)
})
t.Run("should fail to update if any field comes from a different property group", func(t *testing.T) {
// Create a field in a different group
otherGroup, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{Name: "test_other_group", Version: model.PropertyGroupVersionV1})
require.Nil(t, appErr)
fieldInOtherGroup := &model.PropertyField{
GroupID: otherGroup.ID,
Name: "Field in Other Group",
Type: model.PropertyFieldTypeText,
}
createdOther, appErr := th.App.CreatePropertyField(th.Context, fieldInOtherGroup, false, "")
require.Nil(t, appErr)
// Create a field in the main group
fieldInMainGroup := &model.PropertyField{
GroupID: groupID,
Name: "Field in Main Group",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
createdMain, appErr := th.App.CreatePropertyField(th.Context, fieldInMainGroup, false, "")
require.Nil(t, appErr)
// Try to update both fields using the main groupID - should fail for the other group's field
createdMain.Name = "Updated Main"
createdOther.Name = "Updated Other"
_, _, appErr = th.App.UpdatePropertyFields(th.Context, groupID, []*model.PropertyField{createdMain, createdOther}, false, "")
require.NotNil(t, appErr)
// Verify neither field was updated
fetchedMain, appErr := th.App.GetPropertyField(th.Context, groupID, createdMain.ID)
require.Nil(t, appErr)
assert.Equal(t, "Field in Main Group", fetchedMain.Name)
fetchedOther, appErr := th.App.GetPropertyField(th.Context, otherGroup.ID, createdOther.ID)
require.Nil(t, appErr)
assert.Equal(t, "Field in Other Group", fetchedOther.Name)
})
}
func TestCreatePropertyFieldVersionEnforcement(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("should reject creating a v2 field on a v1 group", func(t *testing.T) {
group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{Name: "v1_group_reject_v2_field", Version: model.PropertyGroupVersionV1})
require.Nil(t, appErr)
field := &model.PropertyField{
GroupID: group.ID,
Name: "V2 Field on V1 Group",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.NotNil(t, appErr)
assert.Nil(t, created)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("should reject creating a v1 field on a v2 group", func(t *testing.T) {
group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{Name: "v2_group_reject_v1_field", Version: model.PropertyGroupVersionV2})
require.Nil(t, appErr)
field := &model.PropertyField{
GroupID: group.ID,
Name: "V1 Field on V2 Group",
Type: model.PropertyFieldTypeText,
TargetType: "user",
// No ObjectType → PSAv1 field
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.NotNil(t, appErr)
assert.Nil(t, created)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("should allow creating a v1 field on a v1 group", func(t *testing.T) {
group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{Name: "v1_group_allow_v1_field", Version: model.PropertyGroupVersionV1})
require.Nil(t, appErr)
field := &model.PropertyField{
GroupID: group.ID,
Name: "V1 Field on V1 Group",
Type: model.PropertyFieldTypeText,
TargetType: "user",
// No ObjectType → PSAv1 field
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
assert.NotEmpty(t, created.ID)
})
t.Run("should allow creating a v2 field on a v2 group", func(t *testing.T) {
group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{Name: "v2_group_allow_v2_field", Version: model.PropertyGroupVersionV2})
require.Nil(t, appErr)
field := &model.PropertyField{
GroupID: group.ID,
Name: "V2 Field on V2 Group",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
assert.NotEmpty(t, created.ID)
})
}
func TestUpdatePropertyFieldVersionEnforcement(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("should reject updating a v2 field on a v1 group", func(t *testing.T) {
v1Group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{Name: "v1_group_update_reject_v2", Version: model.PropertyGroupVersionV1})
require.Nil(t, appErr)
// Create a v1 field on the v1 group (allowed)
field := &model.PropertyField{
GroupID: v1Group.ID,
Name: "V1 Field for Update Test",
Type: model.PropertyFieldTypeText,
TargetType: "user",
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
// Attempt to update it as a v2 field (add ObjectType to make it v2)
created.ObjectType = model.PropertyFieldObjectTypeUser
created.TargetType = string(model.PropertyFieldTargetLevelSystem)
updated, _, appErr := th.App.UpdatePropertyField(th.Context, v1Group.ID, created, false, "")
require.NotNil(t, appErr)
assert.Nil(t, updated)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("should reject updating a v1 field on a v2 group", func(t *testing.T) {
v2Group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{Name: "v2_group_update_reject_v1", Version: model.PropertyGroupVersionV2})
require.Nil(t, appErr)
// Create a v2 field on the v2 group (allowed)
field := &model.PropertyField{
GroupID: v2Group.ID,
Name: "V2 Field for Update Test",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
// Attempt to update it as a v1 field (remove ObjectType to make it v1)
created.ObjectType = ""
created.TargetType = "user"
updated, _, appErr := th.App.UpdatePropertyField(th.Context, v2Group.ID, created, false, "")
require.NotNil(t, appErr)
assert.Nil(t, updated)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("should allow updating a v1 field on a v1 group", func(t *testing.T) {
v1Group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{Name: "v1_group_update_allow_v1", Version: model.PropertyGroupVersionV1})
require.Nil(t, appErr)
field := &model.PropertyField{
GroupID: v1Group.ID,
Name: "V1 Field Update Allowed",
Type: model.PropertyFieldTypeText,
TargetType: "user",
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
created.Name = "V1 Field Updated"
updated, _, appErr := th.App.UpdatePropertyField(th.Context, v1Group.ID, created, false, "")
require.Nil(t, appErr)
assert.Equal(t, "V1 Field Updated", updated.Name)
})
t.Run("should allow updating a v2 field on a v2 group", func(t *testing.T) {
v2Group, appErr := th.App.RegisterPropertyGroup(th.Context, &model.PropertyGroup{Name: "v2_group_update_allow_v2", Version: model.PropertyGroupVersionV2})
require.Nil(t, appErr)
field := &model.PropertyField{
GroupID: v2Group.ID,
Name: "V2 Field Update Allowed",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
created.Name = "V2 Field Updated"
updated, _, appErr := th.App.UpdatePropertyField(th.Context, v2Group.ID, created, false, "")
require.Nil(t, appErr)
assert.Equal(t, "V2 Field Updated", updated.Name)
})
}
func TestDeletePropertyField(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
t.Run("should delete a non-protected field without bypass", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Field to Delete",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
appErr = th.App.DeletePropertyField(th.Context, groupID, created.ID, false, "")
require.Nil(t, appErr)
// Verify soft deletion (DeleteAt > 0)
deleted, appErr := th.App.GetPropertyField(th.Context, groupID, created.ID)
require.Nil(t, appErr)
assert.NotZero(t, deleted.DeleteAt)
})
t.Run("should reject deleting protected field without bypass", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Protected Field to Delete",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, true, "")
require.Nil(t, appErr)
appErr = th.App.DeletePropertyField(th.Context, groupID, created.ID, false, "")
require.NotNil(t, appErr)
assert.Equal(t, "app.property_field.delete.protected.app_error", appErr.Id)
assert.Equal(t, http.StatusForbidden, appErr.StatusCode)
// Verify field still exists
existing, appErr := th.App.GetPropertyField(th.Context, groupID, created.ID)
require.Nil(t, appErr)
assert.NotNil(t, existing)
assert.Zero(t, existing.DeleteAt)
})
t.Run("should allow deleting protected field with bypass", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Protected Field Bypass Delete",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Protected: true,
PermissionField: model.NewPointer(model.PermissionLevelNone),
PermissionValues: model.NewPointer(model.PermissionLevelMember),
PermissionOptions: model.NewPointer(model.PermissionLevelSysadmin),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, true, "")
require.Nil(t, appErr)
appErr = th.App.DeletePropertyField(th.Context, groupID, created.ID, true, "")
require.Nil(t, appErr)
// Verify soft deletion (DeleteAt > 0)
deleted, appErr := th.App.GetPropertyField(th.Context, groupID, created.ID)
require.Nil(t, appErr)
assert.NotZero(t, deleted.DeleteAt)
})
t.Run("should delete a user-targeted field without triggering broadcast", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "User Targeted Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
appErr = th.App.DeletePropertyField(th.Context, groupID, created.ID, false, "")
require.Nil(t, appErr)
// Verify soft deletion (DeleteAt > 0)
deleted, appErr := th.App.GetPropertyField(th.Context, groupID, created.ID)
require.Nil(t, appErr)
assert.NotZero(t, deleted.DeleteAt)
})
}
func TestPropertyFieldCacheInvalidation(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
newField := func(groupID, name string) *model.PropertyField {
return &model.PropertyField{
GroupID: groupID,
Name: name,
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
}
t.Run("create invalidates the cached group listing", func(t *testing.T) {
groupID := registerTestPropertyGroup(t, th)
_, appErr := th.App.CreatePropertyField(th.Context, newField(groupID, "First Field"), false, "")
require.Nil(t, appErr)
// Populate the cache for this group.
fields, appErr := th.App.GetPropertyFieldsForGroup(th.Context, groupID)
require.Nil(t, appErr)
require.Len(t, fields, 1)
_, appErr = th.App.CreatePropertyField(th.Context, newField(groupID, "Second Field"), false, "")
require.Nil(t, appErr)
// A stale cache would still return a single field here.
fields, appErr = th.App.GetPropertyFieldsForGroup(th.Context, groupID)
require.Nil(t, appErr)
assert.Len(t, fields, 2)
})
t.Run("update invalidates the cached group listing", func(t *testing.T) {
groupID := registerTestPropertyGroup(t, th)
created, appErr := th.App.CreatePropertyField(th.Context, newField(groupID, "Original Name"), false, "")
require.Nil(t, appErr)
fields, appErr := th.App.GetPropertyFieldsForGroup(th.Context, groupID)
require.Nil(t, appErr)
require.Len(t, fields, 1)
require.Equal(t, "Original Name", fields[0].Name)
created.Name = "Updated Name"
_, _, appErr = th.App.UpdatePropertyField(th.Context, groupID, created, false, "")
require.Nil(t, appErr)
// A stale cache would still return the original name here.
fields, appErr = th.App.GetPropertyFieldsForGroup(th.Context, groupID)
require.Nil(t, appErr)
require.Len(t, fields, 1)
assert.Equal(t, "Updated Name", fields[0].Name)
})
t.Run("delete invalidates the cached group listing", func(t *testing.T) {
groupID := registerTestPropertyGroup(t, th)
created, appErr := th.App.CreatePropertyField(th.Context, newField(groupID, "Field to Delete"), false, "")
require.Nil(t, appErr)
fields, appErr := th.App.GetPropertyFieldsForGroup(th.Context, groupID)
require.Nil(t, appErr)
require.Len(t, fields, 1)
appErr = th.App.DeletePropertyField(th.Context, groupID, created.ID, false, "")
require.Nil(t, appErr)
// A stale cache would still return the deleted field here.
fields, appErr = th.App.GetPropertyFieldsForGroup(th.Context, groupID)
require.Nil(t, appErr)
assert.Empty(t, fields)
})
}
func TestPropertyFieldBroadcastParams(t *testing.T) {
rctx := request.TestContext(t)
t.Run("team target type returns team ID", func(t *testing.T) {
field := &model.PropertyField{TargetType: "team", TargetID: "team123"}
teamID, channelID, ok := propertyFieldBroadcastParams(rctx, field)
assert.True(t, ok)
assert.Equal(t, "team123", teamID)
assert.Empty(t, channelID)
})
t.Run("channel target type returns channel ID", func(t *testing.T) {
field := &model.PropertyField{TargetType: "channel", TargetID: "chan123"}
teamID, channelID, ok := propertyFieldBroadcastParams(rctx, field)
assert.True(t, ok)
assert.Empty(t, teamID)
assert.Equal(t, "chan123", channelID)
})
t.Run("system target type returns empty strings", func(t *testing.T) {
field := &model.PropertyField{TargetType: "system"}
teamID, channelID, ok := propertyFieldBroadcastParams(rctx, field)
assert.True(t, ok)
assert.Empty(t, teamID)
assert.Empty(t, channelID)
})
t.Run("empty target type returns ok=false", func(t *testing.T) {
field := &model.PropertyField{TargetType: ""}
_, _, ok := propertyFieldBroadcastParams(rctx, field)
assert.False(t, ok)
})
t.Run("unrecognized target type returns ok=false", func(t *testing.T) {
field := &model.PropertyField{TargetType: "organization", TargetID: "org123"}
_, _, ok := propertyFieldBroadcastParams(rctx, field)
assert.False(t, ok)
})
}
func TestGetPropertyField(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
t.Run("should get an existing field", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Field to Get",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
fetched, appErr := th.App.GetPropertyField(th.Context, groupID, created.ID)
require.Nil(t, appErr)
assert.Equal(t, created.ID, fetched.ID)
assert.Equal(t, "Field to Get", fetched.Name)
})
t.Run("should return error for non-existent field", func(t *testing.T) {
_, appErr := th.App.GetPropertyField(th.Context, groupID, model.NewId())
require.NotNil(t, appErr)
})
}
func TestGetPropertyFields(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
t.Run("should get multiple fields", func(t *testing.T) {
field1 := &model.PropertyField{
GroupID: groupID,
Name: "Multi Get Field 1",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
field2 := &model.PropertyField{
GroupID: groupID,
Name: "Multi Get Field 2",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created1, appErr := th.App.CreatePropertyField(th.Context, field1, false, "")
require.Nil(t, appErr)
created2, appErr := th.App.CreatePropertyField(th.Context, field2, false, "")
require.Nil(t, appErr)
fetched, appErr := th.App.GetPropertyFields(th.Context, groupID, []string{created1.ID, created2.ID})
require.Nil(t, appErr)
assert.Len(t, fetched, 2)
})
}
func TestSearchPropertyFields(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
t.Run("should search for fields", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Searchable Field",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
_, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
opts := model.PropertyFieldSearchOpts{
GroupID: groupID,
PerPage: 100,
}
results, appErr := th.App.SearchPropertyFields(th.Context, groupID, opts)
require.Nil(t, appErr)
assert.NotEmpty(t, results)
})
}
func TestCreatePropertyField_SystemCanonicalization(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
t.Run("system object: TargetType+TargetID and Permission* are canonicalized", func(t *testing.T) {
member := model.PermissionLevelMember
field := &model.PropertyField{
GroupID: groupID,
Name: "System Canonicalize",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeSystem,
TargetType: "channel",
TargetID: model.NewId(),
PermissionField: &member,
PermissionValues: &member,
PermissionOptions: &member,
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
assert.Equal(t, string(model.PropertyFieldTargetLevelSystem), created.TargetType)
assert.Empty(t, created.TargetID)
require.NotNil(t, created.PermissionField)
assert.Equal(t, model.PermissionLevelSysadmin, *created.PermissionField)
require.NotNil(t, created.PermissionValues)
assert.Equal(t, model.PermissionLevelSysadmin, *created.PermissionValues)
require.NotNil(t, created.PermissionOptions)
assert.Equal(t, model.PermissionLevelSysadmin, *created.PermissionOptions)
})
}
func TestCreatePropertyField_TrimName(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
t.Run("trims whitespace around name", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: " trim-me ",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
assert.Equal(t, "trim-me", created.Name)
})
}
func TestUpdatePropertyField_TrimNameOnUpdate(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
t.Run("trims whitespace on update", func(t *testing.T) {
field := &model.PropertyField{
GroupID: groupID,
Name: "Trim Update",
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
created.Name = " trimmed-on-update "
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, created, false, "")
require.Nil(t, appErr)
assert.Equal(t, "trimmed-on-update", updated.Name)
})
}
func TestUpdatePropertyField_LinkedFieldInvariants(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
makeLinkedPair := func(t *testing.T) (template, linked *model.PropertyField) {
t.Helper()
tmpl := &model.PropertyField{
GroupID: groupID,
Name: "tmpl-" + model.NewId(),
Type: model.PropertyFieldTypeSelect,
ObjectType: model.PropertyFieldObjectTypeTemplate,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Attrs: model.StringInterface{
model.PropertyFieldAttributeOptions: []map[string]any{
{"id": model.NewId(), "name": "opt1"},
},
},
}
createdTmpl, appErr := th.App.CreatePropertyField(th.Context, tmpl, false, "")
require.Nil(t, appErr)
linkedID := createdTmpl.ID
linkedField := &model.PropertyField{
GroupID: groupID,
Name: "linked-" + model.NewId(),
Type: model.PropertyFieldTypeSelect,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
LinkedFieldID: &linkedID,
}
createdLinked, appErr := th.App.CreatePropertyField(th.Context, linkedField, false, "")
require.Nil(t, appErr)
return createdTmpl, createdLinked
}
t.Run("type immutable on linked field", func(t *testing.T) {
_, linked := makeLinkedPair(t)
linked.Type = model.PropertyFieldTypeText
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, linked, false, "")
require.NotNil(t, appErr)
assert.Nil(t, updated)
assert.Equal(t, "app.property_field.update.linked_type_change.app_error", appErr.Id)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("options immutable on linked field", func(t *testing.T) {
_, linked := makeLinkedPair(t)
linked.Attrs = model.StringInterface{
model.PropertyFieldAttributeOptions: []map[string]any{
{"id": model.NewId(), "name": "different"},
},
}
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, linked, false, "")
require.NotNil(t, appErr)
assert.Nil(t, updated)
assert.Equal(t, "app.property_field.update.linked_options_change.app_error", appErr.Id)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("link target immutable: cannot change to different target", func(t *testing.T) {
altTmpl, linked := makeLinkedPair(t)
// Create another template to point to
_ = altTmpl
newTmpl := &model.PropertyField{
GroupID: groupID,
Name: "tmpl-alt-" + model.NewId(),
Type: model.PropertyFieldTypeSelect,
ObjectType: model.PropertyFieldObjectTypeTemplate,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Attrs: model.StringInterface{
model.PropertyFieldAttributeOptions: []map[string]any{
{"id": model.NewId(), "name": "x"},
},
},
}
createdNew, appErr := th.App.CreatePropertyField(th.Context, newTmpl, false, "")
require.Nil(t, appErr)
newID := createdNew.ID
linked.LinkedFieldID = &newID
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, linked, false, "")
require.NotNil(t, appErr)
assert.Nil(t, updated)
assert.Equal(t, "app.property_field.update.cannot_change_link_target.app_error", appErr.Id)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("cannot link a previously-unlinked field", func(t *testing.T) {
unlinked := &model.PropertyField{
GroupID: groupID,
Name: "unlinked-" + model.NewId(),
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
createdUnlinked, appErr := th.App.CreatePropertyField(th.Context, unlinked, false, "")
require.Nil(t, appErr)
// Create a template to link to
tmpl := &model.PropertyField{
GroupID: groupID,
Name: "tmpl-late-" + model.NewId(),
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeTemplate,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
createdTmpl, appErr := th.App.CreatePropertyField(th.Context, tmpl, false, "")
require.Nil(t, appErr)
tID := createdTmpl.ID
createdUnlinked.LinkedFieldID = &tID
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, createdUnlinked, false, "")
require.NotNil(t, appErr)
assert.Nil(t, updated)
assert.Equal(t, "app.property_field.update.cannot_link_existing.app_error", appErr.Id)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
}
func TestUpdatePropertyField_LinkedFieldNoOpPatchOK(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
t.Run("setting Type to current value on a linked field passes", func(t *testing.T) {
// Build template + linked
tmpl := &model.PropertyField{
GroupID: groupID,
Name: "tmpl-noop-" + model.NewId(),
Type: model.PropertyFieldTypeSelect,
ObjectType: model.PropertyFieldObjectTypeTemplate,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Attrs: model.StringInterface{
model.PropertyFieldAttributeOptions: []map[string]any{
{"id": model.NewId(), "name": "n"},
},
},
}
createdTmpl, appErr := th.App.CreatePropertyField(th.Context, tmpl, false, "")
require.Nil(t, appErr)
linkedID := createdTmpl.ID
linked := &model.PropertyField{
GroupID: groupID,
Name: "linked-noop-" + model.NewId(),
Type: model.PropertyFieldTypeSelect,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
LinkedFieldID: &linkedID,
}
createdLinked, appErr := th.App.CreatePropertyField(th.Context, linked, false, "")
require.Nil(t, appErr)
// No-op update: Type unchanged.
createdLinked.Name = "linked-renamed"
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, createdLinked, false, "")
require.Nil(t, appErr)
assert.Equal(t, "linked-renamed", updated.Name)
})
}
func TestUpdatePropertyField_LinkedFieldUnlinkAllowed(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
t.Run("plugin path: setting LinkedFieldID = nil on a linked field unlinks it", func(t *testing.T) {
tmpl := &model.PropertyField{
GroupID: groupID,
Name: "tmpl-unlink-" + model.NewId(),
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeTemplate,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
createdTmpl, appErr := th.App.CreatePropertyField(th.Context, tmpl, false, "")
require.Nil(t, appErr)
linkedID := createdTmpl.ID
linked := &model.PropertyField{
GroupID: groupID,
Name: "linked-unlink-" + model.NewId(),
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
LinkedFieldID: &linkedID,
}
createdLinked, appErr := th.App.CreatePropertyField(th.Context, linked, false, "")
require.Nil(t, appErr)
createdLinked.LinkedFieldID = nil
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, createdLinked, false, "")
require.Nil(t, appErr)
assert.Nil(t, updated.LinkedFieldID)
})
}
// TestPropertyFieldAccessControlSignalling verifies that mutating a property
// field notifies the access control service via OnPropertyFieldOptionsChanged
// so it can drop cached per-field metadata (e.g. the rank-by-name lookup) and
// invalidate compiled-policy cache entries. The call is guarded by a nil check
// because the AccessControl service is enterprise-only.
func TestPropertyFieldAccessControlSignalling(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
// Rank fields are gated behind the PropertyFieldRank feature flag.
th.ConfigStore.SetReadOnlyFF(false)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.FeatureFlags.PropertyFieldRank = true
})
groupID := registerTestPropertyGroup(t, th)
newRankField := func(t *testing.T) *model.PropertyField {
t.Helper()
field := &model.PropertyField{
GroupID: groupID,
Name: "rank_" + model.NewId(),
Type: model.PropertyFieldTypeRank,
TargetType: "system",
ObjectType: "user",
Attrs: model.StringInterface{
model.PropertyFieldAttributeOptions: []any{
map[string]any{"name": "LOW", "rank": 1},
map[string]any{"name": "HIGH", "rank": 2},
},
},
}
created, appErr := th.App.CreatePropertyField(th.Context, field, false, "")
require.Nil(t, appErr)
return created
}
t.Run("UpdatePropertyFields signals for each updated field", func(t *testing.T) {
f1 := newRankField(t)
f2 := newRankField(t)
mockACS := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockACS
t.Cleanup(func() { th.App.Srv().ch.AccessControl = nil })
mockACS.On("OnPropertyFieldOptionsChanged", mock.Anything, f1.ID).Return().Once()
mockACS.On("OnPropertyFieldOptionsChanged", mock.Anything, f2.ID).Return().Once()
_, _, appErr := th.App.UpdatePropertyFields(th.Context, groupID, []*model.PropertyField{f1, f2}, false, "")
require.Nil(t, appErr)
mockACS.AssertExpectations(t)
})
t.Run("DeletePropertyField signals for the deleted field", func(t *testing.T) {
f := newRankField(t)
mockACS := &mocks.AccessControlServiceInterface{}
th.App.Srv().ch.AccessControl = mockACS
t.Cleanup(func() { th.App.Srv().ch.AccessControl = nil })
mockACS.On("OnPropertyFieldOptionsChanged", mock.Anything, f.ID).Return().Once()
appErr := th.App.DeletePropertyField(th.Context, groupID, f.ID, false, "")
require.Nil(t, appErr)
mockACS.AssertExpectations(t)
})
t.Run("mutations succeed (no panic) when access control is unavailable", func(t *testing.T) {
// The signalling is guarded by `if acs != nil`; with no enterprise
// service installed the field CRUD must still succeed.
th.App.Srv().ch.AccessControl = nil
f := newRankField(t)
_, _, appErr := th.App.UpdatePropertyFields(th.Context, groupID, []*model.PropertyField{f}, false, "")
require.Nil(t, appErr)
appErr = th.App.DeletePropertyField(th.Context, groupID, f.ID, false, "")
require.Nil(t, appErr)
})
}
// TestPropertyFieldRankGate verifies the consolidated PropertyFieldRank feature
// flag gate in CreatePropertyField / UpdatePropertyFields: when the flag is off
// (the default), the app layer must reject both creating a rank field and
// converting an existing field to rank, while leaving non-rank fields alone.
func TestPropertyFieldRankGate(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
groupID := registerTestPropertyGroup(t, th)
// Feature flags are read-only at runtime by default; allow this test to
// toggle PropertyFieldRank on and off.
th.ConfigStore.SetReadOnlyFF(false)
setRankFlag := func(t *testing.T, enabled bool) {
t.Helper()
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.FeatureFlags.PropertyFieldRank = enabled
})
}
rankField := func() *model.PropertyField {
return &model.PropertyField{
GroupID: groupID,
Name: "rank_" + model.NewId(),
Type: model.PropertyFieldTypeRank,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Attrs: model.StringInterface{
model.PropertyFieldAttributeOptions: []any{
map[string]any{"name": "LOW", "rank": 1},
map[string]any{"name": "HIGH", "rank": 2},
},
},
}
}
textField := func() *model.PropertyField {
return &model.PropertyField{
GroupID: groupID,
Name: "text_" + model.NewId(),
Type: model.PropertyFieldTypeText,
ObjectType: model.PropertyFieldObjectTypeUser,
TargetType: string(model.PropertyFieldTargetLevelSystem),
}
}
// classificationRankField models how the classification-markings feature
// uses the rank type: a non-user object type (template/system/channel). The
// gate is scoped to user-object fields, so these must remain creatable and
// editable even with the flag off — otherwise the classification admin
// panel breaks in the default configuration.
classificationRankField := func() *model.PropertyField {
return &model.PropertyField{
GroupID: groupID,
Name: "classification_" + model.NewId(),
Type: model.PropertyFieldTypeRank,
ObjectType: model.PropertyFieldObjectTypeChannel,
TargetType: string(model.PropertyFieldTargetLevelSystem),
Attrs: model.StringInterface{
model.PropertyFieldAttributeOptions: []any{
map[string]any{"name": "UNCLASSIFIED", "rank": 1},
map[string]any{"name": "SECRET", "rank": 2},
},
},
}
}
t.Run("rejects creating a rank field when the flag is off", func(t *testing.T) {
setRankFlag(t, false)
created, appErr := th.App.CreatePropertyField(th.Context, rankField(), false, "")
require.NotNil(t, appErr)
assert.Nil(t, created)
assert.Equal(t, "app.property_field.rank_disabled.app_error", appErr.Id)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("rejects converting an existing field to rank when the flag is off", func(t *testing.T) {
setRankFlag(t, false)
created, appErr := th.App.CreatePropertyField(th.Context, textField(), false, "")
require.Nil(t, appErr)
created.Type = model.PropertyFieldTypeRank
created.Attrs = model.StringInterface{
model.PropertyFieldAttributeOptions: []any{
map[string]any{"name": "LOW", "rank": 1},
},
}
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, created, false, "")
require.NotNil(t, appErr)
assert.Nil(t, updated)
assert.Equal(t, "app.property_field.rank_disabled.app_error", appErr.Id)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
t.Run("allows non-rank field create and update when the flag is off", func(t *testing.T) {
setRankFlag(t, false)
created, appErr := th.App.CreatePropertyField(th.Context, textField(), false, "")
require.Nil(t, appErr)
created.Name = "renamed_" + model.NewId()
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, created, false, "")
require.Nil(t, appErr)
assert.Equal(t, model.PropertyFieldTypeText, updated.Type)
})
t.Run("allows creating a non-user (classification) rank field when the flag is off", func(t *testing.T) {
setRankFlag(t, false)
created, appErr := th.App.CreatePropertyField(th.Context, classificationRankField(), false, "")
require.Nil(t, appErr)
assert.Equal(t, model.PropertyFieldTypeRank, created.Type)
assert.Equal(t, model.PropertyFieldObjectTypeChannel, created.ObjectType)
})
t.Run("allows converting a non-user field to rank when the flag is off", func(t *testing.T) {
setRankFlag(t, false)
channelText := textField()
channelText.ObjectType = model.PropertyFieldObjectTypeChannel
created, appErr := th.App.CreatePropertyField(th.Context, channelText, false, "")
require.Nil(t, appErr)
created.Type = model.PropertyFieldTypeRank
created.Attrs = model.StringInterface{
model.PropertyFieldAttributeOptions: []any{
map[string]any{"name": "UNCLASSIFIED", "rank": 1},
},
}
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, created, false, "")
require.Nil(t, appErr)
assert.Equal(t, model.PropertyFieldTypeRank, updated.Type)
})
t.Run("allows creating a rank field when the flag is on", func(t *testing.T) {
setRankFlag(t, true)
created, appErr := th.App.CreatePropertyField(th.Context, rankField(), false, "")
require.Nil(t, appErr)
assert.Equal(t, model.PropertyFieldTypeRank, created.Type)
})
t.Run("allows converting an existing field to rank when the flag is on", func(t *testing.T) {
setRankFlag(t, true)
created, appErr := th.App.CreatePropertyField(th.Context, textField(), false, "")
require.Nil(t, appErr)
created.Type = model.PropertyFieldTypeRank
created.Attrs = model.StringInterface{
model.PropertyFieldAttributeOptions: []any{
map[string]any{"name": "LOW", "rank": 1},
},
}
updated, _, appErr := th.App.UpdatePropertyField(th.Context, groupID, created, false, "")
require.Nil(t, appErr)
assert.Equal(t, model.PropertyFieldTypeRank, updated.Type)
})
}