diff --git a/backend/_example/memory_store/accessor/admin.go b/backend/_example/memory_store/accessor/admin.go index 59a5f0d2..efc4f09d 100644 --- a/backend/_example/memory_store/accessor/admin.go +++ b/backend/_example/memory_store/accessor/admin.go @@ -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" ) diff --git a/backend/_example/memory_store/accessor/data.go b/backend/_example/memory_store/accessor/data.go index 7c045c1e..94705230 100644 --- a/backend/_example/memory_store/accessor/data.go +++ b/backend/_example/memory_store/accessor/data.go @@ -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 diff --git a/backend/_example/memory_store/go.sum b/backend/_example/memory_store/go.sum index 10a9ebac..1c2a3f90 100644 --- a/backend/_example/memory_store/go.sum +++ b/backend/_example/memory_store/go.sum @@ -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= diff --git a/backend/_example/memory_store/server/admin.go b/backend/_example/memory_store/server/admin.go new file mode 100644 index 00000000..9c1d38be --- /dev/null +++ b/backend/_example/memory_store/server/admin.go @@ -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) +} diff --git a/backend/_example/memory_store/server/admin_test.go b/backend/_example/memory_store/server/admin_test.go new file mode 100644 index 00000000..f91b3998 --- /dev/null +++ b/backend/_example/memory_store/server/admin_test.go @@ -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) +} diff --git a/backend/_example/memory_store/server/data.go b/backend/_example/memory_store/server/data.go new file mode 100644 index 00000000..9b6a9424 --- /dev/null +++ b/backend/_example/memory_store/server/data.go @@ -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{} +} diff --git a/backend/_example/memory_store/server/data_test.go b/backend/_example/memory_store/server/data_test.go new file mode 100644 index 00000000..938e73c0 --- /dev/null +++ b/backend/_example/memory_store/server/data_test.go @@ -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) +} diff --git a/backend/_example/memory_store/server/rpc.go b/backend/_example/memory_store/server/rpc.go index e5b1414d..b5ce3290 100644 --- a/backend/_example/memory_store/server/rpc.go +++ b/backend/_example/memory_store/server/rpc.go @@ -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) -} diff --git a/backend/_example/memory_store/server/rpc_test.go b/backend/_example/memory_store/server/rpc_test.go index 5142cf8c..d9d395ef 100644 --- a/backend/_example/memory_store/server/rpc_test.go +++ b/backend/_example/memory_store/server/rpc_test.go @@ -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)) diff --git a/backend/app/rest/api/rest_public.go b/backend/app/rest/api/rest_public.go index efd86781..e53f6b9a 100644 --- a/backend/app/rest/api/rest_public.go +++ b/backend/app/rest/api/rest_public.go @@ -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) } } diff --git a/backend/app/rest/proxy/image.go b/backend/app/rest/proxy/image.go index c53c4880..3a5f7484 100644 --- a/backend/app/rest/proxy/image.go +++ b/backend/app/rest/proxy/image.go @@ -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. diff --git a/backend/app/rest/proxy/image_test.go b/backend/app/rest/proxy/image_test.go index 0122bd25..8b1ce808 100644 --- a/backend/app/rest/proxy/image_test.go +++ b/backend/app/rest/proxy/image_test.go @@ -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) diff --git a/backend/app/store/engine/engine_mock.go b/backend/app/store/engine/engine_mock.go index 603373da..47a66e3b 100644 --- a/backend/app/store/engine/engine_mock.go +++ b/backend/app/store/engine/engine_mock.go @@ -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 { diff --git a/backend/app/store/image/bolt_store.go b/backend/app/store/image/bolt_store.go index e9588006..f1d13a1c 100644 --- a/backend/app/store/image/bolt_store.go +++ b/backend/app/store/image/bolt_store.go @@ -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() diff --git a/backend/app/store/image/bolt_store_test.go b/backend/app/store/image/bolt_store_test.go index d206c49a..27e01fed 100644 --- a/backend/app/store/image/bolt_store_test.go +++ b/backend/app/store/image/bolt_store_test.go @@ -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) diff --git a/backend/app/store/image/fs_store.go b/backend/app/store/image/fs_store.go index ab9b4a05..80377d6b 100644 --- a/backend/app/store/image/fs_store.go +++ b/backend/app/store/image/fs_store.go @@ -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 diff --git a/backend/app/store/image/fs_store_test.go b/backend/app/store/image/fs_store_test.go index 08926301..d2077d81 100644 --- a/backend/app/store/image/fs_store_test.go +++ b/backend/app/store/image/fs_store_test.go @@ -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) diff --git a/backend/app/store/image/image.go b/backend/app/store/image/image.go index a229cf86..7a55e5f6 100644 --- a/backend/app/store/image/image.go +++ b/backend/app/store/image/image.go @@ -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 } diff --git a/backend/app/store/image/image_mock.go b/backend/app/store/image/image_mock.go index 3ab8c2b5..8c23293c 100644 --- a/backend/app/store/image/image_mock.go +++ b/backend/app/store/image/image_mock.go @@ -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 -} diff --git a/backend/app/store/image/image_test.go b/backend/app/store/image/image_test.go index 5a7a7c4d..5d55789c 100644 --- a/backend/app/store/image/image_test.go +++ b/backend/app/store/image/image_test.go @@ -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) { diff --git a/backend/app/store/service/service_test.go b/backend/app/store/service/service_test.go index d4ba3700..bf15b6dc 100644 --- a/backend/app/store/service/service_test.go +++ b/backend/app/store/service/service_test.go @@ -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