Merge branch 'master' of github.com:umputun/remark
This commit is contained in:
@@ -161,6 +161,8 @@ _this is the recommended way to run remark42_
|
||||
| ssl.acme-email | SSL_ACME_EMAIL | | admin email for receiving notifications from LE |
|
||||
| max-comment | MAX_COMMENT_SIZE | `2048` | comment's size limit |
|
||||
| max-votes | MAX_VOTES | `-1` | votes limit per comment, `-1` - unlimited |
|
||||
| votes-ip | VOTES_IP |`false` | restrict votes from the same ip |
|
||||
| votes-ip-time | VOTES_IP_TIME |`5m` | same ip vote restriction time, `0s` - unlimited |
|
||||
| low-score | LOW_SCORE | `-5` | low score threshold |
|
||||
| critical-score | CRITICAL_SCORE | `-10` | critical score threshold |
|
||||
| positive-score | POSITIVE_SCORE | `false` | restricts comment's score to be only positive |
|
||||
|
||||
@@ -56,6 +56,8 @@ type ServerCommand struct {
|
||||
ImageProxy bool `long:"img-proxy" env:"IMG_PROXY" description:"enable image proxy"`
|
||||
MaxCommentSize int `long:"max-comment" env:"MAX_COMMENT_SIZE" default:"2048" description:"max comment size"`
|
||||
MaxVotes int `long:"max-votes" env:"MAX_VOTES" default:"-1" description:"maximum number of votes per comment"`
|
||||
RestrictVoteIP bool `long:"votes-ip" env:"VOTES_IP" description:"restrict votes from the same ip"`
|
||||
DurationVoteIP time.Duration `long:"votes-ip-time" env:"VOTES_IP_TIME" default:"5m" description:"same ip vote duration"`
|
||||
LowScore int `long:"low-score" env:"LOW_SCORE" default:"-5" description:"low score threshold"`
|
||||
CriticalScore int `long:"critical-score" env:"CRITICAL_SCORE" default:"-10" description:"critical score threshold"`
|
||||
PositiveScore bool `long:"positive-score" env:"POSITIVE_SCORE" description:"enable positive score only"`
|
||||
@@ -277,6 +279,8 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
TitleExtractor: service.NewTitleExtractor(http.Client{Timeout: time.Second * 5}),
|
||||
RestrictedWordsMatcher: service.NewRestrictedWordsMatcher(service.StaticRestrictedWordsLister{Words: s.RestrictedWords}),
|
||||
}
|
||||
dataService.RestrictSameIPVotes.Enabled = s.RestrictVoteIP
|
||||
dataService.RestrictSameIPVotes.Duration = s.DurationVoteIP
|
||||
|
||||
loadingCache, err := s.makeCache()
|
||||
if err != nil {
|
||||
|
||||
@@ -40,7 +40,7 @@ type private struct {
|
||||
type privStore interface {
|
||||
Create(comment store.Comment) (commentID string, err error)
|
||||
EditComment(locator store.Locator, commentID string, req service.EditRequest) (comment store.Comment, err error)
|
||||
Vote(locator store.Locator, commentID string, userID string, val bool) (comment store.Comment, err error)
|
||||
Vote(req service.VoteReq) (comment store.Comment, err error)
|
||||
Get(locator store.Locator, commentID string, user store.User) (store.Comment, error)
|
||||
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
||||
ValidateComment(c *store.Comment) error
|
||||
@@ -198,7 +198,14 @@ func (s *private) voteCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := s.dataService.Vote(locator, id, user.ID, vote)
|
||||
req := service.VoteReq{
|
||||
Locator: locator,
|
||||
CommentID: id,
|
||||
UserID: user.ID,
|
||||
UserIP: strings.Split(r.RemoteAddr, ":")[0],
|
||||
Val: vote,
|
||||
}
|
||||
comment, err := s.dataService.Vote(req)
|
||||
if err != nil {
|
||||
code := parseError(err, rest.ErrVoteRejected)
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't vote for comment", code)
|
||||
|
||||
@@ -420,8 +420,6 @@ func TestRest_Vote(t *testing.T) {
|
||||
assert.Equal(t, -1, cr.Score)
|
||||
assert.Equal(t, 0, cr.Vote, "no vote info for different user")
|
||||
assert.Equal(t, map[string]bool(nil), cr.Votes)
|
||||
|
||||
assert.Equal(t, map[string]bool(nil), cr.Votes)
|
||||
}
|
||||
|
||||
func TestRest_UserAllData(t *testing.T) {
|
||||
|
||||
@@ -11,21 +11,22 @@ import (
|
||||
|
||||
// Comment represents a single comment with optional reference to its parent
|
||||
type Comment struct {
|
||||
ID string `json:"id" bson:"_id"`
|
||||
ParentID string `json:"pid"`
|
||||
Text string `json:"text"`
|
||||
Orig string `json:"orig,omitempty"`
|
||||
User User `json:"user"`
|
||||
Locator Locator `json:"locator"`
|
||||
Score int `json:"score"`
|
||||
Votes map[string]bool `json:"votes,omitempty"`
|
||||
Vote int `json:"vote"` // vote for the current user, -1/1/0.
|
||||
Controversy float64 `json:"controversy,omitempty"`
|
||||
Timestamp time.Time `json:"time" bson:"time"`
|
||||
Edit *Edit `json:"edit,omitempty" bson:"edit,omitempty"` // pointer to have empty default in json response
|
||||
Pin bool `json:"pin,omitempty" bson:"pin,omitempty"`
|
||||
Deleted bool `json:"delete,omitempty" bson:"delete"`
|
||||
PostTitle string `json:"title,omitempty" bson:"title"`
|
||||
ID string `json:"id" bson:"_id"`
|
||||
ParentID string `json:"pid"`
|
||||
Text string `json:"text"`
|
||||
Orig string `json:"orig,omitempty"`
|
||||
User User `json:"user"`
|
||||
Locator Locator `json:"locator"`
|
||||
Score int `json:"score"`
|
||||
Votes map[string]bool `json:"votes,omitempty"`
|
||||
VotedIPs map[string]VotedIPInfo `json:"voted_ips,omitempty"` // voted ips (hashes) with TS
|
||||
Vote int `json:"vote"` // vote for the current user, -1/1/0.
|
||||
Controversy float64 `json:"controversy,omitempty"`
|
||||
Timestamp time.Time `json:"time" bson:"time"`
|
||||
Edit *Edit `json:"edit,omitempty" bson:"edit,omitempty"` // pointer to have empty default in json response
|
||||
Pin bool `json:"pin,omitempty" bson:"pin,omitempty"`
|
||||
Deleted bool `json:"delete,omitempty" bson:"delete"`
|
||||
PostTitle string `json:"title,omitempty" bson:"title"`
|
||||
}
|
||||
|
||||
// Locator keeps site and url of the post
|
||||
@@ -56,6 +57,12 @@ type BlockedUser struct {
|
||||
Until time.Time `json:"time"`
|
||||
}
|
||||
|
||||
// VotedIPInfo keeps timestamp and voting value (direction). Used as VotedIPs value
|
||||
type VotedIPInfo struct {
|
||||
Timestamp time.Time
|
||||
Value bool
|
||||
}
|
||||
|
||||
// DeleteMode defines how much comment info will be erased
|
||||
type DeleteMode int
|
||||
|
||||
|
||||
@@ -24,11 +24,15 @@ import (
|
||||
|
||||
// DataStore wraps store.Interface with additional methods
|
||||
type DataStore struct {
|
||||
Engine engine.Interface
|
||||
EditDuration time.Duration
|
||||
AdminStore admin.Store
|
||||
MaxCommentSize int
|
||||
MaxVotes int
|
||||
Engine engine.Interface
|
||||
EditDuration time.Duration
|
||||
AdminStore admin.Store
|
||||
MaxCommentSize int
|
||||
MaxVotes int
|
||||
RestrictSameIPVotes struct {
|
||||
Enabled bool
|
||||
Duration time.Duration
|
||||
}
|
||||
PositiveScore bool
|
||||
TitleExtractor *TitleExtractor
|
||||
RestrictedWordsMatcher *RestrictedWordsMatcher
|
||||
@@ -213,29 +217,47 @@ func (s *DataStore) SetPin(locator store.Locator, commentID string, status bool)
|
||||
return s.Engine.Update(comment)
|
||||
}
|
||||
|
||||
// Vote for comment by id and locator
|
||||
func (s *DataStore) Vote(locator store.Locator, commentID string, userID string, val bool) (comment store.Comment, err error) {
|
||||
// VoteReq is the request ot make a vote
|
||||
type VoteReq struct {
|
||||
Locator store.Locator
|
||||
CommentID string
|
||||
UserID string
|
||||
UserIP string
|
||||
Val bool
|
||||
}
|
||||
|
||||
cLock := s.getScopedLocks(locator.URL) // get lock for URL scope
|
||||
cLock.Lock() // prevents race on voting
|
||||
// Vote for comment by id and locator
|
||||
func (s *DataStore) Vote(req VoteReq) (comment store.Comment, err error) {
|
||||
|
||||
cLock := s.getScopedLocks(req.Locator.URL) // get lock for URL scope
|
||||
cLock.Lock() // prevents race on voting
|
||||
defer cLock.Unlock()
|
||||
|
||||
comment, err = s.Engine.Get(engine.GetRequest{Locator: locator, CommentID: commentID})
|
||||
comment, err = s.Engine.Get(engine.GetRequest{Locator: req.Locator, CommentID: req.CommentID})
|
||||
if err != nil {
|
||||
return comment, err
|
||||
}
|
||||
|
||||
if comment.User.ID == userID && userID != "dev" {
|
||||
return comment, errors.Errorf("user %s can not vote for his own comment %s", userID, commentID)
|
||||
if comment.User.ID == req.UserID && req.UserID != "dev" {
|
||||
return comment, errors.Errorf("user %s can not vote for his own comment %s", req.UserID, req.CommentID)
|
||||
}
|
||||
|
||||
if comment.Votes == nil {
|
||||
comment.Votes = make(map[string]bool)
|
||||
}
|
||||
v, voted := comment.Votes[userID]
|
||||
|
||||
if voted && v == val {
|
||||
return comment, errors.Errorf("user %s already voted for %s", userID, commentID)
|
||||
v, voted := comment.Votes[req.UserID]
|
||||
if voted && v == req.Val {
|
||||
return comment, errors.Errorf("user %s already voted for %s", req.UserID, req.CommentID)
|
||||
}
|
||||
|
||||
secret, err := s.AdminStore.Key()
|
||||
if err != nil {
|
||||
return store.Comment{}, errors.Wrapf(err, "can't get secret for site %s", comment.Locator.SiteID)
|
||||
}
|
||||
userIPHash := store.HashValue(req.UserIP, secret)
|
||||
if s.isSameIPVote(req, userIPHash, comment) {
|
||||
return comment, errors.Errorf("the same ip %s already voted for %s", userIPHash, req.CommentID)
|
||||
}
|
||||
|
||||
maxVotes := s.MaxVotes // 0 value allowed and treated as "no comments allowed"
|
||||
@@ -244,32 +266,39 @@ func (s *DataStore) Vote(locator store.Locator, commentID string, userID string,
|
||||
}
|
||||
|
||||
if maxVotes >= 0 && len(comment.Votes) >= maxVotes {
|
||||
return comment, errors.Errorf("maximum number of votes exceeded for comment %s", commentID)
|
||||
return comment, errors.Errorf("maximum number of votes exceeded for comment %s", req.CommentID)
|
||||
}
|
||||
|
||||
if s.PositiveScore && comment.Score <= 0 && !val {
|
||||
return comment, errors.Errorf("minimal score reached for comment %s", commentID)
|
||||
if s.PositiveScore && comment.Score <= 0 && !req.Val {
|
||||
return comment, errors.Errorf("minimal score reached for comment %s", req.CommentID)
|
||||
}
|
||||
|
||||
// reset vote if user changed to opposite
|
||||
if voted && v != val {
|
||||
delete(comment.Votes, userID)
|
||||
if voted && v != req.Val {
|
||||
delete(comment.Votes, req.UserID)
|
||||
}
|
||||
|
||||
// add to voted map if first vote
|
||||
if !voted {
|
||||
comment.Votes[userID] = val
|
||||
comment.Votes[req.UserID] = req.Val
|
||||
}
|
||||
|
||||
// add ip hash to voted ip map
|
||||
if comment.VotedIPs == nil {
|
||||
comment.VotedIPs = map[string]store.VotedIPInfo{}
|
||||
}
|
||||
|
||||
comment.VotedIPs[userIPHash] = store.VotedIPInfo{Timestamp: time.Now(), Value: req.Val}
|
||||
|
||||
// update score
|
||||
if val {
|
||||
if req.Val {
|
||||
comment.Score++
|
||||
} else {
|
||||
comment.Score--
|
||||
}
|
||||
|
||||
comment.Vote = 0
|
||||
if vv, ok := comment.Votes[userID]; ok {
|
||||
if vv, ok := comment.Votes[req.UserID]; ok {
|
||||
if vv {
|
||||
comment.Vote = 1
|
||||
} else {
|
||||
@@ -278,10 +307,26 @@ func (s *DataStore) Vote(locator store.Locator, commentID string, userID string,
|
||||
}
|
||||
|
||||
comment.Controversy = s.controversy(s.upsAndDowns(comment))
|
||||
comment.Locator = locator
|
||||
comment.Locator = req.Locator
|
||||
return comment, s.Engine.Update(comment)
|
||||
}
|
||||
|
||||
func (s *DataStore) isSameIPVote(req VoteReq, userIPHash string, comment store.Comment) bool {
|
||||
if req.UserIP == "" || !s.RestrictSameIPVotes.Enabled {
|
||||
return false
|
||||
}
|
||||
|
||||
if v, ipFound := comment.VotedIPs[userIPHash]; ipFound {
|
||||
if v.Value != req.Val {
|
||||
return false // opposite direction vote allowed
|
||||
}
|
||||
if s.RestrictSameIPVotes.Duration == 0 || v.Timestamp.Add(s.RestrictSameIPVotes.Duration).After(time.Now()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// controversy calculates controversial index of votes
|
||||
// source - https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx#L60
|
||||
func (s *DataStore) controversy(ups, downs int) float64 {
|
||||
|
||||
@@ -183,7 +183,14 @@ func TestService_Vote(t *testing.T) {
|
||||
assert.Equal(t, map[string]bool(nil), res[0].Votes, "no votes initially")
|
||||
|
||||
// vote +1 as user1
|
||||
c, err := b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true)
|
||||
req := VoteReq{
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
CommentID: res[0].ID,
|
||||
UserID: "user1",
|
||||
UserIP: "123",
|
||||
Val: true,
|
||||
}
|
||||
c, err := b.Vote(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, c.Score)
|
||||
assert.Equal(t, 1, c.Vote)
|
||||
@@ -201,10 +208,24 @@ func TestService_Vote(t *testing.T) {
|
||||
assert.Equal(t, 0, c.Vote, "can't see other user vote result")
|
||||
assert.Nil(t, c.Votes)
|
||||
|
||||
c, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user", true)
|
||||
req = VoteReq{
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
CommentID: res[0].ID,
|
||||
UserID: "user",
|
||||
UserIP: "123",
|
||||
Val: true,
|
||||
}
|
||||
c, err = b.Vote(req)
|
||||
assert.NotNil(t, err, "self-voting not allowed")
|
||||
|
||||
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true)
|
||||
req = VoteReq{
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
CommentID: res[0].ID,
|
||||
UserID: "user1",
|
||||
UserIP: "123",
|
||||
Val: true,
|
||||
}
|
||||
_, err = b.Vote(req)
|
||||
assert.NotNil(t, err, "double-voting rejected")
|
||||
assert.True(t, strings.HasPrefix(err.Error(), "user user1 already voted"))
|
||||
|
||||
@@ -226,7 +247,14 @@ func TestService_Vote(t *testing.T) {
|
||||
assert.Equal(t, 0, res[0].Vote)
|
||||
assert.Equal(t, 0.0, res[0].Controversy)
|
||||
|
||||
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", false)
|
||||
req = VoteReq{
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
CommentID: res[0].ID,
|
||||
UserID: "user1",
|
||||
UserIP: "123",
|
||||
Val: false,
|
||||
}
|
||||
_, err = b.Vote(req)
|
||||
assert.NoError(t, err, "vote reset")
|
||||
res, err = b.Last("radio-t", 0, time.Time{}, store.User{})
|
||||
assert.NoError(t, err)
|
||||
@@ -240,17 +268,21 @@ func TestService_VoteLimit(t *testing.T) {
|
||||
defer teardown(t)
|
||||
b := DataStore{Engine: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: 2}
|
||||
|
||||
_, err := b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-1", "user2", true)
|
||||
_, err := b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-1",
|
||||
UserID: "user2", Val: true})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-1", "user3", true)
|
||||
_, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-1",
|
||||
UserID: "user3", Val: true})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-1", "user4", true)
|
||||
_, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-1",
|
||||
UserID: "user4", Val: true})
|
||||
assert.NotNil(t, err, "vote limit reached")
|
||||
assert.True(t, strings.HasPrefix(err.Error(), "maximum number of votes exceeded for comment id-1"))
|
||||
|
||||
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-2", "user4", true)
|
||||
_, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
|
||||
UserID: "user4", Val: true})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -258,7 +290,8 @@ func TestService_VotesDisabled(t *testing.T) {
|
||||
defer teardown(t)
|
||||
b := DataStore{Engine: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: 0}
|
||||
|
||||
_, err := b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-1", "user2", true)
|
||||
_, err := b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-1",
|
||||
UserID: "user2", Val: true})
|
||||
assert.EqualError(t, err, "maximum number of votes exceeded for comment id-1")
|
||||
}
|
||||
|
||||
@@ -282,7 +315,8 @@ func TestService_VoteAggressive(t *testing.T) {
|
||||
assert.Equal(t, map[string]bool(nil), res[0].Votes, "no votes initially")
|
||||
|
||||
// add a vote as user2
|
||||
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user2", true)
|
||||
_, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: res[0].ID,
|
||||
UserID: "user2", Val: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
// crazy vote +1 as user1
|
||||
@@ -291,7 +325,8 @@ func TestService_VoteAggressive(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _ = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true)
|
||||
_, _ = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: res[0].ID,
|
||||
UserID: "user1", Val: true})
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -311,7 +346,8 @@ func TestService_VoteAggressive(t *testing.T) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
val := rand.Intn(2) > 0
|
||||
_, _ = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", val)
|
||||
_, _ = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: res[0].ID,
|
||||
UserID: "user1", Val: val})
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -344,8 +380,8 @@ func TestService_VoteConcurrent(t *testing.T) {
|
||||
ii := i
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _ = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
|
||||
fmt.Sprintf("user1-%d", ii), true)
|
||||
_, _ = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: res[0].ID,
|
||||
UserID: fmt.Sprintf("user1-%d", ii), Val: true})
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -361,15 +397,18 @@ func TestService_VotePositive(t *testing.T) {
|
||||
b := DataStore{Engine: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"),
|
||||
MaxVotes: -1, PositiveScore: true}
|
||||
|
||||
_, err := b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-1", "user2", false)
|
||||
_, err := b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-1",
|
||||
UserID: "user2", Val: false})
|
||||
assert.EqualError(t, err, "minimal score reached for comment id-1")
|
||||
|
||||
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-1", "user3", true)
|
||||
_, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-1",
|
||||
UserID: "user3", Val: true})
|
||||
assert.NoError(t, err, "minimal score doesn't affect positive vote")
|
||||
|
||||
b = DataStore{Engine: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"),
|
||||
MaxVotes: -1, PositiveScore: false}
|
||||
c, err := b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-1", "user2", false)
|
||||
c, err := b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-1",
|
||||
UserID: "user2", Val: false})
|
||||
assert.NoError(t, err, "minimal score ignored")
|
||||
assert.Equal(t, -1, c.Score)
|
||||
assert.Equal(t, 0.0, c.Controversy)
|
||||
@@ -379,17 +418,20 @@ func TestService_VoteControversy(t *testing.T) {
|
||||
defer teardown(t)
|
||||
b := DataStore{Engine: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: -1}
|
||||
|
||||
c, err := b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-2", "user2", false)
|
||||
c, err := b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
|
||||
UserID: "user2", Val: false})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, -1, c.Score, "should have -1 score")
|
||||
assert.InDelta(t, 0.00, c.Controversy, 0.01)
|
||||
|
||||
c, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-2", "user3", true)
|
||||
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
|
||||
UserID: "user3", Val: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, c.Score, "should have 0 score")
|
||||
assert.InDelta(t, 2.00, c.Controversy, 0.01)
|
||||
|
||||
c, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-2", "user4", true)
|
||||
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
|
||||
UserID: "user4", Val: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, c.Score, "should have 1 score")
|
||||
assert.InDelta(t, 1.73, c.Controversy, 0.01)
|
||||
@@ -401,6 +443,58 @@ func TestService_VoteControversy(t *testing.T) {
|
||||
assert.InDelta(t, 1.73, res[0].Controversy, 0.01)
|
||||
}
|
||||
|
||||
func TestService_VoteSameIP(t *testing.T) {
|
||||
defer teardown(t)
|
||||
b := DataStore{Engine: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"),
|
||||
MaxVotes: -1}
|
||||
b.RestrictSameIPVotes.Enabled = true
|
||||
|
||||
c, err := b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
|
||||
UserID: "user2", UserIP: "123", Val: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, c.Score, "should have 1 score")
|
||||
|
||||
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
|
||||
UserID: "user3", UserIP: "123", Val: true})
|
||||
assert.EqualError(t, err, "the same ip cce61be6e0a692420ae0de31dceca179123c3b8a already voted for id-2")
|
||||
assert.Equal(t, 1, c.Score, "still have 1 score")
|
||||
|
||||
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
|
||||
UserID: "user3", UserIP: "123", Val: false})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, c.Score, "reset to 0 score, opposite vote allowed")
|
||||
}
|
||||
|
||||
func TestService_VoteSameIPWithDuration(t *testing.T) {
|
||||
defer teardown(t)
|
||||
b := DataStore{Engine: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123"),
|
||||
MaxVotes: -1}
|
||||
b.RestrictSameIPVotes.Enabled = true
|
||||
b.RestrictSameIPVotes.Duration = 50 * time.Millisecond
|
||||
|
||||
c, err := b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
|
||||
UserID: "user2", UserIP: "123", Val: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, c.Score, "should have 1 score")
|
||||
|
||||
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
|
||||
UserID: "user3", UserIP: "123", Val: true})
|
||||
assert.EqualError(t, err, "the same ip cce61be6e0a692420ae0de31dceca179123c3b8a already voted for id-2")
|
||||
assert.Equal(t, 1, c.Score, "still have 1 score")
|
||||
|
||||
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
|
||||
UserID: "user4", UserIP: "12345", Val: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, c.Score, "have 2 score")
|
||||
|
||||
time.Sleep(51 * time.Millisecond)
|
||||
|
||||
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
|
||||
UserID: "user3", UserIP: "123", Val: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, c.Score, "have 3 score")
|
||||
}
|
||||
|
||||
func TestService_Controversy(t *testing.T) {
|
||||
tbl := []struct {
|
||||
ups, downs int
|
||||
|
||||
Reference in New Issue
Block a user