[MM-56073] MMCTL delete post command (#27539)

Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
Этот коммит содержится в:
Ben Cooke
2024-10-08 10:45:31 -04:00
коммит произвёл GitHub
родитель a671f80d2c
Коммит b3c7ef0b97
39 изменённых файлов: 1246 добавлений и 152 удалений

Просмотреть файл

@@ -159,4 +159,6 @@ type Client interface {
GetPreferenceByCategoryAndName(ctx context.Context, userId, category, preferenceName string) (*model.Preference, *model.Response, error)
UpdatePreferences(ctx context.Context, userId string, preferences model.Preferences) (*model.Response, error)
DeletePreferences(ctx context.Context, userId string, preferences model.Preferences) (*model.Response, error)
PermanentDeletePost(ctx context.Context, postID string) (*model.Response, error)
DeletePost(ctx context.Context, postId string) (*model.Response, error)
}

Просмотреть файл

@@ -41,6 +41,22 @@ var PostListCmd = &cobra.Command{
RunE: withClient(postListCmdF),
}
var PostDeleteCmd = &cobra.Command{
Use: "delete [posts]",
Short: "Mark posts as deleted or permanently delete posts with the --permanent flag",
Long: `This command will mark the post as deleted and remove it from the user's clients, but it does not permanently delete the post from the database. Please use the --permanent flag to permanently delete a post and its attachments from your database.`,
Example: ` # Mark Post as deleted
$ mmctl post delete udjmt396tjghi8wnsk3a1qs1sw
# Permanently delete a post and it's file contents from the database and filestore
$ mmctl post delete udjmt396tjghi8wnsk3a1qs1sw --permanent
# Permanently delete multiple posts and their file contents from the database and filestore
$ mmctl post delete udjmt396tjghi8wnsk3a1qs1sw 7jgcjt7tyjyyu83qz81wo84w6o --permanent`,
Args: cobra.MinimumNArgs(1),
RunE: withClient(deletePostsCmdF),
}
const (
ISO8601Layout = "2006-01-02T15:04:05-07:00"
PostTimeFormat = "2006-01-02 15:04:05-07:00"
@@ -55,9 +71,13 @@ func init() {
PostListCmd.Flags().BoolP("follow", "f", false, "Output appended data as new messages are posted to the channel")
PostListCmd.Flags().StringP("since", "s", "", "List messages posted after a certain time (ISO 8601)")
PostDeleteCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the post and a DB backup has been performed")
PostDeleteCmd.Flags().Bool("permanent", false, "Permanently delete the post and its contents from the database")
PostCmd.AddCommand(
PostCreateCmd,
PostListCmd,
PostDeleteCmd,
)
RootCmd.AddCommand(PostCmd)
@@ -217,3 +237,42 @@ func postListCmdF(c client.Client, cmd *cobra.Command, args []string) error {
}
return multiErr.ErrorOrNil()
}
func deletePostsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
permanent, err := cmd.Flags().GetBool("permanent")
if err != nil {
return err
}
confirmFlag, _ := cmd.Flags().GetBool("confirm")
if !confirmFlag && permanent {
if err = getConfirmation("Are you sure you want to delete the posts specified?", true); err != nil {
return err
}
}
var result *multierror.Error
var deleteFunc func(ctx context.Context, postID string) (*model.Response, error)
if permanent {
deleteFunc = c.PermanentDeletePost
} else {
deleteFunc = c.DeletePost
}
for _, postID := range args {
isValidId := model.IsValidId(postID)
if !isValidId {
printer.PrintError(fmt.Sprintf("Invalid postID: %s", postID))
result = multierror.Append(result, err)
continue
}
if _, err := deleteFunc(context.TODO(), postID); err != nil {
printer.PrintError(fmt.Sprintf("Error deleting post: %s. Error: %s", postID, err.Error()))
result = multierror.Append(result, err)
continue
}
printer.Print(fmt.Sprintf("%s successfully deleted", postID))
}
return result.ErrorOrNil()
}

Просмотреть файл

@@ -5,6 +5,7 @@ package commands
import (
"context"
"net/http"
"time"
"github.com/mattermost/mattermost/server/public/model"
@@ -257,3 +258,122 @@ func (s *MmctlUnitTestSuite) TestPostListCmdF() {
s.Len(printer.GetErrorLines(), 0)
})
}
func (s *MmctlUnitTestSuite) TestDeletePostsCmdF() {
postID1 := "ux9bxc1b8bf1zdoj1tfu14836e"
postID2 := "ux9bxc1b8bf1zdoj1tfu14836f"
s.Run("invalid post id", func() {
id := "invalid-id"
cmd := &cobra.Command{}
cmd.Flags().Bool("confirm", true, "")
cmd.Flags().Bool("permanent", false, "")
err := deletePostsCmdF(s.client, cmd, []string{id})
s.Require().Nil(err)
s.Require().Equal("Invalid postID: invalid-id", printer.GetErrorLines()[0])
})
s.Run("successfully permanently delete one post", func() {
printer.Clean()
s.client.
EXPECT().
PermanentDeletePost(context.TODO(), postID1).
Return(&model.Response{StatusCode: http.StatusOK}, nil).
Times(1)
cmd := &cobra.Command{}
cmd.Flags().Bool("confirm", true, "")
cmd.Flags().Bool("permanent", true, "")
err := deletePostsCmdF(s.client, cmd, []string{postID1})
s.Require().Nil(err)
s.Require().Equal(postID1+" successfully deleted", printer.GetLines()[0])
})
s.Run("successfully soft delete one post", func() {
printer.Clean()
s.client.
EXPECT().
DeletePost(context.TODO(), postID1).
Return(&model.Response{StatusCode: http.StatusOK}, nil).
Times(1)
cmd := &cobra.Command{}
cmd.Flags().Bool("confirm", true, "")
cmd.Flags().Bool("permanent", false, "")
err := deletePostsCmdF(s.client, cmd, []string{postID1})
s.Require().Nil(err)
s.Require().Equal(postID1+" successfully deleted", printer.GetLines()[0])
})
s.Run("successfully delete multiple posts", func() {
printer.Clean()
s.client.
EXPECT().
PermanentDeletePost(context.TODO(), postID1).
Return(&model.Response{StatusCode: http.StatusOK}, nil).
Times(1)
s.client.
EXPECT().
PermanentDeletePost(context.TODO(), postID2).
Return(&model.Response{StatusCode: http.StatusOK}, nil).
Times(1)
cmd := &cobra.Command{}
cmd.Flags().Bool("confirm", true, "")
cmd.Flags().Bool("permanent", true, "")
err := deletePostsCmdF(s.client, cmd, []string{postID1, postID2})
s.Require().Nil(err)
s.Require().Equal(postID1+" successfully deleted", printer.GetLines()[0])
s.Require().Equal(postID2+" successfully deleted", printer.GetLines()[1])
})
s.Run("PermanentDeletePost api request returns an error", func() {
printer.Clean()
mockError := errors.New("an error occurred on deleting a post")
s.client.
EXPECT().
PermanentDeletePost(context.TODO(), postID1).
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockError).
Times(1)
cmd := &cobra.Command{}
cmd.Flags().Bool("confirm", true, "")
cmd.Flags().Bool("permanent", true, "")
err := deletePostsCmdF(s.client, cmd, []string{postID1})
s.Require().ErrorContains(err, "an error occurred on deleting a post")
s.Require().Len(printer.GetErrorLines(), 1)
s.Require().Equal("Error deleting post: "+postID1+". Error: an error occurred on deleting a post",
printer.GetErrorLines()[0])
})
s.Run("Delete multiple posts but one fails with an error", func() {
printer.Clean()
mockError := errors.New("an error occurred on deleting a post")
s.client.
EXPECT().
PermanentDeletePost(context.TODO(), postID1).
Return(&model.Response{StatusCode: http.StatusOK}, nil).
Times(1)
s.client.
EXPECT().
PermanentDeletePost(context.TODO(), postID2).
Return(&model.Response{StatusCode: http.StatusBadRequest}, mockError).
Times(1)
cmd := &cobra.Command{}
cmd.Flags().Bool("confirm", true, "")
cmd.Flags().Bool("permanent", true, "")
err := deletePostsCmdF(s.client, cmd, []string{postID1, postID2})
s.Require().ErrorContains(err, "an error occurred on deleting a post")
s.Require().Len(printer.GetLines(), 1)
s.Require().Len(printer.GetErrorLines(), 1)
s.Require().Equal(postID1+" successfully deleted", printer.GetLines()[0])
s.Require().Equal("Error deleting post: "+postID2+". Error: an error occurred on deleting a post",
printer.GetErrorLines()[0])
})
}

Просмотреть файл

@@ -38,5 +38,6 @@ SEE ALSO
* `mmctl <mmctl.rst>`_ - Remote client for the Open Source, self-hosted Slack-alternative
* `mmctl post create <mmctl_post_create.rst>`_ - Create a post
* `mmctl post delete <mmctl_post_delete.rst>`_ - Mark posts as deleted or permanently delete posts with the --permanent flag
* `mmctl post list <mmctl_post_list.rst>`_ - List posts for a channel

60
server/cmd/mmctl/docs/mmctl_post_delete.rst Обычный файл
Просмотреть файл

@@ -0,0 +1,60 @@
.. _mmctl_post_delete:
mmctl post delete
-----------------
Mark posts as deleted or permanently delete posts with the --permanent flag
Synopsis
~~~~~~~~
This command will mark the post as deleted and remove it from the user's clients, but it does not permanently delete the post from the database. Please use the --permanent flag to permanently delete a post and its attachments from your database.
::
mmctl post delete [posts] [flags]
Examples
~~~~~~~~
::
# Mark Post as deleted
$ mmctl post delete udjmt396tjghi8wnsk3a1qs1sw
# Permanently delete a post and it's file contents from the database and filestore
$ mmctl post delete udjmt396tjghi8wnsk3a1qs1sw --permanent
# Permanently delete multiple posts and their file contents from the database and filestore
$ mmctl post delete udjmt396tjghi8wnsk3a1qs1sw 7jgcjt7tyjyyu83qz81wo84w6o --permanent
Options
~~~~~~~
::
--confirm Confirm you really want to delete the post and a DB backup has been performed
-h, --help help for delete
--permanent Permanently delete the post and its contents from the database
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 post <mmctl_post.rst>`_ - Management of posts

Просмотреть файл

@@ -417,6 +417,21 @@ func (mr *MockClientMockRecorder) DeleteOutgoingWebhook(arg0, arg1 interface{})
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOutgoingWebhook", reflect.TypeOf((*MockClient)(nil).DeleteOutgoingWebhook), arg0, arg1)
}
// DeletePost mocks base method.
func (m *MockClient) DeletePost(arg0 context.Context, arg1 string) (*model.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeletePost", arg0, arg1)
ret0, _ := ret[0].(*model.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeletePost indicates an expected call of DeletePost.
func (mr *MockClientMockRecorder) DeletePost(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePost", reflect.TypeOf((*MockClient)(nil).DeletePost), arg0, arg1)
}
// DeletePreferences mocks base method.
func (m *MockClient) DeletePreferences(arg0 context.Context, arg1 string, arg2 model.Preferences) (*model.Response, error) {
m.ctrl.T.Helper()
@@ -1750,6 +1765,21 @@ func (mr *MockClientMockRecorder) PermanentDeleteChannel(arg0, arg1 interface{})
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PermanentDeleteChannel", reflect.TypeOf((*MockClient)(nil).PermanentDeleteChannel), arg0, arg1)
}
// PermanentDeletePost mocks base method.
func (m *MockClient) PermanentDeletePost(arg0 context.Context, arg1 string) (*model.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PermanentDeletePost", arg0, arg1)
ret0, _ := ret[0].(*model.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PermanentDeletePost indicates an expected call of PermanentDeletePost.
func (mr *MockClientMockRecorder) PermanentDeletePost(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PermanentDeletePost", reflect.TypeOf((*MockClient)(nil).PermanentDeletePost), arg0, arg1)
}
// PermanentDeleteTeam mocks base method.
func (m *MockClient) PermanentDeleteTeam(arg0 context.Context, arg1 string) (*model.Response, error) {
m.ctrl.T.Helper()