Skip to content

Commit

Permalink
collect /find Info for tree and plain types consistently
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
paskal committed Oct 18, 2023
1 parent 69b18d3 commit 9d9622a
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 75 deletions.
11 changes: 11 additions & 0 deletions backend/app/rest/api/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ func TestAdmin_ReadOnlyNoComments(t *testing.T) {
_, err = srv.DataService.Info(store.Locator{SiteID: "remark42", URL: "https://radio-t.com/blah"}, 0)
assert.Error(t, err)

// test format "tree"
res, code := get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah&format=tree")
assert.Equal(t, http.StatusOK, code)
comments := commentsWithInfo{}
Expand All @@ -521,6 +522,16 @@ func TestAdmin_ReadOnlyNoComments(t *testing.T) {
assert.Equal(t, 0, len(comments.Comments), "should have 0 comments")
assert.True(t, comments.Info.ReadOnly)
t.Logf("%+v", comments)

// test format "plain"
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah")
assert.Equal(t, http.StatusOK, code)
comments = commentsWithInfo{}
err = json.Unmarshal([]byte(res), &comments)
assert.NoError(t, err)
assert.Equal(t, 0, len(comments.Comments), "should have 0 comments")
assert.True(t, comments.Info.ReadOnly)
t.Logf("%+v", comments)
}

func TestAdmin_ReadOnlyWithAge(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions backend/app/rest/api/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ type commentsWithInfo struct {
Info store.PostInfo `json:"info,omitempty"`
}

type treeWithInfo struct {
*service.Tree
Info store.PostInfo `json:"info,omitempty"`
}

// Run the lister and request's router, activate rest server
func (s *Rest) Run(address string, port int) {
if address == "*" {
Expand Down
24 changes: 15 additions & 9 deletions backend/app/rest/api/rest_public.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,28 @@ func (s *public) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
comments = []store.Comment{} // error should clear comments and continue for post info
}
comments = s.applyView(comments, view)

var commentsInfo store.PostInfo
if info, ee := s.dataService.Info(locator, s.readOnlyAge); ee == nil {
commentsInfo = info
}
// check IsReadOnly separately for the scenario of a URL with no comments and read-only status
// set by admin so that Info call would return error, but read-only can be checked independently and is true
if commentsInfo.ReadOnly || s.dataService.IsReadOnly(locator) {
commentsInfo.ReadOnly = true
}

var b []byte
switch format {
case "tree":
tree := service.MakeTree(comments, sort, s.readOnlyAge)
tree := service.MakeTree(comments, sort)
if tree.Nodes == nil { // eliminate json nil serialization
tree.Nodes = []*service.Node{}
}
if s.dataService.IsReadOnly(locator) {
tree.Info.ReadOnly = true
}
b, e = encodeJSONWithHTML(tree)
withInfo := treeWithInfo{Tree: tree, Info: commentsInfo}
b, e = encodeJSONWithHTML(withInfo)
default:
withInfo := commentsWithInfo{Comments: comments}
if info, ee := s.dataService.Info(locator, s.readOnlyAge); ee == nil {
withInfo.Info = info
}
withInfo := commentsWithInfo{Comments: comments, Info: commentsInfo}
b, e = encodeJSONWithHTML(withInfo)
}
return b, e
Expand Down
86 changes: 82 additions & 4 deletions backend/app/rest/api/rest_public_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -210,7 +211,7 @@ func TestRest_Find(t *testing.T) {
assert.Equal(t, id2, comments.Comments[0].ID)

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

tree := service.Tree{}
tree := treeWithInfo{}

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

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

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

func TestPublic_FindCommentsCtrl_ConsistentCount(t *testing.T) {
// test that comment counting is consistent between tree and plain formats
ts, srv, teardown := startupT(t)
defer teardown()

commentLocator := store.Locator{URL: "test-url", SiteID: "remark42"}

// Adding initial comments and voting
c1 := store.Comment{Text: "top-level comment 1", Locator: commentLocator}
id1 := addComment(t, c1, ts)

c2 := store.Comment{Text: "top-level comment 2", Locator: commentLocator}
id2 := addComment(t, c2, ts)

c3 := store.Comment{Text: "second-level comment 1", ParentID: id1, Locator: commentLocator}
id3 := addComment(t, c3, ts)

c4 := store.Comment{Text: "third-level comment 1", ParentID: id3, Locator: commentLocator}
_ = addComment(t, c4, ts)

c5 := store.Comment{Text: "second-level comment 2", ParentID: id2, Locator: commentLocator}
id5 := addComment(t, c5, ts)

c6 := store.Comment{Text: "third-level comment 2", ParentID: id5, Locator: commentLocator}
id6 := addComment(t, c6, ts)

c7 := store.Comment{Text: "top-level comment 3", Locator: commentLocator}
id7 := addComment(t, c7, ts)

c8 := store.Comment{Text: "second-level comment 3", ParentID: id7, Locator: commentLocator}
id8 := addComment(t, c8, ts)

err := srv.DataService.Delete(commentLocator, id8, store.SoftDelete)
assert.NoError(t, err)
err = srv.DataService.Delete(commentLocator, id6, store.HardDelete)
assert.NoError(t, err)
srv.Cache.Flush(cache.FlusherRequest{})

currentTimestamp := time.Now().Unix() * 1000 // For the "since" parameter.

testCases := []struct {
params string
expectedBody string
}{
{"", `"info":{"url":"test-url","count":6,`},
{"format=plain", `"info":{"url":"test-url","count":6,`},
{"since=" + strconv.FormatInt(currentTimestamp-10, 10), `"info":{"url":"test-url","count":6,`},
{"format=plain&since=" + strconv.FormatInt(currentTimestamp-10, 10), `"info":{"url":"test-url","count":6,`},
{"format=plain&since=" + strconv.FormatInt(currentTimestamp-10, 10), `"info":{"url":"test-url","count":6,`},
{"format=tree", `"info":{"url":"test-url","count":6`},
{"format=tree&since=" + strconv.FormatInt(currentTimestamp-10, 10), `"info":{"url":"test-url","count":6`},
{"format=tree&sort=+time", `"info":{"url":"test-url","count":6`},
{"format=tree&sort=-score", `"info":{"url":"test-url","count":6`},
{"sort=+time", `"info":{"url":"test-url","count":6`},
{"sort=-time", `"info":{"url":"test-url","count":6`},
{"sort=+score", `"info":{"url":"test-url","count":6`},
{"sort=-score", `"info":{"url":"test-url","count":6`},
{"sort=+controversy", `"info":{"url":"test-url","count":6`},
{"sort=-controversy", `"info":{"url":"test-url","count":6`},
{"format=tree&sort=+time&since=" + strconv.FormatInt(currentTimestamp-10, 10), `"info":{"url":"test-url","count":6`},
{"format=tree&sort=-score&since=" + strconv.FormatInt(currentTimestamp-100, 10), `"info":{"url":"test-url","count":6`},
{"format=tree&sort=+controversy&since=" + strconv.FormatInt(currentTimestamp-1000, 10), `"info":{"url":"test-url","count":6`},
}

for _, tc := range testCases {
t.Run(tc.params, func(t *testing.T) {
url := fmt.Sprintf(ts.URL+"/api/v1/find?site=remark42&%s", tc.params)
body, code := get(t, url)
assert.Equal(t, http.StatusOK, code)
assert.Contains(t, body, tc.expectedBody)
t.Log(body)
// prevent hit limiter from engaging
time.Sleep(30 * time.Millisecond)
})
}
}

func TestRest_UserInfo(t *testing.T) {
ts, _, teardown := startupT(t)
defer teardown()
Expand Down
10 changes: 2 additions & 8 deletions backend/app/store/service/testdata/tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,5 @@
"time": "2017-12-25T19:47:22Z"
}
}
],
"info": {
"url": "url",
"count": 12,
"first_time": "2017-12-25T19:46:01Z",
"last_time": "2017-12-25T19:47:22Z"
}
}
]
}
10 changes: 2 additions & 8 deletions backend/app/store/service/testdata/tree_del.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,5 @@
}
}]
}]
}],
"info": {
"url": "url",
"count": 8,
"first_time": "2017-12-25T19:46:01Z",
"last_time": "2017-12-25T19:47:05Z"
}
}
}]
}
35 changes: 7 additions & 28 deletions backend/app/store/service/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import (

// Tree is formatter making tree from the list of comments
type Tree struct {
Nodes []*Node `json:"comments"`
Info store.PostInfo `json:"info,omitempty"`
Nodes []*Node `json:"comments"`
}

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

// MakeTree gets unsorted list of comments and produces Tree
// It will make store.PostInfo by itself and will mark Info.ReadOnly based on passed readOnlyAge
// 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
// to handle store's read-only status. This status should be set by caller.
func MakeTree(comments []store.Comment, sortType string, readOnlyAge int) *Tree {
func MakeTree(comments []store.Comment, sortType string) *Tree {
if len(comments) == 0 {
return &Tree{}
}

res := Tree{
Info: store.PostInfo{
URL: comments[0].Locator.URL,
FirstTS: comments[0].Timestamp,
LastTS: comments[0].Timestamp,
},
}
res.Info.Count = len(res.filter(comments, func(c store.Comment) bool { return !c.Deleted }))
res := Tree{}

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

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

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

commentsTree.tsModified, commentsTree.tsCreated = tsModified, tsCreated
if commentsTree.tsCreated.Before(res.Info.FirstTS) {
res.Info.FirstTS = commentsTree.tsCreated
}
if commentsTree.tsModified.After(res.Info.LastTS) {
res.Info.LastTS = commentsTree.tsModified
}

res.Info.ReadOnly = readOnlyAge > 0 && !res.Info.FirstTS.IsZero() &&
res.Info.FirstTS.AddDate(0, 0, readOnlyAge).Before(time.Now())

res.Nodes = append(res.Nodes, commentsTree)
}

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

// proc makes tree for one top-level comment recursively
func (t *Tree) proc(comments []store.Comment, node *Node, rd *recurData, parentID string) (result *Node, modified, created time.Time) {
func (t *Tree) proc(comments []store.Comment, node *Node, rd *recurData, parentID string) (result *Node) {
if rd.tsModified.IsZero() || rd.tsCreated.IsZero() {
rd.tsModified, rd.tsCreated = node.Comment.Timestamp, node.Comment.Timestamp
}
Expand All @@ -106,7 +84,8 @@ func (t *Tree) proc(comments []store.Comment, node *Node, rd *recurData, parentI
sort.Slice(node.Replies, func(i, j int) bool {
return node.Replies[i].Comment.Timestamp.Before(node.Replies[j].Comment.Timestamp)
})
return node, rd.tsModified, rd.tsCreated
node.tsModified, node.tsCreated = rd.tsModified, rd.tsCreated
return node
}

// filter returns comments for parentID
Expand Down
Loading

0 comments on commit 9d9622a

Please sign in to comment.