simpler rest structure
This commit is contained in:
+2
-2
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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, "<p><strong>test</strong> <em>123</em> http://radio-t.com</p>", 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, "<p>updated text</p>", 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() {}
|
||||
@@ -1,4 +1,4 @@
|
||||
package format
|
||||
package rest
|
||||
|
||||
import (
|
||||
"sort"
|
||||
@@ -1,4 +1,4 @@
|
||||
package format
|
||||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
Reference in New Issue
Block a user