diff --git a/app/main.go b/app/main.go index 5460f456..2c68dd31 100644 --- a/app/main.go +++ b/app/main.go @@ -72,12 +72,13 @@ func main() { return } + dataService := store.Service{Interface: dataStore} sessionStore := sessions.NewFilesystemStore(opts.ServerCommand.SessionStore, []byte(opts.ServerCommand.StoreKey)) exporter := migrator.Remark{DataStore: dataStore} srv := rest.Server{ Version: revision, - Store: dataStore, + DataService: dataService, SessionStore: sessionStore, Admins: opts.Admins, DevMode: opts.DevMode, diff --git a/app/rest/admin.go b/app/rest/admin.go index b973f490..841c21c7 100644 --- a/app/rest/admin.go +++ b/app/rest/admin.go @@ -15,9 +15,9 @@ import ( // admin provides router for all requests available for admin only type admin struct { - dataStore store.Interface - exporter migrator.Exporter - respCache *cache.Cache + dataService store.Service + exporter migrator.Exporter + respCache *cache.Cache } func (a *admin) routes() chi.Router { @@ -37,7 +37,7 @@ func (a *admin) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) { log.Printf("[INFO] delete comment %s", id) url := r.URL.Query().Get("url") - err := a.dataStore.Delete(store.Locator{URL: url}, id) + err := a.dataService.Delete(store.Locator{URL: url}, id) if err != nil { log.Printf("[WARN] can't delete comment, %s", err) httpError(w, r, http.StatusInternalServerError, err, "can't delete comment") @@ -54,7 +54,7 @@ func (a *admin) setBlockCtrl(w http.ResponseWriter, r *http.Request) { siteID := r.URL.Query().Get("site") blockStatus := r.URL.Query().Get("block") == "1" - if err := a.dataStore.SetBlock(store.Locator{SiteID: siteID}, userID, blockStatus); err != nil { + if err := a.dataService.SetBlock(store.Locator{SiteID: siteID}, userID, blockStatus); err != nil { httpError(w, r, http.StatusBadRequest, err, "can't set blocking status") return } @@ -68,7 +68,7 @@ func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) { url := r.URL.Query().Get("url") pinStatus := r.URL.Query().Get("pin") == "1" - if err := a.dataStore.SetPin(store.Locator{URL: url}, commentID, pinStatus); err != nil { + if err := a.dataService.SetPin(store.Locator{URL: url}, commentID, pinStatus); err != nil { httpError(w, r, http.StatusBadRequest, err, "can't set pin status") return } @@ -84,5 +84,5 @@ func (a *admin) exportCtrl(w http.ResponseWriter, r *http.Request) { } } func (a *admin) checkBlocked(locator store.Locator, user store.User) bool { - return a.dataStore.IsBlocked(store.Locator{}, user.ID) + return a.dataService.IsBlocked(store.Locator{}, user.ID) } diff --git a/app/rest/server.go b/app/rest/server.go index 18c5fb5e..3b3f48c0 100644 --- a/app/rest/server.go +++ b/app/rest/server.go @@ -27,7 +27,7 @@ import ( // Server is a rest access server type Server struct { Version string - Store store.Interface + DataService store.Service Admins []string AuthGoogle *auth.Provider AuthGithub *auth.Provider @@ -80,7 +80,7 @@ func (s *Server) Run() { rauth.Get("/user", s.userInfoCtrl) rauth.Put("/vote/{id}", s.voteCtrl) - s.mod = admin{dataStore: s.Store, exporter: s.Exporter, respCache: s.respCache} + s.mod = admin{dataService: s.DataService, exporter: s.Exporter, respCache: s.respCache} rauth.Mount("/admin", s.mod.routes()) }) @@ -140,7 +140,7 @@ func (s *Server) createCommentCtrl(w http.ResponseWriter, r *http.Request) { return } - id, err := s.Store.Create(comment) + id, err := s.DataService.Create(comment) if err != nil { log.Printf("[WARN] can't save comment, %s", err) httpError(w, r, http.StatusInternalServerError, err, "can't save comment") @@ -160,7 +160,7 @@ func (s *Server) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) { log.Printf("[DEBUG] delete comment %s", id) url := r.URL.Query().Get("url") - err := s.Store.Delete(store.Locator{URL: url}, id) + err := s.DataService.Delete(store.Locator{URL: url}, id) if err != nil { log.Printf("[WARN] can't delete comment, %s", err) httpError(w, r, http.StatusInternalServerError, err, "can't delete comment") @@ -185,7 +185,7 @@ func (s *Server) findCommentsCtrl(w http.ResponseWriter, r *http.Request) { return } - comments, err := s.Store.Find(store.Request{Locator: store.Locator{URL: url}, Sort: r.URL.Query().Get("sort")}) + comments, err := s.DataService.Find(store.Request{Locator: store.Locator{URL: url}, Sort: r.URL.Query().Get("sort")}) if err != nil { log.Printf("[WARN] can't get comments for %s, %s", url, err) httpError(w, r, http.StatusInternalServerError, err, "can't load comments comment") @@ -216,7 +216,7 @@ func (s *Server) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) { return } - comments, err := s.Store.Last(store.Locator{}, max) + comments, err := s.DataService.Last(store.Locator{}, max) if err != nil { log.Printf("[WARN] can't get last comments, %s", err) httpError(w, r, http.StatusInternalServerError, err, "can't get last comments") @@ -236,7 +236,7 @@ func (s *Server) commentByIDCtrl(w http.ResponseWriter, r *http.Request) { log.Printf("[DEBUG] get comments by id %s, %s", id, url) - comment, err := s.Store.Get(store.Locator{URL: url}, id) + comment, err := s.DataService.GetByID(store.Locator{URL: url}, id) if err != nil { log.Printf("[WARN] can't get comment, %s", err) httpError(w, r, http.StatusInternalServerError, err, "can't get comment by id") @@ -260,7 +260,7 @@ func (s *Server) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) { return } - comments, err := s.Store.GetForUser(store.Locator{}, userID) + comments, err := s.DataService.GetByUser(store.Locator{}, userID) if err != nil { log.Printf("[WARN] can't get comment, %s", err) httpError(w, r, http.StatusBadRequest, err, "can't get comment by user id") @@ -285,7 +285,7 @@ func (s *Server) userInfoCtrl(w http.ResponseWriter, r *http.Request) { // GET /count?url=post-url func (s *Server) countCtrl(w http.ResponseWriter, r *http.Request) { url := r.URL.Query().Get("url") - count, err := s.Store.Count(store.Locator{URL: url}) + count, err := s.DataService.Count(store.Locator{URL: url}) if err != nil { httpError(w, r, http.StatusBadRequest, err, "can't get count") return @@ -308,7 +308,7 @@ func (s *Server) voteCtrl(w http.ResponseWriter, r *http.Request) { url := r.URL.Query().Get("url") vote := r.URL.Query().Get("vote") == "1" - comment, err := s.Store.Vote(store.Locator{URL: url}, id, user.ID, vote) + comment, err := s.DataService.Vote(store.Locator{URL: url}, id, user.ID, vote) if err != nil { log.Printf("[WARN] vote rejected for %s - %s, %s", user.ID, id, err) httpError(w, r, http.StatusBadRequest, err, "can't vote for comment") diff --git a/app/store/bolt.go b/app/store/bolt.go index 1d3d2496..aef6212e 100644 --- a/app/store/bolt.go +++ b/app/store/bolt.go @@ -178,8 +178,8 @@ func (b *BoltDB) Find(request Request) ([]Comment, error) { return res, err } -// Get comment by id across posts -func (b *BoltDB) Get(locator Locator, commentID string) (comment Comment, err error) { +// GetByID returns comment by id across posts +func (b *BoltDB) GetByID(locator Locator, commentID string) (comment Comment, err error) { err = b.View(func(tx *bolt.Tx) error { @@ -262,29 +262,6 @@ func (b *BoltDB) Last(locator Locator, max int) (result []Comment, err error) { return result, err } -// Vote for comment by id and locator -func (b *BoltDB) Vote(locator Locator, commentID string, userID string, val bool) (comment Comment, err error) { - - comment, err = b.getComment(locator.URL, commentID) - if err != nil { - return comment, err - } - - if _, voted := comment.Votes[userID]; voted { - return comment, errors.Errorf("user %s already voted for %s", userID, commentID) - } - // update votes and score - comment.Votes[userID] = val - - if val { - comment.Score++ - } else { - comment.Score-- - } - - return comment, b.putComment(locator.URL, comment) -} - // Count returns number of comments for locator func (b *BoltDB) Count(locator Locator) (count int, err error) { err = b.View(func(tx *bolt.Tx) error { @@ -348,19 +325,9 @@ func (b BoltDB) List(locator Locator) (result []string, err error) { return result, err } -// SetPin pin/un-pin comment as special -func (b *BoltDB) SetPin(locator Locator, commentID string, status bool) error { - comment, err := b.getComment(locator.URL, commentID) - if err != nil { - return err - } - comment.Pin = status - return b.putComment(locator.URL, comment) -} - -// GetForUser extracts all comments for given site and given userID +// GetByUser extracts all comments for given site and given userID // "users" bucket has sub-bucket for each userID, and keeps it as ts:ref -func (b *BoltDB) GetForUser(locator Locator, userID string) (comments []Comment, err error) { +func (b *BoltDB) GetByUser(locator Locator, userID string) (comments []Comment, err error) { comments = []Comment{} commentRefs := []string{} @@ -397,7 +364,7 @@ func (b *BoltDB) GetForUser(locator Locator, userID string) (comments []Comment, if e != nil { return comments, errors.Wrapf(e, "can't parse reference %s", v) } - if c, e := b.Get(Locator{URL: url, SiteID: locator.SiteID}, commentID); e == nil { + if c, e := b.GetByID(Locator{URL: url, SiteID: locator.SiteID}, commentID); e == nil { comments = append(comments, c) } } @@ -405,18 +372,19 @@ func (b *BoltDB) GetForUser(locator Locator, userID string) (comments []Comment, return comments, err } -func (b *BoltDB) getComment(url string, commentID string) (comment Comment, err error) { +// GetComment for locator.URL and commentID string +func (b *BoltDB) GetComment(locator Locator, commentID string) (comment Comment, err error) { err = b.View(func(tx *bolt.Tx) error { - bucket := tx.Bucket([]byte(url)) + bucket := tx.Bucket([]byte(locator.URL)) if bucket == nil { - return errors.Errorf("no bucket %s in store", url) + return errors.Errorf("no bucket %s in store", locator.URL) } // get and unmarshal comment commentVal := bucket.Get([]byte(commentID)) if commentVal == nil { - return errors.Errorf("no comment for %s in store %s", commentID, url) + return errors.Errorf("no comment for %s in store %s", commentID, locator.URL) } if e := json.Unmarshal(commentVal, &comment); e != nil { @@ -427,9 +395,10 @@ func (b *BoltDB) getComment(url string, commentID string) (comment Comment, err return comment, err } -func (b *BoltDB) putComment(url string, comment Comment) error { +// PutComment updates comment for locator.URL with mutable part of comment +func (b *BoltDB) PutComment(locator Locator, comment Comment) error { - if curComment, err := b.getComment(url, comment.ID); err == nil { + if curComment, err := b.GetComment(locator, comment.ID); err == nil { // preserve immutable fields comment.ParentID = curComment.ParentID comment.Locator = curComment.Locator @@ -438,9 +407,9 @@ func (b *BoltDB) putComment(url string, comment Comment) error { } return b.Update(func(tx *bolt.Tx) error { - bucket := tx.Bucket([]byte(url)) + bucket := tx.Bucket([]byte(locator.URL)) if bucket == nil { - return errors.Errorf("no bucket %s in store", url) + return errors.Errorf("no bucket %s in store", locator.URL) } // serialize comment to json []byte for bolt and save @@ -449,7 +418,7 @@ func (b *BoltDB) putComment(url string, comment Comment) error { return errors.Wrap(jerr, "can't marshal comment") } if err := bucket.Put([]byte(comment.ID), jdata); err != nil { - return errors.Wrapf(err, "failed to put key %s to bucket %s", comment.ID, url) + return errors.Wrapf(err, "failed to put key %s to bucket %s", comment.ID, locator.URL) } return nil }) diff --git a/app/store/bolt_test.go b/app/store/bolt_test.go index 8628d395..3b261436 100644 --- a/app/store/bolt_test.go +++ b/app/store/bolt_test.go @@ -45,7 +45,7 @@ func TestBoltDB_Delete(t *testing.T) { assert.Equal(t, 1, len(comments), "only 1 left in last") } -func TestBoltDB_Get(t *testing.T) { +func TestBoltDB_GetByID(t *testing.T) { defer os.Remove(testDb) b := prep(t) @@ -53,11 +53,11 @@ func TestBoltDB_Get(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 2, len(res)) - comment, err := b.Get(Locator{URL: "https://radio-t.com"}, res[1].ID) + comment, err := b.GetByID(Locator{URL: "https://radio-t.com"}, res[1].ID) assert.Nil(t, err) assert.Equal(t, "some text2", comment.Text) - comment, err = b.Get(Locator{URL: "https://radio-t.com"}, "1234567") + comment, err = b.GetByID(Locator{URL: "https://radio-t.com"}, "1234567") assert.NotNil(t, err) } @@ -76,31 +76,6 @@ func TestBoltDB_Last(t *testing.T) { assert.Equal(t, "some text2", res[0].Text) } -func TestBoltDB_Vote(t *testing.T) { - defer os.Remove(testDb) - b := prep(t) - - res, err := b.Last(Locator{URL: "https://radio-t.com"}, 0) - t.Logf("%+v", res[0]) - assert.Nil(t, err) - assert.Equal(t, 2, len(res)) - assert.Equal(t, 0, res[0].Score) - assert.Equal(t, map[string]bool{}, res[0].Votes) - - c, err := b.Vote(Locator{URL: "https://radio-t.com"}, res[0].ID, "user1", true) - assert.Nil(t, err) - assert.Equal(t, 1, c.Score) - assert.Equal(t, map[string]bool{"user1": true}, c.Votes) - - _, err = b.Vote(Locator{URL: "https://radio-t.com"}, res[0].ID, "user1", true) - assert.NotNil(t, err, "double-voting rejected") - - res, err = b.Last(Locator{URL: "https://radio-t.com"}, 0) - assert.Nil(t, err) - assert.Equal(t, 2, len(res)) - assert.Equal(t, 1, res[0].Score) -} - func TestBoltDB_Count(t *testing.T) { defer os.Remove(testDb) b := prep(t) @@ -141,35 +116,11 @@ func TestBoltDB_List(t *testing.T) { assert.Equal(t, []string{"https://radio-t.com", "https://radio-t.com/2"}, res) } -func TestBoltDB_Pin(t *testing.T) { - defer os.Remove(testDb) - b := prep(t) - - res, err := b.Last(Locator{URL: "https://radio-t.com"}, 0) - t.Logf("%+v", res[0]) - assert.Nil(t, err) - assert.Equal(t, 2, len(res)) - assert.Equal(t, false, res[0].Pin) - - err = b.SetPin(Locator{URL: "https://radio-t.com"}, res[0].ID, true) - assert.Nil(t, err) - - c, err := b.Get(Locator{URL: "https://radio-t.com"}, res[0].ID) - assert.Nil(t, err) - assert.Equal(t, true, c.Pin) - - err = b.SetPin(Locator{URL: "https://radio-t.com"}, res[0].ID, false) - assert.Nil(t, err) - c, err = b.Get(Locator{URL: "https://radio-t.com"}, res[0].ID) - assert.Nil(t, err) - assert.Equal(t, false, c.Pin) -} - func TestBoltDB_GetForUser(t *testing.T) { defer os.Remove(testDb) b := prep(t) - res, err := b.GetForUser(Locator{SiteID: "radio-t"}, "user1") + res, err := b.GetByUser(Locator{SiteID: "radio-t"}, "user1") assert.Nil(t, err) assert.Equal(t, 2, len(res)) assert.Equal(t, "some text2", res[0].Text, "sorted by -time") diff --git a/app/store/service.go b/app/store/service.go new file mode 100644 index 00000000..7e5f2e14 --- /dev/null +++ b/app/store/service.go @@ -0,0 +1,41 @@ +package store + +import "github.com/pkg/errors" + +// Service wraps store.Interface with additional methods +type Service struct { + Interface +} + +// SetPin pin/un-pin comment as special +func (s *Service) SetPin(locator Locator, commentID string, status bool) error { + comment, err := s.GetComment(locator, commentID) + if err != nil { + return err + } + comment.Pin = status + return s.PutComment(locator, comment) +} + +// Vote for comment by id and locator +func (s *Service) Vote(locator Locator, commentID string, userID string, val bool) (comment Comment, err error) { + + comment, err = s.GetComment(locator, commentID) + if err != nil { + return comment, err + } + + if _, voted := comment.Votes[userID]; voted { + return comment, errors.Errorf("user %s already voted for %s", userID, commentID) + } + // update votes and score + comment.Votes[userID] = val + + if val { + comment.Score++ + } else { + comment.Score-- + } + + return comment, s.PutComment(locator, comment) +} diff --git a/app/store/service_test.go b/app/store/service_test.go new file mode 100644 index 00000000..8f89093b --- /dev/null +++ b/app/store/service_test.go @@ -0,0 +1,57 @@ +package store + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestService_Vote(t *testing.T) { + defer os.Remove(testDb) + b := Service{Interface: prep(t)} + + res, err := b.Last(Locator{URL: "https://radio-t.com"}, 0) + t.Logf("%+v", res[0]) + assert.Nil(t, err) + assert.Equal(t, 2, len(res)) + assert.Equal(t, 0, res[0].Score) + assert.Equal(t, map[string]bool{}, res[0].Votes) + + c, err := b.Vote(Locator{URL: "https://radio-t.com"}, res[0].ID, "user1", true) + assert.Nil(t, err) + assert.Equal(t, 1, c.Score) + assert.Equal(t, map[string]bool{"user1": true}, c.Votes) + + _, err = b.Vote(Locator{URL: "https://radio-t.com"}, res[0].ID, "user1", true) + assert.NotNil(t, err, "double-voting rejected") + + res, err = b.Last(Locator{URL: "https://radio-t.com"}, 0) + assert.Nil(t, err) + assert.Equal(t, 2, len(res)) + assert.Equal(t, 1, res[0].Score) +} + +func TestBoltDB_Pin(t *testing.T) { + defer os.Remove(testDb) + b := Service{Interface: prep(t)} + + res, err := b.Last(Locator{URL: "https://radio-t.com"}, 0) + t.Logf("%+v", res[0]) + assert.Nil(t, err) + assert.Equal(t, 2, len(res)) + assert.Equal(t, false, res[0].Pin) + + err = b.SetPin(Locator{URL: "https://radio-t.com"}, res[0].ID, true) + assert.Nil(t, err) + + c, err := b.GetByID(Locator{URL: "https://radio-t.com"}, res[0].ID) + assert.Nil(t, err) + assert.Equal(t, true, c.Pin) + + err = b.SetPin(Locator{URL: "https://radio-t.com"}, res[0].ID, false) + assert.Nil(t, err) + c, err = b.GetByID(Locator{URL: "https://radio-t.com"}, res[0].ID) + assert.Nil(t, err) + assert.Equal(t, false, c.Pin) +} diff --git a/app/store/store.go b/app/store/store.go index e1bc7dcf..ff129284 100644 --- a/app/store/store.go +++ b/app/store/store.go @@ -54,18 +54,18 @@ type Request struct { // Interface defines basic CRUD for comments type Interface interface { Create(comment Comment) (commentID string, err error) + GetComment(locator Locator, commentID string) (comment Comment, err error) + PutComment(locator Locator, comment Comment) error Delete(locator Locator, commentID string) error Find(request Request) ([]Comment, error) Last(locator Locator, max int) ([]Comment, error) - Get(locator Locator, commentID string) (Comment, error) - Vote(locator Locator, commentID string, userID string, val bool) (Comment, error) + GetByID(locator Locator, commentID string) (Comment, error) + GetByUser(locator Locator, userID string) ([]Comment, error) Count(locator Locator) (int, error) List(locator Locator) ([]string, error) - GetForUser(locator Locator, userID string) ([]Comment, error) SetBlock(locator Locator, userID string, status bool) error IsBlocked(locator Locator, userID string) bool - SetPin(locator Locator, commentID string, status bool) error } func makeCommentID() string {