From 68235164ff8282d55679b9397c632d2e6653b9e2 Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 26 Aug 2019 20:53:04 -0500 Subject: [PATCH 1/3] add OnEvent event to admin on each update/create/vote/delete --- .../_example/memory_store/accessor/admin.go | 23 +++++++++++++++---- .../memory_store/accessor/admin_test.go | 6 ++--- backend/_example/memory_store/server/rpc.go | 23 +++++++++++++++++++ .../_example/memory_store/server/rpc_test.go | 14 +++++++++++ backend/app/store/admin/admin.go | 15 ++++++++++++ backend/app/store/admin/remote.go | 9 ++++++++ backend/app/store/admin/remote_test.go | 12 ++++++++++ backend/app/store/service/service.go | 15 ++++++++++++ 8 files changed, 110 insertions(+), 7 deletions(-) diff --git a/backend/_example/memory_store/accessor/admin.go b/backend/_example/memory_store/accessor/admin.go index 70581bfa..59a5f0d2 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" ) // MemAdmin implements admin.Store with memory backend @@ -19,10 +20,11 @@ type MemAdmin struct { // AdminRec is a records per site with all admin info in type AdminRec struct { - SiteID string - IDs []string // admin ids - Email string // admin email - Enabled bool // site enabled + SiteID string + IDs []string // admin ids + Email string // admin email + Enabled bool // site enabled + CountCreated int64 // number of created posts } // NewMemAdminStore makes admin Store in memory @@ -65,6 +67,19 @@ func (m *MemAdmin) Enabled(siteID string) (ok bool, err error) { return resp.Enabled, nil } +// OnEvent reacts on events from updates, created, delete and vote +func (m *MemAdmin) OnEvent(siteID string, ev admin.EventType) error { + resp, ok := m.data[siteID] + if !ok { + return errors.Errorf("site %s not found", siteID) + } + if ev == admin.EvCreate { + resp.CountCreated += 1 // not a good idea, just for demo + m.data[siteID] = resp + } + return nil +} + // Set admin data for siteID func (m *MemAdmin) Set(siteID string, arec AdminRec) { m.data[siteID] = arec diff --git a/backend/_example/memory_store/accessor/admin_test.go b/backend/_example/memory_store/accessor/admin_test.go index e05bc956..f5eb3261 100644 --- a/backend/_example/memory_store/accessor/admin_test.go +++ b/backend/_example/memory_store/accessor/admin_test.go @@ -20,9 +20,9 @@ func TestMemAdmin_Get(t *testing.T) { var ms admin.Store = adm adm.data = map[string]AdminRec{ - "site1": {"site1", []string{"i11", "i12"}, "e1", true}, - "site2": {"site2", []string{"i21", "i22"}, "e2", true}, - "site3": {"site3", []string{"i21", "i22"}, "e3", false}, + "site1": {"site1", []string{"i11", "i12"}, "e1", true, 0}, + "site2": {"site2", []string{"i21", "i22"}, "e2", true, 0}, + "site3": {"site3", []string{"i21", "i22"}, "e3", false, 0}, } admins, err := ms.Admins("site1") diff --git a/backend/_example/memory_store/server/rpc.go b/backend/_example/memory_store/server/rpc.go index 20cf03a6..0253ba64 100644 --- a/backend/_example/memory_store/server/rpc.go +++ b/backend/_example/memory_store/server/rpc.go @@ -46,6 +46,7 @@ func (s *RPC) addHandlers() { "admins": s.admAdminsHndl, "email": s.admEmailHndl, "enabled": s.admEnabledHndl, + "event": s.admEventHndl, }) } @@ -196,3 +197,25 @@ func (s *RPC) admEnabledHndl(id uint64, params json.RawMessage) (rr jrpc.Respons } 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 f0c5194a..33b4ee91 100644 --- a/backend/_example/memory_store/server/rpc_test.go +++ b/backend/_example/memory_store/server/rpc_test.go @@ -311,6 +311,20 @@ func TestRPC_admEnabledHndl(t *testing.T) { 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 prepTestStore(t *testing.T) (s *RPC, port int, teardown func()) { port = 40000 + int(rand.Int31n(10000)) diff --git a/backend/app/store/admin/admin.go b/backend/app/store/admin/admin.go index c369325e..3e6052a5 100644 --- a/backend/app/store/admin/admin.go +++ b/backend/app/store/admin/admin.go @@ -14,8 +14,20 @@ type Store interface { Admins(siteID string) (ids []string, err error) Email(siteID string) (email string, err error) Enabled(siteID string) (ok bool, err error) + OnEvent(siteID string, et EventType) error } +// EventType indicates type of the event +type EventType int + +// enum of all event types +const ( + EvCreate EventType = iota + EvDelete + EvUpdate + EvVote +) + // StaticStore implements keys.Store with a single set of admins and email for all sites type StaticStore struct { admins []string @@ -65,3 +77,6 @@ func (s *StaticStore) Enabled(site string) (ok bool, err error) { } return false, nil } + +// OnEvent doesn nothing for StaticStore +func (s *StaticStore) OnEvent(siteID string, et EventType) error { return nil } diff --git a/backend/app/store/admin/remote.go b/backend/app/store/admin/remote.go index a5f4a83f..5797d18b 100644 --- a/backend/app/store/admin/remote.go +++ b/backend/app/store/admin/remote.go @@ -66,3 +66,12 @@ func (r *RPC) Enabled(siteID string) (ok bool, err error) { } return ok, nil } + +// OnEvent reacts (register) events about data modification +func (r *RPC) OnEvent(siteID string, et EventType) error { + _, err := r.Call("admin.event", siteID, et) + if err != nil { + return err + } + return nil +} diff --git a/backend/app/store/admin/remote_test.go b/backend/app/store/admin/remote_test.go index 88b7d645..f9d2722f 100644 --- a/backend/app/store/admin/remote_test.go +++ b/backend/app/store/admin/remote_test.go @@ -78,6 +78,18 @@ func TestRemote_Enables(t *testing.T) { t.Logf("%v %T", res, res) } +func TestRemote_OnEvent(t *testing.T) { + ts := testServer(t, `{"method":"admin.event","params":["site-1",2],"id":1}`, `{"id":1}`) + defer ts.Close() + c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}} + + var a Store = &c + _ = a + + err := c.OnEvent("site-1", EvUpdate) + assert.NoError(t, err) +} + func testServer(t *testing.T, req, resp string) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) diff --git a/backend/app/store/service/service.go b/backend/app/store/service/service.go index cf1b11ef..97adcb57 100644 --- a/backend/app/store/service/service.go +++ b/backend/app/store/service/service.go @@ -102,6 +102,10 @@ func (s *DataStore) Create(comment store.Comment) (commentID string, err error) }() s.submitImages(comment) + if e := s.AdminStore.OnEvent(comment.Locator.SiteID, admin.EvCreate); e != nil { + log.Printf("[WARN] failed to send create event, %s", e) + } + return s.Engine.Create(comment) } @@ -306,6 +310,10 @@ func (s *DataStore) Vote(req VoteReq) (comment store.Comment, err error) { } } + if e := s.AdminStore.OnEvent(comment.Locator.SiteID, admin.EvVote); e != nil { + log.Printf("[WARN] failed to send vote event, %s", e) + } + comment.Controversy = s.controversy(s.upsAndDowns(comment)) comment.Locator = req.Locator return comment, s.Engine.Update(comment) @@ -386,6 +394,10 @@ func (s *DataStore) EditComment(locator store.Locator, commentID string, req Edi comment.Locator = locator comment.Sanitize() + if e := s.AdminStore.OnEvent(comment.Locator.SiteID, admin.EvUpdate); e != nil { + log.Printf("[WARN] failed to send update event, %s", e) + } + err = s.Engine.Update(comment) return comment, err } @@ -608,6 +620,9 @@ func (s *DataStore) Info(locator store.Locator, readonlyAge int) (store.PostInfo // Delete comment by id func (s *DataStore) Delete(locator store.Locator, commentID string, mode store.DeleteMode) error { + if e := s.AdminStore.OnEvent(locator.SiteID, admin.EvDelete); e != nil { + log.Printf("[WARN] failed to send delete event, %s", e) + } req := engine.DeleteRequest{Locator: locator, CommentID: commentID, DeleteMode: mode} return s.Engine.Delete(req) } From 8b6452f938524c2fa14c99a6d8a4340371f0bb54 Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 26 Aug 2019 22:36:52 -0500 Subject: [PATCH 2/3] missing delete event in edit request --- backend/app/store/service/service.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/app/store/service/service.go b/backend/app/store/service/service.go index 97adcb57..0225cd8d 100644 --- a/backend/app/store/service/service.go +++ b/backend/app/store/service/service.go @@ -376,6 +376,9 @@ func (s *DataStore) EditComment(locator store.Locator, commentID string, req Edi } if req.Delete { // delete request + if e := s.AdminStore.OnEvent(comment.Locator.SiteID, admin.EvDelete); e != nil { + log.Printf("[WARN] failed to send delete event, %s", e) + } comment.Deleted = true delReq := engine.DeleteRequest{Locator: locator, CommentID: commentID, DeleteMode: store.SoftDelete} return comment, s.Engine.Delete(delReq) From 074edc25edff529f3c01559938af9060346de3d8 Mon Sep 17 00:00:00 2001 From: Umputun Date: Tue, 27 Aug 2019 03:12:11 -0500 Subject: [PATCH 3/3] fix unstable auth hook test --- backend/app/cmd/server_test.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/backend/app/cmd/server_test.go b/backend/app/cmd/server_test.go index b42e42af..5fd183e4 100644 --- a/backend/app/cmd/server_test.go +++ b/backend/app/cmd/server_test.go @@ -388,13 +388,12 @@ func TestServerAuthHooks(t *testing.T) { defer resp.Body.Close() assert.Equal(t, http.StatusCreated, resp.StatusCode, "non-blocked user able to post") - time.Sleep(1200 * time.Millisecond) // prevent limiter to be triggered // add comment with no-aud claim claimsNoAud := claims - claims.Audience = "" + claimsNoAud.Audience = "" tkNoAud, err := tkService.Token(claimsNoAud) require.NoError(t, err) - t.Log(tkNoAud) + t.Logf("no-aud claims: %s", tkNoAud) req, err = http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/api/v1/comment", port), strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/p/2018/12/29/podcast-631/", "site": "remark"}}`)) @@ -403,7 +402,9 @@ func TestServerAuthHooks(t *testing.T) { resp, err = client.Do(req) require.NoError(t, err) defer resp.Body.Close() - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "user without aud claim rejected") + body, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "user without aud claim rejected, \n"+tkNoAud+"\n"+string(body)) // block user dev as admin req, e := http.NewRequest(http.MethodPut, @@ -426,7 +427,10 @@ func TestServerAuthHooks(t *testing.T) { resp, err = client.Do(req) require.Nil(t, err) defer resp.Body.Close() - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "blocked user can't post") + body, err = ioutil.ReadAll(resp.Body) + require.NoError(t, err) + assert.True(t, resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized, + "blocked user can't post, \n"+tk+"\n"+string(body)) app.Wait() }