From 9646bddd21bf778349d1563e4fde756d4e981dd2 Mon Sep 17 00:00:00 2001 From: Ruzette Tanyag Date: Tue, 21 Feb 2017 07:36:52 -0500 Subject: Implement posts endpoints for APIv4 (#5480) * Implement delete post endpoint for apiv4 * Implement POST search post endpoint for APIv4 * removed delete post quotes * rearrange formatting --- api4/apitestlib.go | 19 ++++ api4/post.go | 57 ++++++++++++ api4/post_test.go | 257 +++++++++++++++++++++++++++++++++++++++++++++++++++++ app/post.go | 1 + model/client4.go | 22 +++++ 5 files changed, 356 insertions(+) diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 84f87d494..bb5ee1594 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -203,6 +203,10 @@ func (me *TestHelper) CreatePost() *model.Post { return me.CreatePostWithClient(me.Client, me.BasicChannel) } +func (me *TestHelper) CreateMessagePost(message string) *model.Post { + return me.CreateMessagePostWithClient(me.Client, me.BasicChannel, message) +} + func (me *TestHelper) CreatePostWithClient(client *model.Client4, channel *model.Channel) *model.Post { id := model.NewId() @@ -220,6 +224,21 @@ func (me *TestHelper) CreatePostWithClient(client *model.Client4, channel *model return rpost } +func (me *TestHelper) CreateMessagePostWithClient(client *model.Client4, channel *model.Channel, message string) *model.Post { + post := &model.Post{ + ChannelId: channel.Id, + Message: message, + } + + utils.DisableDebugLogForTest() + rpost, resp := client.CreatePost(post) + if resp.Error != nil { + panic(resp.Error) + } + utils.EnableDebugLogForTest() + return rpost +} + func (me *TestHelper) LoginBasic() { me.LoginBasicWithClient(me.Client) } diff --git a/api4/post.go b/api4/post.go index 9510fe0c6..7290ce8ef 100644 --- a/api4/post.go +++ b/api4/post.go @@ -5,6 +5,7 @@ package api4 import ( "net/http" + "strconv" l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/app" @@ -17,8 +18,11 @@ func InitPost() { BaseRoutes.Posts.Handle("", ApiSessionRequired(createPost)).Methods("POST") BaseRoutes.Post.Handle("", ApiSessionRequired(getPost)).Methods("GET") + BaseRoutes.Post.Handle("", ApiSessionRequired(deletePost)).Methods("DELETE") BaseRoutes.Post.Handle("/thread", ApiSessionRequired(getPostThread)).Methods("GET") BaseRoutes.PostsForChannel.Handle("", ApiSessionRequired(getPostsForChannel)).Methods("GET") + + BaseRoutes.Team.Handle("/posts/search", ApiSessionRequired(searchPosts)).Methods("POST") } func createPost(c *Context, w http.ResponseWriter, r *http.Request) { @@ -96,6 +100,25 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) { } } +func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequirePostId() + if c.Err != nil { + return + } + + if !app.SessionHasPermissionToPost(c.Session, c.Params.PostId, model.PERMISSION_DELETE_OTHERS_POSTS) { + c.SetPermissionError(model.PERMISSION_DELETE_OTHERS_POSTS) + return + } + + if _, err := app.DeletePost(c.Params.PostId); err != nil { + c.Err = err + return + } + + ReturnStatusOK(w) +} + func getPostThread(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePostId() if c.Err != nil { @@ -117,3 +140,37 @@ func getPostThread(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(list.ToJson())) } } + +func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireTeamId() + if c.Err != nil { + return + } + + if !app.SessionHasPermissionToTeam(c.Session, c.Params.TeamId, model.PERMISSION_VIEW_TEAM) { + c.SetPermissionError(model.PERMISSION_VIEW_TEAM) + return + } + + props := model.MapFromJson(r.Body) + terms := props["terms"] + + if len(terms) == 0 { + c.SetInvalidParam("terms") + return + } + + isOrSearch := false + if val, ok := props["is_or_search"]; ok && val != "" { + isOrSearch, _ = strconv.ParseBool(val) + } + + posts, err := app.SearchPostsInTeam(terms, c.Session.UserId, c.Params.TeamId, isOrSearch) + if err != nil { + c.Err = err + return + } + + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Write([]byte(posts.ToJson())) +} diff --git a/api4/post_test.go b/api4/post_test.go index 604920b96..5c224cb06 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -7,7 +7,9 @@ import ( "net/http" "strconv" "testing" + "time" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" ) @@ -207,6 +209,44 @@ func TestGetPost(t *testing.T) { CheckNoError(t, resp) } +func TestDeletePost(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + _, resp := Client.DeletePost("") + CheckNotFoundStatus(t, resp) + + _, resp = Client.DeletePost("junk") + CheckBadRequestStatus(t, resp) + + _, resp = Client.DeletePost(th.BasicPost.Id) + CheckForbiddenStatus(t, resp) + + Client.Login(th.TeamAdminUser.Email, th.TeamAdminUser.Password) + _, resp = Client.DeletePost(th.BasicPost.Id) + CheckNoError(t, resp) + + post := th.CreatePost() + user := th.CreateUser() + + Client.Logout() + Client.Login(user.Email, user.Password) + + _, resp = Client.DeletePost(post.Id) + CheckForbiddenStatus(t, resp) + + Client.Logout() + _, resp = Client.DeletePost(model.NewId()) + CheckUnauthorizedStatus(t, resp) + + status, resp := th.SystemAdminClient.DeletePost(post.Id) + if status == false { + t.Fatal("post should return status OK") + } + CheckNoError(t, resp) +} + func TestGetPostThread(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer TearDown() @@ -247,3 +287,220 @@ func TestGetPostThread(t *testing.T) { list, resp = th.SystemAdminClient.GetPostThread(th.BasicPost.Id, "") CheckNoError(t, resp) } + +func TestSearchPosts(t *testing.T) { + th := Setup().InitBasic() + defer TearDown() + th.LoginBasic() + Client := th.Client + + message := "search for post1" + _ = th.CreateMessagePost(message) + + message = "search for post2" + post2 := th.CreateMessagePost(message) + + message = "#hashtag search for post3" + post3 := th.CreateMessagePost(message) + + message = "hashtag for post4" + _ = th.CreateMessagePost(message) + + posts, resp := Client.SearchPosts(th.BasicTeam.Id, "search", false) + CheckNoError(t, resp) + if len(posts.Order) != 3 { + t.Fatal("wrong search") + } + + posts, resp = Client.SearchPosts(th.BasicTeam.Id, "post2", false) + CheckNoError(t, resp) + if len(posts.Order) != 1 && posts.Order[0] == post2.Id { + t.Fatal("wrong search") + } + + posts, resp = Client.SearchPosts(th.BasicTeam.Id, "#hashtag", false) + CheckNoError(t, resp) + if len(posts.Order) != 1 && posts.Order[0] == post3.Id { + t.Fatal("wrong search") + } + + if posts, resp = Client.SearchPosts(th.BasicTeam.Id, "*", false); len(posts.Order) != 0 { + t.Fatal("searching for just * shouldn't return any results") + } + + posts, resp = Client.SearchPosts(th.BasicTeam.Id, "post1 post2", true) + CheckNoError(t, resp) + if len(posts.Order) != 2 { + t.Fatal("wrong search results") + } + + _, resp = Client.SearchPosts("junk", "#sgtitlereview", false) + CheckBadRequestStatus(t, resp) + + _, resp = Client.SearchPosts(model.NewId(), "#sgtitlereview", false) + CheckForbiddenStatus(t, resp) + + _, resp = Client.SearchPosts(th.BasicTeam.Id, "", false) + CheckBadRequestStatus(t, resp) + + Client.Logout() + _, resp = Client.SearchPosts(th.BasicTeam.Id, "#sgtitlereview", false) + CheckUnauthorizedStatus(t, resp) + +} + +func TestSearchHashtagPosts(t *testing.T) { + th := Setup().InitBasic() + defer TearDown() + th.LoginBasic() + Client := th.Client + + message := "#sgtitlereview with space" + _ = th.CreateMessagePost(message) + + message = "#sgtitlereview\n with return" + _ = th.CreateMessagePost(message) + + message = "no hashtag" + _ = th.CreateMessagePost(message) + + posts, resp := Client.SearchPosts(th.BasicTeam.Id, "#sgtitlereview", false) + CheckNoError(t, resp) + if len(posts.Order) != 2 { + t.Fatal("wrong search results") + } + + Client.Logout() + _, resp = Client.SearchPosts(th.BasicTeam.Id, "#sgtitlereview", false) + CheckUnauthorizedStatus(t, resp) +} + +func TestSearchPostsInChannel(t *testing.T) { + th := Setup().InitBasic() + defer TearDown() + th.LoginBasic() + Client := th.Client + + channel := th.CreatePublicChannel() + + message := "sgtitlereview with space" + _ = th.CreateMessagePost(message) + + message = "sgtitlereview\n with return" + _ = th.CreateMessagePostWithClient(Client, th.BasicChannel2, message) + + message = "other message with no return" + _ = th.CreateMessagePostWithClient(Client, th.BasicChannel2, message) + + message = "other message with no return" + _ = th.CreateMessagePostWithClient(Client, channel, message) + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "channel:", false); len(posts.Order) != 0 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "in:", false); len(posts.Order) != 0 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "channel:"+th.BasicChannel.Name, false); len(posts.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "in:"+th.BasicChannel2.Name, false); len(posts.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "channel:"+th.BasicChannel2.Name, false); len(posts.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "ChAnNeL:"+th.BasicChannel2.Name, false); len(posts.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "sgtitlereview", false); len(posts.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "sgtitlereview channel:"+th.BasicChannel.Name, false); len(posts.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "sgtitlereview in: "+th.BasicChannel2.Name, false); len(posts.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "sgtitlereview channel: "+th.BasicChannel2.Name, false); len(posts.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "channel: "+th.BasicChannel2.Name+" channel: "+channel.Name, false); len(posts.Order) != 3 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + +} + +func TestSearchPostsFromUser(t *testing.T) { + th := Setup().InitBasic() + defer TearDown() + Client := th.Client + + th.LoginTeamAdmin() + user := th.CreateUser() + LinkUserToTeam(user, th.BasicTeam) + app.AddUserToChannel(user, th.BasicChannel) + app.AddUserToChannel(user, th.BasicChannel2) + + message := "sgtitlereview with space" + _ = th.CreateMessagePost(message) + + Client.Logout() + th.LoginBasic2() + + message = "sgtitlereview\n with return" + _ = th.CreateMessagePostWithClient(Client, th.BasicChannel2, message) + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "from: "+th.TeamAdminUser.Username, false); len(posts.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false); len(posts.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" sgtitlereview", false); len(posts.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + message = "hullo" + _ = th.CreateMessagePost(message) + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" in:"+th.BasicChannel.Name, false); len(posts.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + Client.Login(user.Email, user.Password) + + // wait for the join/leave messages to be created for user3 since they're done asynchronously + time.Sleep(100 * time.Millisecond) + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false); len(posts.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username, false); len(posts.Order) != 2 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name, false); len(posts.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } + + message = "coconut" + _ = th.CreateMessagePostWithClient(Client, th.BasicChannel2, message) + + if posts, _ := Client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name+" coconut", false); len(posts.Order) != 1 { + t.Fatalf("wrong number of posts returned %v", len(posts.Order)) + } +} diff --git a/app/post.go b/app/post.go index a89a72e62..9ba119413 100644 --- a/app/post.go +++ b/app/post.go @@ -400,6 +400,7 @@ func GetPostsAroundPost(postId, channelId string, offset, limit int, before bool func DeletePost(postId string) (*model.Post, *model.AppError) { if result := <-Srv.Store.Post().GetSingle(postId); result.Err != nil { + result.Err.StatusCode = http.StatusBadRequest return nil, result.Err } else { post := result.Data.(*model.Post) diff --git a/model/client4.go b/model/client4.go index 833050561..6ace37dca 100644 --- a/model/client4.go +++ b/model/client4.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "mime/multipart" "net/http" + "strconv" "strings" ) @@ -684,6 +685,16 @@ func (c *Client4) GetPost(postId string, etag string) (*Post, *Response) { } } +// DeletePost deletes a post from the provided post id string. +func (c *Client4) DeletePost(postId string) (bool, *Response) { + if r, err := c.DoApiDelete(c.GetPostRoute(postId)); err != nil { + return false, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + // GetPostThread gets a post with all the other posts in the same thread. func (c *Client4) GetPostThread(postId string, etag string) (*PostList, *Response) { if r, err := c.DoApiGet(c.GetPostRoute(postId)+"/thread", etag); err != nil { @@ -705,6 +716,17 @@ func (c *Client4) GetPostsForChannel(channelId string, page, perPage int, etag s } } +// SearchPosts returns any posts with matching terms string. +func (c *Client4) SearchPosts(teamId string, terms string, isOrSearch bool) (*PostList, *Response) { + requestBody := map[string]string{"terms": terms, "is_or_search": strconv.FormatBool(isOrSearch)} + if r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/posts/search", MapToJson(requestBody)); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) + } +} + // File Section // UploadFile will upload a file to a channel, to be later attached to a post. -- cgit v1.2.3-1-g7c22