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

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

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

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/hanzoteam/server/server/public/model"
"github.com/hanzoteam/server/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(), "[email protected]", "").
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{"[email protected]"})
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(), "[email protected]", "").
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{"[email protected]"})
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(), "[email protected]", "").
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{"[email protected]"})
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(), "[email protected]", "").
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{"[email protected]"})
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(), "[email protected]", "").
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{"[email protected]"})
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{"[email protected]"})
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(), "[email protected]", "").
Return(nil, notFoundResponse, notFoundError).
Times(1)
s.client.
EXPECT().
GetUserByUsername(context.TODO(), "[email protected]", "").
Return(nil, notFoundResponse, notFoundError).
Times(1)
s.client.
EXPECT().
GetUser(context.TODO(), "[email protected]", "").
Return(nil, notFoundResponse, notFoundError).
Times(1)
err := cpaValueListCmdF(s.client, &cobra.Command{}, []string{"[email protected]"})
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(), "[email protected]", "").
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{"[email protected]", 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(), "[email protected]", "").
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{"[email protected]", 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(), "[email protected]", "").
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{"[email protected]", "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(), "[email protected]", "").
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{"[email protected]", 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(), "[email protected]", "").
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{"[email protected]", "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)
})
}