diff --git a/app/main.go b/app/main.go index c60e41c8..a73cf853 100644 --- a/app/main.go +++ b/app/main.go @@ -17,8 +17,8 @@ import ( "github.com/pkg/errors" "github.com/umputun/remark/app/migrator" + "github.com/umputun/remark/app/rest/api" "github.com/umputun/remark/app/rest/auth" - "github.com/umputun/remark/app/rest/server" "github.com/umputun/remark/app/store" ) @@ -108,7 +108,7 @@ func main() { DefaultAvatar: opts.ServerCommand.DefaultAvatar, } - srv := server.Rest{ + srv := api.Rest{ Version: revision, DataService: dataService, Exporter: &exporter, diff --git a/app/rest/server/admin.go b/app/rest/server/admin.go deleted file mode 100644 index 20b1b8c0..00000000 --- a/app/rest/server/admin.go +++ /dev/null @@ -1,158 +0,0 @@ -package server - -import ( - "compress/gzip" - "fmt" - "io" - "log" - "net/http" - "time" - - "github.com/go-chi/chi" - "github.com/go-chi/render" - - "github.com/umputun/remark/app/migrator" - "github.com/umputun/remark/app/rest" - "github.com/umputun/remark/app/store" -) - -// admin provides router for all requests available for admin users only -type admin struct { - dataService store.Service - exporter migrator.Exporter - importer migrator.Importer - cache rest.LoadingCache - defAvatarURL string -} - -func (a *admin) routes(middlewares ...func(http.Handler) http.Handler) chi.Router { - router := chi.NewRouter() - router.Use(middlewares...) - router.Delete("/comment/{id}", a.deleteCommentCtrl) - router.Put("/user/{userid}", a.setBlockCtrl) - router.Get("/export", a.exportCtrl) - router.Post("/import", a.importCtrl) - router.Put("/pin/{id}", a.setPinCtrl) - router.Get("/blocked", a.blockedUsersCtrl) - return router -} - -// DELETE /comment/{id}?site=siteID&url=post-url - removes comment -func (a *admin) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) { - - id := chi.URLParam(r, "id") - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - log.Printf("[INFO] delete comment %s", id) - - err := a.dataService.Delete(locator, id) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't delete comment") - return - } - a.cache.Flush() - render.Status(r, http.StatusOK) - render.JSON(w, r, JSON{"id": id, "locator": locator}) -} - -// PUT /user/{userid}?site=side-id&block=1 - block or unblock user -func (a *admin) setBlockCtrl(w http.ResponseWriter, r *http.Request) { - userID := chi.URLParam(r, "userid") - siteID := r.URL.Query().Get("site") - blockStatus := r.URL.Query().Get("block") == "1" - - if err := a.dataService.SetBlock(siteID, userID, blockStatus); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set blocking status") - return - } - a.cache.Flush() - render.JSON(w, r, JSON{"user_id": userID, "site_id": siteID, "block": blockStatus}) -} - -// GET /blocked?site=siteID - list blocked users -func (a *admin) blockedUsersCtrl(w http.ResponseWriter, r *http.Request) { - siteID := r.URL.Query().Get("site") - users, err := a.dataService.Blocked(siteID) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get blocked users") - return - } - render.JSON(w, r, users) -} - -// PUT /pin/{id}?site=siteID&url=post-url&pin=1 -// mark/unmark comment as a special -func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) { - commentID := chi.URLParam(r, "id") - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - pinStatus := r.URL.Query().Get("pin") == "1" - - if err := a.dataService.SetPin(locator, commentID, pinStatus); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set pin status") - return - } - a.cache.Flush() - render.JSON(w, r, JSON{"id": commentID, "locator": locator, "pin": pinStatus}) -} - -// GET /export?site=site-id?mode=file|stream -// exports all comments for siteID as json stream or gz file -func (a *admin) exportCtrl(w http.ResponseWriter, r *http.Request) { - siteID := r.URL.Query().Get("site") - var writer io.Writer = w - if r.URL.Query().Get("mode") == "file" { - exportFile := fmt.Sprintf("%s-%s.json.gz", siteID, time.Now().Format("20060102")) - w.Header().Set("Content-Type", "application/gzip") - w.Header().Set("Content-Disposition", "attachment;filename="+exportFile) - w.WriteHeader(http.StatusOK) - writer = gzip.NewWriter(w) - } - - if err := a.exporter.Export(writer, siteID); err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed") - } -} - -// POST /import?site=site-id -// imports comments from post body. -func (a *admin) importCtrl(w http.ResponseWriter, r *http.Request) { - siteID := r.URL.Query().Get("site") - if err := a.importer.Import(r.Body, siteID); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "import failed") - } - a.cache.Flush() -} - -func (a *admin) checkBlocked(siteID string, user store.User) bool { - return a.dataService.IsBlocked(siteID, user.ID) -} - -// processes comments and hides text of all comments for blocked users. -// resets score and votes too. Also hides sensitive info for non-admin users -func (a *admin) alterComments(comments []store.Comment, r *http.Request) (res []store.Comment) { - res = make([]store.Comment, len(comments)) - - user, err := rest.GetUserInfo(r) - isAdmin := (err == nil && user.Admin) // make seprate cache key for admins - - for i, c := range comments { - - // process blocked users - if a.dataService.IsBlocked(c.Locator.SiteID, c.User.ID) { - c.Mask() - c.User.Blocked = true - } - - // set default avatar - if c.User.Picture == "" { - c.User.Picture = a.defAvatarURL - } - - // hide info from non-admins - if !isAdmin { - c.User.IP = "" - } - - res[i] = c - } - return res -} diff --git a/app/rest/server/admin_test.go b/app/rest/server/admin_test.go deleted file mode 100644 index 69d4113b..00000000 --- a/app/rest/server/admin_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/umputun/remark/app/store" -) - -func TestAdmin_Delete(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} - - id1 := addComment(t, c1, port) - addComment(t, c2, port) - - client := http.Client{} - req, err := http.NewRequest(http.MethodDelete, - fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/admin/comment/%s?site=radio-t&url=https://radio-t.com/blah", - port, id1), nil) - assert.Nil(t, err) - resp, err := client.Do(req) - assert.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - - body, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", port, id1)) - assert.Equal(t, 200, code) - cr := store.Comment{} - err = json.Unmarshal([]byte(body), &cr) - assert.Nil(t, err) - assert.Equal(t, "this comment was deleted", cr.Text) - assert.True(t, cr.Deleted) -} - -func TestAdmin_Pin(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} - - id1 := addComment(t, c1, port) - addComment(t, c2, port) - - pin := func(val int) int { - client := http.Client{} - req, err := http.NewRequest(http.MethodPut, - fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/admin/pin/%s?site=radio-t&url=https://radio-t.com/blah&pin=%d", port, id1, val), nil) - assert.Nil(t, err) - resp, err := client.Do(req) - assert.Nil(t, err) - return resp.StatusCode - } - - code := pin(1) - assert.Equal(t, 200, code) - - body, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", port, id1)) - assert.Equal(t, 200, code) - cr := store.Comment{} - err := json.Unmarshal([]byte(body), &cr) - assert.Nil(t, err) - assert.True(t, cr.Pin) - - code = pin(-1) - assert.Equal(t, 200, code) - body, code = get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", port, id1)) - assert.Equal(t, 200, code) - cr = store.Comment{} - err = json.Unmarshal([]byte(body), &cr) - assert.Nil(t, err) - assert.False(t, cr.Pin) -} - -func TestAdmin_Block(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user2", ID: "user2"}} - - _, err := srv.DataService.Create(c1) - assert.Nil(t, err) - _, err = srv.DataService.Create(c2) - assert.Nil(t, err) - - block := func(val int) (code int, body []byte) { - client := http.Client{} - req, err := http.NewRequest(http.MethodPut, - fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/admin/user/%s?site=radio-t&block=%d", port, "user1", val), nil) - assert.Nil(t, err) - resp, err := client.Do(req) - require.Nil(t, err) - body, err = ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - resp.Body.Close() - return resp.StatusCode, body - } - - code, body := block(1) - require.Equal(t, 200, code) - j := JSON{} - err = json.Unmarshal(body, &j) - assert.Nil(t, err) - assert.Equal(t, "user1", j["user_id"]) - assert.Equal(t, true, j["block"]) - assert.Equal(t, "radio-t", j["site_id"]) - - res, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/find?site=radio-t&url=https://radio-t.com/blah&sort=+time", port)) - assert.Equal(t, 200, code) - comments := []store.Comment{} - err = json.Unmarshal([]byte(res), &comments) - assert.Nil(t, err) - assert.Equal(t, 2, len(comments), "should have 2 comments") - assert.Equal(t, "this comment was deleted", comments[0].Text) - - code, body = block(-1) - require.Equal(t, 200, code) - err = json.Unmarshal(body, &j) - assert.Nil(t, err) - assert.Equal(t, false, j["block"]) -} - -func TestAdmin_Export(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} - - addComment(t, c1, port) - addComment(t, c2, port) - - body, code := get(t, fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/admin/export?site=radio-t&mode=stream", port)) - assert.Equal(t, 200, code) - assert.Equal(t, 2, strings.Count(body, "\n")) - assert.Equal(t, 2, strings.Count(body, "\"text\"")) - t.Logf("%s", body) -} diff --git a/app/rest/server/middleware.go b/app/rest/server/middleware.go deleted file mode 100644 index 67078fa4..00000000 --- a/app/rest/server/middleware.go +++ /dev/null @@ -1,159 +0,0 @@ -package server - -import ( - "bytes" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/url" - "os" - "regexp" - "runtime/debug" - "strings" - "time" - - "github.com/go-chi/chi/middleware" - "github.com/umputun/remark/app/rest" -) - -// JSON is a map alias, just for convenience -type JSON map[string]interface{} - -// AppInfo adds custom app-info to the response header -func AppInfo(app string, version string) func(http.Handler) http.Handler { - f := func(h http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Org", "Umputun") - w.Header().Set("App-Name", app) - w.Header().Set("App-Version", version) - if mhost := os.Getenv("MHOST"); mhost != "" { - w.Header().Set("Host", mhost) - } - h.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) - } - return f -} - -// Ping middleware response with pong to /ping. Stops chain if ping request detected -func Ping(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - - if r.Method == "GET" && strings.HasSuffix(strings.ToLower(r.URL.Path), "/ping") { - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte("pong")); err != nil { - log.Printf("[WARN] can't send pong, %s", err) - } - return - } - next.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) -} - -// Recoverer is a middleware that recovers from panics, logs the panic and returns a HTTP 500 status if possible. -func Recoverer(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - defer func() { - if rvr := recover(); rvr != nil { - log.Printf("[WARN] request panic, %v", rvr) - debug.PrintStack() - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }() - next.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) -} - -// LoggerFlag type -type LoggerFlag int - -// logger flags enum -const ( - LogAll LoggerFlag = iota - LogUser - LogBody -) -const maxBody = 1024 - -var reMultWhtsp = regexp.MustCompile(`[\s\p{Zs}]{2,}`) - -// Logger middleware prints http log. Customized by set of LoggerFlag -func Logger(flags ...LoggerFlag) func(http.Handler) http.Handler { - - inFlags := func(f LoggerFlag) bool { - for _, flg := range flags { - if flg == LogAll || flg == f { - return true - } - } - return false - } - - f := func(h http.Handler) http.Handler { - - fn := func(w http.ResponseWriter, r *http.Request) { - ww := middleware.NewWrapResponseWriter(w, 1) - - body, user := func() (body string, user string) { - ctx := r.Context() - if ctx == nil { - return "", "" - } - - if inFlags(LogBody) { - if content, err := ioutil.ReadAll(r.Body); err == nil { - body = string(content) - r.Body = ioutil.NopCloser(bytes.NewReader(content)) - - if len(body) > 0 { - body = strings.Replace(body, "\n", " ", -1) - body = reMultWhtsp.ReplaceAllString(body, " ") - } - - if len(body) > maxBody { - body = body[:maxBody] + "..." - } - } - } - - if inFlags(LogUser) { - u, err := rest.GetUserInfo(r) - if err == nil && u.Name != "" { - user = fmt.Sprintf(" - %s %q", u.ID, u.Name) - } - } - - return body, user - }() - - t1 := time.Now() - defer func() { - t2 := time.Now() - - q := r.URL.String() - if qun, err := url.QueryUnescape(q); err == nil { - q = qun - } - - remoteIP := strings.Split(r.RemoteAddr, ":")[0] - if strings.HasPrefix(r.RemoteAddr, "[") { - remoteIP = strings.Split(r.RemoteAddr, "]:")[0] + "]" - } - - log.Printf("[INFO] REST %s - %s - %s - %d (%d) - %v %s %s", - r.Method, q, remoteIP, ww.Status(), ww.BytesWritten(), t2.Sub(t1), user, body) - }() - - h.ServeHTTP(ww, r) - } - return http.HandlerFunc(fn) - } - - return f - -} diff --git a/app/rest/server/rest.go b/app/rest/server/rest.go deleted file mode 100644 index 2f4cf589..00000000 --- a/app/rest/server/rest.go +++ /dev/null @@ -1,508 +0,0 @@ -package server - -import ( - "bytes" - "encoding/json" - "fmt" - "log" - "net/http" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/didip/tollbooth" - "github.com/didip/tollbooth_chi" - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" - "github.com/go-chi/render" - "github.com/gorilla/context" - "github.com/pkg/errors" - "gopkg.in/russross/blackfriday.v2" - - "github.com/umputun/remark/app/migrator" - "github.com/umputun/remark/app/notifier" - "github.com/umputun/remark/app/rest" - "github.com/umputun/remark/app/rest/auth" - "github.com/umputun/remark/app/rest/format" - "github.com/umputun/remark/app/store" -) - -// Rest is a rest access server -type Rest struct { - Version string - - DataService store.Service - Authenticator auth.Authenticator - Exporter migrator.Exporter - Cache rest.LoadingCache - Notifier notifier.Interface - - httpServer *http.Server - mod admin -} - -// Run the lister and request's router, activate rest server -func (s *Rest) Run(port int) { - log.Print("[INFO] activate rest server") - - if len(s.Authenticator.Admins) > 0 { - log.Printf("[DEBUG] admins %+v", s.Authenticator.Admins) - } - - router := chi.NewRouter() - router.Use(middleware.RealIP, Recoverer) - router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second)) - router.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil))) - - // all request by default allow anonymous access - router.Use(s.Authenticator.Auth(false)) - - router.Use(AppInfo("remark42", s.Version), Ping, Logger(LogAll)) - router.Use(context.ClearHandler) // if you aren't using gorilla/mux, you need to wrap your handlers with context.ClearHandler - - // auth routes for all providers - router.Route("/auth", func(r chi.Router) { - for _, provider := range s.Authenticator.Providers { - r.Mount("/"+provider.Name, provider.Routes()) // mount auth providers as /auth/{name} - } - if len(s.Authenticator.Providers) > 0 { - // shortcut, can be any of providers, all logouts do the same - removes cookie - r.Get("/logout", s.Authenticator.Providers[0].LogoutHandler) - } - }) - - router.Mount(s.Authenticator.AvatarProxy.Routes()) - - // api routes - router.Route("/api/v1", func(rapi chi.Router) { - - // open routes - rapi.Get("/find", s.findCommentsCtrl) - rapi.Get("/id/{id}", s.commentByIDCtrl) - rapi.Get("/comments", s.findUserCommentsCtrl) - rapi.Get("/last/{max}", s.lastCommentsCtrl) - rapi.Get("/count", s.countCtrl) - rapi.Get("/list", s.listCtrl) - rapi.Get("/config", s.configCtrl) - - // protected routes, require auth - rapi.With(s.Authenticator.Auth(true)).Group(func(rauth chi.Router) { - rauth.Post("/comment", s.createCommentCtrl) - rauth.Put("/comment/{id}", s.updateCommentCtrl) - rauth.Get("/user", s.userInfoCtrl) - rauth.Put("/vote/{id}", s.voteCtrl) - rauth.Put("/notify", s.notifyActionCtrl) - rauth.Get("/notify", s.notifyStatusCtrl) - // admin routes, admin users only - s.mod = admin{ - dataService: s.DataService, - exporter: s.Exporter, - cache: s.Cache, - defAvatarURL: s.Authenticator.AvatarProxy.Default(), - } - rauth.Mount("/admin", s.mod.routes(s.Authenticator.AdminOnly)) - }) - }) - - // add robots and file server for static content from /web - router.Get("/robots.txt", func(w http.ResponseWriter, r *http.Request) { - render.PlainText(w, r, "User-agent: *\nDisallow: /auth/\nDisallow: /api/\n") - }) - s.addFileServer(router, "/web", http.Dir(filepath.Join(".", "web"))) - - s.httpServer = &http.Server{Addr: fmt.Sprintf(":%d", port), Handler: router} - err := s.httpServer.ListenAndServe() - log.Printf("[WARN] http server terminated, %s", err) -} - -// POST /comment - adds comment, resets all immutable fields -func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) { - - comment := store.Comment{} - if err := render.DecodeJSON(r.Body, &comment); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment") - return - } - - user, err := rest.GetUserInfo(r) - if err != nil { // this not suppose to happen (handled by Auth), just dbl-check - rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") - return - } - - // reset comment to initial state - func() { - comment.ID = "" // don't allow user to define ID, force auto-gen - comment.Timestamp = time.Time{} // reset time, force auto-gen - comment.Votes = make(map[string]bool) - comment.Score = 0 - comment.Edit = nil - comment.Pin = false - }() - - comment.User = user - comment.User.IP = strings.Split(r.RemoteAddr, ":")[0] - - // render markdown - comment.Text = string(blackfriday.Run([]byte(comment.Text), blackfriday.WithNoExtensions())) - - log.Printf("[DEBUG] create comment %+v", comment) - - // check if user blocked - if s.mod.checkBlocked(comment.Locator.SiteID, comment.User) { - rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "user blocked") - return - } - - id, err := s.DataService.Create(comment) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't save comment") - return - } - - if err = s.Notifier.OnUpdate(comment); err != nil { - log.Printf("[WARN] can't send notify event for %+v, %s", comment.Locator, err) - } - - s.Cache.Flush() // reset all caches - render.Status(r, http.StatusCreated) - render.JSON(w, r, JSON{"id": id, "locator": comment.Locator}) -} - -// PUT /comment/{id}?site=siteID&url=post-url - update comment -func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) { - - edit := struct { - Text string - Summary string - }{} - - if err := render.DecodeJSON(r.Body, &edit); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment") - return - } - - user, err := rest.GetUserInfo(r) - if err != nil { // this not suppose to happen (handled by Auth), just dbl-check - rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") - return - } - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - id := chi.URLParam(r, "id") - - // render markdown - edit.Text = string(blackfriday.Run([]byte(edit.Text), blackfriday.WithNoExtensions())) - - log.Printf("[DEBUG] update comment %s, %+v", id, edit) - - var currComment store.Comment - if currComment, err = s.DataService.Get(locator, id); err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't find comment") - return - } - - if currComment.User.ID != user.ID { - rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "can not edit comments for other users") - return - } - - res, err := s.DataService.EditComment(locator, id, edit.Text, store.Edit{Summary: edit.Summary}) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't update comment") - return - } - - s.Cache.Flush() // reset all caches - render.JSON(w, r, res) -} - -// GET /find?site=siteID&url=post-url&format=[tree|plain]&sort=[+/-time|+/-score] -// find comments for given post. Returns in tree or plain formats, sorted -func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) { - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - log.Printf("[DEBUG] get comments for %+v", locator) - - data, err := s.Cache.Get(s.urlKey(r), time.Hour, func() ([]byte, error) { - comments, e := s.DataService.Find(locator, r.URL.Query().Get("sort")) - if e != nil { - return nil, e - } - maskedComments := s.mod.alterComments(comments, r) - var b []byte - switch r.URL.Query().Get("format") { - case "tree": - b, e = encodeJSONWithHTML(format.MakeTree(maskedComments, r.URL.Query().Get("sort"))) - default: - b, e = encodeJSONWithHTML(maskedComments) - } - return b, e - }) - - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't find comments") - return - } - renderJSONFromBytes(w, r, data) -} - -// GET /last/{max}?site=siteID - last comments for the siteID, across all posts, sorted by time -func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) { - - log.Printf("[DEBUG] get last comments for %s", r.URL.Query().Get("site")) - - max, err := strconv.Atoi(chi.URLParam(r, "max")) - if err != nil { - max = 0 - } - - data, err := s.Cache.Get(s.urlKey(r), time.Hour, func() ([]byte, error) { - comments, e := s.DataService.Last(r.URL.Query().Get("site"), max) - if e != nil { - return nil, e - } - comments = s.mod.alterComments(comments, r) - return encodeJSONWithHTML(comments) - }) - - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't get last comments") - return - } - renderJSONFromBytes(w, r, data) -} - -// GET /id/{id}?site=siteID&url=post-url - gets a comment by id -func (s *Rest) commentByIDCtrl(w http.ResponseWriter, r *http.Request) { - - id := chi.URLParam(r, "id") - siteID := r.URL.Query().Get("site") - url := r.URL.Query().Get("url") - - log.Printf("[DEBUG] get comments by id %s, %s %s", id, siteID, url) - - comment, err := s.DataService.Get(store.Locator{SiteID: siteID, URL: url}, id) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by id") - return - } - comment = s.mod.alterComments([]store.Comment{comment}, r)[0] - render.Status(r, http.StatusOK) - renderJSONWithHTML(w, r, comment) -} - -// GET /comments?site=siteID&user=id - returns comments for given userID -func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) { - - userID := r.URL.Query().Get("user") - siteID := r.URL.Query().Get("site") - - resp := struct { - Comments []store.Comment - Count int - }{} - - log.Printf("[DEBUG] get comments for userID %s, %s", userID, siteID) - - data, err := s.Cache.Get(s.urlKey(r), time.Hour, func() ([]byte, error) { - comments, count, e := s.DataService.User(siteID, userID) - if e != nil { - return nil, e - } - comments = s.mod.alterComments(comments, r) - resp.Comments, resp.Count = comments, count - return encodeJSONWithHTML(resp) - }) - - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by user id") - return - } - renderJSONFromBytes(w, r, data) -} - -// GET /config?site=siteID - returns configuration -func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) { - type config struct { - Version string `json:"version"` - EditDuration int `json:"edit_duration"` - Admins []string `json:"admins"` - Auth []string `json:"auth_providers"` - } - - cnf := config{ - Version: s.Version, - EditDuration: int(s.DataService.EditDuration.Seconds()), - Admins: s.Authenticator.Admins, - } - authNames := []string{} - for _, ap := range s.Authenticator.Providers { - authNames = append(authNames, ap.Name) - } - cnf.Auth = authNames - render.Status(r, http.StatusOK) - render.JSON(w, r, cnf) -} - -// GET /user - returns user info -func (s *Rest) userInfoCtrl(w http.ResponseWriter, r *http.Request) { - user, err := rest.GetUserInfo(r) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") - return - } - render.JSON(w, r, user) -} - -// GET /count?site=siteID&url=post-url - get number of comments for given post -func (s *Rest) countCtrl(w http.ResponseWriter, r *http.Request) { - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - count, err := s.DataService.Count(locator) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get count") - return - } - render.JSON(w, r, JSON{"count": count, "locator": locator}) -} - -// GET /list?site=siteID - list posts with comments -func (s *Rest) listCtrl(w http.ResponseWriter, r *http.Request) { - - siteID := r.URL.Query().Get("site") - data, err := s.Cache.Get(s.urlKey(r), 8*time.Hour, func() ([]byte, error) { - posts, e := s.DataService.List(siteID) - if e != nil { - return nil, e - } - return encodeJSONWithHTML(posts) - }) - - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get list of comments for "+siteID) - return - } - renderJSONFromBytes(w, r, data) -} - -// PUT /vote/{id}?site=siteID&url=post-url&vote=1 - vote for/against comment -func (s *Rest) voteCtrl(w http.ResponseWriter, r *http.Request) { - - user, err := rest.GetUserInfo(r) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") - return - } - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - id := chi.URLParam(r, "id") - log.Printf("[DEBUG] vote for comment %s", id) - - vote := r.URL.Query().Get("vote") == "1" - - comment, err := s.DataService.Vote(locator, id, user.ID, vote) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't vote for comment") - return - } - s.Cache.Flush() - render.JSON(w, r, JSON{"id": comment.ID, "score": comment.Score}) -} - -// PUT /notify?site=siteID&url=post-url&action=1 - subscribe/unsubscribe to notification -func (s *Rest) notifyActionCtrl(w http.ResponseWriter, r *http.Request) { - user, err := rest.GetUserInfo(r) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") - return - } - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - action := "unknown" - switch r.URL.Query().Get("action") { - case "1": - err = s.Notifier.Subscribe(locator, user) - action = "subscribe" - case "0": - err = s.Notifier.UnSubscribe(locator, user) - action = "unsubscribe" - } - if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't subscribe/unsubscribe for notifications") - return - } - render.JSON(w, r, JSON{"locator": locator, "user": user.ID, "action": action}) -} - -// GET /notify?site=siteID&url=post-url - get notification status -func (s *Rest) notifyStatusCtrl(w http.ResponseWriter, r *http.Request) { - user, err := rest.GetUserInfo(r) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") - return - } - locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - status := "not subscribed" - if st, err := s.Notifier.Status(locator, user); err == nil && st { - status = "subscribed" - } - render.JSON(w, r, JSON{"locator": locator, "user": user.ID, "status": status}) -} - -// serves static files from /web -func (s *Rest) addFileServer(r chi.Router, path string, root http.FileSystem) { - log.Printf("[INFO] run file server for %s", root) - fs := http.StripPrefix(path, http.FileServer(root)) - if path != "/" && path[len(path)-1] != '/' { - r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP) - path += "/" - } - path += "*" - - r.Get(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // don't show dirs, just serve files - if strings.HasSuffix(r.URL.Path, "/") && len(r.URL.Path) > 1 && r.URL.Path != "/web/" { - http.NotFound(w, r) - return - } - fs.ServeHTTP(w, r) - })) -} - -// urlKey gets url from request to use is as cache key -// admins will have separate keys in order tp prevent leak of admin-only data to regular users -func (s *Rest) urlKey(r *http.Request) string { - key := r.URL.String() - if user, err := rest.GetUserInfo(r); err == nil && user.Admin { // make seprate cache key for admins - key = "admin!!" + key - } - return key -} - -// renderJSONWithHTML allows html tags and forces charset=utf-8 -func renderJSONWithHTML(w http.ResponseWriter, r *http.Request, v interface{}) { - data, err := encodeJSONWithHTML(v) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't render json response") - return - } - renderJSONFromBytes(w, r, data) -} - -func encodeJSONWithHTML(v interface{}) ([]byte, error) { - buf := &bytes.Buffer{} - enc := json.NewEncoder(buf) - enc.SetEscapeHTML(false) - if err := enc.Encode(v); err != nil { - return nil, errors.Wrap(err, "can't encode to json") - } - return buf.Bytes(), nil -} - -// renderJSONWithHTML allows html tags and forces charset=utf-8 -func renderJSONFromBytes(w http.ResponseWriter, r *http.Request, data []byte) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - if status, ok := r.Context().Value(render.StatusCtxKey).(int); ok { - w.WriteHeader(status) - } - if _, err := w.Write(data); err != nil { - log.Printf("[WARN] can't send response to %s, %s", r.RemoteAddr, err) - } -} diff --git a/app/rest/server/rest_test.go b/app/rest/server/rest_test.go deleted file mode 100644 index 1b8a9be5..00000000 --- a/app/rest/server/rest_test.go +++ /dev/null @@ -1,405 +0,0 @@ -package server - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io/ioutil" - "math/rand" - "net/http" - "os" - "strings" - "testing" - "time" - - "github.com/gorilla/sessions" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/umputun/remark/app/migrator" - "github.com/umputun/remark/app/notifier" - "github.com/umputun/remark/app/rest/auth" - "github.com/umputun/remark/app/store" -) - -var testDb = "/tmp/test-remark.db" - -func TestServer_Ping(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - res, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/ping", port)) - assert.Equal(t, "pong", res) - assert.Equal(t, 200, code) -} - -func TestServer_Create(t *testing.T) { - srv, port := prep(t) - require.NotNil(t, srv) - defer cleanup(srv) - - r := strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) - resp, err := http.Post(fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/comment", port), "application/json", r) - assert.Nil(t, err) - assert.Equal(t, http.StatusCreated, resp.StatusCode) - - b, err := ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - c := JSON{} - err = json.Unmarshal(b, &c) - assert.Nil(t, err) - loc := c["locator"].(map[string]interface{}) - assert.Equal(t, "radio-t", loc["site"]) - assert.Equal(t, "https://radio-t.com/blah1", loc["url"]) - assert.True(t, len(c["id"].(string)) > 8) -} - -func TestServer_CreateAndGet(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - // create comment - r := strings.NewReader(`{"text": "**test** *123* http://radio-t.com", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) - resp, err := http.Post(fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/comment", port), "application/json", r) - assert.Nil(t, err) - assert.Equal(t, http.StatusCreated, resp.StatusCode) - b, err := ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - c := JSON{} - err = json.Unmarshal(b, &c) - assert.Nil(t, err) - - id := c["id"].(string) - - // get created comment by id - res, code := get(t, fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", port, id)) - assert.Equal(t, 200, code) - comment := store.Comment{} - err = json.Unmarshal([]byte(res), &comment) - assert.Nil(t, err) - assert.Equal(t, "
test 123 http://radio-t.com
", comment.Text) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", - Picture: "https://friends.radio-t.com/resources/images/rt_logo_64.png", - Profile: "https://radio-t.com/info/", Admin: true, Blocked: false, IP: "127.0.0.1"}, - comment.User) - t.Logf("%+v", comment) -} - -func TestServer_Find(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - _, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/find?site=radio-t&url=https://radio-t.com/blah1", port)) - assert.Equal(t, 400, code, "nothing in") - - c1 := store.Comment{Text: "test test #1", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - - id1 := addComment(t, c1, port) - id2 := addComment(t, c2, port) - assert.NotEqual(t, id1, id2) - - // get sorted by +time - res, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&sort=+time", port)) - assert.Equal(t, 200, code) - comments := []store.Comment{} - err := json.Unmarshal([]byte(res), &comments) - assert.Nil(t, err) - assert.Equal(t, 2, len(comments), "should have 2 comments") - assert.Equal(t, id1, comments[0].ID) - assert.Equal(t, id2, comments[1].ID) - - // get sorted by -time - res, code = get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&sort=-time", port)) - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(res), &comments) - assert.Nil(t, err) - assert.Equal(t, 2, len(comments), "should have 2 comments") - assert.Equal(t, id1, comments[1].ID) - assert.Equal(t, id2, comments[0].ID) -} - -func TestServer_Update(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - c1 := store.Comment{Text: "test test #1", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - id := addComment(t, c1, port) - - client := http.Client{} - req, err := http.NewRequest(http.MethodPut, - fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/comment/"+id+"?site=radio-t&url=https://radio-t.com/blah1", port), - strings.NewReader(`{"text":"updated text", "summary":"my edit"}`)) - assert.Nil(t, err) - b, err := client.Do(req) - assert.Nil(t, err) - body, err := ioutil.ReadAll(b.Body) - assert.Nil(t, err) - assert.Equal(t, 200, b.StatusCode, string(body)) - - // comments returned by update - c2 := store.Comment{} - err = json.Unmarshal(body, &c2) - assert.Nil(t, err) - assert.Equal(t, id, c2.ID) - assert.Equal(t, "updated text
", c2.Text) - assert.Equal(t, "my edit", c2.Edit.Summary) - assert.True(t, time.Since(c2.Edit.Timestamp) < 1*time.Second) - - // read updated comment - res, code := get(t, fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", port, id)) - assert.Equal(t, 200, code) - c3 := store.Comment{} - err = json.Unmarshal([]byte(res), &c3) - assert.Nil(t, err) - assert.Equal(t, c2, c3, "same as response from update") -} - -func TestServer_Last(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - c1 := store.Comment{Text: "test test #1", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} - - // add 3 comments - addComment(t, c1, port) - id1 := addComment(t, c1, port) - id2 := addComment(t, c2, port) - - res, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/last/2?site=radio-t", port)) - assert.Equal(t, 200, code) - comments := []store.Comment{} - err := json.Unmarshal([]byte(res), &comments) - assert.Nil(t, err) - assert.Equal(t, 2, len(comments), "should have 2 comments") - assert.Equal(t, id1, comments[1].ID) - assert.Equal(t, id2, comments[0].ID) - - res, code = get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/last/5?site=radio-t", port)) - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(res), &comments) - assert.Nil(t, err) - assert.Equal(t, 3, len(comments), "should have 3 comments") -} - -func TestServer_FindUserComments(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - c2 := store.Comment{Text: "test test #3", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} - - // add 3 comments - addComment(t, c1, port) - addComment(t, c2, port) - addComment(t, c2, port) - - _, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/comments?site=radio-t&user=blah", port)) - assert.Equal(t, 400, code, "noting for user blah") - - res, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/comments?site=radio-t&user=dev", port)) - assert.Equal(t, 200, code) - - resp := struct { - Comments []store.Comment - Count int - }{} - - err := json.Unmarshal([]byte(res), &resp) - assert.Nil(t, err) - assert.Equal(t, 3, len(resp.Comments), "should have 3 comments") - assert.Equal(t, 3, resp.Count, "should have 3 count") -} - -func TestServer_UserInfo(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - body, code := get(t, fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/user?site=radio-t", port)) - assert.Equal(t, 200, code) - user := store.User{} - err := json.Unmarshal([]byte(body), &user) - assert.Nil(t, err) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", - Picture: "https://friends.radio-t.com/resources/images/rt_logo_64.png", Profile: "https://radio-t.com/info/", - Admin: true, Blocked: false, IP: ""}, user) -} - -func TestServer_Vote(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} - - id1 := addComment(t, c1, port) - addComment(t, c2, port) - - vote := func(val int) int { - client := http.Client{} - req, err := http.NewRequest(http.MethodPut, - fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/vote/%s?site=radio-t&url=https://radio-t.com/blah&vote=%d", - port, id1, val), nil) - assert.Nil(t, err) - resp, err := client.Do(req) - assert.Nil(t, err) - return resp.StatusCode - } - - assert.Equal(t, 200, vote(1), "first vote allowed") - assert.Equal(t, 400, vote(1), "second vote rejected") - body, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", port, id1)) - assert.Equal(t, 200, code) - cr := store.Comment{} - err := json.Unmarshal([]byte(body), &cr) - assert.Nil(t, err) - assert.Equal(t, 1, cr.Score) - assert.Equal(t, map[string]bool{"dev": true}, cr.Votes) - - assert.Equal(t, 200, vote(-1), "opposite vote allowed") - body, code = get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", port, id1)) - assert.Equal(t, 200, code) - cr = store.Comment{} - err = json.Unmarshal([]byte(body), &cr) - assert.Nil(t, err) - assert.Equal(t, 0, cr.Score) - assert.Equal(t, map[string]bool{}, cr.Votes) - -} - -func TestServer_Count(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} - - addComment(t, c1, port) - addComment(t, c1, port) - addComment(t, c1, port) - addComment(t, c2, port) - addComment(t, c2, port) - - body, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/count?site=radio-t&url=https://radio-t.com/blah1", port)) - assert.Equal(t, 200, code) - j := JSON{} - err := json.Unmarshal([]byte(body), &j) - assert.Nil(t, err) - assert.Equal(t, 3.0, j["count"]) - - body, code = get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/count?site=radio-t&url=https://radio-t.com/blah2", port)) - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(body), &j) - assert.Nil(t, err) - assert.Equal(t, 2.0, j["count"]) -} - -func TestServer_List(t *testing.T) { - srv, port := prep(t) - assert.NotNil(t, srv) - defer cleanup(srv) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} - - addComment(t, c1, port) - addComment(t, c1, port) - addComment(t, c1, port) - addComment(t, c2, port) - addComment(t, c2, port) - - body, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/list?site=radio-t", port)) - assert.Equal(t, 200, code) - pi := []store.PostInfo{} - err := json.Unmarshal([]byte(body), &pi) - assert.Nil(t, err) - assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/blah1", Count: 3}, {URL: "https://radio-t.com/blah2", Count: 2}}, pi) -} - -func prep(t *testing.T) (srv *Rest, port int) { - dataStore, err := store.NewBoltDB(store.BoltSite{FileName: testDb, SiteID: "radio-t"}) - require.Nil(t, err) - srv = &Rest{ - DataService: store.Service{Interface: dataStore, EditDuration: 5 * time.Minute}, - Authenticator: auth.Authenticator{ - SessionStore: sessions.NewFilesystemStore("/tmp", []byte("blah")), - DevEnabled: true, - DevPasswd: "password", - Providers: nil, - AvatarProxy: &auth.AvatarProxy{StorePath: "/tmp", RoutePath: "/api/v1/avatar"}, - }, - Exporter: &migrator.Remark{DataStore: dataStore}, - Cache: &mockCache{}, - Notifier: notifier.NewNoOperation(), - } - go func() { - port = rand.Intn(50000) + 1025 - srv.Run(port) - }() - time.Sleep(100 * time.Millisecond) - return srv, port -} - -func get(t *testing.T, url string) (string, int) { - r, err := http.Get(url) - assert.Nil(t, err) - defer r.Body.Close() - body, err := ioutil.ReadAll(r.Body) - assert.Nil(t, err) - return string(body), r.StatusCode -} - -func addComment(t *testing.T, c store.Comment, port int) string { - - b, err := json.Marshal(c) - assert.Nil(t, err, "can't marshal comment %+v", c) - resp, err := http.Post(fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/comment", port), "application/json", bytes.NewBuffer(b)) - assert.Nil(t, err) - assert.Equal(t, http.StatusCreated, resp.StatusCode) - b, err = ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - - crResp := JSON{} - err = json.Unmarshal(b, &crResp) - assert.Nil(t, err) - time.Sleep(time.Nanosecond * 10) - return crResp["id"].(string) -} - -func cleanup(srv *Rest) { - srv.httpServer.Close() - srv.httpServer.Shutdown(context.Background()) - os.Remove(testDb) -} - -type mockCache struct{} - -func (mc *mockCache) Get(key string, ttl time.Duration, fn func() ([]byte, error)) (data []byte, err error) { - return fn() -} - -func (mc *mockCache) Flush() {} diff --git a/app/rest/format/testfile.json b/app/rest/testfile.json similarity index 100% rename from app/rest/format/testfile.json rename to app/rest/testfile.json diff --git a/app/rest/format/tree.go b/app/rest/tree.go similarity index 99% rename from app/rest/format/tree.go rename to app/rest/tree.go index b7ca1427..6f87f306 100644 --- a/app/rest/format/tree.go +++ b/app/rest/tree.go @@ -1,4 +1,4 @@ -package format +package rest import ( "sort" diff --git a/app/rest/format/tree_test.go b/app/rest/tree_test.go similarity index 99% rename from app/rest/format/tree_test.go rename to app/rest/tree_test.go index 5e4deb0c..209bbb18 100644 --- a/app/rest/format/tree_test.go +++ b/app/rest/tree_test.go @@ -1,4 +1,4 @@ -package format +package rest import ( "bytes"