refactor store to isolate service and engine levels

This commit is contained in:
Umputun
2018-05-16 18:03:08 -05:00
parent 7e6a15a0f4
commit 9638d95279
22 changed files with 314 additions and 234 deletions
+7 -6
View File
@@ -13,13 +13,14 @@ import (
"github.com/hashicorp/logutils"
"github.com/jessevdk/go-flags"
"github.com/pkg/errors"
"github.com/umputun/remark/app/store/engine"
"github.com/umputun/remark/app/store/service"
"github.com/umputun/remark/app/migrator"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/rest/api"
"github.com/umputun/remark/app/rest/auth"
"github.com/umputun/remark/app/rest/avatar"
"github.com/umputun/remark/app/store"
)
var opts struct {
@@ -74,7 +75,7 @@ func main() {
log.Printf("[WARN] running in dev mode")
}
dataService := store.Service{
dataService := service.DataStore{
Interface: dataStore,
EditDuration: 5 * time.Minute,
Secret: opts.SecretKey,
@@ -142,12 +143,12 @@ func activateBackup(exporter migrator.Exporter) {
}
// makeBoltStore creates store for all sites
func makeBoltStore(siteNames []string) store.Interface {
sites := []store.BoltSite{}
func makeBoltStore(siteNames []string) engine.Interface {
sites := []engine.BoltSite{}
for _, site := range siteNames {
sites = append(sites, store.BoltSite{SiteID: site, FileName: fmt.Sprintf("%s/%s.db", opts.BoltPath, site)})
sites = append(sites, engine.BoltSite{SiteID: site, FileName: fmt.Sprintf("%s/%s.db", opts.BoltPath, site)})
}
result, err := store.NewBoltDB(bolt.Options{Timeout: 30 * time.Second}, sites...)
result, err := engine.NewBoltDB(bolt.Options{Timeout: 30 * time.Second}, sites...)
if err != nil {
log.Fatalf("[ERROR] can't initialize data store, %+v", err)
}
+2 -1
View File
@@ -10,11 +10,12 @@ import (
"github.com/pkg/errors"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/service"
)
// Disqus implements Importer from disqus xml
type Disqus struct {
DataStore store.Interface
DataStore *service.DataStore
}
type disqusThread struct {
+7 -4
View File
@@ -8,16 +8,19 @@ import (
"github.com/coreos/bbolt"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/engine"
"github.com/umputun/remark/app/store/service"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/app/store"
)
func TestDisqus_Import(t *testing.T) {
defer os.Remove("/tmp/remark-test.db")
dataStore, err := store.NewBoltDB(bolt.Options{}, store.BoltSite{FileName: "/tmp/remark-test.db", SiteID: "test"})
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: "/tmp/remark-test.db", SiteID: "test"})
require.Nil(t, err, "create store")
d := Disqus{DataStore: dataStore}
dataStore := service.DataStore{Interface: b}
d := Disqus{DataStore: &dataStore}
size, err := d.Import(strings.NewReader(xmlTest), "test")
assert.Nil(t, err)
assert.Equal(t, 3, size)
@@ -33,7 +36,7 @@ func TestDisqus_Import(t *testing.T) {
assert.Equal(t, store.Locator{SiteID: "test", URL: "http://radio-t.umputun.com/2011/03/229_8880.html"}, c.Locator)
assert.Equal(t, "Dmitry Noname", c.User.Name)
assert.Equal(t, "disqus_8799342cdf328253e03313958ffc6a433659d7ff", c.User.ID)
assert.Equal(t, "89.89.89.139", c.User.IP)
assert.Equal(t, "96243f024cf6ad42b66f0c72709ae20b5d10ec14", c.User.IP)
posts, err := dataStore.List("test", 0, 0)
assert.Nil(t, err)
+2 -2
View File
@@ -10,7 +10,7 @@ import (
"github.com/pkg/errors"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/service"
)
// Importer defines interface to convert posts from external sources
@@ -25,7 +25,7 @@ type Exporter interface {
// ImportParams defines everything needed to run import
type ImportParams struct {
DataStore *store.Service
DataStore *service.DataStore
InputFile string
Provider string
SiteID string
+6 -5
View File
@@ -8,8 +8,9 @@ import (
"github.com/coreos/bbolt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/store/service"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/engine"
)
func TestMigrator_ImportDisqus(t *testing.T) {
@@ -21,9 +22,9 @@ func TestMigrator_ImportDisqus(t *testing.T) {
err := ioutil.WriteFile("/tmp/disqus-test.xml", []byte(xmlTest), 0600)
require.Nil(t, err)
b, err := store.NewBoltDB(bolt.Options{}, store.BoltSite{FileName: "/tmp/remark-test.db", SiteID: "test"})
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: "/tmp/remark-test.db", SiteID: "test"})
require.Nil(t, err, "create store")
dataStore := &store.Service{Interface: b}
dataStore := &service.DataStore{Interface: b}
size, err := ImportComments(ImportParams{
DataStore: dataStore,
InputFile: "/tmp/disqus-test.xml",
@@ -50,9 +51,9 @@ func TestMigrator_ImportRemark(t *testing.T) {
err := ioutil.WriteFile("/tmp/disqus-test.r42", []byte(data), 0600)
require.Nil(t, err)
b, err := store.NewBoltDB(bolt.Options{}, store.BoltSite{FileName: "/tmp/remark-test.db", SiteID: "radio-t"})
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: "/tmp/remark-test.db", SiteID: "radio-t"})
require.Nil(t, err, "create store")
dataStore := &store.Service{Interface: b}
dataStore := &service.DataStore{Interface: b}
size, err := ImportComments(ImportParams{
DataStore: dataStore,
+2 -1
View File
@@ -8,13 +8,14 @@ import (
"log"
"github.com/pkg/errors"
"github.com/umputun/remark/app/store/service"
"github.com/umputun/remark/app/store"
)
// Remark implements exporter and importer for internal store format
type Remark struct {
DataStore *store.Service
DataStore *service.DataStore
}
// Export all comments to writer as json strings. Each comment is one string, separated by "\n"
+7 -6
View File
@@ -9,8 +9,9 @@ import (
"github.com/coreos/bbolt"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/engine"
"github.com/umputun/remark/app/store/service"
)
var testDb = "/tmp/test-remark.db"
@@ -41,9 +42,9 @@ func TestRemark_Import(t *testing.T) {
buf.WriteString(r2)
os.Remove(testDb)
b, err := store.NewBoltDB(bolt.Options{}, store.BoltSite{SiteID: "radio-t", FileName: testDb})
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{SiteID: "radio-t", FileName: testDb})
assert.Nil(t, err)
r := Remark{DataStore: &store.Service{Interface: b}}
r := Remark{DataStore: &service.DataStore{Interface: b}}
size, err := r.Import(buf, "radio-t")
assert.Nil(t, err)
assert.Equal(t, 2, size)
@@ -57,13 +58,13 @@ func TestRemark_Import(t *testing.T) {
}
// makes new boltdb, put two records
func prep(t *testing.T) *store.Service {
func prep(t *testing.T) *service.DataStore {
os.Remove(testDb)
boltStore, err := store.NewBoltDB(bolt.Options{}, store.BoltSite{SiteID: "radio-t", FileName: testDb})
boltStore, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{SiteID: "radio-t", FileName: testDb})
assert.Nil(t, err)
b := &store.Service{Interface: boltStore}
b := &service.DataStore{Interface: boltStore}
comment := store.Comment{
ID: "efbc17f177ee1a1c0ee6e1e025749966ec071adc",
+2 -1
View File
@@ -14,11 +14,12 @@ import (
"github.com/umputun/remark/app/migrator"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/service"
)
// admin provides router for all requests available for admin users only
type admin struct {
dataService store.Service
dataService service.DataStore
exporter migrator.Exporter
cache rest.LoadingCache
defAvatarURL string
+4 -3
View File
@@ -14,9 +14,10 @@ import (
"github.com/coreos/bbolt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/store/engine"
"github.com/umputun/remark/app/store/service"
"github.com/umputun/remark/app/migrator"
"github.com/umputun/remark/app/store"
)
func TestImport(t *testing.T) {
@@ -52,9 +53,9 @@ func TestImportRejected(t *testing.T) {
}
func prepImportSrv(t *testing.T) (srv *Import, port int) {
b, err := store.NewBoltDB(bolt.Options{}, store.BoltSite{FileName: testDb, SiteID: "radio-t"})
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDb, SiteID: "radio-t"})
require.Nil(t, err)
dataStore := &store.Service{Interface: b}
dataStore := &service.DataStore{Interface: b}
srv = &Import{
DisqusImporter: &migrator.Disqus{DataStore: dataStore},
NativeImporter: &migrator.Remark{DataStore: dataStore},
+3 -2
View File
@@ -19,6 +19,7 @@ import (
"github.com/go-chi/render"
"github.com/gorilla/context"
"github.com/pkg/errors"
"github.com/umputun/remark/app/store/service"
"gopkg.in/russross/blackfriday.v2"
"github.com/umputun/remark/app/migrator"
@@ -30,7 +31,7 @@ import (
// Rest is a rest access server
type Rest struct {
Version string
DataService store.Service
DataService service.DataStore
Authenticator auth.Authenticator
Exporter migrator.Exporter
Cache rest.LoadingCache
@@ -238,7 +239,7 @@ func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
return
}
editReq := store.EditRequest{
editReq := service.EditRequest{
Text: string(blackfriday.Run([]byte(edit.Text), blackfriday.WithExtensions(mdExt))), // render markdown
Orig: edit.Text,
Summary: edit.Summary,
+10 -8
View File
@@ -17,11 +17,13 @@ import (
"github.com/gorilla/sessions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/app/rest/avatar"
"github.com/umputun/remark/app/store/service"
"github.com/umputun/remark/app/migrator"
"github.com/umputun/remark/app/rest/auth"
"github.com/umputun/remark/app/rest/avatar"
"github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/engine"
)
var testDb = "/tmp/test-remark.db"
@@ -147,7 +149,7 @@ func TestServer_CreateAndGet(t *testing.T) {
assert.Equal(t, `<p><strong>test</strong> <em>123</em> <a href="http://radio-t.com" rel="nofollow">http://radio-t.com</a></p>`+"\n", comment.Text)
assert.Equal(t, "**test** *123* http://radio-t.com", comment.Orig)
assert.Equal(t, store.User{Name: "developer one", ID: "dev",
Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: "ea64bfc178468d943ca5b836e2e700c335404973"},
Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: "dbc7c999343f003f189f70aaf52cc04443f90790"},
comment.User)
t.Logf("%+v", comment)
}
@@ -462,11 +464,11 @@ func TestServer_FileServer(t *testing.T) {
}
func prep(t *testing.T) (srv *Rest, port int) {
b, err := store.NewBoltDB(bolt.Options{}, store.BoltSite{FileName: testDb, SiteID: "radio-t"})
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDb, SiteID: "radio-t"})
require.Nil(t, err)
dataStore := &store.Service{Interface: b}
dataStore := service.DataStore{Interface: b, EditDuration: 5 * time.Minute, MaxCommentSize: 4000, Secret: "123456"}
srv = &Rest{
DataService: store.Service{Interface: dataStore, EditDuration: 5 * time.Minute, MaxCommentSize: 4000},
DataService: dataStore,
Authenticator: auth.Authenticator{
SessionStore: sessions.NewFilesystemStore("/tmp", []byte("blah")),
DevPasswd: "password",
@@ -474,14 +476,14 @@ func prep(t *testing.T) (srv *Rest, port int) {
AvatarProxy: &avatar.Proxy{StorePath: "/tmp", RoutePath: "/api/v1/avatar"},
Admins: []string{"a1", "a2"},
},
Exporter: &migrator.Remark{DataStore: dataStore},
Exporter: &migrator.Remark{DataStore: &dataStore},
Cache: &mockCache{},
WebRoot: "/tmp",
}
importSrv := &Import{
DisqusImporter: &migrator.Disqus{DataStore: dataStore},
NativeImporter: &migrator.Remark{DataStore: dataStore},
DisqusImporter: &migrator.Disqus{DataStore: &dataStore},
NativeImporter: &migrator.Remark{DataStore: &dataStore},
Cache: &mockCache{},
}
+1 -1
View File
@@ -15,10 +15,10 @@ import (
"github.com/go-chi/chi"
"github.com/go-chi/render"
"github.com/gorilla/sessions"
"github.com/umputun/remark/app/rest/avatar"
"golang.org/x/oauth2"
"github.com/umputun/remark/app/rest"
"github.com/umputun/remark/app/rest/avatar"
"github.com/umputun/remark/app/store"
)
-46
View File
@@ -1,14 +1,8 @@
package store
import (
"crypto/hmac"
"crypto/sha1"
"fmt"
"hash/crc64"
"html/template"
"log"
"regexp"
"strconv"
"time"
"github.com/microcosm-cc/bluemonday"
@@ -36,16 +30,6 @@ type Locator struct {
URL string `json:"url"`
}
// User holds user-related info
type User struct {
Name string `json:"name"`
ID string `json:"id"`
Picture string `json:"picture"`
Admin bool `json:"admin"`
Blocked bool `json:"block,omitempty"`
IP string `json:"ip,omitempty"`
}
// Edit indication
type Edit struct {
Timestamp time.Time `json:"time"`
@@ -97,33 +81,3 @@ func (c *Comment) Sanitize() {
c.User.Name = template.HTMLEscapeString(c.User.Name)
c.User.Picture = p.Sanitize(c.User.Picture)
}
// hashIP replace IP field with hashed hmac
func (u *User) hashIP(secret string) {
hashVal := func(val string) string {
if _, err := strconv.ParseUint(val, 16, 64); err == nil || val == "" {
return val // already hashed
}
key := []byte(secret)
h := hmac.New(sha1.New, key)
if _, err := h.Write([]byte(val)); err != nil {
log.Printf("[WARN] can't hash ip, %s", err)
}
return fmt.Sprintf("%x", h.Sum(nil))
}
u.IP = hashVal(u.IP)
}
// EncodeID hashes id to sha1. The function intentionally left outside of User struct because in some cases
// we need hashing for parts of id, in some others hashing for non-User values.
func EncodeID(id string) string {
h := sha1.New()
if _, err := h.Write([]byte(id)); err != nil {
// fail back to crc64
log.Printf("[WARN] can't hash id %s, %s", id, err)
return fmt.Sprintf("%x", crc64.Checksum([]byte(id), crc64.MakeTable(crc64.ECMA)))
}
return fmt.Sprintf("%x", h.Sum(nil))
}
-15
View File
@@ -58,18 +58,3 @@ func TestComment_PrepareUntrusted(t *testing.T) {
assert.Equal(t, User{ID: "username"}, comment.User)
}
func TestComment_EncodeID(t *testing.T) {
tbl := []struct {
id string
hash string
}{
{"myid", "6e34471f84557e1713012d64a7477c71bfdac631"},
{"", "da39a3ee5e6b4b0d3255bfef95601890afd80709"},
{"blah blah", "135a1e01bae742c4a576b20fd41a683f6483ca43"},
}
for i, tt := range tbl {
assert.Equal(t, tt.hash, EncodeID(tt.id), "case #%d", i)
}
}
+23 -21
View File
@@ -1,4 +1,4 @@
package store
package engine
import (
"encoding/json"
@@ -10,6 +10,8 @@ import (
"github.com/coreos/bbolt"
"github.com/pkg/errors"
"github.com/umputun/remark/app/store"
)
// BoltDB implements store.Interface, represents multiple sites with multiplexing to different bolt dbs. Thread safe.
@@ -78,7 +80,7 @@ func NewBoltDB(options bolt.Options, sites ...BoltSite) (*BoltDB, error) {
}
// Create saves new comment to store. Adds to posts bucket, reference to last and user bucket and increments count bucket
func (b *BoltDB) Create(comment Comment) (commentID string, err error) {
func (b *BoltDB) Create(comment store.Comment) (commentID string, err error) {
bdb, err := b.db(comment.Locator.SiteID)
if err != nil {
@@ -135,7 +137,7 @@ func (b *BoltDB) Create(comment Comment) (commentID string, err error) {
// Delete removes comment, by locator from the store.
// Posts collection only sets status to deleted and clear fields in order to prevent breaking trees of replies.
// From last bucket removed for real.
func (b *BoltDB) Delete(locator Locator, commentID string) error {
func (b *BoltDB) Delete(locator store.Locator, commentID string) error {
bdb, err := b.db(locator.SiteID)
if err != nil {
@@ -204,8 +206,8 @@ func (b *BoltDB) DeleteAll(siteID string) error {
}
// Find returns all comments for post and sorts results
func (b *BoltDB) Find(locator Locator, sortFld string) (comments []Comment, err error) {
comments = []Comment{}
func (b *BoltDB) Find(locator store.Locator, sortFld string) (comments []store.Comment, err error) {
comments = []store.Comment{}
bdb, err := b.db(locator.SiteID)
if err != nil {
@@ -220,7 +222,7 @@ func (b *BoltDB) Find(locator Locator, sortFld string) (comments []Comment, err
}
return bucket.ForEach(func(k, v []byte) error {
comment := Comment{}
comment := store.Comment{}
if e := json.Unmarshal(v, &comment); e != nil {
return errors.Wrap(e, "failed to unmarshal")
}
@@ -234,7 +236,7 @@ func (b *BoltDB) Find(locator Locator, sortFld string) (comments []Comment, err
}
// Last returns up to max last comments for given siteID
func (b *BoltDB) Last(siteID string, max int) (comments []Comment, err error) {
func (b *BoltDB) Last(siteID string, max int) (comments []store.Comment, err error) {
if max > lastLimit || max == 0 {
max = lastLimit
@@ -278,7 +280,7 @@ func (b *BoltDB) Last(siteID string, max int) (comments []Comment, err error) {
}
// Count returns number of comments for locator
func (b *BoltDB) Count(locator Locator) (count int, err error) {
func (b *BoltDB) Count(locator store.Locator) (count int, err error) {
bdb, err := b.db(locator.SiteID)
if err != nil {
@@ -336,8 +338,8 @@ func (b *BoltDB) IsBlocked(siteID string, userID string) (blocked bool) {
// Blocked get lists of blocked users for given site
// bucket uses userID:
func (b *BoltDB) Blocked(siteID string) (users []BlockedUser, err error) {
users = []BlockedUser{}
func (b *BoltDB) Blocked(siteID string) (users []store.BlockedUser, err error) {
users = []store.BlockedUser{}
bdb, err := b.db(siteID)
if err != nil {
return nil, err
@@ -357,7 +359,7 @@ func (b *BoltDB) Blocked(siteID string) (users []BlockedUser, err error) {
userName = userComments[0].User.Name
}
users = append(users, BlockedUser{ID: string(k), Name: userName, Timestamp: ts})
users = append(users, store.BlockedUser{ID: string(k), Name: userName, Timestamp: ts})
return nil
})
})
@@ -367,7 +369,7 @@ func (b *BoltDB) Blocked(siteID string) (users []BlockedUser, err error) {
// List returns list of all commented posts with counters
// uses count bucket to get number of comments
func (b BoltDB) List(siteID string, limit, skip int) (list []PostInfo, err error) {
func (b BoltDB) List(siteID string, limit, skip int) (list []store.PostInfo, err error) {
bdb, err := b.db(siteID)
if err != nil {
@@ -389,7 +391,7 @@ func (b BoltDB) List(siteID string, limit, skip int) (list []PostInfo, err error
if e != nil {
return e
}
list = append(list, PostInfo{URL: postURL, Count: count})
list = append(list, store.PostInfo{URL: postURL, Count: count})
if limit > 0 && len(list) >= limit {
break
}
@@ -402,9 +404,9 @@ func (b BoltDB) List(siteID string, limit, skip int) (list []PostInfo, err error
// User extracts all comments for given site and given userID
// "users" bucket has sub-bucket for each userID, and keeps it as ts:ref
func (b *BoltDB) User(siteID string, userID string, limit int) (comments []Comment, totalComments int, err error) {
func (b *BoltDB) User(siteID string, userID string, limit int) (comments []store.Comment, totalComments int, err error) {
comments = []Comment{}
comments = []store.Comment{}
commentRefs := []string{}
bdb, err := b.db(siteID)
@@ -445,7 +447,7 @@ func (b *BoltDB) User(siteID string, userID string, limit int) (comments []Comme
if e != nil {
return comments, totalComments, errors.Wrapf(e, "can't parse reference %s", v)
}
if c, e := b.Get(Locator{SiteID: siteID, URL: url}, commentID); e == nil {
if c, e := b.Get(store.Locator{SiteID: siteID, URL: url}, commentID); e == nil {
comments = append(comments, c)
}
}
@@ -454,7 +456,7 @@ func (b *BoltDB) User(siteID string, userID string, limit int) (comments []Comme
}
// Get returns comment for locator.URL and commentID string
func (b *BoltDB) Get(locator Locator, commentID string) (comment Comment, err error) {
func (b *BoltDB) Get(locator store.Locator, commentID string) (comment store.Comment, err error) {
bdb, err := b.db(locator.SiteID)
if err != nil {
@@ -473,7 +475,7 @@ func (b *BoltDB) Get(locator Locator, commentID string) (comment Comment, err er
}
// Put updates comment for locator.URL with mutable part of comment
func (b *BoltDB) Put(locator Locator, comment Comment) error {
func (b *BoltDB) Put(locator store.Locator, comment store.Comment) error {
if curComment, err := b.Get(locator, comment.ID); err == nil {
// preserve immutable fields
@@ -533,7 +535,7 @@ func (b *BoltDB) getUserBucket(tx *bolt.Tx, userID string) (*bolt.Bucket, error)
}
// save comment to key for bucket. Should run in update tx
func (b *BoltDB) save(bkt *bolt.Bucket, key []byte, comment Comment) (err error) {
func (b *BoltDB) save(bkt *bolt.Bucket, key []byte, comment store.Comment) (err error) {
jdata, jerr := json.Marshal(&comment)
if jerr != nil {
return errors.Wrap(jerr, "can't marshal comment")
@@ -545,7 +547,7 @@ func (b *BoltDB) save(bkt *bolt.Bucket, key []byte, comment Comment) (err error)
}
// load comment by key from bucket. Should run in view tx
func (b *BoltDB) load(bkt *bolt.Bucket, key []byte) (comment Comment, err error) {
func (b *BoltDB) load(bkt *bolt.Bucket, key []byte) (comment store.Comment, err error) {
commentVal := bkt.Get(key)
if commentVal == nil {
return comment, errors.Errorf("no comment for %s", key)
@@ -590,7 +592,7 @@ func (b *BoltDB) db(siteID string) (*bolt.DB, error) {
}
// makeRef creates reference combining url and comment id
func (b *BoltDB) makeRef(comment Comment) []byte {
func (b *BoltDB) makeRef(comment store.Comment) []byte {
return []byte(fmt.Sprintf("%s!!%s", comment.Locator.URL, comment.ID))
}
@@ -1,4 +1,4 @@
package store
package engine
import (
"os"
@@ -7,6 +7,8 @@ import (
"github.com/coreos/bbolt"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/app/store"
)
var testDb = "/tmp/test-remark.db"
@@ -15,14 +17,14 @@ func TestBoltDB_CreateAndFind(t *testing.T) {
var b = prep(t)
defer os.Remove(testDb)
res, err := b.Find(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "time")
res, err := b.Find(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, `some text, <a href="http://radio-t.com">link</a>`, res[0].Text)
assert.Equal(t, "user1", res[0].User.ID)
t.Log(res[0].ID)
_, err = b.Create(Comment{ID: res[0].ID, Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"}})
_, err = b.Create(store.Comment{ID: res[0].ID, Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}})
assert.NotNil(t, err)
assert.Equal(t, "key id-1 already in store", err.Error())
}
@@ -31,7 +33,7 @@ func TestBoltDB_Delete(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
loc := Locator{URL: "https://radio-t.com", SiteID: "radio-t"}
loc := store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}
res, err := b.Find(loc, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res), "initially 2 comments")
@@ -56,7 +58,7 @@ func TestBoltDB_DeleteAll(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
loc := Locator{URL: "https://radio-t.com", SiteID: "radio-t"}
loc := store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}
res, err := b.Find(loc, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res), "initially 2 comments")
@@ -68,7 +70,7 @@ func TestBoltDB_DeleteAll(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 0, len(comments), "nothing left")
c, err := b.Count(Locator{URL: "https://radio-t.com", SiteID: "radio-t"})
c, err := b.Count(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"})
assert.Nil(t, err)
assert.Equal(t, 0, c, "0 count")
}
@@ -77,22 +79,22 @@ func TestBoltDB_Get(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
res, err := b.Find(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "time")
res, err := b.Find(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
comment, err := b.Get(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[1].ID)
comment, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[1].ID)
assert.Nil(t, err)
assert.Equal(t, "some text2", comment.Text)
comment, err = b.Get(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "1234567")
comment, err = b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "1234567")
assert.NotNil(t, err)
}
func TestBoltDB_Put(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
loc := Locator{URL: "https://radio-t.com", SiteID: "radio-t"}
loc := store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}
res, err := b.Find(loc, "time")
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
@@ -129,7 +131,7 @@ func TestBoltDB_Count(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
c, err := b.Count(Locator{URL: "https://radio-t.com", SiteID: "radio-t"})
c, err := b.Count(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"})
assert.Nil(t, err)
assert.Equal(t, 2, c)
}
@@ -171,27 +173,27 @@ func TestBoltDB_List(t *testing.T) {
b := prep(t) // two comments for https://radio-t.com
// add one more for https://radio-t.com/2
comment := Comment{
comment := store.Comment{
ID: "12345",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: User{ID: "user1", Name: "user name"},
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.Nil(t, err)
res, err := b.List("radio-t", 0, 0)
assert.Nil(t, err)
assert.Equal(t, []PostInfo{{URL: "https://radio-t.com/2", Count: 1}, {URL: "https://radio-t.com", Count: 2}}, res)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1}, {URL: "https://radio-t.com", Count: 2}}, res)
res, err = b.List("radio-t", 1, 0)
assert.Nil(t, err)
assert.Equal(t, []PostInfo{{URL: "https://radio-t.com/2", Count: 1}}, res)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1}}, res)
res, err = b.List("radio-t", 1, 1)
assert.Nil(t, err)
assert.Equal(t, []PostInfo{{URL: "https://radio-t.com", Count: 2}}, res)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com", Count: 2}}, res)
}
func TestBoltDB_GetForUser(t *testing.T) {
@@ -220,22 +222,22 @@ func prep(t *testing.T) *BoltDB {
assert.Nil(t, err)
b := boltStore
comment := Comment{
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: Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: User{ID: "user1", Name: "user name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = b.Create(comment)
assert.Nil(t, err)
comment = Comment{
comment = store.Comment{
ID: "id-2",
Text: "some text2",
Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: User{ID: "user1", Name: "user name"},
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = b.Create(comment)
assert.Nil(t, err)
@@ -1,12 +1,16 @@
package store
//go:generate sh -c "mockery -inpkg -name Interface -print > file.tmp && mv file.tmp store_mock.go"
// Package engine defines interfaces each supported storage should implement.
// Includes default implementation with boltdb
package engine
import (
"sort"
"strings"
R "github.com/umputun/remark/app/store"
)
//go:generate sh -c "mockery -inpkg -name Interface -print > file.tmp && mv file.tmp engine_mock.go"
// Interface combines all store interfaces
type Interface interface {
Accessor
@@ -15,26 +19,26 @@ type Interface interface {
// Accessor defines all usual access ops avail for regular user
type Accessor interface {
Create(comment Comment) (commentID string, err error) // create new comment, avoid dups by id
Get(locator Locator, commentID string) (comment Comment, err error) // get comment by id
Put(locator Locator, comment Comment) error // update comment, mutable parts only
Find(locator Locator, sort string) ([]Comment, error) // find comments for locator
Last(siteID string, limit int) ([]Comment, error) // last comments for given site, sorted by time
User(siteID string, userID string, limit int) ([]Comment, int, error) // comments by user, sorted by time
Count(locator Locator) (int, error) // number of comments for the post
List(siteID string, limit int, skip int) ([]PostInfo, error) // list of commented posts
Create(comment R.Comment) (commentID string, err error) // create new comment, avoid dups by id
Get(locator R.Locator, commentID string) (comment R.Comment, err error) // get comment by id
Put(locator R.Locator, comment R.Comment) error // update comment, mutable parts only
Find(locator R.Locator, sort string) ([]R.Comment, error) // find comments for locator
Last(siteID string, limit int) ([]R.Comment, error) // last comments for given site, sorted by time
User(siteID string, userID string, limit int) ([]R.Comment, int, error) // comments by user, sorted by time
Count(locator R.Locator) (int, error) // number of comments for the post
List(siteID string, limit int, skip int) ([]R.PostInfo, error) // list of commented posts
}
// Admin defines all store ops avail for admin only
type Admin interface {
Delete(locator Locator, commentID string) error // delete comment by id
Delete(locator R.Locator, commentID string) error // delete comment by id
DeleteAll(siteID string) error // delete all data from site
SetBlock(siteID string, userID string, status bool) error // block or unblock user
IsBlocked(siteID string, userID string) bool // check if user blocked
Blocked(siteID string) ([]BlockedUser, error) // get list of blocked users
Blocked(siteID string) ([]R.BlockedUser, error) // get list of blocked users
}
func sortComments(comments []Comment, sortFld string) []Comment {
func sortComments(comments []R.Comment, sortFld string) []R.Comment {
sort.Slice(comments, func(i, j int) bool {
switch sortFld {
case "+time", "-time", "time", "+active", "-active", "active":
@@ -1,14 +1,16 @@
package store
package engine
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/app/store"
)
func TestStore_sortComments(t *testing.T) {
cc := []Comment{
func TestEngine_sortComments(t *testing.T) {
cc := []store.Comment{
{ID: "1", Score: 5, Timestamp: time.Date(2018, 2, 5, 10, 1, 0, 0, time.Local)},
{ID: "2", Score: 4, Timestamp: time.Date(2018, 2, 5, 10, 2, 0, 0, time.Local)},
{ID: "3", Score: 6, Timestamp: time.Date(2018, 2, 5, 10, 3, 0, 0, time.Local)},
@@ -1,15 +1,18 @@
package store
package service
import (
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
R "github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/engine"
)
// Service wraps store.Interface with additional methods
type Service struct {
Interface
// DataStore wraps store.Interface with additional methods
type DataStore struct {
engine.Interface
EditDuration time.Duration
Secret string
MaxCommentSize int
@@ -18,7 +21,7 @@ type Service struct {
const defaultCommentMaxSize = 2000
// Create prepares comment and forward to Interface.Create
func (s *Service) Create(comment Comment) (commentID string, err error) {
func (s *DataStore) Create(comment R.Comment) (commentID string, err error) {
// fill ID and time if empty
if comment.ID == "" {
comment.ID = uuid.New().String()
@@ -32,13 +35,13 @@ func (s *Service) Create(comment Comment) (commentID string, err error) {
}
comment.Sanitize() // clear potentially dangerous js from all parts of comment
comment.User.hashIP(s.Secret) // replace ip by hash
comment.User.HashIP(s.Secret) // replace ip by hash
return s.Interface.Create(comment)
}
// SetPin pin/un-pin comment as special
func (s *Service) SetPin(locator Locator, commentID string, status bool) error {
func (s *DataStore) SetPin(locator R.Locator, commentID string, status bool) error {
comment, err := s.Get(locator, commentID)
if err != nil {
return err
@@ -48,7 +51,7 @@ func (s *Service) SetPin(locator Locator, commentID string, status bool) error {
}
// Vote for comment by id and locator
func (s *Service) Vote(locator Locator, commentID string, userID string, val bool) (comment Comment, err error) {
func (s *DataStore) Vote(locator R.Locator, commentID string, userID string, val bool) (comment R.Comment, err error) {
comment, err = s.Get(locator, commentID)
if err != nil {
@@ -96,7 +99,7 @@ type EditRequest struct {
}
// EditComment to edit text and update Edit info
func (s *Service) EditComment(locator Locator, commentID string, req EditRequest) (comment Comment, err error) {
func (s *DataStore) EditComment(locator R.Locator, commentID string, req EditRequest) (comment R.Comment, err error) {
comment, err = s.Get(locator, commentID)
if err != nil {
return comment, err
@@ -113,7 +116,7 @@ func (s *Service) EditComment(locator Locator, commentID string, req EditRequest
comment.Text = req.Text
comment.Orig = req.Orig
comment.Edit = &Edit{
comment.Edit = &R.Edit{
Timestamp: time.Now(),
Summary: req.Summary,
}
@@ -124,18 +127,18 @@ func (s *Service) EditComment(locator Locator, commentID string, req EditRequest
}
// Counts returns postID+count list for given comments
func (s *Service) Counts(siteID string, postIDs []string) ([]PostInfo, error) {
res := []PostInfo{}
func (s *DataStore) Counts(siteID string, postIDs []string) ([]R.PostInfo, error) {
res := []R.PostInfo{}
for _, p := range postIDs {
if c, err := s.Count(Locator{SiteID: siteID, URL: p}); err == nil {
res = append(res, PostInfo{URL: p, Count: c})
if c, err := s.Count(R.Locator{SiteID: siteID, URL: p}); err == nil {
res = append(res, R.PostInfo{URL: p, Count: c})
}
}
return res, nil
}
// ValidateComment checks if comment size below max and user fields set
func (s *Service) ValidateComment(c *Comment) error {
func (s *DataStore) ValidateComment(c *R.Comment) error {
maxSize := s.MaxCommentSize
if s.MaxCommentSize <= 0 {
maxSize = defaultCommentMaxSize
@@ -1,4 +1,4 @@
package store
package service
import (
"fmt"
@@ -10,21 +10,26 @@ import (
"github.com/coreos/bbolt"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
R "github.com/umputun/remark/app/store"
"github.com/umputun/remark/app/store/engine"
)
var testDb = "/tmp/test-remark.db"
func TestService_CreateFromEmpty(t *testing.T) {
defer os.Remove(testDb)
b := Service{Interface: prep(t), Secret: "secret 123"}
comment := Comment{
b := DataStore{Interface: prepStoreEngine(t), Secret: "secret 123"}
comment := R.Comment{
Text: "text",
User: User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: R.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: R.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.Get(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id)
res, err := b.Get(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id)
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "text", res.Text)
@@ -37,19 +42,19 @@ func TestService_CreateFromEmpty(t *testing.T) {
func TestService_CreateFromPartial(t *testing.T) {
defer os.Remove(testDb)
b := Service{Interface: prep(t), Secret: "secret 123"}
comment := Comment{
b := DataStore{Interface: prepStoreEngine(t), Secret: "secret 123"}
comment := R.Comment{
Text: "text",
Timestamp: time.Date(2018, 3, 25, 16, 34, 33, 0, time.UTC),
Votes: map[string]bool{"u1": true, "u2": false},
User: User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: R.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: R.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.Get(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id)
res, err := b.Get(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id)
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "text", res.Text)
@@ -62,12 +67,12 @@ func TestService_CreateFromPartial(t *testing.T) {
func TestService_Vote(t *testing.T) {
defer os.Remove(testDb)
b := Service{Interface: prep(t)}
b := DataStore{Interface: prepStoreEngine(t)}
comment := Comment{
comment := R.Comment{
Text: "text",
User: User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: R.User{IP: "192.168.1.1", ID: "user", Name: "name"},
Locator: R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
@@ -79,15 +84,15 @@ func TestService_Vote(t *testing.T) {
assert.Equal(t, 0, res[0].Score)
assert.Equal(t, map[string]bool{}, res[0].Votes, "no votes initially")
c, err := b.Vote(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true)
c, err := b.Vote(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true)
assert.Nil(t, err)
assert.Equal(t, 1, c.Score)
assert.Equal(t, map[string]bool{"user1": true}, c.Votes, "user voted +")
c, err = b.Vote(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user", true)
c, err = b.Vote(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user", true)
assert.NotNil(t, err, "self-voting not allowed")
_, err = b.Vote(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true)
_, err = b.Vote(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", true)
assert.NotNil(t, err, "double-voting rejected")
assert.True(t, strings.HasPrefix(err.Error(), "user user1 already voted"))
@@ -96,7 +101,7 @@ func TestService_Vote(t *testing.T) {
assert.Equal(t, 3, len(res))
assert.Equal(t, 1, res[0].Score)
_, err = b.Vote(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", false)
_, err = b.Vote(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", false)
assert.Nil(t, err, "vote reset")
res, err = b.Last("radio-t", 0)
assert.Nil(t, err)
@@ -107,7 +112,7 @@ func TestService_Vote(t *testing.T) {
func TestService_Pin(t *testing.T) {
defer os.Remove(testDb)
b := Service{Interface: prep(t)}
b := DataStore{Interface: prepStoreEngine(t)}
res, err := b.Last("radio-t", 0)
t.Logf("%+v", res[0])
@@ -115,23 +120,23 @@ func TestService_Pin(t *testing.T) {
assert.Equal(t, 2, len(res))
assert.Equal(t, false, res[0].Pin)
err = b.SetPin(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, true)
err = b.SetPin(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, true)
assert.Nil(t, err)
c, err := b.Get(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
c, err := b.Get(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, true, c.Pin)
err = b.SetPin(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, false)
err = b.SetPin(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, false)
assert.Nil(t, err)
c, err = b.Get(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
c, err = b.Get(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, false, c.Pin)
}
func TestService_EditComment(t *testing.T) {
defer os.Remove(testDb)
b := Service{Interface: prep(t)}
b := DataStore{Interface: prepStoreEngine(t)}
res, err := b.Last("radio-t", 0)
t.Logf("%+v", res[0])
@@ -139,66 +144,53 @@ func TestService_EditComment(t *testing.T) {
assert.Equal(t, 2, len(res))
assert.Nil(t, res[0].Edit)
comment, err := b.EditComment(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
comment, err := b.EditComment(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"})
assert.Nil(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.Get(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
c, err := b.Get(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, "my edit", c.Edit.Summary)
assert.Equal(t, "xxx", c.Text)
_, err = b.EditComment(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
_, err = b.EditComment(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"})
assert.NotNil(t, err, "allow edit once")
}
func TestService_EditCommentDurationFailed(t *testing.T) {
defer os.Remove(testDb)
blt, err := NewBoltDB(bolt.Options{}, BoltSite{FileName: "/tmp/test-remark.db", SiteID: "radio-t"})
assert.Nil(t, err)
comment := Comment{
ID: "id-1",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: User{ID: "user1", Name: "user name"},
}
_, err = blt.Create(comment)
assert.Nil(t, err)
b := Service{Interface: blt, EditDuration: 100 * time.Millisecond}
b := DataStore{Interface: prepStoreEngine(t), EditDuration: 100 * time.Millisecond}
res, err := b.Last("radio-t", 0)
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 1, len(res))
assert.Equal(t, 2, len(res))
assert.Nil(t, res[0].Edit)
time.Sleep(time.Second)
_, err = b.EditComment(Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
_, err = b.EditComment(R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID,
EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"})
assert.NotNil(t, err)
}
func TestService_ValidateComment(t *testing.T) {
b := Service{MaxCommentSize: 2000}
b := DataStore{MaxCommentSize: 2000}
longText := fmt.Sprintf("%4000s", "X")
tbl := []struct {
inp Comment
inp R.Comment
err error
}{
{inp: Comment{}, err: errors.New("empty comment text")},
{inp: Comment{Orig: "something blah", User: User{ID: "myid", Name: "name"}}, err: nil},
{inp: Comment{Orig: "something blah", User: User{ID: "myid"}}, err: errors.New("empty user info")},
{inp: Comment{Orig: longText, User: User{ID: "myid", Name: "name"}}, err: errors.New("comment text exceeded max allowed size 2000 (4000)")},
{inp: R.Comment{}, err: errors.New("empty comment text")},
{inp: R.Comment{Orig: "something blah", User: R.User{ID: "myid", Name: "name"}}, err: nil},
{inp: R.Comment{Orig: "something blah", User: R.User{ID: "myid"}}, err: errors.New("empty user info")},
{inp: R.Comment{Orig: longText, User: R.User{ID: "myid", Name: "name"}}, err: errors.New("comment text exceeded max allowed size 2000 (4000)")},
}
for n, tt := range tbl {
@@ -213,29 +205,60 @@ func TestService_ValidateComment(t *testing.T) {
func TestService_Counts(t *testing.T) {
defer os.Remove(testDb)
b := prep(t) // two comments for https://radio-t.com
b := prepStoreEngine(t) // two comments for https://radio-t.com
// add one more for https://radio-t.com/2
comment := Comment{
comment := R.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: Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: User{ID: "user1", Name: "user name"},
Locator: R.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: R.User{ID: "user1", Name: "user name"},
}
_, err := b.Create(comment)
assert.Nil(t, err)
svc := Service{Interface: b}
svc := DataStore{Interface: b}
res, err := svc.Counts("radio-t", []string{"https://radio-t.com/2"})
assert.Nil(t, err)
assert.Equal(t, []PostInfo{{URL: "https://radio-t.com/2", Count: 1}}, res)
assert.Equal(t, []R.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.Nil(t, err)
assert.Equal(t, []PostInfo{
assert.Equal(t, []R.PostInfo{
{URL: "https://radio-t.com", Count: 2},
{URL: "https://radio-t.com/2", Count: 1},
{URL: "blah", Count: 0},
}, res)
}
// makes new boltdb, put two records
func prepStoreEngine(t *testing.T) engine.Interface {
os.Remove(testDb)
boltStore, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: "/tmp/test-remark.db", SiteID: "radio-t"})
assert.Nil(t, err)
b := boltStore
comment := R.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: R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: R.User{ID: "user1", Name: "user name"},
}
_, err = b.Create(comment)
assert.Nil(t, err)
comment = R.Comment{
ID: "id-2",
Text: "some text2",
Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
Locator: R.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: R.User{ID: "user1", Name: "user name"},
}
_, err = b.Create(comment)
assert.Nil(t, err)
return b
}
+50
View File
@@ -0,0 +1,50 @@
package store
import (
"crypto/hmac"
"crypto/sha1"
"fmt"
"hash/crc64"
"log"
"strconv"
)
// User holds user-related info
type User struct {
Name string `json:"name"`
ID string `json:"id"`
Picture string `json:"picture"`
Admin bool `json:"admin"`
Blocked bool `json:"block,omitempty"`
IP string `json:"ip,omitempty"`
}
// HashIP replace IP field with hashed hmac
func (u *User) HashIP(secret string) {
hashVal := func(val string) string {
if _, err := strconv.ParseUint(val, 16, 64); err == nil || val == "" {
return val // already hashed
}
key := []byte(secret)
h := hmac.New(sha1.New, key)
if _, err := h.Write([]byte(val)); err != nil {
log.Printf("[WARN] can't hash ip, %s", err)
}
return fmt.Sprintf("%x", h.Sum(nil))
}
u.IP = hashVal(u.IP)
}
// EncodeID hashes id to sha1. The function intentionally left outside of User struct because in some cases
// we need hashing for parts of id, in some others hashing for non-User values.
func EncodeID(id string) string {
h := sha1.New()
if _, err := h.Write([]byte(id)); err != nil {
// fail back to crc64
log.Printf("[WARN] can't hash id %s, %s", id, err)
return fmt.Sprintf("%x", crc64.Checksum([]byte(id), crc64.MakeTable(crc64.ECMA)))
}
return fmt.Sprintf("%x", h.Sum(nil))
}
+42
View File
@@ -0,0 +1,42 @@
package store
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUser_EncodeID(t *testing.T) {
tbl := []struct {
id string
hash string
}{
{"myid", "6e34471f84557e1713012d64a7477c71bfdac631"},
{"", "da39a3ee5e6b4b0d3255bfef95601890afd80709"},
{"blah blah", "135a1e01bae742c4a576b20fd41a683f6483ca43"},
}
for i, tt := range tbl {
assert.Equal(t, tt.hash, EncodeID(tt.id), "case #%d", i)
}
}
func TestUser_HashIP(t *testing.T) {
tbl := []struct {
ip string
hash1, hash2 string
}{
{"127.0.0.1", "ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741", "dbc7c999343f003f189f70aaf52cc04443f90790"},
{"8.8.8.8", "8cee77c27e32a2b5aec95c29888ac9946618d9a2", "70a46afce9633f010b06e129b8ad08243a1c4da9"},
}
for i, tt := range tbl {
u := User{IP: tt.ip}
u.HashIP("")
assert.Equal(t, tt.hash1, u.IP, "case #%d", i)
u = User{IP: tt.ip}
u.HashIP("123456")
assert.Equal(t, tt.hash2, u.IP, "case #%d", i)
}
}