Files
remark42/backend/app/store/service/service_test.go
T

1971 lines
74 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"net/http/httptest"
"os"
"path"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/go-pkgz/lgr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
bolt "go.etcd.io/bbolt"
"github.com/umputun/remark42/backend/app/store"
"github.com/umputun/remark42/backend/app/store/admin"
"github.com/umputun/remark42/backend/app/store/engine"
"github.com/umputun/remark42/backend/app/store/image"
)
func TestService_CreateFromEmpty(t *testing.T) {
ks := admin.NewStaticKeyStore("secret 123")
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: ks}
comment := store.Comment{
Text: "text",
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
id, err := b.Create(comment)
assert.NoError(t, err)
assert.True(t, id != "", id)
res, err := b.Engine.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id))
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "text", res.Text)
assert.True(t, time.Since(res.Timestamp).Seconds() < 1)
assert.Equal(t, "user", res.User.ID)
assert.Equal(t, "name", res.User.Name)
assert.Equal(t, "23f97cf4d5c29ef788ca2bdd1c9e75656c0e4149", res.User.IP)
assert.Equal(t, map[string]bool(nil), res.Votes)
assert.Equal(t, map[string]store.VotedIPInfo(nil), res.VotedIPs)
}
func TestService_CreateSiteDisabled(t *testing.T) {
ks := admin.NewStaticStore("secret 123", []string{"xxx"}, nil, "email")
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: ks}
comment := store.Comment{
Text: "text",
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
_, err := b.Create(comment)
assert.EqualError(t, err, "failed to prepare comment: can't get secret for site radio-t: site radio-t disabled")
}
func TestService_CreateFromPartial(t *testing.T) {
ks := admin.NewStaticKeyStore("secret 123")
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: ks}
comment := store.Comment{
Text: "text",
Timestamp: time.Date(2018, 3, 25, 16, 34, 33, 0, time.UTC),
Votes: map[string]bool{"u1": true, "u2": false},
VotedIPs: map[string]store.VotedIPInfo{"xxx": {Value: true, Timestamp: time.Now()},
"yyy": {Value: false, Timestamp: time.Now()}},
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
id, err := b.Create(comment)
assert.NoError(t, err)
assert.True(t, id != "", id)
res, err := b.Engine.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id))
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "text", res.Text)
assert.Equal(t, comment.Timestamp, res.Timestamp)
assert.Equal(t, "user", res.User.ID)
assert.Equal(t, "name", res.User.Name)
assert.Equal(t, "23f97cf4d5c29ef788ca2bdd1c9e75656c0e4149", res.User.IP)
assert.Equal(t, "", res.PostTitle)
assert.Equal(t, comment.Votes, res.Votes)
}
func TestService_CreateWithQuotesInTitle(t *testing.T) {
ks := admin.NewStaticKeyStore("secret 123")
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: ks}
comment := store.Comment{
Text: "text",
PostTitle: "some example \"in quotes\"",
Timestamp: time.Date(2018, 3, 25, 16, 34, 33, 0, time.UTC),
Votes: map[string]bool{"u1": true, "u2": false},
VotedIPs: map[string]store.VotedIPInfo{"xxx": {Value: true, Timestamp: time.Now()},
"yyy": {Value: false, Timestamp: time.Now()}},
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
id, err := b.Create(comment)
assert.NoError(t, err)
assert.True(t, id != "", id)
res, err := b.Engine.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id))
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "text", res.Text)
assert.Equal(t, comment.Timestamp, res.Timestamp)
assert.Equal(t, "user", res.User.ID)
assert.Equal(t, "name", res.User.Name)
assert.Equal(t, "23f97cf4d5c29ef788ca2bdd1c9e75656c0e4149", res.User.IP)
assert.Equal(t, "some example \"in quotes\"", res.PostTitle)
}
func TestService_CreateFromPartialWithTitle(t *testing.T) {
ks := admin.NewStaticKeyStore("secret 123")
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: ks,
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second}, []string{"127.0.0.1"})}
defer b.Close()
postPath := "/post/42"
postTitle := "Post Title 42"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.String() == postPath {
_, err := fmt.Fprintf(w, "<html><title>%s</title><body>...</body></html>", postTitle)
assert.NoError(t, err)
return
}
w.WriteHeader(404)
}))
defer ts.Close()
comment := store.Comment{
Text: "text",
Timestamp: time.Date(2018, 3, 25, 16, 34, 33, 0, time.UTC),
Votes: map[string]bool{"u1": true, "u2": false},
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: ts.URL + postPath, SiteID: "radio-t"},
}
id, err := b.Create(comment)
assert.NoError(t, err)
assert.True(t, id != "", id)
res, err := b.Engine.Get(getReq(store.Locator{URL: ts.URL + postPath, SiteID: "radio-t"}, id))
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, postTitle, res.PostTitle)
comment.PostTitle = "post blah"
id, err = b.Create(comment)
assert.NoError(t, err)
res, err = b.Engine.Get(getReq(store.Locator{URL: ts.URL + postPath, SiteID: "radio-t"}, id))
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "post blah", res.PostTitle, "keep comment title")
}
func TestService_Put(t *testing.T) {
ks := admin.NewStaticKeyStore("secret 123")
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: ks}
comment := store.Comment{
ID: "c-1",
ParentID: "id-1",
Text: "test text",
User: store.User{ID: "user2", Name: "user name 2"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
}
_, err := b.Create(comment)
require.NoError(t, err)
// create new comment with everything different to replace the first one with fields below
updatedComment := store.Comment{
ID: "c-1",
ParentID: "id-new",
Text: "new text",
User: store.User{ID: "user3", Name: "user name 3"},
Locator: store.Locator{URL: "https://example.com", SiteID: "example"},
Timestamp: time.Date(2018, 12, 20, 15, 18, 22, 0, time.Local),
}
err = b.Put(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, updatedComment)
require.NoError(t, err)
// request with wrong user, should not affect the comment user
got, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "c-1", store.User{ID: "user1", Name: "user name 1"})
require.NoError(t, err)
assert.Equal(t, "c-1", got.ID)
assert.Equal(t, "new text", got.Text)
assert.Equal(t, "id-1", got.ParentID, "should be unaltered")
assert.Equal(t, "user2", got.User.ID, "should be unaltered")
assert.Equal(t, "user name 2", got.User.Name, "should be unaltered")
assert.Equal(t, "https://radio-t.com", got.Locator.URL, "should be unaltered")
assert.Equal(t, "radio-t", got.Locator.SiteID, "should be unaltered")
assert.Equal(t, time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local), got.Timestamp, "should be unaltered")
}
func TestService_SetTitle(t *testing.T) {
var titleEnable int32
tss := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if atomic.LoadInt32(&titleEnable) == 0 {
w.WriteHeader(404)
}
if r.URL.String() == "/post1" {
_, err := w.Write([]byte("<html><title>post1 blah 123</title><body> 2222</body></html>"))
assert.NoError(t, err)
return
}
if r.URL.String() == "/post2" {
_, err := w.Write([]byte("<html><title>post2 blah 123</title><body> 2222</body></html>"))
assert.NoError(t, err)
return
}
w.WriteHeader(404)
}))
defer tss.Close()
ks := admin.NewStaticKeyStore("secret 123")
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: ks,
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second}, []string{"127.0.0.1"})}
defer b.Close()
comment := store.Comment{
Text: "text",
Timestamp: time.Date(2018, 3, 25, 16, 34, 33, 0, time.UTC),
Votes: map[string]bool{"u1": true, "u2": false},
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: tss.URL + "/post1", SiteID: "radio-t"},
}
id, err := b.Create(comment)
assert.NoError(t, err)
assert.True(t, id != "", id)
res, err := b.Engine.Get(getReq(store.Locator{URL: tss.URL + "/post1", SiteID: "radio-t"}, id))
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "", res.PostTitle)
b.TitleExtractor.cache.Purge()
atomic.StoreInt32(&titleEnable, 1)
c, err := b.SetTitle(store.Locator{URL: tss.URL + "/post1", SiteID: "radio-t"}, id)
require.NoError(t, err)
assert.Equal(t, "post1 blah 123", c.PostTitle)
bErr := DataStore{Engine: eng, AdminStore: ks}
defer bErr.Close()
_, err = bErr.SetTitle(store.Locator{URL: tss.URL + "/post1", SiteID: "radio-t"}, id)
require.EqualError(t, err, "no title extractor")
}
func TestService_Vote(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: -1}
comment := store.Comment{
Text: "text",
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
t.Logf("%+v", res[0])
assert.NoError(t, err)
require.Equal(t, 3, len(res))
assert.Equal(t, 0, res[0].Score)
assert.Equal(t, 0, res[0].Vote)
assert.Equal(t, map[string]bool(nil), res[0].Votes, "no votes initially")
// vote +1 as user1
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)
assert.Equal(t, map[string]bool{"user1": true}, c.Votes, "user voted +")
// check result as user1
c, err = b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, store.User{ID: "user1"})
assert.NoError(t, err)
assert.Equal(t, 1, c.Score)
assert.Equal(t, 1, c.Vote, "can see own vote result")
assert.Nil(t, c.Votes)
assert.Nil(t, c.VotedIPs)
// check result as user2
c, err = b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, store.User{ID: "user2"})
assert.NoError(t, err)
assert.Equal(t, 1, c.Score)
assert.Equal(t, 0, c.Vote, "can't see other user vote result")
assert.Nil(t, c.Votes)
assert.Nil(t, c.VotedIPs)
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.Error(t, err, "self-voting not allowed")
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.Error(t, err, "double-voting rejected")
assert.True(t, strings.HasPrefix(err.Error(), "user user1 already voted"))
// check in last as user1
res, err = b.Last("radio-t", 0, time.Time{}, store.User{ID: "user1"})
assert.NoError(t, err)
t.Logf("%+v", res[0])
require.Equal(t, 3, len(res))
assert.Equal(t, 1, res[0].Score)
assert.Equal(t, 1, res[0].Vote)
assert.Equal(t, 0.0, res[0].Controversy)
// check in last as user2
res, err = b.Last("radio-t", 0, time.Time{}, store.User{ID: "user2"})
assert.NoError(t, err)
t.Logf("%+v", res[0])
require.Equal(t, 3, len(res))
assert.Equal(t, 1, res[0].Score)
assert.Equal(t, 0, res[0].Vote)
assert.Equal(t, 0.0, res[0].Controversy)
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)
require.Equal(t, 3, len(res))
assert.Equal(t, 0, res[0].Score)
assert.Equal(t, 0, res[0].Vote)
assert.Equal(t, map[string]bool(nil), res[0].Votes, "vote reset ok")
assert.Equal(t, map[string]store.VotedIPInfo(nil), res[0].VotedIPs, "vote reset ok")
}
func TestService_VoteLimit(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: 2}
_, 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(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(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-1",
UserID: "user4", Val: true})
assert.Error(t, err, "vote limit reached")
assert.True(t, strings.HasPrefix(err.Error(), "maximum number of votes exceeded for comment id-1"))
_, 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)
}
func TestService_VotesDisabled(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: 0}
_, 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")
}
func TestService_VoteAggressive(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: -1}
comment := store.Comment{
Text: "text",
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
require.NoError(t, err)
t.Logf("%+v", res[0])
require.Equal(t, 3, len(res))
assert.Equal(t, 0, res[0].Score)
assert.Equal(t, map[string]bool(nil), res[0].Votes, "no votes initially")
// add a vote as user2
_, 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
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, _ = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: res[0].ID,
UserID: "user1", Val: true})
}()
}
wg.Wait()
res, err = b.Last("radio-t", 0, time.Time{}, store.User{ID: "user1"})
require.NoError(t, err)
t.Logf("%+v", res[0])
require.Equal(t, 3, len(res))
assert.Equal(t, 2, res[0].Score, "add single +1")
assert.Equal(t, 1, res[0].Vote, "user1 voted +1")
assert.Equal(t, 0, len(res[0].Votes), "votes hidden")
assert.Equal(t, 0, len(res[0].VotedIPs), "vote ips hidden")
// random +1/-1 result should be [0..2]
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
val := rand.Intn(2) > 0
_, _ = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: res[0].ID,
UserID: "user1", Val: val})
}()
}
wg.Wait()
res, err = b.Last("radio-t", 0, time.Time{}, store.User{})
require.NoError(t, err)
require.Equal(t, 3, len(res))
t.Logf("%+v %d", res[0], res[0].Score)
assert.True(t, res[0].Score >= 0 && res[0].Score <= 2, "unexpected score %d", res[0].Score)
}
func TestService_VoteConcurrent(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: -1}
comment := store.Comment{
Text: "text",
User: store.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
require.NoError(t, err)
// concurrent vote +1 as multiple users for the same comment
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
ii := i
go func() {
defer wg.Done()
_, _ = 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()
res, err = b.Last("radio-t", 0, time.Time{}, store.User{})
require.NoError(t, err)
assert.Equal(t, 100, res[0].Score, "should have 100 score")
assert.Equal(t, 0, len(res[0].Votes), "should hide votes")
assert.Equal(t, 0, len(res[0].VotedIPs), "should hide vote ips")
assert.Equal(t, 0.0, res[0].Controversy, "should have 0 controversy")
}
func TestService_VotePositive(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123"),
MaxVotes: -1, PositiveScore: true} // allow positive voting only
_, 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")
c, 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")
assert.Equal(t, 1, c.Score)
c, 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, "vote set to +2")
assert.Equal(t, 2, c.Score)
// check +, -, -
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-1",
UserID: "user5", Val: true})
assert.NoError(t, err, "user5 +1, score 3")
assert.Equal(t, 3, c.Score)
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-1",
UserID: "user5", Val: false})
assert.NoError(t, err, "user5 -1, score reset to 2")
assert.Equal(t, 2, c.Score)
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-1",
UserID: "user5", Val: false})
assert.NoError(t, err, "user5 -1, score 1")
assert.Equal(t, 1, c.Score)
// allow negative voting
b.PositiveScore = 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, 0, c.Score)
assert.Equal(t, 2.0, c.Controversy)
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-1",
UserID: "user4", Val: false})
assert.NoError(t, err, "minimal score ignored")
assert.Equal(t, -1, c.Score)
}
func TestService_VoteControversy(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123"), MaxVotes: -1}
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(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(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)
// check if stored
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
require.NoError(t, err)
assert.Equal(t, 1, res[0].Score, "should have 1 score")
assert.InDelta(t, 1.73, res[0].Controversy, 0.01)
}
func TestService_RestrictedWords(t *testing.T) {
ks := admin.NewStaticKeyStore("secret 123")
eng, teardown := prepStoreEngine(t)
defer teardown()
restictedWordLister := StaticRestrictedWordsLister{Words: []string{"restricted"}}
b := DataStore{Engine: eng, AdminStore: ks, RestrictedWordsMatcher: NewRestrictedWordsMatcher(restictedWordLister)}
// test creating a comment with restricted words which should fail with appropriate error
reply := store.Comment{
ID: "c-1",
ParentID: "id-1",
Text: "restricted word",
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user2", Name: "user name 2"},
}
id, err := b.Create(reply)
assert.EqualError(t, err, ErrRestrictedWordsFound.Error(), "should fail with RestrictedWordError")
assert.Empty(t, id)
}
func TestDataStore_AdminStoreErrors(t *testing.T) {
badKey := true
badEnabled := true
as := admin.StoreMock{
OnEventFunc: func(string, admin.EventType) error { return errors.New("err") },
KeyFunc: func(string) (string, error) {
if badKey {
return "", errors.New("mock key err")
}
return "secret", nil
},
EnabledFunc: func(string) (bool, error) {
if badEnabled {
return false, errors.New("mock enabled err")
}
return true, nil
},
AdminsFunc: func(string) ([]string, error) { return nil, errors.New("mock admins err") },
}
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: &as, MaxVotes: -1}
comment := store.Comment{
ID: "c-1",
ParentID: "id-1",
Text: "restricted word",
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user2", Name: "user name 2"},
}
// Key call error
id, err := b.Create(comment)
assert.ErrorContainsf(t, err, "mock key err", "should fail with mock error")
assert.Empty(t, id)
assert.Equal(t, len(as.KeyCalls()), 1)
assert.Equal(t, len(as.EnabledCalls()), 0)
assert.Equal(t, len(as.OnEventCalls()), 0)
// Enabled call error
badKey = false
id, err = b.Create(comment)
assert.ErrorContains(t, err, "mock enabled err", "should fail with mock error")
assert.Empty(t, id)
assert.Equal(t, len(as.KeyCalls()), 2)
assert.Equal(t, len(as.EnabledCalls()), 1)
assert.Equal(t, len(as.OnEventCalls()), 0)
// only OnEvent error
badEnabled = false
id, err = b.Create(comment)
assert.NoError(t, err, "OnEvent error should be just logged")
assert.Equal(t, id, "c-1")
assert.Equal(t, len(as.KeyCalls()), 3)
assert.Equal(t, len(as.EnabledCalls()), 2)
assert.Equal(t, len(as.OnEventCalls()), 1)
// OnEvent error on Vote call
_, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "c-1",
UserID: "user4", Val: true})
assert.NoError(t, err, "OnEvent error should be just logged")
assert.Equal(t, len(as.KeyCalls()), 4)
assert.Equal(t, len(as.EnabledCalls()), 3)
assert.Equal(t, len(as.OnEventCalls()), 2)
// Admins error
isAdmin := b.IsAdmin("radio-t", "user2")
assert.False(t, isAdmin)
assert.Equal(t, len(as.AdminsCalls()), 1)
assert.Equal(t, len(as.OnEventCalls()), 2)
// OnEvent error on EditComment call
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "c-1", EditRequest{Text: "new text"})
assert.NoError(t, err, "OnEvent error should be just logged")
// OnEvent error on Delete call
err = b.Delete(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "c-1", store.SoftDelete)
assert.NoError(t, err, "OnEvent error should be just logged")
// OnEvent error on EditComment Delete call
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "c-1", EditRequest{Delete: true})
assert.NoError(t, err, "OnEvent error should be just logged")
}
func TestService_VoteSameIP(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, 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, rejected")
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
UserID: "user2", UserIP: "123", Val: false})
assert.NoError(t, err)
assert.Equal(t, 0, c.Score, "reset to 0 score, opposite vote allowed")
c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2",
UserID: "user2", UserIP: "123", Val: false})
assert.NoError(t, err)
assert.Equal(t, -1, c.Score, "set to -1 score, correction vote allowed")
}
func TestService_VoteSameIPWithDuration(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123"),
MaxVotes: -1}
b.RestrictSameIPVotes.Enabled = true
b.RestrictSameIPVotes.Duration = 500 * 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(501 * 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
res float64
}{
{0, 0, 0},
{10, 5, 3.87},
{20, 5, 2.24},
{20, 50, 5.47},
{20, 0, 0},
{1100, 500, 28.60},
{1100, 12100, 2.37},
{100, 100, 200},
{101, 101, 202},
}
b := DataStore{}
for i, tt := range tbl {
t.Run(fmt.Sprintf("check-%d-%d:%d", i, tt.ups, tt.downs), func(t *testing.T) {
assert.InDelta(t, tt.res, b.controversy(tt.ups, tt.downs), 0.01)
})
}
}
func TestService_Pin(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123")}
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
t.Logf("%+v", res[0])
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, false, res[0].Pin)
err = b.SetPin(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, true)
assert.NoError(t, err)
c, err := b.Engine.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID))
assert.NoError(t, err)
assert.Equal(t, true, c.Pin)
err = b.SetPin(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, false)
assert.NoError(t, err)
c, err = b.Engine.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID))
assert.NoError(t, err)
assert.Equal(t, false, c.Pin)
}
func TestService_EditComment(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123")}
defer b.Close()
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
t.Logf("%+v", res[0])
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Nil(t, res[0].Edit)
comment, err := b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"})
assert.NoError(t, err)
assert.Equal(t, "my edit", comment.Edit.Summary)
assert.Equal(t, "xxx", comment.Text)
assert.Equal(t, "yyy", comment.Orig)
c, err := b.Engine.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID))
assert.NoError(t, err)
assert.Equal(t, "my edit", c.Edit.Summary)
assert.Equal(t, "xxx", c.Text)
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"})
assert.NoError(t, err, "allow second edit")
}
func TestService_DeleteComment(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123")}
defer b.Close()
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
t.Logf("%+v", res[0])
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Nil(t, res[0].Edit)
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, EditRequest{Delete: true})
assert.NoError(t, err)
c, err := b.Engine.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID))
assert.NoError(t, err)
assert.True(t, c.Deleted)
t.Logf("%+v", c)
}
func TestService_EditCommentDurationFailed(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticKeyStore("secret 123")}
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
t.Logf("%+v", res[0])
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Nil(t, res[0].Edit)
time.Sleep(time.Second)
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"})
assert.Error(t, err)
}
func TestService_EditCommentReplyFailed(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123")}
defer b.Close()
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
t.Logf("%+v", res[1])
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Nil(t, res[1].Edit)
reply := store.Comment{
ID: "123456",
ParentID: "id-1",
Text: "some text",
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user2", Name: "user name 2"},
}
_, err = b.Create(reply)
assert.NoError(t, err)
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[1].ID,
EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"})
assert.EqualError(t, err, "parent comment with reply can't be edited, id-1")
}
func TestService_EditCommentAdmin(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticKeyStore("secret 123"), AdminEdits: true}
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
t.Logf("%+v", res[0])
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Nil(t, res[0].Edit)
time.Sleep(time.Second)
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit", Admin: true})
assert.NoError(t, err)
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit", Admin: false})
assert.Error(t, err)
}
func TestService_ValidateComment(t *testing.T) {
b := DataStore{MinCommentSize: 6, MaxCommentSize: 2000, AdminStore: admin.NewStaticKeyStore("secret 123")}
longText := fmt.Sprintf("%4000s", "X")
tbl := []struct {
inp store.Comment
err string
}{
{inp: store.Comment{}, err: "empty comment text"},
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid", Name: "name"}}, err: ""},
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid"}}, err: "empty user info"},
{inp: store.Comment{Orig: "short", User: store.User{ID: "myid", Name: "name"}}, err: "comment text is smaller than min allowed size 6 (5)"},
{inp: store.Comment{Orig: longText, User: store.User{ID: "myid", Name: "name"}}, err: "comment text exceeded max allowed size 2000 (4000)"},
{inp: store.Comment{Orig: "here is a link with relative URL: [google.com](url)", User: store.User{ID: "myid", Name: "name"}}, err: "links should start with mailto:, http:// or https://"},
{inp: store.Comment{Orig: "here is a link with relative URL: [google.com](url)", User: store.User{ID: "myid", Name: "name"}}, err: "links should start with mailto:, http:// or https://"},
{inp: store.Comment{Orig: "multiple links, one is bad: [test](http://test) [test2](bad_url) [test3](https://test3)", User: store.User{ID: "myid", Name: "name"}}, err: "links should start with mailto:, http:// or https://"},
}
for n, tt := range tbl {
err := b.ValidateComment(&tt.inp)
if tt.err == "" {
assert.NoError(t, err, "check #%d", n)
continue
}
require.Error(t, err)
assert.EqualError(t, err, tt.err, "check #%d", n)
}
}
func TestService_Counts(t *testing.T) {
b, teardown := prepStoreEngine(t) // two comments for https://radio-t.com
defer teardown()
// add one more for https://radio-t.com/2
comment := store.Comment{
ID: "123456",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
svc := DataStore{Engine: b}
res, err := svc.Counts("radio-t", []string{"https://radio-t.com/2"})
assert.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1}}, res)
res, err = svc.Counts("radio-t", []string{"https://radio-t.com", "https://radio-t.com/2", "blah"})
assert.NoError(t, err)
assert.Equal(t, []store.PostInfo{
{URL: "https://radio-t.com", Count: 2},
{URL: "https://radio-t.com/2", Count: 1},
{URL: "blah", Count: 0},
}, res)
}
func TestService_GetMetas(t *testing.T) {
// two comments for https://radio-t.com
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticKeyStore("secret 123")}
um, pm, err := b.Metas("radio-t")
require.NoError(t, err)
assert.Equal(t, 0, len(um))
assert.Equal(t, 0, len(pm))
assert.NoError(t, b.SetVerified("radio-t", "user1", true))
assert.NoError(t, b.SetBlock("radio-t", "user1", true, time.Hour))
assert.NoError(t, b.SetBlock("radio-t", "user2", true, time.Hour))
assert.NoError(t, b.SetReadOnly(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, true))
// set email for one existing and one non-existing user
req := engine.UserDetailRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user2", Detail: engine.UserEmail, Update: "test@example.org"}
value, err := b.Engine.UserDetail(req)
assert.NoError(t, err)
assert.Equal(t, []engine.UserDetailEntry{{UserID: "user2", Email: "test@example.org"}}, value)
req.UserID = "user3"
value, err = b.Engine.UserDetail(req)
assert.NoError(t, err)
assert.Equal(t, []engine.UserDetailEntry{{UserID: "user3", Email: "test@example.org"}}, value)
um, pm, err = b.Metas("radio-t")
require.NoError(t, err)
require.Equal(t, 3, len(um))
assert.Equal(t, "user1", um[0].ID)
assert.Equal(t, true, um[0].Verified)
assert.Equal(t, engine.UserDetailEntry{Email: ""}, um[0].Details)
assert.Equal(t, true, um[0].Blocked.Status)
assert.Equal(t, false, um[1].Verified)
assert.Equal(t, true, um[1].Blocked.Status)
assert.Equal(t, "test@example.org", um[1].Details.Email)
assert.Equal(t, "user3", um[2].ID)
assert.Equal(t, "test@example.org", um[2].Details.Email)
require.Equal(t, 1, len(pm))
assert.Equal(t, "https://radio-t.com", pm[0].URL)
assert.Equal(t, true, pm[0].ReadOnly)
}
func TestService_SetMetas(t *testing.T) {
// two comments for https://radio-t.com
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticKeyStore("secret 123")}
umetas := []UserMetaData{}
pmetas := []PostMetaData{}
err := b.SetMetas("radio-t", umetas, pmetas)
assert.NoError(t, err, "empty metas")
um1 := UserMetaData{ID: "user1", Verified: true, Details: engine.UserDetailEntry{Email: "test@example.org"}}
um2 := UserMetaData{ID: "user2"}
um2.Blocked.Status = true
um2.Blocked.Until = time.Now().AddDate(0, 1, 1)
pmetas = []PostMetaData{{URL: "https://radio-t.com", ReadOnly: true}}
err = b.SetMetas("radio-t", []UserMetaData{um1, um2}, pmetas)
assert.NoError(t, err)
assert.True(t, b.IsReadOnly(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}))
assert.True(t, b.IsVerified("radio-t", "user1"))
assert.True(t, b.IsBlocked("radio-t", "user2"))
val, err := b.Engine.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Detail: engine.UserEmail})
assert.NoError(t, err)
assert.Equal(t, []engine.UserDetailEntry{{UserID: "user1", Email: "test@example.org"}}, val)
}
func TestService_UserDetailsOperations(t *testing.T) {
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticKeyStore("secret 123")}
// add single valid entry
result, err := b.SetUserEmail("radio-t", "u1", "test@example.com")
assert.NoError(t, err, "No error inserting entry expected")
assert.Equal(t, "test@example.com", result)
result, err = b.SetUserTelegram("radio-t", "u1", "test@example.com")
assert.NoError(t, err, "No error inserting entry expected")
assert.Equal(t, "test@example.com", result)
// read valid entry back
result, err = b.GetUserEmail("radio-t", "u1")
assert.NoError(t, err, "No error reading entry expected")
assert.Equal(t, "test@example.com", result)
result, err = b.GetUserTelegram("radio-t", "u1")
assert.NoError(t, err, "No error reading entry expected")
assert.Equal(t, "test@example.com", result)
// delete existing entry
err = b.DeleteUserDetail("radio-t", "u1", engine.UserEmail)
assert.NoError(t, err, "No error deleting entry expected")
err = b.DeleteUserDetail("radio-t", "u1", engine.UserTelegram)
assert.NoError(t, err, "No error deleting entry expected")
// read deleted entry
result, err = b.GetUserEmail("radio-t", "u1")
assert.NoError(t, err, "No error reading entry expected")
assert.Empty(t, result)
result, err = b.GetUserTelegram("radio-t", "u1")
assert.NoError(t, err, "No error reading entry expected")
assert.Empty(t, result)
// insert entry with invalid site_id
result, err = b.SetUserEmail("bad-site", "u3", "does_not_matter@example.com")
assert.Error(t, err, "Site not found")
assert.Empty(t, result)
result, err = b.SetUserTelegram("bad-site", "u3", "does_not_matter@example.com")
assert.Error(t, err, "Site not found")
assert.Empty(t, result)
// read entry with invalid site_id
result, err = b.GetUserEmail("bad-site", "u3")
assert.Error(t, err, "Site not found")
assert.Empty(t, result)
result, err = b.GetUserTelegram("bad-site", "u3")
assert.Error(t, err, "Site not found")
assert.Empty(t, result)
}
func TestService_IsAdmin(t *testing.T) {
// two comments for https://radio-t.com
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticStore("secret 123", []string{"radio-t"}, []string{"user2"}, "user@email.com")}
assert.False(t, b.IsAdmin("radio-t", "user1"))
assert.True(t, b.IsAdmin("radio-t", "user2"))
assert.False(t, b.IsAdmin("radio-t-bad", "user1"))
}
func TestService_HasReplies(t *testing.T) {
// two comments for https://radio-t.com, no reply
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng,
AdminStore: admin.NewStaticStore("secret 123", []string{"radio-t"}, []string{"user2"}, "user@email.com")}
defer b.Close()
comment := store.Comment{
ID: "id-1",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
assert.False(t, b.HasReplies(comment))
reply := store.Comment{
ID: "c-1",
ParentID: "id-1",
Text: "some text",
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user2", Name: "user name 2"},
}
_, err := b.Create(reply)
assert.NoError(t, err)
_, found := b.repliesCache.Peek(comment.ID)
assert.False(t, found, "not yet checked")
assert.True(t, b.HasReplies(comment))
_, found = b.repliesCache.Peek(reply.ParentID)
assert.True(t, found, "checked and has replies")
// deletion of the parent comment shouldn't work as the comment has replies
_, err = b.EditComment(reply.Locator, comment.ID, EditRequest{Orig: comment.ID, Delete: true, Summary: "user deletes the comment"})
assert.EqualError(t, err, "parent comment with reply can't be edited, "+comment.ID)
_, found = b.repliesCache.Peek(reply.ParentID)
assert.True(t, found, "checked and has replies")
// should not report replies after deletion of the child
err = b.Delete(reply.Locator, reply.ID, store.HardDelete)
assert.NoError(t, err)
_, found = b.repliesCache.Peek(reply.ParentID)
assert.False(t, found, "cleaned up from cache by Delete call")
assert.False(t, b.HasReplies(comment))
_, found = b.repliesCache.Peek(reply.ParentID)
assert.False(t, found, "checked and has no replies")
// recreate reply with the new ID
reply.ID = "c-2"
_, err = b.Create(reply)
assert.NoError(t, err)
_, found = b.repliesCache.Peek(reply.ParentID)
assert.False(t, found, "not yet checked")
assert.True(t, b.HasReplies(comment))
_, found = b.repliesCache.Peek(reply.ParentID)
assert.True(t, found, "checked and has replies again")
// should not report replies after deletion of the child using Edit mechanism
_, err = b.EditComment(reply.Locator, reply.ID, EditRequest{Orig: reply.ID, Delete: true, Summary: "user deletes the comment"})
assert.NoError(t, err)
_, found = b.repliesCache.Peek(reply.ParentID)
assert.False(t, found, "cleaned up from cache by EditComment call")
assert.False(t, b.HasReplies(comment))
_, found = b.repliesCache.Peek(reply.ParentID)
assert.False(t, found, "checked and has no replies")
}
func TestService_UserReplies(t *testing.T) {
// two comments for https://radio-t.com, no reply
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng,
AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")}
c1 := store.Comment{
ID: "comment-id-1",
Text: "test 123",
Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"},
User: store.User{ID: "u1", Name: "developer one u1"},
}
c2 := store.Comment{
ID: "comment-id-2",
ParentID: "comment-id-1",
Text: "xyz test",
Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"},
User: store.User{ID: "u2", Name: "developer one u2"},
}
c3 := store.Comment{
ID: "comment-id-3",
ParentID: "comment-id-1",
Text: "xyz test",
Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"},
User: store.User{ID: "u2", Name: "developer one u3"},
}
c4 := store.Comment{
ID: "comment-id-4",
ParentID: "",
Text: "xyz test",
Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"},
User: store.User{ID: "u4", Name: "developer one u4"},
}
c5 := store.Comment{
ID: "comment-id-5",
ParentID: "comment-id-1",
Text: "xyz test",
Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"},
User: store.User{ID: "u2", Name: "developer one u2"},
}
_, err := b.Create(c1)
require.NoError(t, err)
_, err = b.Create(c2)
require.NoError(t, err)
_, err = b.Create(c3)
require.NoError(t, err)
_, err = b.Create(c4)
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
_, err = b.Create(c5)
require.NoError(t, err)
cc, u, err := b.UserReplies("radio-t", "u1", 10, time.Hour)
assert.NoError(t, err)
require.Equal(t, 3, len(cc), "3 replies to u1")
assert.Equal(t, "developer one u1", u)
// mutex to prevent multiple b.UserReplies calls resulting in data race
l := sync.Mutex{}
assert.Eventually(t, func() bool {
l.Lock()
defer l.Unlock()
cc, u, err = b.UserReplies("radio-t", "u1", 10, time.Millisecond*299)
require.NoError(t, err)
require.Equal(t, "developer one u1", u)
return len(cc) == 1
}, 300*time.Millisecond, 30*time.Millisecond, "1 reply to u1 in the last 300ms")
l.Lock()
defer l.Unlock()
cc, u, err = b.UserReplies("radio-t", "u2", 10, time.Hour)
assert.NoError(t, err)
assert.Equal(t, 0, len(cc), "0 replies to u2")
assert.Equal(t, "developer one u2", u)
cc, u, err = b.UserReplies("radio-t", "uxxx", 10, time.Hour)
assert.NoError(t, err)
assert.Equal(t, 0, len(cc), "0 replies to uxxx")
assert.Equal(t, "", u)
}
func TestService_Find(t *testing.T) {
// two comments for https://radio-t.com, no reply
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")}
res, err := b.Find(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "time", store.User{})
require.NoError(t, err)
assert.Equal(t, 2, len(res))
// add one more for https://radio-t.com/2
comment := store.Comment{
ID: "123456",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
Score: 1,
Votes: map[string]bool{"id-1": true, "id-2": true, "123456": false},
PostTitle: `some title, <a href="http://radio-t.com">link</a>`,
}
_, err = b.Engine.Create(comment) // create directly with engine, doesn't set Controversy
assert.NoError(t, err)
// make sure Controversy altered
res, err = b.Find(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "-controversy", store.User{})
require.NoError(t, err)
require.Equal(t, 3, len(res))
assert.Equal(t, "123456", res[0].ID)
assert.InDelta(t, 1.73, res[0].Controversy, 0.01)
assert.Equal(t, "id-1", res[1].ID)
assert.InDelta(t, 0, res[1].Controversy, 0.01)
// make sure title sanitized
assert.Equal(t, "some title, link", res[0].PostTitle)
}
func TestService_FindSince(t *testing.T) {
// two comments for https://radio-t.com, no reply
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")}
res, err := b.FindSince(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "time", store.User{}, time.Time{})
require.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, "id-1", res[0].ID)
res, err = b.FindSince(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "time", store.User{},
time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local))
require.NoError(t, err)
require.Equal(t, 1, len(res))
assert.Equal(t, "id-2", res[0].ID)
}
func TestService_Info(t *testing.T) {
// two comments for https://radio-t.com, no reply
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")}
// add one more comment for another URL to test site-wide Info request
comment := store.Comment{
ID: "123456xyz",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2018, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/another", SiteID: "radio-t"},
User: store.User{ID: "user2", Name: "user name"},
}
_, err := b.Create(comment)
require.NoError(t, err)
// get non-existing URL info
info, err := b.Info(store.Locator{URL: "bad", SiteID: "radio-t"}, 0)
assert.Error(t, err)
assert.Empty(t, info)
// get non-existing site info
info, err = b.Info(store.Locator{SiteID: "bad"}, 0)
assert.Error(t, err)
assert.Empty(t, info)
// test two initially created comments and store first comment FirstTS
info, err = b.Info(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, 0)
require.NoError(t, err)
assert.Equal(t, "https://radio-t.com", info.URL)
assert.Equal(t, 2, info.Count)
assert.False(t, info.ReadOnly)
assert.True(t, info.LastTS.After(info.FirstTS))
firstTS := info.FirstTS
time.Sleep(1 * time.Second) // make post RO in 1sec
info, err = b.Info(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, 1)
require.NoError(t, err)
assert.Equal(t, "https://radio-t.com", info.URL)
assert.True(t, info.ReadOnly)
// get last created comment LastTS
info, err = b.Info(store.Locator{URL: "https://radio-t.com/another", SiteID: "radio-t"}, 0)
require.NoError(t, err)
lastTS := info.LastTS
// site-level request
info, err = b.Info(store.Locator{SiteID: "radio-t"}, 1)
require.NoError(t, err)
assert.Equal(t, 3, info.Count)
assert.Empty(t, info.URL, "site-level request should not set URL")
assert.False(t, info.ReadOnly, "site-level request should not set ReadOnly")
assert.Equal(t, firstTS, info.FirstTS, "site-level request should have FirstTS from the first post")
assert.Equal(t, lastTS, info.LastTS, "site-level request should have LastTS from the last post")
}
func TestService_Delete(t *testing.T) {
// two comments for https://radio-t.com, no reply
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")}
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
require.Equal(t, 2, len(res))
assert.NoError(t, err)
err = b.Delete(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, store.SoftDelete)
assert.NoError(t, err)
res, err = b.Last("radio-t", 0, time.Time{}, store.User{})
assert.Equal(t, 1, len(res), "one left")
assert.NoError(t, err)
}
func TestService_deleteImagesOnCommentDelete(t *testing.T) {
lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc)
mockStore := image.StoreMock{
DeleteFunc: func(string) error { return nil },
CommitFunc: func(string) error { return nil },
ResetCleanupTimerFunc: func(string) error { return nil },
}
imgSvc := image.NewService(&mockStore,
image.ServiceParams{
EditDuration: 50 * time.Millisecond,
ImageAPI: "/images/dev/",
ProxyAPI: "/non_existent",
})
defer imgSvc.Close(context.TODO())
// two comments for https://radio-t.com
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 50 * time.Millisecond,
AdminStore: admin.NewStaticKeyStore("secret 123"), ImageService: imgSvc}
c := store.Comment{
ID: "id-22",
Text: `some text <img src="/images/dev/pic1.png"/> xx <img src="/images/dev/pic2.png"/>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := b.Engine.Create(c) // create directly with engine, doesn't call submitImages
assert.NoError(t, err)
b.submitImages(c)
// reply to the first comment with one new image and one existing one
c = store.Comment{
ID: "id-23",
ParentID: "id-22",
Text: `some text <img src="/images/dev/pic2.png"/> xx <img src="/images/dev/pic3.png"/>`,
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = b.Engine.Create(c) // create directly with engine, doesn't call submitImages
assert.NoError(t, err)
b.submitImages(c)
// verify that images are in staging store
assert.Equal(t, 4, len(mockStore.ResetCleanupTimerCalls()))
assert.Equal(t, "dev/pic1.png", mockStore.ResetCleanupTimerCalls()[0].ID)
assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[1].ID)
assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[2].ID)
assert.Equal(t, "dev/pic3.png", mockStore.ResetCleanupTimerCalls()[3].ID)
time.Sleep(b.EditDuration + 100*time.Millisecond)
// verify that they got into the main store
assert.Equal(t, 4, len(mockStore.CommitCalls()))
assert.Equal(t, "dev/pic1.png", mockStore.CommitCalls()[0].ID)
assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[1].ID)
assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[2].ID)
assert.Equal(t, "dev/pic3.png", mockStore.CommitCalls()[3].ID)
// delete the first comment
err = b.Delete(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-22", store.SoftDelete)
assert.NoError(t, err)
// verify that images are deleted from the main store
assert.Equal(t, 1, len(mockStore.DeleteCalls()))
assert.Equal(t, "dev/pic1.png", mockStore.DeleteCalls()[0].ID)
// delete the second comment
err = b.Delete(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-23", store.SoftDelete)
assert.NoError(t, err)
// verify that images are deleted from the main store
assert.Equal(t, 3, len(mockStore.DeleteCalls()))
assert.Equal(t, "dev/pic2.png", mockStore.DeleteCalls()[1].ID)
assert.Equal(t, "dev/pic3.png", mockStore.DeleteCalls()[2].ID)
}
// DeleteUser removes all comments from user
func TestService_DeleteUser(t *testing.T) {
// two comments for https://radio-t.com, no reply
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")}
// add one more for user2
comment := store.Comment{
ID: "123456xyz",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2018, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user2", Name: "user name"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
assert.Equal(t, 3, len(res), "3 comments initially, for 2 diff users and 2 posts")
assert.NoError(t, err)
err = b.DeleteUser("radio-t", "user1", store.HardDelete)
assert.NoError(t, err)
res, err = b.Last("radio-t", 0, time.Time{}, store.User{})
require.Equal(t, 1, len(res), "only one comment left for user2")
assert.NoError(t, err)
assert.Equal(t, "user2", res[0].User.ID)
}
func TestService_List(t *testing.T) {
// two comments for https://radio-t.com, no reply
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")}
// add one more for user2
comment := store.Comment{
ID: "id-3",
Timestamp: time.Date(2018, 12, 20, 15, 18, 22, 0, time.Local),
Text: `some text, <a href="http://radio-t.com">link</a>`,
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user2", Name: "user name"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
res, err := b.List("radio-t", 0, 0)
assert.NoError(t, err)
require.Equal(t, 2, len(res), "2 posts")
assert.Equal(t, "https://radio-t.com/2", res[0].URL)
assert.Equal(t, 1, res[0].Count)
assert.Equal(t, time.Date(2018, 12, 20, 15, 18, 22, 0, time.Local), res[0].FirstTS)
assert.Equal(t, time.Date(2018, 12, 20, 15, 18, 22, 0, time.Local), res[0].LastTS)
assert.Equal(t, "https://radio-t.com", res[1].URL)
assert.Equal(t, 2, res[1].Count)
assert.Equal(t, time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local), res[1].FirstTS)
assert.Equal(t, time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local), res[1].LastTS)
}
func TestService_Count(t *testing.T) {
// two comments for https://radio-t.com, no reply
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")}
// add one more for user2
comment := store.Comment{
ID: "id-3",
Timestamp: time.Date(2018, 12, 20, 15, 18, 22, 0, time.Local),
Text: `some text, <a href="http://radio-t.com">link</a>`,
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user2", Name: "user name"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
c, err := b.Count(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"})
assert.NoError(t, err)
assert.Equal(t, 2, c)
c, err = b.Count(store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"})
assert.NoError(t, err)
assert.Equal(t, 1, c)
c, err = b.Count(store.Locator{URL: "https://radio-t.com/3", SiteID: "radio-t"})
assert.NoError(t, err)
assert.Equal(t, 0, c)
}
func TestService_UserComments(t *testing.T) {
// two comments for https://radio-t.com, no reply
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")}
// add one more for user2
comment := store.Comment{
ID: "id-3",
Timestamp: time.Date(2018, 12, 20, 15, 18, 22, 0, time.Local),
Text: `some text, <a href="http://radio-t.com">link</a>`,
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user2", Name: "user name"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
cc, err := b.User("radio-t", "user1", 0, 0, store.User{})
assert.NoError(t, err)
require.Equal(t, 2, len(cc), "two recs for user1")
assert.Equal(t, "id-2", cc[0].ID, "reverse sort")
assert.Equal(t, "id-1", cc[1].ID, "reverse sort")
}
func TestService_UserCount(t *testing.T) {
// two comments for https://radio-t.com, no reply
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")}
// add one more for user2
comment := store.Comment{
ID: "id-3",
Timestamp: time.Date(2018, 12, 20, 15, 18, 22, 0, time.Local),
Text: `some text, <a href="http://radio-t.com">link</a>`,
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user2", Name: "user name"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
c, err := b.UserCount("radio-t", "user1")
assert.NoError(t, err)
assert.Equal(t, 2, c)
c, err = b.UserCount("radio-t", "user2")
assert.NoError(t, err)
assert.Equal(t, 1, c)
_, err = b.UserCount("radio-t", "userBad")
assert.EqualError(t, err, "no comments for user userBad in store for radio-t site")
}
func TestService_DeleteAll(t *testing.T) {
// two comments for https://radio-t.com, no reply
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")}
// add one more for user2
comment := store.Comment{
ID: "id-3",
Timestamp: time.Date(2018, 12, 20, 15, 18, 22, 0, time.Local),
Text: `some text, <a href="http://radio-t.com">link</a>`,
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user2", Name: "user name"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
err = b.DeleteAll("radio-t")
assert.NoError(t, err)
res, err := b.Last("radio-t", 0, time.Time{}, store.User{})
assert.NoError(t, err)
assert.Equal(t, 0, len(res))
}
func TestService_submitImages(t *testing.T) {
lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc)
mockStore := image.StoreMock{
CommitFunc: func(string) error { return nil },
ResetCleanupTimerFunc: func(string) error { return nil },
}
imgSvc := image.NewService(&mockStore,
image.ServiceParams{
EditDuration: 50 * time.Millisecond,
ImageAPI: "/images/dev/",
ProxyAPI: "/non_existent",
})
defer imgSvc.Close(context.TODO())
// two comments for https://radio-t.com
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 50 * time.Millisecond,
AdminStore: admin.NewStaticKeyStore("secret 123"), ImageService: imgSvc}
c := store.Comment{
ID: "id-22",
Text: `some text <img src="/images/dev/pic1.png"/> xx <img src="/images/dev/pic2.png"/>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := b.Engine.Create(c) // create directly with engine, doesn't call submitImages
assert.NoError(t, err)
b.submitImages(c)
assert.Equal(t, 2, len(mockStore.ResetCleanupTimerCalls()))
assert.Equal(t, "dev/pic1.png", mockStore.ResetCleanupTimerCalls()[0].ID)
assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[1].ID)
time.Sleep(b.EditDuration + 100*time.Millisecond)
assert.Equal(t, 2, len(mockStore.CommitCalls()))
assert.Equal(t, "dev/pic1.png", mockStore.CommitCalls()[0].ID)
assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[1].ID)
}
func TestService_ResubmitStagingImages(t *testing.T) {
mockStore := image.StoreMock{
InfoFunc: func() (image.StoreInfo, error) {
return image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil
},
CommitFunc: func(string) error {
return nil
},
ResetCleanupTimerFunc: func(string) error { return nil },
}
imgSvc := image.NewService(&mockStore,
image.ServiceParams{
EditDuration: 10 * time.Millisecond,
ImageAPI: "http://127.0.0.1:8080/api/v1/picture/",
ProxyAPI: "http://127.0.0.1:8080/api/v1/img",
})
defer imgSvc.Close(context.TODO())
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvc}
// create comment with three images without preparing it properly
comment := store.Comment{
ID: "id-0",
Text: `<img src="http://127.0.0.1:8080/api/v1/picture/dev_user/bqf122eq9r8ad657n3ng" alt="startrails_01.jpg"><br/>
<img src="http://127.0.0.1:8080/api/v1/picture/dev_user/bqf321eq9r8ad657n3ng" alt="cat.png"><br/>
<img src="http://127.0.0.1:8080/api/v1/img?src=aHR0cHM6Ly9ob21lcGFnZXMuY2FlLndpc2MuZWR1L35lY2U1MzMvaW1hZ2VzL2JvYXQucG5n" alt="cat.png"><br/>
<img src="https://homepages.cae.wisc.edu/~ece533/images/boat.png" alt="boat.png">`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := b.Engine.Create(comment)
require.NoError(t, err)
// resubmit single comment with three images, of which two are in staging storage
err = b.ResubmitStagingImages([]string{"radio-t"})
assert.NoError(t, err)
// wait for Submit goroutine to commit image
time.Sleep(b.EditDuration + time.Millisecond*100)
assert.Equal(t, 1, len(mockStore.InfoCalls()))
assert.Equal(t, 3, len(mockStore.CommitCalls()))
// empty answer
mockStoreEmpty := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) {
return image.StoreInfo{FirstStagingImageTS: time.Time{}}, nil
}}
imgSvcEmpty := image.NewService(&mockStoreEmpty,
image.ServiceParams{
EditDuration: 10 * time.Millisecond,
ImageAPI: "http://127.0.0.1:8080/api/v1/picture/",
})
defer imgSvcEmpty.Close(context.TODO())
bEmpty := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcEmpty}
// resubmit receive empty timestamp and should do nothing )
err = bEmpty.ResubmitStagingImages([]string{"radio-t", "non_existent"})
assert.NoError(t, err)
assert.Equal(t, 1, len(mockStore.InfoCalls()))
// error from image storage
mockStoreError := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) {
return image.StoreInfo{}, fmt.Errorf("mock_err")
}}
imgSvcError := image.NewService(&mockStoreError,
image.ServiceParams{
EditDuration: 10 * time.Millisecond,
ImageAPI: "http://127.0.0.1:8080/api/v1/picture/",
})
defer imgSvcError.Close(context.TODO())
bError := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcError}
// resubmit will receive error from image storage and should return it
err = bError.ResubmitStagingImages([]string{"radio-t"})
assert.EqualError(t, err, "mock_err")
assert.Equal(t, 1, len(mockStore.InfoCalls()))
assert.Equal(t, 3, len(mockStore.ResetCleanupTimerCalls()))
assert.Equal(t, "dev_user/bqf122eq9r8ad657n3ng", mockStore.ResetCleanupTimerCalls()[0].ID)
assert.Equal(t, "dev_user/bqf321eq9r8ad657n3ng", mockStore.ResetCleanupTimerCalls()[1].ID)
assert.Equal(t, "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e", mockStore.ResetCleanupTimerCalls()[2].ID)
}
func TestService_ResubmitStagingImages_EngineError(t *testing.T) {
mockStore := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) {
return image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil
}}
imgSvc := image.NewService(&mockStore,
image.ServiceParams{
EditDuration: 10 * time.Millisecond,
ImageAPI: "http://127.0.0.1:8080/api/v1/picture/",
})
defer imgSvc.Close(context.TODO())
first := true
engineMock := engine.InterfaceMock{
FindFunc: func(engine.FindRequest) ([]store.Comment, error) {
if first {
first = false
return nil, nil
}
return nil, fmt.Errorf("mockError")
},
}
site1Req := engine.FindRequest{Locator: store.Locator{SiteID: "site1", URL: ""}, Sort: "time", Since: time.Time{}.Add(time.Second)}
site2Req := engine.FindRequest{Locator: store.Locator{SiteID: "site2", URL: ""}, Sort: "time", Since: time.Time{}.Add(time.Second)}
b := DataStore{Engine: &engineMock, EditDuration: 10 * time.Millisecond, ImageService: imgSvc}
// One call without error and one with error
err := b.ResubmitStagingImages([]string{"site1", "site2"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "problem finding comments for site site2: mockError")
assert.Equal(t, 1, len(mockStore.InfoCalls()))
assert.Equal(t, 2, len(engineMock.FindCalls()))
assert.Equal(t, site1Req, engineMock.FindCalls()[0].Req)
assert.Equal(t, site2Req, engineMock.FindCalls()[1].Req)
}
func TestService_alterComment(t *testing.T) {
engineMock := engine.InterfaceMock{
FlagFunc: func(engine.FlagRequest) (bool, error) {
return false, nil
},
}
svc := DataStore{Engine: &engineMock}
r := svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid"},
Locator: store.Locator{URL: "http://example.com?foo=bar"}},
store.User{Name: "dev", ID: "devid", Admin: false})
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "", ID: "devid"},
Locator: store.Locator{URL: "http://example.com?foo=bar"}}, r, "ip cleaned")
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid"}},
store.User{Name: "dev", ID: "devid", Admin: true})
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid"}}, r, "ip not cleaned")
assert.Equal(t, 4, len(engineMock.FlagCalls()))
assert.Equal(t, engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}, engineMock.FlagCalls()[0].Req)
assert.Equal(t, engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}, engineMock.FlagCalls()[1].Req)
assert.Equal(t, engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}, engineMock.FlagCalls()[2].Req)
assert.Equal(t, engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}, engineMock.FlagCalls()[3].Req)
first := true
engineMock = engine.InterfaceMock{
FlagFunc: func(engine.FlagRequest) (bool, error) {
if first {
first = false
return false, nil
}
return true, nil
},
}
svc = DataStore{Engine: &engineMock}
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid", Verified: true}},
store.User{Name: "dev", ID: "devid", Admin: false})
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "", ID: "devid", Verified: true}}, r, "verified set")
assert.Equal(t, 2, len(engineMock.FlagCalls()))
assert.Equal(t, engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}, engineMock.FlagCalls()[0].Req)
assert.Equal(t, engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}, engineMock.FlagCalls()[1].Req)
first = true
engineMock = engine.InterfaceMock{
FlagFunc: func(engine.FlagRequest) (bool, error) {
if first {
first = false
return true, nil
}
return false, nil
},
}
svc = DataStore{Engine: &engineMock}
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid", Verified: true},
Locator: store.Locator{URL: "javascript:alert('XSS1')"}},
store.User{Name: "dev", ID: "devid", Admin: false})
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "", Verified: true, Blocked: true, ID: "devid"},
Deleted: false}, r, "blocked")
assert.Equal(t, 1, len(engineMock.FlagCalls()))
assert.Equal(t, engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}, engineMock.FlagCalls()[0].Req)
}
func Benchmark_ServiceCreate(b *testing.B) {
dbFile := fmt.Sprintf("%s/test-remark42-%d.db", os.TempDir(), rand.Intn(9999999999))
defer func() { _ = os.Remove(dbFile) }()
boltStore, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: dbFile, SiteID: "radio-t"})
svc := DataStore{Engine: boltStore, EditDuration: 50 * time.Millisecond, AdminStore: admin.NewStaticKeyStore("secret 123")}
require.NoError(b, err)
defer func() { assert.NoError(b, svc.Close()) }()
for i := 0; i < b.N; i++ {
comment := store.Comment{
ID: "id-" + strconv.Itoa(i),
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = svc.Create(comment)
require.NoError(b, err)
}
}
func TestService_DoubleClose_Bolt(t *testing.T) {
dbFile := fmt.Sprintf("%s/test-remark42-%d.db", os.TempDir(), rand.Intn(9999999999))
defer func() { _ = os.Remove(dbFile) }()
boltStore, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: dbFile, SiteID: "radio-t"})
svc := DataStore{Engine: boltStore, EditDuration: 50 * time.Millisecond, AdminStore: admin.NewStaticKeyStore("secret 123")}
require.NoError(t, err)
assert.NoError(t, boltStore.Close())
assert.NoError(t, boltStore.Close(), "second call should not result in panic or errors")
assert.NoError(t, svc.Close())
assert.NoError(t, svc.Close(), "second call should not result in panic or errors")
}
func TestService_DoubleClose_Static(t *testing.T) {
ks := admin.NewStaticKeyStore("secret 123")
eng, teardown := prepStoreEngine(t)
defer teardown()
b := DataStore{Engine: eng, AdminStore: ks,
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second}, []string{})}
assert.NoError(t, b.Close())
// second call should not result in panic or errors
assert.NoError(t, b.Close())
}
// makes new boltdb, put two records
func prepStoreEngine(t *testing.T) (e engine.Interface, teardown func()) {
testDBLoc, err := os.MkdirTemp("", "test_image_r42")
require.NoError(t, err)
testDB := path.Join(testDBLoc, "test.db")
_ = os.Remove(testDB)
st := time.Now()
boltStore, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDB, SiteID: "radio-t"})
assert.NoError(t, err)
comment := store.Comment{
ID: "id-1",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = boltStore.Create(comment)
assert.NoError(t, err)
comment = store.Comment{
ID: "id-2",
Text: "some text2",
Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = boltStore.Create(comment)
assert.NoError(t, err)
t.Logf("prepared store engine in %v", time.Since(st))
return boltStore, func() {
assert.NoError(t, boltStore.Close())
_ = os.Remove(testDB)
}
}
func getReq(locator store.Locator, commentID string) engine.GetRequest {
return engine.GetRequest{
Locator: locator,
CommentID: commentID,
}
}