Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1282 lines
47 KiB
Go
1282 lines
47 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package api4
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCreateCPAField(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
})
|
|
defer th.TearDown()
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
field := &model.PropertyField{Name: model.NewId(), Type: model.PropertyFieldTypeText}
|
|
|
|
createdField, resp, err := client.CreateCPAField(context.Background(), field)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
require.Empty(t, createdField)
|
|
}, "endpoint should not work if no valid license is present")
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
t.Run("a user without admin permissions should not be able to create a field", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
}
|
|
|
|
_, resp, err := th.Client.CreateCPAField(context.Background(), field)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
field := &model.PropertyField{Name: model.NewId()}
|
|
|
|
createdField, resp, err := client.CreateCPAField(context.Background(), field)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
require.Empty(t, createdField)
|
|
}, "an invalid field should be rejected")
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
|
|
|
name := model.NewId()
|
|
field := &model.PropertyField{
|
|
Name: fmt.Sprintf(" %s\t", name), // name should be sanitized
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: map[string]any{"visibility": "when_set"},
|
|
}
|
|
|
|
createdField, resp, err := client.CreateCPAField(context.Background(), field)
|
|
CheckCreatedStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, createdField.ID)
|
|
require.Equal(t, name, createdField.Name)
|
|
require.Equal(t, "when_set", createdField.Attrs["visibility"])
|
|
|
|
t.Run("a websocket event should be fired as part of the field creation", func(t *testing.T) {
|
|
var wsField model.PropertyField
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case event := <-webSocketClient.EventChannel:
|
|
if event.EventType() == model.WebsocketEventCPAFieldCreated {
|
|
fieldData, err := json.Marshal(event.GetData()["field"])
|
|
require.NoError(t, err)
|
|
require.NoError(t, json.Unmarshal(fieldData, &wsField))
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return false
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
require.NotEmpty(t, wsField.ID)
|
|
require.Equal(t, createdField, &wsField)
|
|
})
|
|
}, "a user with admin permissions should be able to create the field")
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
managedField := &model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsManaged: "admin",
|
|
"visibility": "when_set",
|
|
},
|
|
}
|
|
|
|
createdManagedField, resp, err := client.CreateCPAField(context.Background(), managedField)
|
|
CheckCreatedStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, createdManagedField.ID)
|
|
require.Equal(t, managedField.Name, createdManagedField.Name)
|
|
require.Equal(t, "admin", createdManagedField.Attrs[model.CustomProfileAttributesPropertyAttrsManaged])
|
|
require.Equal(t, "when_set", createdManagedField.Attrs["visibility"])
|
|
}, "admin should be able to create a managed field")
|
|
}
|
|
|
|
func TestListCPAFields(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
})
|
|
defer th.TearDown()
|
|
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: map[string]any{"visibility": "when_set"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
t.Run("endpoint should not work if no valid license is present", func(t *testing.T) {
|
|
fields, resp, err := th.Client.ListCPAFields(context.Background())
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
require.Empty(t, fields)
|
|
})
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
t.Run("any user should be able to list fields", func(t *testing.T) {
|
|
fields, resp, err := th.Client.ListCPAFields(context.Background())
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, fields)
|
|
require.Len(t, fields, 1)
|
|
require.Equal(t, createdField.ID, fields[0].ID)
|
|
})
|
|
|
|
t.Run("the endpoint should only list non deleted fields", func(t *testing.T) {
|
|
require.Nil(t, th.App.DeleteCPAField(createdField.ID))
|
|
fields, resp, err := th.Client.ListCPAFields(context.Background())
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Empty(t, fields)
|
|
})
|
|
}
|
|
|
|
func TestPatchCPAField(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
})
|
|
defer th.TearDown()
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
patch := &model.PropertyFieldPatch{Name: model.NewPointer(model.NewId())}
|
|
patchedField, resp, err := client.PatchCPAField(context.Background(), model.NewId(), patch)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
require.Empty(t, patchedField)
|
|
}, "endpoint should not work if no valid license is present")
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
t.Run("a user without admin permissions should not be able to patch a field", func(t *testing.T) {
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
patch := &model.PropertyFieldPatch{Name: model.NewPointer(model.NewId())}
|
|
_, resp, err := th.Client.PatchCPAField(context.Background(), createdField.ID, patch)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
|
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
newName := model.NewId()
|
|
patch := &model.PropertyFieldPatch{Name: model.NewPointer(fmt.Sprintf(" %s \t ", newName))} // name should be sanitized
|
|
patchedField, resp, err := client.PatchCPAField(context.Background(), createdField.ID, patch)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, newName, patchedField.Name)
|
|
|
|
t.Run("a websocket event should be fired as part of the field patch", func(t *testing.T) {
|
|
var wsField model.PropertyField
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case event := <-webSocketClient.EventChannel:
|
|
if event.EventType() == model.WebsocketEventCPAFieldUpdated {
|
|
fieldData, err := json.Marshal(event.GetData()["field"])
|
|
require.NoError(t, err)
|
|
require.NoError(t, json.Unmarshal(fieldData, &wsField))
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return false
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
require.NotEmpty(t, wsField.ID)
|
|
require.Equal(t, patchedField, &wsField)
|
|
})
|
|
|
|
t.Run("sanitization should remove options and sync details when necessary", func(t *testing.T) {
|
|
// Create a select field with options
|
|
optionID1 := model.NewId()
|
|
optionID2 := model.NewId()
|
|
selectField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeSelect,
|
|
Attrs: model.StringInterface{
|
|
"options": []map[string]any{
|
|
{"id": optionID1, "name": "Option 1", "color": "#FF0000"},
|
|
{"id": optionID2, "name": "Option 2", "color": "#00FF00"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, _, err := client.CreateCPAField(context.Background(), selectField.ToPropertyField())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdField)
|
|
|
|
// Verify options were created
|
|
options, ok := createdField.Attrs["options"]
|
|
require.True(t, ok)
|
|
require.NotNil(t, options)
|
|
|
|
// Patch to change type to text with LDAP attribute
|
|
// Options should be automatically removed even though we don't explicitly remove them
|
|
ldapAttr := "user_attribute"
|
|
textPatch := &model.PropertyFieldPatch{
|
|
Type: model.NewPointer(model.PropertyFieldTypeText),
|
|
Attrs: &model.StringInterface{"ldap": ldapAttr},
|
|
}
|
|
|
|
patchedTextField, resp, err := client.PatchCPAField(context.Background(), createdField.ID, textPatch)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, model.PropertyFieldTypeText, patchedTextField.Type)
|
|
|
|
// Verify options were removed
|
|
options = patchedTextField.Attrs["options"]
|
|
require.Empty(t, options)
|
|
|
|
// Verify LDAP attribute was set
|
|
ldap, ok := patchedTextField.Attrs["ldap"]
|
|
require.True(t, ok)
|
|
require.Equal(t, ldapAttr, ldap)
|
|
|
|
// Now patch to change type to date
|
|
// LDAP attribute should be automatically removed even though we don't explicitly remove it
|
|
datePatch := &model.PropertyFieldPatch{
|
|
Type: model.NewPointer(model.PropertyFieldTypeDate),
|
|
}
|
|
|
|
patchedDateField, resp, err := client.PatchCPAField(context.Background(), patchedTextField.ID, datePatch)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, model.PropertyFieldTypeDate, patchedDateField.Type)
|
|
|
|
// Verify LDAP attribute was removed
|
|
ldap = patchedDateField.Attrs["ldap"]
|
|
require.Empty(t, ldap)
|
|
})
|
|
}, "a user with admin permissions should be able to patch the field")
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
// Create a regular field first
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
// Verify field is not isManaged initially
|
|
require.Empty(t, createdField.Attrs[model.CustomProfileAttributesPropertyAttrsManaged])
|
|
|
|
// Patch to make it managed
|
|
managedPatch := &model.PropertyFieldPatch{
|
|
Attrs: &model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsManaged: "admin",
|
|
},
|
|
}
|
|
|
|
patchedManagedField, resp, err := client.PatchCPAField(context.Background(), createdField.ID, managedPatch)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "admin", patchedManagedField.Attrs[model.CustomProfileAttributesPropertyAttrsManaged])
|
|
|
|
// Patch to remove managed attribute
|
|
unManagedPatch := &model.PropertyFieldPatch{
|
|
Attrs: &model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsManaged: "",
|
|
},
|
|
}
|
|
|
|
patchedUnmanagedField, resp, err := client.PatchCPAField(context.Background(), patchedManagedField.ID, unManagedPatch)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
|
|
// Verify managed attribute is removed or empty
|
|
require.Empty(t, patchedUnmanagedField.Attrs[model.CustomProfileAttributesPropertyAttrsManaged])
|
|
}, "admin should be able to toggle managed attribute on existing field")
|
|
}
|
|
|
|
func TestDeleteCPAField(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
})
|
|
defer th.TearDown()
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
resp, err := client.DeleteCPAField(context.Background(), model.NewId())
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
}, "endpoint should not work if no valid license is present")
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
t.Run("a user without admin permissions should not be able to delete a field", func(t *testing.T) {
|
|
field := &model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
}
|
|
createdField, _, err := th.SystemAdminClient.CreateCPAField(context.Background(), field)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdField)
|
|
|
|
resp, err := th.Client.DeleteCPAField(context.Background(), createdField.ID)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
|
|
|
field := &model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
}
|
|
createdField, _, err := th.SystemAdminClient.CreateCPAField(context.Background(), field)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdField)
|
|
require.Zero(t, createdField.DeleteAt)
|
|
|
|
resp, err := client.DeleteCPAField(context.Background(), createdField.ID)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
|
|
deletedField, appErr := th.App.GetCPAField(createdField.ID)
|
|
require.Nil(t, appErr)
|
|
require.NotZero(t, deletedField.DeleteAt)
|
|
|
|
t.Run("a websocket event should be fired as part of the field deletion", func(t *testing.T) {
|
|
var fieldID string
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case event := <-webSocketClient.EventChannel:
|
|
if event.EventType() == model.WebsocketEventCPAFieldDeleted {
|
|
var ok bool
|
|
fieldID, ok = event.GetData()["field_id"].(string)
|
|
require.True(t, ok)
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return false
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
require.Equal(t, createdField.ID, fieldID)
|
|
})
|
|
}, "a user with admin permissions should be able to delete the field")
|
|
}
|
|
|
|
func TestListCPAValues(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
}).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
th.RemovePermissionFromRole(model.PermissionViewMembers.Id, model.SystemUserRoleId)
|
|
defer th.AddPermissionToRole(model.PermissionViewMembers.Id, model.SystemUserRoleId)
|
|
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
_, appErr = th.App.PatchCPAValue(th.BasicUser.Id, createdField.ID, json.RawMessage(`"Field Value"`), true)
|
|
require.Nil(t, appErr)
|
|
|
|
t.Run("endpoint should not work if no valid license is present", func(t *testing.T) {
|
|
values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
require.Empty(t, values)
|
|
})
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
// login with Client2 from this point on
|
|
th.LoginBasic2()
|
|
|
|
t.Run("any team member should be able to list values", func(t *testing.T) {
|
|
values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, values)
|
|
require.Len(t, values, 1)
|
|
})
|
|
|
|
t.Run("should handle array values correctly", func(t *testing.T) {
|
|
optionID1 := model.NewId()
|
|
optionID2 := model.NewId()
|
|
arrayField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
Attrs: model.StringInterface{
|
|
"options": []map[string]any{
|
|
{"id": optionID1, "name": "option1"},
|
|
{"id": optionID2, "name": "option2"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdArrayField, appErr := th.App.CreateCPAField(arrayField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdArrayField)
|
|
|
|
_, appErr = th.App.PatchCPAValue(th.BasicUser.Id, createdArrayField.ID, json.RawMessage(fmt.Sprintf(`["%s", "%s"]`, optionID1, optionID2)), true)
|
|
require.Nil(t, appErr)
|
|
|
|
values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, values)
|
|
|
|
var arrayValues []string
|
|
require.NoError(t, json.Unmarshal(values[createdArrayField.ID], &arrayValues))
|
|
require.ElementsMatch(t, []string{optionID1, optionID2}, arrayValues)
|
|
})
|
|
|
|
t.Run("non team member should NOT be able to list values", func(t *testing.T) {
|
|
resp, err := th.SystemAdminClient.RemoveTeamMember(context.Background(), th.BasicTeam.Id, th.BasicUser2.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
|
|
_, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestPatchCPAValues(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
}).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
t.Run("endpoint should not work if no valid license is present", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{createdField.ID: json.RawMessage(`"Field Value"`)}
|
|
patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
require.Empty(t, patchedValues)
|
|
})
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
t.Run("any team member should be able to create their own values", func(t *testing.T) {
|
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
|
|
|
values := map[string]json.RawMessage{}
|
|
value := "Field Value"
|
|
values[createdField.ID] = json.RawMessage(fmt.Sprintf(`" %s "`, value)) // value should be sanitized
|
|
patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
require.Len(t, patchedValues, 1)
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
|
|
values, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, values)
|
|
require.Len(t, values, 1)
|
|
actualValue = ""
|
|
require.NoError(t, json.Unmarshal(values[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
|
|
t.Run("a websocket event should be fired as part of the value changes", func(t *testing.T) {
|
|
var wsValues map[string]json.RawMessage
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case event := <-webSocketClient.EventChannel:
|
|
if event.EventType() == model.WebsocketEventCPAValuesUpdated {
|
|
valuesData, err := json.Marshal(event.GetData()["values"])
|
|
require.NoError(t, err)
|
|
require.NoError(t, json.Unmarshal(valuesData, &wsValues))
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return false
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
require.NotEmpty(t, wsValues)
|
|
require.Equal(t, patchedValues, wsValues)
|
|
})
|
|
})
|
|
|
|
t.Run("any team member should be able to patch their own values", func(t *testing.T) {
|
|
values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, values)
|
|
require.Len(t, values, 1)
|
|
|
|
value := "Updated Field Value"
|
|
values[createdField.ID] = json.RawMessage(fmt.Sprintf(`" %s \t"`, value)) // value should be sanitized
|
|
patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
|
|
values, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
actualValue = ""
|
|
require.NoError(t, json.Unmarshal(values[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
})
|
|
|
|
t.Run("should handle array values correctly", func(t *testing.T) {
|
|
optionsID := []string{model.NewId(), model.NewId(), model.NewId(), model.NewId()}
|
|
|
|
arrayField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
Attrs: model.StringInterface{
|
|
"options": []map[string]any{
|
|
{"id": optionsID[0], "name": "option1"},
|
|
{"id": optionsID[1], "name": "option2"},
|
|
{"id": optionsID[2], "name": "option3"},
|
|
{"id": optionsID[3], "name": "option4"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdArrayField, appErr := th.App.CreateCPAField(arrayField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdArrayField)
|
|
|
|
values := map[string]json.RawMessage{
|
|
createdArrayField.ID: json.RawMessage(fmt.Sprintf(`["%s", "%s", "%s"]`, optionsID[0], optionsID[1], optionsID[2])),
|
|
}
|
|
patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValues []string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdArrayField.ID], &actualValues))
|
|
require.Equal(t, optionsID[:3], actualValues)
|
|
|
|
// Test updating array values
|
|
values[createdArrayField.ID] = json.RawMessage(fmt.Sprintf(`["%s", "%s"]`, optionsID[2], optionsID[3]))
|
|
patchedValues, resp, err = th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
|
|
actualValues = nil
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdArrayField.ID], &actualValues))
|
|
require.Equal(t, optionsID[2:4], actualValues)
|
|
})
|
|
|
|
t.Run("should fail if any of the values belongs to a field that is LDAP/SAML synced", func(t *testing.T) {
|
|
// Create a field with LDAP attribute
|
|
ldapField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsLDAP: "ldap_attr",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdLDAPField, appErr := th.App.CreateCPAField(ldapField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdLDAPField)
|
|
|
|
// Create a field with SAML attribute
|
|
samlField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsSAML: "saml_attr",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdSAMLField, appErr := th.App.CreateCPAField(samlField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdSAMLField)
|
|
|
|
// Test LDAP field
|
|
values := map[string]json.RawMessage{
|
|
createdLDAPField.ID: json.RawMessage(`"LDAP Value"`),
|
|
}
|
|
_, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_synced.app_error")
|
|
|
|
// Test SAML field
|
|
values = map[string]json.RawMessage{
|
|
createdSAMLField.ID: json.RawMessage(`"SAML Value"`),
|
|
}
|
|
_, resp, err = th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_synced.app_error")
|
|
|
|
// Test multiple fields with one being LDAP synced
|
|
values = map[string]json.RawMessage{
|
|
createdField.ID: json.RawMessage(`"Regular Value"`),
|
|
createdLDAPField.ID: json.RawMessage(`"LDAP Value"`),
|
|
}
|
|
_, resp, err = th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_synced.app_error")
|
|
})
|
|
|
|
t.Run("an invalid patch should be rejected", func(t *testing.T) {
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
// Create a value that's too long (over 64 characters)
|
|
tooLongValue := strings.Repeat("a", model.CPAValueTypeTextMaxLength+1)
|
|
values := map[string]json.RawMessage{
|
|
createdField.ID: json.RawMessage(fmt.Sprintf(`"%s"`, tooLongValue)),
|
|
}
|
|
|
|
_, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Failed to validate property value")
|
|
})
|
|
|
|
t.Run("admin-managed fields", func(t *testing.T) {
|
|
// Create a managed field (only admins can create fields)
|
|
managedField := &model.PropertyField{
|
|
Name: "Managed Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsManaged: "admin",
|
|
},
|
|
}
|
|
|
|
createdManagedField, resp, err := th.SystemAdminClient.CreateCPAField(context.Background(), managedField)
|
|
CheckCreatedStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdManagedField)
|
|
|
|
// Create a non-managed field for comparison
|
|
regularField := &model.PropertyField{
|
|
Name: "Regular Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
}
|
|
|
|
createdRegularField, resp, err := th.SystemAdminClient.CreateCPAField(context.Background(), regularField)
|
|
CheckCreatedStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdRegularField)
|
|
|
|
t.Run("regular user cannot update managed field", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Managed Value"`),
|
|
}
|
|
|
|
_, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_managed.app_error")
|
|
})
|
|
|
|
t.Run("regular user can update non-managed field", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdRegularField.ID: json.RawMessage(`"Regular Value"`),
|
|
}
|
|
|
|
patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdRegularField.ID], &actualValue))
|
|
require.Equal(t, "Regular Value", actualValue)
|
|
})
|
|
|
|
t.Run("system admin can update managed field", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Admin Updated Value"`),
|
|
}
|
|
|
|
patchedValues, resp, err := th.SystemAdminClient.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdManagedField.ID], &actualValue))
|
|
require.Equal(t, "Admin Updated Value", actualValue)
|
|
})
|
|
|
|
t.Run("batch update with managed fields fails for regular user", func(t *testing.T) {
|
|
// First set some initial values to ensure we can verify they don't change
|
|
// Set initial values for both fields using th.App (admins can set managed field values)
|
|
_, appErr := th.App.PatchCPAValue(th.BasicUser.Id, createdRegularField.ID, json.RawMessage(`"Initial Regular Value"`), false)
|
|
require.Nil(t, appErr)
|
|
|
|
_, appErr = th.App.PatchCPAValue(th.BasicUser.Id, createdManagedField.ID, json.RawMessage(`"Initial Managed Value"`), true)
|
|
require.Nil(t, appErr)
|
|
|
|
// Try to batch update both managed and regular fields - this should fail
|
|
attemptedValues := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Managed Batch Value"`),
|
|
createdRegularField.ID: json.RawMessage(`"Regular Batch Value"`),
|
|
}
|
|
|
|
_, resp, err := th.Client.PatchCPAValues(context.Background(), attemptedValues)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_managed.app_error")
|
|
|
|
// Verify that no values were updated when the batch operation failed
|
|
currentValues, appErr := th.App.ListCPAValues(th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
|
|
// Check that values remain unchanged - both fields should retain their initial values
|
|
regularFieldHasOriginalValue := false
|
|
managedFieldHasOriginalValue := false
|
|
|
|
for _, value := range currentValues {
|
|
if value.FieldID == createdManagedField.ID {
|
|
var currentValue string
|
|
require.NoError(t, json.Unmarshal(value.Value, ¤tValue))
|
|
if currentValue == "Initial Managed Value" {
|
|
managedFieldHasOriginalValue = true
|
|
}
|
|
// Verify it's not the attempted update value
|
|
require.NotEqual(t, "Managed Batch Value", currentValue, "Managed field should not have been updated in failed batch operation")
|
|
}
|
|
if value.FieldID == createdRegularField.ID {
|
|
var currentValue string
|
|
require.NoError(t, json.Unmarshal(value.Value, ¤tValue))
|
|
if currentValue == "Initial Regular Value" {
|
|
regularFieldHasOriginalValue = true
|
|
}
|
|
// Verify it's not the attempted update value
|
|
require.NotEqual(t, "Regular Batch Value", currentValue, "Regular field should not have been updated in failed batch operation")
|
|
}
|
|
}
|
|
|
|
// Both fields should retain their original values after the failed batch operation
|
|
require.True(t, regularFieldHasOriginalValue, "Regular field should retain its original value")
|
|
require.True(t, managedFieldHasOriginalValue, "Managed field should retain its original value")
|
|
})
|
|
|
|
t.Run("batch update with managed fields succeeds for admin", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Admin Managed Batch"`),
|
|
createdRegularField.ID: json.RawMessage(`"Admin Regular Batch"`),
|
|
}
|
|
|
|
patchedValues, resp, err := th.SystemAdminClient.PatchCPAValues(context.Background(), values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Len(t, patchedValues, 2)
|
|
|
|
var managedValue, regularValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdManagedField.ID], &managedValue))
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdRegularField.ID], ®ularValue))
|
|
require.Equal(t, "Admin Managed Batch", managedValue)
|
|
require.Equal(t, "Admin Regular Batch", regularValue)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestPatchCPAValuesForUser(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := SetupConfig(t, func(cfg *model.Config) {
|
|
cfg.FeatureFlags.CustomProfileAttributes = true
|
|
}).InitBasic()
|
|
defer th.TearDown()
|
|
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
t.Run("endpoint should not work if no valid license is present", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{createdField.ID: json.RawMessage(`"Field Value"`)}
|
|
patchedValues, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.custom_profile_attributes.license_error")
|
|
require.Empty(t, patchedValues)
|
|
})
|
|
|
|
// add a valid license
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
|
|
|
t.Run("any team member should be able to create their own values", func(t *testing.T) {
|
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
|
|
|
values := map[string]json.RawMessage{}
|
|
value := "Field Value"
|
|
values[createdField.ID] = json.RawMessage(fmt.Sprintf(`" %s "`, value)) // value should be sanitized
|
|
patchedValues, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
require.Len(t, patchedValues, 1)
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
|
|
values, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, values)
|
|
require.Len(t, values, 1)
|
|
actualValue = ""
|
|
require.NoError(t, json.Unmarshal(values[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
|
|
t.Run("a websocket event should be fired as part of the value changes", func(t *testing.T) {
|
|
var wsValues map[string]json.RawMessage
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case event := <-webSocketClient.EventChannel:
|
|
if event.EventType() == model.WebsocketEventCPAValuesUpdated {
|
|
valuesData, err := json.Marshal(event.GetData()["values"])
|
|
require.NoError(t, err)
|
|
require.NoError(t, json.Unmarshal(valuesData, &wsValues))
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
return false
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
require.NotEmpty(t, wsValues)
|
|
require.Equal(t, patchedValues, wsValues)
|
|
})
|
|
})
|
|
|
|
t.Run("any team member should be able to patch their own values", func(t *testing.T) {
|
|
values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, values)
|
|
require.Len(t, values, 1)
|
|
|
|
value := "Updated Field Value"
|
|
values[createdField.ID] = json.RawMessage(fmt.Sprintf(`" %s \t"`, value)) // value should be sanitized
|
|
patchedValues, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
|
|
values, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
actualValue = ""
|
|
require.NoError(t, json.Unmarshal(values[createdField.ID], &actualValue))
|
|
require.Equal(t, value, actualValue)
|
|
})
|
|
|
|
t.Run("should handle array values correctly", func(t *testing.T) {
|
|
optionsID := []string{model.NewId(), model.NewId(), model.NewId(), model.NewId()}
|
|
|
|
arrayField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeMultiselect,
|
|
Attrs: model.StringInterface{
|
|
"options": []map[string]any{
|
|
{"id": optionsID[0], "name": "option1"},
|
|
{"id": optionsID[1], "name": "option2"},
|
|
{"id": optionsID[2], "name": "option3"},
|
|
{"id": optionsID[3], "name": "option4"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdArrayField, appErr := th.App.CreateCPAField(arrayField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdArrayField)
|
|
|
|
values := map[string]json.RawMessage{
|
|
createdArrayField.ID: json.RawMessage(fmt.Sprintf(`["%s", "%s", "%s"]`, optionsID[0], optionsID[1], optionsID[2])),
|
|
}
|
|
patchedValues, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValues []string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdArrayField.ID], &actualValues))
|
|
require.Equal(t, optionsID[:3], actualValues)
|
|
|
|
// Test updating array values
|
|
values[createdArrayField.ID] = json.RawMessage(fmt.Sprintf(`["%s", "%s"]`, optionsID[2], optionsID[3]))
|
|
patchedValues, resp, err = th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
|
|
actualValues = nil
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdArrayField.ID], &actualValues))
|
|
require.Equal(t, optionsID[2:4], actualValues)
|
|
})
|
|
|
|
t.Run("should fail if any of the values belongs to a field that is LDAP/SAML synced", func(t *testing.T) {
|
|
// Create a field with LDAP attribute
|
|
ldapField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsLDAP: "ldap_attr",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdLDAPField, appErr := th.App.CreateCPAField(ldapField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdLDAPField)
|
|
|
|
// Create a field with SAML attribute
|
|
samlField, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsSAML: "saml_attr",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdSAMLField, appErr := th.App.CreateCPAField(samlField)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdSAMLField)
|
|
|
|
// Test LDAP field
|
|
values := map[string]json.RawMessage{
|
|
createdLDAPField.ID: json.RawMessage(`"LDAP Value"`),
|
|
}
|
|
_, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_synced.app_error")
|
|
|
|
// Test SAML field
|
|
values = map[string]json.RawMessage{
|
|
createdSAMLField.ID: json.RawMessage(`"SAML Value"`),
|
|
}
|
|
_, resp, err = th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_synced.app_error")
|
|
|
|
// Test multiple fields with one being LDAP synced
|
|
values = map[string]json.RawMessage{
|
|
createdField.ID: json.RawMessage(`"Regular Value"`),
|
|
createdLDAPField.ID: json.RawMessage(`"LDAP Value"`),
|
|
}
|
|
_, resp, err = th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_synced.app_error")
|
|
})
|
|
|
|
t.Run("an invalid patch should be rejected", func(t *testing.T) {
|
|
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
|
|
Name: model.NewId(),
|
|
Type: model.PropertyFieldTypeText,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createdField, appErr := th.App.CreateCPAField(field)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, createdField)
|
|
|
|
// Create a value that's too long (over 64 characters)
|
|
tooLongValue := strings.Repeat("a", model.CPAValueTypeTextMaxLength+1)
|
|
values := map[string]json.RawMessage{
|
|
createdField.ID: json.RawMessage(fmt.Sprintf(`"%s"`, tooLongValue)),
|
|
}
|
|
|
|
_, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckBadRequestStatus(t, resp)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Failed to validate property value")
|
|
})
|
|
|
|
t.Run("admin-managed fields", func(t *testing.T) {
|
|
// Create a managed field (only admins can create fields)
|
|
managedField := &model.PropertyField{
|
|
Name: "Managed Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
Attrs: model.StringInterface{
|
|
model.CustomProfileAttributesPropertyAttrsManaged: "admin",
|
|
},
|
|
}
|
|
|
|
createdManagedField, resp, err := th.SystemAdminClient.CreateCPAField(context.Background(), managedField)
|
|
CheckCreatedStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdManagedField)
|
|
|
|
// Create a non-managed field for comparison
|
|
regularField := &model.PropertyField{
|
|
Name: "Regular Field",
|
|
Type: model.PropertyFieldTypeText,
|
|
}
|
|
|
|
createdRegularField, resp, err := th.SystemAdminClient.CreateCPAField(context.Background(), regularField)
|
|
CheckCreatedStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, createdRegularField)
|
|
|
|
t.Run("regular user cannot update managed field", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Managed Value"`),
|
|
}
|
|
|
|
_, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_managed.app_error")
|
|
})
|
|
|
|
t.Run("regular user can update non-managed field", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdRegularField.ID: json.RawMessage(`"Regular Value"`),
|
|
}
|
|
|
|
patchedValues, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdRegularField.ID], &actualValue))
|
|
require.Equal(t, "Regular Value", actualValue)
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
// Set initial value through the app layer that we will be replacing during the test
|
|
_, appErr := th.App.PatchCPAValue(th.SystemAdminUser.Id, createdManagedField.ID, json.RawMessage(`"Initial Admin Value"`), true)
|
|
require.Nil(t, appErr)
|
|
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Admin Updated Value"`),
|
|
}
|
|
|
|
patchedValues, resp, err := client.PatchCPAValuesForUser(context.Background(), th.SystemAdminUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdManagedField.ID], &actualValue))
|
|
require.Equal(t, "Admin Updated Value", actualValue)
|
|
}, "system admin can update managed field")
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Admin Updated Managed Value For Other User"`),
|
|
}
|
|
|
|
patchedValues, resp, err := th.SystemAdminClient.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, patchedValues)
|
|
|
|
var actualValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdManagedField.ID], &actualValue))
|
|
require.Equal(t, "Admin Updated Managed Value For Other User", actualValue)
|
|
|
|
// Verify the value was actually set for the target user
|
|
userValues, resp, err := th.SystemAdminClient.ListCPAValues(context.Background(), th.BasicUser.Id)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, userValues)
|
|
|
|
var storedValue string
|
|
require.NoError(t, json.Unmarshal(userValues[createdManagedField.ID], &storedValue))
|
|
require.Equal(t, "Admin Updated Managed Value For Other User", storedValue)
|
|
}, "system admin can update managed field values for other users")
|
|
|
|
t.Run("a user should not be able to update other user's field values", func(t *testing.T) {
|
|
values := map[string]json.RawMessage{
|
|
createdRegularField.ID: json.RawMessage(`"Attempted Value For Other User"`),
|
|
}
|
|
|
|
// th.Client (BasicUser) trying to update th.BasicUser2's values should fail
|
|
_, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser2.Id, values)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "api.context.permissions.app_error")
|
|
})
|
|
|
|
t.Run("batch update with managed fields fails for regular user", func(t *testing.T) {
|
|
// First set some initial values to ensure we can verify they don't change
|
|
// Set initial values for both fields using th.App (admins can set managed field values)
|
|
_, appErr := th.App.PatchCPAValue(th.BasicUser.Id, createdRegularField.ID, json.RawMessage(`"Initial Regular Value"`), false)
|
|
require.Nil(t, appErr)
|
|
|
|
_, appErr = th.App.PatchCPAValue(th.BasicUser.Id, createdManagedField.ID, json.RawMessage(`"Initial Managed Value"`), true)
|
|
require.Nil(t, appErr)
|
|
|
|
// Try to batch update both managed and regular fields - this should fail
|
|
attemptedValues := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Managed Batch Value"`),
|
|
createdRegularField.ID: json.RawMessage(`"Regular Batch Value"`),
|
|
}
|
|
|
|
_, resp, err := th.Client.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, attemptedValues)
|
|
CheckForbiddenStatus(t, resp)
|
|
require.Error(t, err)
|
|
CheckErrorID(t, err, "app.custom_profile_attributes.property_field_is_managed.app_error")
|
|
|
|
// Verify that no values were updated when the batch operation failed
|
|
currentValues, appErr := th.App.ListCPAValues(th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
|
|
// Check that values remain unchanged - both fields should retain their initial values
|
|
regularFieldHasOriginalValue := false
|
|
managedFieldHasOriginalValue := false
|
|
|
|
for _, value := range currentValues {
|
|
if value.FieldID == createdManagedField.ID {
|
|
var currentValue string
|
|
require.NoError(t, json.Unmarshal(value.Value, ¤tValue))
|
|
if currentValue == "Initial Managed Value" {
|
|
managedFieldHasOriginalValue = true
|
|
}
|
|
// Verify it's not the attempted update value
|
|
require.NotEqual(t, "Managed Batch Value", currentValue, "Managed field should not have been updated in failed batch operation")
|
|
}
|
|
if value.FieldID == createdRegularField.ID {
|
|
var currentValue string
|
|
require.NoError(t, json.Unmarshal(value.Value, ¤tValue))
|
|
if currentValue == "Initial Regular Value" {
|
|
regularFieldHasOriginalValue = true
|
|
}
|
|
// Verify it's not the attempted update value
|
|
require.NotEqual(t, "Regular Batch Value", currentValue, "Regular field should not have been updated in failed batch operation")
|
|
}
|
|
}
|
|
|
|
// Both fields should retain their original values after the failed batch operation
|
|
require.True(t, regularFieldHasOriginalValue, "Regular field should retain its original value")
|
|
require.True(t, managedFieldHasOriginalValue, "Managed field should retain its original value")
|
|
})
|
|
|
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
|
values := map[string]json.RawMessage{
|
|
createdManagedField.ID: json.RawMessage(`"Admin Managed Batch"`),
|
|
createdRegularField.ID: json.RawMessage(`"Admin Regular Batch"`),
|
|
}
|
|
|
|
patchedValues, resp, err := th.SystemAdminClient.PatchCPAValuesForUser(context.Background(), th.BasicUser.Id, values)
|
|
CheckOKStatus(t, resp)
|
|
require.NoError(t, err)
|
|
require.Len(t, patchedValues, 2)
|
|
|
|
var managedValue, regularValue string
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdManagedField.ID], &managedValue))
|
|
require.NoError(t, json.Unmarshal(patchedValues[createdRegularField.ID], ®ularValue))
|
|
require.Equal(t, "Admin Managed Batch", managedValue)
|
|
require.Equal(t, "Admin Regular Batch", regularValue)
|
|
}, "batch update with managed fields succeeds for admin")
|
|
})
|
|
}
|