Files
upstream-mattermost/server/cmd/mmctl/commands/user_attributes_value_test.go
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

589 lines
15 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"context"
"encoding/json"
"errors"
"net/http"
"github.com/golang/mock/gomock"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
"github.com/spf13/cobra"
)
func (s *MmctlUnitTestSuite) TestCPAValueListCmd() {
s.Run("Should list all CPA values with plain text output format", func() {
printer.Clean()
printer.SetFormat(printer.FormatPlain)
mockUser := &model.User{
Id: "user123",
Username: "testuser",
}
fieldID1 := model.NewId()
fieldID2 := model.NewId()
mockFields := []*model.PropertyField{
{
ID: fieldID1,
Name: "Department",
Type: model.PropertyFieldTypeText,
},
{
ID: fieldID2,
Name: "Skills",
Type: model.PropertyFieldTypeMultiselect,
Attrs: model.StringInterface{
"options": []*model.CustomProfileAttributesSelectOption{
{ID: "opt1", Name: "Go"},
{ID: "opt2", Name: "React"},
{ID: "opt3", Name: "Python"},
},
},
},
}
mockValues := map[string]json.RawMessage{
fieldID1: json.RawMessage(`"Engineering"`),
fieldID2: json.RawMessage(`["opt1", "opt2", "opt3"]`),
}
s.client.
EXPECT().
ListCPAFields(context.TODO()).
Return(mockFields, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "testuser@example.com", "").
Return(mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
ListCPAValues(context.TODO(), "user123").
Return(mockValues, &model.Response{}, nil).
Times(1)
err := cpaValueListCmdF(s.client, &cobra.Command{}, []string{"testuser@example.com"})
s.Require().NoError(err)
lines := printer.GetLines()
s.Require().NotEmpty(lines)
// Check that we have human-readable output
found := false
for _, line := range lines {
if lineStr, ok := line.(string); ok {
if lineStr == "Department (text): Engineering" {
found = true
break
}
}
}
s.Require().True(found, "Should contain human-readable field name and value")
})
s.Run("Should output raw data structure when --json flag is used", func() {
printer.Clean()
printer.SetFormat(printer.FormatJSON)
mockUser := &model.User{
Id: "user123",
Username: "testuser",
}
fieldID1 := model.NewId()
mockFields := []*model.PropertyField{
{
ID: fieldID1,
Name: "Department",
Type: model.PropertyFieldTypeText,
},
}
mockValues := map[string]json.RawMessage{
fieldID1: json.RawMessage(`"Engineering"`),
}
s.client.
EXPECT().
ListCPAFields(context.TODO()).
Return(mockFields, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "testuser@example.com", "").
Return(mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
ListCPAValues(context.TODO(), "user123").
Return(mockValues, &model.Response{}, nil).
Times(1)
err := cpaValueListCmdF(s.client, &cobra.Command{}, []string{"testuser@example.com"})
s.Require().NoError(err)
lines := printer.GetLines()
s.Require().NotEmpty(lines)
// Check that JSON format outputs raw data structure
found := false
for _, line := range lines {
if lineMap, ok := line.(map[string]any); ok {
if val, exists := lineMap[fieldID1]; exists {
if rawVal, ok := val.(json.RawMessage); ok && string(rawVal) == `"Engineering"` {
found = true
break
}
}
}
}
s.Require().True(found, "JSON output should contain raw field ID and value")
})
s.Run("Should resolve multiselect option names correctly", func() {
printer.Clean()
printer.SetFormat(printer.FormatPlain)
mockUser := &model.User{
Id: "user123",
Username: "testuser",
}
fieldID := model.NewId()
mockFields := []*model.PropertyField{
{
ID: fieldID,
Name: "Skills",
Type: model.PropertyFieldTypeMultiselect,
Attrs: model.StringInterface{
"options": []*model.CustomProfileAttributesSelectOption{
{ID: "opt1", Name: "Go"},
{ID: "opt2", Name: "React"},
{ID: "opt3", Name: "Python"},
},
},
},
}
mockValues := map[string]json.RawMessage{
fieldID: json.RawMessage(`["opt1", "opt3"]`),
}
s.client.
EXPECT().
ListCPAFields(context.TODO()).
Return(mockFields, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "testuser@example.com", "").
Return(mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
ListCPAValues(context.TODO(), "user123").
Return(mockValues, &model.Response{}, nil).
Times(1)
err := cpaValueListCmdF(s.client, &cobra.Command{}, []string{"testuser@example.com"})
s.Require().NoError(err)
lines := printer.GetLines()
s.Require().NotEmpty(lines)
// Check that multiselect options are resolved to names
found := false
for _, line := range lines {
if lineStr, ok := line.(string); ok {
if lineStr == "Skills (multiselect): [Go, Python]" {
found = true
break
}
}
}
s.Require().True(found, "Should resolve multiselect option IDs to names")
})
s.Run("Should handle empty value list scenario", func() {
printer.Clean()
printer.SetFormat(printer.FormatPlain)
mockUser := &model.User{
Id: "user123",
Username: "testuser",
}
s.client.
EXPECT().
ListCPAFields(context.TODO()).
Return([]*model.PropertyField{}, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "testuser@example.com", "").
Return(mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
ListCPAValues(context.TODO(), "user123").
Return(map[string]json.RawMessage{}, &model.Response{}, nil).
Times(1)
err := cpaValueListCmdF(s.client, &cobra.Command{}, []string{"testuser@example.com"})
s.Require().NoError(err)
lines := printer.GetLines()
// When there are no values, no output should be produced
s.Require().Len(lines, 0)
})
s.Run("Should handle API error when ListCPAValues fails", func() {
printer.Clean()
mockUser := &model.User{
Id: "user123",
Username: "testuser",
}
expectedError := errors.New("API error")
s.client.
EXPECT().
ListCPAFields(context.TODO()).
Return([]*model.PropertyField{}, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "testuser@example.com", "").
Return(mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
ListCPAValues(context.TODO(), "user123").
Return(nil, &model.Response{}, expectedError).
Times(1)
err := cpaValueListCmdF(s.client, &cobra.Command{}, []string{"testuser@example.com"})
s.Require().Error(err)
s.Require().Contains(err.Error(), "failed to get CPA values for user")
s.Require().Contains(err.Error(), "API error")
})
s.Run("Should handle API error when ListCPAFields fails", func() {
printer.Clean()
expectedError := errors.New("fields API error")
s.client.
EXPECT().
ListCPAFields(context.TODO()).
Return(nil, &model.Response{}, expectedError).
Times(1)
err := cpaValueListCmdF(s.client, &cobra.Command{}, []string{"testuser@example.com"})
s.Require().Error(err)
s.Require().Contains(err.Error(), "failed to get CPA fields for template context")
s.Require().Contains(err.Error(), "fields API error")
})
s.Run("Should handle getUserFromArg error", func() {
printer.Clean()
notFoundError := errors.New("user not found")
notFoundResponse := &model.Response{StatusCode: http.StatusNotFound}
s.client.
EXPECT().
ListCPAFields(context.TODO()).
Return([]*model.PropertyField{}, &model.Response{}, nil).
Times(1)
// getUserFromArg tries email first, then username, then user ID
// All should return NotFoundError so it tries all methods
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "nonexistent@example.com", "").
Return(nil, notFoundResponse, notFoundError).
Times(1)
s.client.
EXPECT().
GetUserByUsername(context.TODO(), "nonexistent@example.com", "").
Return(nil, notFoundResponse, notFoundError).
Times(1)
s.client.
EXPECT().
GetUser(context.TODO(), "nonexistent@example.com", "").
Return(nil, notFoundResponse, notFoundError).
Times(1)
err := cpaValueListCmdF(s.client, &cobra.Command{}, []string{"nonexistent@example.com"})
s.Require().Error(err)
})
}
func (s *MmctlUnitTestSuite) TestCPAValueSetCmd() {
s.Run("Should successfully set single CPA value", func() {
printer.Clean()
printer.SetFormat(printer.FormatPlain)
mockUser := &model.User{
Id: "user123",
Username: "testuser",
}
fieldID := model.NewId()
mockFields := []*model.PropertyField{
{
ID: fieldID,
Name: "Department",
Type: model.PropertyFieldTypeText,
},
}
mockUpdatedValues := map[string]json.RawMessage{
fieldID: json.RawMessage(`"Engineering"`),
}
cmd := &cobra.Command{}
cmd.Flags().StringSlice("value", []string{"Engineering"}, "")
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "testuser@example.com", "").
Return(mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
ListCPAFields(context.TODO()).
Return(mockFields, &model.Response{}, nil).
Times(2)
s.client.
EXPECT().
PatchCPAValuesForUser(context.TODO(), "user123", gomock.Any()).
Return(mockUpdatedValues, &model.Response{}, nil).
Times(1)
err := cpaValueSetCmdF(s.client, cmd, []string{"testuser@example.com", fieldID})
s.Require().NoError(err)
})
s.Run("Should successfully set multiple CPA values", func() {
printer.Clean()
printer.SetFormat(printer.FormatPlain)
mockUser := &model.User{
Id: "user123",
Username: "testuser",
}
fieldID := model.NewId()
mockFields := []*model.PropertyField{
{
ID: fieldID,
Name: "Skills",
Type: model.PropertyFieldTypeMultiselect,
},
}
mockUpdatedValues := map[string]json.RawMessage{
fieldID: json.RawMessage(`["Go", "React", "Python"]`),
}
cmd := &cobra.Command{}
cmd.Flags().StringSlice("value", []string{"Go", "React", "Python"}, "")
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "testuser@example.com", "").
Return(mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
ListCPAFields(context.TODO()).
Return(mockFields, &model.Response{}, nil).
Times(2)
s.client.
EXPECT().
PatchCPAValuesForUser(context.TODO(), "user123", gomock.Any()).
Return(mockUpdatedValues, &model.Response{}, nil).
Times(1)
err := cpaValueSetCmdF(s.client, cmd, []string{"testuser@example.com", fieldID})
s.Require().NoError(err)
})
s.Run("Should handle field not found error", func() {
printer.Clean()
mockUser := &model.User{
Id: "user123",
Username: "testuser",
}
mockFields := []*model.PropertyField{
{
ID: "different_field",
Name: "Department",
Type: model.PropertyFieldTypeText,
},
}
cmd := &cobra.Command{}
cmd.Flags().StringSlice("value", []string{"Engineering"}, "")
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "testuser@example.com", "").
Return(mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
ListCPAFields(context.TODO()).
Return(mockFields, &model.Response{}, nil).
Times(1)
err := cpaValueSetCmdF(s.client, cmd, []string{"testuser@example.com", "nonexistent_field"})
s.Require().Error(err)
s.Require().Contains(err.Error(), "failed to get field for \"nonexistent_field\"")
})
s.Run("Should handle API error when PatchCPAValuesForUser fails", func() {
printer.Clean()
mockUser := &model.User{
Id: "user123",
Username: "testuser",
}
fieldID := model.NewId()
mockFields := []*model.PropertyField{
{
ID: fieldID,
Name: "Department",
Type: model.PropertyFieldTypeText,
},
}
expectedError := errors.New("permission denied")
cmd := &cobra.Command{}
cmd.Flags().StringSlice("value", []string{"Engineering"}, "")
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "testuser@example.com", "").
Return(mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
ListCPAFields(context.TODO()).
Return(mockFields, &model.Response{}, nil).
Times(2)
s.client.
EXPECT().
PatchCPAValuesForUser(context.TODO(), "user123", gomock.Any()).
Return(nil, &model.Response{}, expectedError).
Times(1)
err := cpaValueSetCmdF(s.client, cmd, []string{"testuser@example.com", fieldID})
s.Require().Error(err)
s.Require().Contains(err.Error(), "failed to set CPA value")
s.Require().Contains(err.Error(), "permission denied")
})
s.Run("Should handle ListCPAFields API error", func() {
printer.Clean()
mockUser := &model.User{
Id: "user123",
Username: "testuser",
}
expectedError := errors.New("fields API error")
cmd := &cobra.Command{}
cmd.Flags().StringSlice("value", []string{"Engineering"}, "")
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "testuser@example.com", "").
Return(mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
ListCPAFields(context.TODO()).
Return(nil, &model.Response{}, expectedError).
Times(1)
err := cpaValueSetCmdF(s.client, cmd, []string{"testuser@example.com", "field123"})
s.Require().Error(err)
s.Require().Contains(err.Error(), "failed to get CPA fields")
s.Require().Contains(err.Error(), "fields API error")
})
}
// TestResolveOptionNamesToIDs covers the assignment side of rank-typed
// attribute resolution: when an admin sets a rank value by option name, the
// name is resolved to the stored option ID. Rank fields share the single-value
// resolution path with select fields.
func (s *MmctlUnitTestSuite) TestResolveOptionNamesToIDs() {
secretID := model.NewId()
topSecretID := model.NewId()
rankField := &model.PropertyField{
ID: model.NewId(),
Name: "clearance",
Type: model.PropertyFieldTypeRank,
Attrs: model.StringInterface{
model.PropertyFieldAttributeOptions: []any{
map[string]any{"id": secretID, "name": "SECRET", "rank": 2},
map[string]any{"id": topSecretID, "name": "TOP SECRET", "rank": 3},
},
},
}
s.Run("resolves an option name to its ID for assignment", func() {
resolved, err := resolveOptionNamesToIDs(rankField, []string{"TOP SECRET"})
s.Require().NoError(err)
s.Equal([]string{topSecretID}, resolved)
})
s.Run("passes through a value that is already an option ID", func() {
resolved, err := resolveOptionNamesToIDs(rankField, []string{secretID})
s.Require().NoError(err)
s.Equal([]string{secretID}, resolved)
})
s.Run("passes through an unknown option name unchanged", func() {
resolved, err := resolveOptionNamesToIDs(rankField, []string{"UNKNOWN"})
s.Require().NoError(err)
s.Equal([]string{"UNKNOWN"}, resolved)
})
}