From 49593a3327df428c09570ebfc04684abdcba27bb Mon Sep 17 00:00:00 2001 From: Umputun Date: Sat, 29 Jun 2019 21:17:34 -0500 Subject: [PATCH] wip: memory rpc example --- backend/_example/rpc/main.go | 22 + backend/_example/rpc/plugin/admin.go | 58 ++ backend/_example/rpc/plugin/admin_test.go | 52 ++ backend/_example/rpc/plugin/engine.go | 436 +++++++++++++ backend/_example/rpc/plugin/engine_test.go | 725 +++++++++++++++++++++ backend/_example/rpc/server/store.go | 230 +++++++ 6 files changed, 1523 insertions(+) create mode 100644 backend/_example/rpc/main.go create mode 100644 backend/_example/rpc/plugin/admin.go create mode 100644 backend/_example/rpc/plugin/admin_test.go create mode 100644 backend/_example/rpc/plugin/engine.go create mode 100644 backend/_example/rpc/plugin/engine_test.go create mode 100644 backend/_example/rpc/server/store.go diff --git a/backend/_example/rpc/main.go b/backend/_example/rpc/main.go new file mode 100644 index 00000000..de668734 --- /dev/null +++ b/backend/_example/rpc/main.go @@ -0,0 +1,22 @@ +/* + * Copyright 2019 Umputun. All rights reserved. + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file. + */ + +package main + +// Opts with all cli commands and flags +var opts struct { + API string `long:"api" env:"API" default:"/" description:"api root url"` + Port int `long:"port" env:"PORT" default:"8080" description:"rpc server port"` + AuthUser string `long:"auth-user" env:"AUTH_USER" default:"" description:"auth user name"` + AuthPasswd string `long:"auth-passwd" env:"AUTH_PASSWD" default:"" description:"auth password"` + Secret string `long:"secret" env:"SECRET" required:"true" description:"secret key"` + Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"` +} + + +func main() { + +} diff --git a/backend/_example/rpc/plugin/admin.go b/backend/_example/rpc/plugin/admin.go new file mode 100644 index 00000000..6f457199 --- /dev/null +++ b/backend/_example/rpc/plugin/admin.go @@ -0,0 +1,58 @@ +/* + * Copyright 2019 Umputun. All rights reserved. + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file. + */ + +package plugin + +import ( + log "github.com/go-pkgz/lgr" + "github.com/pkg/errors" +) + +// MemAdminStore implements admin.Store with mongo backend +type MemAdminStore struct { + data map[string]AdminRec + key string +} + +// AdminRec is a records per site with all admin info in +type AdminRec struct { + SiteID string `bson:"site"` + IDs []string `bson:"ids"` + Email string `bson:"email"` +} + +const mongoAdmin = "admin" + +// NewMemAdminStore makes admin Store in memory +func NewMemAdminStore(key string) *MemAdminStore { + log.Print("[DEBUG] make memory admin store") + return &MemAdminStore{data: map[string]AdminRec{}, key: key} +} + +// Key executes find by siteID and returns substructure with secret key +func (m *MemAdminStore) Key() (key string, err error) { + return m.key, nil +} + +// Admins executes find by siteID and returns admins ids +func (m *MemAdminStore) Admins(siteID string) (ids []string, err error) { + resp, ok := m.data[siteID] + if !ok { + return nil, errors.Errorf("site %s not found", siteID) + } + + return resp.IDs, nil +} + +// Email executes find by siteID and returns admin's email +func (m *MemAdminStore) Email(siteID string) (email string, err error) { + resp, ok := m.data[siteID] + if !ok { + return "", errors.Errorf("site %s not found", siteID) + } + + return resp.Email, nil +} diff --git a/backend/_example/rpc/plugin/admin_test.go b/backend/_example/rpc/plugin/admin_test.go new file mode 100644 index 00000000..1fcb46e8 --- /dev/null +++ b/backend/_example/rpc/plugin/admin_test.go @@ -0,0 +1,52 @@ +/* + * Copyright 2019 Umputun. All rights reserved. + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file. + */ + +package plugin + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/umputun/remark/backend/app/store/admin" +) + +func TestMongoStore_Get(t *testing.T) { + + adm := NewMemAdminStore("secret") + var ms admin.Store = adm + + adm.data = map[string]AdminRec{ + "site1": {"site1", []string{"i11", "i12"}, "e1"}, + "site2": {"site2", []string{"i21", "i22"}, "e2"}, + } + + admins, err := ms.Admins("site1") + assert.NoError(t, err) + assert.Equal(t, []string{"i11", "i12"}, admins) + email, err := ms.Email("site1") + assert.NoError(t, err) + assert.Equal(t, "e1", email) + key, err := ms.Key() + assert.NoError(t, err) + assert.Equal(t, "secret", key) + + admins, err = ms.Admins("site2") + assert.NoError(t, err) + assert.Equal(t, []string{"i21", "i22"}, admins) + email, err = ms.Email("site2") + assert.NoError(t, err) + assert.Equal(t, "e2", email) + key, err = ms.Key() + assert.NoError(t, err) + assert.Equal(t, "secret", key) + + admins, err = ms.Admins("no-site-in-db") + assert.EqualError(t, err, "site no-site-in-db not found") + + email, err = ms.Email("no-site-in-db") + assert.EqualError(t, err, "site no-site-in-db not found") +} diff --git a/backend/_example/rpc/plugin/engine.go b/backend/_example/rpc/plugin/engine.go new file mode 100644 index 00000000..ed892ded --- /dev/null +++ b/backend/_example/rpc/plugin/engine.go @@ -0,0 +1,436 @@ +/* + * Copyright 2019 Umputun. All rights reserved. + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file. + */ + +package plugin + +import ( + "log" + "sort" + "sync" + "time" + + "github.com/pkg/errors" + + "github.com/umputun/remark/backend/app/store/engine" + + "github.com/umputun/remark/backend/app/store" +) + +const lastLimit = 1000 + +// MemEngine implements in-memory engine interface +type MemEngine struct { + posts map[string][]store.Comment // key is siteID + metaUsers map[string]metaUser // key is userID + metaPosts map[store.Locator]metaPost // key is post's locator + sync.RWMutex +} + +type metaPost struct { + PostURL string + SiteID string + ReadOnly bool +} + +type metaUser struct { + UserID string + SiteID string + Verified bool + Blocked bool + BlockedUntil time.Time +} + +// NewMemEngine makes in-memory engine. +func NewMemEngine() *MemEngine { + + result := &MemEngine{ + posts: map[string][]store.Comment{}, + metaUsers: map[string]metaUser{}, + metaPosts: map[store.Locator]metaPost{}, + } + return result +} + +// Create new comment, write can be buffered and delayed. +func (m *MemEngine) Create(comment store.Comment) (commentID string, err error) { + + if ro, e := m.Flag(engine.FlagRequest{Flag: engine.ReadOnly, Locator: comment.Locator}); e == nil && ro { + return "", errors.Errorf("post %s is read-only", comment.Locator.URL) + } + + m.Lock() + defer m.Unlock() + comments := m.posts[comment.Locator.SiteID] + for _, c := range comments { + if c.ID == comment.ID { + return "", errors.New("dup key") + } + } + comments = append(comments, comment) + m.posts[comment.Locator.SiteID] = comments + return comment.ID, nil +} + +// Find returns all comments for post and sorts results +func (m *MemEngine) Find(req engine.FindRequest) (comments []store.Comment, err error) { + m.RLock() + defer m.RUnlock() + + comments = []store.Comment{} + + if req.Sort == "" { + req.Sort = "time" + } + + switch { + + case req.Locator.SiteID != "" && req.Locator.URL != "": // find comments for site and url + comments = m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool { + return c.Locator == req.Locator + }) + + case req.Locator.SiteID != "" && req.Locator.URL == "" && req.UserID == "": // find last comments for site + if req.Limit > lastLimit || req.Limit == 0 { + req.Limit = lastLimit + } + if req.Since.IsZero() { + req.Since = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + } + + comments = m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool { + return !c.Deleted && c.Timestamp.After(req.Since) + }) + comments = engine.SortComments(comments, "-time") + if len(comments) > req.Limit { + comments = comments[:req.Limit] + } + return comments, nil + + case req.Locator.SiteID != "" && req.UserID != "": // find comments for user + comments = m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool { + return c.User.ID == req.UserID + }) + } + comments = engine.SortComments(comments, req.Sort) + if req.Skip > 0 && req.Skip > len(comments) { + return []store.Comment{}, nil + } + if req.Skip > 0 && req.Skip < len(comments) { + comments = comments[req.Skip:] + } + + if req.Limit > 0 && req.Limit < len(comments) { + comments = comments[:req.Limit] + } + + return comments, err +} + +// Get returns comment for locator.URL and commentID string +func (m *MemEngine) Get(req engine.GetRequest) (comment store.Comment, err error) { + m.RLock() + defer m.RUnlock() + return m.get(req.Locator, req.CommentID) +} + +// Update updates comment for locator.URL with mutable part of comment +func (m *MemEngine) Update(comment store.Comment) error { + m.Lock() + defer m.Unlock() + return m.updateComment(comment) +} + +// Count returns number of comments for post or user +func (m *MemEngine) Count(req engine.FindRequest) (count int, err error) { + m.RLock() + defer m.RUnlock() + + switch { + case req.Locator.URL != "": // comment's count for post + comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool { + return c.Locator == req.Locator && !c.Deleted + }) + return len(comments), nil + case req.UserID != "": + comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool { + return c.User.ID == req.UserID && !c.Deleted + }) + return len(comments), nil + default: + return 0, errors.Errorf("invalid count request %+v", req) + } +} + +// Info get post(s) meta info +func (m *MemEngine) Info(req engine.InfoRequest) (res []store.PostInfo, err error) { + m.RLock() + defer m.RUnlock() + res = []store.PostInfo{} + + if req.Locator.URL != "" { // post info + comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool { + return c.Locator == req.Locator + }) + if len(comments) == 0 { + return nil, errors.New("not found") + } + info := store.PostInfo{ + URL: req.Locator.URL, + Count: len(comments), + ReadOnly: false, + FirstTS: comments[0].Timestamp.UTC(), + LastTS: comments[len(comments)-1].Timestamp.UTC(), + } + // set read-only from age and manual bucket + info.ReadOnly = req.ReadOnlyAge > 0 && !info.FirstTS.IsZero() && + info.FirstTS.AddDate(0, 0, req.ReadOnlyAge).Before(time.Now()) + if !info.ReadOnly { + v := m.checkFlag(engine.FlagRequest{Flag: engine.ReadOnly, Locator: req.Locator}) + info.ReadOnly = v + } + return []store.PostInfo{info}, nil + } + + if req.Locator.URL == "" && req.Locator.SiteID != "" { // site info (list) + if req.Limit <= 0 { + req.Limit = 1000 + } + if req.Skip < 0 { + req.Skip = 0 + } + + infoAll := map[store.Locator]store.PostInfo{} + for _, c := range m.posts[req.Locator.SiteID] { + var info store.PostInfo + var ok bool + if info, ok = infoAll[c.Locator]; !ok { + info = store.PostInfo{URL: c.Locator.URL, FirstTS: c.Timestamp.UTC()} + } + info.Count++ + info.LastTS = c.Timestamp.UTC() + infoAll[c.Locator] = info + } + + n := 0 + for _, v := range infoAll { + n++ + if len(res) >= req.Limit { + break + } + if req.Skip > 0 && n <= req.Skip { + continue + } + res = append(res, v) + } + sort.Slice(res, func(i, j int) bool { + return res[i].URL > res[j].URL + }) + return res, nil + } + + return nil, errors.Errorf("invalid info request %+v", req) +} + +// Flag sets and gets flag values +func (m *MemEngine) Flag(req engine.FlagRequest) (val bool, err error) { + m.Lock() + defer m.Unlock() + + if req.Update == engine.FlagNonSet { // read flag value, no update requested + return m.checkFlag(req), nil + } + // write flag value + return m.setFlag(req) +} + +// ListFlags get list of flagged keys, like blocked & verified user +// works for full locator (post flags) or with userID +func (m *MemEngine) ListFlags(req engine.FlagRequest) (res []interface{}, err error) { + m.RLock() + defer m.RUnlock() + + res = []interface{}{} + + switch req.Flag { + case engine.Verified: + for _, u := range m.metaUsers { + if u.SiteID == req.Locator.SiteID { + res = append(res, u.UserID) + } + } + return res, nil + + case engine.Blocked: + log.Printf("%+v", m.metaUsers) + for _, u := range m.metaUsers { + if u.SiteID == req.Locator.SiteID && u.Blocked && u.BlockedUntil.After(time.Now()) { + res = append(res, store.BlockedUser{ID: u.UserID, Until: u.BlockedUntil}) + } + } + return res, nil + } + + return nil, errors.Errorf("flag %s not listable", req.Flag) +} + +// Delete post(s) by id or by userID +func (m *MemEngine) Delete(req engine.DeleteRequest) error { + + m.Lock() + defer m.Unlock() + + switch { + case req.Locator.URL != "" && req.CommentID != "": // delete comment + return m.deleteComment(req.Locator, req.CommentID, req.DeleteMode) + + case req.Locator.SiteID != "" && req.UserID != "" && req.CommentID == "": // delete user + comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool { + return c.User.ID == req.UserID && !c.Deleted + }) + for _, c := range comments { + if e := m.deleteComment(c.Locator, c.ID, req.DeleteMode); e != nil { + return e + } + } + return nil + + case req.Locator.SiteID != "" && req.Locator.URL == "" && req.CommentID == "" && req.UserID == "": // delete site + if _, ok := m.posts[req.Locator.SiteID]; !ok { + return errors.New("not found") + } + m.posts[req.Locator.SiteID] = []store.Comment{} + return nil + } + + return errors.Errorf("invalid delete request %+v", req) +} + +func (m *MemEngine) deleteComment(loc store.Locator, id string, mode store.DeleteMode) error { + + comments := m.match(m.posts[loc.SiteID], func(c store.Comment) bool { + return c.Locator == loc && c.ID == id + }) + if len(comments) == 0 { + return errors.New("not found") + } + + comments[0].SetDeleted(mode) + return m.updateComment(comments[0]) +} + +// Close store +func (m *MemEngine) Close() error { + return nil +} + +func (m *MemEngine) checkFlag(req engine.FlagRequest) (val bool) { + switch req.Flag { + case engine.Blocked: + if meta, ok := m.metaUsers[req.UserID]; ok { + if meta.SiteID != req.Locator.SiteID { + return false + } + return meta.Blocked && meta.BlockedUntil.After(time.Now()) + } + case engine.Verified: + if meta, ok := m.metaUsers[req.UserID]; ok { + if meta.SiteID != req.Locator.SiteID { + return false + } + return meta.Verified + } + case engine.ReadOnly: + if meta, ok := m.metaPosts[req.Locator]; ok { + return meta.ReadOnly + } + } + return false +} + +func (m *MemEngine) setFlag(req engine.FlagRequest) (res bool, err error) { + + status := false + if req.Update == engine.FlagTrue { + status = true + } + + switch req.Flag { + + case engine.Blocked: + until := time.Time{} + if status { + until = time.Now().AddDate(100, 0, 0) // permanent is 100years + if req.TTL > 0 { + until = time.Now().Add(req.TTL) + } + } + meta := m.metaUsers[req.UserID] + meta = metaUser{ + UserID: req.UserID, + SiteID: req.Locator.SiteID, + Blocked: status, + BlockedUntil: until, + } + m.metaUsers[req.UserID] = meta + + case engine.Verified: + meta := m.metaUsers[req.UserID] + meta = metaUser{ + UserID: req.UserID, + SiteID: req.Locator.SiteID, + Verified: status, + } + m.metaUsers[req.UserID] = meta + + case engine.ReadOnly: + info, ok := m.metaPosts[req.Locator] + if !ok { + info.SiteID = req.Locator.SiteID + info.PostURL = req.Locator.URL + } + info.ReadOnly = status + m.metaPosts[req.Locator] = info + } + return status, errors.Wrapf(err, "failed to set flag %+v", req) +} + +func (m *MemEngine) get(loc store.Locator, commentID string) (store.Comment, error) { + comments := m.match(m.posts[loc.SiteID], func(c store.Comment) bool { + return c.Locator == loc && c.ID == commentID + }) + if len(comments) == 0 { + return store.Comment{}, errors.New("not found") + } + return comments[0], nil +} + +func (m *MemEngine) updateComment(comment store.Comment) error { + comments := m.posts[comment.Locator.SiteID] + for i, c := range comments { + if c.ID == comment.ID && c.Locator == comment.Locator { + c.Text = comment.Text + c.Orig = comment.Orig + c.Score = comment.Score + c.Votes = comment.Votes + c.Pin = comment.Pin + c.Deleted = comment.Deleted + c.User = comment.User + comments[i] = c + m.posts[comment.Locator.SiteID] = comments + return nil + } + } + return errors.New("not found") +} + +func (m *MemEngine) match(comments []store.Comment, fn func(c store.Comment) bool) (res []store.Comment) { + for _, c := range comments { + if fn(c) { + res = append(res, c) + } + } + return res +} diff --git a/backend/_example/rpc/plugin/engine_test.go b/backend/_example/rpc/plugin/engine_test.go new file mode 100644 index 00000000..6ff3a6ee --- /dev/null +++ b/backend/_example/rpc/plugin/engine_test.go @@ -0,0 +1,725 @@ +/* + * Copyright 2019 Umputun. All rights reserved. + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file. + */ + +package plugin + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/umputun/remark/backend/app/store" + "github.com/umputun/remark/backend/app/store/engine" +) + +func TestMem_CreateAndFind(t *testing.T) { + m := prepMem(t) // adds two comments + + req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"} + res, err := m.Find(req) + assert.NoError(t, err) + require.Equal(t, 2, len(res)) + assert.Equal(t, `some text, link`, res[0].Text) + assert.Equal(t, "user1", res[0].User.ID) + t.Log(res[0].ID) + + _, err = m.Create(store.Comment{ID: res[0].ID, Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}}) + require.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), "dup key"), err.Error()) + + id, err := m.Create(store.Comment{ID: "id-3", Locator: store.Locator{URL: "https://radio-t2.com", SiteID: "radio-t2"}}) + require.NoError(t, err) + assert.Equal(t, "id-3", id) + req = engine.FindRequest{Locator: store.Locator{URL: "https://radio-t2.com", SiteID: "radio-t2"}, Sort: "time"} + res, err = m.Find(req) + assert.NoError(t, err) + require.Equal(t, 1, len(res)) +} + +func TestMem_CreateFailedReadOnly(t *testing.T) { + b := prepMem(t) + comment := store.Comment{ + ID: "id-ro", + Text: `some text, link`, + Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local), + Locator: store.Locator{URL: "https://radio-t.com/ro", SiteID: "radio-t"}, + User: store.User{ID: "user1", Name: "user name"}, + } + + flagReq := engine.FlagRequest{Locator: comment.Locator, Flag: engine.ReadOnly, Update: engine.FlagTrue} + v, err := b.Flag(flagReq) + require.NoError(t, err) + assert.Equal(t, true, v) + + _, err = b.Create(comment) + assert.NotNil(t, err) + assert.Equal(t, "post https://radio-t.com/ro is read-only", err.Error()) + + flagReq = engine.FlagRequest{Locator: comment.Locator, Flag: engine.ReadOnly, Update: engine.FlagFalse} + v, err = b.Flag(flagReq) + require.NoError(t, err) + assert.Equal(t, false, v) + + _, err = b.Create(comment) + assert.NoError(t, err) +} + +func TestMem_Get(t *testing.T) { + b := prepMem(t) + req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"} + res, err := b.Find(req) + assert.NoError(t, err) + require.Equal(t, 2, len(res), "2 records initially") + + comment, err := b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[1].ID)) + assert.NoError(t, err) + assert.Equal(t, "some text2", comment.Text) + + _, err = b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "1234567")) + assert.EqualError(t, err, `not found`) + + _, err = b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "bad"}, res[1].ID)) + assert.EqualError(t, err, `not found`) +} + +func TestMem_Update(t *testing.T) { + b := prepMem(t) + req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"} + res, err := b.Find(req) + assert.NoError(t, err) + require.Equal(t, 2, len(res), "2 records initially") + + comment := res[0] + comment.Text = "abc 123" + comment.Score = 100 + err = b.Update(comment) + assert.NoError(t, err) + + comment, err = b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)) + assert.NoError(t, err) + assert.Equal(t, "abc 123", comment.Text) + assert.Equal(t, res[0].ID, comment.ID) + assert.Equal(t, 100, comment.Score) + + comment.Locator.SiteID = "bad" + err = b.Update(comment) + assert.EqualError(t, err, `not found`) + + comment.Locator.SiteID = "https://radio-t.com" + comment.Locator.URL = "https://radio-t.com-bad" + err = b.Update(comment) + assert.EqualError(t, err, `not found`) +} + +func TestMem_FindLast(t *testing.T) { + b := prepMem(t) + req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time"} + res, err := b.Find(req) + assert.NoError(t, err) + require.Equal(t, 2, len(res)) + assert.Equal(t, "some text2", res[0].Text) + + req.Limit = 1 + res, err = b.Find(req) + assert.NoError(t, err) + require.Equal(t, 1, len(res)) + assert.Equal(t, "some text2", res[0].Text) + + req.Locator.SiteID = "bad" + res, err = b.Find(req) + assert.NoError(t, err) + assert.Equal(t, 0, len(res)) +} + +func TestMem_FindLastSince(t *testing.T) { + b := prepMem(t) + ts := time.Date(2017, 12, 20, 15, 18, 21, 0, time.Local) + req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", Since: ts} + res, err := b.Find(req) + assert.NoError(t, err) + require.Equal(t, 2, len(res)) + assert.Equal(t, "some text2", res[0].Text) + + req.Since = time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local) + res, err = b.Find(req) + assert.NoError(t, err) + require.Equal(t, 1, len(res)) + assert.Equal(t, "some text2", res[0].Text) + + req.Since = time.Date(2017, 12, 20, 16, 18, 22, 0, time.Local) + res, err = b.Find(req) + assert.NoError(t, err) + assert.Equal(t, 0, len(res)) +} + +func TestMem_FindForUser(t *testing.T) { + b := prepMem(t) + req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 5} + res, err := b.Find(req) + assert.NoError(t, err) + require.Equal(t, 2, len(res)) + assert.Equal(t, "some text2", res[0].Text, "sorted by -time") + + req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 1} + res, err = b.Find(req) + assert.NoError(t, err) + require.Equal(t, 1, len(res), "allow 1 comment") + assert.Equal(t, "some text2", res[0].Text, "sorted by -time") + + req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 1, Skip: 1} + res, err = b.Find(req) + assert.NoError(t, err) + require.Equal(t, 1, len(res), "allow 1 comment") + assert.Equal(t, `some text, link`, res[0].Text, "second comment") + + req = engine.FindRequest{Locator: store.Locator{SiteID: "bad"}, Sort: "-time", UserID: "user1", Limit: 1, Skip: 1} + res, err = b.Find(req) + assert.NoError(t, err) + assert.Equal(t, 0, len(res), "no comments") + + req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "userZ", Limit: 1, Skip: 1} + res, err = b.Find(req) + assert.NoError(t, err) + assert.Equal(t, 0, len(res), "no comments") +} + +func TestMem_FindForUserPagination(t *testing.T) { + b := NewMemEngine() + + c := store.Comment{ + Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, + User: store.User{ID: "user1", Name: "user name"}, + } + + // write 200 comments + for i := 0; i < 200; i++ { + c.ID = fmt.Sprintf("idd-%d", i) + c.Text = fmt.Sprintf("text #%d", i) + c.Timestamp = time.Date(2017, 12, 20, 15, 18, i, 0, time.Local) + _, err := b.Create(c) + require.Nil(t, err) + } + + // get all comments + req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1"} + res, err := b.Find(req) + assert.NoError(t, err) + require.Equal(t, 200, len(res)) + assert.Equal(t, "idd-199", res[0].ID) + + // seek 0, 5 comments + req.Limit = 5 + res, err = b.Find(req) + assert.NoError(t, err) + require.Equal(t, 5, len(res)) + assert.Equal(t, "idd-199", res[0].ID) + assert.Equal(t, "idd-195", res[4].ID) + + // seek 10, 3 comments + req.Skip, req.Limit = 10, 3 + res, err = b.Find(req) + assert.NoError(t, err) + require.Equal(t, 3, len(res)) + assert.Equal(t, "idd-189", res[0].ID) + assert.Equal(t, "idd-187", res[2].ID) + + // seek 195, ask 10 comments + req.Skip, req.Limit = 195, 10 + res, err = b.Find(req) + assert.NoError(t, err) + require.Equal(t, 5, len(res)) + assert.Equal(t, "idd-4", res[0].ID) + assert.Equal(t, "idd-0", res[4].ID) + + // seek 255, ask 10 comments + req.Skip, req.Limit = 255, 10 + res, err = b.Find(req) + assert.NoError(t, err) + assert.Equal(t, 0, len(res)) +} + +func TestMem_CountPost(t *testing.T) { + b := prepMem(t) + req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}} + c, err := b.Count(req) + assert.NoError(t, err) + require.Equal(t, 2, c) + + req = engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com-xxx", SiteID: "radio-t"}} + c, err = b.Count(req) + assert.NoError(t, err) + assert.Equal(t, 0, c) + + req = engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "bad"}} + c, err = b.Count(req) + assert.NoError(t, err) + assert.Equal(t, 0, c) +} + +func TestMem_CountUser(t *testing.T) { + b := prepMem(t) + req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"} + c, err := b.Count(req) + assert.NoError(t, err) + require.Equal(t, 2, c) + + req = engine.FindRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "user1"} + c, err = b.Count(req) + assert.NoError(t, err) + assert.Equal(t, 0, c) + + req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "userZ"} + c, err = b.Count(req) + assert.NoError(t, err) + assert.Equal(t, 0, c) +} + +func TestMem_InfoPost(t *testing.T) { + b := prepMem(t) + ts := func(min int) time.Time { return time.Date(2017, 12, 20, 15, 18, min, 0, time.Local).In(time.UTC) } + + // add one more for https://radio-t.com/2 + comment := store.Comment{ + ID: "12345", + Text: `some text, link`, + Timestamp: time.Date(2017, 12, 20, 15, 18, 24, 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) + + req := engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 0} + r, err := b.Info(req) + require.NoError(t, err) + assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24)}}, r) + + req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 10} + r, err = b.Info(req) + require.NoError(t, err) + assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, + FirstTS: ts(24), LastTS: ts(24), ReadOnly: true}}, r) + + req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, ReadOnlyAge: 0} + r, err = b.Info(req) + require.NoError(t, err) + assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, r) + + req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/error", SiteID: "radio-t"}, ReadOnlyAge: 0} + _, err = b.Info(req) + require.NotNil(t, err) + + req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t-error"}, ReadOnlyAge: 0} + _, err = b.Info(req) + require.NotNil(t, err) + + fr := engine.FlagRequest{Flag: engine.ReadOnly, + Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, Update: engine.FlagTrue} + _, err = b.Flag(fr) + require.NoError(t, err) + req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 0} + r, err = b.Info(req) + require.NoError(t, err) + assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24), + ReadOnly: true}}, r) +} + +func TestMem_InfoList(t *testing.T) { + b := prepMem(t) + // add one more for https://radio-t.com/2 + comment := store.Comment{ + ID: "12345", + Text: `some text, link`, + 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) + + ts := func(sec int) time.Time { return time.Date(2017, 12, 20, 15, 18, sec, 0, time.Local).In(time.UTC) } + + req := engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}} + res, err := b.Info(req) + assert.NoError(t, err) + assert.EqualValues(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)}, + {URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res) + + req = engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: -1, Skip: -1} + res, err = b.Info(req) + assert.NoError(t, err) + assert.EqualValues(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)}, + {URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res) + + req = engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 1} + res, err = b.Info(req) + assert.NoError(t, err) + assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)}}, res) + + req = engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 1, Skip: 1} + res, err = b.Info(req) + assert.NoError(t, err) + assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res) + + req = engine.InfoRequest{Locator: store.Locator{SiteID: "bad"}, Limit: 1, Skip: 1} + res, err = b.Info(req) + assert.NoError(t, err) + assert.Equal(t, []store.PostInfo{}, res) +} + +func TestMem_FlagBlockedUser(t *testing.T) { + + b := prepMem(t) + req := engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"} + val, err := b.Flag(req) + assert.NoError(t, err) + assert.False(t, val, "nothing blocked yet") + + req = engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", + Update: engine.FlagTrue} + _, err = b.Flag(req) + assert.NoError(t, err) + val, err = b.Flag(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"}) + assert.NoError(t, err) + assert.True(t, val, "user1 blocked") + + req = engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", + Update: engine.FlagTrue} + _, err = b.Flag(req) + assert.NoError(t, err) + val, err = b.Flag(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"}) + assert.NoError(t, err) + assert.True(t, val, "user1 still blocked") + + req = engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", + Update: engine.FlagFalse} + _, err = b.Flag(req) + assert.NoError(t, err) + val, err = b.Flag(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"}) + assert.NoError(t, err) + assert.False(t, val, "user1 unblocked") +} + +func TestMem_FlagReadOnlyPost(t *testing.T) { + + b := prepMem(t) + req := engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly} + val, err := b.Flag(req) + assert.NoError(t, err) + assert.False(t, val, "nothing ro") + + req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly, + Update: engine.FlagTrue} + val, err = b.Flag(req) + assert.NoError(t, err) + req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly} + val, err = b.Flag(req) + assert.NoError(t, err) + assert.True(t, val, "url-1 ro") + + req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-2"}, Flag: engine.ReadOnly} + val, err = b.Flag(req) + assert.NoError(t, err) + assert.False(t, val, "url-2 still writable") + + req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly, + Update: engine.FlagFalse} + _, err = b.Flag(req) + assert.NoError(t, err) + req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly} + val, err = b.Flag(req) + assert.NoError(t, err) + assert.False(t, val, "url-1 writable") +} + +func TestMem_FlagVerified(t *testing.T) { + + b := prepMem(t) + isVerified := func(site, user string) bool { + req := engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: site}, UserID: user} + v, err := b.Flag(req) + require.NoError(t, err) + return v + } + + setVerified := func(site, user string, status engine.FlagStatus) error { + req := engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: site}, UserID: user, Update: status} + _, err := b.Flag(req) + return err + } + + assert.False(t, isVerified("radio-t", "u1"), "nothing verified") + + assert.NoError(t, setVerified("radio-t", "u1", engine.FlagTrue)) + assert.True(t, isVerified("radio-t", "u1"), "u1 verified") + + assert.False(t, isVerified("radio-t", "u2"), "u2 still not verified") + assert.NoError(t, setVerified("radio-t", "u1", engine.FlagFalse)) + assert.False(t, isVerified("radio-t", "u1"), "u1 not verified anymore") + + assert.NoError(t, setVerified("bad", "u1", engine.FlagTrue)) + assert.NoError(t, setVerified("radio-t", "u1xyz", engine.FlagFalse)) + + assert.False(t, isVerified("radio-t-bad", "u1"), "nothing verified on wrong site") + + assert.NoError(t, setVerified("radio-t", "u1", engine.FlagTrue)) + assert.NoError(t, setVerified("radio-t", "u2", engine.FlagTrue)) + assert.NoError(t, setVerified("radio-t", "u3", engine.FlagFalse)) +} + +func TestMem_FlagListVerified(t *testing.T) { + + b := prepMem(t) + toIDs := func(inp []interface{}) (res []string) { + res = make([]string, len(inp)) + for i, v := range inp { + vv, ok := v.(string) + require.True(t, ok) + res[i] = vv + } + return res + } + + setVerified := func(site, user string, status engine.FlagStatus) error { + req := engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: site}, UserID: user, Update: status} + _, err := b.Flag(req) + return err + } + + ids, err := b.ListFlags(engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: "radio-t"}}) + assert.NoError(t, err) + assert.Equal(t, []string{}, toIDs(ids), "verified list empty") + + assert.NoError(t, setVerified("radio-t", "u1", engine.FlagTrue)) + assert.NoError(t, setVerified("radio-t", "u2", engine.FlagTrue)) + ids, err = b.ListFlags(engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: "radio-t"}}) + assert.NoError(t, err) + assert.EqualValues(t, []string{"u1", "u2"}, toIDs(ids), "verified 2 ids") + + ids, err = b.ListFlags(engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: "radio-t-bad"}}) + assert.NoError(t, err) + assert.Equal(t, 0, len(ids)) +} + +func TestMem_FlagListBlocked(t *testing.T) { + + b := prepMem(t) + setBlocked := func(site, user string, status engine.FlagStatus, ttl time.Duration) error { + req := engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: site}, UserID: user, Update: status, + TTL: ttl} + _, err := b.Flag(req) + return err + } + + toBlocked := func(inp []interface{}) (res []store.BlockedUser) { + res = make([]store.BlockedUser, len(inp)) + for i, v := range inp { + vv, ok := v.(store.BlockedUser) + require.True(t, ok) + res[i] = vv + } + return res + } + assert.NoError(t, setBlocked("radio-t", "user1", engine.FlagTrue, 0)) + assert.NoError(t, setBlocked("radio-t", "user2", engine.FlagTrue, 50*time.Millisecond)) + assert.NoError(t, setBlocked("radio-t", "user3", engine.FlagFalse, 0)) + + vv, err := b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}}) + assert.NoError(t, err) + + blockedList := toBlocked(vv) + require.Equal(t, 2, len(blockedList), b.metaUsers) + assert.Equal(t, "user1", blockedList[0].ID) + assert.Equal(t, "user2", blockedList[1].ID) + t.Logf("%+v", blockedList) + + // check block expiration + time.Sleep(50 * time.Millisecond) + vv, err = b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}}) + assert.NoError(t, err) + blockedList = toBlocked(vv) + require.Equal(t, 1, len(blockedList)) + assert.Equal(t, "user1", blockedList[0].ID) + + vv, err = b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "bad"}}) + assert.NoError(t, err) + assert.Equal(t, 0, len(vv)) +} + +func TestMem_DeleteComment(t *testing.T) { + + b := prepMem(t) + reqReq := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"} + res, err := b.Find(reqReq) + assert.NoError(t, err) + require.Equal(t, 2, len(res), "initially 2 comments") + + count, err := b.Count(reqReq) + require.NoError(t, err) + require.Equal(t, 2, count, "count=2 initially") + + delReq := engine.DeleteRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, + CommentID: res[0].ID, DeleteMode: store.SoftDelete} + + err = b.Delete(delReq) + assert.NoError(t, err) + + res, err = b.Find(reqReq) + assert.NoError(t, err) + require.Equal(t, 2, len(res)) + assert.Equal(t, "", res[0].Text) + assert.True(t, res[0].Deleted, "marked deleted") + assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User) + + assert.Equal(t, "some text2", res[1].Text) + assert.False(t, res[1].Deleted) + + comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 10}) + assert.NoError(t, err) + assert.Equal(t, 1, len(comments), "1 in last, 1 removed") + + count, err = b.Count(reqReq) + require.NoError(t, err) + assert.Equal(t, 1, count) + + delReq.CommentID = "123456" + err = b.Delete(delReq) + assert.NotNil(t, err) + + delReq.Locator.SiteID = "bad" + delReq.CommentID = res[0].ID + err = b.Delete(delReq) + assert.EqualError(t, err, `not found`) + + delReq.Locator = store.Locator{URL: "https://radio-t.com/bad", SiteID: "radio-t"} + err = b.Delete(delReq) + assert.EqualError(t, err, `not found`) +} + +func TestMem_DeleteHard(t *testing.T) { + + b := prepMem(t) + reqReq := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"} + res, err := b.Find(reqReq) + assert.NoError(t, err) + assert.Equal(t, 2, len(res), "initially 2 comments") + + delReq := engine.DeleteRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, + CommentID: res[0].ID, DeleteMode: store.HardDelete} + err = b.Delete(delReq) + assert.NoError(t, err) + + res, err = b.Find(reqReq) + assert.NoError(t, err) + require.Equal(t, 2, len(res)) + assert.Equal(t, "", res[0].Text) + assert.True(t, res[0].Deleted, "marked deleted") + assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User) +} + +func TestMem_DeleteAll(t *testing.T) { + b := prepMem(t) + delReq := engine.DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}} + err := b.Delete(delReq) + assert.NoError(t, err) + + comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 10}) + assert.NoError(t, err) + assert.Equal(t, 0, len(comments), "nothing left") +} + +func TestBoltAdmin_DeleteUserHard(t *testing.T) { + b := prepMem(t) + err := b.Delete(engine.DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", + DeleteMode: store.HardDelete}) + require.NoError(t, err) + + comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}, + Sort: "time"}) + assert.NoError(t, err) + require.Equal(t, 2, len(comments), "2 comments with deleted info") + assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[0].User) + assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[1].User) + + c, err := b.Count(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}}) + assert.NoError(t, err) + assert.Equal(t, 0, c, "0 count") + + _, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Limit: 5}) + assert.NoError(t, err, "no comments for user user1 in store") + + comments, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "time"}) + assert.NoError(t, err) + assert.Equal(t, 0, len(comments), "nothing left") +} + +func TestBoltAdmin_DeleteUserSoft(t *testing.T) { + + b := prepMem(t) + err := b.Delete(engine.DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", + DeleteMode: store.SoftDelete}) + require.NoError(t, err) + + comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}, + Sort: "time"}) + assert.NoError(t, err) + require.Equal(t, 2, len(comments), "2 comments with deleted info") + assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[0].User) + assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[1].User) + + c, err := b.Count(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}}) + assert.NoError(t, err) + assert.Equal(t, 0, c, "0 count") + + comments, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Limit: 5}) + assert.NoError(t, err, "no comments for user user1 in store") + require.Equal(t, 2, len(comments), "2 comments with deleted info") + assert.True(t, comments[0].Deleted) + assert.True(t, comments[1].Deleted) + assert.Equal(t, "", comments[0].Text) + assert.Equal(t, "", comments[1].Text) + + comments, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "time"}) + assert.NoError(t, err) + assert.Equal(t, 0, len(comments), "nothing left") +} + +func prepMem(t *testing.T) *MemEngine { + + m := NewMemEngine() + + comment := store.Comment{ + ID: "id-1", + Text: `some text, link`, + 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 := m.Create(comment) + require.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 = m.Create(comment) + require.NoError(t, err) + return m +} + +func getReq(locator store.Locator, commentID string) engine.GetRequest { + return engine.GetRequest{ + Locator: locator, + CommentID: commentID, + } +} diff --git a/backend/_example/rpc/server/store.go b/backend/_example/rpc/server/store.go new file mode 100644 index 00000000..30f029a6 --- /dev/null +++ b/backend/_example/rpc/server/store.go @@ -0,0 +1,230 @@ +package server + +import ( + "encoding/json" + + "github.com/umputun/remark/backend/app/rpc" + "github.com/umputun/remark/backend/app/store" + "github.com/umputun/remark/backend/app/store/admin" + "github.com/umputun/remark/backend/app/store/engine" +) + +// Store handler wraps both engine and remote server and implements all handlers +// Note: this file can be used as-is in any custom rpc plugin +type Store struct { + *rpc.Server + eng engine.Interface + adm admin.Store +} + +// NewStore makes Store instance and register handlers +func NewStore(e engine.Interface, a admin.Store, r *rpc.Server) *Store { + res := &Store{eng: e, adm: a, Server: r} + res.addHandlers() + return res +} + + +func (s *Store) addHandlers() { + // data store handlers + s.Group("store", rpc.HandlersGroup{ + "create": s.createHndl, + "find": s.findHndl, + "get": s.getHndl, + "update": s.updateHndl, + "count": s.countHndl, + "info": s.infoHndl, + "flag": s.flagHndl, + "list_flags": s.listFlagsHndl, + "delete": s.deleteHndl, + "close": s.closeHndl, + }) + + // admin store handlers + s.Group("admin", rpc.HandlersGroup{ + "key": s.admKeyHndl, + "admins": s.admAdminsHndl, + "email": s.admEmailHndl, + }) +} + +func (s *Store) createHndl(id uint64, params json.RawMessage) (rr rpc.Response) { + comment := store.Comment{} + if err := json.Unmarshal(params, &comment); err != nil { + return rpc.Response{Error: err.Error()} + } + commentID, err := s.eng.Create(comment) + if rr, err = s.EncodeResponse(id, commentID, err); err != nil { + return rpc.Response{Error: err.Error()} + + } + return rr +} + +// Find comments +func (s *Store) findHndl(id uint64, params json.RawMessage) (rr rpc.Response) { + req := engine.FindRequest{} + if err := json.Unmarshal(params, &req); err != nil { + return rpc.Response{Error: err.Error()} + } + resp, err := s.eng.Find(req) + if rr, err = s.EncodeResponse(id, resp, err); err != nil { + return rpc.Response{Error: err.Error()} + } + return rr +} + +// Get comment +func (s *Store) getHndl(id uint64, params json.RawMessage) (rr rpc.Response) { + req := engine.GetRequest{} + if err := json.Unmarshal(params, &req); err != nil { + return rpc.Response{Error: err.Error()} + } + comment, err := s.eng.Get(req) + if rr, err = s.EncodeResponse(id, comment, err); err != nil { + return rpc.Response{Error: err.Error()} + } + return rr +} + +// Update comment +func (s *Store) updateHndl(id uint64, params json.RawMessage) (rr rpc.Response) { + comment := store.Comment{} + if err := json.Unmarshal(params, &comment); err != nil { + return rpc.Response{Error: err.Error()} + } + err := s.eng.Update(comment) + if rr, err = s.EncodeResponse(id, nil, err); err != nil { + return rpc.Response{Error: err.Error()} + } + return rr +} + +// counts for site and users +func (s *Store) countHndl(id uint64, params json.RawMessage) (rr rpc.Response) { + req := engine.FindRequest{} + if err := json.Unmarshal(params, &req); err != nil { + return rpc.Response{Error: err.Error()} + } + count, err := s.eng.Count(req) + if rr, err = s.EncodeResponse(id, count, err); err != nil { + return rpc.Response{Error: err.Error()} + } + return rr +} + +// info get post meta info +func (s *Store) infoHndl(id uint64, params json.RawMessage) (rr rpc.Response) { + req := engine.InfoRequest{} + if err := json.Unmarshal(params, &req); err != nil { + return rpc.Response{Error: err.Error()} + } + info, err := s.eng.Info(req) + if rr, err = s.EncodeResponse(id, info, err); err != nil { + return rpc.Response{Error: err.Error()} + } + return rr +} + +// flagHndl get and sets flag value +func (s *Store) flagHndl(id uint64, params json.RawMessage) (rr rpc.Response) { + req := engine.FlagRequest{} + if err := json.Unmarshal(params, &req); err != nil { + return rpc.Response{Error: err.Error()} + } + status, err := s.eng.Flag(req) + if rr, err = s.EncodeResponse(id, status, err); err != nil { + return rpc.Response{Error: err.Error()} + } + return rr +} + +// listFlagsHndl list flags for given request +func (s *Store) listFlagsHndl(id uint64, params json.RawMessage) (rr rpc.Response) { + req := engine.FlagRequest{} + if err := json.Unmarshal(params, &req); err != nil { + return rpc.Response{Error: err.Error()} + } + flags, err := s.eng.ListFlags(req) + if rr, err = s.EncodeResponse(id, flags, err); err != nil { + return rpc.Response{Error: err.Error()} + } + return rr +} + +// deleteHndl remove comment(s) +func (s *Store) deleteHndl(id uint64, params json.RawMessage) (rr rpc.Response) { + req := engine.DeleteRequest{} + if err := json.Unmarshal(params, &req); err != nil { + return rpc.Response{Error: err.Error()} + } + err := s.eng.Delete(req) + if rr, err = s.EncodeResponse(id, nil, err); err != nil { + return rpc.Response{Error: err.Error()} + } + return rr +} + +// close store +func (s *Store) closeHndl(id uint64, _ json.RawMessage) (rr rpc.Response) { + if err := s.eng.Close(); err != nil { + return rpc.Response{Error: err.Error()} + } + return rpc.Response{} +} + +// get admin key +func (s *Store) admKeyHndl(id uint64, _ json.RawMessage) (rr rpc.Response) { + key, err := s.adm.Key() + if err != nil { + return rpc.Response{Error: err.Error()} + } + if rr, err = s.EncodeResponse(id, key, err); err != nil { + return rpc.Response{Error: err.Error()} + } + return rr +} + +// get admins list +func (s *Store) admAdminsHndl(id uint64, params json.RawMessage) (rr rpc.Response) { + args := []interface{}{} + if err := json.Unmarshal(params, &args); err != nil { + return rpc.Response{Error: err.Error()} + } + siteID, ok := args[0].(string) + if !ok { + return rpc.Response{Error: "incompatible argument"} + } + + admins, err := s.adm.Admins(siteID) + if err != nil { + return rpc.Response{Error: err.Error()} + } + + if rr, err = s.EncodeResponse(id, admins, err); err != nil { + return rpc.Response{Error: err.Error()} + } + return rr +} + +// get admin email +func (s *Store) admEmailHndl(id uint64, params json.RawMessage) (rr rpc.Response) { + args := []interface{}{} + if err := json.Unmarshal(params, &args); err != nil { + return rpc.Response{Error: err.Error()} + } + siteID, ok := args[0].(string) + if !ok { + return rpc.Response{Error: "incompatible argument"} + } + + email, err := s.adm.Email(siteID) + if err != nil { + return rpc.Response{Error: err.Error()} + } + + if rr, err = s.EncodeResponse(id, email, err); err != nil { + return rpc.Response{Error: err.Error()} + } + return rr +}