switch example to go-pkgz/jrpc

This commit is contained in:
Umputun
2019-07-31 18:34:32 -05:00
parent fcf175c905
commit e84a155ef5
13 changed files with 858 additions and 304 deletions
+4
View File
@@ -0,0 +1,4 @@
# sample store implementation
`memory_store` illustrates how to make a storage plugin for remark42.
@@ -0,0 +1,61 @@
/*
* 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 accessor
import (
log "github.com/go-pkgz/lgr"
"github.com/pkg/errors"
)
// MemAdmin implements admin.Store with memory backend
type MemAdmin struct {
data map[string]AdminRec // admin info per site
key string
}
// AdminRec is a records per site with all admin info in
type AdminRec struct {
SiteID string `bson:"site"`
IDs []string `bson:"ids"` // admin ids
Email string `bson:"email"` // admin email
}
// NewMemAdminStore makes admin Store in memory
func NewMemAdminStore(key string) *MemAdmin {
log.Print("[DEBUG] make memory admin store")
return &MemAdmin{data: map[string]AdminRec{}, key: key}
}
// Key executes find by siteID and returns substructure with secret key
func (m *MemAdmin) Key() (key string, err error) {
return m.key, nil
}
// Admins executes find by siteID and returns admins ids
func (m *MemAdmin) 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 *MemAdmin) 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
}
// Set admin data for siteID
func (m *MemAdmin) Set(siteID string, arec AdminRec) {
m.data[siteID] = arec
}
@@ -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 accessor
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/backend/app/store/admin"
)
func TestMemAdmin_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")
}
@@ -0,0 +1,441 @@
/*
* 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 accessor
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
// MemData implements in-memory data store
type MemData 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
}
// NewMemData makes in-memory engine.
func NewMemData() *MemData {
result := &MemData{
posts: map[string][]store.Comment{},
metaUsers: map[string]metaUser{},
metaPosts: map[store.Locator]metaPost{},
}
return result
}
// Create new comment
func (m *MemData) 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 { // don't allow duplicated IDs
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 *MemData) 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 && c.Timestamp.After(req.Since)
})
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 *MemData) 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 *MemData) 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 *MemData) 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 *MemData) 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
}
for _, v := range infoAll {
res = append(res, v)
}
sort.Slice(res, func(i, j int) bool {
return res[i].URL > res[j].URL
})
if req.Skip > 0 {
if req.Skip >= len(res) {
return []store.PostInfo{}, nil
}
res = res[req.Skip:]
}
if req.Limit > 0 && req.Limit < len(res) {
res = res[:req.Limit]
}
return res, nil
}
return nil, errors.Errorf("invalid info request %+v", req)
}
// Flag sets and gets flag values
func (m *MemData) 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 *MemData) 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 *MemData) 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 *MemData) 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 *MemData) Close() error {
return nil
}
func (m *MemData) 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 *MemData) 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 *MemData) 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 *MemData) 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 *MemData) match(comments []store.Comment, fn func(c store.Comment) bool) (res []store.Comment) {
res = []store.Comment{}
for _, c := range comments {
if fn(c) {
res = append(res, c)
}
}
return res
}
@@ -0,0 +1,727 @@
/*
* 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 accessor
import (
"fmt"
"sort"
"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, <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 = 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, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/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, <a href="http://radio-t.com">link</a>`, 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 := NewMemData()
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, <a href="http://radio-t.com">link</a>`,
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, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
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
}
sort.Strings(res)
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) *MemData {
m := NewMemData()
comment := store.Comment{
ID: "id-1",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := 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,
}
}
+16
View File
@@ -0,0 +1,16 @@
module github.com/umputun/remark/backend/_example/memory_store
go 1.12
require (
github.com/go-pkgz/jrpc v0.1.0
github.com/go-pkgz/lgr v0.6.3
github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.3.0
github.com/umputun/remark/backend v1.4.0
)
replace github.com/umputun/remark/backend => ../../
replace gopkg.in/russross/blackfriday.v2 => github.com/russross/blackfriday/v2 v2.0.1
+192
View File
@@ -0,0 +1,192 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.40.0/go.mod h1:Tk58MuI9rbLMKlAjeO/bDnteAx7tX2gJIXw4T5Jwlro=
cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/didip/tollbooth v4.0.0+incompatible h1:ayQZYuF5QOxx3NdYRNuRVFLv9/2b64JtSUlewb+0TMo=
github.com/didip/tollbooth v4.0.0+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M=
github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d h1:vs5Nf6IE0N/PwGJ8//zRed4gpCdcr99K2HzX7RuLOQ8=
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d/go.mod h1:YWyIfq3y4ArRfWZ9XksmuusP+7Mad+T0iFZ0kv0XG/M=
github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-pkgz/auth v0.7.2 h1:+LvAgqwQtYuWphpZE8qLtspVd65+VgreJkFLsKrtmmk=
github.com/go-pkgz/auth v0.7.2/go.mod h1:ibOpZYISiaOvAHe2bsKj2s3v4AkMam2WxxIFn+zhulo=
github.com/go-pkgz/jrpc v0.1.0 h1:hNg/IyfEqJcSWOKkuHw0ZwcuGc9TDp7QZREsD2ycmiM=
github.com/go-pkgz/jrpc v0.1.0/go.mod h1:JxZsvoBklA50DNhELVJnJ567Rt+KrMH9rR3u515wvE8=
github.com/go-pkgz/lcw v0.3.1/go.mod h1:k+PY1CkCMTLXILtFoJOyK65Qqi9rkoTYunFH1vE/C0I=
github.com/go-pkgz/lgr v0.2.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
github.com/go-pkgz/lgr v0.6.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
github.com/go-pkgz/lgr v0.6.3 h1:n9pGk2paBV8w/Y/FVEq5MkwDmP33dnUPKbY4CyyygwM=
github.com/go-pkgz/lgr v0.6.3/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
github.com/go-pkgz/mongo v1.1.2/go.mod h1:0NkWnzpiUxoL5fYZuttCtJrpC67oNDidfYxcdPqHTf0=
github.com/go-pkgz/repeater v1.1.2/go.mod h1:QfNR/a+xqjs+f9wSxWqOQlw9aQhmKlUaSwXCiZ+Ko2w=
github.com/go-pkgz/rest v1.4.1 h1:DmaVLPH2O7yLehrWOW0uz01d2mVHz9fBR/iuTiPRzaw=
github.com/go-pkgz/rest v1.4.1/go.mod h1:COazNj35u3RXAgQNBr6neR599tYP3URiOpsu9p0rOtk=
github.com/go-pkgz/syncs v1.1.1/go.mod h1:bt9lxWRRJ9vOCMGc8Big8ttjYHLKP88ofj1y38UlaHE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc h1:0L2sGkaj6MWuV1BfXsrLJ/+XA8RzKKVsYlLVXNkK1Lw=
github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kyokomi/emoji v2.1.0+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+72
View File
@@ -0,0 +1,72 @@
/*
* 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
import (
"fmt"
"os"
log "github.com/go-pkgz/lgr"
"github.com/jessevdk/go-flags"
"github.com/umputun/remark/backend/app/rpc"
"github.com/umputun/remark/backend/_example/memory_store/accessor"
"github.com/umputun/remark/backend/_example/memory_store/server"
)
// 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:"rpc auth user name"`
AuthPasswd string `long:"auth-passwd" env:"AUTH_PASSWD" default:"" description:"rpc auth password"`
Secret string `long:"secret" env:"SECRET" required:"true" description:"secret key"`
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
}
var revision = "unknown"
func main() {
fmt.Printf("remark42-memory module %s\n", revision)
if _, err := flags.Parse(&opts); err != nil {
os.Exit(2)
}
setupLog(opts.Dbg)
dataStore := accessor.NewMemData()
adminStore := accessor.NewMemAdminStore(opts.Secret)
rpcServer := rpc.Server{
API: opts.API,
AuthUser: opts.AuthUser,
AuthPasswd: opts.AuthPasswd,
Version: revision,
AppName: "remark42-memory",
}
srv := server.NewRPC(dataStore, adminStore, &rpcServer)
admRec := accessor.AdminRec{
SiteID: "example",
IDs: []string{"id1", "id2"},
Email: "admin@example.com",
}
adminStore.Set("example", admRec)
err := srv.Run(opts.Port)
log.Printf("[ERROR] server failed or terminated, %+v", err)
}
func setupLog(dbg bool) {
if dbg {
log.Setup(log.Debug, log.CallerFile, log.CallerFunc, log.Msec, log.LevelBraces)
return
}
log.Setup(log.Msec, log.LevelBraces)
}
+191
View File
@@ -0,0 +1,191 @@
package server
import (
"encoding/json"
"github.com/go-pkgz/jrpc"
"github.com/umputun/remark/backend/app/store"
"github.com/umputun/remark/backend/app/store/admin"
"github.com/umputun/remark/backend/app/store/engine"
)
// RPC handler wraps both engine and remote server and implements all handlers
// Note: this file can be used as-is in any custom jrpc plugin
type RPC struct {
*jrpc.Server
eng engine.Interface
adm admin.Store
}
// NewRPC makes RPC instance and register handlers
func NewRPC(e engine.Interface, a admin.Store, r *jrpc.Server) *RPC {
res := &RPC{eng: e, adm: a, Server: r}
res.addHandlers()
return res
}
func (s *RPC) addHandlers() {
// data store handlers
s.Group("store", jrpc.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", jrpc.HandlersGroup{
"key": s.admKeyHndl,
"admins": s.admAdminsHndl,
"email": s.admEmailHndl,
})
}
func (s *RPC) createHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
comment := store.Comment{}
if err := json.Unmarshal(params, &comment); err != nil {
return jrpc.Response{Error: err.Error()}
}
commentID, err := s.eng.Create(comment)
return jrpc.EncodeResponse(id, commentID, err)
}
// Find comments
func (s *RPC) findHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.FindRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
resp, err := s.eng.Find(req)
return jrpc.EncodeResponse(id, resp, err)
}
// Get comment
func (s *RPC) getHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.GetRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
comment, err := s.eng.Get(req)
return jrpc.EncodeResponse(id, comment, err)
}
// Update comment
func (s *RPC) updateHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
comment := store.Comment{}
if err := json.Unmarshal(params, &comment); err != nil {
return jrpc.Response{Error: err.Error()}
}
err := s.eng.Update(comment)
return jrpc.EncodeResponse(id, nil, err)
}
// counts for site and users
func (s *RPC) countHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.FindRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
count, err := s.eng.Count(req)
return jrpc.EncodeResponse(id, count, err)
}
// info get post meta info
func (s *RPC) infoHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.InfoRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
info, err := s.eng.Info(req)
return jrpc.EncodeResponse(id, info, err)
}
// flagHndl get and sets flag value
func (s *RPC) flagHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.FlagRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
status, err := s.eng.Flag(req)
return jrpc.EncodeResponse(id, status, err)
}
// listFlagsHndl list flags for given request
func (s *RPC) listFlagsHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.FlagRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
flags, err := s.eng.ListFlags(req)
return jrpc.EncodeResponse(id, flags, err)
}
// deleteHndl remove comment(s)
func (s *RPC) deleteHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.DeleteRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
err := s.eng.Delete(req)
return jrpc.EncodeResponse(id, nil, err)
}
// close store
func (s *RPC) closeHndl(id uint64, _ json.RawMessage) (rr jrpc.Response) {
if err := s.eng.Close(); err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.Response{}
}
// get admin key
func (s *RPC) admKeyHndl(id uint64, _ json.RawMessage) (rr jrpc.Response) {
key, err := s.adm.Key()
if err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.EncodeResponse(id, key, err)
}
// get admins list
func (s *RPC) admAdminsHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
args := []interface{}{}
if err := json.Unmarshal(params, &args); err != nil {
return jrpc.Response{Error: err.Error()}
}
siteID, ok := args[0].(string)
if !ok {
return jrpc.Response{Error: "incompatible argument"}
}
admins, err := s.adm.Admins(siteID)
if err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.EncodeResponse(id, admins, err)
}
// get admin email
func (s *RPC) admEmailHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
args := []interface{}{}
if err := json.Unmarshal(params, &args); err != nil {
return jrpc.Response{Error: err.Error()}
}
siteID, ok := args[0].(string)
if !ok {
return jrpc.Response{Error: "incompatible argument"}
}
email, err := s.adm.Email(siteID)
if err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.EncodeResponse(id, email, err)
}
@@ -0,0 +1,320 @@
/*
* 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 server
import (
"fmt"
"math/rand"
"net/http"
"testing"
"time"
"github.com/go-pkgz/jrpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"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"
"github.com/umputun/remark/backend/_example/memory_store/accessor"
)
func TestRPC_createHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
id, err := re.Create(store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}})
assert.NoError(t, err)
assert.Equal(t, "123456", id)
}
func TestRPC_findHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
findReq := engine.FindRequest{Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"}}
comments, err := re.Find(findReq)
require.NoError(t, err)
assert.Equal(t, 0, len(comments))
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
comments, err = re.Find(findReq)
require.NoError(t, err)
assert.Equal(t, 1, len(comments))
assert.Equal(t, c, comments[0])
}
func TestRPC_getHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
req := engine.GetRequest{
Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
CommentID: "123456",
}
_, err := re.Get(req)
assert.EqualError(t, err, "not found")
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
_, err = re.Create(c)
assert.NoError(t, err)
comment, err := re.Get(req)
assert.NoError(t, err)
assert.Equal(t, c, comment)
}
func TestRPC_updateHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
err := re.Update(c)
assert.EqualError(t, err, "not found")
_, err = re.Create(c)
assert.NoError(t, err)
c.Text = "updates"
err = re.Update(c)
assert.NoError(t, err)
req := engine.GetRequest{
Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
CommentID: "123456",
}
comment, err := re.Get(req)
assert.NoError(t, err)
assert.Equal(t, c, comment)
}
func TestRPC_countHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
findReq := engine.FindRequest{Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"}}
count, err := re.Count(findReq)
require.NoError(t, err)
assert.Equal(t, 0, count)
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
count, err = re.Count(findReq)
require.NoError(t, err)
assert.Equal(t, 1, count)
}
func TestRPC_infoHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
infoReq := engine.InfoRequest{Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"}}
info, err := re.Info(infoReq)
require.NoError(t, err)
assert.Equal(t, 1, len(info))
i := info[0]
assert.Equal(t, store.PostInfo{URL: "http://example.com/post1", Count: 1}, i)
}
func TestRPC_flagHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
flagReq := engine.FlagRequest{
Flag: engine.Verified,
Locator: store.Locator{
SiteID: "test-site",
},
UserID: "u1",
}
status, err := re.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, false, status)
flagReq.Update = engine.FlagTrue
status, err = re.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, true, status)
flagReq.Update = engine.FlagNonSet
status, err = re.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, true, status)
}
func TestRPC_listFlagsHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
flagReq := engine.FlagRequest{
Flag: engine.Verified,
UserID: "u1",
Locator: store.Locator{
SiteID: "test-site",
},
}
flags, err := re.ListFlags(flagReq)
require.NoError(t, err)
assert.Equal(t, []interface{}{}, flags)
flagReq.Update = engine.FlagTrue
status, err := re.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, true, status)
flags, err = re.ListFlags(flagReq)
require.NoError(t, err)
assert.Equal(t, []interface{}{"u1"}, flags)
}
func TestRPC_deleteHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
req := engine.DeleteRequest{
Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
CommentID: "123456",
}
err := re.Delete(req)
assert.EqualError(t, err, "not found")
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
_, err = re.Create(c)
assert.NoError(t, err)
err = re.Delete(req)
assert.NoError(t, err)
}
func TestRPC_closeHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
err := re.Close()
assert.NoError(t, err)
}
func TestRPC_admKeyHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ra := admin.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
key, err := ra.Key()
assert.NoError(t, err)
assert.Equal(t, "secret", key)
}
func TestRPC_admAdminsHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ra := admin.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
_, err := ra.Admins("bad site")
assert.EqualError(t, err, "site bad site not found")
admins, err := ra.Admins("test-site")
assert.NoError(t, err)
assert.Equal(t, []string{"id1", "id2"}, admins)
}
func TestRPC_admEmailHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ra := admin.RPC{Client: rpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
_, err := ra.Admins("bad site")
assert.EqualError(t, err, "site bad site not found")
email, err := ra.Email("test-site")
assert.NoError(t, err)
assert.Equal(t, "admin@example.com", email)
}
func prepTestStore(t *testing.T) (s *RPC, port int, teardown func()) {
port = 40000 + int(rand.Int31n(10000))
mg := accessor.NewMemData()
adm := accessor.NewMemAdminStore("secret")
s = NewRPC(mg, adm, &jrpc.Server{API: "/test", Logger: jrpc.NoOpLogger})
admRec := accessor.AdminRec{
SiteID: "test-site",
IDs: []string{"id1", "id2"},
Email: "admin@example.com",
}
adm.Set("test-site", admRec)
go func() {
t.Log(s.Run(port))
}()
time.Sleep(time.Millisecond * 10)
return s, port, func() {
require.NoError(t, s.Shutdown())
}
}