mattermost/server/channels/api4/custom_profile_attributes_test.go
Miguel de la Cruz 58dd9e1bb4
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-external-links (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
Add property system app layer architecture (#35157)
* Refactor property system with app layer routing and access control separation

Establish the app layer as the primary entry point for property operations
with intelligent routing based on group type. This architecture separates
access-controlled operations (CPA groups) from standard operations,
improving performance and code clarity.

Architecture Changes:
- App layer now routes operations based on group type:
  - CPA groups -> PropertyAccessService (enforces access control)
  - Non-CPA groups -> PropertyService (direct, no access control)
- PropertyAccessService simplified to handle only CPA operations
- Eliminated redundant group type checks throughout the codebase

* Move access control routing into PropertyService

This change makes the PropertyService the main entrypoint for property
related operations, and adds a routing mechanism to decide if extra
behaviors or checks should run for each operation, in this case, the
property access service logic.

To add specific payloads that pluggable checks and operations may
need, we use the request context. When the request comes from the API,
the endpoints are in charge of adding the caller ID to the payload,
and in the case of the plugin API, on receiving a request, the server
automatically tags the context with the plugin ID so the property
service can react accordingly.

Finally, the new design enforces all these checks migrating the actual
property logic to internal, non-exposed methods, so any caller from
the App layer needs to go through the service checks that decide if
pluggable logic is needed, avoiding any possibility of a bypass.

* Fix i18n

* Fix bad error string

* Added nil guards to property methods

* Add check for multiple group IDs on value operations

* Add nil guard to the plugin checker

* Fix build error

* Update value tests

* Fix linter

* Adds early return when content flaggin a thread with no replies

* Fix mocks

* Clean the state of plugin property tests before each run

* Do not wrap appErr on API response and fix i18n

* Fix create property field test

* Remove the need to cache cpaGroupID as part of the property service

* Split the property.go file into multiple

* Not found group doesn't bypass access control check

* Unexport SetPluginCheckerForTests

* Rename plugin context getter to be more PSA specific

---------

Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
2026-03-26 07:54:50 +00:00

1275 lines
48 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/mattermost/mattermost/server/public/shared/request"
"github.com/stretchr/testify/require"
)
func TestCreateCPAField(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
})
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
})
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(request.TestContext(t), 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(request.TestContext(t), 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
})
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(request.TestContext(t), 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(request.TestContext(t), 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(request.TestContext(t), field)
require.Nil(t, appErr)
require.NotNil(t, createdField)
// Verify field is not isManaged initially
require.Empty(t, createdField.Attrs.Managed)
// 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
})
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(request.TestContext(t), 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(t)
th.RemovePermissionFromRole(t, model.PermissionViewMembers.Id, model.SystemUserRoleId)
defer th.AddPermissionToRole(t, 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(request.TestContext(t), field)
require.Nil(t, appErr)
require.NotNil(t, createdField)
_, appErr = th.App.PatchCPAValue(request.TestContext(t), 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)
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(request.TestContext(t), arrayField)
require.Nil(t, appErr)
require.NotNil(t, createdArrayField)
_, appErr = th.App.PatchCPAValue(request.TestContext(t), 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(t)
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
Name: model.NewId(),
Type: model.PropertyFieldTypeText,
})
require.NoError(t, err)
createdField, appErr := th.App.CreateCPAField(request.TestContext(t), 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(request.TestContext(t), 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(request.TestContext(t), 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(request.TestContext(t), 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(request.TestContext(t), 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(request.TestContext(t), th.BasicUser.Id, createdRegularField.ID, json.RawMessage(`"Initial Regular Value"`), false)
require.Nil(t, appErr)
_, appErr = th.App.PatchCPAValue(request.TestContext(t), 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(request.TestContext(t), 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, &currentValue))
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, &currentValue))
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], &regularValue))
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(t)
field, err := model.NewCPAFieldFromPropertyField(&model.PropertyField{
Name: model.NewId(),
Type: model.PropertyFieldTypeText,
})
require.NoError(t, err)
createdField, appErr := th.App.CreateCPAField(request.TestContext(t), 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(request.TestContext(t), 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(request.TestContext(t), 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(request.TestContext(t), 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(request.TestContext(t), 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(request.TestContext(t), 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(request.TestContext(t), th.BasicUser.Id, createdRegularField.ID, json.RawMessage(`"Initial Regular Value"`), false)
require.Nil(t, appErr)
_, appErr = th.App.PatchCPAValue(request.TestContext(t), 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(request.TestContext(t), 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, &currentValue))
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, &currentValue))
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], &regularValue))
require.Equal(t, "Admin Managed Batch", managedValue)
require.Equal(t, "Admin Regular Batch", regularValue)
}, "batch update with managed fields succeeds for admin")
})
}