Skip to content

Commit

Permalink
MM-17818 Adding API for adding/replacing links. (#85)
Browse files Browse the repository at this point in the history
* Adding API for adding/replacing links.

* link -> autolink

* Adding an autolink client

* Renaming and content type.

* Moving to using headers to transmit plugin id.

* check for "no change" (#88)

* check for "no change"

* fixed an error treating empty UserID

* minor

Co-authored-by: Lev <1187448+levb@users.noreply.github.com>
  • Loading branch information
crspeller and levb committed Feb 3, 2020
1 parent d67f2d5 commit 8c82b7d
Show file tree
Hide file tree
Showing 14 changed files with 691 additions and 102 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/mattermost/mattermost-plugin-autolink
go 1.12

require (
github.com/gorilla/mux v1.7.3
github.com/mattermost/mattermost-server/v5 v5.18.0
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.4.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekf
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand Down Expand Up @@ -120,6 +121,7 @@ github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORR
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
Expand Down
138 changes: 138 additions & 0 deletions server/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package api

import (
"encoding/json"
"fmt"
"net/http"

"github.com/gorilla/mux"
"github.com/mattermost/mattermost-plugin-autolink/server/autolink"
)

type Store interface {
GetLinks() []autolink.Autolink
SaveLinks([]autolink.Autolink) error
}

type Authorization interface {
IsAuthorizedAdmin(userID string) (bool, error)
}

type Handler struct {
root *mux.Router
store Store
authorization Authorization
}

func NewHandler(store Store, authorization Authorization) *Handler {
h := &Handler{
store: store,
authorization: authorization,
}

root := mux.NewRouter()
api := root.PathPrefix("/api/v1").Subrouter()
api.Use(h.adminOrPluginRequired)
api.HandleFunc("/link", h.setLink).Methods("POST")

api.Handle("{anything:.*}", http.NotFoundHandler())

h.root = root

return h
}

func (h *Handler) handleError(w http.ResponseWriter, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
b, _ := json.Marshal(struct {
Error string `json:"error"`
Details string `json:"details"`
}{
Error: "An internal error has occurred. Check app server logs for details.",
Details: err.Error(),
})
_, _ = w.Write(b)
}

func (h *Handler) handleErrorWithCode(w http.ResponseWriter, code int, errTitle string, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
b, _ := json.Marshal(struct {
Error string `json:"error"`
Details string `json:"details"`
}{
Error: errTitle,
Details: err.Error(),
})
_, _ = w.Write(b)
}

func (h *Handler) adminOrPluginRequired(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
authorized := false
pluginId := r.Header.Get("Mattermost-Plugin-ID")
if pluginId != "" {
// All other plugins are allowed
authorized = true
}

userID := r.Header.Get("Mattermost-User-ID")
if !authorized && userID != "" {
authorized, err = h.authorization.IsAuthorizedAdmin(userID)
if err != nil {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
}

if !authorized {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.root.ServeHTTP(w, r)
}

func (h *Handler) setLink(w http.ResponseWriter, r *http.Request) {
var newLink autolink.Autolink
if err := json.NewDecoder(r.Body).Decode(&newLink); err != nil {
h.handleError(w, fmt.Errorf("Unable to decode body: %w", err))
return
}

links := h.store.GetLinks()
found := false
changed := false
for i := range links {
if links[i].Name == newLink.Name || links[i].Pattern == newLink.Pattern {
if !links[i].Equals(newLink) {
links[i] = newLink
changed = true
}
found = true
break
}
}
if !found {
links = append(h.store.GetLinks(), newLink)
changed = true
}
status := http.StatusNotModified
if changed {
if err := h.store.SaveLinks(links); err != nil {
h.handleError(w, fmt.Errorf("Unable to save link: %w", err))
return
}
status = http.StatusOK
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, _ = w.Write([]byte(`{"status": "OK"}`))
}
187 changes: 187 additions & 0 deletions server/api/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package api

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/mattermost/mattermost-plugin-autolink/server/autolink"
"github.com/stretchr/testify/require"
)

type authorizeAll struct{}

func (authorizeAll) IsAuthorizedAdmin(string) (bool, error) {
return true, nil
}

type linkStore struct {
prev []autolink.Autolink
saveCalled *bool
saved *[]autolink.Autolink
}

func (s *linkStore) GetLinks() []autolink.Autolink {
return s.prev
}

func (s *linkStore) SaveLinks(links []autolink.Autolink) error {
*s.saved = links
*s.saveCalled = true
return nil
}

func TestSetLink(t *testing.T) {
for _, tc := range []struct {
name string
method string
prevLinks []autolink.Autolink
link autolink.Autolink
expectSaveCalled bool
expectSaved []autolink.Autolink
expectStatus int
}{
{
name: "happy simple",
link: autolink.Autolink{
Name: "test",
},
expectStatus: http.StatusOK,
expectSaveCalled: true,
expectSaved: []autolink.Autolink{
autolink.Autolink{
Name: "test",
},
},
},
{
name: "add new link",
link: autolink.Autolink{
Name: "test1",
Pattern: ".*1",
Template: "test1",
},
prevLinks: []autolink.Autolink{
autolink.Autolink{
Name: "test2",
Pattern: ".*2",
Template: "test2",
},
},
expectStatus: http.StatusOK,
expectSaveCalled: true,
expectSaved: []autolink.Autolink{
autolink.Autolink{
Name: "test2",
Pattern: ".*2",
Template: "test2",
},
autolink.Autolink{
Name: "test1",
Pattern: ".*1",
Template: "test1",
},
},
}, {
name: "replace link",
link: autolink.Autolink{
Name: "test2",
Pattern: ".*2",
Template: "new template",
},
prevLinks: []autolink.Autolink{
autolink.Autolink{
Name: "test1",
Pattern: ".*1",
Template: "test1",
},
autolink.Autolink{
Name: "test2",
Pattern: ".*2",
Template: "test2",
},
autolink.Autolink{
Name: "test3",
Pattern: ".*3",
Template: "test3",
},
},
expectStatus: http.StatusOK,
expectSaveCalled: true,
expectSaved: []autolink.Autolink{
autolink.Autolink{
Name: "test1",
Pattern: ".*1",
Template: "test1",
},
autolink.Autolink{
Name: "test2",
Pattern: ".*2",
Template: "new template",
},
autolink.Autolink{
Name: "test3",
Pattern: ".*3",
Template: "test3",
},
},
},
{
name: "no change",
link: autolink.Autolink{
Name: "test2",
Pattern: ".*2",
Template: "test2",
},
prevLinks: []autolink.Autolink{
autolink.Autolink{
Name: "test1",
Pattern: ".*1",
Template: "test1",
},
autolink.Autolink{
Name: "test2",
Pattern: ".*2",
Template: "test2",
},
},
expectStatus: http.StatusNotModified,
expectSaveCalled: false,
},
} {
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{},
)

body, err := json.Marshal(tc.link)
require.NoError(t, err)

w := httptest.NewRecorder()
method := "POST"
if tc.method != "" {
method = tc.method
}
r, err := http.NewRequest(method, "/api/v1/link", bytes.NewReader(body))
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)
require.Equal(t, tc.expectSaveCalled, saveCalled)
require.Equal(t, tc.expectSaved, saved)
})
}
}

0 comments on commit 8c82b7d

Please sign in to comment.