diff --git a/bin/golangci-lint b/bin/golangci-lint new file mode 100755 index 00000000..f5975abd Binary files /dev/null and b/bin/golangci-lint differ diff --git a/bin/gotestsum b/bin/gotestsum new file mode 100755 index 00000000..d37d9342 Binary files /dev/null and b/bin/gotestsum differ diff --git a/server/api/api.go b/server/api/api.go index 06cff29d..e965b5b4 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/mattermost/mattermost-plugin-autolink/server/autolink" + "github.com/mattermost/mattermost-plugin-autolink/server/autolinkclient" ) type Store interface { @@ -34,7 +35,10 @@ func NewHandler(store Store, authorization Authorization) *Handler { root := mux.NewRouter() api := root.PathPrefix("/api/v1").Subrouter() api.Use(h.adminOrPluginRequired) - api.HandleFunc("/link", h.setLink).Methods("POST") + link := api.PathPrefix("/link").Subrouter() + link.HandleFunc("", h.setLink).Methods(http.MethodPost) + link.HandleFunc("", h.deleteLink).Methods(http.MethodDelete) + link.HandleFunc("", h.getLinks).Methods(http.MethodGet) api.Handle("{anything:.*}", http.NotFoundHandler()) @@ -98,7 +102,7 @@ func (h *Handler) setLink(w http.ResponseWriter, r *http.Request) { found := false changed := false for i := range links { - if links[i].Name == newLink.Name || links[i].Pattern == newLink.Pattern { + if links[i].Name == newLink.Name { if !links[i].Equals(newLink) { links[i] = newLink changed = true @@ -114,12 +118,84 @@ func (h *Handler) setLink(w http.ResponseWriter, r *http.Request) { status := http.StatusNotModified if changed { if err := h.store.SaveLinks(links); err != nil { - h.handleError(w, errors.Wrap(err, "unable to save link")) + h.handleError(w, errors.Wrap(err, "unable to save the link")) return } status = http.StatusOK } + ReturnStatusOK(status, w) +} + +func (h *Handler) deleteLink(w http.ResponseWriter, r *http.Request) { + autolinkName := r.URL.Query().Get(autolinkclient.AutolinkNameQueryParam) + if autolinkName == "" { + h.handleError(w, errors.New("autolink name should not be empty")) + return + } + + links := h.store.GetLinks() + found := false + for i := 0; i < len(links); i++ { + if links[i].Name == autolinkName { + links = append(links[:i], links[i+1:]...) + found = true + break + } + } + + status := http.StatusNotFound + if found { + if err := h.store.SaveLinks(links); err != nil { + h.handleError(w, errors.Wrap(err, "unable to save the link")) + return + } + status = http.StatusOK + } + + ReturnStatusOK(status, w) +} + +func (h *Handler) getLinks(w http.ResponseWriter, r *http.Request) { + links := h.store.GetLinks() + + autolinkName := r.URL.Query().Get(autolinkclient.AutolinkNameQueryParam) + if autolinkName == "" { + h.handleSendingJSONContent(w, links) + return + } + + var autolink *autolink.Autolink + for _, link := range links { + currentLink := link + if currentLink.Name == autolinkName { + autolink = ¤tLink + break + } + } + if autolink == nil { + h.handleError(w, errors.Errorf("no autolink found with name %s", autolinkName)) + return + } + + h.handleSendingJSONContent(w, autolink) +} + +func (h *Handler) handleSendingJSONContent(w http.ResponseWriter, v interface{}) { + w.Header().Set("Content-Type", "application/json") + b, err := json.Marshal(v) + if err != nil { + h.handleError(w, errors.Wrap(err, "failed to marshal JSON response")) + return + } + + if _, err = w.Write(b); err != nil { + h.handleError(w, errors.Wrap(err, "failed to write JSON response")) + return + } +} + +func ReturnStatusOK(status int, w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _, _ = w.Write([]byte(`{"status": "OK"}`)) diff --git a/server/api/api_test.go b/server/api/api_test.go index faca9f9f..53b21be1 100644 --- a/server/api/api_test.go +++ b/server/api/api_test.go @@ -3,6 +3,8 @@ package api import ( "bytes" "encoding/json" + "fmt" + "io" "net/http" "net/http/httptest" "testing" @@ -168,3 +170,113 @@ func TestSetLink(t *testing.T) { }) } } + +func TestGetLink(t *testing.T) { + prevLinks := []autolink.Autolink{{ + Name: "test", + Pattern: ".*1", + Template: "test", + }} + + for _, tc := range []struct { + name string + autoLinkName string + expectStatus int + expectReturn string + }{ + { + name: "get the autolink", + autoLinkName: "test", + expectStatus: http.StatusOK, + expectReturn: `{"Name":"test","Disabled":false,"Pattern":".*1","Template":"test","Scope":null,"WordMatch":false,"DisableNonWordPrefix":false,"DisableNonWordSuffix":false,"ProcessBotPosts":false}`, + }, + { + name: "not found", + autoLinkName: "test-1", + expectStatus: http.StatusInternalServerError, + expectReturn: `{"error":"An internal error has occurred. Check app server logs for details.","details":"no autolink found with name test-1"}`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var saved []autolink.Autolink + var saveCalled bool + + h := NewHandler( + &linkStore{ + prev: prevLinks, + saveCalled: &saveCalled, + saved: &saved, + }, + authorizeAll{}, + ) + + w := httptest.NewRecorder() + r, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/link?autolinkName=%s", tc.autoLinkName), nil) + require.NoError(t, err) + + r.Header.Set("Mattermost-Plugin-ID", "testfrom") + r.Header.Set("Mattermost-User-ID", "testuser") + + h.ServeHTTP(w, r) + + respBody, err := io.ReadAll(w.Body) + require.NoError(t, err) + + require.Equal(t, tc.expectStatus, w.Code) + require.Equal(t, tc.expectReturn, string(respBody)) + }) + } +} + +func TestDeleteLink(t *testing.T) { + autoLinkName := "test" + for _, tc := range []struct { + name string + prevLinks []autolink.Autolink + expectStatus int + }{ + { + name: "delete the autolink", + prevLinks: []autolink.Autolink{{ + Name: "test", + Pattern: ".*1", + Template: "test", + }}, + expectStatus: http.StatusOK, + }, + { + name: "not found", + prevLinks: []autolink.Autolink{{ + Name: "test1", + Pattern: ".*1", + Template: "test", + }}, + expectStatus: http.StatusNotFound, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var saved []autolink.Autolink + var saveCalled bool + + h := NewHandler( + &linkStore{ + prev: tc.prevLinks, + saveCalled: &saveCalled, + saved: &saved, + }, + authorizeAll{}, + ) + + w := httptest.NewRecorder() + r, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/api/v1/link?autolinkName=%s", autoLinkName), nil) + require.NoError(t, err) + + r.Header.Set("Mattermost-Plugin-ID", "testfrom") + r.Header.Set("Mattermost-User-ID", "testuser") + + h.ServeHTTP(w, r) + + require.Equal(t, tc.expectStatus, w.Code) + }) + } +} diff --git a/server/autolinkclient/client.go b/server/autolinkclient/client.go index c59ed4e4..0ca01f30 100644 --- a/server/autolinkclient/client.go +++ b/server/autolinkclient/client.go @@ -6,11 +6,15 @@ import ( "fmt" "io" "net/http" + "net/url" "github.com/mattermost/mattermost-plugin-autolink/server/autolink" ) -const autolinkPluginID = "mattermost-autolink" +const ( + autolinkPluginID = "mattermost-autolink" + AutolinkNameQueryParam = "autolinkName" +) type PluginAPI interface { PluginHTTP(*http.Request) *http.Response @@ -45,12 +49,28 @@ func (c *Client) Add(links ...autolink.Autolink) error { return err } - req, err := http.NewRequest("POST", "/"+autolinkPluginID+"/api/v1/link", bytes.NewReader(linkBytes)) + resp, err := c.call("/"+autolinkPluginID+"/api/v1/link", http.MethodPost, linkBytes, nil) if err != nil { return err } + defer resp.Body.Close() - resp, err := c.Do(req) + if resp.StatusCode != http.StatusOK { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("unable to add the link %s. Error: %v, %v", link.Name, resp.StatusCode, string(respBody)) + } + } + + return nil +} + +func (c *Client) Delete(links ...string) error { + for _, link := range links { + queryParams := url.Values{ + AutolinkNameQueryParam: {link}, + } + + resp, err := c.call("/"+autolinkPluginID+"/api/v1/link", http.MethodDelete, nil, queryParams) if err != nil { return err } @@ -58,9 +78,53 @@ func (c *Client) Add(links ...autolink.Autolink) error { if resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) - return fmt.Errorf("unable to install autolink. Error: %v, %v", resp.StatusCode, string(respBody)) + return fmt.Errorf("unable to delete the link %s. Error: %v, %v", link, resp.StatusCode, string(respBody)) } } return nil } + +func (c *Client) Get(autolinkName string) (*autolink.Autolink, error) { + queryParams := url.Values{ + AutolinkNameQueryParam: {autolinkName}, + } + + resp, err := c.call("/"+autolinkPluginID+"/api/v1/link", http.MethodGet, nil, queryParams) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unable to get the link %s. Error: %v, %v", autolinkName, resp.StatusCode, string(respBody)) + } + + var response *autolink.Autolink + if err = json.Unmarshal(respBody, &response); err != nil { + return nil, err + } + + return response, nil +} + +func (c *Client) call(url, method string, body []byte, queryParams url.Values) (*http.Response, error) { + req, err := http.NewRequest(method, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.URL.RawQuery = queryParams.Encode() + + resp, err := c.Do(req) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/server/autolinkclient/client_test.go b/server/autolinkclient/client_test.go index 0e9a13cc..88117cfa 100644 --- a/server/autolinkclient/client_test.go +++ b/server/autolinkclient/client_test.go @@ -1,7 +1,10 @@ package autolinkclient import ( + "errors" + "io" "net/http" + "strings" "testing" "github.com/mattermost/mattermost/server/public/plugin/plugintest" @@ -53,3 +56,77 @@ func TestAddAutolinksErr(t *testing.T) { err := client.Add(autolink.Autolink{}) require.Error(t, err) } + +func TestDeleteAutolinks(t *testing.T) { + for _, tc := range []struct { + name string + setupAPI func(*plugintest.API) + err error + }{ + { + name: "delete the autolink", + setupAPI: func(api *plugintest.API) { + body := io.NopCloser(strings.NewReader("{}")) + api.On("PluginHTTP", mock.AnythingOfType("*http.Request")).Return(&http.Response{StatusCode: http.StatusOK, Body: body}) + }, + }, + { + name: "got error", + setupAPI: func(api *plugintest.API) { + api.On("PluginHTTP", mock.AnythingOfType("*http.Request")).Return(nil) + }, + err: errors.New("not able to delete the autolink"), + }, + } { + t.Run(tc.name, func(t *testing.T) { + mockPluginAPI := &plugintest.API{} + tc.setupAPI(mockPluginAPI) + + client := NewClientPlugin(mockPluginAPI) + err := client.Delete("") + + if tc.err != nil { + require.Error(t, err) + } else { + require.Nil(t, err) + } + }) + } +} + +func TestGetAutolinks(t *testing.T) { + for _, tc := range []struct { + name string + setupAPI func(*plugintest.API) + err error + }{ + { + name: "get the autolink", + setupAPI: func(api *plugintest.API) { + body := io.NopCloser(strings.NewReader("{}")) + api.On("PluginHTTP", mock.AnythingOfType("*http.Request")).Return(&http.Response{StatusCode: http.StatusOK, Body: body}) + }, + }, + { + name: "got error", + setupAPI: func(api *plugintest.API) { + api.On("PluginHTTP", mock.AnythingOfType("*http.Request")).Return(nil) + }, + err: errors.New("not able to get the autolink"), + }, + } { + t.Run(tc.name, func(t *testing.T) { + mockPluginAPI := &plugintest.API{} + tc.setupAPI(mockPluginAPI) + + client := NewClientPlugin(mockPluginAPI) + _, err := client.Get("") + + if tc.err != nil { + require.Error(t, err) + } else { + require.Nil(t, err) + } + }) + } +}