mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-26 21:27:40 -05:00
[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:
co-authored by
mattermost-code
Cursor Agent
parent
97750a8641
commit
3759dd5031
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user