7
7
"net/http"
8
8
"net/http/httptest"
9
9
"os"
10
+ "strconv"
10
11
"strings"
11
12
"testing"
12
13
"time"
@@ -210,7 +211,7 @@ func TestRest_Find(t *testing.T) {
210
211
assert .Equal (t , id2 , comments .Comments [0 ].ID )
211
212
212
213
// get in tree mode
213
- tree := service. Tree {}
214
+ tree := treeWithInfo {}
214
215
res , code = get (t , ts .URL + "/api/v1/find?site=remark42&url=https://radio-t.com/blah1&format=tree" )
215
216
assert .Equal (t , http .StatusOK , code )
216
217
err = json .Unmarshal ([]byte (res ), & tree )
@@ -236,7 +237,7 @@ func TestRest_FindAge(t *testing.T) {
236
237
_ , err = srv .DataService .Create (c2 )
237
238
require .NoError (t , err )
238
239
239
- tree := service. Tree {}
240
+ tree := treeWithInfo {}
240
241
241
242
res , code := get (t , ts .URL + "/api/v1/find?site=remark42&url=https://radio-t.com/blah1&format=tree" )
242
243
assert .Equal (t , http .StatusOK , code )
@@ -279,15 +280,15 @@ func TestRest_FindReadOnly(t *testing.T) {
279
280
require .NoError (t , err )
280
281
require .NoError (t , resp .Body .Close ())
281
282
282
- tree := service. Tree {}
283
+ tree := treeWithInfo {}
283
284
res , code := get (t , ts .URL + "/api/v1/find?site=remark42&url=https://radio-t.com/blah1&format=tree" )
284
285
assert .Equal (t , http .StatusOK , code )
285
286
err = json .Unmarshal ([]byte (res ), & tree )
286
287
require .NoError (t , err )
287
288
assert .Equal (t , "https://radio-t.com/blah1" , tree .Info .URL )
288
289
assert .True (t , tree .Info .ReadOnly , "post is ro" )
289
290
290
- tree = service. Tree {}
291
+ tree = treeWithInfo {}
291
292
res , code = get (t , ts .URL + "/api/v1/find?site=remark42&url=https://radio-t.com/blah2&format=tree" )
292
293
assert .Equal (t , http .StatusOK , code )
293
294
err = json .Unmarshal ([]byte (res ), & tree )
@@ -515,6 +516,159 @@ func TestRest_FindUserComments_CWE_918(t *testing.T) {
515
516
assert .Equal (t , arbitraryServer .URL , resp .Comments [0 ].Locator .URL , "arbitrary URL provided by the request" )
516
517
}
517
518
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
+
518
672
func TestRest_UserInfo (t * testing.T ) {
519
673
ts , _ , teardown := startupT (t )
520
674
defer teardown ()
0 commit comments