Image interface changes (#623)
@paskal * sort imports, add missing copyright * regenerate engine mock * make all image.Store interface functions public * go mod tidy * make image.Store.Load return []byte instead of io.ReadCloser * separate memory_store example RPC server to multiple files by handlers groups
This commit is contained in:
@@ -9,6 +9,7 @@ package accessor
|
||||
import (
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store/admin"
|
||||
)
|
||||
|
||||
|
||||
@@ -14,9 +14,8 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store/engine"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/engine"
|
||||
)
|
||||
|
||||
const lastLimit = 1000
|
||||
|
||||
@@ -157,6 +157,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
|
||||
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=
|
||||
@@ -220,6 +221,7 @@ golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgn
|
||||
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-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/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=
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2020 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 (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/go-pkgz/jrpc"
|
||||
"github.com/umputun/remark/backend/app/store/admin"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
var siteID string
|
||||
if err := json.Unmarshal(params, &siteID); err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
|
||||
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) {
|
||||
var siteID string
|
||||
if err := json.Unmarshal(params, &siteID); err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
|
||||
email, err := s.adm.Email(siteID)
|
||||
if err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
return jrpc.EncodeResponse(id, email, err)
|
||||
}
|
||||
|
||||
// return site enabled status
|
||||
func (s *RPC) admEnabledHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
|
||||
var siteID string
|
||||
if err := json.Unmarshal(params, &siteID); err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
|
||||
ok, err := s.adm.Enabled(siteID)
|
||||
if err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
return jrpc.EncodeResponse(id, ok, err)
|
||||
}
|
||||
|
||||
// onEvent returns nothing, callback to OnEvent
|
||||
func (s *RPC) admEventHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
|
||||
var siteID string
|
||||
var ps []interface{}
|
||||
if err := json.Unmarshal(params, &ps); err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
siteID, ok := ps[0].(string)
|
||||
if !ok {
|
||||
return jrpc.Response{Error: "wrong siteID type"}
|
||||
}
|
||||
evType, ok := ps[1].(float64)
|
||||
if !ok {
|
||||
return jrpc.Response{Error: "wrong event type"}
|
||||
}
|
||||
err := s.adm.OnEvent(siteID, admin.EventType(evType))
|
||||
if err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
return jrpc.EncodeResponse(id, nil, err)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2020 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"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/jrpc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/umputun/remark/backend/app/store/admin"
|
||||
)
|
||||
|
||||
func TestRPC_admKeyHndl(t *testing.T) {
|
||||
_, port, teardown := prepTestStore(t)
|
||||
defer teardown()
|
||||
api := fmt.Sprintf("http://localhost:%d/test", port)
|
||||
|
||||
ra := admin.RPC{Client: jrpc.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: jrpc.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: jrpc.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 TestRPC_admEnabledHndl(t *testing.T) {
|
||||
_, port, teardown := prepTestStore(t)
|
||||
defer teardown()
|
||||
api := fmt.Sprintf("http://localhost:%d/test", port)
|
||||
|
||||
ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
|
||||
_, err := ra.Enabled("bad site")
|
||||
assert.EqualError(t, err, "site bad site not found")
|
||||
|
||||
ok, err := ra.Enabled("test-site")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, ok)
|
||||
|
||||
ok, err = ra.Enabled("test-site-disabled")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, ok)
|
||||
}
|
||||
|
||||
func TestRPC_admEventHndl(t *testing.T) {
|
||||
_, port, teardown := prepTestStore(t)
|
||||
defer teardown()
|
||||
api := fmt.Sprintf("http://localhost:%d/test", port)
|
||||
|
||||
ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
|
||||
err := ra.OnEvent("bad site", admin.EvCreate)
|
||||
assert.EqualError(t, err, "site bad site not found")
|
||||
|
||||
err = ra.OnEvent("test-site", admin.EvCreate)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2020 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 (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/go-pkgz/jrpc"
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/engine"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// userDetailHndl sets or gets single detail value, or gets all details for requested site.
|
||||
// userDetailHndl returns list even for single entry request is a compromise in order to have both single detail getting and setting
|
||||
// and all site's details listing under the same function (and not to extend engine interface by two separate functions).
|
||||
func (s *RPC) userDetailHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
|
||||
req := engine.UserDetailRequest{}
|
||||
if err := json.Unmarshal(params, &req); err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
value, err := s.eng.UserDetail(req)
|
||||
return jrpc.EncodeResponse(id, value, err)
|
||||
}
|
||||
|
||||
// deleteHndl delete post(s), user, comment, user details, or everything
|
||||
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(_ uint64, _ json.RawMessage) (rr jrpc.Response) {
|
||||
if err := s.eng.Close(); err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
return jrpc.Response{}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* Copyright 2020 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"
|
||||
"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/store"
|
||||
"github.com/umputun/remark/backend/app/store/engine"
|
||||
)
|
||||
|
||||
func TestRPC_createHndl(t *testing.T) {
|
||||
_, port, teardown := prepTestStore(t)
|
||||
defer teardown()
|
||||
api := fmt.Sprintf("http://localhost:%d/test", port)
|
||||
|
||||
re := engine.RPC{Client: jrpc.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: jrpc.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: jrpc.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: jrpc.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: jrpc.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: jrpc.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: jrpc.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: jrpc.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_userDetailHndl(t *testing.T) {
|
||||
_, port, teardown := prepTestStore(t)
|
||||
defer teardown()
|
||||
api := fmt.Sprintf("http://localhost:%d/test", port)
|
||||
|
||||
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
|
||||
|
||||
// add to entries to DB before we start
|
||||
result, err := re.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "u1", Detail: engine.UserEmail, Update: "test@example.com"})
|
||||
assert.NoError(t, err, "No error inserting entry expected")
|
||||
assert.ElementsMatch(t, []engine.UserDetailEntry{{UserID: "u1", Email: "test@example.com"}}, result)
|
||||
result, err = re.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "u2", Detail: engine.UserEmail, Update: "other@example.com"})
|
||||
assert.NoError(t, err, "No error inserting entry expected")
|
||||
assert.ElementsMatch(t, []engine.UserDetailEntry{{UserID: "u2", Email: "other@example.com"}}, result)
|
||||
|
||||
// try to change existing entry with wrong SiteID
|
||||
result, err = re.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "u2", Detail: engine.UserEmail, Update: "not_relevant"})
|
||||
assert.NoError(t, err, "Updating existing entry with wrong SiteID doesn't produce error")
|
||||
assert.ElementsMatch(t, []engine.UserDetailEntry{}, result, "Updating existing entry with wrong SiteID doesn't change anything")
|
||||
|
||||
// stateless tests without changing the state we set up before
|
||||
var testData = []struct {
|
||||
req engine.UserDetailRequest
|
||||
error string
|
||||
expected []engine.UserDetailEntry
|
||||
}{
|
||||
{req: engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "u1", Detail: engine.UserEmail},
|
||||
expected: []engine.UserDetailEntry{{UserID: "u1", Email: "test@example.com"}}},
|
||||
{req: engine.UserDetailRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "u1", Detail: engine.UserEmail},
|
||||
expected: []engine.UserDetailEntry{}},
|
||||
{req: engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "u1xyz", Detail: engine.UserEmail},
|
||||
expected: []engine.UserDetailEntry{}},
|
||||
{req: engine.UserDetailRequest{Detail: engine.UserEmail, Update: "new_value"},
|
||||
error: `userid cannot be empty in request for single detail`},
|
||||
{req: engine.UserDetailRequest{Detail: engine.UserDetail("bad")},
|
||||
error: `unsupported detail "bad"`},
|
||||
{req: engine.UserDetailRequest{Update: "not_relevant", Detail: engine.AllUserDetails},
|
||||
error: `unsupported request with userdetail all`},
|
||||
{req: engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, Detail: engine.AllUserDetails},
|
||||
expected: []engine.UserDetailEntry{{UserID: "u1", Email: "test@example.com"}, {UserID: "u2", Email: "other@example.com"}}},
|
||||
}
|
||||
|
||||
for i, x := range testData {
|
||||
result, err := re.UserDetail(x.req)
|
||||
if x.error != "" {
|
||||
assert.EqualError(t, err, x.error, "Error should match expected for case %d", i)
|
||||
} else {
|
||||
assert.NoError(t, err, "Error is not expected expected for case %d", i)
|
||||
}
|
||||
assert.ElementsMatch(t, x.expected, result, "Result should match expected for case %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRPC_deleteHndl(t *testing.T) {
|
||||
_, port, teardown := prepTestStore(t)
|
||||
defer teardown()
|
||||
api := fmt.Sprintf("http://localhost:%d/test", port)
|
||||
|
||||
re := engine.RPC{Client: jrpc.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: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
|
||||
err := re.Close()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
/*
|
||||
* Copyright 2020 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 (
|
||||
"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"
|
||||
)
|
||||
@@ -50,185 +53,3 @@ func (s *RPC) addHandlers() {
|
||||
"event": s.admEventHndl,
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// userDetailHndl sets or gets single detail value, or gets all details for requested site.
|
||||
// userDetailHndl returns list even for single entry request is a compromise in order to have both single detail getting and setting
|
||||
// and all site's details listing under the same function (and not to extend engine interface by two separate functions).
|
||||
func (s *RPC) userDetailHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
|
||||
req := engine.UserDetailRequest{}
|
||||
if err := json.Unmarshal(params, &req); err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
value, err := s.eng.UserDetail(req)
|
||||
return jrpc.EncodeResponse(id, value, err)
|
||||
}
|
||||
|
||||
// deleteHndl delete post(s), user, comment, user details, or everything
|
||||
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) {
|
||||
var siteID string
|
||||
if err := json.Unmarshal(params, &siteID); err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
|
||||
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) {
|
||||
var siteID string
|
||||
if err := json.Unmarshal(params, &siteID); err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
|
||||
email, err := s.adm.Email(siteID)
|
||||
if err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
return jrpc.EncodeResponse(id, email, err)
|
||||
}
|
||||
|
||||
// return site enabled status
|
||||
func (s *RPC) admEnabledHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
|
||||
var siteID string
|
||||
if err := json.Unmarshal(params, &siteID); err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
|
||||
ok, err := s.adm.Enabled(siteID)
|
||||
if err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
return jrpc.EncodeResponse(id, ok, err)
|
||||
}
|
||||
|
||||
// onEvent returns nothing, callback to OnEvent
|
||||
func (s *RPC) admEventHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
|
||||
var siteID string
|
||||
ps := []interface{}{}
|
||||
if err := json.Unmarshal(params, &ps); err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
siteID, ok := ps[0].(string)
|
||||
if !ok {
|
||||
return jrpc.Response{Error: "wrong siteID type"}
|
||||
}
|
||||
evType, ok := ps[1].(float64)
|
||||
if !ok {
|
||||
return jrpc.Response{Error: "wrong event type"}
|
||||
}
|
||||
err := s.adm.OnEvent(siteID, admin.EventType(evType))
|
||||
if err != nil {
|
||||
return jrpc.Response{Error: err.Error()}
|
||||
}
|
||||
return jrpc.EncodeResponse(id, nil, err)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019 Umputun. All rights reserved.
|
||||
* Copyright 2020 Umputun. All rights reserved.
|
||||
* Use of this source code is governed by a MIT-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
@@ -16,369 +16,11 @@ import (
|
||||
|
||||
"github.com/go-pkgz/jrpc"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"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/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: jrpc.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: jrpc.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: jrpc.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: jrpc.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: jrpc.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: jrpc.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: jrpc.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: jrpc.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_userDetailHndl(t *testing.T) {
|
||||
_, port, teardown := prepTestStore(t)
|
||||
defer teardown()
|
||||
api := fmt.Sprintf("http://localhost:%d/test", port)
|
||||
|
||||
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
|
||||
|
||||
// add to entries to DB before we start
|
||||
result, err := re.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "u1", Detail: engine.UserEmail, Update: "test@example.com"})
|
||||
assert.NoError(t, err, "No error inserting entry expected")
|
||||
assert.ElementsMatch(t, []engine.UserDetailEntry{{UserID: "u1", Email: "test@example.com"}}, result)
|
||||
result, err = re.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "u2", Detail: engine.UserEmail, Update: "other@example.com"})
|
||||
assert.NoError(t, err, "No error inserting entry expected")
|
||||
assert.ElementsMatch(t, []engine.UserDetailEntry{{UserID: "u2", Email: "other@example.com"}}, result)
|
||||
|
||||
// try to change existing entry with wrong SiteID
|
||||
result, err = re.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "u2", Detail: engine.UserEmail, Update: "not_relevant"})
|
||||
assert.NoError(t, err, "Updating existing entry with wrong SiteID doesn't produce error")
|
||||
assert.ElementsMatch(t, []engine.UserDetailEntry{}, result, "Updating existing entry with wrong SiteID doesn't change anything")
|
||||
|
||||
// stateless tests without changing the state we set up before
|
||||
var testData = []struct {
|
||||
req engine.UserDetailRequest
|
||||
error string
|
||||
expected []engine.UserDetailEntry
|
||||
}{
|
||||
{req: engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "u1", Detail: engine.UserEmail},
|
||||
expected: []engine.UserDetailEntry{{UserID: "u1", Email: "test@example.com"}}},
|
||||
{req: engine.UserDetailRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "u1", Detail: engine.UserEmail},
|
||||
expected: []engine.UserDetailEntry{}},
|
||||
{req: engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "u1xyz", Detail: engine.UserEmail},
|
||||
expected: []engine.UserDetailEntry{}},
|
||||
{req: engine.UserDetailRequest{Detail: engine.UserEmail, Update: "new_value"},
|
||||
error: `userid cannot be empty in request for single detail`},
|
||||
{req: engine.UserDetailRequest{Detail: engine.UserDetail("bad")},
|
||||
error: `unsupported detail "bad"`},
|
||||
{req: engine.UserDetailRequest{Update: "not_relevant", Detail: engine.AllUserDetails},
|
||||
error: `unsupported request with userdetail all`},
|
||||
{req: engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, Detail: engine.AllUserDetails},
|
||||
expected: []engine.UserDetailEntry{{UserID: "u1", Email: "test@example.com"}, {UserID: "u2", Email: "other@example.com"}}},
|
||||
}
|
||||
|
||||
for i, x := range testData {
|
||||
result, err := re.UserDetail(x.req)
|
||||
if x.error != "" {
|
||||
assert.EqualError(t, err, x.error, "Error should match expected for case %d", i)
|
||||
} else {
|
||||
assert.NoError(t, err, "Error is not expected expected for case %d", i)
|
||||
}
|
||||
assert.ElementsMatch(t, x.expected, result, "Result should match expected for case %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRPC_deleteHndl(t *testing.T) {
|
||||
_, port, teardown := prepTestStore(t)
|
||||
defer teardown()
|
||||
api := fmt.Sprintf("http://localhost:%d/test", port)
|
||||
|
||||
re := engine.RPC{Client: jrpc.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: jrpc.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: jrpc.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: jrpc.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: jrpc.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 TestRPC_admEnabledHndl(t *testing.T) {
|
||||
_, port, teardown := prepTestStore(t)
|
||||
defer teardown()
|
||||
api := fmt.Sprintf("http://localhost:%d/test", port)
|
||||
|
||||
ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
|
||||
_, err := ra.Enabled("bad site")
|
||||
assert.EqualError(t, err, "site bad site not found")
|
||||
|
||||
ok, err := ra.Enabled("test-site")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, ok)
|
||||
|
||||
ok, err = ra.Enabled("test-site-disabled")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, ok)
|
||||
}
|
||||
|
||||
func TestRPC_admEventHndl(t *testing.T) {
|
||||
_, port, teardown := prepTestStore(t)
|
||||
defer teardown()
|
||||
api := fmt.Sprintf("http://localhost:%d/test", port)
|
||||
|
||||
ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
|
||||
err := ra.OnEvent("bad site", admin.EvCreate)
|
||||
assert.EqualError(t, err, "site bad site not found")
|
||||
|
||||
err = ra.OnEvent("test-site", admin.EvCreate)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func chooseRandomUnusedPort() (port int) {
|
||||
for i := 0; i < 10; i++ {
|
||||
port = 40000 + int(rand.Int31n(10000))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1" // nolint
|
||||
"encoding/base64"
|
||||
"io"
|
||||
@@ -436,7 +437,7 @@ func (s *public) loadPictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
id := chi.URLParam(r, "user") + "/" + chi.URLParam(r, "id")
|
||||
imgRdr, size, err := s.imageService.Load(id)
|
||||
img, err := s.imageService.Load(id)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get image "+id, rest.ErrAssetNotFound)
|
||||
return
|
||||
@@ -452,16 +453,10 @@ func (s *public) loadPictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := imgRdr.Close(); e != nil {
|
||||
log.Printf("[WARN] failed to close reader for picture %s, %v", id, e)
|
||||
}
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", imgContentType(id))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(int(size)))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(img)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err = io.Copy(w, imgRdr); err != nil {
|
||||
if _, err = io.Copy(w, bytes.NewReader(img)); err != nil {
|
||||
log.Printf("[WARN] can't send response to %s, %s", r.RemoteAddr, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package proxy
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha1" // nolint
|
||||
"crypto/sha1" //nolint:gosec // not used for cryptography
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -100,40 +100,25 @@ func (p Image) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
imgURL := string(src)
|
||||
var imgReader io.ReadCloser
|
||||
var img []byte
|
||||
imgID, err := cachedImgID(imgURL)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't parse image url "+imgURL, rest.ErrAssetNotFound)
|
||||
return
|
||||
}
|
||||
if p.CacheExternal {
|
||||
imgReader, _, err = p.ImageService.Load(imgID)
|
||||
if err != nil {
|
||||
imgReader = nil
|
||||
}
|
||||
img, _ = p.ImageService.Load(imgID)
|
||||
}
|
||||
if imgReader == nil {
|
||||
imgReader, err = p.downloadImage(context.Background(), imgURL)
|
||||
if img == nil {
|
||||
img, err = p.downloadImage(context.Background(), imgURL)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusNotFound, err, "can't get image "+imgURL, rest.ErrAssetNotFound)
|
||||
return
|
||||
}
|
||||
if p.CacheExternal {
|
||||
var buf bytes.Buffer
|
||||
// We need to duplicate data into a new buffer because `cacheImage` would read provider Reader
|
||||
// and we would need another one to read data for response
|
||||
p.cacheImage(io.TeeReader(imgReader, &buf), imgID)
|
||||
if err := imgReader.Close(); err != nil {
|
||||
log.Printf("[WARN] can't close image reader, %s", err)
|
||||
}
|
||||
imgReader = ioutil.NopCloser(&buf)
|
||||
p.cacheImage(bytes.NewReader(img), imgID)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
if e := imgReader.Close(); e != nil {
|
||||
log.Printf("[WARN] can't close image reader, %s", e)
|
||||
}
|
||||
}()
|
||||
|
||||
// enforce client-side caching
|
||||
etag := `"` + r.URL.Query().Get("src") + `"`
|
||||
@@ -147,7 +132,7 @@ func (p Image) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "image/*")
|
||||
_, err = io.Copy(w, imgReader)
|
||||
_, err = io.Copy(w, bytes.NewReader(img))
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't copy image stream, %s", err)
|
||||
}
|
||||
@@ -162,8 +147,8 @@ func (p Image) cacheImage(r io.Reader, imgID string) {
|
||||
p.ImageService.Submit(func() []string { return []string{id} })
|
||||
}
|
||||
|
||||
// download an image. Returns a Reader which has to be closed by a caller
|
||||
func (p Image) downloadImage(ctx context.Context, imgURL string) (io.ReadCloser, error) {
|
||||
// download an image.
|
||||
func (p Image) downloadImage(ctx context.Context, imgURL string) ([]byte, error) {
|
||||
log.Printf("[DEBUG] downloading image %s", imgURL)
|
||||
|
||||
timeout := 60 * time.Second // default
|
||||
@@ -197,11 +182,11 @@ func (p Image) downloadImage(ctx context.Context, imgURL string) (io.ReadCloser,
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("unable to read image body")
|
||||
}
|
||||
return ioutil.NopCloser(bytes.NewBuffer(imgData)), nil
|
||||
return imgData, nil
|
||||
}
|
||||
|
||||
func sha1Str(s string) string {
|
||||
return fmt.Sprintf("%x", sha1.Sum([]byte(s))) // nolint
|
||||
return fmt.Sprintf("%x", sha1.Sum([]byte(s))) //nolint:gosec // not used for cryptography
|
||||
}
|
||||
|
||||
// generates ID for a cached image.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -16,6 +15,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
)
|
||||
|
||||
@@ -112,9 +112,9 @@ func TestImage_RoutesCachingImage(t *testing.T) {
|
||||
imgURL := httpSrv.URL + "/image/img1.png"
|
||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(imgURL))
|
||||
|
||||
imageStore.On("Load", mock.Anything).Once().Return(nil, int64(0), nil)
|
||||
imageStore.On("Load", mock.Anything).Once().Return(nil, nil)
|
||||
imageStore.On("SaveWithID", mock.Anything, mock.Anything).Once().Run(func(args mock.Arguments) { _, _ = ioutil.ReadAll(args.Get(1).(io.Reader)) }).Return("", nil)
|
||||
imageStore.On("commit", mock.Anything).Once().Return(nil)
|
||||
imageStore.On("Commit", mock.Anything).Once().Return(nil)
|
||||
|
||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.Nil(t, err)
|
||||
@@ -124,7 +124,7 @@ func TestImage_RoutesCachingImage(t *testing.T) {
|
||||
|
||||
imageStore.AssertCalled(t, "Load", mock.Anything)
|
||||
imageStore.AssertCalled(t, "SaveWithID", "cached_images/4b84b15bff6ee5796152495a230e45e3d7e947d9-"+sha1Str(imgURL), mock.Anything)
|
||||
imageStore.AssertCalled(t, "commit", mock.Anything)
|
||||
imageStore.AssertCalled(t, "Commit", mock.Anything)
|
||||
}
|
||||
|
||||
func TestImage_RoutesUsingCachedImage(t *testing.T) {
|
||||
@@ -144,8 +144,8 @@ func TestImage_RoutesUsingCachedImage(t *testing.T) {
|
||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
|
||||
|
||||
// In order to validate that cached data used cache "will return" some other data from what http server would
|
||||
imageReader := ioutil.NopCloser(bytes.NewReader([]byte(fmt.Sprintf("%256s", "X"))))
|
||||
imageStore.On("Load", mock.Anything).Once().Return(imageReader, int64(256), nil)
|
||||
testImage := []byte(fmt.Sprintf("%256s", "X"))
|
||||
imageStore.On("Load", mock.Anything).Once().Return(testImage, nil)
|
||||
|
||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.Nil(t, err)
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
store "github.com/umputun/remark/backend/app/store"
|
||||
)
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import store "github.com/umputun/remark/backend/app/store"
|
||||
|
||||
// MockInterface is an autogenerated mock type for the Interface type
|
||||
type MockInterface struct {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
@@ -93,8 +92,8 @@ func (b *Bolt) Save(_ string, userID string, r io.Reader) (id string, err error)
|
||||
}
|
||||
|
||||
// Commit file stored in staging bucket by copying it to permanent bucket
|
||||
// Data from staging bucket not removed immediately, but would be removed on cleanup
|
||||
func (b *Bolt) commit(id string) error {
|
||||
// Data from staging bucket not removed immediately, but would be removed on Cleanup
|
||||
func (b *Bolt) Commit(id string) error {
|
||||
err := b.db.Update(func(tx *bolt.Tx) error {
|
||||
data := tx.Bucket([]byte(imagesStagedBktName)).Get([]byte(id))
|
||||
if data == nil {
|
||||
@@ -107,27 +106,27 @@ func (b *Bolt) commit(id string) error {
|
||||
}
|
||||
|
||||
// Load image from DB
|
||||
// returns ReadCloser and caller should call close after processing completed.
|
||||
func (b *Bolt) Load(id string) (io.ReadCloser, int64, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
var size int = 0
|
||||
func (b *Bolt) Load(id string) ([]byte, error) {
|
||||
var data []byte
|
||||
err := b.db.View(func(tx *bolt.Tx) error {
|
||||
data := tx.Bucket([]byte(imagesBktName)).Get([]byte(id))
|
||||
data = tx.Bucket([]byte(imagesBktName)).Get([]byte(id))
|
||||
if data == nil {
|
||||
data = tx.Bucket([]byte(imagesStagedBktName)).Get([]byte(id))
|
||||
}
|
||||
if data == nil {
|
||||
return errors.Errorf("can't load image %s", id)
|
||||
}
|
||||
var err error
|
||||
size, err = buf.Write(data)
|
||||
return errors.Wrapf(err, "failed to write for %s", id)
|
||||
return nil
|
||||
})
|
||||
return ioutil.NopCloser(buf), int64(size), err
|
||||
if err != nil {
|
||||
// separate error handler to return nil and not empty []byte
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Cleanup runs scan of staging and removes old data based on ttl
|
||||
func (b *Bolt) cleanup(_ context.Context, ttl time.Duration) error {
|
||||
func (b *Bolt) Cleanup(_ context.Context, ttl time.Duration) error {
|
||||
err := b.db.Update(func(tx *bolt.Tx) error {
|
||||
c := tx.Bucket([]byte(insertTimeBktName)).Cursor()
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestBoltStore_SaveCommit(t *testing.T) {
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = svc.commit(id)
|
||||
err = svc.Commit(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = svc.db.View(func(tx *bolt.Tx) error {
|
||||
@@ -53,16 +53,11 @@ func TestBoltStore_LoadAfterSave(t *testing.T) {
|
||||
assert.Contains(t, id, "user1")
|
||||
t.Log(id)
|
||||
|
||||
r, sz, err := svc.Load(id)
|
||||
assert.NoError(t, err)
|
||||
defer func() { assert.NoError(t, r.Close()) }()
|
||||
data, err := ioutil.ReadAll(r)
|
||||
|
||||
data, err := svc.Load(id)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1462, len(data))
|
||||
assert.Equal(t, int64(1462), sz)
|
||||
|
||||
_, _, err = svc.Load("abcd")
|
||||
_, err = svc.Load("abcd")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -90,7 +85,7 @@ func TestBoltStore_Cleanup(t *testing.T) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
img3 := save("blah_ff3.png", "user2")
|
||||
|
||||
err := svc.cleanup(context.Background(), time.Since(img1ts)) // clean first images
|
||||
err := svc.Cleanup(context.Background(), time.Since(img1ts)) // clean first images
|
||||
assert.NoError(t, err)
|
||||
|
||||
assertBoltImgNil(t, svc.db, imagesStagedBktName, img1)
|
||||
@@ -98,10 +93,10 @@ func TestBoltStore_Cleanup(t *testing.T) {
|
||||
assertBoltImgNotNil(t, svc.db, imagesStagedBktName, img2)
|
||||
assertBoltImgNotNil(t, svc.db, imagesStagedBktName, img3)
|
||||
|
||||
err = svc.commit(img3)
|
||||
err = svc.Commit(img3)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = svc.cleanup(context.Background(), time.Millisecond*10)
|
||||
err = svc.Cleanup(context.Background(), time.Millisecond*10)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assertBoltImgNil(t, svc.db, imagesStagedBktName, img2)
|
||||
|
||||
@@ -70,8 +70,8 @@ func (f *FileSystem) Save(fileName string, userID string, r io.Reader) (id strin
|
||||
}
|
||||
|
||||
// Commit file stored in staging location by moving it to permanent location
|
||||
func (f *FileSystem) commit(id string) error {
|
||||
log.Printf("[DEBUG] commit image %s", id)
|
||||
func (f *FileSystem) Commit(id string) error {
|
||||
log.Printf("[DEBUG] Commit image %s", id)
|
||||
stagingImage, permImage := f.location(f.Staging, id), f.location(f.Location, id)
|
||||
|
||||
if err := os.MkdirAll(path.Dir(permImage), 0700); err != nil {
|
||||
@@ -83,34 +83,33 @@ func (f *FileSystem) commit(id string) error {
|
||||
}
|
||||
|
||||
// Load image from FS. Uses id to get partition subdirectory.
|
||||
// returns ReadCloser and caller should call close after processing completed.
|
||||
func (f *FileSystem) Load(id string) (io.ReadCloser, int64, error) {
|
||||
func (f *FileSystem) Load(id string) ([]byte, error) {
|
||||
|
||||
// get image file by id. first try permanent location and if not found - staging
|
||||
img := func(id string) (file string, st os.FileInfo, err error) {
|
||||
img := func(id string) (file string, err error) {
|
||||
file = f.location(f.Location, id)
|
||||
st, err = os.Stat(file)
|
||||
_, err = os.Stat(file)
|
||||
if err != nil {
|
||||
file = f.location(f.Staging, id)
|
||||
st, err = os.Stat(file)
|
||||
_, err = os.Stat(file)
|
||||
}
|
||||
return file, st, errors.Wrapf(err, "can't get image stats for %s", id)
|
||||
return file, errors.Wrapf(err, "can't get image stats for %s", id)
|
||||
}
|
||||
|
||||
imgFile, st, err := img(id)
|
||||
imgFile, err := img(id)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrapf(err, "can't get image file for %s", id)
|
||||
return nil, errors.Wrapf(err, "can't get image file for %s", id)
|
||||
}
|
||||
|
||||
fh, err := os.Open(imgFile) // nolint
|
||||
fh, err := os.Open(imgFile) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrapf(err, "can't load image %s", id)
|
||||
return nil, errors.Wrapf(err, "can't load image %s", id)
|
||||
}
|
||||
return fh, st.Size(), nil
|
||||
return ioutil.ReadAll(fh)
|
||||
}
|
||||
|
||||
// Cleanup runs scan of staging and removes old files based on ttl
|
||||
func (f *FileSystem) cleanup(_ context.Context, ttl time.Duration) error {
|
||||
func (f *FileSystem) Cleanup(_ context.Context, ttl time.Duration) error {
|
||||
|
||||
if _, err := os.Stat(f.Staging); os.IsNotExist(err) {
|
||||
return nil
|
||||
|
||||
@@ -129,7 +129,7 @@ func TestFsStore_SaveAndCommit(t *testing.T) {
|
||||
|
||||
id, err := svc.Save("file1.png", "user1", gopherPNG())
|
||||
require.NoError(t, err)
|
||||
err = svc.commit(id)
|
||||
err = svc.Commit(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
imgStaging := svc.location(svc.Staging, id)
|
||||
@@ -161,14 +161,10 @@ func TestFsStore_LoadAfterSave(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
t.Log(id)
|
||||
|
||||
r, sz, err := svc.Load(id)
|
||||
assert.NoError(t, err)
|
||||
defer func() { assert.NoError(t, r.Close()) }()
|
||||
data, err := ioutil.ReadAll(r)
|
||||
data, err := svc.Load(id)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1462, len(data))
|
||||
assert.Equal(t, int64(1462), sz)
|
||||
_, _, err = svc.Load("abcd")
|
||||
_, err = svc.Load("abcd")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -180,17 +176,13 @@ func TestFsStore_LoadAfterCommit(t *testing.T) {
|
||||
id, err := svc.Save("blah_ff1.png", "user1", gopherPNG())
|
||||
assert.NoError(t, err)
|
||||
t.Log(id)
|
||||
err = svc.commit(id)
|
||||
err = svc.Commit(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
r, sz, err := svc.Load(id)
|
||||
assert.NoError(t, err)
|
||||
defer func() { assert.NoError(t, r.Close()) }()
|
||||
data, err := ioutil.ReadAll(r)
|
||||
data, err := svc.Load(id)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1462, len(data))
|
||||
assert.Equal(t, int64(1462), sz)
|
||||
_, _, err = svc.Load("abcd")
|
||||
_, err = svc.Load("abcd")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -260,13 +252,13 @@ func TestFsStore_Cleanup(t *testing.T) {
|
||||
img3 := save("blah_ff3.png", "user2")
|
||||
|
||||
time.Sleep(200 * time.Millisecond) // make first image expired
|
||||
err := svc.cleanup(context.Background(), time.Millisecond*300)
|
||||
err := svc.Cleanup(context.Background(), time.Millisecond*300)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(img1)
|
||||
assert.Error(t, err, "no file on staging anymore")
|
||||
// sometimes two images for user1 are put into same directory, which means that
|
||||
// after first image cleanup it's not empty and won't be deleted
|
||||
// after first image Cleanup it's not empty and won't be deleted
|
||||
_, err = os.Stat(path.Dir(img1))
|
||||
if path.Dir(img1) != path.Dir(img2) {
|
||||
assert.Error(t, err, "no dir %s on staging anymore", path.Dir(img1))
|
||||
@@ -280,7 +272,7 @@ func TestFsStore_Cleanup(t *testing.T) {
|
||||
assert.NoError(t, err, "file on staging")
|
||||
|
||||
time.Sleep(200 * time.Millisecond) // make all images expired
|
||||
err = svc.cleanup(context.Background(), time.Millisecond*300)
|
||||
err = svc.Cleanup(context.Background(), time.Millisecond*300)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(img2)
|
||||
|
||||
@@ -41,16 +41,19 @@ type Service struct {
|
||||
term int32 // term value used atomically to detect emergency termination
|
||||
}
|
||||
|
||||
// To regenerate mock run from this directory:
|
||||
// sh -c "mockery -inpkg -name Store -print > /tmp/image-mock.tmp && mv /tmp/image-mock.tmp image_mock.go"
|
||||
|
||||
// Store defines interface for saving and loading pictures.
|
||||
// Declares two-stage save with commit. Save stores to staging area and Commit moves to the final location
|
||||
// Declares two-stage save with Commit. Save stores to staging area and Commit moves to the final location
|
||||
type Store interface {
|
||||
Save(fileName string, userID string, r io.Reader) (id string, err error) // get name and reader and returns ID of stored (staging) image
|
||||
SaveWithID(id string, r io.Reader) (string, error) // store image for passed id to staging
|
||||
Load(id string) (io.ReadCloser, int64, error) // load image by ID. Caller has to close the reader.
|
||||
Load(id string) ([]byte, error) // load image by ID. Caller has to close the reader.
|
||||
SizeLimit() int // max image size
|
||||
|
||||
commit(id string) error // move image from staging to permanent
|
||||
cleanup(ctx context.Context, ttl time.Duration) error // run removal loop for old images on staging
|
||||
Commit(id string) error // move image from staging to permanent
|
||||
Cleanup(ctx context.Context, ttl time.Duration) error // run removal loop for old images on staging
|
||||
}
|
||||
|
||||
const submitQueueSize = 5000
|
||||
@@ -78,7 +81,7 @@ func (s *Service) Submit(idsFn func() []string) {
|
||||
time.Sleep(time.Millisecond * 10) // small sleep to relive busy wait but keep reactive for term (close)
|
||||
}
|
||||
for _, id := range req.idsFn() {
|
||||
if err := s.commit(id); err != nil {
|
||||
if err := s.Commit(id); err != nil {
|
||||
log.Printf("[WARN] failed to commit image %s", id)
|
||||
}
|
||||
}
|
||||
@@ -124,7 +127,7 @@ func (s *Service) Cleanup(ctx context.Context) {
|
||||
log.Printf("[INFO] cleanup terminated, %v", ctx.Err())
|
||||
return
|
||||
case <-time.After(s.TTL / 2): // cleanup call on every 1/2 TTL
|
||||
if err := s.Store.cleanup(ctx, s.TTL); err != nil {
|
||||
if err := s.Store.Cleanup(ctx, s.TTL); err != nil {
|
||||
log.Printf("[WARN] failed to cleanup, %v", err)
|
||||
}
|
||||
}
|
||||
@@ -136,7 +139,7 @@ func (s *Service) Close() {
|
||||
log.Printf("[INFO] close image service ")
|
||||
atomic.StoreInt32(&s.term, 1) // enforce non-delayed commits for all ids left in submitCh
|
||||
for {
|
||||
// set to 0 by commit goroutine after everything waited on TTL sent
|
||||
// set to 0 by Commit goroutine after everything waited on TTL sent
|
||||
if atomic.LoadInt32(&s.term) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2,48 +2,65 @@
|
||||
|
||||
package image
|
||||
|
||||
import (
|
||||
context "context"
|
||||
io "io"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
time "time"
|
||||
)
|
||||
import context "context"
|
||||
import io "io"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import time "time"
|
||||
|
||||
// MockStore is an autogenerated mock type for the Store type
|
||||
type MockStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Load provides a mock function with given fields: id
|
||||
func (_m *MockStore) Load(id string) (io.ReadCloser, int64, error) {
|
||||
// Cleanup provides a mock function with given fields: ctx, ttl
|
||||
func (_m *MockStore) Cleanup(ctx context.Context, ttl time.Duration) error {
|
||||
ret := _m.Called(ctx, ttl)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, time.Duration) error); ok {
|
||||
r0 = rf(ctx, ttl)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Commit provides a mock function with given fields: id
|
||||
func (_m *MockStore) Commit(id string) error {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 io.ReadCloser
|
||||
if rf, ok := ret.Get(0).(func(string) io.ReadCloser); ok {
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Load provides a mock function with given fields: id
|
||||
func (_m *MockStore) Load(id string) ([]byte, error) {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 []byte
|
||||
if rf, ok := ret.Get(0).(func(string) []byte); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(io.ReadCloser)
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 int64
|
||||
if rf, ok := ret.Get(1).(func(string) int64); ok {
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(id)
|
||||
} else {
|
||||
r1 = ret.Get(1).(int64)
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(2).(func(string) error); ok {
|
||||
r2 = rf(id)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Save provides a mock function with given fields: fileName, userID, r
|
||||
@@ -101,31 +118,3 @@ func (_m *MockStore) SizeLimit() int {
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// cleanup provides a mock function with given fields: ctx, ttl
|
||||
func (_m *MockStore) cleanup(ctx context.Context, ttl time.Duration) error {
|
||||
ret := _m.Called(ctx, ttl)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, time.Duration) error); ok {
|
||||
r0 = rf(ctx, ttl)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// commit provides a mock function with given fields: id
|
||||
func (_m *MockStore) commit(id string) error {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
@@ -37,49 +37,49 @@ func TestService_ExtractPictures2(t *testing.T) {
|
||||
|
||||
func TestService_Cleanup(t *testing.T) {
|
||||
store := MockStore{}
|
||||
store.On("cleanup", mock.Anything, mock.Anything).Times(10).Return(nil)
|
||||
store.On("Cleanup", mock.Anything, mock.Anything).Times(10).Return(nil)
|
||||
|
||||
svc := Service{Store: &store, TTL: 100 * time.Millisecond}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*549)
|
||||
defer cancel()
|
||||
svc.Cleanup(ctx)
|
||||
store.AssertNumberOfCalls(t, "cleanup", 10)
|
||||
store.AssertNumberOfCalls(t, "Cleanup", 10)
|
||||
}
|
||||
|
||||
func TestService_Submit(t *testing.T) {
|
||||
store := MockStore{}
|
||||
store.On("commit", mock.Anything, mock.Anything).Times(5).Return(nil)
|
||||
store.On("Commit", mock.Anything, mock.Anything).Times(5).Return(nil)
|
||||
svc := Service{Store: &store, ImageAPI: "/blah/", TTL: time.Millisecond * 100}
|
||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||
svc.Submit(func() []string { return []string{"id4", "id5"} })
|
||||
svc.Submit(nil)
|
||||
store.AssertNumberOfCalls(t, "commit", 0)
|
||||
store.AssertNumberOfCalls(t, "Commit", 0)
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
store.AssertNumberOfCalls(t, "commit", 5)
|
||||
store.AssertNumberOfCalls(t, "Commit", 5)
|
||||
}
|
||||
|
||||
func TestService_Close(t *testing.T) {
|
||||
store := MockStore{}
|
||||
store.On("commit", mock.Anything, mock.Anything).Times(5).Return(nil)
|
||||
store.On("Commit", mock.Anything, mock.Anything).Times(5).Return(nil)
|
||||
svc := Service{Store: &store, ImageAPI: "/blah/", TTL: time.Millisecond * 500}
|
||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||
svc.Submit(func() []string { return []string{"id4", "id5"} })
|
||||
svc.Submit(nil)
|
||||
svc.Close()
|
||||
store.AssertNumberOfCalls(t, "commit", 5)
|
||||
store.AssertNumberOfCalls(t, "Commit", 5)
|
||||
}
|
||||
|
||||
func TestService_SubmitDelay(t *testing.T) {
|
||||
store := MockStore{}
|
||||
store.On("commit", mock.Anything, mock.Anything).Times(5).Return(nil)
|
||||
store.On("Commit", mock.Anything, mock.Anything).Times(5).Return(nil)
|
||||
svc := Service{Store: &store, ImageAPI: "/blah/", TTL: time.Millisecond * 100}
|
||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||
time.Sleep(150 * time.Millisecond) // let first batch to pass TTL
|
||||
svc.Submit(func() []string { return []string{"id4", "id5"} })
|
||||
svc.Submit(nil)
|
||||
store.AssertNumberOfCalls(t, "commit", 3)
|
||||
store.AssertNumberOfCalls(t, "Commit", 3)
|
||||
svc.Close()
|
||||
store.AssertNumberOfCalls(t, "commit", 5)
|
||||
store.AssertNumberOfCalls(t, "Commit", 5)
|
||||
}
|
||||
|
||||
func TestService_resize(t *testing.T) {
|
||||
|
||||
@@ -1277,7 +1277,7 @@ func TestService_submitImages(t *testing.T) {
|
||||
lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc)
|
||||
|
||||
mockStore := image.MockStore{}
|
||||
mockStore.On("commit", mock.Anything, mock.Anything).Times(2).Return(nil)
|
||||
mockStore.On("Commit", mock.Anything, mock.Anything).Times(2).Return(nil)
|
||||
imgSvc := &image.Service{Store: &mockStore, TTL: time.Millisecond * 50}
|
||||
|
||||
// two comments for https://radio-t.com
|
||||
|
||||
Reference in New Issue
Block a user