diff --git a/backend/app/store/engine/remote.go b/backend/app/store/engine/remote.go new file mode 100644 index 00000000..997b9492 --- /dev/null +++ b/backend/app/store/engine/remote.go @@ -0,0 +1,104 @@ +package engine + +import ( + "encoding/json" + + "github.com/umputun/remark/backend/app/store" + "github.com/umputun/remark/backend/app/store/remote" +) + +// Remote implements remote engine and delegates all Calls to remote http server +type Remote struct { + remote.Client +} + +// Create comment and return ID +func (r *Remote) Create(comment store.Comment) (commentID string, err error) { + + resp, err := r.Call("create", comment) + if err != nil { + return "", err + } + + err = json.Unmarshal(*resp.Result, &commentID) + return commentID, err +} + +// Get comment by ID +func (r *Remote) Get(locator store.Locator, commentID string) (comment store.Comment, err error) { + resp, err := r.Call("get", locator, commentID) + if err != nil { + return store.Comment{}, err + } + + err = json.Unmarshal(*resp.Result, &comment) + return comment, err +} + +// Update comment, mutable parts only +func (r *Remote) Update(locator store.Locator, comment store.Comment) error { + _, err := r.Call("update", locator, comment) + return err +} + +// Find comments for locator +func (r *Remote) Find(req FindRequest) (comments []store.Comment, err error) { + resp, err := r.Call("find", req) + if err != nil { + return nil, err + } + err = json.Unmarshal(*resp.Result, &comments) + return comments, err +} + +// Info returns post(s) meta info +func (r *Remote) Info(req InfoRequest) (info []store.PostInfo, err error) { + resp, err := r.Call("info", req) + if err != nil { + return nil, err + } + err = json.Unmarshal(*resp.Result, &info) + return info, err +} + +// Flag sets and gets flags +func (r *Remote) Flag(req FlagRequest) (status bool, err error) { + resp, err := r.Call("flag", req) + if err != nil { + return false, err + } + err = json.Unmarshal(*resp.Result, &status) + return status, err +} + +// ListFlags get list of flagged keys, like blocked & verified user +func (r *Remote) ListFlags(siteID string, flag Flag) (list []interface{}, err error) { + resp, err := r.Call("list_flags", siteID, flag) + if err != nil { + return nil, err + } + err = json.Unmarshal(*resp.Result, &list) + return list, err +} + +// Count gets comments count by user or site +func (r *Remote) Count(req FindRequest) (count int, err error) { + resp, err := r.Call("count", req) + if err != nil { + return 0, err + } + err = json.Unmarshal(*resp.Result, &count) + return count, err +} + +// Delete post(s) by id or by userID +func (r *Remote) Delete(req DeleteRequest) error { + _, err := r.Call("delete", req) + return err +} + +// Close storage engine +func (r *Remote) Close() error { + _, err := r.Call("close") + return err +} diff --git a/backend/app/store/engine/remote_test.go b/backend/app/store/engine/remote_test.go new file mode 100644 index 00000000..91e013f5 --- /dev/null +++ b/backend/app/store/engine/remote_test.go @@ -0,0 +1,171 @@ +package engine + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/umputun/remark/backend/app/store" + "github.com/umputun/remark/backend/app/store/remote" +) + +func TestClient_Create(t *testing.T) { + ts := testServer(t, `{"method":"create","params":[{"id":"123","pid":"","text":"msg","user":{"name":"","id":"","picture":"","admin":false},"locator":{"site":"site","url":"http://example.com/url"},"score":0,"vote":0,"time":"0001-01-01T00:00:00Z"}]}`, `{"result":"12345"}`) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + + res, err := c.Create(store.Comment{ID: "123", Locator: store.Locator{URL: "http://example.com/url", SiteID: "site"}, + Text: "msg"}) + assert.NoError(t, err) + assert.Equal(t, "12345", res) + t.Logf("%v %T", res, res) +} + +func TestClient_Get(t *testing.T) { + ts := testServer(t, `{"method":"get","params":[{"url":"http://example.com/url"},"site"]}`, + `{"result":{"id":"123","pid":"","text":"msg","delete":true}}`) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + + res, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site") + assert.NoError(t, err) + assert.Equal(t, store.Comment{ID: "123", Text: "msg", Deleted: true}, res) + t.Logf("%v %T", res, res) +} + +func TestClient_GetWithErrorResult(t *testing.T) { + ts := testServer(t, `{"method":"get","params":[{"url":"http://example.com/url"},"site"]}`, `{"error":"failed"}`) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + + _, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site") + assert.EqualError(t, err, "failed") +} + +func TestClient_GetWithErrorDecode(t *testing.T) { + ts := testServer(t, `{"method":"get","params":[{"url":"http://example.com/url"},"site"]}`, ``) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + + _, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site") + assert.EqualError(t, err, "failed to decode response for get: EOF") +} + +func TestClient_GetWithErrorRemote(t *testing.T) { + c := Remote{Client: remote.Client{API: "http://127.0.0.2", Client: http.Client{Timeout: 10 * time.Millisecond}}} + + _, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site") + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), "remote Call failed for get:")) +} + +func TestClient_FailedStatus(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + t.Logf("req: %s", string(body)) + w.WriteHeader(400) + })) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + + _, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site") + assert.EqualError(t, err, "bad status 400 for get") +} + +func TestClient_Update(t *testing.T) { + ts := testServer(t, `{"method":"update","params":[{"url":"http://example.com/url"},{"id":"123","pid":"","text":"msg","user":{"name":"","id":"","picture":"","admin":false},"locator":{"site":"site123","url":"http://example.com/url"},"score":0,"vote":0,"time":"0001-01-01T00:00:00Z"}]}`, `{}`) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + + err := c.Update(store.Locator{URL: "http://example.com/url"}, store.Comment{ID: "123", + Locator: store.Locator{URL: "http://example.com/url", SiteID: "site123"}, Text: "msg"}) + assert.NoError(t, err) + +} + +func TestClient_Find(t *testing.T) { + ts := testServer(t, `{"method":"find","params":[{"locator":{"url":"http://example.com/url"},"sort":"-time","since":"0001-01-01T00:00:00Z","limit":10}]}`, `{"result":[{"text":"1"},{"text":"2"}]}`) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + + res, err := c.Find(FindRequest{Locator: store.Locator{URL: "http://example.com/url"}, Sort: "-time", Limit: 10}) + assert.NoError(t, err) + assert.Equal(t, []store.Comment{{Text: "1"}, {Text: "2"}}, res) +} + +func TestClient_Info(t *testing.T) { + ts := testServer(t, `{"method":"info","params":[{"locator":{"url":"http://example.com/url"},"limit":10,"skip":5,"ro_age":10}]}`, `{"result":[{"url":"u1","count":22},{"url":"u2","count":33}]}`) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + + res, err := c.Info(InfoRequest{Locator: store.Locator{URL: "http://example.com/url"}, + Limit: 10, Skip: 5, ReadOnlyAge: 10}) + assert.NoError(t, err) + assert.Equal(t, []store.PostInfo{{URL: "u1", Count: 22}, {URL: "u2", Count: 33}}, res) +} + +func TestClient_Flag(t *testing.T) { + ts := testServer(t, `{"method":"flag","params":[{"flag":"verified","locator":{"url":"http://example.com/url"}}]}`, + `{"result":false}`) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + + res, err := c.Flag(FlagRequest{Locator: store.Locator{URL: "http://example.com/url"}, Flag: Verified}) + assert.NoError(t, err) + assert.Equal(t, false, res) +} + +func TestClient_ListFlag(t *testing.T) { + ts := testServer(t, `{"method":"list_flags","params":["site_id","blocked"]}`, `{"result":[{"ID":"id1"},{"ID":"id2"}]}`) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + res, err := c.ListFlags("site_id", Blocked) + assert.NoError(t, err) + assert.Equal(t, []interface{}{map[string]interface{}{"ID": "id1"}, map[string]interface{}{"ID": "id2"}}, res) +} + +func TestClient_Count(t *testing.T) { + ts := testServer(t, `{"method":"count","params":[{"locator":{"url":"http://example.com/url"},"since":"0001-01-01T00:00:00Z"}]}`, + `{"result":11}`) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + + res, err := c.Count(FindRequest{Locator: store.Locator{URL: "http://example.com/url"}}) + assert.NoError(t, err) + assert.Equal(t, 11, res) +} + +func TestClient_Delete(t *testing.T) { + ts := testServer(t, `{"method":"delete","params":[{"locator":{"url":"http://example.com/url"},"del_mode":0}]}`, `{}`) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + + err := c.Delete(DeleteRequest{Locator: store.Locator{URL: "http://example.com/url"}}) + assert.NoError(t, err) +} + +func TestClient_Close(t *testing.T) { + ts := testServer(t, `{"method":"close","params":null}`, `{}`) + defer ts.Close() + c := Remote{Client: remote.Client{API: ts.URL, Client: http.Client{}}} + err := c.Close() + 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) + require.NoError(t, err) + assert.Equal(t, req, string(body)) + t.Logf("req: %s", string(body)) + fmt.Fprintf(w, resp) + })) +} diff --git a/backend/app/store/remote/remote.go b/backend/app/store/remote/remote.go index a39a5249..77c68898 100644 --- a/backend/app/store/remote/remote.go +++ b/backend/app/store/remote/remote.go @@ -6,9 +6,6 @@ import ( "net/http" "github.com/pkg/errors" - - "github.com/umputun/remark/backend/app/store" - "github.com/umputun/remark/backend/app/store/engine" ) // Client implements remote engine and delegates all calls to remote http server @@ -31,98 +28,8 @@ type Response struct { Error string `json:"error,omitempty"` } -// Create comment and return ID -func (r *Client) Create(comment store.Comment) (commentID string, err error) { - - resp, err := r.call("create", comment) - if err != nil { - return "", err - } - - err = json.Unmarshal(*resp.Result, &commentID) - return commentID, err -} - -// Get comment by ID -func (r *Client) Get(locator store.Locator, commentID string) (comment store.Comment, err error) { - resp, err := r.call("get", locator, commentID) - if err != nil { - return store.Comment{}, err - } - - err = json.Unmarshal(*resp.Result, &comment) - return comment, err -} - -// Update comment, mutable parts only -func (r *Client) Update(locator store.Locator, comment store.Comment) error { - _, err := r.call("update", locator, comment) - return err -} - -// Find comments for locator -func (r *Client) Find(req engine.FindRequest) (comments []store.Comment, err error) { - resp, err := r.call("find", req) - if err != nil { - return nil, err - } - err = json.Unmarshal(*resp.Result, &comments) - return comments, err -} - -// Info returns post(s) meta info -func (r *Client) Info(req engine.InfoRequest) (info []store.PostInfo, err error) { - resp, err := r.call("info", req) - if err != nil { - return nil, err - } - err = json.Unmarshal(*resp.Result, &info) - return info, err -} - -// Flag sets and gets flags -func (r *Client) Flag(req engine.FlagRequest) (status bool, err error) { - resp, err := r.call("flag", req) - if err != nil { - return false, err - } - err = json.Unmarshal(*resp.Result, &status) - return status, err -} - -// ListFlags get list of flagged keys, like blocked & verified user -func (r *Client) ListFlags(siteID string, flag engine.Flag) (list []interface{}, err error) { - resp, err := r.call("list_flags", siteID, flag) - if err != nil { - return nil, err - } - err = json.Unmarshal(*resp.Result, &list) - return list, err -} - -// Count gets comments count by user or site -func (r *Client) Count(req engine.FindRequest) (count int, err error) { - resp, err := r.call("count", req) - if err != nil { - return 0, err - } - err = json.Unmarshal(*resp.Result, &count) - return count, err -} - -// Delete post(s) by id or by userID -func (r *Client) Delete(req engine.DeleteRequest) error { - _, err := r.call("delete", req) - return err -} - -// Close storage engine -func (r *Client) Close() error { - _, err := r.call("close") - return err -} - -func (r *Client) call(method string, args ...interface{}) (*Response, error) { +// Call remote server with given method and arguments +func (r *Client) Call(method string, args ...interface{}) (*Response, error) { b, err := json.Marshal(Request{Method: method, Params: args}) if err != nil { @@ -137,7 +44,7 @@ func (r *Client) call(method string, args ...interface{}) (*Response, error) { req.SetBasicAuth(r.AuthUser, r.AuthPasswd) resp, err := r.Client.Do(req) if err != nil { - return nil, errors.Wrapf(err, "remote call failed for %s", method) + return nil, errors.Wrapf(err, "remote Call failed for %s", method) } defer resp.Body.Close() if resp.StatusCode != 200 { diff --git a/backend/app/store/remote/remote_test.go b/backend/app/store/remote/remote_test.go index 45814c3a..11105bef 100644 --- a/backend/app/store/remote/remote_test.go +++ b/backend/app/store/remote/remote_test.go @@ -1,163 +1,52 @@ package remote import ( + "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httptest" - "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/umputun/remark/backend/app/store" - "github.com/umputun/remark/backend/app/store/engine" ) -func TestClient_Create(t *testing.T) { - ts := testServer(t, `{"method":"create","params":[{"id":"123","pid":"","text":"msg","user":{"name":"","id":"","picture":"","admin":false},"locator":{"site":"site","url":"http://example.com/url"},"score":0,"vote":0,"time":"0001-01-01T00:00:00Z"}]}`, `{"result":"12345"}`) +func TestClient_Call(t *testing.T) { + ts := testServer(t, `{"method":"test","params":[123,"abc"]}`, `{"result":"12345"}`) defer ts.Close() c := Client{API: ts.URL, Client: http.Client{}} - - res, err := c.Create(store.Comment{ID: "123", Locator: store.Locator{URL: "http://example.com/url", SiteID: "site"}, - Text: "msg"}) + resp, err := c.Call("test", 123, "abc") assert.NoError(t, err) + res := "" + err = json.Unmarshal(*resp.Result, &res) assert.Equal(t, "12345", res) t.Logf("%v %T", res, res) } -func TestClient_Get(t *testing.T) { - ts := testServer(t, `{"method":"get","params":[{"url":"http://example.com/url"},"site"]}`, - `{"result":{"id":"123","pid":"","text":"msg","delete":true}}`) +func TestClient_CallError(t *testing.T) { + ts := testServer(t, `{"method":"test","params":[123,"abc"]}`, `{"error":"some error"}`) defer ts.Close() c := Client{API: ts.URL, Client: http.Client{}} - - res, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site") - assert.NoError(t, err) - assert.Equal(t, store.Comment{ID: "123", Text: "msg", Deleted: true}, res) - t.Logf("%v %T", res, res) + _, err := c.Call("test", 123, "abc") + assert.EqualError(t, err, "some error") } -func TestClient_GetWithErrorResult(t *testing.T) { - ts := testServer(t, `{"method":"get","params":[{"url":"http://example.com/url"},"site"]}`, `{"error":"failed"}`) +func TestClient_CallBadResponse(t *testing.T) { + ts := testServer(t, `{"method":"test","params":[123,"abc"]}`, `{"result":"12345 invalid}`) defer ts.Close() c := Client{API: ts.URL, Client: http.Client{}} - - _, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site") - assert.EqualError(t, err, "failed") -} - -func TestClient_GetWithErrorDecode(t *testing.T) { - ts := testServer(t, `{"method":"get","params":[{"url":"http://example.com/url"},"site"]}`, ``) - defer ts.Close() - c := Client{API: ts.URL, Client: http.Client{}} - - _, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site") - assert.EqualError(t, err, "failed to decode response for get: EOF") -} - -func TestClient_GetWithErrorRemote(t *testing.T) { - c := Client{API: "http://127.0.0.2", Client: http.Client{Timeout: 10 * time.Millisecond}} - - _, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site") + _, err := c.Call("test", 123, "abc") assert.NotNil(t, err) - assert.True(t, strings.Contains(err.Error(), "remote call failed for get:")) } -func TestClient_FailedStatus(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := ioutil.ReadAll(r.Body) - require.NoError(t, err) - t.Logf("req: %s", string(body)) - w.WriteHeader(400) - })) +func TestClient_CallBadRemote(t *testing.T) { + ts := testServer(t, `{"method":"test","params":[123,"abc"]}`, `{"result":"12345"}`) defer ts.Close() - c := Client{API: ts.URL, Client: http.Client{}} - - _, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site") - assert.EqualError(t, err, "bad status 400 for get") -} - -func TestClient_Update(t *testing.T) { - ts := testServer(t, `{"method":"update","params":[{"url":"http://example.com/url"},{"id":"123","pid":"","text":"msg","user":{"name":"","id":"","picture":"","admin":false},"locator":{"site":"site123","url":"http://example.com/url"},"score":0,"vote":0,"time":"0001-01-01T00:00:00Z"}]}`, `{}`) - defer ts.Close() - c := Client{API: ts.URL, Client: http.Client{}} - - err := c.Update(store.Locator{URL: "http://example.com/url"}, store.Comment{ID: "123", - Locator: store.Locator{URL: "http://example.com/url", SiteID: "site123"}, Text: "msg"}) - assert.NoError(t, err) - -} - -func TestClient_Find(t *testing.T) { - ts := testServer(t, `{"method":"find","params":[{"locator":{"url":"http://example.com/url"},"sort":"-time","since":"0001-01-01T00:00:00Z","limit":10}]}`, `{"result":[{"text":"1"},{"text":"2"}]}`) - defer ts.Close() - c := Client{API: ts.URL, Client: http.Client{}} - - res, err := c.Find(engine.FindRequest{Locator: store.Locator{URL: "http://example.com/url"}, Sort: "-time", Limit: 10}) - assert.NoError(t, err) - assert.Equal(t, []store.Comment{{Text: "1"}, {Text: "2"}}, res) -} - -func TestClient_Info(t *testing.T) { - ts := testServer(t, `{"method":"info","params":[{"locator":{"url":"http://example.com/url"},"limit":10,"skip":5,"ro_age":10}]}`, `{"result":[{"url":"u1","count":22},{"url":"u2","count":33}]}`) - defer ts.Close() - c := Client{API: ts.URL, Client: http.Client{}} - - res, err := c.Info(engine.InfoRequest{Locator: store.Locator{URL: "http://example.com/url"}, - Limit: 10, Skip: 5, ReadOnlyAge: 10}) - assert.NoError(t, err) - assert.Equal(t, []store.PostInfo{{URL: "u1", Count: 22}, {URL: "u2", Count: 33}}, res) -} - -func TestClient_Flag(t *testing.T) { - ts := testServer(t, `{"method":"flag","params":[{"flag":"verified","locator":{"url":"http://example.com/url"}}]}`, - `{"result":false}`) - defer ts.Close() - c := Client{API: ts.URL, Client: http.Client{}} - - res, err := c.Flag(engine.FlagRequest{Locator: store.Locator{URL: "http://example.com/url"}, Flag: engine.Verified}) - assert.NoError(t, err) - assert.Equal(t, false, res) -} - -func TestClient_ListFlag(t *testing.T) { - ts := testServer(t, `{"method":"list_flags","params":["site_id","blocked"]}`, `{"result":[{"ID":"id1"},{"ID":"id2"}]}`) - defer ts.Close() - c := Client{API: ts.URL, Client: http.Client{}} - res, err := c.ListFlags("site_id", engine.Blocked) - assert.NoError(t, err) - assert.Equal(t, []interface{}{map[string]interface{}{"ID": "id1"}, map[string]interface{}{"ID": "id2"}}, res) -} - -func TestClient_Count(t *testing.T) { - ts := testServer(t, `{"method":"count","params":[{"locator":{"url":"http://example.com/url"},"since":"0001-01-01T00:00:00Z"}]}`, - `{"result":11}`) - defer ts.Close() - c := Client{API: ts.URL, Client: http.Client{}} - - res, err := c.Count(engine.FindRequest{Locator: store.Locator{URL: "http://example.com/url"}}) - assert.NoError(t, err) - assert.Equal(t, 11, res) -} - -func TestClient_Delete(t *testing.T) { - ts := testServer(t, `{"method":"delete","params":[{"locator":{"url":"http://example.com/url"},"del_mode":0}]}`, `{}`) - defer ts.Close() - c := Client{API: ts.URL, Client: http.Client{}} - - err := c.Delete(engine.DeleteRequest{Locator: store.Locator{URL: "http://example.com/url"}}) - assert.NoError(t, err) -} - -func TestClient_Close(t *testing.T) { - ts := testServer(t, `{"method":"close","params":null}`, `{}`) - defer ts.Close() - c := Client{API: ts.URL, Client: http.Client{}} - err := c.Close() - assert.NoError(t, err) + c := Client{API: "http://127.0.0.2", Client: http.Client{Timeout: 10 * time.Millisecond}} + _, err := c.Call("test", 123) + assert.NotNil(t, err) } func testServer(t *testing.T, req, resp string) *httptest.Server {