Skip to content

Commit 646fd4c

Browse files
committed
collect /find Info for tree and plain types consistently
MakeTree calculated Info locally for historical reasons, and the results were consistent with the dataService.Info call but calculated differently. That change fixes that, ensuring that Info is requested in the same manner.
1 parent 1313dee commit 646fd4c

File tree

9 files changed

+236
-80
lines changed

9 files changed

+236
-80
lines changed

backend/app/rest/api/admin_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,7 @@ func TestAdmin_ReadOnlyNoComments(t *testing.T) {
513513
_, err = srv.DataService.Info(store.Locator{SiteID: "remark42", URL: "https://radio-t.com/blah"}, 0)
514514
assert.Error(t, err)
515515

516+
// test format "tree"
516517
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah&format=tree")
517518
assert.Equal(t, http.StatusOK, code)
518519
comments := commentsWithInfo{}
@@ -521,6 +522,16 @@ func TestAdmin_ReadOnlyNoComments(t *testing.T) {
521522
assert.Equal(t, 0, len(comments.Comments), "should have 0 comments")
522523
assert.True(t, comments.Info.ReadOnly)
523524
t.Logf("%+v", comments)
525+
526+
// test format "plain"
527+
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah")
528+
assert.Equal(t, http.StatusOK, code)
529+
comments = commentsWithInfo{}
530+
err = json.Unmarshal([]byte(res), &comments)
531+
assert.NoError(t, err)
532+
assert.Equal(t, 0, len(comments.Comments), "should have 0 comments")
533+
assert.True(t, comments.Info.ReadOnly)
534+
t.Logf("%+v", comments)
524535
}
525536

526537
func TestAdmin_ReadOnlyWithAge(t *testing.T) {

backend/app/rest/api/rest.go

+5
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ type commentsWithInfo struct {
9898
Info store.PostInfo `json:"info,omitempty"`
9999
}
100100

101+
type treeWithInfo struct {
102+
*service.Tree
103+
Info store.PostInfo `json:"info,omitempty"`
104+
}
105+
101106
// Run the lister and request's router, activate rest server
102107
func (s *Rest) Run(address string, port int) {
103108
if address == "*" {

backend/app/rest/api/rest_public.go

+27-11
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ type pubStore interface {
5050

5151
// GET /find?site=siteID&url=post-url&format=[tree|plain]&sort=[+/-time|+/-score|+/-controversy]&view=[user|all]&since=unix_ts_msec
5252
// find comments for given post. Returns in tree or plain formats, sorted
53+
//
54+
// When `url` parameter is not set (e.g. request is for site-wide comments), does not return deleted comments.
5355
func (s *public) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
5456
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
5557
sort := r.URL.Query().Get("sort")
@@ -77,22 +79,36 @@ func (s *public) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
7779
comments = []store.Comment{} // error should clear comments and continue for post info
7880
}
7981
comments = s.applyView(comments, view)
82+
83+
var commentsInfo store.PostInfo
84+
if info, ee := s.dataService.Info(locator, s.readOnlyAge); ee == nil {
85+
commentsInfo = info
86+
}
87+
88+
if !since.IsZero() { // if since is set, number of comments can be different from total in the DB
89+
commentsInfo.Count = 0
90+
for _, c := range comments {
91+
if !c.Deleted {
92+
commentsInfo.Count++
93+
}
94+
}
95+
}
96+
97+
// post might be readonly without any comments, Info call will fail then and ReadOnly flag should be checked separately
98+
if !commentsInfo.ReadOnly && locator.URL != "" && s.dataService.IsReadOnly(locator) {
99+
commentsInfo.ReadOnly = true
100+
}
101+
80102
var b []byte
81103
switch format {
82104
case "tree":
83-
tree := service.MakeTree(comments, sort, s.readOnlyAge)
84-
if tree.Nodes == nil { // eliminate json nil serialization
85-
tree.Nodes = []*service.Node{}
86-
}
87-
if s.dataService.IsReadOnly(locator) {
88-
tree.Info.ReadOnly = true
105+
withInfo := treeWithInfo{Tree: service.MakeTree(comments, sort), Info: commentsInfo}
106+
if withInfo.Nodes == nil { // eliminate json nil serialization
107+
withInfo.Nodes = []*service.Node{}
89108
}
90-
b, e = encodeJSONWithHTML(tree)
109+
b, e = encodeJSONWithHTML(withInfo)
91110
default:
92-
withInfo := commentsWithInfo{Comments: comments}
93-
if info, ee := s.dataService.Info(locator, s.readOnlyAge); ee == nil {
94-
withInfo.Info = info
95-
}
111+
withInfo := commentsWithInfo{Comments: comments, Info: commentsInfo}
96112
b, e = encodeJSONWithHTML(withInfo)
97113
}
98114
return b, e

backend/app/rest/api/rest_public_test.go

+158-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"net/http/httptest"
99
"os"
10+
"strconv"
1011
"strings"
1112
"testing"
1213
"time"
@@ -228,7 +229,7 @@ func TestRest_Find(t *testing.T) {
228229
assert.Equal(t, id2, comments.Comments[0].ID)
229230

230231
// get in tree mode
231-
tree := service.Tree{}
232+
tree := treeWithInfo{}
232233
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1&format=tree")
233234
assert.Equal(t, http.StatusOK, code)
234235
err = json.Unmarshal([]byte(res), &tree)
@@ -254,7 +255,7 @@ func TestRest_FindAge(t *testing.T) {
254255
_, err = srv.DataService.Create(c2)
255256
require.NoError(t, err)
256257

257-
tree := service.Tree{}
258+
tree := treeWithInfo{}
258259

259260
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1&format=tree")
260261
assert.Equal(t, http.StatusOK, code)
@@ -297,15 +298,15 @@ func TestRest_FindReadOnly(t *testing.T) {
297298
require.NoError(t, err)
298299
require.NoError(t, resp.Body.Close())
299300

300-
tree := service.Tree{}
301+
tree := treeWithInfo{}
301302
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1&format=tree")
302303
assert.Equal(t, http.StatusOK, code)
303304
err = json.Unmarshal([]byte(res), &tree)
304305
require.NoError(t, err)
305306
assert.Equal(t, "https://radio-t.com/blah1", tree.Info.URL)
306307
assert.True(t, tree.Info.ReadOnly, "post is ro")
307308

308-
tree = service.Tree{}
309+
tree = treeWithInfo{}
309310
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah2&format=tree")
310311
assert.Equal(t, http.StatusOK, code)
311312
err = json.Unmarshal([]byte(res), &tree)
@@ -533,6 +534,159 @@ func TestRest_FindUserComments_CWE_918(t *testing.T) {
533534
assert.Equal(t, arbitraryServer.URL, resp.Comments[0].Locator.URL, "arbitrary URL provided by the request")
534535
}
535536

537+
func TestPublic_FindCommentsCtrl_ConsistentCount(t *testing.T) {
538+
// test that comment counting is consistent between tree and plain formats
539+
ts, srv, teardown := startupT(t)
540+
defer teardown()
541+
542+
commentLocator := store.Locator{URL: "test-url", SiteID: "remark42"}
543+
544+
// vote for comment multiple times
545+
setScore := func(locator store.Locator, id string, val int) {
546+
abs := func(x int) int {
547+
if x < 0 {
548+
return -x
549+
}
550+
return x
551+
}
552+
for i := 0; i < abs(val); i++ {
553+
_, err := srv.DataService.Vote(service.VoteReq{
554+
Locator: locator,
555+
CommentID: id,
556+
// unique user ID is needed for correct counting of controversial votes
557+
UserID: "user" + strconv.Itoa(val) + strconv.Itoa(i),
558+
Val: val > 0,
559+
})
560+
require.NoError(t, err)
561+
}
562+
}
563+
564+
// Adding initial comments (8 to test-url and 1 to another-url) and voting, and delete two of comments to the first post.
565+
// With sleep so that at least few millisecond pass between each comment
566+
// and later we would be able to use that in "since" filter with millisecond precision
567+
ids := make([]string, 9)
568+
timestamps := make([]time.Time, 9)
569+
c1 := store.Comment{Text: "top-level comment 1", Locator: commentLocator}
570+
ids[0], timestamps[0] = addCommentGetCreatedTime(t, c1, ts)
571+
// #3 by score
572+
setScore(commentLocator, ids[0], 1)
573+
time.Sleep(time.Millisecond * 5)
574+
575+
c2 := store.Comment{Text: "top-level comment 2", Locator: commentLocator}
576+
ids[1], timestamps[1] = addCommentGetCreatedTime(t, c2, ts)
577+
// #2 by score
578+
setScore(commentLocator, ids[1], 2)
579+
time.Sleep(time.Millisecond * 5)
580+
581+
c3 := store.Comment{Text: "second-level comment 1", ParentID: ids[0], Locator: commentLocator}
582+
ids[2], timestamps[2] = addCommentGetCreatedTime(t, c3, ts)
583+
// #1 by score
584+
setScore(commentLocator, ids[2], 10)
585+
time.Sleep(time.Millisecond * 5)
586+
587+
c4 := store.Comment{Text: "third-level comment 1", ParentID: ids[2], Locator: commentLocator}
588+
ids[3], timestamps[3] = addCommentGetCreatedTime(t, c4, ts)
589+
// #5 by score, #1 by controversy
590+
setScore(commentLocator, ids[3], 4)
591+
setScore(commentLocator, ids[3], -4)
592+
time.Sleep(time.Millisecond * 5)
593+
594+
c5 := store.Comment{Text: "second-level comment 2", ParentID: ids[1], Locator: commentLocator}
595+
ids[4], timestamps[4] = addCommentGetCreatedTime(t, c5, ts)
596+
// #5 by score, #2 by controversy
597+
setScore(commentLocator, ids[4], 2)
598+
setScore(commentLocator, ids[4], -3)
599+
time.Sleep(time.Millisecond * 5)
600+
601+
c6 := store.Comment{Text: "third-level comment 2", ParentID: ids[4], Locator: commentLocator}
602+
ids[5], timestamps[5] = addCommentGetCreatedTime(t, c6, ts)
603+
// deleted later so not visible in site-wide requests
604+
setScore(commentLocator, ids[5], 10)
605+
setScore(commentLocator, ids[5], -10)
606+
time.Sleep(time.Millisecond * 5)
607+
608+
c7 := store.Comment{Text: "top-level comment 3", Locator: commentLocator}
609+
ids[6], timestamps[6] = addCommentGetCreatedTime(t, c7, ts)
610+
// #6 by score, #4 by controversy
611+
setScore(commentLocator, ids[6], -3)
612+
setScore(commentLocator, ids[6], 1)
613+
time.Sleep(time.Millisecond * 5)
614+
615+
c8 := store.Comment{Text: "second-level comment 3", ParentID: ids[6], Locator: commentLocator}
616+
ids[7], timestamps[7] = addCommentGetCreatedTime(t, c8, ts)
617+
// deleted later so not visible in site-wide requests
618+
setScore(commentLocator, ids[7], -20)
619+
620+
c9 := store.Comment{Text: "comment to post 2", Locator: store.Locator{URL: "another-url", SiteID: "remark42"}}
621+
ids[8], timestamps[8] = addCommentGetCreatedTime(t, c9, ts)
622+
// #7 by score
623+
setScore(store.Locator{URL: "another-url", SiteID: "remark42"}, ids[8], -25)
624+
625+
// delete two comments bringing the total from 9 to 6
626+
err := srv.DataService.Delete(commentLocator, ids[7], store.SoftDelete)
627+
assert.NoError(t, err)
628+
err = srv.DataService.Delete(commentLocator, ids[5], store.HardDelete)
629+
assert.NoError(t, err)
630+
srv.Cache.Flush(cache.FlusherRequest{})
631+
632+
sinceTenSecondsAgo := strconv.FormatInt(time.Now().Add(-time.Second*10).UnixNano()/1000000, 10)
633+
sinceTS := make([]string, 9)
634+
formattedTS := make([]string, 9)
635+
for i, created := range timestamps {
636+
sinceTS[i] = strconv.FormatInt(created.UnixNano()/1000000, 10)
637+
formattedTS[i] = created.Format(time.RFC3339Nano)
638+
}
639+
t.Logf("last timestamp: %v", timestamps[7])
640+
641+
testCases := []struct {
642+
params string
643+
expectedBody string
644+
}{
645+
{"", fmt.Sprintf(`"info":{"count":7,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
646+
{"url=test-url", fmt.Sprintf(`"info":{"url":"test-url","count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
647+
{"format=plain", fmt.Sprintf(`"info":{"count":7,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
648+
{"format=plain&url=test-url", fmt.Sprintf(`"info":{"url":"test-url","count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
649+
{"since=" + sinceTenSecondsAgo, fmt.Sprintf(`"info":{"count":7,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
650+
{"url=test-url&since=" + sinceTenSecondsAgo, fmt.Sprintf(`"info":{"url":"test-url","count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
651+
{"since=" + sinceTS[0], fmt.Sprintf(`"info":{"count":7,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
652+
{"url=test-url&since=" + sinceTS[0], fmt.Sprintf(`"info":{"url":"test-url","count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
653+
{"since=" + sinceTS[1], fmt.Sprintf(`"info":{"count":6,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
654+
{"url=test-url&since=" + sinceTS[1], fmt.Sprintf(`"info":{"url":"test-url","count":5,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
655+
{"since=" + sinceTS[4], fmt.Sprintf(`"info":{"count":3,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[8])},
656+
{"url=test-url&since=" + sinceTS[4], fmt.Sprintf(`"info":{"url":"test-url","count":2,"first_time":%q,"last_time":%q}`, formattedTS[0], formattedTS[7])},
657+
{"format=tree", `"info":{"count":7`},
658+
{"format=tree&url=test-url", `"info":{"url":"test-url","count":6`},
659+
{"format=tree&sort=+time", `"info":{"count":7`},
660+
{"format=tree&url=test-url&sort=+time", `"info":{"url":"test-url","count":6`},
661+
{"format=tree&sort=-score", `"info":{"count":7`},
662+
{"format=tree&url=test-url&sort=-score", `"info":{"url":"test-url","count":6`},
663+
{"sort=+time", fmt.Sprintf(`"score":-25,"vote":0,"time":%q}],"info":{"count":7`, formattedTS[8])},
664+
{"sort=-time", fmt.Sprintf(`"score":1,"vote":0,"time":%q}],"info":{"count":7`, formattedTS[0])},
665+
{"sort=+score", fmt.Sprintf(`"score":10,"vote":0,"time":%q}],"info":{"count":7`, formattedTS[2])},
666+
{"sort=+score&url=test-url", fmt.Sprintf(`"score":10,"vote":0,"time":%q}],"info":{"url":"test-url","count":6`, formattedTS[2])},
667+
{"sort=-score", fmt.Sprintf(`"score":-25,"vote":0,"time":%q}],"info":{"count":7`, formattedTS[8])},
668+
{"sort=-score&url=test-url", fmt.Sprintf(`"score":-2,"vote":0,"controversy":1.5874010519681994,"time":%q}],"info":{"url":"test-url","count":6`, formattedTS[6])},
669+
{"sort=-time&since=" + sinceTS[4], fmt.Sprintf(`"score":-1,"vote":0,"controversy":2.924017738212866,"time":%q}],"info":{"count":3`, formattedTS[4])},
670+
{"sort=-score&since=" + sinceTS[3], fmt.Sprintf(`"score":-25,"vote":0,"time":%q}],"info":{"count":4`, formattedTS[8])},
671+
{"sort=-score&url=test-url&since=" + sinceTS[3], fmt.Sprintf(`"score":-2,"vote":0,"controversy":1.5874010519681994,"time":%q}],"info":{"url":"test-url","count":3`, formattedTS[6])},
672+
{"sort=+controversy&url=test-url&since=" + sinceTS[5], fmt.Sprintf(`"score":-2,"vote":0,"controversy":1.5874010519681994,"time":%q}],"info":{"url":"test-url","count":1`, formattedTS[6])},
673+
// three comments of which last one deleted and doesn't have controversy so returned last
674+
{"sort=-controversy&url=test-url&since=" + sinceTS[5], fmt.Sprintf(`"score":0,"vote":0,"time":%q,"delete":true}],"info":{"url":"test-url","count":1`, formattedTS[7])},
675+
}
676+
677+
for _, tc := range testCases {
678+
t.Run(tc.params, func(t *testing.T) {
679+
url := fmt.Sprintf(ts.URL+"/api/v1/find?site=remark42&%s", tc.params)
680+
body, code := get(t, url)
681+
assert.Equal(t, http.StatusOK, code)
682+
assert.Contains(t, body, tc.expectedBody)
683+
t.Log(body)
684+
// prevent hit limiter from engaging
685+
time.Sleep(50 * time.Millisecond)
686+
})
687+
}
688+
}
689+
536690
func TestRest_UserInfo(t *testing.T) {
537691
ts, _, teardown := startupT(t)
538692
defer teardown()

backend/app/rest/api/rest_test.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ func post(t *testing.T, url, body string) (*http.Response, error) {
599599
return client.Do(req)
600600
}
601601

602-
func addComment(t *testing.T, c store.Comment, ts *httptest.Server) string {
602+
func addCommentGetCreatedTime(t *testing.T, c store.Comment, ts *httptest.Server) (id string, created time.Time) {
603603
b, err := json.Marshal(c)
604604
require.NoError(t, err, "can't marshal comment %+v", c)
605605

@@ -619,7 +619,14 @@ func addComment(t *testing.T, c store.Comment, ts *httptest.Server) string {
619619
err = json.Unmarshal(b, &crResp)
620620
require.NoError(t, err)
621621
time.Sleep(time.Nanosecond * 10)
622-
return crResp["id"].(string)
622+
created, err = time.Parse(time.RFC3339, crResp["time"].(string))
623+
require.NoError(t, err)
624+
return crResp["id"].(string), created
625+
}
626+
627+
func addComment(t *testing.T, c store.Comment, ts *httptest.Server) string {
628+
id, _ := addCommentGetCreatedTime(t, c, ts)
629+
return id
623630
}
624631

625632
func requireAdminOnly(t *testing.T, req *http.Request) {

backend/app/store/service/testdata/tree.json

+2-8
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,5 @@
242242
"time": "2017-12-25T19:47:22Z"
243243
}
244244
}
245-
],
246-
"info": {
247-
"url": "url",
248-
"count": 12,
249-
"first_time": "2017-12-25T19:46:01Z",
250-
"last_time": "2017-12-25T19:47:22Z"
251-
}
252-
}
245+
]
246+
}

backend/app/store/service/testdata/tree_del.json

+2-8
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,5 @@
176176
}
177177
}]
178178
}]
179-
}],
180-
"info": {
181-
"url": "url",
182-
"count": 8,
183-
"first_time": "2017-12-25T19:46:01Z",
184-
"last_time": "2017-12-25T19:47:05Z"
185-
}
186-
}
179+
}]
180+
}

0 commit comments

Comments
 (0)