mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
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.
1362 lines
50 KiB
Go
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)
|
|
})
|
|
}
|