[MM-69598] Add mmctl user status command to get and set a user's status (#37312)

* Add mmctl user status get/set commands

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Register local-mode status routes and add mmctl status tests

Expose GET/PUT /users/{user_id}/status on the local socket API so
mmctl --local can read and set statuses, and add unit + e2e coverage
for the new user status commands.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Improve mmctl status tests and validate dnd-end-time earlier

Validate --dnd-end-time before resolving the target user, add command
wiring/flag coverage, a GetMe failure case, and stronger e2e assertions
from the test-quality review.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Regenerate mmctl docs for user status command

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Accept RFC3339 UTC timestamps for mmctl status --dnd-end-time

Use time.RFC3339 parsing so --dnd-end-time accepts Z-suffixed UTC
timestamps in addition to numeric offsets, per CodeRabbit review feedback.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Show @username in mmctl user status output

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
This commit is contained in:
cursor[bot]
2026-07-02 12:38:58 +02:00
committed by GitHub
co-authored by mattermost-code Cursor Agent
parent 97750a8641
commit 3759dd5031
10 changed files with 826 additions and 0 deletions
+1
View File
@@ -516,6 +516,7 @@ func InitLocal(srv *app.Server) *API {
api.InitSamlLocal()
api.InitCustomProfileAttributesLocal()
api.InitAccessControlPolicyLocal()
api.InitStatusLocal()
srv.LocalRouter.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
+11
View File
@@ -0,0 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import "net/http"
func (api *API) InitStatusLocal() {
api.BaseRoutes.User.Handle("/status", api.APILocal(getUserStatus)).Methods(http.MethodGet)
api.BaseRoutes.User.Handle("/status", api.APILocal(updateUserStatus)).Methods(http.MethodPut)
}
+2
View File
@@ -91,6 +91,8 @@ type Client interface {
ConvertBotToUser(ctx context.Context, userID string, userPatch *model.UserPatch, setSystemAdmin bool) (*model.User, *model.Response, error)
PromoteGuestToUser(ctx context.Context, userID string) (*model.Response, error)
DemoteUserToGuest(ctx context.Context, guestID string) (*model.Response, error)
GetUserStatus(ctx context.Context, userID, etag string) (*model.Status, *model.Response, error)
UpdateUserStatus(ctx context.Context, userID string, userStatus *model.Status) (*model.Status, *model.Response, error)
CreateCommand(ctx context.Context, cmd *model.Command) (*model.Command, *model.Response, error)
ListCommands(ctx context.Context, teamID string, customOnly bool) ([]*model.Command, *model.Response, error)
GetCommandById(ctx context.Context, cmdID string) (*model.Command, *model.Response, error)
+127
View File
@@ -12,6 +12,7 @@ import (
"os"
"sort"
"testing"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/stretchr/testify/require"
@@ -22,6 +23,7 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var UserCmd = &cobra.Command{
@@ -211,6 +213,38 @@ var DemoteUserToGuestCmd = &cobra.Command{
Args: cobra.MinimumNArgs(1),
}
var UserStatusCmd = &cobra.Command{
Use: "status",
Short: "Get a user's status",
Long: "Get a user's presence status: online, away, dnd or offline.",
Example: ` # You can get the status of the currently authenticated user
$ mmctl user status
# You can get the status of a specific user
$ mmctl user status --user user@example.com
# In local mode there is no authenticated user, so the --user flag is required
$ mmctl --local user status --user user@example.com`,
Args: cobra.NoArgs,
RunE: withClient(userStatusGetCmdF),
}
var UserStatusSetCmd = &cobra.Command{
Use: "set [status]",
Short: "Set a user's status",
Long: "Set a user's presence status. Allowed values are online, away, dnd and offline.",
Example: ` # You can set the status of the currently authenticated user
$ mmctl user status set away
# You can set the status of a specific user
$ mmctl user status set --user user@example.com dnd
# You can set a "dnd" status that expires at a given time (ISO 8601)
$ mmctl user status set --user user@example.com --dnd-end-time 2100-01-02T15:04:05-07:00 dnd`,
Args: cobra.ExactArgs(1),
RunE: withClient(userStatusSetCmdF),
}
var UserConvertCmd = &cobra.Command{
Use: "convert (--bot [emails] [usernames] [userIds] | --user <username> --password PASSWORD [--email EMAIL])",
Short: "Convert users to bots, or a bot to a user",
@@ -368,6 +402,10 @@ func init() {
UserConvertCmd.Flags().String("locale", "", "The locale (ex: en, fr) for converted new user account. Required when the \"bot\" flag is set")
UserConvertCmd.Flags().Bool("system-admin", false, "If supplied, the converted user will be a system administrator. Defaults to false. Required when the \"bot\" flag is set")
UserStatusCmd.Flags().String("user", "", "Optional. The user (specified by email, username or ID) whose status to get. Defaults to the currently authenticated user. Required in local mode.")
UserStatusSetCmd.Flags().String("user", "", "Optional. The user (specified by email, username or ID) whose status to set. Defaults to the currently authenticated user. Required in local mode.")
UserStatusSetCmd.Flags().String("dnd-end-time", "", "Optional. The time at which a \"dnd\" status expires, formatted as ISO 8601 (e.g. 2006-01-02T15:04:05-07:00). Only valid with the \"dnd\" status.")
ChangePasswordUserCmd.Flags().StringP("current", "c", "", "The current password of the user. Use only if changing your own password")
ChangePasswordUserCmd.Flags().StringP("password", "p", "", "The new password for the user")
ChangePasswordUserCmd.Flags().Bool("hashed", false, "The supplied password is already hashed")
@@ -423,8 +461,12 @@ Global Flags:
MigrateAuthCmd,
PromoteGuestToUserCmd,
DemoteUserToGuestCmd,
UserStatusCmd,
PreferenceCmd,
)
UserStatusCmd.AddCommand(
UserStatusSetCmd,
)
PreferenceCmd.AddCommand(
PreferenceListCmd,
PreferenceGetCmd,
@@ -1073,6 +1115,91 @@ func demoteUserToGuestCmdF(c client.Client, _ *cobra.Command, userArgs []string)
return errs.ErrorOrNil()
}
// resolveStatusTargetUser resolves the user whose status a command operates on.
// When the --user flag is omitted, it falls back to the currently authenticated
// user, which is unavailable in local mode.
func resolveStatusTargetUser(c client.Client, cmd *cobra.Command) (*model.User, error) {
userArg, _ := cmd.Flags().GetString("user")
if userArg != "" {
return getUserFromArg(c, userArg)
}
if viper.GetBool("local") {
return nil, errors.New("the --user flag is required in local mode")
}
me, _, err := c.GetMe(context.TODO(), "")
if err != nil {
return nil, fmt.Errorf("could not retrieve the current user: %w", err)
}
return me, nil
}
func userStatusGetCmdF(c client.Client, cmd *cobra.Command, _ []string) error {
printer.SetSingle(true)
user, err := resolveStatusTargetUser(c, cmd)
if err != nil {
return err
}
status, _, err := c.GetUserStatus(context.TODO(), user.Id, "")
if err != nil {
return fmt.Errorf("could not get status for user %s: %w", user.Id, err)
}
printer.PrintT("@"+user.Username+" has status: {{.Status}}", status)
return nil
}
func userStatusSetCmdF(c client.Client, cmd *cobra.Command, args []string) error {
printer.SetSingle(true)
newStatus := args[0]
switch newStatus {
case model.StatusOnline, model.StatusAway, model.StatusDnd, model.StatusOffline:
default:
return fmt.Errorf("invalid status %q, must be one of: %s, %s, %s, %s", newStatus, model.StatusOnline, model.StatusAway, model.StatusDnd, model.StatusOffline)
}
dndEndTimeArg, _ := cmd.Flags().GetString("dnd-end-time")
if dndEndTimeArg != "" && newStatus != model.StatusDnd {
return fmt.Errorf("the --dnd-end-time flag can only be used with the %q status", model.StatusDnd)
}
var dndEndTime int64
if dndEndTimeArg != "" {
endTime, err := time.Parse(time.RFC3339, dndEndTimeArg)
if err != nil {
return fmt.Errorf("invalid dnd-end-time %q, expected RFC3339 format (e.g. 2006-01-02T15:04:05-07:00 or 2006-01-02T15:04:05Z)", dndEndTimeArg)
}
if !endTime.After(time.Now()) {
return errors.New("dnd-end-time must be in the future")
}
// DNDEndTime is expressed in seconds rather than milliseconds.
dndEndTime = endTime.Unix()
}
user, err := resolveStatusTargetUser(c, cmd)
if err != nil {
return err
}
status := &model.Status{
UserId: user.Id,
Status: newStatus,
DNDEndTime: dndEndTime,
}
updatedStatus, _, err := c.UpdateUserStatus(context.TODO(), user.Id, status)
if err != nil {
return fmt.Errorf("could not set status for user %s: %w", user.Id, err)
}
printer.PrintT("Set status of @"+user.Username+" to status: {{.Status}}", updatedStatus)
return nil
}
func userEditCompletionF(ctx context.Context, c client.Client, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) >= 1 {
return nil, cobra.ShellCompDirectiveNoFileComp
+171
View File
@@ -7,6 +7,7 @@ import (
"context"
"fmt"
"net/http"
"time"
"github.com/hashicorp/go-multierror"
"github.com/mattermost/mattermost/server/public/model"
@@ -1812,3 +1813,173 @@ func (s *MmctlE2ETestSuite) TestUserEditAuthdataCmd() {
s.Require().Equal(newAuthdata, *updatedUser.AuthData)
})
}
func (s *MmctlE2ETestSuite) TestUserStatusGetCmd() {
s.SetupTestHelper().InitBasic(s.T())
s.RunForSystemAdminAndLocal("Get the status of a specific user", func(c client.Client) {
printer.Clean()
s.th.App.SaveAndBroadcastStatus(&model.Status{UserId: s.th.BasicUser.Id, Status: model.StatusOnline, Manual: true})
cmd := newUserStatusCmd()
s.Require().NoError(cmd.Flags().Set("user", s.th.BasicUser.Email))
err := userStatusGetCmdF(c, cmd, []string{})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 1)
status, ok := printer.GetLines()[0].(*model.Status)
s.Require().True(ok)
s.Require().Equal(s.th.BasicUser.Id, status.UserId)
s.Require().Equal(model.StatusOnline, status.Status)
})
s.Run("Get the status of the authenticated user when --user is omitted", func() {
printer.Clean()
s.th.App.SaveAndBroadcastStatus(&model.Status{UserId: s.th.SystemAdminUser.Id, Status: model.StatusOnline, Manual: true})
err := userStatusGetCmdF(s.th.SystemAdminClient, newUserStatusCmd(), []string{})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 1)
status, ok := printer.GetLines()[0].(*model.Status)
s.Require().True(ok)
s.Require().Equal(s.th.SystemAdminUser.Id, status.UserId)
s.Require().Equal(model.StatusOnline, status.Status)
})
s.Run("A regular user can read another user's status", func() {
printer.Clean()
s.th.App.SaveAndBroadcastStatus(&model.Status{UserId: s.th.BasicUser2.Id, Status: model.StatusOnline, Manual: true})
cmd := newUserStatusCmd()
s.Require().NoError(cmd.Flags().Set("user", s.th.BasicUser2.Email))
err := userStatusGetCmdF(s.th.Client, cmd, []string{})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 1)
status, ok := printer.GetLines()[0].(*model.Status)
s.Require().True(ok)
s.Require().Equal(s.th.BasicUser2.Id, status.UserId)
s.Require().Equal(model.StatusOnline, status.Status)
})
s.RunForAllClients("Get the status of a nonexistent user", func(c client.Client) {
printer.Clean()
cmd := newUserStatusCmd()
s.Require().NoError(cmd.Flags().Set("user", "nonexistent@example.com"))
err := userStatusGetCmdF(c, cmd, []string{})
s.Require().EqualError(err, "user nonexistent@example.com not found")
s.Require().Len(printer.GetLines(), 0)
})
}
func (s *MmctlE2ETestSuite) TestUserStatusSetCmd() {
s.SetupTestHelper().InitBasic(s.T())
s.RunForSystemAdminAndLocal("Set the status of a specific user", func(c client.Client) {
printer.Clean()
cmd := newUserStatusSetCmd()
s.Require().NoError(cmd.Flags().Set("user", s.th.BasicUser.Email))
err := userStatusSetCmdF(c, cmd, []string{model.StatusDnd})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 1)
status, ok := printer.GetLines()[0].(*model.Status)
s.Require().True(ok)
s.Require().Equal(model.StatusDnd, status.Status)
stored, appErr := s.th.App.GetStatus(s.th.BasicUser.Id)
s.Require().Nil(appErr)
s.Require().Equal(model.StatusDnd, stored.Status)
})
s.RunForSystemAdminAndLocal("Set a dnd status with an end time", func(c client.Client) {
printer.Clean()
endTimeArg := time.Now().Add(2 * time.Hour).Format(ISO8601Layout)
endTime, err := time.Parse(ISO8601Layout, endTimeArg)
s.Require().NoError(err)
cmd := newUserStatusSetCmd()
s.Require().NoError(cmd.Flags().Set("user", s.th.BasicUser.Email))
s.Require().NoError(cmd.Flags().Set("dnd-end-time", endTimeArg))
err = userStatusSetCmdF(c, cmd, []string{model.StatusDnd})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 1)
stored, appErr := s.th.App.GetStatus(s.th.BasicUser.Id)
s.Require().Nil(appErr)
s.Require().Equal(model.StatusDnd, stored.Status)
// The server truncates the DND end time to the minute to align with the expiry job.
s.Require().Equal(endTime.Truncate(model.DNDExpiryInterval).Unix(), stored.DNDEndTime)
})
s.Run("Set the status of the authenticated user when --user is omitted", func() {
printer.Clean()
err := userStatusSetCmdF(s.th.SystemAdminClient, newUserStatusSetCmd(), []string{model.StatusAway})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 1)
stored, appErr := s.th.App.GetStatus(s.th.SystemAdminUser.Id)
s.Require().Nil(appErr)
s.Require().Equal(model.StatusAway, stored.Status)
})
s.Run("A regular user can set their own status when --user is omitted", func() {
printer.Clean()
// Seed a distinct starting status so the read-back proves the write happened
// rather than passing on leftover state from earlier subtests.
s.th.App.SaveAndBroadcastStatus(&model.Status{UserId: s.th.BasicUser.Id, Status: model.StatusDnd, Manual: true})
err := userStatusSetCmdF(s.th.Client, newUserStatusSetCmd(), []string{model.StatusAway})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 1)
status, ok := printer.GetLines()[0].(*model.Status)
s.Require().True(ok)
s.Require().Equal(model.StatusAway, status.Status)
stored, appErr := s.th.App.GetStatus(s.th.BasicUser.Id)
s.Require().Nil(appErr)
s.Require().Equal(model.StatusAway, stored.Status)
})
s.Run("A regular user cannot set another user's status", func() {
printer.Clean()
cmd := newUserStatusSetCmd()
s.Require().NoError(cmd.Flags().Set("user", s.th.SystemAdminUser.Email))
err := userStatusSetCmdF(s.th.Client, cmd, []string{model.StatusOnline})
s.Require().Error(err)
s.CheckErrorID(err, "api.context.permissions.app_error")
s.Require().Len(printer.GetLines(), 0)
})
s.Run("Reject an invalid status value", func() {
printer.Clean()
err := userStatusSetCmdF(s.th.SystemAdminClient, newUserStatusSetCmd(), []string{"busy"})
s.Require().EqualError(err, "invalid status \"busy\", must be one of: online, away, dnd, offline")
s.Require().Len(printer.GetLines(), 0)
})
}
+361
View File
@@ -10,6 +10,7 @@ import (
"net/http"
"os"
"strings"
"time"
"github.com/mattermost/mattermost/server/public/model"
@@ -18,6 +19,7 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func (s *MmctlUnitTestSuite) TestUserActivateCmd() {
@@ -3140,3 +3142,362 @@ func (s *MmctlUnitTestSuite) TestUserEditAuthdataCmd() {
s.Require().EqualError(err, "failed to update user authdata: API error")
})
}
func newUserStatusCmd() *cobra.Command {
cmd := &cobra.Command{}
cmd.Flags().String("user", "", "")
return cmd
}
func newUserStatusSetCmd() *cobra.Command {
cmd := newUserStatusCmd()
cmd.Flags().String("dnd-end-time", "", "")
return cmd
}
func (s *MmctlUnitTestSuite) TestUserStatusCmdWiring() {
s.Run("status command is registered under user with a set subcommand", func() {
s.Require().True(UserCmd.HasSubCommands())
s.Require().Contains(UserCmd.Commands(), UserStatusCmd)
s.Require().Contains(UserStatusCmd.Commands(), UserStatusSetCmd)
})
s.Run("the expected flags are registered", func() {
s.Require().NotNil(UserStatusCmd.Flags().Lookup("user"))
s.Require().NotNil(UserStatusSetCmd.Flags().Lookup("user"))
s.Require().NotNil(UserStatusSetCmd.Flags().Lookup("dnd-end-time"))
})
s.Run("argument contracts are enforced", func() {
s.Require().NoError(UserStatusCmd.Args(UserStatusCmd, []string{}))
s.Require().Error(UserStatusCmd.Args(UserStatusCmd, []string{"online"}))
s.Require().NoError(UserStatusSetCmd.Args(UserStatusSetCmd, []string{"online"}))
s.Require().Error(UserStatusSetCmd.Args(UserStatusSetCmd, []string{}))
})
}
func (s *MmctlUnitTestSuite) TestUserStatusGetCmd() {
s.Run("Get status of the current user when --user is omitted", func() {
printer.Clean()
mockUser := model.User{Id: "me-id", Username: "me"}
mockStatus := &model.Status{UserId: "me-id", Status: model.StatusOnline}
s.client.
EXPECT().
GetMe(context.TODO(), "").
Return(&mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
GetUserStatus(context.TODO(), "me-id", "").
Return(mockStatus, &model.Response{}, nil).
Times(1)
err := userStatusGetCmdF(s.client, newUserStatusCmd(), []string{})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 1)
s.Require().Equal(mockStatus, printer.GetLines()[0])
})
s.Run("Get status of a specific user via --user", func() {
printer.Clean()
cmd := newUserStatusCmd()
err := cmd.Flags().Set("user", "target@example.com")
s.Require().NoError(err)
mockUser := model.User{Id: "target-id", Username: "target", Email: "target@example.com"}
mockStatus := &model.Status{UserId: "target-id", Status: model.StatusDnd}
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "target@example.com", "").
Return(&mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
GetUserStatus(context.TODO(), "target-id", "").
Return(mockStatus, &model.Response{}, nil).
Times(1)
err = userStatusGetCmdF(s.client, cmd, []string{})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 1)
s.Require().Equal(mockStatus, printer.GetLines()[0])
})
s.Run("Require --user in local mode", func() {
printer.Clean()
prevLocal := viper.GetBool("local")
viper.Set("local", true)
defer viper.Set("local", prevLocal)
err := userStatusGetCmdF(s.client, newUserStatusCmd(), []string{})
s.Require().EqualError(err, "the --user flag is required in local mode")
s.Require().Len(printer.GetLines(), 0)
})
s.Run("Return error when status retrieval fails", func() {
printer.Clean()
mockUser := model.User{Id: "me-id", Username: "me"}
s.client.
EXPECT().
GetMe(context.TODO(), "").
Return(&mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
GetUserStatus(context.TODO(), "me-id", "").
Return(nil, &model.Response{}, errors.New("status error")).
Times(1)
err := userStatusGetCmdF(s.client, newUserStatusCmd(), []string{})
s.Require().EqualError(err, "could not get status for user me-id: status error")
s.Require().Len(printer.GetLines(), 0)
})
s.Run("Return error when user cannot be found", func() {
printer.Clean()
cmd := newUserStatusCmd()
err := cmd.Flags().Set("user", "ghost")
s.Require().NoError(err)
s.client.
EXPECT().
GetUserByUsername(context.TODO(), "ghost", "").
Return(nil, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
GetUser(context.TODO(), "ghost", "").
Return(nil, &model.Response{}, nil).
Times(1)
err = userStatusGetCmdF(s.client, cmd, []string{})
s.Require().EqualError(err, "user ghost not found")
s.Require().Len(printer.GetLines(), 0)
})
}
func (s *MmctlUnitTestSuite) TestUserStatusSetCmd() {
s.Run("Set status of the current user when --user is omitted", func() {
printer.Clean()
mockUser := model.User{Id: "me-id", Username: "me"}
wantStatus := &model.Status{UserId: "me-id", Status: model.StatusAway}
returnedStatus := &model.Status{UserId: "me-id", Status: model.StatusAway, Manual: true}
s.client.
EXPECT().
GetMe(context.TODO(), "").
Return(&mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
UpdateUserStatus(context.TODO(), "me-id", wantStatus).
Return(returnedStatus, &model.Response{}, nil).
Times(1)
err := userStatusSetCmdF(s.client, newUserStatusSetCmd(), []string{model.StatusAway})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 1)
s.Require().Equal(returnedStatus, printer.GetLines()[0])
})
s.Run("Set status of a specific user via --user", func() {
printer.Clean()
cmd := newUserStatusSetCmd()
err := cmd.Flags().Set("user", "target@example.com")
s.Require().NoError(err)
mockUser := model.User{Id: "target-id", Username: "target", Email: "target@example.com"}
wantStatus := &model.Status{UserId: "target-id", Status: model.StatusOffline}
s.client.
EXPECT().
GetUserByEmail(context.TODO(), "target@example.com", "").
Return(&mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
UpdateUserStatus(context.TODO(), "target-id", wantStatus).
Return(wantStatus, &model.Response{}, nil).
Times(1)
err = userStatusSetCmdF(s.client, cmd, []string{model.StatusOffline})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 1)
s.Require().Equal(wantStatus, printer.GetLines()[0])
})
s.Run("Set dnd status with an end time", func() {
printer.Clean()
endTimeArg := time.Now().Add(24 * time.Hour).Format(ISO8601Layout)
endTime, err := time.Parse(ISO8601Layout, endTimeArg)
s.Require().NoError(err)
cmd := newUserStatusSetCmd()
err = cmd.Flags().Set("dnd-end-time", endTimeArg)
s.Require().NoError(err)
mockUser := model.User{Id: "me-id", Username: "me"}
wantStatus := &model.Status{UserId: "me-id", Status: model.StatusDnd, DNDEndTime: endTime.Unix()}
s.client.
EXPECT().
GetMe(context.TODO(), "").
Return(&mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
UpdateUserStatus(context.TODO(), "me-id", wantStatus).
Return(wantStatus, &model.Response{}, nil).
Times(1)
err = userStatusSetCmdF(s.client, cmd, []string{model.StatusDnd})
s.Require().NoError(err)
s.Require().Len(printer.GetErrorLines(), 0)
s.Require().Len(printer.GetLines(), 1)
s.Require().Equal(wantStatus, printer.GetLines()[0])
})
s.Run("Reject an invalid status value", func() {
printer.Clean()
err := userStatusSetCmdF(s.client, newUserStatusSetCmd(), []string{"busy"})
s.Require().EqualError(err, "invalid status \"busy\", must be one of: online, away, dnd, offline")
s.Require().Len(printer.GetLines(), 0)
})
s.Run("Reject --dnd-end-time with a non-dnd status", func() {
printer.Clean()
cmd := newUserStatusSetCmd()
err := cmd.Flags().Set("dnd-end-time", time.Now().Add(time.Hour).Format(ISO8601Layout))
s.Require().NoError(err)
err = userStatusSetCmdF(s.client, cmd, []string{model.StatusAway})
s.Require().EqualError(err, "the --dnd-end-time flag can only be used with the \"dnd\" status")
s.Require().Len(printer.GetLines(), 0)
})
s.Run("Set dnd status with a UTC end time", func() {
printer.Clean()
endTime := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second)
endTimeArg := endTime.Format(time.RFC3339)
cmd := newUserStatusSetCmd()
err := cmd.Flags().Set("dnd-end-time", endTimeArg)
s.Require().NoError(err)
mockUser := model.User{Id: "me-id", Username: "me"}
wantStatus := &model.Status{UserId: "me-id", Status: model.StatusDnd, DNDEndTime: endTime.Unix()}
s.client.
EXPECT().
GetMe(context.TODO(), "").
Return(&mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
UpdateUserStatus(context.TODO(), "me-id", wantStatus).
Return(wantStatus, &model.Response{}, nil).
Times(1)
err = userStatusSetCmdF(s.client, cmd, []string{model.StatusDnd})
s.Require().NoError(err)
s.Require().Len(printer.GetLines(), 1)
s.Require().Equal(wantStatus, printer.GetLines()[0])
})
s.Run("Reject a dnd-end-time in the past", func() {
printer.Clean()
cmd := newUserStatusSetCmd()
err := cmd.Flags().Set("dnd-end-time", time.Now().Add(-time.Hour).Format(ISO8601Layout))
s.Require().NoError(err)
err = userStatusSetCmdF(s.client, cmd, []string{model.StatusDnd})
s.Require().EqualError(err, "dnd-end-time must be in the future")
s.Require().Len(printer.GetLines(), 0)
})
s.Run("Reject a malformed dnd-end-time", func() {
printer.Clean()
cmd := newUserStatusSetCmd()
err := cmd.Flags().Set("dnd-end-time", "not-a-time")
s.Require().NoError(err)
err = userStatusSetCmdF(s.client, cmd, []string{model.StatusDnd})
s.Require().EqualError(err, "invalid dnd-end-time \"not-a-time\", expected RFC3339 format (e.g. 2006-01-02T15:04:05-07:00 or 2006-01-02T15:04:05Z)")
s.Require().Len(printer.GetLines(), 0)
})
s.Run("Return error when the current user cannot be resolved", func() {
printer.Clean()
s.client.
EXPECT().
GetMe(context.TODO(), "").
Return(nil, &model.Response{}, errors.New("me error")).
Times(1)
err := userStatusSetCmdF(s.client, newUserStatusSetCmd(), []string{model.StatusOnline})
s.Require().EqualError(err, "could not retrieve the current user: me error")
s.Require().Len(printer.GetLines(), 0)
})
s.Run("Require --user in local mode", func() {
printer.Clean()
prevLocal := viper.GetBool("local")
viper.Set("local", true)
defer viper.Set("local", prevLocal)
err := userStatusSetCmdF(s.client, newUserStatusSetCmd(), []string{model.StatusOnline})
s.Require().EqualError(err, "the --user flag is required in local mode")
s.Require().Len(printer.GetLines(), 0)
})
s.Run("Return error when status update fails", func() {
printer.Clean()
mockUser := model.User{Id: "me-id", Username: "me"}
wantStatus := &model.Status{UserId: "me-id", Status: model.StatusOnline}
s.client.
EXPECT().
GetMe(context.TODO(), "").
Return(&mockUser, &model.Response{}, nil).
Times(1)
s.client.
EXPECT().
UpdateUserStatus(context.TODO(), "me-id", wantStatus).
Return(nil, &model.Response{}, errors.New("update error")).
Times(1)
err := userStatusSetCmdF(s.client, newUserStatusSetCmd(), []string{model.StatusOnline})
s.Require().EqualError(err, "could not set status for user me-id: update error")
s.Require().Len(printer.GetLines(), 0)
})
}
+1
View File
@@ -54,5 +54,6 @@ SEE ALSO
* `mmctl user reset-password <mmctl_user_reset-password.rst>`_ - Send users an email to reset their password
* `mmctl user resetmfa <mmctl_user_resetmfa.rst>`_ - Turn off MFA
* `mmctl user search <mmctl_user_search.rst>`_ - Search for users
* `mmctl user status <mmctl_user_status.rst>`_ - Get a user's status
* `mmctl user verify <mmctl_user_verify.rst>`_ - Mark user's email as verified
@@ -0,0 +1,60 @@
.. _mmctl_user_status:
mmctl user status
-----------------
Get a user's status
Synopsis
~~~~~~~~
Get a user's presence status: online, away, dnd or offline.
::
mmctl user status [flags]
Examples
~~~~~~~~
::
# You can get the status of the currently authenticated user
$ mmctl user status
# You can get the status of a specific user
$ mmctl user status --user user@example.com
# In local mode there is no authenticated user, so the --user flag is required
$ mmctl --local user status --user user@example.com
Options
~~~~~~~
::
-h, --help help for status
--user string Optional. The user (specified by email, username or ID) whose status to get. Defaults to the currently authenticated user. Required in local mode.
Options inherited from parent commands
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
::
--config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config")
--disable-pager disables paged output
--insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1
--insecure-tls-version allows to use TLS versions 1.0 and 1.1
--json the output format will be in json format
--local allows communicating with the server through a unix socket
--quiet prevent mmctl to generate output for the commands
--strict will only run commands if the mmctl version matches the server one
--suppress-warnings disables printing warning messages
SEE ALSO
~~~~~~~~
* `mmctl user <mmctl_user.rst>`_ - Management of users
* `mmctl user status set <mmctl_user_status_set.rst>`_ - Set a user's status
@@ -0,0 +1,60 @@
.. _mmctl_user_status_set:
mmctl user status set
---------------------
Set a user's status
Synopsis
~~~~~~~~
Set a user's presence status. Allowed values are online, away, dnd and offline.
::
mmctl user status set [status] [flags]
Examples
~~~~~~~~
::
# You can set the status of the currently authenticated user
$ mmctl user status set away
# You can set the status of a specific user
$ mmctl user status set --user user@example.com dnd
# You can set a "dnd" status that expires at a given time (ISO 8601)
$ mmctl user status set --user user@example.com --dnd-end-time 2100-01-02T15:04:05-07:00 dnd
Options
~~~~~~~
::
--dnd-end-time string Optional. The time at which a "dnd" status expires, formatted as ISO 8601 (e.g. 2006-01-02T15:04:05-07:00). Only valid with the "dnd" status.
-h, --help help for set
--user string Optional. The user (specified by email, username or ID) whose status to set. Defaults to the currently authenticated user. Required in local mode.
Options inherited from parent commands
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
::
--config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config")
--disable-pager disables paged output
--insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1
--insecure-tls-version allows to use TLS versions 1.0 and 1.1
--json the output format will be in json format
--local allows communicating with the server through a unix socket
--quiet prevent mmctl to generate output for the commands
--strict will only run commands if the mmctl version matches the server one
--suppress-warnings disables printing warning messages
SEE ALSO
~~~~~~~~
* `mmctl user status <mmctl_user_status.rst>`_ - Get a user's status
+32
View File
@@ -1564,6 +1564,22 @@ func (mr *MockClientMockRecorder) GetUserByUsername(arg0, arg1, arg2 interface{}
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockClient)(nil).GetUserByUsername), arg0, arg1, arg2)
}
// GetUserStatus mocks base method.
func (m *MockClient) GetUserStatus(arg0 context.Context, arg1, arg2 string) (*model.Status, *model.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserStatus", arg0, arg1, arg2)
ret0, _ := ret[0].(*model.Status)
ret1, _ := ret[1].(*model.Response)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetUserStatus indicates an expected call of GetUserStatus.
func (mr *MockClientMockRecorder) GetUserStatus(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatus", reflect.TypeOf((*MockClient)(nil).GetUserStatus), arg0, arg1, arg2)
}
// GetUsers mocks base method.
func (m *MockClient) GetUsers(arg0 context.Context, arg1, arg2 int, arg3 string) ([]*model.User, *model.Response, error) {
m.ctrl.T.Helper()
@@ -2575,6 +2591,22 @@ func (mr *MockClientMockRecorder) UpdateUserRoles(arg0, arg1, arg2 interface{})
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserRoles", reflect.TypeOf((*MockClient)(nil).UpdateUserRoles), arg0, arg1, arg2)
}
// UpdateUserStatus mocks base method.
func (m *MockClient) UpdateUserStatus(arg0 context.Context, arg1 string, arg2 *model.Status) (*model.Status, *model.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUserStatus", arg0, arg1, arg2)
ret0, _ := ret[0].(*model.Status)
ret1, _ := ret[1].(*model.Response)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// UpdateUserStatus indicates an expected call of UpdateUserStatus.
func (mr *MockClientMockRecorder) UpdateUserStatus(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockClient)(nil).UpdateUserStatus), arg0, arg1, arg2)
}
// UploadData mocks base method.
func (m *MockClient) UploadData(arg0 context.Context, arg1 string, arg2 io.Reader) (*model.FileInfo, *model.Response, error) {
m.ctrl.T.Helper()