@@ -507,7 +507,7 @@ type Comment struct {
|
||||
User User `json:"user"` // user info, read only
|
||||
Locator Locator `json:"locator"` // post locator
|
||||
Score int `json:"score"` // comment score, read only
|
||||
Votes map[string]bool `json:"votes"` // comment votes, read only
|
||||
Vote int `json:"vote"` // vote for the current user, -1/1/0.
|
||||
Controversy float64 `json:"controversy,omitempty"` // comment controversy, read only
|
||||
Timestamp time.Time `json:"time"` // time stamp, read only
|
||||
Edit *Edit `json:"edit,omitempty" bson:"edit,omitempty"` // pointer to have empty default in json response
|
||||
@@ -609,11 +609,11 @@ Sort can be `time`, `active` or `score`. Supported sort order with prefix -/+, i
|
||||
|
||||
### Images management
|
||||
|
||||
* `GET /api/v1/picture/{user}/{id}` - load stored image
|
||||
* `GET /api/v1/picture/{user}/{id}` - load stored image
|
||||
* `POST /api/v1/picture` - upload and store image, uses post form with `FormFile("file")`. returns `{"id": user/imgid}` _auth required_
|
||||
|
||||
_returned id should be appended to load image url on caller side_
|
||||
|
||||
|
||||
### Admin
|
||||
|
||||
* `DELETE /api/v1/admin/comment/{id}?site=site-id&url=post-url` - delete comment by `id`.
|
||||
|
||||
@@ -305,20 +305,54 @@ func (s *Rest) routes() chi.Router {
|
||||
return router
|
||||
}
|
||||
|
||||
func (s *Rest) alterComments(comments []store.Comment, r *http.Request) (res []store.Comment) {
|
||||
|
||||
res = s.adminService.alterComments(comments, r) // apply admin's alteration
|
||||
|
||||
// prepare vote info for client view
|
||||
vote := func(c store.Comment, r *http.Request) store.Comment {
|
||||
|
||||
c.Vote = 0 // default is "none" (not voted)
|
||||
|
||||
user, err := rest.GetUserInfo(r)
|
||||
if err != nil {
|
||||
c.Votes = nil // hide voters list and don't set Vote for non-authed user
|
||||
return c
|
||||
}
|
||||
|
||||
if v, ok := c.Votes[user.ID]; ok {
|
||||
if v {
|
||||
c.Vote = 1
|
||||
} else {
|
||||
c.Vote = -1
|
||||
}
|
||||
}
|
||||
|
||||
c.Votes = nil // hide voters list
|
||||
return c
|
||||
}
|
||||
|
||||
for i, c := range res {
|
||||
c = vote(c, r)
|
||||
res[i] = c
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// serves static files from /web or embedded by statik
|
||||
func addFileServer(r chi.Router, path string, root http.FileSystem) {
|
||||
|
||||
var webFS http.Handler
|
||||
|
||||
statikFS, err := fs.New()
|
||||
if err == nil {
|
||||
log.Printf("[INFO] run file server for %s, embedded", root)
|
||||
webFS = http.FileServer(statikFS)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] no embedded assets loaded, %s", err)
|
||||
log.Printf("[INFO] run file server for %s, path %s", root, path)
|
||||
webFS = http.FileServer(root)
|
||||
} else {
|
||||
log.Printf("[INFO] run file server for %s, embedded", root)
|
||||
webFS = http.FileServer(statikFS)
|
||||
}
|
||||
|
||||
origPath := path
|
||||
@@ -364,9 +398,24 @@ func filterComments(comments []store.Comment, fn func(c store.Comment) bool) []s
|
||||
// admins will have different keys in order to prevent leak of admin-only data to regular users
|
||||
func URLKey(r *http.Request) string {
|
||||
adminPrefix := "admin!!"
|
||||
key := strings.TrimPrefix(r.URL.String(), adminPrefix) // prevents attach with fake url to get admin view
|
||||
if user, err := rest.GetUserInfo(r); err == nil && user.Admin { // make separate cache key for admins
|
||||
key = adminPrefix + key
|
||||
key := strings.TrimPrefix(r.URL.String(), adminPrefix) // prevents attach with fake url to get admin view
|
||||
if user, err := rest.GetUserInfo(r); err == nil && user.Admin {
|
||||
key = adminPrefix + key // make separate cache key for admins
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// URLKeyWithUser gets url from request to use it as cache key and attaching user ID
|
||||
// admins will have different keys in order to prevent leak of admin-only data to regular users
|
||||
func URLKeyWithUser(r *http.Request) string {
|
||||
adminPrefix := "admin!!"
|
||||
key := strings.TrimPrefix(r.URL.String(), adminPrefix) // prevents attach with fake url to get admin view
|
||||
if user, err := rest.GetUserInfo(r); err == nil {
|
||||
if user.Admin {
|
||||
key = adminPrefix + user.ID + "!!" + key // make separate cache key for admins
|
||||
} else {
|
||||
key = user.ID + "!!" + key // make separate cache key for authed users
|
||||
}
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
@@ -352,7 +352,7 @@ func TestRest_Vote(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodPut,
|
||||
fmt.Sprintf("%s/api/v1/vote/%s?site=radio-t&url=https://radio-t.com/blah&vote=%d", ts.URL, id1, val), nil)
|
||||
assert.Nil(t, err)
|
||||
req.SetBasicAuth("admin", "password")
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
resp, err := client.Do(req)
|
||||
assert.Nil(t, err)
|
||||
return resp.StatusCode
|
||||
@@ -360,22 +360,65 @@ func TestRest_Vote(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 200, vote(1), "first vote allowed")
|
||||
assert.Equal(t, 400, vote(1), "second vote rejected")
|
||||
body, code := get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1))
|
||||
body, code := getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, 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{"admin": true}, cr.Votes)
|
||||
assert.Equal(t, 1, cr.Vote)
|
||||
assert.Equal(t, map[string]bool(nil), cr.Votes)
|
||||
|
||||
assert.Equal(t, 200, vote(-1), "opposite vote allowed")
|
||||
body, code = get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1))
|
||||
body, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, 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)
|
||||
assert.Equal(t, 0, cr.Vote)
|
||||
|
||||
assert.Equal(t, 200, vote(-1), "opposite vote allowed one more time")
|
||||
body, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, 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, -1, cr.Vote)
|
||||
|
||||
assert.Equal(t, 400, vote(-1), "dbl vote not allowed")
|
||||
body, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, 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, -1, cr.Vote)
|
||||
|
||||
body, code = get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, 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, 0, cr.Vote, "no vote info for not authed user")
|
||||
assert.Equal(t, map[string]bool(nil), cr.Votes)
|
||||
|
||||
req, err := http.NewRequest("GET",
|
||||
fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1), nil)
|
||||
assert.NoError(t, err)
|
||||
resp, err := sendReq(t, req, adminUmputunToken)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
cr = store.Comment{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&cr)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, -1, cr.Score)
|
||||
assert.Equal(t, 0, cr.Vote, "no vote info for different user")
|
||||
assert.Equal(t, map[string]bool(nil), cr.Votes)
|
||||
|
||||
assert.Equal(t, map[string]bool(nil), cr.Votes)
|
||||
}
|
||||
|
||||
func TestRest_UserAllData(t *testing.T) {
|
||||
|
||||
@@ -28,13 +28,13 @@ func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
log.Printf("[DEBUG] get comments for %+v, sort %s, format %s", locator, sort, r.URL.Query().Get("format"))
|
||||
|
||||
key := cache.NewKey(locator.SiteID).ID(URLKey(r)).Scopes(locator.SiteID, locator.URL)
|
||||
key := cache.NewKey(locator.SiteID).ID(URLKeyWithUser(r)).Scopes(locator.SiteID, locator.URL)
|
||||
data, err := s.Cache.Get(key, func() ([]byte, error) {
|
||||
comments, e := s.DataService.Find(locator, sort)
|
||||
if e != nil {
|
||||
comments = []store.Comment{} // error should clear comments and continue for post info
|
||||
}
|
||||
maskedComments := s.adminService.alterComments(comments, r)
|
||||
maskedComments := s.alterComments(comments, r)
|
||||
var b []byte
|
||||
switch r.URL.Query().Get("format") {
|
||||
case "tree":
|
||||
@@ -130,7 +130,7 @@ func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
comments = s.adminService.alterComments(comments, r)
|
||||
comments = s.alterComments(comments, r)
|
||||
// filter deleted from last comments view. Blocked marked as deleted and will sneak in without
|
||||
filterDeleted := filterComments(comments, func(c store.Comment) bool { return !c.Deleted })
|
||||
return encodeJSONWithHTML(filterDeleted)
|
||||
@@ -160,7 +160,7 @@ func (s *Rest) commentByIDCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by id", rest.ErrCommentNotFound)
|
||||
return
|
||||
}
|
||||
comment = s.adminService.alterComments([]store.Comment{comment}, r)[0]
|
||||
comment = s.alterComments([]store.Comment{comment}, r)[0]
|
||||
render.Status(r, http.StatusOK)
|
||||
|
||||
if err = R.RenderJSONWithHTML(w, r, comment); err != nil {
|
||||
@@ -186,13 +186,13 @@ func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log.Printf("[DEBUG] get comments for userID %s, %s", userID, siteID)
|
||||
|
||||
key := cache.NewKey(siteID).ID(URLKey(r)).Scopes(userID, siteID)
|
||||
key := cache.NewKey(siteID).ID(URLKeyWithUser(r)).Scopes(userID, siteID)
|
||||
data, err := s.Cache.Get(key, func() ([]byte, error) {
|
||||
comments, e := s.DataService.User(siteID, userID, limit, 0)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
comments = s.adminService.alterComments(comments, r)
|
||||
comments = s.alterComments(comments, r)
|
||||
comments = filterComments(comments, func(c store.Comment) bool { return !c.Deleted })
|
||||
count, e := s.DataService.UserCount(siteID, userID)
|
||||
if e != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -38,6 +39,8 @@ var getStartedHTML = "/tmp/getstarted.html"
|
||||
|
||||
var devToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg"
|
||||
|
||||
var adminUmputunToken = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJyYWRpb3QiLCJleHAiOjE5NTQ1OTc5ODAsImp0aSI6Ijk3YTJlMGFjNGRjN2Q1ZjY5MjZkNWU4NjIwYWNlZjlhNDBjMCIsImlhdCI6MTQ1NDU5NzY4MCwiaXNzIjoicmVtYXJrNDIiLCJ1c2VyIjp7Im5hbWUiOiJVbXB1dHVuIiwiaWQiOiJnaXRodWJfZWYwZjcwNmE3IiwicGljdHVyZSI6Imh0dHBzOi8vcmVtYXJrNDIucmFkaW8tdC5jb20vYXBpL3YxL2F2YXRhci9jYjQyZmY0OTNhZGU2OTZkODhhM2E1OTBmMTM2YWU5ZTM0ZGU3YzFiLmltYWdlIiwiYXR0cnMiOnsiYWRtaW4iOnRydWUsImJsb2NrZWQiOmZhbHNlfX19.I5a8EHbUJy8mApuYCPDRThbC-1jP0sbPh1qwNyY1V4E"
|
||||
|
||||
func TestRest_FileServer(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
defer teardown()
|
||||
@@ -178,7 +181,7 @@ func TestRest_RunAutocertModeHTTPOnly(t *testing.T) {
|
||||
srv.Shutdown()
|
||||
}
|
||||
|
||||
func Test_rejectAnonUser(t *testing.T) {
|
||||
func TestRest_rejectAnonUser(t *testing.T) {
|
||||
|
||||
ts := httptest.NewServer(fakeAuth(rejectAnonUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "Hello")
|
||||
@@ -198,6 +201,54 @@ func Test_rejectAnonUser(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "real user")
|
||||
}
|
||||
|
||||
func Test_URLKey(t *testing.T) {
|
||||
tbl := []struct {
|
||||
url string
|
||||
user store.User
|
||||
key string
|
||||
}{
|
||||
{"http://example.com/1", store.User{}, "http://example.com/1"},
|
||||
{"http://example.com/1", store.User{ID: "user"}, "http://example.com/1"},
|
||||
{"http://example.com/1", store.User{ID: "user", Admin: true}, "admin!!http://example.com/1"},
|
||||
}
|
||||
|
||||
for i, tt := range tbl {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
r, err := http.NewRequest("GET", tt.url, nil)
|
||||
require.NoError(t, err)
|
||||
if tt.user.ID != "" {
|
||||
r = rest.SetUserInfo(r, tt.user)
|
||||
}
|
||||
assert.Equal(t, tt.key, URLKey(r))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_URLKeyWithUser(t *testing.T) {
|
||||
tbl := []struct {
|
||||
url string
|
||||
user store.User
|
||||
key string
|
||||
}{
|
||||
{"http://example.com/1", store.User{}, "http://example.com/1"},
|
||||
{"http://example.com/1", store.User{ID: "user"}, "user!!http://example.com/1"},
|
||||
{"http://example.com/2", store.User{ID: "user2"}, "user2!!http://example.com/2"},
|
||||
{"http://example.com/1", store.User{ID: "user", Admin: true}, "admin!!user!!http://example.com/1"},
|
||||
}
|
||||
|
||||
for i, tt := range tbl {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
r, err := http.NewRequest("GET", tt.url, nil)
|
||||
require.NoError(t, err)
|
||||
if tt.user.ID != "" {
|
||||
r = rest.SetUserInfo(r, tt.user)
|
||||
}
|
||||
assert.Equal(t, tt.key, URLKeyWithUser(r))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
|
||||
|
||||
testDb := fmt.Sprintf("/tmp/test-remark-%d.db", rand.Int31())
|
||||
@@ -296,6 +347,14 @@ func get(t *testing.T, url string) (string, int) {
|
||||
return string(body), r.StatusCode
|
||||
}
|
||||
|
||||
func sendReq(t *testing.T, r *http.Request, token string) (*http.Response, error) {
|
||||
client := http.Client{Timeout: 5 * time.Second}
|
||||
if token != "" {
|
||||
r.Header.Set("X-JWT", token)
|
||||
}
|
||||
return client.Do(r)
|
||||
}
|
||||
|
||||
func getWithDevAuth(t *testing.T, url string) (body string, code int) {
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
|
||||
@@ -41,7 +41,7 @@ func (s *Rest) rssPostCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
comments = s.adminService.alterComments(comments, r)
|
||||
comments = s.alterComments(comments, r)
|
||||
rss, e := s.toRssFeed(locator.URL, comments, "post comments for "+r.URL.Query().Get("url"))
|
||||
if e != nil {
|
||||
return nil, e
|
||||
@@ -73,7 +73,7 @@ func (s *Rest) rssSiteCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
comments = s.adminService.alterComments(comments, r)
|
||||
comments = s.alterComments(comments, r)
|
||||
|
||||
rss, e := s.toRssFeed(r.URL.Query().Get("site"), comments, "site comment for "+siteID)
|
||||
if e != nil {
|
||||
@@ -107,7 +107,7 @@ func (s *Rest) rssRepliesCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
if e != nil {
|
||||
return nil, errors.Wrap(e, "can't get last comments")
|
||||
}
|
||||
comments = s.adminService.alterComments(comments, r)
|
||||
comments = s.alterComments(comments, r)
|
||||
replies := []store.Comment{}
|
||||
for _, c := range comments {
|
||||
if len(replies) > maxRssItems || c.Timestamp.Add(maxReplyDuration).Before(time.Now()) {
|
||||
|
||||
@@ -17,7 +17,8 @@ type Comment struct {
|
||||
User User `json:"user"`
|
||||
Locator Locator `json:"locator"`
|
||||
Score int `json:"score"`
|
||||
Votes map[string]bool `json:"votes"`
|
||||
Votes map[string]bool `json:"votes,omitempty"`
|
||||
Vote int `json:"vote"` // vote for the current user, -1/1/0.
|
||||
Controversy float64 `json:"controversy,omitempty"`
|
||||
Timestamp time.Time `json:"time" bson:"time"`
|
||||
Edit *Edit `json:"edit,omitempty" bson:"edit,omitempty"` // pointer to have empty default in json response
|
||||
|
||||
@@ -107,6 +107,7 @@ func TestBoltAdmin_DeleteUser(t *testing.T) {
|
||||
|
||||
b, teardown := prep(t)
|
||||
defer teardown()
|
||||
|
||||
err := b.DeleteUser("radio-t", "user1")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -157,6 +158,7 @@ func TestBoltAdmin_BlockUserWithTTL(t *testing.T) {
|
||||
|
||||
b, teardown := prep(t)
|
||||
defer teardown()
|
||||
|
||||
assert.False(t, b.IsBlocked("radio-t", "user1"), "nothing blocked")
|
||||
assert.NoError(t, b.SetBlock("radio-t", "user1", true, 50*time.Millisecond))
|
||||
assert.True(t, b.IsBlocked("radio-t", "user1"), "user1 blocked")
|
||||
|
||||
@@ -206,6 +206,15 @@ func (s *DataStore) Vote(locator store.Locator, commentID string, userID string,
|
||||
comment.Score--
|
||||
}
|
||||
|
||||
comment.Vote = 0
|
||||
if v, ok := comment.Votes[userID]; ok {
|
||||
if v {
|
||||
comment.Vote = 1
|
||||
} else {
|
||||
comment.Vote = -1
|
||||
}
|
||||
}
|
||||
|
||||
comment.Controversy = s.controversy(s.upsAndDowns(comment))
|
||||
|
||||
return comment, s.Put(locator, comment)
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestService_CreateFromEmpty(t *testing.T) {
|
||||
assert.Equal(t, "user", res.User.ID)
|
||||
assert.Equal(t, "name", res.User.Name)
|
||||
assert.Equal(t, "23f97cf4d5c29ef788ca2bdd1c9e75656c0e4149", res.User.IP)
|
||||
assert.Equal(t, map[string]bool{}, res.Votes)
|
||||
assert.Equal(t, map[string]bool(nil), res.Votes)
|
||||
}
|
||||
|
||||
func TestService_CreateFromPartial(t *testing.T) {
|
||||
@@ -179,11 +179,13 @@ func TestService_Vote(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 3, len(res))
|
||||
assert.Equal(t, 0, res[0].Score)
|
||||
assert.Equal(t, map[string]bool{}, res[0].Votes, "no votes initially")
|
||||
assert.Equal(t, 0, res[0].Vote)
|
||||
assert.Equal(t, map[string]bool(nil), res[0].Votes, "no votes initially")
|
||||
|
||||
c, err := b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, c.Score)
|
||||
assert.Equal(t, 1, c.Vote)
|
||||
assert.Equal(t, map[string]bool{"user1": true}, c.Votes, "user voted +")
|
||||
|
||||
c, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user", true)
|
||||
@@ -197,6 +199,7 @@ func TestService_Vote(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 3, len(res))
|
||||
assert.Equal(t, 1, res[0].Score)
|
||||
assert.Equal(t, 1, res[0].Vote)
|
||||
assert.Equal(t, 0.0, res[0].Controversy)
|
||||
|
||||
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", false)
|
||||
@@ -205,7 +208,8 @@ func TestService_Vote(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 3, len(res))
|
||||
assert.Equal(t, 0, res[0].Score)
|
||||
assert.Equal(t, map[string]bool{}, res[0].Votes, "vote reset ok")
|
||||
assert.Equal(t, 0, res[0].Vote)
|
||||
assert.Equal(t, map[string]bool(nil), res[0].Votes, "vote reset ok")
|
||||
}
|
||||
|
||||
func TestService_VoteLimit(t *testing.T) {
|
||||
@@ -251,7 +255,7 @@ func TestService_VoteAggressive(t *testing.T) {
|
||||
t.Logf("%+v", res[0])
|
||||
assert.Equal(t, 3, len(res))
|
||||
assert.Equal(t, 0, res[0].Score)
|
||||
assert.Equal(t, map[string]bool{}, res[0].Votes, "no votes initially")
|
||||
assert.Equal(t, map[string]bool(nil), res[0].Votes, "no votes initially")
|
||||
|
||||
// add a vote as user2
|
||||
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user2", true)
|
||||
|
||||
+6
-1
@@ -5,6 +5,10 @@ GET {{host}}/api/v1/find?site={{site}}&sort=-controversy&format=tree&url={{url}}
|
||||
### find request with plain
|
||||
GET {{host}}/api/v1/find?site={{site}}&sort=-controversy&format=plain&url={{url}}
|
||||
|
||||
### find request with plain
|
||||
GET http://127.0.0.1:8080/api/v1/find?site={{site}}&sort=-controversy&format=plain&url={{url}}
|
||||
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg
|
||||
|
||||
### last 50 comments
|
||||
GET {{host}}/api/v1/last/50?site={{site}}
|
||||
|
||||
@@ -53,7 +57,8 @@ Content-Type: application/json
|
||||
PUT {{host}}/api/v1/admin/pin/3665976683?site={{site}}&url={{url}}&pin=1
|
||||
|
||||
### vote for comment
|
||||
PUT {{host}}/api/v1/vote/73e346f4-d57d-41a8-8803-6671aa187d8e?site={{site}}&url={{url}}&vote=1
|
||||
PUT http://127.0.0.1:8080/api/v1/vote/8a8c0b80-0d0a-41c3-84ad-f4034704e827?site={{site}}&url={{url}}&vote=-1
|
||||
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg
|
||||
|
||||
### get user info
|
||||
GET {{host}}/api/v1/user
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
"browsers": ["> 1%", "android >= 4.4.4", "ios >= 9", "IE >= 11"]
|
||||
},
|
||||
"useBuiltIns": "usage",
|
||||
"corejs": 3
|
||||
"corejs": 3
|
||||
}
|
||||
],
|
||||
[
|
||||
|
||||
+11
-3
@@ -52,7 +52,10 @@ export const logOut = (): Promise<void> =>
|
||||
export const getConfig = (): Promise<Config> => fetcher.get(`/config`);
|
||||
|
||||
export const getPostComments = (sort: Sorting): Promise<Tree> =>
|
||||
fetcher.get(`/find?site=${siteId}&url=${url}&sort=${sort}&format=tree`);
|
||||
fetcher.get({
|
||||
url: `/find?site=${siteId}&url=${url}&sort=${sort}&format=tree`,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
export const getLastComments = (siteId: string, max: number): Promise<Comment[]> =>
|
||||
fetcher.get(`/last/${max}?site=${siteId}`);
|
||||
@@ -63,7 +66,8 @@ export const getCommentsCount = (siteId: string, urls: string[]): Promise<{ url:
|
||||
body: urls,
|
||||
});
|
||||
|
||||
export const getComment = (id: Comment['id']): Promise<Comment> => fetcher.get(`/id/${id}?url=${url}`);
|
||||
export const getComment = (id: Comment['id']): Promise<Comment> =>
|
||||
fetcher.get({ url: `/id/${id}?url=${url}`, withCredentials: true });
|
||||
|
||||
export const getUserComments = (
|
||||
userId: User['id'],
|
||||
@@ -71,7 +75,11 @@ export const getUserComments = (
|
||||
): Promise<{
|
||||
comments: Comment[];
|
||||
count: number;
|
||||
}> => fetcher.get(`/comments?user=${userId}&limit=${limit}`);
|
||||
}> =>
|
||||
fetcher.get({
|
||||
url: `/comments?user=${userId}&limit=${limit}`,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
export const putCommentVote = ({ id, value }: { id: Comment['id']; value: number }): Promise<void> =>
|
||||
fetcher.put({
|
||||
|
||||
@@ -44,8 +44,12 @@ export interface Comment {
|
||||
locator: Locator;
|
||||
/** comment score, read only */
|
||||
score: number;
|
||||
/** comment votes, read only */
|
||||
votes: { [key: string]: boolean };
|
||||
/**
|
||||
* vote delta,
|
||||
* if user hasn't voted delta will be 0,
|
||||
* -1/+1 for downvote/upvote
|
||||
*/
|
||||
vote: number;
|
||||
/** comment controversy, read only */
|
||||
controversy?: number;
|
||||
/** pointer to have empty default in json response */
|
||||
|
||||
@@ -12,7 +12,7 @@ const DefaultProps: Partial<Props> = {
|
||||
view: 'main',
|
||||
data: {
|
||||
text: 'test comment',
|
||||
votes: {},
|
||||
vote: 0,
|
||||
user: {
|
||||
id: 'someone',
|
||||
picture: 'somepicture-url',
|
||||
@@ -121,7 +121,7 @@ describe('<Comment />', () => {
|
||||
const element = (
|
||||
<Comment
|
||||
{...DefaultProps as Props}
|
||||
data={{ ...DefaultProps.data, votes: { [DefaultProps.user!.id]: true } } as Props['data']}
|
||||
data={{ ...DefaultProps.data, vote: +1 } as Props['data']}
|
||||
putCommentVote={voteSpy}
|
||||
/>
|
||||
);
|
||||
@@ -146,7 +146,7 @@ describe('<Comment />', () => {
|
||||
const element = (
|
||||
<Comment
|
||||
{...DefaultProps as Props}
|
||||
data={{ ...DefaultProps.data, votes: { [DefaultProps.user!.id]: false } } as Props['data']}
|
||||
data={{ ...DefaultProps.data, vote: -1 } as Props['data']}
|
||||
putCommentVote={voteSpy}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -104,18 +104,8 @@ export class Comment extends Component<Props, State> {
|
||||
}
|
||||
|
||||
updateState(props: Props) {
|
||||
let scoreDelta = 0;
|
||||
if (props.user) {
|
||||
if (props.data.votes[props.user.id] === true) {
|
||||
++scoreDelta;
|
||||
}
|
||||
if (props.data.votes[props.user.id] === false) {
|
||||
--scoreDelta;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
scoreDelta,
|
||||
scoreDelta: props.data.vote,
|
||||
cachedScore: props.data.score,
|
||||
});
|
||||
|
||||
|
||||
@@ -113,6 +113,17 @@ export class Root extends Component<Props, State> {
|
||||
window.addEventListener('message', this.onMessage.bind(this));
|
||||
}
|
||||
|
||||
logIn = async (p: AuthProvider): Promise<User | null> => {
|
||||
const user = await this.props.logIn(p);
|
||||
await this.props.fetchComments(this.props.sort);
|
||||
return user;
|
||||
};
|
||||
|
||||
logOut = async (): Promise<void> => {
|
||||
await this.props.logOut();
|
||||
await this.props.fetchComments(this.props.sort);
|
||||
};
|
||||
|
||||
checkUrlHash(
|
||||
e: Event & {
|
||||
newURL?: string;
|
||||
@@ -202,8 +213,8 @@ export class Root extends Component<Props, State> {
|
||||
providers={StaticStore.config.auth_providers}
|
||||
isCommentsDisabled={isCommentsDisabled}
|
||||
postInfo={this.props.info}
|
||||
onSignIn={this.props.logIn}
|
||||
onSignOut={this.props.logOut}
|
||||
onSignIn={this.logIn}
|
||||
onSignOut={this.logOut}
|
||||
onBlockedUsersShow={this.onBlockedUsersShow}
|
||||
onBlockedUsersHide={this.onBlockedUsersHide}
|
||||
onCommentsEnable={this.props.enableComments}
|
||||
|
||||
@@ -139,6 +139,7 @@ module.exports = () => ({
|
||||
},
|
||||
stats: {
|
||||
children: false,
|
||||
entrypoints: false,
|
||||
},
|
||||
devServer: {
|
||||
host: 'localhost',
|
||||
@@ -157,5 +158,9 @@ module.exports = () => ({
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
children: false,
|
||||
entrypoints: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user