Skip to content

Commit 317bdad

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 cd481d4 commit 317bdad

File tree

9 files changed

+233
-79
lines changed

9 files changed

+233
-79
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
@@ -97,6 +97,11 @@ type commentsWithInfo struct {
9797
Info store.PostInfo `json:"info,omitempty"`
9898
}
9999

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

backend/app/rest/api/rest_public.go

+25-11
Original file line numberDiff line numberDiff line change
@@ -77,22 +77,36 @@ func (s *public) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
7777
comments = []store.Comment{} // error should clear comments and continue for post info
7878
}
7979
comments = s.applyView(comments, view)
80+
81+
var commentsInfo store.PostInfo
82+
if info, ee := s.dataService.Info(locator, s.readOnlyAge); ee == nil {
83+
commentsInfo = info
84+
}
85+
86+
if !since.IsZero() { // if since is set, number of comments can be different from total in the DB
87+
commentsInfo.Count = 0
88+
for _, c := range comments {
89+
if !c.Deleted {
90+
commentsInfo.Count++
91+
}
92+
}
93+
}
94+
95+
// post might be readonly without any comments, Info call will fail then and ReadOnly flag should be checked separately
96+
if !commentsInfo.ReadOnly && locator.URL != "" && s.dataService.IsReadOnly(locator) {
97+
commentsInfo.ReadOnly = true
98+
}
99+
80100
var b []byte
81101
switch format {
82102
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
103+
withInfo := treeWithInfo{Tree: service.MakeTree(comments, sort), Info: commentsInfo}
104+
if withInfo.Nodes == nil { // eliminate json nil serialization
105+
withInfo.Nodes = []*service.Node{}
89106
}
90-
b, e = encodeJSONWithHTML(tree)
107+
b, e = encodeJSONWithHTML(withInfo)
91108
default:
92-
withInfo := commentsWithInfo{Comments: comments}
93-
if info, ee := s.dataService.Info(locator, s.readOnlyAge); ee == nil {
94-
withInfo.Info = info
95-
}
109+
withInfo := commentsWithInfo{Comments: comments, Info: commentsInfo}
96110
b, e = encodeJSONWithHTML(withInfo)
97111
}
98112
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"
@@ -210,7 +211,7 @@ func TestRest_Find(t *testing.T) {
210211
assert.Equal(t, id2, comments.Comments[0].ID)
211212

212213
// get in tree mode
213-
tree := service.Tree{}
214+
tree := treeWithInfo{}
214215
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1&format=tree")
215216
assert.Equal(t, http.StatusOK, code)
216217
err = json.Unmarshal([]byte(res), &tree)
@@ -236,7 +237,7 @@ func TestRest_FindAge(t *testing.T) {
236237
_, err = srv.DataService.Create(c2)
237238
require.NoError(t, err)
238239

239-
tree := service.Tree{}
240+
tree := treeWithInfo{}
240241

241242
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1&format=tree")
242243
assert.Equal(t, http.StatusOK, code)
@@ -279,15 +280,15 @@ func TestRest_FindReadOnly(t *testing.T) {
279280
require.NoError(t, err)
280281
require.NoError(t, resp.Body.Close())
281282

282-
tree := service.Tree{}
283+
tree := treeWithInfo{}
283284
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah1&format=tree")
284285
assert.Equal(t, http.StatusOK, code)
285286
err = json.Unmarshal([]byte(res), &tree)
286287
require.NoError(t, err)
287288
assert.Equal(t, "https://radio-t.com/blah1", tree.Info.URL)
288289
assert.True(t, tree.Info.ReadOnly, "post is ro")
289290

290-
tree = service.Tree{}
291+
tree = treeWithInfo{}
291292
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah2&format=tree")
292293
assert.Equal(t, http.StatusOK, code)
293294
err = json.Unmarshal([]byte(res), &tree)
@@ -515,6 +516,159 @@ func TestRest_FindUserComments_CWE_918(t *testing.T) {
515516
assert.Equal(t, arbitraryServer.URL, resp.Comments[0].Locator.URL, "arbitrary URL provided by the request")
516517
}
517518

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