Skip to content

Commit 07c67ca

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. Requesting plain format comments, on the other hand, had a problem with not returning the read-only status of the page if it doesn't have any comments yet. That change fixes that, ensuring that Info is requested in the same manner and the read-only status is returned correctly in case the post doesn't have comments but has such a status.
1 parent 69b18d3 commit 07c67ca

File tree

8 files changed

+139
-77
lines changed

8 files changed

+139
-77
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

+16-11
Original file line numberDiff line numberDiff line change
@@ -77,22 +77,27 @@ 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+
// check IsReadOnly separately for the scenario of a URL with no comments and read-only status
86+
// set by admin so that Info call would return error, but read-only can be checked independently and is true
87+
if commentsInfo.ReadOnly || s.dataService.IsReadOnly(locator) {
88+
commentsInfo.ReadOnly = true
89+
}
90+
8091
var b []byte
8192
switch format {
8293
case "tree":
83-
tree := service.MakeTree(comments, sort, s.readOnlyAge)
84-
if tree.Nodes == nil { // eliminate json nil serialization
85-
tree.Nodes = []*service.Node{}
94+
withInfo := treeWithInfo{Tree: service.MakeTree(comments, sort), Info: commentsInfo}
95+
if withInfo.Nodes == nil { // eliminate json nil serialization
96+
withInfo.Nodes = []*service.Node{}
8697
}
87-
if s.dataService.IsReadOnly(locator) {
88-
tree.Info.ReadOnly = true
89-
}
90-
b, e = encodeJSONWithHTML(tree)
98+
b, e = encodeJSONWithHTML(withInfo)
9199
default:
92-
withInfo := commentsWithInfo{Comments: comments}
93-
if info, ee := s.dataService.Info(locator, s.readOnlyAge); ee == nil {
94-
withInfo.Info = info
95-
}
100+
withInfo := commentsWithInfo{Comments: comments, Info: commentsInfo}
96101
b, e = encodeJSONWithHTML(withInfo)
97102
}
98103
return b, e

backend/app/rest/api/rest_public_test.go

+82-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,83 @@ 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+
// Adding initial comments and voting
527+
c1 := store.Comment{Text: "top-level comment 1", Locator: commentLocator}
528+
id1 := addComment(t, c1, ts)
529+
530+
c2 := store.Comment{Text: "top-level comment 2", Locator: commentLocator}
531+
id2 := addComment(t, c2, ts)
532+
533+
c3 := store.Comment{Text: "second-level comment 1", ParentID: id1, Locator: commentLocator}
534+
id3 := addComment(t, c3, ts)
535+
536+
c4 := store.Comment{Text: "third-level comment 1", ParentID: id3, Locator: commentLocator}
537+
_ = addComment(t, c4, ts)
538+
539+
c5 := store.Comment{Text: "second-level comment 2", ParentID: id2, Locator: commentLocator}
540+
id5 := addComment(t, c5, ts)
541+
542+
c6 := store.Comment{Text: "third-level comment 2", ParentID: id5, Locator: commentLocator}
543+
id6 := addComment(t, c6, ts)
544+
545+
c7 := store.Comment{Text: "top-level comment 3", Locator: commentLocator}
546+
id7 := addComment(t, c7, ts)
547+
548+
c8 := store.Comment{Text: "second-level comment 3", ParentID: id7, Locator: commentLocator}
549+
id8 := addComment(t, c8, ts)
550+
551+
err := srv.DataService.Delete(commentLocator, id8, store.SoftDelete)
552+
assert.NoError(t, err)
553+
err = srv.DataService.Delete(commentLocator, id6, store.HardDelete)
554+
assert.NoError(t, err)
555+
srv.Cache.Flush(cache.FlusherRequest{})
556+
557+
currentTimestamp := time.Now().Unix() * 1000 // For the "since" parameter.
558+
559+
testCases := []struct {
560+
params string
561+
expectedBody string
562+
}{
563+
{"", `"info":{"url":"test-url","count":6,`},
564+
{"format=plain", `"info":{"url":"test-url","count":6,`},
565+
{"since=" + strconv.FormatInt(currentTimestamp-10, 10), `"info":{"url":"test-url","count":6,`},
566+
{"format=plain&since=" + strconv.FormatInt(currentTimestamp-10, 10), `"info":{"url":"test-url","count":6,`},
567+
{"format=plain&since=" + strconv.FormatInt(currentTimestamp-10, 10), `"info":{"url":"test-url","count":6,`},
568+
{"format=tree", `"info":{"url":"test-url","count":6`},
569+
{"format=tree&since=" + strconv.FormatInt(currentTimestamp-10, 10), `"info":{"url":"test-url","count":6`},
570+
{"format=tree&sort=+time", `"info":{"url":"test-url","count":6`},
571+
{"format=tree&sort=-score", `"info":{"url":"test-url","count":6`},
572+
{"sort=+time", `"info":{"url":"test-url","count":6`},
573+
{"sort=-time", `"info":{"url":"test-url","count":6`},
574+
{"sort=+score", `"info":{"url":"test-url","count":6`},
575+
{"sort=-score", `"info":{"url":"test-url","count":6`},
576+
{"sort=+controversy", `"info":{"url":"test-url","count":6`},
577+
{"sort=-controversy", `"info":{"url":"test-url","count":6`},
578+
{"format=tree&sort=+time&since=" + strconv.FormatInt(currentTimestamp-10, 10), `"info":{"url":"test-url","count":6`},
579+
{"format=tree&sort=-score&since=" + strconv.FormatInt(currentTimestamp-100, 10), `"info":{"url":"test-url","count":6`},
580+
{"format=tree&sort=+controversy&since=" + strconv.FormatInt(currentTimestamp-1000, 10), `"info":{"url":"test-url","count":6`},
581+
}
582+
583+
for _, tc := range testCases {
584+
t.Run(tc.params, func(t *testing.T) {
585+
url := fmt.Sprintf(ts.URL+"/api/v1/find?site=remark42&%s", tc.params)
586+
body, code := get(t, url)
587+
assert.Equal(t, http.StatusOK, code)
588+
assert.Contains(t, body, tc.expectedBody)
589+
t.Log(body)
590+
// prevent hit limiter from engaging
591+
time.Sleep(30 * time.Millisecond)
592+
})
593+
}
594+
}
595+
518596
func TestRest_UserInfo(t *testing.T) {
519597
ts, _, teardown := startupT(t)
520598
defer teardown()

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+
}

backend/app/store/service/tree.go

+7-28
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import (
1010

1111
// Tree is formatter making tree from the list of comments
1212
type Tree struct {
13-
Nodes []*Node `json:"comments"`
14-
Info store.PostInfo `json:"info,omitempty"`
13+
Nodes []*Node `json:"comments"`
1514
}
1615

1716
// Node is a comment with optional replies
@@ -30,22 +29,12 @@ type recurData struct {
3029
}
3130

3231
// MakeTree gets unsorted list of comments and produces Tree
33-
// It will make store.PostInfo by itself and will mark Info.ReadOnly based on passed readOnlyAge
34-
// Tree maker is local and has no access to the data store. By this reason it has to make Info and won't be able
35-
// to handle store's read-only status. This status should be set by caller.
36-
func MakeTree(comments []store.Comment, sortType string, readOnlyAge int) *Tree {
32+
func MakeTree(comments []store.Comment, sortType string) *Tree {
3733
if len(comments) == 0 {
3834
return &Tree{}
3935
}
4036

41-
res := Tree{
42-
Info: store.PostInfo{
43-
URL: comments[0].Locator.URL,
44-
FirstTS: comments[0].Timestamp,
45-
LastTS: comments[0].Timestamp,
46-
},
47-
}
48-
res.Info.Count = len(res.filter(comments, func(c store.Comment) bool { return !c.Deleted }))
37+
res := Tree{}
4938

5039
topComments := res.filter(comments, func(c store.Comment) bool { return c.ParentID == "" })
5140

@@ -54,23 +43,12 @@ func MakeTree(comments []store.Comment, sortType string, readOnlyAge int) *Tree
5443
node := Node{Comment: rootComment}
5544

5645
rd := recurData{}
57-
commentsTree, tsModified, tsCreated := res.proc(comments, &node, &rd, rootComment.ID)
46+
commentsTree := res.proc(comments, &node, &rd, rootComment.ID)
5847
// skip deleted with no sub-comments ar all sub-comments deleted
5948
if rootComment.Deleted && (len(commentsTree.Replies) == 0 || !rd.visible) {
6049
continue
6150
}
6251

63-
commentsTree.tsModified, commentsTree.tsCreated = tsModified, tsCreated
64-
if commentsTree.tsCreated.Before(res.Info.FirstTS) {
65-
res.Info.FirstTS = commentsTree.tsCreated
66-
}
67-
if commentsTree.tsModified.After(res.Info.LastTS) {
68-
res.Info.LastTS = commentsTree.tsModified
69-
}
70-
71-
res.Info.ReadOnly = readOnlyAge > 0 && !res.Info.FirstTS.IsZero() &&
72-
res.Info.FirstTS.AddDate(0, 0, readOnlyAge).Before(time.Now())
73-
7452
res.Nodes = append(res.Nodes, commentsTree)
7553
}
7654

@@ -79,7 +57,7 @@ func MakeTree(comments []store.Comment, sortType string, readOnlyAge int) *Tree
7957
}
8058

8159
// proc makes tree for one top-level comment recursively
82-
func (t *Tree) proc(comments []store.Comment, node *Node, rd *recurData, parentID string) (result *Node, modified, created time.Time) {
60+
func (t *Tree) proc(comments []store.Comment, node *Node, rd *recurData, parentID string) (result *Node) {
8361
if rd.tsModified.IsZero() || rd.tsCreated.IsZero() {
8462
rd.tsModified, rd.tsCreated = node.Comment.Timestamp, node.Comment.Timestamp
8563
}
@@ -106,7 +84,8 @@ func (t *Tree) proc(comments []store.Comment, node *Node, rd *recurData, parentI
10684
sort.Slice(node.Replies, func(i, j int) bool {
10785
return node.Replies[i].Comment.Timestamp.Before(node.Replies[j].Comment.Timestamp)
10886
})
109-
return node, rd.tsModified, rd.tsCreated
87+
node.tsModified, node.tsCreated = rd.tsModified, rd.tsCreated
88+
return node
11089
}
11190

11291
// filter returns comments for parentID

0 commit comments

Comments
 (0)