-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MM-17818 Adding API for adding/replacing links. (#85)
* 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 <[email protected]>
- Loading branch information
Showing
14 changed files
with
691 additions
and
102 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"}`)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} | ||
} |
Oops, something went wrong.